From 5d7f2d0f6b1c30655980933123be2813e769eee7 Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Sat, 10 Dec 2022 16:06:42 -0800 Subject: [PATCH] fix: empty array bug and unquoted typechecking --- expr.go | 8 ++++---- interpreter.go | 3 +++ interpreter_test.go | 4 +++- typecheck.go | 34 ++++++++++++++++++++++++++++++---- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/expr.go b/expr.go index f5fab66..cd8f5f4 100644 --- a/expr.go +++ b/expr.go @@ -4,7 +4,7 @@ package mexpr // Parse an expression and return the abstract syntax tree. If `types` is // passed, it should be a set of representative example values for the input // which will be used to type check the expression against. -func Parse(expression string, types map[string]interface{}) (*Node, Error) { +func Parse(expression string, types map[string]interface{}, options ...InterpreterOption) (*Node, Error) { l := NewLexer(expression) p := NewParser(l) ast, err := p.Parse() @@ -12,7 +12,7 @@ func Parse(expression string, types map[string]interface{}) (*Node, Error) { return nil, err } if types != nil { - if err := TypeCheck(ast, types); err != nil { + if err := TypeCheck(ast, types, options...); err != nil { return nil, err } } @@ -21,8 +21,8 @@ func Parse(expression string, types map[string]interface{}) (*Node, Error) { // TypeCheck will take a parsed AST and type check against the given input // structure with representative example values. -func TypeCheck(ast *Node, types map[string]interface{}) Error { - i := NewTypeChecker(ast) +func TypeCheck(ast *Node, types map[string]interface{}, options ...InterpreterOption) Error { + i := NewTypeChecker(ast, options...) return i.Run(types) } diff --git a/interpreter.go b/interpreter.go index a0a3e95..cc3bc28 100644 --- a/interpreter.go +++ b/interpreter.go @@ -430,6 +430,9 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) { return nil, err } results := []any{} + if resultLeft == nil { + return nil, nil + } for _, item := range resultLeft.([]any) { resultRight, _ := i.run(ast.Right, item) if i.strict && err != nil { diff --git a/interpreter_test.go b/interpreter_test.go index 9f7831c..c6b6eb7 100644 --- a/interpreter_test.go +++ b/interpreter_test.go @@ -81,6 +81,7 @@ func TestInterpreter(t *testing.T) { {expr: `@.foo + 1`, opts: []InterpreterOption{UnquotedStrings, StrictMode}, err: "cannot get foo"}, {expr: `foo.bar == bar`, opts: []InterpreterOption{UnquotedStrings}, output: false}, {expr: `foo.bar == bar`, skipTC: true, opts: []InterpreterOption{UnquotedStrings}, input: `{"foo": {}}`, output: false}, + {expr: `foo.bar == baz`, opts: []InterpreterOption{UnquotedStrings}, input: `{"foo": {"bar": "baz"}}`, output: true}, // Identifier / fields {expr: "foo", input: `{"foo": 1.0}`, output: 1.0}, {expr: "foo.bar.baz", input: `{"foo": {"bar": {"baz": 1.0}}}`, output: 1.0}, @@ -137,6 +138,7 @@ func TestInterpreter(t *testing.T) { {expr: `items where id > 3 where labels contains "foo"`, input: `{"items": [{"id": 1, "labels": ["foo"]}, {"id": 3}, {"id": 5, "labels": ["foo"]}, {"id": 7}]}`, output: []interface{}{map[string]interface{}{"id": 5.0, "labels": []interface{}{"foo"}}}}, {expr: `(items where id > 3).length == 2`, input: `{"items": [{"id": 1}, {"id": 3}, {"id": 5}, {"id": 7}]}`, output: true}, {expr: `not (items where id > 3)`, input: `{"items": [{"id": 1}, {"id": 3}, {"id": 5}, {"id": 7}]}`, output: false}, + {expr: `items where id > 3`, input: `{}`, skipTC: true, output: nil}, // Order of operations {expr: "1 + 2 + 3", output: 6.0}, {expr: "1 + 2 * 3", output: 7.0}, @@ -181,7 +183,7 @@ func TestInterpreter(t *testing.T) { // Skip type check types = nil } - ast, err := Parse(tc.expr, types) + ast, err := Parse(tc.expr, types, tc.opts...) if tc.err != "" { if err != nil { diff --git a/typecheck.go b/typecheck.go index 82fe5f6..ac67088 100644 --- a/typecheck.go +++ b/typecheck.go @@ -85,14 +85,26 @@ type TypeChecker interface { } // NewTypeChecker returns a type checker for the given AST. -func NewTypeChecker(ast *Node) TypeChecker { +func NewTypeChecker(ast *Node, options ...InterpreterOption) TypeChecker { + unquoted := false + + for _, opt := range options { + switch opt { + case UnquotedStrings: + unquoted = true + } + } + return &typeChecker{ - ast: ast, + ast: ast, + unquoted: unquoted, } } type typeChecker struct { - ast *Node + ast *Node + prevFieldSelect bool + unquoted bool } func (i *typeChecker) Run(value any) Error { @@ -113,6 +125,9 @@ func (i *typeChecker) runBoth(ast *Node, value any) (*schema, *schema, Error) { } func (i *typeChecker) run(ast *Node, value any) (*schema, Error) { + fromSelect := i.prevFieldSelect + i.prevFieldSelect = false + switch ast.Type { case NodeIdentifier: switch ast.Value.(string) { @@ -126,12 +141,17 @@ func (i *typeChecker) run(ast *Node, value any) (*schema, Error) { case "lower", "upper": return schemaString, nil } + errValue := value if s, ok := value.(*schema); ok { if v, ok := s.properties[ast.Value.(string)]; ok { return v, nil } + keys := []string{} + for k := range s.properties { + keys = append(keys, k) + } + errValue = "map with keys [" + strings.Join(keys, ", ") + "]" } - errValue := value if m, ok := value.(map[string]any); ok { if v, ok := m[ast.Value.(string)]; ok { return getSchema(v), nil @@ -152,12 +172,18 @@ func (i *typeChecker) run(ast *Node, value any) (*schema, Error) { } errValue = "map with keys [" + strings.Join(keys, ", ") + "]" } + if i.unquoted && !fromSelect { + // Identifiers not found in the map are treated as strings, but only if + // the previous item was not a `.` like `obj.field`. + return schemaString, nil + } return nil, NewError(ast.Offset, ast.Length, "no property %v in %v", ast.Value, errValue) case NodeFieldSelect: leftType, err := i.run(ast.Left, value) if err != nil { return nil, err } + i.prevFieldSelect = true return i.run(ast.Right, leftType) case NodeArrayIndex: leftType, rightType, err := i.runBoth(ast, value)