diff --git a/pkg/exprhelpers/visitor.go b/pkg/exprhelpers/visitor.go index de47cdb10..5dfcc6085 100644 --- a/pkg/exprhelpers/visitor.go +++ b/pkg/exprhelpers/visitor.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/antonmedv/expr/parser" + "github.com/google/uuid" "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" @@ -19,27 +20,61 @@ Visitor is used to reconstruct variables with its property called in an expr fil Thus, we can debug expr filter by displaying all variables contents present in the filter */ type visitor struct { - newVar bool - currentID string - properties []string - vars []string + newVar bool + currentId string + vars map[string][]string + logger *log.Entry } func (v *visitor) Visit(node *ast.Node) { - if n, ok := (*node).(*ast.IdentifierNode); ok { - if !v.newVar { - v.newVar = true - v.currentID = n.Value - } else { - fullVar := fmt.Sprintf("%s.%s", v.currentID, strings.Join(v.properties, ".")) - v.vars = append(v.vars, fullVar) - v.properties = []string{} - v.currentID = n.Value - } - } else if n2, ok := (*node).(*ast.MemberNode); ok { - if ns, ok := (n2.Property).(*ast.StringNode); ok { - v.properties = append(v.properties, ns.Value) + switch n := (*node).(type) { + case *ast.IdentifierNode: + v.newVar = true + uid, _ := uuid.NewUUID() + v.currentId = uid.String() + v.vars[v.currentId] = []string{n.Value} + case *ast.MemberNode: + if n2, ok := n.Property.(*ast.StringNode); ok { + v.vars[v.currentId] = append(v.vars[v.currentId], n2.Value) } + case *ast.StringNode: //Don't reset here, as any attribute of a member node is a string node (in evt.X, evt is member node, X is string node) + default: + v.newVar = false + v.currentId = "" + /*case *ast.IntegerNode: + v.logger.Infof("integer node found: %+v", n) + case *ast.FloatNode: + v.logger.Infof("float node found: %+v", n) + case *ast.BoolNode: + v.logger.Infof("boolean node found: %+v", n) + case *ast.ArrayNode: + v.logger.Infof("array node found: %+v", n) + case *ast.ConstantNode: + v.logger.Infof("constant node found: %+v", n) + case *ast.UnaryNode: + v.logger.Infof("unary node found: %+v", n) + case *ast.BinaryNode: + v.logger.Infof("binary node found: %+v", n) + case *ast.CallNode: + v.logger.Infof("call node found: %+v", n) + case *ast.BuiltinNode: + v.logger.Infof("builtin node found: %+v", n) + case *ast.ConditionalNode: + v.logger.Infof("conditional node found: %+v", n) + case *ast.ChainNode: + v.logger.Infof("chain node found: %+v", n) + case *ast.PairNode: + v.logger.Infof("pair node found: %+v", n) + case *ast.MapNode: + v.logger.Infof("map node found: %+v", n) + case *ast.SliceNode: + v.logger.Infof("slice node found: %+v", n) + case *ast.ClosureNode: + v.logger.Infof("closure node found: %+v", n) + case *ast.PointerNode: + v.logger.Infof("pointer node found: %+v", n) + default: + v.logger.Infof("unknown node found: %+v | type: %T", n, n)*/ } } @@ -52,34 +87,30 @@ func (v *visitor) Build(filter string, exprEnv expr.Option) (*ExprDebugger, erro filter: filter, } if filter == "" { - log.Debugf("unable to create expr debugger with empty filter") + v.logger.Debugf("unable to create expr debugger with empty filter") return &ExprDebugger{}, nil } v.newVar = false + v.vars = make(map[string][]string) tree, err := parser.Parse(filter) if err != nil { return nil, err } ast.Walk(&tree.Node, v) - if v.currentID != "" && len(v.properties) > 0 { // if its a variable with property (eg. evt.Line.Labels) - fullVar := fmt.Sprintf("%s.%s", v.currentID, strings.Join(v.properties, ".")) - v.vars = append(v.vars, fullVar) - } else if v.currentID != "" && len(v.properties) == 0 { // if it's a variable without property - fullVar := v.currentID - v.vars = append(v.vars, fullVar) - } else { - log.Debugf("no variable in filter : '%s'", filter) - } - v.properties = []string{} - v.currentID = "" + log.Debugf("vars: %+v", v.vars) for _, variable := range v.vars { - debugFilter, err := expr.Compile(variable, exprEnv) + if variable[0] != "evt" { + continue + } + toBuild := strings.Join(variable, ".") + v.logger.Debugf("compiling expression '%s'", toBuild) + debugFilter, err := expr.Compile(toBuild, exprEnv) if err != nil { - return ret, fmt.Errorf("compilation of variable '%s' failed: %v", variable, err) + return ret, fmt.Errorf("compilation of variable '%s' failed: %v", toBuild, err) } tmpExpression := &expression{ - variable, + toBuild, debugFilter, } expressions = append(expressions, tmpExpression) @@ -123,7 +154,8 @@ func (e *ExprDebugger) Run(logger *logrus.Entry, filterResult bool, exprEnv map[ // NewDebugger is the exported function that build the debuggers expressions func NewDebugger(filter string, exprEnv expr.Option) (*ExprDebugger, error) { - visitor := &visitor{} + logger := log.WithField("component", "expr-debugger") + visitor := &visitor{logger: logger} exprDebugger, err := visitor.Build(filter, exprEnv) return exprDebugger, err } diff --git a/pkg/exprhelpers/visitor_test.go b/pkg/exprhelpers/visitor_test.go new file mode 100644 index 000000000..969d19821 --- /dev/null +++ b/pkg/exprhelpers/visitor_test.go @@ -0,0 +1,100 @@ +package exprhelpers + +import ( + "sort" + "testing" + + "github.com/antonmedv/expr" + log "github.com/sirupsen/logrus" +) + +func TestVisitorBuild(t *testing.T) { + tests := []struct { + name string + expr string + want []string + env map[string]interface{} + }{ + { + name: "simple", + expr: "evt.X", + want: []string{"evt.X"}, + env: map[string]interface{}{ + "evt": map[string]interface{}{ + "X": 1, + }, + }, + }, + { + name: "two vars", + expr: "evt.X && evt.Y", + want: []string{"evt.X", "evt.Y"}, + env: map[string]interface{}{ + "evt": map[string]interface{}{ + "X": 1, + "Y": 2, + }, + }, + }, + { + name: "in", + expr: "evt.X in [1,2,3]", + want: []string{"evt.X"}, + env: map[string]interface{}{ + "evt": map[string]interface{}{ + "X": 1, + }, + }, + }, + { + name: "in complex", + expr: "evt.X in [1,2,3] && evt.Y in [1,2,3] || evt.Z in [1,2,3]", + want: []string{"evt.X", "evt.Y", "evt.Z"}, + env: map[string]interface{}{ + "evt": map[string]interface{}{ + "X": 1, + "Y": 2, + "Z": 3, + }, + }, + }, + { + name: "function call", + expr: "Foo(evt.X, 'ads')", + want: []string{"evt.X"}, + env: map[string]interface{}{ + "evt": map[string]interface{}{ + "X": 1, + }, + "Foo": func(x int, y string) int { + return x + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &visitor{logger: log.NewEntry(log.New())} + ret, err := v.Build(tt.expr, expr.Env(tt.env)) + if err != nil { + t.Errorf("visitor.Build() error = %v", err) + return + } + if len(ret.expression) != len(tt.want) { + t.Errorf("visitor.Build() = %v, want %v", ret.expression, tt.want) + } + //Sort both slices as the order is not guaranteed ?? + sort.Slice(tt.want, func(i, j int) bool { + return tt.want[i] < tt.want[j] + }) + sort.Slice(ret.expression, func(i, j int) bool { + return ret.expression[i].Str < ret.expression[j].Str + }) + for idx, v := range ret.expression { + if v.Str != tt.want[idx] { + t.Errorf("visitor.Build() = %v, want %v", v.Str, tt.want[idx]) + } + } + }) + } +}