Flag for fixed list merge key traverse override behavior,

and fix traversing map with merge key that would override local key (completes #2110 fix)
This commit is contained in:
Steven WdV 2025-07-16 16:00:16 +02:00
parent 128bf80eed
commit a47e882c8f
No known key found for this signature in database
6 changed files with 135 additions and 27 deletions

View File

@ -96,6 +96,38 @@ y: 2
x: 1
```
## Override with local key
like https://yaml.org/type/merge.html, but with x: 1 before the merge key. This is legacy behavior, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
- &CENTER
x: 1
y: 2
- &LEFT
x: 0
y: 2
- &BIG
r: 10
- &SMALL
r: 1
- x: 1
!!merge <<:
- *BIG
- *LEFT
- *SMALL
```
then
```bash
yq '.[4] | explode(.)' sample.yml
```
will output
```yaml
x: 0
r: 10
y: 2
```
## Get anchor
Given a sample.yml file of:
```yaml

View File

@ -304,6 +304,8 @@ foo_a
```
## Traversing merge anchors with override
This is legacy behavior, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
foo: &foo
@ -399,7 +401,7 @@ foobar_thing
```
## Traversing merge anchor lists
Note that the keys earlier in the merge anchors sequence override later ones
Note that the later merge anchors override previous, but this is legacy behavior, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
@ -428,10 +430,12 @@ yq '.foobarList.thing' sample.yml
```
will output
```yaml
foo_thing
bar_thing
```
## Splatting merge anchor lists
With legacy override behavior, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
foo: &foo
@ -460,9 +464,9 @@ yq '.foobarList[]' sample.yml
will output
```yaml
bar_b
foo_thing
foobarList_c
foo_a
bar_thing
foobarList_c
```
## Select multiple indices

View File

@ -3,6 +3,7 @@ package yqlib
import (
"container/list"
"fmt"
"slices"
)
func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
@ -237,9 +238,13 @@ func applyMergeAnchor(node *CandidateNode, merge *CandidateNode, mergeIndex int,
return applyMergeAnchorMap(node, merge, mergeIndex, inline, newContent)
case SequenceNode:
log.Debugf("a merge list!")
// Earlier keys take precedence
for index := len(merge.Content) - 1; index >= 0; index = index - 1 {
childValue := merge.Content[index]
// With FixMergeAnchorToSpec, we rely on overrideEntry to reject duplicates
content := slices.All(merge.Content)
if !ConfiguredYamlPreferences.FixMergeAnchorToSpec {
// Even without FixMergeAnchorToSpec, this already gave preference to earlier keys
content = slices.Backward(merge.Content)
}
for _, childValue := range content {
childInline := inline
if childValue.Kind == AliasNode {
childInline = false
@ -303,8 +308,9 @@ func overrideEntry(node *CandidateNode, key *CandidateNode, value *CandidateNode
keyNode := newEl.Value.(*CandidateNode)
log.Debugf("checking new content %v:%v", keyNode.Value, valueEl.Value.(*CandidateNode).Value)
if keyNode.Value == key.Value && keyNode.Alias == nil && key.Alias == nil {
log.Debugf("overridign new content")
log.Debugf("overriding new content")
if !ConfiguredYamlPreferences.FixMergeAnchorToSpec {
//TODO This also fires in when an earlier element in a list merge anchor overwrites a later element, which *is* to the spec
log.Warning("--yaml-fix-merge-anchor-to-spec is false; causing the merge anchor to override the existing value at %v which isn't to the yaml spec. This flag will default to true in late 2025.", keyNode.GetNicePath())
valueEl.Value = value
}

View File

@ -122,6 +122,7 @@ var anchorOperatorScenarios = []expressionScenario{
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
},
//TODO The following 2 tests warn about overwriting [3].r not being to spec while they shouldn't
{
description: "Override",
subdescription: "see https://yaml.org/type/merge.html",
@ -129,6 +130,15 @@ var anchorOperatorScenarios = []expressionScenario{
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\ny: 2\nx: 1\n"},
},
// Correctly warns about overwriting [4].x
{
description: "Override with local key",
subdescription: "like https://yaml.org/type/merge.html, but with x: 1 before the merge key. " +
"This is legacy behavior, see --yaml-fix-merge-anchor-to-spec",
document: specDocument + "- x: 1\n << : [ *BIG, *LEFT, *SMALL ]\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::x: 0\nr: 10\ny: 2\n"},
},
{
description: "Get anchor",
document: `a: &billyBob cat`,

View File

@ -3,7 +3,6 @@ package yqlib
import (
"container/list"
"fmt"
"github.com/elliotchance/orderedmap"
)
@ -280,11 +279,39 @@ func doTraverseMap(newMatches *orderedmap.OrderedMap, node *CandidateNode, wante
log.Debug("MATCHED")
if prefs.IncludeMapKeys {
log.Debug("including key")
newMatches.Set(key.GetKey(), key)
keyName := key.GetKey()
if newMatches.Has(keyName) {
if ConfiguredYamlPreferences.FixMergeAnchorToSpec {
log.Debug("not overwriting existing key")
} else {
log.Warning(
"--yaml-fix-merge-anchor-to-spec is false; "+
"causing the merge anchor to override the existing key at %v which isn't to the yaml spec. "+
"This flag will default to true in late 2025.", key.GetNicePath())
log.Debug("overwriting existing key")
newMatches.Set(keyName, key)
}
} else {
newMatches.Set(keyName, key)
}
}
if !prefs.DontIncludeMapValues {
log.Debug("including value")
newMatches.Set(value.GetKey(), value)
valueName := value.GetKey()
if newMatches.Has(valueName) {
if ConfiguredYamlPreferences.FixMergeAnchorToSpec {
log.Debug("not overwriting existing value")
} else {
log.Warning(
"--yaml-fix-merge-anchor-to-spec is false; "+
"causing the merge anchor to override the existing value at %v which isn't to the yaml spec. "+
"This flag will default to true in late 2025.", key.GetNicePath())
log.Debug("overwriting existing value")
newMatches.Set(valueName, value)
}
} else {
newMatches.Set(valueName, value)
}
}
}
}
@ -300,9 +327,7 @@ func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, merge *CandidateNode
case MappingNode:
return doTraverseMap(newMatches, merge, wantedKey, prefs, splat)
case SequenceNode:
// Earlier keys take precedence
for index := len(merge.Content) - 1; index >= 0; index = index - 1 {
childValue := merge.Content[index]
for _, childValue := range merge.Content {
if childValue.Kind == AliasNode {
childValue = childValue.Alias
}

View File

@ -25,6 +25,26 @@ foobar:
thing: foobar_thing
`
var fixedTraversePathOperatorScenarios = []expressionScenario{
{
description: "Traversing merge anchor lists",
subdescription: "Note that the keys earlier in the merge anchors sequence override later ones",
document: mergeDocSample,
expression: `.foobarList.thing`,
expected: []string{
"D0, P[foo thing], (!!str)::foo_thing\n",
},
},
{
description: "Traversing merge anchors with override",
document: mergeDocSample,
expression: `.foobar.c`,
expected: []string{
"D0, P[foobar c], (!!str)::foobar_c\n",
},
},
}
var traversePathOperatorScenarios = []expressionScenario{
{
skipDoc: true,
@ -400,9 +420,10 @@ var traversePathOperatorScenarios = []expressionScenario{
},
},
{
description: "Traversing merge anchors with override",
document: mergeDocSample,
expression: `.foobar.c`,
description: "Traversing merge anchors with override",
subdescription: "This is legacy behavior, see --yaml-fix-merge-anchor-to-spec",
document: mergeDocSample,
expression: `.foobar.c`,
expected: []string{
"D0, P[foo c], (!!str)::foo_c\n",
},
@ -442,12 +463,13 @@ var traversePathOperatorScenarios = []expressionScenario{
},
},
{
description: "Traversing merge anchor lists",
subdescription: "Note that the keys earlier in the merge anchors sequence override later ones",
document: mergeDocSample,
expression: `.foobarList.thing`,
description: "Traversing merge anchor lists",
subdescription: "Note that the later merge anchors override previous, " +
"but this is legacy behavior, see --yaml-fix-merge-anchor-to-spec",
document: mergeDocSample,
expression: `.foobarList.thing`,
expected: []string{
"D0, P[foo thing], (!!str)::foo_thing\n",
"D0, P[bar thing], (!!str)::bar_thing\n",
},
},
{
@ -467,14 +489,15 @@ var traversePathOperatorScenarios = []expressionScenario{
},
},
{
description: "Splatting merge anchor lists",
document: mergeDocSample,
expression: `.foobarList[]`,
description: "Splatting merge anchor lists",
subdescription: "With legacy override behavior, see --yaml-fix-merge-anchor-to-spec",
document: mergeDocSample,
expression: `.foobarList[]`,
expected: []string{
"D0, P[bar b], (!!str)::bar_b\n",
"D0, P[foo thing], (!!str)::foo_thing\n",
"D0, P[foobarList c], (!!str)::foobarList_c\n",
"D0, P[foo a], (!!str)::foo_a\n",
"D0, P[bar thing], (!!str)::bar_thing\n",
"D0, P[foobarList c], (!!str)::foobarList_c\n",
},
},
{
@ -575,3 +598,11 @@ func TestTraversePathOperatorScenarios(t *testing.T) {
}
documentOperatorScenarios(t, "traverse-read", traversePathOperatorScenarios)
}
func TestTraversePathOperatorAlignedToSpecScenarios(t *testing.T) {
ConfiguredYamlPreferences.FixMergeAnchorToSpec = true
for _, tt := range fixedTraversePathOperatorScenarios {
testScenario(t, &tt)
}
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
}