diff --git a/ast/builtins.go b/ast/builtins.go index 3af0d8cd90..6ca6f9b9f2 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -2470,7 +2470,7 @@ var WalkBuiltin = &Builtin{ types.A, }, nil, - )).Description("pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`"), + )).Description("pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`. If `path` is assigned a wildcard (`_`), the `walk` function will skip path creation entirely for faster evaluation."), ), Categories: graphs, } diff --git a/builtin_metadata.json b/builtin_metadata.json index 93d8e92cf2..075810ef69 100644 --- a/builtin_metadata.json +++ b/builtin_metadata.json @@ -18889,7 +18889,7 @@ "introduced": "v0.17.0", "relation": true, "result": { - "description": "pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`", + "description": "pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`. If `path` is assigned a wildcard (`_`), the `walk` function will skip path creation entirely for faster evaluation.", "name": "output", "type": "array\u003carray[any], any\u003e" }, diff --git a/test/cases/testdata/walkbuiltin/test-walkbuiltin-wildcard-path.yaml b/test/cases/testdata/walkbuiltin/test-walkbuiltin-wildcard-path.yaml new file mode 100644 index 0000000000..e764c9c8fc --- /dev/null +++ b/test/cases/testdata/walkbuiltin/test-walkbuiltin-wildcard-path.yaml @@ -0,0 +1,28 @@ +cases: + - modules: + - | + package testing + + obj := { + "bar": "baz", + "qux": [ + 1, + {"p": "rego", "q": "rules"}, + {1, 2, 3, {"a": "b", "c": {"d", "e", 1}}} + ] + } + + with_path[value] { + walk(obj, [path, value]) + } + + without_path[value] { + walk(obj, [_, value]) + } + + same_values := with_path == without_path + + note: "walkbuiltin/wildcard-path same values as when path provided" + query: x = data.testing.same_values + want_result: + - x: true diff --git a/topdown/walk.go b/topdown/walk.go index 8e0a346f24..0f3b3544b5 100644 --- a/topdown/walk.go +++ b/topdown/walk.go @@ -10,6 +10,15 @@ import ( func evalWalk(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { input := operands[0] + + if pathIsWildcard(operands) { + // When the path assignment is a wildcard: walk(input, [_, value]) + // we may skip the path construction entirely, and simply return + // same pointer in each iteration. This is a much more efficient + // path when only the values are needed. + return walkNoPath(input, iter) + } + filter := getOutputPath(operands) return walk(filter, nil, input, iter) } @@ -70,6 +79,33 @@ func walk(filter, path *ast.Array, input *ast.Term, iter func(*ast.Term) error) return nil } +var emptyArr = ast.ArrayTerm() + +func walkNoPath(input *ast.Term, iter func(*ast.Term) error) error { + if err := iter(ast.ArrayTerm(emptyArr, input)); err != nil { + return err + } + + switch v := input.Value.(type) { + case ast.Object: + return v.Iter(func(_, v *ast.Term) error { + return walkNoPath(v, iter) + }) + case *ast.Array: + for i := 0; i < v.Len(); i++ { + if err := walkNoPath(v.Elem(i), iter); err != nil { + return err + } + } + case ast.Set: + return v.Iter(func(elem *ast.Term) error { + return walkNoPath(elem, iter) + }) + } + + return nil +} + func pathAppend(path *ast.Array, key *ast.Term) *ast.Array { if path == nil { return ast.NewArray(key) @@ -80,17 +116,26 @@ func pathAppend(path *ast.Array, key *ast.Term) *ast.Array { func getOutputPath(operands []*ast.Term) *ast.Array { if len(operands) == 2 { - if arr, ok := operands[1].Value.(*ast.Array); ok { - if arr.Len() == 2 { - if path, ok := arr.Elem(0).Value.(*ast.Array); ok { - return path - } + if arr, ok := operands[1].Value.(*ast.Array); ok && arr.Len() == 2 { + if path, ok := arr.Elem(0).Value.(*ast.Array); ok { + return path } } } return nil } +func pathIsWildcard(operands []*ast.Term) bool { + if len(operands) == 2 { + if arr, ok := operands[1].Value.(*ast.Array); ok && arr.Len() == 2 { + if v, ok := arr.Elem(0).Value.(ast.Var); ok { + return v.IsWildcard() + } + } + } + return false +} + func init() { RegisterBuiltinFunc(ast.WalkBuiltin.Name, evalWalk) }