diff --git a/conf/env.go b/conf/env.go index 0acd44570..da4ac0829 100644 --- a/conf/env.go +++ b/conf/env.go @@ -32,22 +32,24 @@ func EnvWithCache(c *Cache, env any) Nature { v := reflect.ValueOf(env) t := v.Type() - switch deref.Value(v).Kind() { + d := deref.Value(v) + + switch d.Kind() { case reflect.Struct: n := c.FromType(t) n.Strict = true return n case reflect.Map: - n := c.FromType(v.Type()) + n := c.FromType(d.Type()) if n.TypeData == nil { n.TypeData = new(TypeData) } n.Strict = true - n.Fields = make(map[string]Nature, v.Len()) + n.Fields = make(map[string]Nature, d.Len()) - for _, key := range v.MapKeys() { - elem := v.MapIndex(key) + for _, key := range d.MapKeys() { + elem := d.MapIndex(key) if !elem.IsValid() || !elem.CanInterface() { panic(fmt.Sprintf("invalid map value: %s", key)) } diff --git a/test/issues/825/issue_test.go b/test/issues/825/issue_test.go new file mode 100644 index 000000000..3cb871760 --- /dev/null +++ b/test/issues/825/issue_test.go @@ -0,0 +1,48 @@ +package issue_test + +import ( + "testing" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/internal/testify/assert" + "github.com/expr-lang/expr/internal/testify/require" +) + +// TestIssue825 verifies that passing a pointer to a map as the environment is +// dereferenced instead of panicking. +// +// conf.EnvWithCache selected the map branch using the dereferenced value's +// kind, but then read the map keys/length from the original (pointer) value, +// panicking with: +// +// reflect: call of reflect.Value.Len on ptr to non-array Value +func TestIssue825(t *testing.T) { + m := map[string]any{"foo": 42} + + program, err := expr.Compile("foo + 1", expr.Env(&m)) + require.NoError(t, err) + + out, err := expr.Run(program, m) + require.NoError(t, err) + assert.Equal(t, 43, out) +} + +// TestIssue825_Strict verifies that a pointer-to-map env keeps the strict-mode +// and element-type information of the dereferenced map, i.e. it behaves exactly +// like compiling with the map value itself. +func TestIssue825_Strict(t *testing.T) { + m := map[string]int{"a": 1} + + // Unknown names are rejected (strict), just like a plain map env. + _, err := expr.Compile("unknown + 1", expr.Env(&m)) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown name unknown") + + // The element type (int) is inferred from the dereferenced map. + program, err := expr.Compile("a + 1", expr.Env(&m)) + require.NoError(t, err) + + out, err := expr.Run(program, m) + require.NoError(t, err) + assert.Equal(t, 2, out) +}