Add type checks

This commits implements more advance type checks in strict mode.
To enter strict mode set variable types with expr.Define() or
expr.With() functions.
This commit is contained in:
Anton Medvedev
2018-08-08 20:09:07 +03:00
parent cd52a4640b
commit 894c3c96b1
14 changed files with 803 additions and 167 deletions
+3 -2
View File
@@ -24,8 +24,8 @@ Inspired by
## Features
* Works with any valid Go object (structs, maps, etc)
* Compile-time checks for used variables
* Сlear error messages:
* Strict mode with type checks
* User-friendly error messages
```
unclosed "("
(boo + bar]
@@ -48,3 +48,4 @@ go get -u github.com/antonmedv/expr
## License
MIT
+4 -3
View File
@@ -1,8 +1,9 @@
package expr
package expr_test
import (
"testing"
"github.com/antonmedv/expr"
"github.com/dop251/goja"
"github.com/robertkrimen/otto"
)
@@ -30,13 +31,13 @@ func Benchmark_expr(b *testing.B) {
Marker: "test",
}
script, err := Parse(`Segments[0].Origin == "MOW" && Passengers.Adults == 2 && Marker == "test"`)
script, err := expr.Parse(`Segments[0].Origin == "MOW" && Passengers.Adults == 2 && Marker == "test"`)
if err != nil {
b.Fatal(err)
}
for n := 0; n < b.N; n++ {
Run(script, r)
expr.Run(script, r)
}
}
+35 -21
View File
@@ -42,10 +42,10 @@ For example you can pass nested structures without any modification or preparati
Cookies []Cookie
}
type Request struct {
User user
User *user
}
req := Request{User{
req := Request{&User{
Cookies: []Cookie{{"origin", "www"}},
UserAgent: "Firefox",
}}
@@ -71,39 +71,53 @@ You can also pass functions into the expression:
ok, err := expr.Eval(`"www" in Values(Request.User.Cookies)`, data)
Caching
Parsing and caching
If you planning to execute some expression lots times, it's good to compile it first and only one time:
If you planning to execute some expression lots times, it's good to parse it first and only one time:
// Precompile
node, err := expr.Parse(expression)
// Parse expression to AST.
ast, err := expr.Parse(expression)
// Run
ok, err := expr.Run(node, data)
// Run given AST
ok, err := expr.Run(ast, data)
Checking variables and functions
Strict mode
It is possible to check used variables and functions during parsing of the expression.
Expr package support strict parse mode in which some type checks performed during parsing.
To parse expression in strict mode, define all of used variables:
expression := `Request.User.UserAgent matches "Firefox" && "www" in Values(Request.User.Cookies)`
node, err := expr.Parse(expression, expr.Names("Request"), expr.Funcs("Values"))
expression := `Request.User.UserAgent matches "Firefox"`
node, err := expr.Parse(expression, expr.Define("Request", request{}))
Only `Request` and `Values` will be available inside expression, otherwise parse error.
Parse function will check used variables, accessed filed, logical operators and some other type checks.
If you try to use some undeclared variables or functions, an error will be returned during compilation:
If you try to use some undeclared variables, or access unknown field, an error will be returned during paring:
expression := `Unknown(Request.User.UserAgent)`
node, err := expr.Parse(expression, expr.Names("Request"), expr.Funcs("Values"))
expression := `Request.User.Cookies[0].Timestamp`
node, err := expr.Parse(expression, expr.Define("Request", request{}))
// err.Error():
// err: Request.User.Cookies[0].Timestamp undefined (type expr_test.cookie has no field Timestamp)
unknown func Unknown
Unknown(Request.User.UserAgent)
-------^
Also it's possible to define all used variables and functions using expr.With and struct:
type payload struct {
Request *Request
Values func(xs []Cookie) []string
}
node, err := expr.Parse(expression, expr.With(payload{}))
Or with map:
data := map[string]interface{}{
"Request": req,
"Values": func(xs []Cookie) []string {...},
}
node, err := expr.Parse(expression, expr.With(data))
Printing
+59 -9
View File
@@ -16,6 +16,7 @@ func ExampleEval() {
}
fmt.Printf("%v", output)
// Output: hello world
}
@@ -36,6 +37,7 @@ func ExampleEval_map() {
}
fmt.Printf("%v", output)
// Output: hello user
}
@@ -54,6 +56,7 @@ func ExampleEval_struct() {
}
fmt.Printf("%v", output)
// Output: 42
}
@@ -66,6 +69,7 @@ func ExampleEval_error() {
}
fmt.Printf("%v", output)
// Output: err: unclosed "("
//(boo + bar]
//----------^
@@ -80,6 +84,7 @@ func ExampleEval_matches() {
}
fmt.Printf("%v", output)
// Output: err: error parsing regexp: missing closing ): `a(`
//"a" matches "a("
//----------------^
@@ -106,6 +111,7 @@ func ExampleParse() {
}
fmt.Printf("%v", output)
// Output: true
}
@@ -130,11 +136,13 @@ func ExampleRun() {
}
fmt.Printf("%v", output)
// Output: false
}
func ExampleNames() {
_, err := expr.Parse("foo + bar + baz", expr.Names("foo", "bar"))
func ExampleDefine() {
var foo, bar int
_, err := expr.Parse("foo + bar + baz", expr.Define("foo", foo), expr.Define("bar", bar))
if err != nil {
fmt.Printf("err: %v", err)
@@ -142,21 +150,62 @@ func ExampleNames() {
}
// Output: err: unknown name baz
//foo + bar + baz
//------------^
}
func ExampleFuncs() {
_, err := expr.Parse("foo(bar(baz()))", expr.Funcs("foo", "bar"))
func ExampleWith() {
type segment struct {
Origin string
}
type passengers struct {
Adults int
}
type request struct {
Segments []*segment
Passengers *passengers
Marker string
Meta map[string]interface{}
}
code := `Segments[0].Origin == "MOW" && Passengers.Adults == 2 && Marker == "test" && Meta["accept"]`
ast, err := expr.Parse(code, expr.With(&request{}))
if err != nil {
fmt.Printf("err: %v", err)
return
}
// Output: err: unknown func baz
//foo(bar(baz()))
//--------^
r := &request{
Segments: []*segment{
{Origin: "MOW"},
},
Passengers: &passengers{
Adults: 2,
},
Marker: "test",
Meta: map[string]interface{}{"accept": true},
}
output, err := expr.Run(ast, r)
if err != nil {
fmt.Printf("err: %v", err)
return
}
fmt.Printf("%v", output)
// Output: true
}
func ExampleFuncs() {
var foo, bar func()
_, err := expr.Parse("foo(bar(baz()))", expr.Define("foo", foo), expr.Define("bar", bar))
if err != nil {
fmt.Printf("err: %v", err)
return
}
// Output: err: unknown func baz()
}
func ExampleNode() {
@@ -168,5 +217,6 @@ func ExampleNode() {
}
fmt.Printf("%v", node)
// Output: foo.bar
}
+46 -46
View File
@@ -6,10 +6,6 @@ import (
"regexp"
)
type evaluator interface {
eval(env interface{}) (interface{}, error)
}
// Eval parses and evaluates given input.
func Eval(input string, env interface{}) (interface{}, error) {
node, err := Parse(input)
@@ -27,40 +23,37 @@ func Run(node Node, env interface{}) (out interface{}, err error) {
}
}()
if e, ok := node.(evaluator); ok {
return e.eval(env)
}
return nil, fmt.Errorf("implement evaluator for %T", node)
return node.Eval(env)
}
// eval functions
func (n nilNode) eval(env interface{}) (interface{}, error) {
func (n nilNode) Eval(env interface{}) (interface{}, error) {
return nil, nil
}
func (n identifierNode) eval(env interface{}) (interface{}, error) {
func (n identifierNode) Eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n numberNode) eval(env interface{}) (interface{}, error) {
func (n numberNode) Eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n boolNode) eval(env interface{}) (interface{}, error) {
func (n boolNode) Eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n textNode) eval(env interface{}) (interface{}, error) {
func (n textNode) Eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n nameNode) eval(env interface{}) (interface{}, error) {
func (n nameNode) Eval(env interface{}) (interface{}, error) {
return extract(env, n.name)
}
func (n unaryNode) eval(env interface{}) (interface{}, error) {
val, err := Run(n.node, env)
func (n unaryNode) Eval(env interface{}) (interface{}, error) {
val, err := n.node.Eval(env)
if err != nil {
return nil, err
}
@@ -87,8 +80,8 @@ func (n unaryNode) eval(env interface{}) (interface{}, error) {
return nil, fmt.Errorf("implement unary %q operator", n.operator)
}
func (n binaryNode) eval(env interface{}) (interface{}, error) {
left, err := Run(n.left, env)
func (n binaryNode) Eval(env interface{}) (interface{}, error) {
left, err := n.left.Eval(env)
if err != nil {
return nil, err
}
@@ -103,7 +96,7 @@ func (n binaryNode) eval(env interface{}) (interface{}, error) {
return true, nil
}
right, err := Run(n.right, env)
right, err := n.right.Eval(env)
if err != nil {
return nil, err
}
@@ -119,7 +112,7 @@ func (n binaryNode) eval(env interface{}) (interface{}, error) {
}
if toBool(left) {
right, err := Run(n.right, env)
right, err := n.right.Eval(env)
if err != nil {
return nil, err
}
@@ -132,7 +125,7 @@ func (n binaryNode) eval(env interface{}) (interface{}, error) {
return false, nil
}
right, err := Run(n.right, env)
right, err := n.right.Eval(env)
if err != nil {
return nil, err
}
@@ -245,8 +238,8 @@ func makeRange(min, max int64) ([]float64, error) {
return a, nil
}
func (n matchesNode) eval(env interface{}) (interface{}, error) {
left, err := Run(n.left, env)
func (n matchesNode) Eval(env interface{}) (interface{}, error) {
left, err := n.left.Eval(env)
if err != nil {
return nil, err
}
@@ -257,7 +250,7 @@ func (n matchesNode) eval(env interface{}) (interface{}, error) {
}
}
right, err := Run(n.right, env)
right, err := n.right.Eval(env)
if err != nil {
return nil, err
}
@@ -273,34 +266,41 @@ func (n matchesNode) eval(env interface{}) (interface{}, error) {
return nil, fmt.Errorf("operator matches doesn't defined on (%T, %T): %v", left, right, n)
}
func (n propertyNode) eval(env interface{}) (interface{}, error) {
v, err := Run(n.node, env)
if err != nil {
return nil, err
}
p, err := Run(n.property, env)
func (n propertyNode) Eval(env interface{}) (interface{}, error) {
v, err := n.node.Eval(env)
if err != nil {
return nil, err
}
return extract(v, n.property)
}
func (n indexNode) Eval(env interface{}) (interface{}, error) {
v, err := n.node.Eval(env)
if err != nil {
return nil, err
}
p, err := n.index.Eval(env)
if err != nil {
return nil, err
}
return extract(v, p)
}
func (n methodNode) eval(env interface{}) (interface{}, error) {
v, err := Run(n.node, env)
func (n methodNode) Eval(env interface{}) (interface{}, error) {
v, err := n.node.Eval(env)
if err != nil {
return nil, err
}
method, err := extract(v, n.property.value)
method, err := extract(v, n.method)
if err != nil {
return nil, err
}
return call(n.property.value, method, n.arguments, env)
return call(n.method, method, n.arguments, env)
}
func (n builtinNode) eval(env interface{}) (interface{}, error) {
func (n builtinNode) Eval(env interface{}) (interface{}, error) {
if len(n.arguments) == 0 {
return nil, fmt.Errorf("missing argument to %v", n.name)
}
@@ -308,7 +308,7 @@ func (n builtinNode) eval(env interface{}) (interface{}, error) {
return nil, fmt.Errorf("too many arguments to %v: %v", n.name, n)
}
a, err := Run(n.arguments[0], env)
a, err := n.arguments[0].Eval(env)
if err != nil {
return nil, err
}
@@ -321,7 +321,7 @@ func (n builtinNode) eval(env interface{}) (interface{}, error) {
return nil, fmt.Errorf("unknown %q builtin", n.name)
}
func (n functionNode) eval(env interface{}) (interface{}, error) {
func (n functionNode) Eval(env interface{}) (interface{}, error) {
fn, err := extract(env, n.name)
if err != nil {
return nil, err
@@ -330,8 +330,8 @@ func (n functionNode) eval(env interface{}) (interface{}, error) {
return call(n.name, fn, n.arguments, env)
}
func (n conditionalNode) eval(env interface{}) (interface{}, error) {
cond, err := Run(n.cond, env)
func (n conditionalNode) Eval(env interface{}) (interface{}, error) {
cond, err := n.cond.Eval(env)
if err != nil {
return nil, err
}
@@ -342,14 +342,14 @@ func (n conditionalNode) eval(env interface{}) (interface{}, error) {
}
// Then
if toBool(cond) {
a, err := Run(n.exp1, env)
a, err := n.exp1.Eval(env)
if err != nil {
return nil, err
}
return a, nil
}
// Else
b, err := Run(n.exp2, env)
b, err := n.exp2.Eval(env)
if err != nil {
return nil, err
}
@@ -357,10 +357,10 @@ func (n conditionalNode) eval(env interface{}) (interface{}, error) {
}
func (n arrayNode) eval(env interface{}) (interface{}, error) {
func (n arrayNode) Eval(env interface{}) (interface{}, error) {
array := make([]interface{}, 0)
for _, node := range n.nodes {
val, err := Run(node, env)
val, err := node.Eval(env)
if err != nil {
return nil, err
}
@@ -369,14 +369,14 @@ func (n arrayNode) eval(env interface{}) (interface{}, error) {
return array, nil
}
func (n mapNode) eval(env interface{}) (interface{}, error) {
func (n mapNode) Eval(env interface{}) (interface{}, error) {
m := make(map[interface{}]interface{})
for _, pair := range n.pairs {
key, err := Run(pair.key, env)
key, err := pair.key.Eval(env)
if err != nil {
return nil, err
}
value, err := Run(pair.value, env)
value, err := pair.value.Eval(env)
if err != nil {
return nil, err
}
+9 -7
View File
@@ -1,10 +1,12 @@
package expr
package expr_test
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/antonmedv/expr"
)
type evalTest struct {
@@ -413,7 +415,7 @@ var evalErrorTests = []evalErrorTest{
func TestEval(t *testing.T) {
for _, test := range evalTests {
actual, err := Eval(test.input, test.env)
actual, err := expr.Eval(test.input, test.env)
if err != nil {
t.Errorf("%s:\n%v", test.input, err)
continue
@@ -426,7 +428,7 @@ func TestEval(t *testing.T) {
func TestEval_error(t *testing.T) {
for _, test := range evalErrorTests {
_, err := Eval(test.input, test.env)
_, err := expr.Eval(test.input, test.env)
if err == nil {
err = fmt.Errorf("<nil>")
}
@@ -464,12 +466,12 @@ func TestEval_complex(t *testing.T) {
}
input := `Request.User.UserAgent matches "Mozilla" && "www" in Values(Request.User.Cookies)`
node, err := Parse(input, Names("Request"), Funcs("Values"))
node, err := expr.Parse(input, expr.With(p))
if err != nil {
t.Fatal(err)
}
actual, err := Run(node, p)
actual, err := expr.Run(node, p)
if err != nil {
t.Fatal(err)
}
@@ -481,12 +483,12 @@ func TestEval_complex(t *testing.T) {
}
func TestEval_panic(t *testing.T) {
node, err := Parse("foo()")
node, err := expr.Parse("foo()")
if err != nil {
t.Fatal(err)
}
_, err = Run(node, map[string]interface{}{"foo": nil})
_, err = expr.Run(node, map[string]interface{}{"foo": nil})
if err == nil {
err = fmt.Errorf("<nil>")
}
+14 -4
View File
@@ -1,9 +1,14 @@
package expr
import "regexp"
import (
"regexp"
)
// Node represents items of abstract syntax tree.
type Node interface{}
type Node interface {
Type(table typesTable) (Type, error)
Eval(env interface{}) (interface{}, error)
}
type nilNode struct{}
@@ -46,12 +51,17 @@ type matchesNode struct {
type propertyNode struct {
node Node
property Node
property string
}
type indexNode struct {
node Node
index Node
}
type methodNode struct {
node Node
property identifierNode
method string
arguments []Node
}
+71 -56
View File
@@ -2,6 +2,7 @@ package expr
import (
"fmt"
"reflect"
"regexp"
"strconv"
"unicode/utf8"
@@ -62,16 +63,12 @@ type parser struct {
tokens []token
position int
current token
options *options
}
type options struct {
names map[string]struct{}
funcs map[string]struct{}
strict bool
types map[string]Type
}
// OptionFn for configuring parser.
type OptionFn func(p *options)
type OptionFn func(p *parser)
// Parse parses input into ast.
func Parse(input string, ops ...OptionFn) (Node, error) {
@@ -84,11 +81,11 @@ func Parse(input string, ops ...OptionFn) (Node, error) {
input: input,
tokens: tokens,
current: tokens[0],
options: &options{},
types: make(map[string]Type),
}
for _, op := range ops {
op(p.options)
op(p)
}
node, err := p.parseExpression(0)
@@ -99,28 +96,53 @@ func Parse(input string, ops ...OptionFn) (Node, error) {
if !p.isEOF() {
return nil, p.errorf("unexpected token %v", p.current)
}
if p.strict {
_, err = node.Type(p.types)
if err != nil {
return nil, err
}
}
return node, nil
}
// Names sets list of allowed names.
func Names(names ...string) OptionFn {
return func(o *options) {
set := make(map[string]struct{})
for _, name := range names {
set[name] = struct{}{}
}
o.names = set
// Define sets variable for type checks during parsing.
func Define(name string, t interface{}) OptionFn {
return func(p *parser) {
p.strict = true
p.types[name] = reflect.TypeOf(t)
}
}
// Funcs sets list of allowed function.
func Funcs(funcs ...string) OptionFn {
return func(o *options) {
set := make(map[string]struct{})
for _, fn := range funcs {
set[fn] = struct{}{}
// With sets variables for type checks during parsing.
// If struct is passed, all fields will be treated as variables.
// If map is passed, all items will be treated as variables
// (key as name, value as type).
func With(i interface{}) OptionFn {
return func(p *parser) {
p.strict = true
v := reflect.ValueOf(i)
t := reflect.TypeOf(i)
t = dereference(t)
if t == nil {
return
}
switch t.Kind() {
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
p.types[f.Name] = f.Type
}
case reflect.Map:
for _, key := range v.MapKeys() {
value := v.MapIndex(key)
if key.Kind() == reflect.String && value.IsValid() && value.CanInterface() {
p.types[key.String()] = reflect.TypeOf(value.Interface())
}
}
}
o.funcs = set
}
}
@@ -299,32 +321,9 @@ func (p *parser) parsePrimaryExpression() (Node, error) {
case "nil":
return nilNode{}, nil
default:
if p.current.is(punctuation, "(") {
if _, ok := builtins[token.value]; ok {
arguments, err := p.parseArguments()
if err != nil {
return nil, err
}
node = builtinNode{name: token.value, arguments: arguments}
} else {
if p.options.funcs != nil {
if _, ok := p.options.funcs[token.value]; !ok {
return nil, p.errorf("unknown func %v", token.value).at(token)
}
}
arguments, err := p.parseArguments()
if err != nil {
return nil, err
}
node = functionNode{name: token.value, arguments: arguments}
}
} else {
if p.options.names != nil {
if _, ok := p.options.names[token.value]; !ok {
return nil, p.errorf("unknown name %v", token.value).at(token)
}
}
node = nameNode{name: token.value}
node, err = p.parseNameExpression(token)
if err != nil {
return nil, err
}
}
@@ -363,6 +362,24 @@ func (p *parser) parsePrimaryExpression() (Node, error) {
return p.parsePostfixExpression(node)
}
func (p *parser) parseNameExpression(token token) (Node, error) {
var node Node
if p.current.is(punctuation, "(") {
arguments, err := p.parseArguments()
if err != nil {
return nil, err
}
if _, ok := builtins[token.value]; ok {
node = builtinNode{name: token.value, arguments: arguments}
} else {
node = functionNode{name: token.value, arguments: arguments}
}
} else {
node = nameNode{name: token.value}
}
return node, nil
}
func (p *parser) parseArrayExpression() (Node, error) {
nodes, err := p.parseList("array items", "[", "]")
if err != nil {
@@ -456,16 +473,14 @@ func (p *parser) parsePostfixExpression(node Node) (Node, error) {
return nil, p.errorf("expected name").at(token)
}
property := identifierNode{value: token.value}
if p.current.is(punctuation, "(") {
arguments, err := p.parseArguments()
if err != nil {
return nil, err
}
node = methodNode{node: node, property: property, arguments: arguments}
node = methodNode{node: node, method: token.value, arguments: arguments}
} else {
node = propertyNode{node: node, property: property}
node = propertyNode{node: node, property: token.value}
}
} else if token.value == "[" {
@@ -476,10 +491,10 @@ func (p *parser) parsePostfixExpression(node Node) (Node, error) {
arg, err := p.parseExpression(0)
if err != nil {
return err, nil
return nil, err
}
node = propertyNode{node: node, property: arg}
node = indexNode{node: node, index: arg}
err = p.expect(punctuation, "]")
if err != nil {
+9 -9
View File
@@ -71,27 +71,27 @@ var parseTests = []parseTest{
},
{
"foo.bar",
propertyNode{nameNode{"foo"}, identifierNode{"bar"}},
propertyNode{nameNode{"foo"}, "bar"},
},
{
"foo.not",
propertyNode{nameNode{"foo"}, identifierNode{"not"}},
propertyNode{nameNode{"foo"}, "not"},
},
{
"foo.bar()",
methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{}},
methodNode{nameNode{"foo"}, "bar", []Node{}},
},
{
"foo.not()",
methodNode{nameNode{"foo"}, identifierNode{"not"}, []Node{}},
methodNode{nameNode{"foo"}, "not", []Node{}},
},
{
`foo.bar("arg1", 2, true)`,
methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{textNode{"arg1"}, numberNode{2}, boolNode{true}}},
methodNode{nameNode{"foo"}, "bar", []Node{textNode{"arg1"}, numberNode{2}, boolNode{true}}},
},
{
"foo[3]",
propertyNode{nameNode{"foo"}, numberNode{3}},
indexNode{nameNode{"foo"}, numberNode{3}},
},
{
"true ? true : false",
@@ -103,7 +103,7 @@ var parseTests = []parseTest{
},
{
"foo.bar().foo().baz[33]",
propertyNode{propertyNode{methodNode{methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{}}, identifierNode{"foo"}, []Node{}}, identifierNode{"baz"}}, numberNode{33}},
indexNode{propertyNode{methodNode{methodNode{nameNode{"foo"}, "bar", []Node{}}, "foo", []Node{}}, "baz"}, numberNode{33}},
},
{
"+0 != -0",
@@ -123,11 +123,11 @@ var parseTests = []parseTest{
},
{
"[1].foo",
propertyNode{arrayNode{[]Node{numberNode{1}}}, identifierNode{"foo"}},
propertyNode{arrayNode{[]Node{numberNode{1}}}, "foo"},
},
{
"{foo:1}.bar",
propertyNode{mapNode{[]pairNode{{identifierNode{"foo"}, numberNode{1}}}}, identifierNode{"bar"}},
propertyNode{mapNode{[]pairNode{{identifierNode{"foo"}, numberNode{1}}}}, "bar"},
},
{
"len(foo)",
+6 -6
View File
@@ -49,15 +49,15 @@ func (n matchesNode) String() string {
}
func (n propertyNode) String() string {
switch n.property.(type) {
case identifierNode:
return fmt.Sprintf("%v.%v", n.node, n.property)
}
return fmt.Sprintf("%v[%v]", n.node, n.property)
return fmt.Sprintf("%v.%v", n.node, n.property)
}
func (n indexNode) String() string {
return fmt.Sprintf("%v[%v]", n.node, n.index)
}
func (n methodNode) String() string {
s := fmt.Sprintf("%v.%v(", n.node, n.property)
s := fmt.Sprintf("%v.%v(", n.node, n.method)
for i, a := range n.arguments {
if i != 0 {
s += ", "
+3 -3
View File
@@ -12,11 +12,11 @@ type printTest struct {
var printTests = []printTest{
{
methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{textNode{"arg1"}, numberNode{2}, boolNode{true}}},
methodNode{nameNode{"foo"}, "bar", []Node{textNode{"arg1"}, numberNode{2}, boolNode{true}}},
`foo.bar("arg1", 2, true)`,
},
{
propertyNode{propertyNode{methodNode{methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{}}, identifierNode{"foo"}, []Node{}}, identifierNode{"baz"}}, numberNode{33}},
indexNode{propertyNode{methodNode{methodNode{nameNode{"foo"}, "bar", []Node{}}, "foo", []Node{}}, "baz"}, numberNode{33}},
"foo.bar().foo().baz[33]",
},
{
@@ -24,7 +24,7 @@ var printTests = []printTest{
`{"foo": 1, (1 + 2): 2}`,
},
{
functionNode{"call", []Node{propertyNode{arrayNode{[]Node{numberNode{1}, unaryNode{"not", boolNode{true}}}}, identifierNode{"foo"}}}},
functionNode{"call", []Node{propertyNode{arrayNode{[]Node{numberNode{1}, unaryNode{"not", boolNode{true}}}}, "foo"}}},
"call([1, not true].foo)",
},
{
+316
View File
@@ -0,0 +1,316 @@
package expr
import (
"fmt"
"reflect"
)
// Type is a reflect.Type alias.
type Type = reflect.Type
type typesTable map[string]Type
var (
boolType = reflect.TypeOf(true)
numberType = reflect.TypeOf(float64(0))
textType = reflect.TypeOf("")
arrayType = reflect.TypeOf([]interface{}{})
mapType = reflect.TypeOf(map[interface{}]interface{}{})
interfaceType = reflect.TypeOf(new(interface{})).Elem()
)
func (n nilNode) Type(table typesTable) (Type, error) {
return nil, nil
}
func (n identifierNode) Type(table typesTable) (Type, error) {
return textType, nil
}
func (n numberNode) Type(table typesTable) (Type, error) {
return numberType, nil
}
func (n boolNode) Type(table typesTable) (Type, error) {
return boolType, nil
}
func (n textNode) Type(table typesTable) (Type, error) {
return textType, nil
}
func (n nameNode) Type(table typesTable) (Type, error) {
if t, ok := table[n.name]; ok {
return t, nil
}
return nil, fmt.Errorf("unknown name %v", n)
}
func (n unaryNode) Type(table typesTable) (Type, error) {
ntype, err := n.node.Type(table)
if err != nil {
return nil, err
}
switch n.operator {
case "!", "not":
if isBoolType(ntype) {
return boolType, nil
}
return nil, fmt.Errorf(`invalid operation: %v (mismatched type %v)`, n, ntype)
}
return interfaceType, nil
}
func (n binaryNode) Type(table typesTable) (Type, error) {
var err error
ltype, err := n.left.Type(table)
if err != nil {
return nil, err
}
rtype, err := n.right.Type(table)
if err != nil {
return nil, err
}
switch n.operator {
case "==", "!=":
if isComparable(ltype, rtype) {
return boolType, nil
}
return nil, fmt.Errorf(`invalid operation: %v (mismatched types %v and %v)`, n, ltype, rtype)
case "or", "||", "and", "&&":
if isBoolType(ltype) && isBoolType(rtype) {
return boolType, nil
}
return nil, fmt.Errorf(`invalid operation: %v (mismatched types %v and %v)`, n, ltype, rtype)
}
return interfaceType, nil
}
func (n matchesNode) Type(table typesTable) (Type, error) {
return boolType, nil
}
func (n propertyNode) Type(table typesTable) (Type, error) {
ntype, err := n.node.Type(table)
if err != nil {
return nil, err
}
if t, ok := fieldType(ntype, n.property); ok {
return t, nil
}
return nil, fmt.Errorf("%v undefined (type %v has no field %v)", n, ntype, n.property)
}
func (n indexNode) Type(table typesTable) (Type, error) {
ntype, err := n.node.Type(table)
if err != nil {
return nil, err
}
_, err = n.index.Type(table)
if err != nil {
return nil, err
}
if t, ok := indexType(ntype); ok {
return t, nil
}
return nil, fmt.Errorf("invalid operation: %v (type %v does not support indexing)", n, ntype)
}
func (n methodNode) Type(table typesTable) (Type, error) {
ntype, err := n.node.Type(table)
if err != nil {
return nil, err
}
for _, node := range n.arguments {
_, err := node.Type(table)
if err != nil {
return nil, err
}
}
if t, ok := fieldType(ntype, n.method); ok {
if f, ok := funcType(t); ok {
return f, nil
}
}
return nil, fmt.Errorf("%v undefined (type %v has no method %v)", n, ntype, n.method)
}
func (n builtinNode) Type(table typesTable) (Type, error) {
if _, ok := builtins[n.name]; ok {
return nil, nil
}
return nil, fmt.Errorf("%v undefined", n)
}
func (n functionNode) Type(table typesTable) (Type, error) {
for _, node := range n.arguments {
_, err := node.Type(table)
if err != nil {
return nil, err
}
}
if t, ok := table[n.name]; ok {
if f, ok := funcType(t); ok {
return f, nil
}
}
return nil, fmt.Errorf("unknown func %v", n)
}
func (n conditionalNode) Type(table typesTable) (Type, error) {
ctype, err := n.cond.Type(table)
if err != nil {
return nil, err
}
if !isBoolType(ctype) {
return nil, fmt.Errorf("non-bool %v (type %v) used as condition", n.cond, ctype)
}
_, err = n.exp1.Type(table)
if err != nil {
return nil, err
}
_, err = n.exp2.Type(table)
if err != nil {
return nil, err
}
return boolType, nil
}
func (n arrayNode) Type(table typesTable) (Type, error) {
for _, node := range n.nodes {
_, err := node.Type(table)
if err != nil {
return nil, err
}
}
return arrayType, nil
}
func (n mapNode) Type(table typesTable) (Type, error) {
for _, node := range n.pairs {
_, err := node.Type(table)
if err != nil {
return nil, err
}
}
return mapType, nil
}
func (n pairNode) Type(table typesTable) (Type, error) {
var err error
_, err = n.key.Type(table)
if err != nil {
return nil, err
}
_, err = n.value.Type(table)
if err != nil {
return nil, err
}
return nil, nil
}
// helper funcs for reflect
func isComparable(ltype Type, rtype Type) bool {
ltype = dereference(ltype)
if ltype == nil {
return true
}
rtype = dereference(rtype)
if rtype == nil {
return true
}
if canBeNumberType(ltype) && canBeNumberType(rtype) {
return true
} else if ltype.Kind() == reflect.Interface {
return true
} else if rtype.Kind() == reflect.Interface {
return true
} else if ltype == rtype {
return true
}
return false
}
func isBoolType(ntype Type) bool {
ntype = dereference(ntype)
if ntype == nil {
return false
}
switch ntype.Kind() {
case reflect.Interface:
return true
case reflect.Bool:
return true
}
return false
}
func fieldType(ntype Type, name string) (Type, bool) {
ntype = dereference(ntype)
if ntype == nil {
return nil, false
}
switch ntype.Kind() {
case reflect.Interface:
return interfaceType, true
case reflect.Struct:
if t, ok := ntype.FieldByName(name); ok {
return t.Type, true
}
case reflect.Map:
return ntype.Elem(), true
}
return nil, false
}
func indexType(ntype Type) (Type, bool) {
ntype = dereference(ntype)
if ntype == nil {
return nil, false
}
switch ntype.Kind() {
case reflect.Interface:
return interfaceType, true
case reflect.Map, reflect.Array, reflect.Slice:
return ntype.Elem(), true
}
return nil, false
}
func funcType(ntype Type) (Type, bool) {
ntype = dereference(ntype)
if ntype == nil {
return nil, false
}
switch ntype.Kind() {
case reflect.Interface:
return interfaceType, true
case reflect.Func:
return ntype, true
}
return nil, false
}
func dereference(ntype Type) Type {
if ntype == nil {
return nil
}
if ntype.Kind() == reflect.Ptr {
ntype = dereference(ntype.Elem())
}
return ntype
}
+220
View File
@@ -0,0 +1,220 @@
package expr_test
import (
"fmt"
"strings"
"testing"
"github.com/antonmedv/expr"
)
type typeTest string
type typeErrorTest struct {
input string
err string
}
var typeTests = []typeTest{
"Foo.Bar.Baz",
"Arr[0].Bar.Baz",
"Map['string'].Bar.Baz",
"Map.id.Bar.Baz",
"Any.Thing.Is.Ok",
"Irr['string'].next.goes['any thing']",
"Fn(Any)",
"Foo.Fn()",
"true ? Any : Any",
"len([])",
"true == false",
"nil",
"!Ok",
"[1,2,3]",
"{id: Foo.Bar.Baz, (1+1): Ok}",
"Abc()",
"Foo.Abc()",
"'a' == 'b' ~ 'c'",
"Num == 1",
"Int == Num",
"Num == Abc",
"Abc == Num",
"1 == 2 and true or Ok",
"IntPtr == Int",
"!OkPtr == Ok",
"1 == NumPtr",
"Foo.Bar == Map.id.Bar",
"StrPtr == nil",
"nil == nil",
"nil == IntPtr",
"Foo2p.Bar.Baz",
}
var typeErrorTests = []typeErrorTest{
{
"Foo.Bar.Not",
"Foo.Bar.Not undefined (type expr_test.bar has no field Not)",
},
{
"Noo",
"unknown name Noo",
},
{
"Noo()",
"unknown func Noo()",
},
{
"Foo()",
"unknown func Foo()",
},
{
"Foo['string']",
`invalid operation: Foo["string"] (type *expr_test.foo does not support indexing)`,
},
{
"Foo.Fn(Not)",
"unknown name Not",
},
{
"Foo.Bar()",
"Foo.Bar() undefined (type *expr_test.foo has no method Bar)",
},
{
"Foo.Bar.Not()",
"Foo.Bar.Not() undefined (type expr_test.bar has no method Not)",
},
{
"Arr[0].Not",
"Arr[0].Not undefined (type *expr_test.foo has no field Not)",
},
{
"Arr[Not]",
"unknown name Not",
},
{
"Not[0]",
"unknown name Not",
},
{
"Not.Bar",
"unknown name Not",
},
{
"Arr.Not",
"Arr.Not undefined (type []*expr_test.foo has no field Not)",
},
{
"Fn(Not)",
"unknown name Not",
},
{
"Map['str'].Not",
`Map["str"].Not undefined (type *expr_test.foo has no field Not)`,
},
{
"No ? Any.Ok : Any.Not",
"unknown name No",
},
{
"Any.Cond ? No : Any.Not",
"unknown name No",
},
{
"Any.Cond ? Any.Ok : No",
"unknown name No",
},
{
"Any ? Any : Any",
"non-bool Any (type map[string]interface {}) used as condition",
},
{
"!Not",
"unknown name Not",
},
{
"Not == Any",
"unknown name Not",
},
{
"[Not]",
"unknown name Not",
},
{
"{id: Not}",
"unknown name Not",
},
{
"{(Not): Any}",
"unknown name Not",
},
{
"(nil).Foo",
"nil.Foo undefined (type <nil> has no field Foo)",
},
{
"(nil)['Foo']",
`invalid operation: nil["Foo"] (type <nil> does not support indexing)`,
},
{
"1 and false",
"invalid operation: (1 and false) (mismatched types float64 and bool)",
},
{
"true or 0",
"invalid operation: (true or 0) (mismatched types bool and float64)",
},
{
"not IntPtr",
"invalid operation: not IntPtr (mismatched type *int)",
},
}
type abc interface {
Abc()
}
type bar struct {
Baz string
}
type foo struct {
Bar bar
Fn func()
Abc abc
}
type payload struct {
Abc abc
Foo *foo
Arr []*foo
Irr []interface{}
Map map[string]*foo
Any map[string]interface{}
Fn func()
Ok bool
Num float64
Int int
Str string
OkPtr *bool
NumPtr *float64
IntPtr *int
StrPtr *string
Foo2p **foo
}
func TestType(t *testing.T) {
for _, test := range typeTests {
_, err := expr.Parse(string(test), expr.With(&payload{}))
if err != nil {
t.Errorf("%s:\n\t%+v", test, err.Error())
}
}
}
func TestType_error(t *testing.T) {
for _, test := range typeErrorTests {
_, err := expr.Parse(test.input, expr.With(&payload{}))
if err == nil {
err = fmt.Errorf("<nil>")
}
if !strings.HasPrefix(err.Error(), test.err) || test.err == "" {
t.Errorf("%s:\ngot\n\t%+v\nexpected\n\t%v", test.input, err.Error(), test.err)
}
}
}
+8 -1
View File
@@ -55,7 +55,14 @@ func cast(v interface{}) (float64, error) {
func canBeNumber(v interface{}) bool {
if v != nil {
switch reflect.TypeOf(v).Kind() {
return canBeNumberType(reflect.TypeOf(v))
}
return false
}
func canBeNumberType(t Type) bool {
if t != nil {
switch t.Kind() {
case reflect.Float32, reflect.Float64:
fallthrough
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: