diff --git a/checker/checker.go b/checker/checker.go index 00025a33c..a4e5e6d45 100644 --- a/checker/checker.go +++ b/checker/checker.go @@ -383,8 +383,18 @@ func (v *visitor) ChainNode(node *ast.ChainNode) (reflect.Type, info) { } func (v *visitor) MemberNode(node *ast.MemberNode) (reflect.Type, info) { - base, _ := v.visit(node.Node) prop, _ := v.visit(node.Property) + if an, ok := node.Node.(*ast.IdentifierNode); ok && an.Value == "env" { + // If the index is a constant string, can save some + // cycles later by finding the type of its referent + if name, ok := node.Property.(*ast.StringNode); ok { + if t, ok := v.config.Types[name.Value]; ok { + return t.Type, info{method: t.Method} + } // No error if no type found; it may be added to env between compile and run + } + return anyType, info{} + } + base, _ := v.visit(node.Node) if name, ok := node.Property.(*ast.StringNode); ok { if base == nil { diff --git a/compiler/compiler.go b/compiler/compiler.go index 3cd32af0f..423dd6217 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -205,6 +205,10 @@ func (c *compiler) NilNode(_ *ast.NilNode) { } func (c *compiler) IdentifierNode(node *ast.IdentifierNode) { + if node.Value == "env" { + c.emit(OpLoadEnv) + return + } if c.mapEnv { c.emit(OpLoadFast, c.addConstant(node.Value)) } else if len(node.FieldIndex) > 0 { diff --git a/conf/types_table.go b/conf/types_table.go index e917f5fa8..f4d401c9c 100644 --- a/conf/types_table.go +++ b/conf/types_table.go @@ -54,6 +54,9 @@ func CreateTypesTable(i interface{}) TypesTable { for _, key := range v.MapKeys() { value := v.MapIndex(key) if key.Kind() == reflect.String && value.IsValid() && value.CanInterface() { + if key.String() == "env" { // Could check for all keywords here + panic("attempt to misuse env keyword as env map key") + } types[key.String()] = Tag{Type: reflect.TypeOf(value.Interface())} } } @@ -94,10 +97,13 @@ func FieldsFromStruct(t reflect.Type) TypesTable { } } } - - types[FieldName(f)] = Tag{ - Type: f.Type, - FieldIndex: f.Index, + if fn := FieldName(f); fn == "env" { // Could check for all keywords here + panic("attempt to misuse env keyword as env struct field tag") + } else { + types[FieldName(f)] = Tag{ + Type: f.Type, + FieldIndex: f.Index, + } } } } diff --git a/expr_test.go b/expr_test.go index 6246c844b..14cbf7415 100644 --- a/expr_test.go +++ b/expr_test.go @@ -1811,6 +1811,103 @@ func TestEval_nil_in_maps(t *testing.T) { }) } +// Test the use of env keyword. Forms env[] and env[”] are valid. +// The enclosed identifier must be in the expression env. +func TestEnv_keyword(t *testing.T) { + env := map[string]interface{}{ + "space test": "ok", + "space_test": "not ok", // Seems to be some underscore substituting happening, check that. + "Section 1-2a": "ok", + `c:\ndrive\2015 Information Table`: "ok", + "%*worst function name ever!!": func() string { + return "ok" + }(), + "1": "o", + "2": "k", + "num": 10, + "mylist": []int{1, 2, 3, 4, 5}, + "MIN": func(a, b int) int { + if a < b { + return a + } else { + return b + } + }, + "red": "n", + "irect": "um", + "String Map": map[string]string{ + "one": "two", + "three": "four", + }, + "OtherMap": map[string]string{ + "a": "b", + "c": "d", + }, + } + + // No error cases + var tests = []struct { + code string + want interface{} + }{ + {"env['space test']", "ok"}, + {"env['Section 1-2a']", "ok"}, + {`env["c:\\ndrive\\2015 Information Table"]`, "ok"}, + {"env['%*worst function name ever!!']", "ok"}, + {"env['String Map'].one", "two"}, + {"env['1'] + env['2']", "ok"}, + {"1 + env['num'] + env['num']", 21}, + {"MIN(env['num'],0)", 0}, + {"env['nu' + 'm']", 10}, + {"env[red + irect]", 10}, + {"env['String Map']?.five", ""}, + {"env.red", "n"}, + {"env?.blue", nil}, + {"env.mylist[1]", 2}, + {"env?.OtherMap?.a", "b"}, + {"env?.OtherMap?.d", ""}, + } + + for _, tt := range tests { + t.Run(tt.code, func(t *testing.T) { + + program, err := expr.Compile(tt.code, expr.Env(env)) + require.NoError(t, err, "compile error") + + got, err := expr.Run(program, env) + require.NoError(t, err, "execution error") + + assert.Equal(t, tt.want, got, tt.code) + }) + } + + for _, tt := range tests { + t.Run(tt.code, func(t *testing.T) { + got, err := expr.Eval(tt.code, env) + require.NoError(t, err, "eval error: "+tt.code) + + assert.Equal(t, tt.want, got, "eval: "+tt.code) + }) + } + + // error cases + tests = []struct { + code string + want interface{} + }{ + {"env()", "bad"}, + } + + for _, tt := range tests { + t.Run(tt.code, func(t *testing.T) { + _, err := expr.Eval(tt.code, expr.Env(env)) + require.Error(t, err, "compile error") + + }) + } + +} + type Bar interface { Bar() int } diff --git a/vm/opcodes.go b/vm/opcodes.go index b3117e73c..63b9f8a30 100644 --- a/vm/opcodes.go +++ b/vm/opcodes.go @@ -11,6 +11,7 @@ const ( OpLoadFast OpLoadMethod OpLoadFunc + OpLoadEnv OpFetch OpFetchField OpMethod diff --git a/vm/vm.go b/vm/vm.go index af4fc5bf7..3e5411b1f 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -123,6 +123,8 @@ func (vm *VM) Run(program *Program, env interface{}) (_ interface{}, err error) a := vm.pop() vm.push(runtime.FetchField(a, program.Constants[arg].(*runtime.Field))) + case OpLoadEnv: + vm.push(env) case OpMethod: a := vm.pop() vm.push(runtime.FetchMethod(a, program.Constants[arg].(*runtime.Method)))