Files
expr/checker/checker_test.go
T
a60d994e25 Improve Performance of the Type System (#824)
* export the checker and ensure no methods or fields are exported

* make Checker reusable

* add PatchAndCheck method to Checker, and reuse it in visitors

* avoid unnecessary map and consolidate structs

* make all Nature methods use pointer-receiver

* added cache for type and nature information

* optimize type and nature checks

* more aggressively cache some partial results and add cheap checks

* cache reused values

* reduce copying in Nature by breaking down optional fields

* enforce use of cache

* improve ergonomics of FromType and NatureOf

* improve struct method fetching

* make cache private to Nature

* first iteration improving struct reflect caching

* cleaned up struct fields data

* improve caching of functions data

* simplify checker code

* simplify and remove dead code

* add checker benchmarks

* address PR comments and minor improvements

* rename from SetCache to Bind

* make the benchmarks more fair

* fix benchmark option not being applied

* move Cache to be argument instead of field

* merge FuncData into Optional

* rename Optional to TypeData

* simplify struct logic

* cache builtin numeric types

---------

Co-authored-by: Anton Medvedev <[email protected]>
2025-09-18 14:38:27 +02:00

1160 lines
24 KiB
Go

package checker_test
import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"testing"
"github.com/expr-lang/expr/internal/testify/assert"
"github.com/expr-lang/expr/internal/testify/require"
"github.com/expr-lang/expr/types"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/checker"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/parser"
"github.com/expr-lang/expr/test/mock"
)
func TestCheck(t *testing.T) {
var tests = []struct {
input string
}{
{"nil == nil"},
{"nil == IntPtr"},
{"nil == nil"},
{"nil in ArrayOfFoo"},
{"!Bool"},
{"!BoolPtr == Bool"},
{"'a' == 'b' + 'c'"},
{"'foo' contains 'bar'"},
{"'foo' endsWith 'bar'"},
{"'foo' startsWith 'bar'"},
{"(1 == 1) || (String matches Any)"},
{"Int % Int > 1"},
{"Int + Int + Int > 0"},
{"Int == Any"},
{"Int in Int..Int"},
{"IntPtrPtr + 1 > 0"},
{"1 + 2 + Int64 > 0"},
{"Int64 % 1 > 0"},
{"IntPtr == Int"},
{"FloatPtr == 1 + 2."},
{"1 + 2 + Float + 3 + 4 < 0"},
{"1 + Int + Float == 0.5"},
{"-1 + +1 == 0"},
{"1 / 2 == 0"},
{"2**3 + 1 != 0"},
{"2^3 + 1 != 0"},
{"Float == 1"},
{"Float < 1.0"},
{"Float <= 1.0"},
{"Float > 1.0"},
{"Float >= 1.0"},
{`"a" < "b"`},
{"String + (true ? String : String) == ''"},
{"(Any ? nil : '') == ''"},
{"(Any ? 0 : nil) == 0"},
{"(Any ? nil : nil) == nil"},
{"!(Any ? Foo : Foo.Bar).Anything"},
{"Int in ArrayOfInt"},
{"Int not in ArrayOfAny"},
{"String in ArrayOfAny"},
{"String in ArrayOfString"},
{"String in Foo"},
{"String in MapOfFoo"},
{"String matches 'ok'"},
{"String matches Any"},
{"String not matches Any"},
{"StringPtr == nil"},
{"[1, 2, 3] == []"},
{"len([]) > 0"},
{"Any matches Any"},
{"!Any.Things.Contains.Any"},
{"!ArrayOfAny[0].next.goes['any thing']"},
{"ArrayOfFoo[0].Bar.Baz == ''"},
{"ArrayOfFoo[0:10][0].Bar.Baz == ''"},
{"!ArrayOfAny[Any]"},
{"Bool && Any"},
{"FuncParam(true, 1, 'str')"},
{"FuncParamAny(nil)"},
{"!Fast(Any, String)"},
{"Foo.Method().Baz == ''"},
{"Foo.Bar == MapOfAny.id.Bar"},
{"Foo.Bar.Baz == ''"},
{"MapOfFoo['any'].Bar.Baz == ''"},
{"Func() == 0"},
{"FuncFoo(Foo) > 1"},
{"Any() > 0"},
{"Embed.EmbedString == ''"},
{"EmbedString == ''"},
{"EmbedMethod(0) == ''"},
{"Embed.EmbedMethod(0) == ''"},
{"Embed.EmbedString == ''"},
{"EmbedString == ''"},
{"{id: Foo.Bar.Baz, 'str': String} == {}"},
{"Variadic(0, 1, 2) || Variadic(0)"},
{"count(1..30, {# % 3 == 0}) > 0"},
{"map(1..3, {#}) == [1,2,3]"},
{"map(1..3, #index) == [0,1,2]"},
{"map(filter(ArrayOfFoo, {.Bar.Baz != ''}), {.Bar}) == []"},
{"filter(Any, {.AnyMethod()})[0] == ''"},
{"Time == Time"},
{"Any == Time"},
{"Any != Time"},
{"Any > Time"},
{"Any >= Time"},
{"Any < Time"},
{"Any <= Time"},
{"Any - Time > Duration"},
{"Any == Any"},
{"Any != Any"},
{"Any > Any"},
{"Any >= Any"},
{"Any < Any"},
{"Any <= Any"},
{"Any - Any < Duration"},
{"Time == Any"},
{"Time != Any"},
{"Time > Any"},
{"Time >= Any"},
{"Time < Any"},
{"Time <= Any"},
{"Time - Any == Duration"},
{"Time + Duration == Time"},
{"Duration + Time == Time"},
{"Duration + Any == Time"},
{"Any + Duration == Time"},
{"Any.A?.B == nil"},
{"(Any.Bool ?? Bool) > 0"},
{"Bool ?? Bool"},
{"let foo = 1; foo == 1"},
{"(Embed).EmbedPointerEmbedInt > 0"},
}
c := new(checker.Checker)
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
var err error
tree, err := parser.Parse(tt.input)
require.NoError(t, err)
config := conf.New(mock.Env{})
expr.AsBool()(config)
_, err = c.Check(tree, config)
assert.NoError(t, err)
})
}
}
func TestCheck_error(t *testing.T) {
errorTests := []struct{ code, err string }{
{
`Foo.Bar.Not`,
`
type mock.Bar has no field Not (1:9)
| Foo.Bar.Not
| ........^
`,
},
{
`Noo`,
`
unknown name Noo (1:1)
| Noo
| ^
`,
},
{
`Foo()`, `
mock.Foo is not callable (1:1)
| Foo()
| ^
`,
},
{
`Foo['bar']`, `
type mock.Foo has no field bar (1:4)
| Foo['bar']
| ...^
`,
},
{
`Foo.Method(42)`,
`
too many arguments to call Method (1:5)
| Foo.Method(42)
| ....^
`,
},
{`Foo.Bar()`, `
mock.Bar is not callable (1:5)
| Foo.Bar()
| ....^
`,
},
{`Foo.Bar.Not()`, `
type mock.Bar has no method Not (1:9)
| Foo.Bar.Not()
| ........^
`,
},
{`ArrayOfFoo[0].Not`, `
type mock.Foo has no field Not (1:15)
| ArrayOfFoo[0].Not
| ..............^
`,
},
{`ArrayOfFoo[Not]`, `
unknown name Not (1:12)
| ArrayOfFoo[Not]
| ...........^
`,
},
{`Not[0]`, `
unknown name Not (1:1)
| Not[0]
| ^
`,
},
{`Not.Bar`, `
unknown name Not (1:1)
| Not.Bar
| ^
`,
},
{`ArrayOfFoo.Not`, `
array elements can only be selected using an integer (got string) (1:12)
| ArrayOfFoo.Not
| ...........^
`,
},
{`FuncParam(true)`, `
not enough arguments to call FuncParam (1:1)
| FuncParam(true)
| ^
`,
},
{`MapOfFoo['str'].Not`, `
type mock.Foo has no field Not (1:17)
| MapOfFoo['str'].Not
| ................^
`,
},
{`Bool && IntPtr`, `
invalid operation: && (mismatched types bool and int) (1:6)
| Bool && IntPtr
| .....^
`,
},
{`No ? Any.Bool : Any.Not`, `
unknown name No (1:1)
| No ? Any.Bool : Any.Not
| ^
`,
},
{`Any.Cond ? No : Any.Not`, `
unknown name No (1:12)
| Any.Cond ? No : Any.Not
| ...........^
`,
},
{`Any.Cond ? Any.Bool : No`, `
unknown name No (1:23)
| Any.Cond ? Any.Bool : No
| ......................^
`,
},
{`MapOfAny ? Any : Any`, `
non-bool expression (type map[string]interface {}) used as condition (1:1)
| MapOfAny ? Any : Any
| ^
`,
},
{`String matches Int`, `
invalid operation: matches (mismatched types string and int) (1:8)
| String matches Int
| .......^
`,
},
{`Int matches String`, `
invalid operation: matches (mismatched types int and string) (1:5)
| Int matches String
| ....^
`,
},
{`String contains Int`, `
invalid operation: contains (mismatched types string and int) (1:8)
| String contains Int
| .......^
`,
},
{`Int contains String`, `
invalid operation: contains (mismatched types int and string) (1:5)
| Int contains String
| ....^
`,
},
{`!Not`, `
unknown name Not (1:2)
| !Not
| .^
`,
},
{`Not == Any`, `
unknown name Not (1:1)
| Not == Any
| ^
`,
},
{`[Not]`, `
unknown name Not (1:2)
| [Not]
| .^
`,
},
{`{id: Not}`, `
unknown name Not (1:6)
| {id: Not}
| .....^
`,
},
{`(nil).Foo`, `
type nil has no field Foo (1:7)
| (nil).Foo
| ......^
`,
},
{`(nil)['Foo']`, `
type nil has no field Foo (1:6)
| (nil)['Foo']
| .....^
`,
},
{`1 and false`, `
invalid operation: and (mismatched types int and bool) (1:3)
| 1 and false
| ..^
`,
},
{`true or 0`, `
invalid operation: or (mismatched types bool and int) (1:6)
| true or 0
| .....^
`,
},
{`not IntPtr`, `
invalid operation: not (mismatched type int) (1:1)
| not IntPtr
| ^
`,
},
{`len(Not)`, `
unknown name Not (1:5)
| len(Not)
| ....^
`,
},
{`Int < Bool`, `
invalid operation: < (mismatched types int and bool) (1:5)
| Int < Bool
| ....^
`,
},
{`Int > Bool`, `
invalid operation: > (mismatched types int and bool) (1:5)
| Int > Bool
| ....^
`,
},
{`Int >= Bool`, `
invalid operation: >= (mismatched types int and bool) (1:5)
| Int >= Bool
| ....^
`,
},
{`Int <= Bool`, `
invalid operation: <= (mismatched types int and bool) (1:5)
| Int <= Bool
| ....^
`,
},
{`Int + Bool`, `
invalid operation: + (mismatched types int and bool) (1:5)
| Int + Bool
| ....^
`,
},
{`Int - Bool`, `
invalid operation: - (mismatched types int and bool) (1:5)
| Int - Bool
| ....^
`,
},
{`Int * Bool`, `
invalid operation: * (mismatched types int and bool) (1:5)
| Int * Bool
| ....^
`,
},
{`Int / Bool`, `
invalid operation: / (mismatched types int and bool) (1:5)
| Int / Bool
| ....^
`,
},
{`Int % Bool`, `
invalid operation: % (mismatched types int and bool) (1:5)
| Int % Bool
| ....^
`,
},
{`Int ** Bool`, `
invalid operation: ** (mismatched types int and bool) (1:5)
| Int ** Bool
| ....^
`,
},
{`Int .. Bool`, `
invalid operation: .. (mismatched types int and bool) (1:5)
| Int .. Bool
| ....^
`,
},
{`Any > Foo`, `
invalid operation: > (mismatched types interface {} and mock.Foo) (1:5)
| Any > Foo
| ....^
`,
},
{`NilFn() and BoolFn()`, `
func NilFn doesn't return value (1:1)
| NilFn() and BoolFn()
| ^
`,
},
{`'str' in String`, `
invalid operation: in (mismatched types string and string) (1:7)
| 'str' in String
| ......^
`,
},
{`1 in Foo`, `
invalid operation: in (mismatched types int and mock.Foo) (1:3)
| 1 in Foo
| ..^
`,
},
{`1 + ''`, `
invalid operation: + (mismatched types int and string) (1:3)
| 1 + ''
| ..^
`,
},
{`all(ArrayOfFoo, {#.Method() < 0})`, `
invalid operation: < (mismatched types mock.Bar and int) (1:29)
| all(ArrayOfFoo, {#.Method() < 0})
| ............................^
`,
},
{`Variadic()`, `
not enough arguments to call Variadic (1:1)
| Variadic()
| ^
`,
},
{`Variadic(0, '')`, `
cannot use string as argument (type int) to call Variadic (1:13)
| Variadic(0, '')
| ............^
`,
},
{`count(1, {#})`, `
builtin count takes only array (got int) (1:7)
| count(1, {#})
| ......^
`,
},
{`count(ArrayOfInt, {#})`, `
predicate should return boolean (got int) (1:19)
| count(ArrayOfInt, {#})
| ..................^
`,
},
{`all(ArrayOfInt, {# + 1})`, `
predicate should return boolean (got int) (1:17)
| all(ArrayOfInt, {# + 1})
| ................^
`,
},
{`filter(ArrayOfFoo, {.Bar.Baz})`, `
predicate should return boolean (got string) (1:20)
| filter(ArrayOfFoo, {.Bar.Baz})
| ...................^
`,
},
{`find(ArrayOfFoo, {.Bar.Baz})`, `
predicate should return boolean (got string) (1:18)
| find(ArrayOfFoo, {.Bar.Baz})
| .................^
`,
},
{`map(1, {2})`, `
builtin map takes only array (got int) (1:5)
| map(1, {2})
| ....^
`,
},
{`ArrayOfFoo[Foo]`, `
array elements can only be selected using an integer (got mock.Foo) (1:12)
| ArrayOfFoo[Foo]
| ...........^
`,
},
{`ArrayOfFoo[Bool:]`, `
non-integer slice index bool (1:12)
| ArrayOfFoo[Bool:]
| ...........^
`,
},
{`ArrayOfFoo[1:Bool]`, `
non-integer slice index bool (1:14)
| ArrayOfFoo[1:Bool]
| .............^
`,
},
{`Bool[:]`, `
cannot slice bool (1:5)
| Bool[:]
| ....^
`,
},
{`FuncTooManyReturns()`, `
func FuncTooManyReturns returns more then two values (1:1)
| FuncTooManyReturns()
| ^
`,
},
{`len(42)`, `
invalid argument for len (type int) (1:1)
| len(42)
| ^
`,
},
{`any(42, {#})`, `
builtin any takes only array (got int) (1:5)
| any(42, {#})
| ....^
`,
},
{`filter(42, {#})`, `
builtin filter takes only array (got int) (1:8)
| filter(42, {#})
| .......^
`,
},
{`MapOfAny[0]`, `
cannot use int to get an element from map[string]interface {} (1:10)
| MapOfAny[0]
| .........^
`,
},
{`1 /* one */ + "2"`, `
invalid operation: + (mismatched types int and string) (1:13)
| 1 /* one */ + "2"
| ............^
`,
},
{`FuncTyped(42)`, `
cannot use int as argument (type string) to call FuncTyped (1:11)
| FuncTyped(42)
| ..........^
`,
},
{`.0 in MapOfFoo`, `
cannot use float64 as type string in map key (1:4)
| .0 in MapOfFoo
| ...^
`,
},
{`1/2 in MapIntAny`, `
cannot use float64 as type int in map key (1:5)
| 1/2 in MapIntAny
| ....^
`,
},
{`0.5 in ArrayOfFoo`, `
cannot use float64 as type *mock.Foo in array (1:5)
| 0.5 in ArrayOfFoo
| ....^
`,
},
{`repeat("0", 1/0)`, `
cannot use float64 as argument (type int) to call repeat (1:14)
| repeat("0", 1/0)
| .............^
`,
},
{`let map = 42; map`, `
cannot redeclare builtin map (1:5)
| let map = 42; map
| ....^
`,
},
{`let len = 42; len`, `
cannot redeclare builtin len (1:5)
| let len = 42; len
| ....^
`,
},
{`let Float = 42; Float`, `
cannot redeclare Float (1:5)
| let Float = 42; Float
| ....^
`,
},
{`let foo = 1; let foo = 2; foo`, `
cannot redeclare variable foo (1:18)
| let foo = 1; let foo = 2; foo
| .................^
`,
},
{`map(1..9, #unknown)`, `
unknown pointer #unknown (1:11)
| map(1..9, #unknown)
| ..........^
`,
},
{`42 in ["a", "b", "c"]`, `
cannot use int as type string in array (1:4)
| 42 in ["a", "b", "c"]
| ...^
`,
},
{`"foo" matches "[+"`, `
error parsing regexp: missing closing ]: ` + "`[+`" + ` (1:7)
| "foo" matches "[+"
| ......^
`,
},
{`get(false, 2)`, `
type bool does not support indexing (1:5)
| get(false, 2)
| ....^
`,
},
{`get(1..2, 0.5)`, `
non-integer slice index float64 (1:11)
| get(1..2, 0.5)
| ..........^`,
},
{`trimPrefix(nil)`, `
cannot use nil as argument (type string) to call trimPrefix (1:12)
| trimPrefix(nil)
| ...........^
`,
},
{`1..3 | filter(# > 1) | filter(# == "str")`,
`
invalid operation: == (mismatched types int and string) (1:33)
| 1..3 | filter(# > 1) | filter(# == "str")
| ................................^
`,
},
{`1..3 | map("str") | filter(# > 1)`,
`
invalid operation: > (mismatched types string and int) (1:30)
| 1..3 | map("str") | filter(# > 1)
| .............................^
`,
},
{
`1; 2 + true; 3`,
`
invalid operation: + (mismatched types int and bool) (1:6)
| 1; 2 + true; 3
| .....^
`,
},
}
c := new(checker.Checker)
for _, tt := range errorTests {
t.Run(tt.code, func(t *testing.T) {
tree, err := parser.Parse(tt.code)
require.NoError(t, err)
_, err = c.Check(tree, conf.New(mock.Env{}))
if err == nil {
err = fmt.Errorf("<nil>")
}
assert.Equal(t, strings.Trim(tt.err, "\n"), err.Error())
})
}
}
func TestCheck_FloatVsInt(t *testing.T) {
tree, err := parser.Parse(`Int + Float`)
require.NoError(t, err)
typ, err := checker.Check(tree, conf.New(mock.Env{}))
assert.NoError(t, err)
assert.Equal(t, typ.Kind(), reflect.Float64)
}
func TestCheck_IntSums(t *testing.T) {
tree, err := parser.Parse(`Uint32 + Int32`)
require.NoError(t, err)
typ, err := checker.Check(tree, conf.New(mock.Env{}))
assert.NoError(t, err)
assert.Equal(t, typ.Kind(), reflect.Int)
}
func TestVisitor_ConstantNode(t *testing.T) {
tree, err := parser.Parse(`re("[a-z]")`)
require.NoError(t, err)
regexValue := regexp.MustCompile("[a-z]")
constNode := &ast.ConstantNode{Value: regexValue}
ast.Patch(&tree.Node, constNode)
_, err = checker.Check(tree, nil)
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(regexValue), tree.Node.Type())
}
func TestCheck_AsBool(t *testing.T) {
tree, err := parser.Parse(`1+2`)
require.NoError(t, err)
config := &conf.Config{}
expr.AsBool()(config)
_, err = checker.Check(tree, config)
assert.Error(t, err)
assert.Equal(t, "expected bool, but got int", err.Error())
}
func TestCheck_AsInt64(t *testing.T) {
tree, err := parser.Parse(`true`)
require.NoError(t, err)
config := &conf.Config{}
expr.AsInt64()(config)
_, err = checker.Check(tree, config)
assert.Error(t, err)
assert.Equal(t, "expected int64, but got bool", err.Error())
}
func TestCheck_TaggedFieldName(t *testing.T) {
tree, err := parser.Parse(`foo.bar`)
require.NoError(t, err)
config := conf.CreateNew()
expr.Env(struct {
x struct {
y bool `expr:"bar"`
} `expr:"foo"`
}{})(config)
expr.AsBool()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_NoConfig(t *testing.T) {
tree, err := parser.Parse(`any`)
require.NoError(t, err)
_, err = checker.Check(tree, conf.CreateNew())
assert.NoError(t, err)
}
func TestCheck_AllowUndefinedVariables(t *testing.T) {
type Env struct {
A int
}
tree, err := parser.Parse(`Any + fn()`)
require.NoError(t, err)
config := conf.New(Env{})
expr.AllowUndefinedVariables()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_AllowUndefinedVariables_DefaultType(t *testing.T) {
env := map[string]bool{}
tree, err := parser.Parse(`Any`)
require.NoError(t, err)
config := conf.New(env)
expr.AllowUndefinedVariables()(config)
expr.AsBool()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_AllowUndefinedVariables_OptionalChaining(t *testing.T) {
type Env struct{}
tree, err := parser.Parse("Not?.A.B == nil")
require.NoError(t, err)
config := conf.New(Env{})
expr.AllowUndefinedVariables()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_PointerNode(t *testing.T) {
_, err := checker.Check(&parser.Tree{Node: &ast.PointerNode{}}, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot use pointer accessor outside predicate")
}
func TestCheck_TypeWeights(t *testing.T) {
types := map[string]any{
"Uint": uint(1),
"Uint8": uint8(2),
"Uint16": uint16(3),
"Uint32": uint32(4),
"Uint64": uint64(5),
"Int": 6,
"Int8": int8(7),
"Int16": int16(8),
"Int32": int32(9),
"Int64": int64(10),
"Float32": float32(11),
"Float64": float64(12),
}
c := new(checker.Checker)
for a := range types {
for b := range types {
tree, err := parser.Parse(fmt.Sprintf("%s + %s", a, b))
require.NoError(t, err)
config := conf.New(types)
_, err = c.Check(tree, config)
require.NoError(t, err)
}
}
}
func TestCheck_works_with_nil_types(t *testing.T) {
env := map[string]any{
"null": nil,
}
tree, err := parser.Parse("null")
require.NoError(t, err)
_, err = checker.Check(tree, conf.New(env))
require.NoError(t, err)
}
func TestCheck_cast_to_expected_works_with_interface(t *testing.T) {
t.Run("float64", func(t *testing.T) {
type Env struct {
Any any
}
tree, err := parser.Parse("Any")
require.NoError(t, err)
config := conf.New(Env{})
expr.AsFloat64()(config)
expr.AsAny()(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
})
t.Run("kind", func(t *testing.T) {
env := map[string]any{
"Any": any("foo"),
}
tree, err := parser.Parse("Any")
require.NoError(t, err)
config := conf.New(env)
expr.AsKind(reflect.String)(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
})
}
func TestCheck_operator_in_works_with_interfaces(t *testing.T) {
tree, err := parser.Parse(`'Tom' in names`)
require.NoError(t, err)
config := conf.New(nil)
expr.AllowUndefinedVariables()(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
}
func TestCheck_Function_types_are_checked(t *testing.T) {
add := expr.Function(
"add",
func(p ...any) (any, error) {
out := 0
for _, each := range p {
out += each.(int)
}
return out, nil
},
new(func(int) int),
new(func(int, int) int),
new(func(int, int, int) int),
new(func(...int) int),
)
config := conf.CreateNew()
add(config)
c := new(checker.Checker)
tests := []string{
"add(1)",
"add(1, 2)",
"add(1, 2, 3)",
"add(1, 2, 3, 4)",
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
tree, err := parser.Parse(test)
require.NoError(t, err)
_, err = c.Check(tree, config)
require.NoError(t, err)
require.Equal(t, reflect.Int, tree.Node.Type().Kind())
})
}
t.Run("errors", func(t *testing.T) {
tree, err := parser.Parse("add(1, '2')")
require.NoError(t, err)
_, err = c.Check(tree, config)
require.Error(t, err)
require.Equal(t, "cannot use string as argument (type int) to call add (1:8)\n | add(1, '2')\n | .......^", err.Error())
})
}
func TestCheck_Function_without_types(t *testing.T) {
add := expr.Function(
"add",
func(p ...any) (any, error) {
out := 0
for _, each := range p {
out += each.(int)
}
return out, nil
},
)
tree, err := parser.Parse("add(1, 2, 3)")
require.NoError(t, err)
config := conf.CreateNew()
add(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
require.Equal(t, reflect.Interface, tree.Node.Type().Kind())
}
func TestCheck_dont_panic_on_nil_arguments_for_builtins(t *testing.T) {
tests := []string{
"len(nil)",
"abs(nil)",
"int(nil)",
"float(nil)",
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
tree, err := parser.Parse(test)
require.NoError(t, err)
_, err = checker.Check(tree, conf.New(nil))
require.Error(t, err)
})
}
}
func TestCheck_do_not_override_params_for_functions(t *testing.T) {
env := map[string]any{
"foo": func(p string) string {
return "foo"
},
}
config := conf.New(env)
expr.Function(
"bar",
func(p ...any) (any, error) {
return p[0].(string), nil
},
new(func(string) string),
)(config)
config.Check()
t.Run("func from env", func(t *testing.T) {
tree, err := parser.Parse("foo(1)")
require.NoError(t, err)
_, err = checker.Check(tree, config)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot use int as argument")
})
t.Run("func from function", func(t *testing.T) {
tree, err := parser.Parse("bar(1)")
require.NoError(t, err)
_, err = checker.Check(tree, config)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot use int as argument")
})
}
func TestCheck_env_keyword(t *testing.T) {
env := map[string]any{
"num": 42,
"str": "foo",
"name": "str",
}
tests := []struct {
input string
want reflect.Kind
}{
{`$env['str']`, reflect.String},
{`$env['num']`, reflect.Int},
{`$env[name]`, reflect.Interface},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
tree, err := parser.Parse(test.input)
require.NoError(t, err)
rtype, err := c.Check(tree, conf.New(env))
require.NoError(t, err)
require.True(t, rtype.Kind() == test.want, fmt.Sprintf("expected %s, got %s", test.want, rtype.Kind()))
})
}
}
func TestCheck_builtin_without_call(t *testing.T) {
tests := []struct {
input string
err string
}{
{`len + 1`, "invalid operation: + (mismatched types func(...interface {}) (interface {}, error) and int) (1:5)\n | len + 1\n | ....^"},
{`string.A`, "type func(interface {}) string has no field A (1:8)\n | string.A\n | .......^"},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
tree, err := parser.Parse(test.input)
require.NoError(t, err)
_, err = c.Check(tree, conf.New(nil))
require.Error(t, err)
require.Equal(t, test.err, err.Error())
})
}
}
func TestCheck_EmbeddedInterface(t *testing.T) {
t.Run("embedded interface lookup returns compile-error not panic", func(t *testing.T) {
type Env struct {
context.Context
Country string
}
type Wrapper struct {
Ctx Env
}
config := conf.New(Wrapper{
Ctx: Env{
Context: context.Background(),
Country: "TR",
},
})
expr.WithContext("Ctx")(config)
_, err := checker.ParseCheck("Ctx.C", config)
require.Error(t, err)
require.Contains(t, err.Error(), "has no field C")
})
}
func TestCheck_types(t *testing.T) {
env := types.Map{
"foo": types.Map{
"bar": types.Map{
"baz": types.String,
types.Extra: types.String,
},
},
"arr": types.Array(types.Map{
"value": types.String,
}),
types.Extra: types.Any,
}
noerr := "no error"
tests := []struct {
code string
err string
}{
{`unknown`, noerr},
{`[unknown + 42, another_unknown + "foo"]`, noerr},
{`foo.bar.baz > 0`, `invalid operation: > (mismatched types string and int)`},
{`foo.unknown.baz`, `unknown field unknown (1:5)`},
{`foo.bar.unknown`, noerr},
{`foo.bar.unknown + 42`, `invalid operation: + (mismatched types string and int)`},
{`[foo] | map(.unknown)`, `unknown field unknown`},
{`[foo] | map(.bar) | filter(.baz)`, `predicate should return boolean (got string)`},
{`arr | filter(.value > 0)`, `invalid operation: > (mismatched types string and int)`},
{`arr | filter(.value contains "a") | filter(.value == 0)`, `invalid operation: == (mismatched types string and int)`},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.code, func(t *testing.T) {
tree, err := parser.Parse(test.code)
require.NoError(t, err)
config := conf.New(env)
_, err = c.Check(tree, config)
if test.err == noerr {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), test.err)
}
})
}
}