Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
162ea54
Fix merge anchor traversing
stevenwdv Jun 13, 2025
c378279
Merge anchor traversing: add test for aliased sequence, cleanup
stevenwdv Jun 16, 2025
bfcb3fc
Fix merge anchor exploding
stevenwdv Jun 16, 2025
31628e7
Make merge anchor errors for traversing nonfatal
stevenwdv Jun 16, 2025
4d88d51
Fix precedence of merge anchor sequence for traverse (explode was alr…
stevenwdv Jun 16, 2025
4734be9
Fix excessive exploding for merge anchor
stevenwdv Jun 16, 2025
ce9a4af
Fix inline map exploding when it contains aliases
stevenwdv Jun 16, 2025
78c096f
Remove redundant logic
stevenwdv Jun 17, 2025
fa6dc5c
Move new merge test comments to description field
stevenwdv Jul 10, 2025
8c06478
Remove now-unused `badAliasSample`
stevenwdv Jul 10, 2025
128bf80
Merge branch 'master' into merge-anchor-fix
stevenwdv Jul 16, 2025
a47e882
Flag for fixed list merge key traverse override behavior,
stevenwdv Jul 16, 2025
a5b8ef6
Add some tests regarding override behavior.
stevenwdv Jul 16, 2025
b7aa711
Add note
stevenwdv Jul 16, 2025
08ecd39
Add tests for invalid merge key handling for traverse
stevenwdv Jul 16, 2025
5e75db8
UK spelling
stevenwdv Jul 17, 2025
23a7b17
Fixing merge anchor key order
mikefarah Jul 19, 2025
ae87394
Formatting
stevenwdv Jul 20, 2025
3431aeb
Add tests for accessing `!!str <<`
stevenwdv Jul 20, 2025
a4720c0
Merge remote-tracking branch 'origin/MakeExplodeGreatAgain' into merg…
stevenwdv Jul 20, 2025
9c95a9f
Unify reconstructAliasedMap & fixedReconstructAliasedMap
stevenwdv Jul 20, 2025
41cc4fb
Merge remote-tracking branch 'stevenwdv/merge-anchor-fix' into merge-…
stevenwdv Jul 20, 2025
904215e
Fix key overriding in regular maps for traversing
stevenwdv Jul 20, 2025
70ac3d6
Add override behavior comments
stevenwdv Jul 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion pkg/yqlib/doc/operators/anchor-and-alias-operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,39 @@ yq '.[4] | explode(.)' sample.yml
will output
```yaml
r: 10
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
```

Expand Down Expand Up @@ -293,8 +325,8 @@ bar:
foobarList:
b: bar_b
thing: foo_thing
c: foobarList_c
a: foo_a
c: foobarList_c
foobar:
c: foo_c
a: foo_a
Expand Down
6 changes: 5 additions & 1 deletion pkg/yqlib/doc/operators/traverse-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -399,7 +401,7 @@ foobar_thing
```

## Traversing merge anchor lists
Note that the later merge anchors override previous
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
Expand Down Expand Up @@ -432,6 +434,8 @@ 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
Expand Down
118 changes: 79 additions & 39 deletions pkg/yqlib/operator_anchors_aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package yqlib
import (
"container/list"
"fmt"
"slices"
)

func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
Expand Down Expand Up @@ -147,27 +148,15 @@ func reconstructAliasedMap(node *CandidateNode, context Context) error {
keyNode := node.Content[index]
valueNode := node.Content[index+1]
log.Debugf("traversing %v", keyNode.Value)
if keyNode.Value != "<<" {
err := overrideEntry(node, keyNode, valueNode, index, context.ChildContext(newContent))
if keyNode.Tag != "!!merge" {
err := overrideEntry(node, keyNode, valueNode, index, true, context.ChildContext(newContent))
if err != nil {
return err
}
} else {
if valueNode.Kind == SequenceNode {
log.Debugf("an alias merge list!")
for index := len(valueNode.Content) - 1; index >= 0; index = index - 1 {
aliasNode := valueNode.Content[index]
err := applyAlias(node, aliasNode.Alias, index, context.ChildContext(newContent))
if err != nil {
return err
}
}
} else {
log.Debugf("an alias merge!")
err := applyAlias(node, valueNode.Alias, index, context.ChildContext(newContent))
if err != nil {
return err
}
err := applyMergeAnchor(node, valueNode, index, context.ChildContext(newContent))
if err != nil {
return err
}
}
}
Expand Down Expand Up @@ -208,7 +197,7 @@ func explodeNode(node *CandidateNode, context Context) error {
hasAlias := false
for index := 0; index < len(node.Content); index = index + 2 {
keyNode := node.Content[index]
if keyNode.Value == "<<" {
if keyNode.Tag == "!!merge" {
hasAlias = true
break
}
Expand Down Expand Up @@ -237,42 +226,91 @@ func explodeNode(node *CandidateNode, context Context) error {
}
}

func applyAlias(node *CandidateNode, alias *CandidateNode, aliasIndex int, newContent Context) error {
log.Debug("alias is nil ?")
if alias == nil {
func applyMergeAnchor(node *CandidateNode, merge *CandidateNode, mergeIndex int, newContent Context) error {
inline := true
if merge.Kind == AliasNode {
inline = false
merge = merge.Alias
}
switch merge.Kind {
case MappingNode:
log.Debugf("a merge map!")
return applyMergeAnchorMap(node, merge, mergeIndex, inline, newContent)
case SequenceNode:
log.Debugf("a merge list!")
// 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
childValue = childValue.Alias
}
if childValue.Kind != MappingNode {
return fmt.Errorf(
"can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v",
childValue.Tag)
}
err := applyMergeAnchorMap(node, childValue, mergeIndex, childInline, newContent)
if err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got %v", merge.Tag)
}
}

func applyMergeAnchorMap(node *CandidateNode, mergeMap *CandidateNode, mergeIndex int, explode bool, newContent Context) error {
if mergeMap == nil {
log.Debug("merge map is nil")
return nil
}
log.Debug("alias: %v", NodeToString(alias))
if alias.Kind != MappingNode {
return fmt.Errorf("merge anchor only supports maps, got %v instead", alias.Tag)
log.Debug("merge map: %v", NodeToString(mergeMap))
if mergeMap.Kind != MappingNode {
return fmt.Errorf("applyMergeAnchorMap expects !!map, got %v instead", mergeMap.Tag)
}
for index := 0; index < len(alias.Content); index = index + 2 {
keyNode := alias.Content[index]
log.Debugf("applying alias key %v", keyNode.Value)
valueNode := alias.Content[index+1]
err := overrideEntry(node, keyNode, valueNode, aliasIndex, newContent)

if explode {
err := explodeNode(mergeMap, newContent)
if err != nil {
return err
}
}

for index := 0; index < len(mergeMap.Content); index = index + 2 {
keyNode := mergeMap.Content[index]
log.Debugf("applying merge map key %v", keyNode.Value)
valueNode := mergeMap.Content[index+1]
err := overrideEntry(node, keyNode, valueNode, mergeIndex, explode, newContent)
if err != nil {
return err
}
}
return nil
}

func overrideEntry(node *CandidateNode, key *CandidateNode, value *CandidateNode, startIndex int, newContent Context) error {

err := explodeNode(value, newContent)

if err != nil {
return err
func overrideEntry(node *CandidateNode, key *CandidateNode, value *CandidateNode, startIndex int, explode bool, newContent Context) error {
if explode {
err := explodeNode(value, newContent)
if err != nil {
return err
}
}

for newEl := newContent.MatchingNodes.Front(); newEl != nil; newEl = newEl.Next() {
valueEl := newEl.Next() // move forward twice
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh darn so this means I broke that behavior? I thought I would have had a failing test somewhere :/

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
}
Expand All @@ -290,9 +328,11 @@ func overrideEntry(node *CandidateNode, key *CandidateNode, value *CandidateNode
}
}

err = explodeNode(key, newContent)
if err != nil {
return err
if explode {
err := explodeNode(key, newContent)
if err != nil {
return err
}
}
log.Debugf("adding %v:%v", key.Value, value.Value)
newContent.MatchingNodes.PushBack(key)
Expand Down
Loading
Loading