Add nil safe operator (#173)

This commit is contained in:
Zach Badgett
2021-06-07 22:53:12 +03:00
committed by GitHub
parent 8d74465b48
commit 3eb822d35a
13 changed files with 286 additions and 21 deletions
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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{}
+17
View File
@@ -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{
+12
View File
@@ -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
View File
@@ -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
}
+4
View File
@@ -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"}},
+3
View File
@@ -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
+9
View File
@@ -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
View File
@@ -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
+32 -3
View File
@@ -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(&param).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)