Skip to content

Commit 6fbab1e

Browse files
committed
feat: allow field access on concrete types behind interface values
1 parent b90e77c commit 6fbab1e

File tree

3 files changed

+212
-17
lines changed

3 files changed

+212
-17
lines changed

checker/checker.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,15 @@ func (v *Checker) memberNode(node *ast.MemberNode) Nature {
578578
}
579579
return base.Elem(&v.config.NtCache)
580580

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

test/issues/951/issue_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package issue951
2+
3+
import (
4+
"testing"
5+
6+
"github.com/expr-lang/expr"
7+
"github.com/expr-lang/expr/internal/testify/require"
8+
)
9+
10+
type Node interface {
11+
ID() string
12+
}
13+
14+
type Base struct {
15+
Name string
16+
}
17+
18+
func (b Base) ID() string { return b.Name }
19+
20+
type Container struct {
21+
Base
22+
Items []*Item
23+
}
24+
25+
type Item struct {
26+
Kind string
27+
Value string
28+
}
29+
30+
type Wrapper struct {
31+
Node // embedded interface
32+
}
33+
34+
type Proxy struct {
35+
*Wrapper
36+
}
37+
38+
type Nodes []Node
39+
40+
func (ns Nodes) GetByID(id string) Node {
41+
for _, n := range ns {
42+
if n.ID() == id {
43+
return n
44+
}
45+
}
46+
return nil
47+
}
48+
49+
func TestFieldAccessThroughEmbeddedInterface(t *testing.T) {
50+
container := &Container{
51+
Base: Base{Name: "test"},
52+
Items: []*Item{
53+
{Kind: "card", Value: "some_value"},
54+
},
55+
}
56+
proxy := &Proxy{
57+
Wrapper: &Wrapper{
58+
Node: container,
59+
},
60+
}
61+
62+
tests := []struct {
63+
name string
64+
expr string
65+
env any
66+
expect any
67+
}{
68+
{
69+
name: "field through GetByID returning interface",
70+
expr: `data.GetByID("test").Items[0].Value`,
71+
env: map[string]any{"data": Nodes{proxy}},
72+
expect: "some_value",
73+
},
74+
{
75+
name: "optional chaining with embedded interface",
76+
expr: `data.GetByID("test")?.Items[0].Value`,
77+
env: map[string]any{"data": Nodes{proxy}},
78+
expect: "some_value",
79+
},
80+
{
81+
name: "optional chaining nil result",
82+
expr: `data.GetByID("missing")?.Items`,
83+
env: map[string]any{"data": Nodes{proxy}},
84+
expect: nil,
85+
},
86+
{
87+
name: "promoted field through interface",
88+
expr: `data.GetByID("test").Name`,
89+
env: map[string]any{"data": Nodes{proxy}},
90+
expect: "test",
91+
},
92+
{
93+
name: "method on interface still works",
94+
expr: `data.GetByID("test").ID()`,
95+
env: map[string]any{"data": Nodes{proxy}},
96+
expect: "test",
97+
},
98+
}
99+
100+
for _, tt := range tests {
101+
t.Run(tt.name, func(t *testing.T) {
102+
result, err := expr.Eval(tt.expr, tt.env)
103+
require.NoError(t, err)
104+
require.Equal(t, tt.expect, result)
105+
})
106+
}
107+
}
108+
109+
func TestFieldAccessEmbeddedInterfaceNil(t *testing.T) {
110+
proxy := &Proxy{
111+
Wrapper: &Wrapper{
112+
Node: nil,
113+
},
114+
}
115+
116+
_, err := expr.Eval(`Items[0].Value`, proxy)
117+
require.Error(t, err)
118+
}

vm/runtime/runtime.go

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,15 @@ func Fetch(from, i any) any {
7979
if cv, ok := fieldCache.Load(key); ok {
8080
return v.FieldByIndex(cv.([]int)).Interface()
8181
}
82-
field, ok := t.FieldByNameFunc(func(name string) bool {
83-
field, _ := t.FieldByName(name)
84-
switch field.Tag.Get("expr") {
85-
case "-":
86-
return false
87-
case fieldName:
88-
return true
89-
default:
90-
return name == fieldName
91-
}
92-
})
93-
if ok && field.IsExported() {
94-
value := v.FieldByIndex(field.Index)
95-
if value.IsValid() {
96-
fieldCache.Store(key, field.Index)
97-
return value.Interface()
98-
}
82+
if value, field, ok := findStructField(v, fieldName); ok {
83+
fieldCache.Store(key, field.Index)
84+
return value.Interface()
85+
}
86+
// Field isn't found via standard Go promotion. Try to find it
87+
// by traversing embedded interface values whose concrete types
88+
// may contain the requested field.
89+
if result, found := fetchFromEmbeddedInterfaces(v, fieldName); found {
90+
return result
9991
}
10092
}
10193
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
@@ -143,6 +135,82 @@ func fieldByIndex(v reflect.Value, field *Field) reflect.Value {
143135
return v
144136
}
145137

138+
func findStructField(v reflect.Value, fieldName string) (reflect.Value, reflect.StructField, bool) {
139+
t := v.Type()
140+
field, ok := t.FieldByNameFunc(func(name string) bool {
141+
sf, _ := t.FieldByName(name)
142+
switch sf.Tag.Get("expr") {
143+
case "-":
144+
return false
145+
case fieldName:
146+
return true
147+
default:
148+
return name == fieldName
149+
}
150+
})
151+
if ok && field.IsExported() {
152+
value := v.FieldByIndex(field.Index)
153+
if value.IsValid() {
154+
return value, field, true
155+
}
156+
}
157+
return reflect.Value{}, reflect.StructField{}, false
158+
}
159+
160+
func fetchFromEmbeddedInterfaces(v reflect.Value, fieldName string) (any, bool) {
161+
t := v.Type()
162+
for i := 0; i < t.NumField(); i++ {
163+
f := t.Field(i)
164+
if !f.Anonymous {
165+
continue
166+
}
167+
fv := v.Field(i)
168+
fk := f.Type.Kind()
169+
170+
// Dereference pointers to get to the underlying type.
171+
for fk == reflect.Ptr {
172+
if fv.IsNil() {
173+
break
174+
}
175+
fv = fv.Elem()
176+
fk = fv.Kind()
177+
}
178+
179+
switch fk {
180+
case reflect.Interface:
181+
if fv.IsNil() {
182+
continue
183+
}
184+
// Unwrap interface and dereference pointers to reach the
185+
// concrete struct value.
186+
concrete := fv.Elem()
187+
for concrete.Kind() == reflect.Ptr {
188+
if concrete.IsNil() {
189+
break
190+
}
191+
concrete = concrete.Elem()
192+
}
193+
if concrete.Kind() != reflect.Struct {
194+
continue
195+
}
196+
if value, _, ok := findStructField(concrete, fieldName); ok {
197+
return value.Interface(), true
198+
}
199+
// The concrete type itself may have embedded interfaces.
200+
if result, found := fetchFromEmbeddedInterfaces(concrete, fieldName); found {
201+
return result, found
202+
}
203+
204+
case reflect.Struct:
205+
// Recurse into embedded structs to find embedded interfaces.
206+
if result, found := fetchFromEmbeddedInterfaces(fv, fieldName); found {
207+
return result, found
208+
}
209+
}
210+
}
211+
return nil, false
212+
}
213+
146214
type Method struct {
147215
Index int
148216
Name string

0 commit comments

Comments
 (0)