Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,15 @@ func (v *Checker) memberNode(node *ast.MemberNode) Nature {
}
return base.Elem(&v.config.NtCache)

case reflect.Interface:
// For non-any interface types, we don't know the concrete type at
// compile time. Allow field (non-method) access and defer resolution
// to runtime, where the concrete type can be inspected.
if name, ok := node.Property.(*ast.StringNode); ok && node.Method {
return v.error(node, "type %v has no method %v", base.String(), name.Value)
}
return Nature{}

case reflect.Struct:
if name, ok := node.Property.(*ast.StringNode); ok {
propertyName := name.Value
Expand Down
118 changes: 118 additions & 0 deletions test/issues/951/issue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package issue951

import (
"testing"

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/internal/testify/require"
)

type Node interface {
ID() string
}

type Base struct {
Name string
}

func (b Base) ID() string { return b.Name }

type Container struct {
Base
Items []*Item
}

type Item struct {
Kind string
Value string
}

type Wrapper struct {
Node // embedded interface
}

type Proxy struct {
*Wrapper
}

type Nodes []Node

func (ns Nodes) GetByID(id string) Node {
for _, n := range ns {
if n.ID() == id {
return n
}
}
return nil
}

func TestFieldAccessThroughEmbeddedInterface(t *testing.T) {
container := &Container{
Base: Base{Name: "test"},
Items: []*Item{
{Kind: "card", Value: "some_value"},
},
}
proxy := &Proxy{
Wrapper: &Wrapper{
Node: container,
},
}

tests := []struct {
name string
expr string
env any
expect any
}{
{
name: "field through GetByID returning interface",
expr: `data.GetByID("test").Items[0].Value`,
env: map[string]any{"data": Nodes{proxy}},
expect: "some_value",
},
{
name: "optional chaining with embedded interface",
expr: `data.GetByID("test")?.Items[0].Value`,
env: map[string]any{"data": Nodes{proxy}},
expect: "some_value",
},
{
name: "optional chaining nil result",
expr: `data.GetByID("missing")?.Items`,
env: map[string]any{"data": Nodes{proxy}},
expect: nil,
},
{
name: "promoted field through interface",
expr: `data.GetByID("test").Name`,
env: map[string]any{"data": Nodes{proxy}},
expect: "test",
},
{
name: "method on interface still works",
expr: `data.GetByID("test").ID()`,
env: map[string]any{"data": Nodes{proxy}},
expect: "test",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := expr.Eval(tt.expr, tt.env)
require.NoError(t, err)
require.Equal(t, tt.expect, result)
})
}
}

func TestFieldAccessEmbeddedInterfaceNil(t *testing.T) {
proxy := &Proxy{
Wrapper: &Wrapper{
Node: nil,
},
}

_, err := expr.Eval(`Items[0].Value`, proxy)
require.Error(t, err)
}
102 changes: 85 additions & 17 deletions vm/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,15 @@ func Fetch(from, i any) any {
if cv, ok := fieldCache.Load(key); ok {
return v.FieldByIndex(cv.([]int)).Interface()
}
field, ok := t.FieldByNameFunc(func(name string) bool {
field, _ := t.FieldByName(name)
switch field.Tag.Get("expr") {
case "-":
return false
case fieldName:
return true
default:
return name == fieldName
}
})
if ok && field.IsExported() {
value := v.FieldByIndex(field.Index)
if value.IsValid() {
fieldCache.Store(key, field.Index)
return value.Interface()
}
if value, field, ok := findStructField(v, fieldName); ok {
fieldCache.Store(key, field.Index)
return value.Interface()
}
// Field isn't found via standard Go promotion. Try to find it
// by traversing embedded interface values whose concrete types
// may contain the requested field.
if result, found := fetchFromEmbeddedInterfaces(v, fieldName); found {
return result
}
}
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
Expand Down Expand Up @@ -143,6 +135,82 @@ func fieldByIndex(v reflect.Value, field *Field) reflect.Value {
return v
}

func findStructField(v reflect.Value, fieldName string) (reflect.Value, reflect.StructField, bool) {
t := v.Type()
field, ok := t.FieldByNameFunc(func(name string) bool {
sf, _ := t.FieldByName(name)
switch sf.Tag.Get("expr") {
case "-":
return false
case fieldName:
return true
default:
return name == fieldName
}
})
if ok && field.IsExported() {
value := v.FieldByIndex(field.Index)
if value.IsValid() {
return value, field, true
}
}
return reflect.Value{}, reflect.StructField{}, false
}

func fetchFromEmbeddedInterfaces(v reflect.Value, fieldName string) (any, bool) {
Comment thread
antonmedv marked this conversation as resolved.
t := v.Type()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.Anonymous {
continue
}
fv := v.Field(i)
fk := f.Type.Kind()

// Dereference pointers to get to the underlying type.
for fk == reflect.Ptr {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use our deref package.

if fv.IsNil() {
break
}
fv = fv.Elem()
fk = fv.Kind()
}

switch fk {
case reflect.Interface:
if fv.IsNil() {
continue
}
// Unwrap interface and dereference pointers to reach the
// concrete struct value.
concrete := fv.Elem()
for concrete.Kind() == reflect.Ptr {
if concrete.IsNil() {
break
}
concrete = concrete.Elem()
}
if concrete.Kind() != reflect.Struct {
continue
}
if value, _, ok := findStructField(concrete, fieldName); ok {
return value.Interface(), true
}
// The concrete type itself may have embedded interfaces.
if result, found := fetchFromEmbeddedInterfaces(concrete, fieldName); found {
return result, found
}

case reflect.Struct:
// Recurse into embedded structs to find embedded interfaces.
if result, found := fetchFromEmbeddedInterfaces(fv, fieldName); found {
return result, found
}
}
}
return nil, false
}

type Method struct {
Index int
Name string
Expand Down
Loading