From d8bf1d104402457a44d9293e8d2767d8efaef844 Mon Sep 17 00:00:00 2001 From: Edmo Vamerlatti Costa <11836452+edmocosta@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:34:48 +0200 Subject: [PATCH] [pkg/ottl] Add grammar utility to extract paths from statements (#35174) **Description:** This PR is part of https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/29017, and adds a grammar utility to allow extracting `ottl.path` from parsed statements and expressions. The new functions will power the context inferrer utility (see [draft](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/35050)), which implementation will basically gather all declared statement's `Path.Context`, and choose the highest priority one. For the statements rewriter utility purpose, it changes the `grammar.go` file including a new field `Pos lexer.Position` into the `ottl.path` struct. This value is automatically set by the `participle` lexer and holds the path's offsets, being useful for identifying their positions in the raw statement, without using regexes an being coupled to the grammar's definition. **Additional changes**: This proposal uses the visitor pattern to gather all path's from the parsed statements. Considering the grammar's custom error mechanism (`checkForCustomError`) also works with a similar pattern, I've added the visitor implementation as part of the grammar objects, and refactored all `checkForCustomError` functions to be part of a validator visitor. Differently of the current implementation, it joins and displays all errors at once instead of one by one, IMO, it's specially useful for statements with more than one error, for example: `set(name, int(2), float(1))`. *** We can change it back do be error-by-error if necessary. *** If modifying the custom validator is not desired, I can roll those changes back keeping only the necessary code to extract the path's + `Pos` field. **Link to tracking Issue:** https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/29017 **Testing:** Unit tests were added **Documentation:** No changes --- pkg/ottl/grammar.go | 163 +++++++++++++++ pkg/ottl/parser_test.go | 171 +++++++++++++++ pkg/ottl/paths.go | 32 +++ pkg/ottl/paths_test.go | 450 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 816 insertions(+) create mode 100644 pkg/ottl/paths.go create mode 100644 pkg/ottl/paths_test.go diff --git a/pkg/ottl/grammar.go b/pkg/ottl/grammar.go index 369eff00ecbd..2407a138c1c2 100644 --- a/pkg/ottl/grammar.go +++ b/pkg/ottl/grammar.go @@ -57,6 +57,18 @@ func (b *booleanValue) checkForCustomError() error { return nil } +func (b *booleanValue) accept(v grammarVisitor) { + if b.Comparison != nil { + b.Comparison.accept(v) + } + if b.ConstExpr != nil && b.ConstExpr.Converter != nil { + b.ConstExpr.Converter.accept(v) + } + if b.SubExpr != nil { + b.SubExpr.accept(v) + } +} + // opAndBooleanValue represents the right side of an AND boolean expression. type opAndBooleanValue struct { Operator string `parser:"@OpAnd"` @@ -67,6 +79,12 @@ func (b *opAndBooleanValue) checkForCustomError() error { return b.Value.checkForCustomError() } +func (b *opAndBooleanValue) accept(v grammarVisitor) { + if b.Value != nil { + b.Value.accept(v) + } +} + // term represents an arbitrary number of boolean values joined by AND. type term struct { Left *booleanValue `parser:"@@"` @@ -87,6 +105,17 @@ func (b *term) checkForCustomError() error { return nil } +func (b *term) accept(v grammarVisitor) { + if b.Left != nil { + b.Left.accept(v) + } + for _, r := range b.Right { + if r != nil { + r.accept(v) + } + } +} + // opOrTerm represents the right side of an OR boolean expression. type opOrTerm struct { Operator string `parser:"@OpOr"` @@ -97,6 +126,12 @@ func (b *opOrTerm) checkForCustomError() error { return b.Term.checkForCustomError() } +func (b *opOrTerm) accept(v grammarVisitor) { + if b.Term != nil { + b.Term.accept(v) + } +} + // booleanExpression represents a true/false decision expressed // as an arbitrary number of terms separated by OR. type booleanExpression struct { @@ -118,6 +153,17 @@ func (b *booleanExpression) checkForCustomError() error { return nil } +func (b *booleanExpression) accept(v grammarVisitor) { + if b.Left != nil { + b.Left.accept(v) + } + for _, r := range b.Right { + if r != nil { + r.accept(v) + } + } +} + // compareOp is the type of a comparison operator. type compareOp int @@ -187,6 +233,11 @@ func (c *comparison) checkForCustomError() error { return err } +func (c *comparison) accept(v grammarVisitor) { + c.Left.accept(v) + c.Right.accept(v) +} + // editor represents the function call of a statement. type editor struct { Function string `parser:"@(Lowercase(Uppercase | Lowercase)*)"` @@ -210,6 +261,13 @@ func (i *editor) checkForCustomError() error { return nil } +func (i *editor) accept(v grammarVisitor) { + v.visitEditor(i) + for _, arg := range i.Arguments { + arg.accept(v) + } +} + // converter represents a converter function call. type converter struct { Function string `parser:"@(Uppercase(Uppercase | Lowercase)*)"` @@ -217,6 +275,14 @@ type converter struct { Keys []key `parser:"( @@ )*"` } +func (c *converter) accept(v grammarVisitor) { + if c.Arguments != nil { + for _, a := range c.Arguments { + a.accept(v) + } + } +} + type argument struct { Name string `parser:"(@(Lowercase(Uppercase | Lowercase)*) Equal)?"` Value value `parser:"( @@"` @@ -227,6 +293,10 @@ func (a *argument) checkForCustomError() error { return a.Value.checkForCustomError() } +func (a *argument) accept(v grammarVisitor) { + a.Value.accept(v) +} + // value represents a part of a parsed statement which is resolved to a value of some sort. This can be a telemetry path // mathExpression, function call, or literal. type value struct { @@ -251,8 +321,27 @@ func (v *value) checkForCustomError() error { return nil } +func (v *value) accept(vis grammarVisitor) { + vis.visitValue(v) + if v.Literal != nil { + v.Literal.accept(vis) + } + if v.MathExpression != nil { + v.MathExpression.accept(vis) + } + if v.Map != nil { + v.Map.accept(vis) + } + if v.List != nil { + for _, i := range v.List.Values { + i.accept(vis) + } + } +} + // path represents a telemetry path mathExpression. type path struct { + Pos lexer.Position Context string `parser:"(@Lowercase '.')?"` Fields []field `parser:"@@ ( '.' @@ )*"` } @@ -276,6 +365,14 @@ type mapValue struct { Values []mapItem `parser:"'{' (@@ ','?)* '}'"` } +func (m *mapValue) accept(v grammarVisitor) { + for _, i := range m.Values { + if i.Value != nil { + i.Value.accept(v) + } + } +} + type mapItem struct { Key *string `parser:"@String ':'"` Value *value `parser:"@@"` @@ -326,6 +423,19 @@ func (m *mathExprLiteral) checkForCustomError() error { return nil } +func (m *mathExprLiteral) accept(v grammarVisitor) { + v.visitMathExprLiteral(m) + if m.Path != nil { + v.visitPath(m.Path) + } + if m.Editor != nil { + m.Editor.accept(v) + } + if m.Converter != nil { + m.Converter.accept(v) + } +} + type mathValue struct { Literal *mathExprLiteral `parser:"( @@"` SubExpression *mathExpression `parser:"| '(' @@ ')' )"` @@ -338,6 +448,15 @@ func (m *mathValue) checkForCustomError() error { return m.SubExpression.checkForCustomError() } +func (m *mathValue) accept(v grammarVisitor) { + if m.Literal != nil { + m.Literal.accept(v) + } + if m.SubExpression != nil { + m.SubExpression.accept(v) + } +} + type opMultDivValue struct { Operator mathOp `parser:"@OpMultDiv"` Value *mathValue `parser:"@@"` @@ -347,6 +466,12 @@ func (m *opMultDivValue) checkForCustomError() error { return m.Value.checkForCustomError() } +func (m *opMultDivValue) accept(v grammarVisitor) { + if m.Value != nil { + m.Value.accept(v) + } +} + type addSubTerm struct { Left *mathValue `parser:"@@"` Right []*opMultDivValue `parser:"@@*"` @@ -366,6 +491,17 @@ func (m *addSubTerm) checkForCustomError() error { return nil } +func (m *addSubTerm) accept(v grammarVisitor) { + if m.Left != nil { + m.Left.accept(v) + } + for _, r := range m.Right { + if r != nil { + r.accept(v) + } + } +} + type opAddSubTerm struct { Operator mathOp `parser:"@OpAddSub"` Term *addSubTerm `parser:"@@"` @@ -375,6 +511,12 @@ func (m *opAddSubTerm) checkForCustomError() error { return m.Term.checkForCustomError() } +func (m *opAddSubTerm) accept(v grammarVisitor) { + if m.Term != nil { + m.Term.accept(v) + } +} + type mathExpression struct { Left *addSubTerm `parser:"@@"` Right []*opAddSubTerm `parser:"@@*"` @@ -394,6 +536,19 @@ func (m *mathExpression) checkForCustomError() error { return nil } +func (m *mathExpression) accept(v grammarVisitor) { + if m.Left != nil { + m.Left.accept(v) + } + if m.Right != nil { + for _, r := range m.Right { + if r != nil { + r.accept(v) + } + } + } +} + type mathOp int const ( @@ -464,3 +619,11 @@ func buildLexer() *lexer.StatefulDefinition { {Name: "whitespace", Pattern: `\s+`}, }) } + +// grammarVisitor allows accessing the grammar AST nodes using the visitor pattern. +type grammarVisitor interface { + visitPath(v *path) + visitEditor(v *editor) + visitValue(v *value) + visitMathExprLiteral(v *mathExprLiteral) +} diff --git a/pkg/ottl/parser_test.go b/pkg/ottl/parser_test.go index d0a5e4b47add..8a6040741f63 100644 --- a/pkg/ottl/parser_test.go +++ b/pkg/ottl/parser_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/alecthomas/participle/v2/lexer" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/component/componenttest" @@ -207,6 +208,11 @@ func Test_parse(t *testing.T) { Value: &value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 38, + Line: 1, + Column: 39, + }, Context: "bear", Fields: []field{ { @@ -267,6 +273,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 24, + Line: 1, + Column: 25, + }, Context: "bear", Fields: []field{ { @@ -298,6 +309,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Context: "foo", Fields: []field{ { @@ -337,6 +353,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Context: "", Fields: []field{ { @@ -373,6 +394,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 16, + Line: 1, + Column: 17, + }, Fields: []field{ { Name: "attributes", @@ -396,6 +422,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 51, + Line: 1, + Column: 52, + }, Fields: []field{ { Name: "attributes", @@ -431,6 +462,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 16, + Line: 1, + Column: 17, + }, Fields: []field{ { Name: "attributes", @@ -464,6 +500,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 16, + Line: 1, + Column: 17, + }, Fields: []field{ { Name: "attributes", @@ -499,6 +540,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Context: "foo", Fields: []field{ { @@ -553,6 +599,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Context: "foo", Fields: []field{ { @@ -585,6 +636,11 @@ func Test_parse(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 44, + Line: 1, + Column: 45, + }, Fields: []field{ { Name: "name", @@ -614,6 +670,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Context: "foo", Fields: []field{ { @@ -646,6 +707,11 @@ func Test_parse(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 44, + Line: 1, + Column: 45, + }, Fields: []field{ { Name: "name", @@ -675,6 +741,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 7, + Line: 1, + Column: 8, + }, Context: "foo", Fields: []field{ { @@ -707,6 +778,11 @@ func Test_parse(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 52, + Line: 1, + Column: 53, + }, Fields: []field{ { Name: "name", @@ -797,6 +873,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -832,6 +913,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -867,6 +953,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -902,6 +993,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -939,6 +1035,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -980,6 +1081,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -1024,6 +1130,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -1095,6 +1206,11 @@ func Test_parse(t *testing.T) { { Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 70, + Line: 1, + Column: 71, + }, Fields: []field{ { Name: "attributes", @@ -1128,6 +1244,11 @@ func Test_parse(t *testing.T) { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "attributes", @@ -1213,6 +1334,11 @@ func Test_parse(t *testing.T) { Left: &mathValue{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 55, + Line: 1, + Column: 56, + }, Fields: []field{ { Name: "three", @@ -1287,6 +1413,11 @@ func Test_parseCondition_full(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 0, + Line: 1, + Column: 1, + }, Fields: []field{ { Name: "name", @@ -1314,6 +1445,11 @@ func Test_parseCondition_full(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 0, + Line: 1, + Column: 1, + }, Fields: []field{ { Name: "name", @@ -1378,6 +1514,11 @@ func Test_parseCondition_full(t *testing.T) { Left: &mathValue{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 13, + Line: 1, + Column: 14, + }, Fields: []field{ { Name: "three", @@ -1477,6 +1618,11 @@ func setNameTest(b *booleanExpression) *parsedStatement { Value: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, Fields: []field{ { Name: "name", @@ -1713,6 +1859,11 @@ func Test_parseWhere(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 24, + Line: 1, + Column: 25, + }, Fields: []field{ { Name: "name", @@ -1735,6 +1886,11 @@ func Test_parseWhere(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 42, + Line: 1, + Column: 43, + }, Fields: []field{ { Name: "name", @@ -1763,6 +1919,11 @@ func Test_parseWhere(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 24, + Line: 1, + Column: 25, + }, Fields: []field{ { Name: "name", @@ -1787,6 +1948,11 @@ func Test_parseWhere(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 41, + Line: 1, + Column: 42, + }, Fields: []field{ { Name: "name", @@ -1839,6 +2005,11 @@ func Test_parseWhere(t *testing.T) { Left: value{ Literal: &mathExprLiteral{ Path: &path{ + Pos: lexer.Position{ + Offset: 28, + Line: 1, + Column: 29, + }, Fields: []field{ { Name: "name", diff --git a/pkg/ottl/paths.go b/pkg/ottl/paths.go new file mode 100644 index 000000000000..dbb66ee7c994 --- /dev/null +++ b/pkg/ottl/paths.go @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottl // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" + +// grammarPathVisitor is used to extract all path from a parsedStatement or booleanExpression +type grammarPathVisitor struct { + paths []path +} + +func (v *grammarPathVisitor) visitEditor(_ *editor) {} +func (v *grammarPathVisitor) visitValue(_ *value) {} +func (v *grammarPathVisitor) visitMathExprLiteral(_ *mathExprLiteral) {} + +func (v *grammarPathVisitor) visitPath(value *path) { + v.paths = append(v.paths, *value) +} + +func getParsedStatementPaths(ps *parsedStatement) []path { + visitor := &grammarPathVisitor{} + ps.Editor.accept(visitor) + if ps.WhereClause != nil { + ps.WhereClause.accept(visitor) + } + return visitor.paths +} + +func getBooleanExpressionPaths(be *booleanExpression) []path { + visitor := &grammarPathVisitor{} + be.accept(visitor) + return visitor.paths +} diff --git a/pkg/ottl/paths_test.go b/pkg/ottl/paths_test.go new file mode 100644 index 000000000000..9f31dda15718 --- /dev/null +++ b/pkg/ottl/paths_test.go @@ -0,0 +1,450 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottl + +import ( + "testing" + + "github.com/alecthomas/participle/v2/lexer" + "github.com/stretchr/testify/require" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottltest" +) + +func Test_getParsedStatementPaths(t *testing.T) { + tests := []struct { + name string + statement string + expected []path + }{ + { + name: "editor with nested map with path", + statement: `fff({"mapAttr": {"foo": "bar", "get": bear.honey, "arrayAttr":["foo", "bar"]}})`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 38, + Line: 1, + Column: 39, + }, + Context: "bear", + Fields: []field{ + { + Name: "honey", + }, + }, + }, + }, + }, + { + name: "editor with function path parameter", + statement: `set("foo", GetSomething(bear.honey))`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 24, + Line: 1, + Column: 25, + }, + Context: "bear", + Fields: []field{ + { + Name: "honey", + }, + }, + }, + }, + }, + { + name: "path with key", + statement: `set(foo.attributes["bar"].cat, "dog")`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Context: "foo", + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("bar"), + }, + }, + }, + { + Name: "cat", + }, + }, + }, + }, + }, + { + name: "single path field segment", + statement: `set(attributes["bar"], "dog")`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Context: "", + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("bar"), + }, + }, + }, + }, + }, + }, + }, + { + name: "converter parameters", + statement: `replace_pattern(attributes["message"], "device=*", attributes["device_name"], SHA256)`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 16, + Line: 1, + Column: 17, + }, + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("message"), + }, + }, + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 51, + Line: 1, + Column: 52, + }, + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("device_name"), + }, + }, + }, + }, + }, + }, + }, + { + name: "complex path with multiple keys", + statement: `set(foo.bar["x"]["y"].z, Test()[0]["pass"])`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Context: "foo", + Fields: []field{ + { + Name: "bar", + Keys: []key{ + { + String: ottltest.Strp("x"), + }, + { + String: ottltest.Strp("y"), + }, + }, + }, + { + Name: "z", + }, + }, + }, + }, + }, + { + name: "where clause", + statement: `set(foo.attributes["bar"].cat, "dog") where name == "fido"`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Context: "foo", + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("bar"), + }, + }, + }, + { + Name: "cat", + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 44, + Line: 1, + Column: 45, + }, + Fields: []field{ + { + Name: "name", + }, + }, + }, + }, + }, + { + name: "where clause multiple conditions", + statement: `set(foo.attributes["bar"].cat, "dog") where name == "fido" and surname == "dido" or surname == "DIDO"`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Context: "foo", + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("bar"), + }, + }, + }, + { + Name: "cat", + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 44, + Line: 1, + Column: 45, + }, + Fields: []field{ + { + Name: "name", + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 63, + Line: 1, + Column: 64, + }, + Fields: []field{ + { + Name: "surname", + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 84, + Line: 1, + Column: 85, + }, + Fields: []field{ + { + Name: "surname", + }, + }, + }, + }, + }, + { + name: "where clause sub expression", + statement: `set(foo.attributes["bar"].cat, "value") where three / (1 + 1) == foo.value`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Context: "foo", + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("bar"), + }, + }, + }, + { + Name: "cat", + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 46, + Line: 1, + Column: 47, + }, + Fields: []field{ + { + Name: "three", + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 65, + Line: 1, + Column: 66, + }, + Context: "foo", + Fields: []field{ + { + Name: "value", + }, + }, + }, + }, + }, + { + name: "converter with path list", + statement: `set(attributes["test"], [bear.bear, bear.honey])`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("test"), + }, + }, + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 25, + Line: 1, + Column: 26, + }, + Context: "bear", + Fields: []field{{Name: "bear"}}, + }, + { + Pos: lexer.Position{ + Offset: 36, + Line: 1, + Column: 37, + }, + Context: "bear", + Fields: []field{{Name: "honey"}}, + }, + }, + }, + { + name: "converter math math expression", + statement: `set(attributes["test"], 1000 - 600) where 1 + 1 * 2 == three / One()`, + expected: []path{ + { + Pos: lexer.Position{ + Offset: 4, + Line: 1, + Column: 5, + }, + Fields: []field{ + { + Name: "attributes", + Keys: []key{ + { + String: ottltest.Strp("test"), + }, + }, + }, + }, + }, + { + Pos: lexer.Position{ + Offset: 55, + Line: 1, + Column: 56, + }, + Fields: []field{ + { + Name: "three", + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ps, err := parseStatement(tt.statement) + require.NoError(t, err) + + paths := getParsedStatementPaths(ps) + require.Equal(t, tt.expected, paths) + }) + } +} + +func Test_getBooleanExpressionPaths(t *testing.T) { + expected := []path{ + { + Pos: lexer.Position{ + Offset: 0, + Line: 1, + Column: 1, + }, + Context: "honey", + Fields: []field{{Name: "bear"}}, + }, + { + Pos: lexer.Position{ + Offset: 21, + Line: 1, + Column: 22, + }, + Context: "foo", + Fields: []field{{Name: "bar"}}, + }, + } + + c, err := parseCondition("honey.bear == 1 and (foo.bar == true or 1 == 1)") + require.NoError(t, err) + + paths := getBooleanExpressionPaths(c) + require.Equal(t, expected, paths) +}