From bfcb3fc6b705264c3e84dd749f9860111fb2b698 Mon Sep 17 00:00:00 2001 From: Steven WdV Date: Mon, 16 Jun 2025 09:58:26 +0200 Subject: [PATCH] Fix merge anchor exploding - Allow inline maps instead of just aliases - Allow aliased sequences - Disallow other types - Use tag `!!merge` instead of key `<<` - Fix insertion index for sequence merge Closes #2386 --- .../operators/anchor-and-alias-operators.md | 4 +- pkg/yqlib/operator_anchors_aliases.go | 74 ++++++++++++------- pkg/yqlib/operator_anchors_aliases_test.go | 34 +++++++-- 3 files changed, 78 insertions(+), 34 deletions(-) diff --git a/pkg/yqlib/doc/operators/anchor-and-alias-operators.md b/pkg/yqlib/doc/operators/anchor-and-alias-operators.md index 14b0bef2..d624b88b 100644 --- a/pkg/yqlib/doc/operators/anchor-and-alias-operators.md +++ b/pkg/yqlib/doc/operators/anchor-and-alias-operators.md @@ -92,8 +92,8 @@ yq '.[4] | explode(.)' sample.yml will output ```yaml r: 10 -x: 1 y: 2 +x: 1 ``` ## Get anchor @@ -293,8 +293,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 diff --git a/pkg/yqlib/operator_anchors_aliases.go b/pkg/yqlib/operator_anchors_aliases.go index 4a2cdf4e..a33006e7 100644 --- a/pkg/yqlib/operator_anchors_aliases.go +++ b/pkg/yqlib/operator_anchors_aliases.go @@ -147,27 +147,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 != "<<" { + if keyNode.Tag != "!!merge" { err := overrideEntry(node, keyNode, valueNode, index, 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, newContent) + if err != nil { + return err } } } @@ -208,7 +196,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 } @@ -237,19 +225,51 @@ 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, context Context, newContent *list.List) error { + if merge.Kind == AliasNode { + merge = merge.Alias + } + switch merge.Kind { + case MappingNode: + log.Debugf("a merge map!") + return applyMergeAnchorMap(node, merge, mergeIndex, context.ChildContext(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] + if childValue.Kind == AliasNode { + 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, context.ChildContext(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, aliasIndex int, newContent Context) error { + log.Debug("merge map is nil ?") + if mergeMap == 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] + 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, aliasIndex, newContent) if err != nil { return err diff --git a/pkg/yqlib/operator_anchors_aliases_test.go b/pkg/yqlib/operator_anchors_aliases_test.go index a24f3fdd..9f02154e 100644 --- a/pkg/yqlib/operator_anchors_aliases_test.go +++ b/pkg/yqlib/operator_anchors_aliases_test.go @@ -45,8 +45,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 @@ -58,7 +58,7 @@ var anchorOperatorScenarios = []expressionScenario{ skipDoc: true, description: "merge anchor not map", document: "a: &a\n - 0\nc:\n <<: [*a]\n", - expectedError: "merge anchor only supports maps, got !!seq instead", + expectedError: "can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing !!seq", expression: "explode(.)", }, { @@ -80,7 +80,7 @@ var anchorOperatorScenarios = []expressionScenario{ subdescription: "see https://yaml.org/type/merge.html", document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n", expression: ".[4] | explode(.)", - expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"}, + expected: []string{"D0, P[4], (!!map)::r: 10\ny: 2\nx: 1\n"}, }, { description: "Get anchor", @@ -254,7 +254,7 @@ var anchorOperatorScenarios = []expressionScenario{ expression: `.foo* | explode(.) | (. style="flow")`, expected: []string{ "D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n", - "D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n", + "D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, a: foo_a, c: foobarList_c}\n", "D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n", }, }, @@ -264,7 +264,7 @@ var anchorOperatorScenarios = []expressionScenario{ expression: `.foo* | explode(explode(.)) | (. style="flow")`, expected: []string{ "D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n", - "D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n", + "D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, a: foo_a, c: foobarList_c}\n", "D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n", }, }, @@ -283,6 +283,30 @@ var anchorOperatorScenarios = []expressionScenario{ expression: `.thingOne |= explode(.) * {"value": false}`, expected: []string{expectedUpdatedArrayRef}, }, + { // Merge anchor with inline map + skipDoc: true, + document: `{<<: {a: 42}}`, + expression: `explode(.)`, + expected: []string{ + "D0, P[], (!!map)::{a: 42}\n", + }, + }, + { // Merge anchor with sequence with inline map + skipDoc: true, + document: `{<<: [{a: 42}]}`, + expression: `explode(.)`, + expected: []string{ + "D0, P[], (!!map)::{a: 42}\n", + }, + }, + { // Merge anchor with aliased sequence with inline map + skipDoc: true, + document: `{s: &s [{a: 42}], m: {<<: *s}}`, + expression: `.m | explode(.)`, + expected: []string{ + "D0, P[m], (!!map)::{a: 42}\n", + }, + }, } func TestAnchorAliasOperatorScenarios(t *testing.T) {