Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ jobs:
strategy:
matrix:
go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24', '1.25', '1.26' ]
tags: [ '', 'expr_noreflectmethod' ]
steps:
- uses: actions/checkout@v3
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Test
run: go test ./...
run: go test -tags=${{ matrix.tags }} ./...

debug:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions builtin/builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
)

func TestBuiltin(t *testing.T) {
assert.SkipNoReflectMethod(t)
ArrayWithNil := []any{42}
env := map[string]any{
"ArrayOfString": []string{"foo", "bar", "baz"},
Expand Down Expand Up @@ -345,6 +346,7 @@ func TestBuiltin_types(t *testing.T) {
}

func TestBuiltin_memory_limits(t *testing.T) {
assert.SkipNoReflectMethod(t)
tests := []struct {
input string
}{
Expand Down Expand Up @@ -696,6 +698,7 @@ func Test_int_unwraps_underlying_value(t *testing.T) {
}

func TestBuiltin_with_deref(t *testing.T) {
assert.SkipNoReflectMethod(t)
x := 42
arr := []int{1, 2, 3}
arrStr := []string{"1", "2", "3"}
Expand Down
5 changes: 2 additions & 3 deletions builtin/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,9 +562,8 @@ func get(params ...any) (out any, err error) {
// Methods can be defined on any type.
if v.NumMethod() > 0 {
if methodName, ok := i.(string); ok {
method := v.MethodByName(methodName)
if method.IsValid() {
return method.Interface(), nil
if m, ok := runtime.MethodByName(v, methodName); ok {
return m, nil
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
)

func TestCheck(t *testing.T) {
assert.SkipNoReflectMethod(t)
var tests = []struct {
input string
}{
Expand Down Expand Up @@ -155,6 +156,7 @@ func TestCheck(t *testing.T) {
}

func TestCheck_error(t *testing.T) {
assert.SkipNoReflectMethod(t)
errorTests := []struct{ code, err string }{
{
`Foo.Bar.Not`,
Expand Down
8 changes: 4 additions & 4 deletions checker/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ func MethodIndex(c *Cache, env Nature, node ast.Node) (bool, int, string) {
}
case *ast.MemberNode:
if name, ok := n.Property.(*ast.StringNode); ok {
base := n.Node.Type()
if base != nil && base.Kind() != reflect.Interface {
if m, ok := base.MethodByName(name.Value); ok {
return true, m.Index, name.Value
base := n.Node.Nature()
if base != nil && base.Kind != reflect.Interface {
if m, ok := base.MethodByName(c, name.Value); ok {
return true, m.MethodIndex, name.Value
}
}
}
Expand Down
9 changes: 0 additions & 9 deletions checker/nature/nature.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,15 +294,6 @@ func (n *Nature) NumMethods(c *Cache) int {
return 0
}

func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
if s := n.getMethodset(c); s != nil {
if m := s.method(c, name); m != nil {
return m.nature, true
}
}
return Nature{}, false
}

func (n *Nature) NumIn() int {
if n.numInSet {
return n.numIn
Expand Down
11 changes: 11 additions & 0 deletions checker/nature/no_reflectmethod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build expr_noreflectmethod

package nature

// MethodByName is a no-op stub used when building with the
// expr_noreflectmethod tag. It avoids reaching reflect.Type.Method via the
// methodset cache so the Go linker can perform full method dead-code
// elimination on user types.
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
return Nature{}, false
}
14 changes: 14 additions & 0 deletions checker/nature/reflectmethod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !expr_noreflectmethod

package nature

// MethodByName looks up a method on a Nature by name. It transitively reaches
// reflect.Type.Method via the methodset cache.
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
if s := n.getMethodset(c); s != nil {
if m := s.method(c, name); m != nil {
return m.nature, true
}
}
return Nature{}, false
}
9 changes: 6 additions & 3 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,12 @@ func (c *compiler) CallNode(node *ast.CallNode) {
switch callee := node.Callee.(type) {
case *ast.MemberNode:
if prop, ok := callee.Property.(*ast.StringNode); ok {
if _, ok = callee.Node.Type().MethodByName(prop.Value); ok && callee.Node.Type().Kind() != reflect.Interface {
fnInOffset = 1
fnNumIn--
base := callee.Node.Nature()
if base != nil && base.Kind != reflect.Interface {
if _, ok := base.MethodByName(c.ntCache, prop.Value); ok {
fnInOffset = 1
fnNumIn--
}
}
}
case *ast.IdentifierNode:
Expand Down
3 changes: 3 additions & 0 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (e Env) Func() B {
}

func TestCompile(t *testing.T) {
assert.SkipNoReflectMethod(t)
var tests = []struct {
code string
want vm.Program
Expand Down Expand Up @@ -436,6 +437,7 @@ func TestCompile_FuncTypes(t *testing.T) {
}

func TestCompile_FuncTypes_with_Method(t *testing.T) {
assert.SkipNoReflectMethod(t)
env := mock.Env{}
program, err := expr.Compile("FuncTyped('bar')", expr.Env(env))
require.NoError(t, err)
Expand Down Expand Up @@ -647,6 +649,7 @@ func TestCompile_optimizes_jumps(t *testing.T) {
}

func TestCompile_IntegerArgsFunc(t *testing.T) {
assert.SkipNoReflectMethod(t)
env := mock.Env{}
tests := []struct{ code string }{
{"FuncInt(0)"},
Expand Down
28 changes: 2 additions & 26 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/optimizer"
"github.com/expr-lang/expr/parser"
"github.com/expr-lang/expr/patcher"
"github.com/expr-lang/expr/vm"
)
Expand All @@ -25,7 +24,8 @@ type Option func(c *conf.Config)
// If struct is passed, all fields will be treated as variables,
// as well as all fields of embedded structs and struct itself.
// If map is passed, all items will be treated as variables.
// Methods defined on this type will be available as functions.
// Methods defined on this type will be available as functions,
// unless built with the expr_noreflectmethod build tag.
func Env(env any) Option {
return func(c *conf.Config) {
c.WithEnv(env)
Expand Down Expand Up @@ -264,27 +264,3 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
func Run(program *vm.Program, env any) (any, error) {
return vm.Run(program, env)
}

// Eval parses, compiles and runs given input.
func Eval(input string, env any) (any, error) {
if _, ok := env.(Option); ok {
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
}

tree, err := parser.Parse(input)
if err != nil {
return nil, err
}

program, err := compiler.Compile(tree, nil)
if err != nil {
return nil, err
}

output, err := Run(program, env)
if err != nil {
return nil, err
}

return output, nil
}
10 changes: 10 additions & 0 deletions expr_no_reflectmethod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build expr_noreflectmethod

package expr

// Eval is a panic-only stub used when building with the expr_noreflectmethod
// tag. The real Eval relies on runtime dispatch on the env, which requires
// reflect-based method resolution. Use Compile + Run instead.
func Eval(input string, env any) (any, error) {
panic("expr.Eval is not available with the expr_noreflectmethod build tag")
}
38 changes: 38 additions & 0 deletions expr_reflectmethod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build !expr_noreflectmethod

package expr

import (
"fmt"

"github.com/expr-lang/expr/compiler"
"github.com/expr-lang/expr/parser"
)

// Eval parses, compiles and runs given input.
//
// Eval is excluded from the build under the expr_noreflectmethod tag because
// it relies on runtime dispatch on the env, which requires reflect-based
// method resolution.
func Eval(input string, env any) (any, error) {
if _, ok := env.(Option); ok {
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
}

tree, err := parser.Parse(input)
if err != nil {
return nil, err
}

program, err := compiler.Compile(tree, nil)
if err != nil {
return nil, err
}

output, err := Run(program, env)
if err != nil {
return nil, err
}

return output, nil
}
Loading
Loading