Skip to content

Commit ebfc047

Browse files
committed
feat: add "expr_noreflectmethod" build tag
By enabling this tag, two features are removed: - functions in environment - Eval() method Fix #863.
1 parent 3a46b19 commit ebfc047

12 files changed

Lines changed: 134 additions & 53 deletions

File tree

builtin/lib.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,9 +562,8 @@ func get(params ...any) (out any, err error) {
562562
// Methods can be defined on any type.
563563
if v.NumMethod() > 0 {
564564
if methodName, ok := i.(string); ok {
565-
method := v.MethodByName(methodName)
566-
if method.IsValid() {
567-
return method.Interface(), nil
565+
if m, ok := runtime.MethodByName(v, methodName); ok {
566+
return m, nil
568567
}
569568
}
570569
}

checker/info.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ func MethodIndex(c *Cache, env Nature, node ast.Node) (bool, int, string) {
3737
}
3838
case *ast.MemberNode:
3939
if name, ok := n.Property.(*ast.StringNode); ok {
40-
base := n.Node.Type()
41-
if base != nil && base.Kind() != reflect.Interface {
42-
if m, ok := base.MethodByName(name.Value); ok {
43-
return true, m.Index, name.Value
40+
base := n.Node.Nature()
41+
if base != nil && base.Kind != reflect.Interface {
42+
if m, ok := base.MethodByName(c, name.Value); ok {
43+
return true, m.MethodIndex, name.Value
4444
}
4545
}
4646
}

checker/nature/nature.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,6 @@ func (n *Nature) NumMethods(c *Cache) int {
294294
return 0
295295
}
296296

297-
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
298-
if s := n.getMethodset(c); s != nil {
299-
if m := s.method(c, name); m != nil {
300-
return m.nature, true
301-
}
302-
}
303-
return Nature{}, false
304-
}
305-
306297
func (n *Nature) NumIn() int {
307298
if n.numInSet {
308299
return n.numIn

checker/nature/no_reflectmethod.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build expr_noreflectmethod
2+
3+
package nature
4+
5+
// MethodByName is a no-op stub used when building with the
6+
// expr_noreflectmethod tag. It avoids reaching reflect.Type.Method via the
7+
// methodset cache so the Go linker can perform full method dead-code
8+
// elimination on user types.
9+
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
10+
return Nature{}, false
11+
}

checker/nature/reflectmethod.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build !expr_noreflectmethod
2+
3+
package nature
4+
5+
// MethodByName looks up a method on a Nature by name. It transitively reaches
6+
// reflect.Type.Method via the methodset cache.
7+
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
8+
if s := n.getMethodset(c); s != nil {
9+
if m := s.method(c, name); m != nil {
10+
return m.nature, true
11+
}
12+
}
13+
return Nature{}, false
14+
}

compiler/compiler.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -783,9 +783,12 @@ func (c *compiler) CallNode(node *ast.CallNode) {
783783
switch callee := node.Callee.(type) {
784784
case *ast.MemberNode:
785785
if prop, ok := callee.Property.(*ast.StringNode); ok {
786-
if _, ok = callee.Node.Type().MethodByName(prop.Value); ok && callee.Node.Type().Kind() != reflect.Interface {
787-
fnInOffset = 1
788-
fnNumIn--
786+
base := callee.Node.Nature()
787+
if base != nil && base.Kind != reflect.Interface {
788+
if _, ok := base.MethodByName(c.ntCache, prop.Value); ok {
789+
fnInOffset = 1
790+
fnNumIn--
791+
}
789792
}
790793
}
791794
case *ast.IdentifierNode:

expr.go

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/expr-lang/expr/conf"
1414
"github.com/expr-lang/expr/file"
1515
"github.com/expr-lang/expr/optimizer"
16-
"github.com/expr-lang/expr/parser"
1716
"github.com/expr-lang/expr/patcher"
1817
"github.com/expr-lang/expr/vm"
1918
)
@@ -25,7 +24,8 @@ type Option func(c *conf.Config)
2524
// If struct is passed, all fields will be treated as variables,
2625
// as well as all fields of embedded structs and struct itself.
2726
// If map is passed, all items will be treated as variables.
28-
// Methods defined on this type will be available as functions.
27+
// Methods defined on this type will be available as functions,
28+
// unless built with the expr_noreflectmethod build tag.
2929
func Env(env any) Option {
3030
return func(c *conf.Config) {
3131
c.WithEnv(env)
@@ -264,27 +264,3 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
264264
func Run(program *vm.Program, env any) (any, error) {
265265
return vm.Run(program, env)
266266
}
267-
268-
// Eval parses, compiles and runs given input.
269-
func Eval(input string, env any) (any, error) {
270-
if _, ok := env.(Option); ok {
271-
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
272-
}
273-
274-
tree, err := parser.Parse(input)
275-
if err != nil {
276-
return nil, err
277-
}
278-
279-
program, err := compiler.Compile(tree, nil)
280-
if err != nil {
281-
return nil, err
282-
}
283-
284-
output, err := Run(program, env)
285-
if err != nil {
286-
return nil, err
287-
}
288-
289-
return output, nil
290-
}

expr_no_reflectmethod.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//go:build expr_noreflectmethod
2+
3+
package expr
4+
5+
// Eval is a panic-only stub used when building with the expr_noreflectmethod
6+
// tag. The real Eval relies on runtime dispatch on the env, which requires
7+
// reflect-based method resolution. Use Compile + Run instead.
8+
func Eval(input string, env any) (any, error) {
9+
panic("expr.Eval is not available with the expr_noreflectmethod build tag")
10+
}

expr_reflectmethod.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//go:build !expr_noreflectmethod
2+
3+
package expr
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/expr-lang/expr/compiler"
9+
"github.com/expr-lang/expr/parser"
10+
)
11+
12+
// Eval parses, compiles and runs given input.
13+
//
14+
// Eval is excluded from the build under the expr_noreflectmethod tag because
15+
// it relies on runtime dispatch on the env, which requires reflect-based
16+
// method resolution.
17+
func Eval(input string, env any) (any, error) {
18+
if _, ok := env.(Option); ok {
19+
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
20+
}
21+
22+
tree, err := parser.Parse(input)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
program, err := compiler.Compile(tree, nil)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
output, err := Run(program, env)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
return output, nil
38+
}

vm/runtime/no_reflectmethod.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//go:build expr_noreflectmethod
2+
3+
package runtime
4+
5+
import "reflect"
6+
7+
// MethodByName is a no-op stub used when building with the
8+
// expr_noreflectmethod tag. It avoids reaching reflect.Value.MethodByName so
9+
// the Go linker can perform full method dead-code elimination on user types.
10+
func MethodByName(v reflect.Value, name string) (any, bool) {
11+
return nil, false
12+
}
13+
14+
// MethodByIndex is a no-op stub used when building with the
15+
// expr_noreflectmethod tag. It avoids reaching reflect.Value.Method so the Go
16+
// linker can perform full method dead-code elimination on user types.
17+
func MethodByIndex(v reflect.Value, index int) (any, bool) {
18+
return nil, false
19+
}

0 commit comments

Comments
 (0)