fix: apply per-candidate index when traversing arrays/maps by a streamed index

`$o[.]` over a streamed context (e.g. `keys[] | $o[.]`) only returned the
first match. The index expression yields one index set per incoming
candidate, but traverseArrayOperator used only the first set
(rhs.MatchingNodes.Front()), dropping the rest.

Pair each index set with its candidate: when the LHS has one node per
candidate (e.g. `.[] | .[idx]`) each node is traversed with its own index
set; when the LHS collapses to a single node (a variable) it is traversed
against every index set. Covers both arrays and maps.

Fixes #2593.
This commit is contained in:
StressTestor 2026-06-15 16:00:44 -06:00
parent 5cf0adcc5b
commit 0cc5c19843
2 changed files with 53 additions and 11 deletions

View File

@ -111,10 +111,10 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode
return Context{}, err
}
// rhs is a collect expression that will yield indices to retrieve of the arrays
// rhs is a collect expression that yields the indices to retrieve. It is
// evaluated over the whole context, producing one index set per incoming
// candidate.
rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.RHS)
if err != nil {
return Context{}, err
}
@ -123,16 +123,37 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode
if expressionNode.Operation.Preferences != nil {
prefs = expressionNode.Operation.Preferences.(traversePreferences)
}
var indicesToTraverse = rhs.MatchingNodes.Front().Value.(*CandidateNode).Content
log.Debugf("indicesToTraverse %v", len(indicesToTraverse))
//now we traverse the result of the lhs against the indices we found
result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, prefs)
if err != nil {
return Context{}, err
results := list.New()
if lhs.MatchingNodes.Len() == rhs.MatchingNodes.Len() {
// One index set per LHS node (both derive from the same context):
// traverse each LHS node with its own index set. Previously only the
// first index set was used, so a context-dependent index like `$o[.]`
// over a `keys[]` stream dropped every match but the first (#2593).
rhsEl := rhs.MatchingNodes.Front()
for lhsEl := lhs.MatchingNodes.Front(); lhsEl != nil; lhsEl = lhsEl.Next() {
indicesToTraverse := rhsEl.Value.(*CandidateNode).Content
result, err := traverseNodesWithArrayIndices(context.SingleChildContext(lhsEl.Value.(*CandidateNode)), indicesToTraverse, prefs)
if err != nil {
return Context{}, err
}
results.PushBackList(result.MatchingNodes)
rhsEl = rhsEl.Next()
}
} else {
// LHS collapsed to a single node (e.g. a variable) while the index
// varies per candidate: traverse the LHS against every index set.
for rhsEl := rhs.MatchingNodes.Front(); rhsEl != nil; rhsEl = rhsEl.Next() {
indicesToTraverse := rhsEl.Value.(*CandidateNode).Content
result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, prefs)
if err != nil {
return Context{}, err
}
results.PushBackList(result.MatchingNodes)
}
}
return context.ChildContext(result.MatchingNodes), nil
return context.ChildContext(results), nil
}
func traverseNodesWithArrayIndices(context Context, indicesToTraverse []*CandidateNode, prefs traversePreferences) (Context, error) {

View File

@ -675,6 +675,27 @@ var traversePathOperatorScenarios = []expressionScenario{
expression: ". = (.x = 1)",
expectedError: "alias cycle detected",
},
{
// Regression test for https://github.com/mikefarah/yq/issues/2593
// A context-dependent index (here the streamed key) must be applied
// per candidate; previously only the first index was used.
skipDoc: true,
document: `["a","b"]`,
expression: `. as $o | keys[] | $o[.]`,
expected: []string{
"D0, P[0], (!!str)::a\n",
"D0, P[1], (!!str)::b\n",
},
},
{
skipDoc: true,
document: `{"x": 1, "y": 2}`,
expression: `. as $o | keys[] | $o[.]`,
expected: []string{
"D0, P[x], (!!int)::1\n",
"D0, P[y], (!!int)::2\n",
},
},
}
func TestTraversePathOperatorScenarios(t *testing.T) {