mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
Add nil safe operator (#173)
This commit is contained in:
+4
-1
@@ -48,7 +48,8 @@ type NilNode struct {
|
||||
|
||||
type IdentifierNode struct {
|
||||
base
|
||||
Value string
|
||||
Value string
|
||||
NilSafe bool
|
||||
}
|
||||
|
||||
type IntegerNode struct {
|
||||
@@ -100,6 +101,7 @@ type PropertyNode struct {
|
||||
base
|
||||
Node Node
|
||||
Property string
|
||||
NilSafe bool
|
||||
}
|
||||
|
||||
type IndexNode struct {
|
||||
@@ -120,6 +122,7 @@ type MethodNode struct {
|
||||
Node Node
|
||||
Method string
|
||||
Arguments []Node
|
||||
NilSafe bool
|
||||
}
|
||||
|
||||
type FunctionNode struct {
|
||||
|
||||
+12
-5
@@ -136,7 +136,10 @@ func (v *visitor) IdentifierNode(node *ast.IdentifierNode) reflect.Type {
|
||||
}
|
||||
return interfaceType
|
||||
}
|
||||
return v.error(node, "unknown name %v", node.Value)
|
||||
if !node.NilSafe {
|
||||
return v.error(node, "unknown name %v", node.Value)
|
||||
}
|
||||
return nilType
|
||||
}
|
||||
|
||||
func (v *visitor) IntegerNode(*ast.IntegerNode) reflect.Type {
|
||||
@@ -276,12 +279,13 @@ func (v *visitor) MatchesNode(node *ast.MatchesNode) reflect.Type {
|
||||
|
||||
func (v *visitor) PropertyNode(node *ast.PropertyNode) reflect.Type {
|
||||
t := v.visit(node.Node)
|
||||
|
||||
if t, ok := fieldType(t, node.Property); ok {
|
||||
return t
|
||||
}
|
||||
|
||||
return v.error(node, "type %v has no field %v", t, node.Property)
|
||||
if !node.NilSafe {
|
||||
return v.error(node, "type %v has no field %v", t, node.Property)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *visitor) IndexNode(node *ast.IndexNode) reflect.Type {
|
||||
@@ -361,7 +365,10 @@ func (v *visitor) MethodNode(node *ast.MethodNode) reflect.Type {
|
||||
return v.checkFunc(fn, method, node, node.Method, node.Arguments)
|
||||
}
|
||||
}
|
||||
return v.error(node, "type %v has no method %v", t, node.Method)
|
||||
if !node.NilSafe {
|
||||
return v.error(node, "type %v has no method %v", t, node.Method)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkFunc checks func arguments and returns "return type" of func or method.
|
||||
|
||||
+10
-2
@@ -87,7 +87,11 @@ func (v *visitor) Exit(ref *Node) {
|
||||
|
||||
case *PropertyNode:
|
||||
a := v.pop()
|
||||
v.push(fmt.Sprintf(".%v", node.Property))
|
||||
if !node.NilSafe {
|
||||
v.push(fmt.Sprintf(".%v", node.Property))
|
||||
} else {
|
||||
v.push(fmt.Sprintf("?.%v", node.Property))
|
||||
}
|
||||
v.link(a)
|
||||
|
||||
case *IndexNode:
|
||||
@@ -103,7 +107,11 @@ func (v *visitor) Exit(ref *Node) {
|
||||
args = append(args, v.pop())
|
||||
}
|
||||
a := v.pop()
|
||||
v.push(fmt.Sprintf(".%v(...)", node.Method))
|
||||
if !node.NilSafe {
|
||||
v.push(fmt.Sprintf(".%v(...)", node.Method))
|
||||
} else {
|
||||
v.push(fmt.Sprintf("?.%v(...)", node.Method))
|
||||
}
|
||||
v.link(a)
|
||||
for i := len(args) - 1; i >= 0; i-- {
|
||||
v.link(args[i])
|
||||
|
||||
+12
-2
@@ -180,6 +180,8 @@ func (c *compiler) IdentifierNode(node *ast.IdentifierNode) {
|
||||
v := c.makeConstant(node.Value)
|
||||
if c.mapEnv {
|
||||
c.emit(OpFetchMap, v...)
|
||||
} else if node.NilSafe {
|
||||
c.emit(OpFetchNilSafe, v...)
|
||||
} else {
|
||||
c.emit(OpFetch, v...)
|
||||
}
|
||||
@@ -401,7 +403,11 @@ func (c *compiler) MatchesNode(node *ast.MatchesNode) {
|
||||
|
||||
func (c *compiler) PropertyNode(node *ast.PropertyNode) {
|
||||
c.compile(node.Node)
|
||||
c.emit(OpProperty, c.makeConstant(node.Property)...)
|
||||
if !node.NilSafe {
|
||||
c.emit(OpProperty, c.makeConstant(node.Property)...)
|
||||
} else {
|
||||
c.emit(OpPropertyNilSafe, c.makeConstant(node.Property)...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) IndexNode(node *ast.IndexNode) {
|
||||
@@ -430,7 +436,11 @@ func (c *compiler) MethodNode(node *ast.MethodNode) {
|
||||
for _, arg := range node.Arguments {
|
||||
c.compile(arg)
|
||||
}
|
||||
c.emit(OpMethod, c.makeConstant(Call{Name: node.Method, Size: len(node.Arguments)})...)
|
||||
if !node.NilSafe {
|
||||
c.emit(OpMethod, c.makeConstant(Call{Name: node.Method, Size: len(node.Arguments)})...)
|
||||
} else {
|
||||
c.emit(OpMethodNilSafe, c.makeConstant(Call{Name: node.Method, Size: len(node.Arguments)})...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) FunctionNode(node *ast.FunctionNode) {
|
||||
|
||||
+145
@@ -944,6 +944,151 @@ func TestExpr_map_default_values(t *testing.T) {
|
||||
require.Equal(t, true, output)
|
||||
}
|
||||
|
||||
func TestExpr_nil_safe(t *testing.T) {
|
||||
env := map[string]interface{}{
|
||||
"bar": map[string]*string{},
|
||||
}
|
||||
|
||||
input := `foo?.missing?.test == '' && bar['missing'] == nil`
|
||||
|
||||
program, err := expr.Compile(input, expr.Env(env))
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, output)
|
||||
}
|
||||
|
||||
func TestExpr_nil_safe_first_ident(t *testing.T) {
|
||||
env := map[string]interface{}{
|
||||
"bar": map[string]*string{},
|
||||
}
|
||||
|
||||
input := `foo?.missing.test == '' && bar['missing'] == nil`
|
||||
|
||||
program, err := expr.Compile(input, expr.Env(env))
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, output)
|
||||
}
|
||||
|
||||
func TestExpr_nil_safe_not_strict(t *testing.T) {
|
||||
env := map[string]interface{}{
|
||||
"bar": map[string]*string{},
|
||||
}
|
||||
|
||||
input := `foo?.missing?.test == '' && bar['missing'] == nil`
|
||||
|
||||
program, err := expr.Compile(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, output)
|
||||
}
|
||||
|
||||
func TestExpr_nil_safe_valid_value(t *testing.T) {
|
||||
env := map[string]interface{}{
|
||||
"foo": map[string]map[string]interface{}{
|
||||
"missing": {
|
||||
"test": "hello",
|
||||
},
|
||||
},
|
||||
"bar": map[string]*string{},
|
||||
}
|
||||
|
||||
input := `foo?.missing?.test == 'hello' && bar['missing'] == nil`
|
||||
|
||||
program, err := expr.Compile(input, expr.Env(env))
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, output)
|
||||
}
|
||||
|
||||
func TestExpr_nil_safe_method(t *testing.T) {
|
||||
env := map[string]interface{}{
|
||||
"bar": map[string]*string{},
|
||||
}
|
||||
|
||||
input := `foo?.missing?.test() == '' && bar['missing'] == nil`
|
||||
|
||||
program, err := expr.Compile(input, expr.Env(env))
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, output)
|
||||
}
|
||||
|
||||
func TestExpr_nil_safe_struct(t *testing.T) {
|
||||
type P struct {
|
||||
Test string
|
||||
}
|
||||
type Env struct {
|
||||
Foo struct {
|
||||
Missing *P
|
||||
}
|
||||
Bar struct {
|
||||
Missing *P
|
||||
}
|
||||
}
|
||||
env := Env{
|
||||
Bar: struct {
|
||||
Missing *P
|
||||
}{
|
||||
Missing: nil,
|
||||
},
|
||||
}
|
||||
input := `Foo?.Missing?.Test == '' && Bar.Missing == nil`
|
||||
|
||||
program, err := expr.Compile(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, output)
|
||||
}
|
||||
|
||||
func TestExpr_nil_safe_struct_valid(t *testing.T) {
|
||||
type P struct {
|
||||
Test string
|
||||
}
|
||||
type Env struct {
|
||||
Foo struct {
|
||||
Missing *P
|
||||
}
|
||||
Bar struct {
|
||||
Missing *P
|
||||
}
|
||||
}
|
||||
env := Env{
|
||||
Foo: struct {
|
||||
Missing *P
|
||||
}{
|
||||
Missing: &P{
|
||||
Test: "hello",
|
||||
},
|
||||
},
|
||||
Bar: struct {
|
||||
Missing *P
|
||||
}{
|
||||
Missing: nil,
|
||||
},
|
||||
}
|
||||
input := `Foo?.Missing?.Test == 'hello' && Bar.Missing == nil`
|
||||
|
||||
program, err := expr.Compile(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, output)
|
||||
}
|
||||
|
||||
func TestExpr_map_default_values_compile_check(t *testing.T) {
|
||||
tests := []struct {
|
||||
env interface{}
|
||||
|
||||
@@ -63,6 +63,23 @@ var lexTests = []lexTest{
|
||||
{Kind: EOF},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a and orb().val and foo?.bar",
|
||||
[]Token{
|
||||
{Kind: Identifier, Value: "a"},
|
||||
{Kind: Operator, Value: "and"},
|
||||
{Kind: Identifier, Value: "orb"},
|
||||
{Kind: Bracket, Value: "("},
|
||||
{Kind: Bracket, Value: ")"},
|
||||
{Kind: Operator, Value: "."},
|
||||
{Kind: Identifier, Value: "val"},
|
||||
{Kind: Operator, Value: "and"},
|
||||
{Kind: Identifier, Value: "foo"},
|
||||
{Kind: Operator, Value: "?."},
|
||||
{Kind: Identifier, Value: "bar"},
|
||||
{Kind: EOF},
|
||||
},
|
||||
},
|
||||
{
|
||||
`not in not abc not i not(false) not in not in`,
|
||||
[]Token{
|
||||
|
||||
@@ -24,6 +24,11 @@ func root(l *lexer) stateFn {
|
||||
case '0' <= r && r <= '9':
|
||||
l.backup()
|
||||
return number
|
||||
case r == '?':
|
||||
if l.peek() == '.' {
|
||||
return nilsafe
|
||||
}
|
||||
l.emit(Operator)
|
||||
case strings.ContainsRune("([{", r):
|
||||
l.emit(Bracket)
|
||||
case strings.ContainsRune(")]}", r):
|
||||
@@ -102,6 +107,13 @@ func dot(l *lexer) stateFn {
|
||||
return root
|
||||
}
|
||||
|
||||
func nilsafe(l *lexer) stateFn {
|
||||
l.next()
|
||||
l.accept("?.")
|
||||
l.emit(Operator)
|
||||
return root
|
||||
}
|
||||
|
||||
func identifier(l *lexer) stateFn {
|
||||
loop:
|
||||
for {
|
||||
|
||||
+14
-5
@@ -283,7 +283,7 @@ func (p *parser) parsePrimaryExpression() Node {
|
||||
node.SetLocation(token.Location)
|
||||
return node
|
||||
default:
|
||||
node = p.parseIdentifierExpression(token)
|
||||
node = p.parseIdentifierExpression(token, p.current)
|
||||
}
|
||||
|
||||
case Number:
|
||||
@@ -334,7 +334,7 @@ func (p *parser) parsePrimaryExpression() Node {
|
||||
return p.parsePostfixExpression(node)
|
||||
}
|
||||
|
||||
func (p *parser) parseIdentifierExpression(token Token) Node {
|
||||
func (p *parser) parseIdentifierExpression(token, next Token) Node {
|
||||
var node Node
|
||||
if p.current.Is(Bracket, "(") {
|
||||
var arguments []Node
|
||||
@@ -367,7 +367,11 @@ func (p *parser) parseIdentifierExpression(token Token) Node {
|
||||
node.SetLocation(token.Location)
|
||||
}
|
||||
} else {
|
||||
node = &IdentifierNode{Value: token.Value}
|
||||
var nilsafe bool
|
||||
if next.Value == "?." {
|
||||
nilsafe = true
|
||||
}
|
||||
node = &IdentifierNode{Value: token.Value, NilSafe: nilsafe}
|
||||
node.SetLocation(token.Location)
|
||||
}
|
||||
return node
|
||||
@@ -460,8 +464,12 @@ end:
|
||||
|
||||
func (p *parser) parsePostfixExpression(node Node) Node {
|
||||
token := p.current
|
||||
var nilsafe bool
|
||||
for (token.Is(Operator) || token.Is(Bracket)) && p.err == nil {
|
||||
if token.Value == "." {
|
||||
if token.Value == "." || token.Value == "?." {
|
||||
if token.Value == "?." {
|
||||
nilsafe = true
|
||||
}
|
||||
p.next()
|
||||
|
||||
token = p.current
|
||||
@@ -479,12 +487,14 @@ func (p *parser) parsePostfixExpression(node Node) Node {
|
||||
Node: node,
|
||||
Method: token.Value,
|
||||
Arguments: arguments,
|
||||
NilSafe: nilsafe,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
} else {
|
||||
node = &PropertyNode{
|
||||
Node: node,
|
||||
Property: token.Value,
|
||||
NilSafe: nilsafe,
|
||||
}
|
||||
node.SetLocation(token.Location)
|
||||
}
|
||||
@@ -537,7 +547,6 @@ func (p *parser) parsePostfixExpression(node Node) Node {
|
||||
p.expect(Bracket, "]")
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ func TestParse(t *testing.T) {
|
||||
"foo.bar",
|
||||
&ast.PropertyNode{Node: &ast.IdentifierNode{Value: "foo"}, Property: "bar"},
|
||||
},
|
||||
{
|
||||
"foo?.bar",
|
||||
&ast.PropertyNode{Node: &ast.IdentifierNode{Value: "foo", NilSafe: true}, Property: "bar", NilSafe: true},
|
||||
},
|
||||
{
|
||||
"foo['all']",
|
||||
&ast.IndexNode{Node: &ast.IdentifierNode{Value: "foo"}, Index: &ast.StringNode{Value: "all"}},
|
||||
|
||||
@@ -5,6 +5,7 @@ const (
|
||||
OpPop
|
||||
OpRot
|
||||
OpFetch
|
||||
OpFetchNilSafe
|
||||
OpFetchMap
|
||||
OpTrue
|
||||
OpFalse
|
||||
@@ -38,9 +39,11 @@ const (
|
||||
OpIndex
|
||||
OpSlice
|
||||
OpProperty
|
||||
OpPropertyNilSafe
|
||||
OpCall
|
||||
OpCallFast
|
||||
OpMethod
|
||||
OpMethodNilSafe
|
||||
OpArray
|
||||
OpMap
|
||||
OpLen
|
||||
|
||||
@@ -73,6 +73,9 @@ func (program *Program) Disassemble() string {
|
||||
case OpFetch:
|
||||
constant("OpFetch")
|
||||
|
||||
case OpFetchNilSafe:
|
||||
constant("OpFetchNilSafe")
|
||||
|
||||
case OpFetchMap:
|
||||
constant("OpFetchMap")
|
||||
|
||||
@@ -172,6 +175,9 @@ func (program *Program) Disassemble() string {
|
||||
case OpProperty:
|
||||
constant("OpProperty")
|
||||
|
||||
case OpPropertyNilSafe:
|
||||
constant("OpPropertyNilSafe")
|
||||
|
||||
case OpCall:
|
||||
constant("OpCall")
|
||||
|
||||
@@ -181,6 +187,9 @@ func (program *Program) Disassemble() string {
|
||||
case OpMethod:
|
||||
constant("OpMethod")
|
||||
|
||||
case OpMethodNilSafe:
|
||||
constant("OpMethodNilSafe")
|
||||
|
||||
case OpArray:
|
||||
code("OpArray")
|
||||
|
||||
|
||||
+12
-3
@@ -15,7 +15,7 @@ type Call struct {
|
||||
|
||||
type Scope map[string]interface{}
|
||||
|
||||
func fetch(from interface{}, i interface{}) interface{} {
|
||||
func fetch(from, i interface{}, nilsafe bool) interface{} {
|
||||
v := reflect.ValueOf(from)
|
||||
kind := v.Kind()
|
||||
|
||||
@@ -51,8 +51,10 @@ func fetch(from interface{}, i interface{}) interface{} {
|
||||
return value.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
|
||||
if !nilsafe {
|
||||
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func slice(array, from, to interface{}) interface{} {
|
||||
@@ -118,6 +120,13 @@ func FetchFn(from interface{}, name string) reflect.Value {
|
||||
panic(fmt.Sprintf(`cannot get "%v" from %T`, name, from))
|
||||
}
|
||||
|
||||
func FetchFnNil(from interface{}, name string) reflect.Value {
|
||||
if v := reflect.ValueOf(from); !v.IsValid() {
|
||||
return v
|
||||
}
|
||||
return FetchFn(from, name)
|
||||
}
|
||||
|
||||
func in(needle interface{}, array interface{}) bool {
|
||||
if array == nil {
|
||||
return false
|
||||
|
||||
@@ -98,7 +98,10 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
|
||||
vm.push(a)
|
||||
|
||||
case OpFetch:
|
||||
vm.push(fetch(env, vm.constant()))
|
||||
vm.push(fetch(env, vm.constant(), false))
|
||||
|
||||
case OpFetchNilSafe:
|
||||
vm.push(fetch(env, vm.constant(), true))
|
||||
|
||||
case OpFetchMap:
|
||||
vm.push(env.(map[string]interface{})[vm.constant().(string)])
|
||||
@@ -255,7 +258,7 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
|
||||
case OpIndex:
|
||||
b := vm.pop()
|
||||
a := vm.pop()
|
||||
vm.push(fetch(a, b))
|
||||
vm.push(fetch(a, b, false))
|
||||
|
||||
case OpSlice:
|
||||
from := vm.pop()
|
||||
@@ -266,7 +269,12 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
|
||||
case OpProperty:
|
||||
a := vm.pop()
|
||||
b := vm.constant()
|
||||
vm.push(fetch(a, b))
|
||||
vm.push(fetch(a, b, false))
|
||||
|
||||
case OpPropertyNilSafe:
|
||||
a := vm.pop()
|
||||
b := vm.constant()
|
||||
vm.push(fetch(a, b, true))
|
||||
|
||||
case OpCall:
|
||||
call := vm.constant().(Call)
|
||||
@@ -309,6 +317,27 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
|
||||
out := FetchFn(vm.pop(), call.Name).Call(in)
|
||||
vm.push(out[0].Interface())
|
||||
|
||||
case OpMethodNilSafe:
|
||||
call := vm.constants[vm.arg()].(Call)
|
||||
in := make([]reflect.Value, call.Size)
|
||||
for i := call.Size - 1; i >= 0; i-- {
|
||||
param := vm.pop()
|
||||
if param == nil && reflect.TypeOf(param) == nil {
|
||||
// In case of nil value and nil type use this hack,
|
||||
// otherwise reflect.Call will panic on zero value.
|
||||
in[i] = reflect.ValueOf(¶m).Elem()
|
||||
} else {
|
||||
in[i] = reflect.ValueOf(param)
|
||||
}
|
||||
}
|
||||
fn := FetchFnNil(vm.pop(), call.Name)
|
||||
if !fn.IsValid() {
|
||||
vm.push(nil)
|
||||
} else {
|
||||
out := fn.Call(in)
|
||||
vm.push(out[0].Interface())
|
||||
}
|
||||
|
||||
case OpArray:
|
||||
size := vm.pop().(int)
|
||||
array := make([]interface{}, size)
|
||||
|
||||
Reference in New Issue
Block a user