Skip to content
Closed
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
6 changes: 4 additions & 2 deletions builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,8 @@ var Builtins = []*Function{
case reflect.Interface:
return anyType, nil
case reflect.Slice, reflect.Array:
return args[0].Elem(), nil
// Empty collection returns nil; type as any so WarnOnAny catches it.
return anyType, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] Preserve member checking for known element types

Returning any here removes known struct-field validation for nonempty typed collections. For example, with type User struct { Name string } and env := map[string]any{"users": []User{{Name: "Alice"}}}, compiling first(users).Nmae == "Alice" with expr.Env(env), expr.AsBool(), expr.WarnOnAny() returns type main.User has no field Nmae at base 4b31df3, but succeeds at 46f9251; expr.Run then fails with cannot fetch Nmae from main.User. The first/get cases also reproduce with WarnOnAny disabled; typed get/last/find/findLast results reproduce with it enabled. Applications using Compile to validate expressions therefore discover these typos only at execution time.

The final comparison is Boolean, so WarnOnAny is correctly checking its documented final-result contract; it cannot restore the member information erased by this change. Preserve that known element/member information when representing possible nil. The original nil-to-int cases are correctly fixed in the probe.

Standalone public-API probe and original outputs: identical 24 cases at the exact base/head, eight reproductions of this one diagnostic regression, eight intended nil-conversion fixes, and eight unchanged controls including true/false comparisons and direct-index rejection. AI-assisted review, cached Go 1.27.3/macOS arm64; full suite/race/vet/CI were not run for this review.

}
return anyType, fmt.Errorf("cannot get first element from %s", args[0])
},
Expand All @@ -634,7 +635,8 @@ var Builtins = []*Function{
case reflect.Interface:
return anyType, nil
case reflect.Slice, reflect.Array:
return args[0].Elem(), nil
// Empty collection returns nil; type as any so WarnOnAny catches it.
return anyType, nil
}
return anyType, fmt.Errorf("cannot get last element from %s", args[0])
},
Expand Down
12 changes: 6 additions & 6 deletions builtin/builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,12 @@ func TestBuiltin_types(t *testing.T) {
input string
want reflect.Kind
}{
{`get(ArrayOfString, 0)`, reflect.String},
{`get(ArrayOfInt, 0)`, reflect.Int},
{`first(ArrayOfString)`, reflect.String},
{`first(ArrayOfInt)`, reflect.Int},
{`last(ArrayOfString)`, reflect.String},
{`last(ArrayOfInt)`, reflect.Int},
{`get(ArrayOfString, 0)`, reflect.Interface}, // nilable
{`get(ArrayOfInt, 0)`, reflect.Interface},
{`first(ArrayOfString)`, reflect.Interface},
{`first(ArrayOfInt)`, reflect.Interface},
{`last(ArrayOfString)`, reflect.Interface},
{`last(ArrayOfInt)`, reflect.Interface},
{`get($env, 'str')`, reflect.String},
{`get($env, 'num')`, reflect.Int},
{`get($env, 'ArrayOfString')`, reflect.Slice},
Expand Down
15 changes: 8 additions & 7 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -842,10 +842,8 @@ func (v *Checker) builtinNode(node *ast.BuiltinNode) Nature {
if !predicateOut.IsBool() && !predicateOut.IsUnknown(&v.config.NtCache) {
return v.error(node.Arguments[1], "predicate should return boolean (got %s)", predicateOut.String())
}
if collection.IsUnknown(&v.config.NtCache) {
return Nature{}
}
return collection.Elem(&v.config.NtCache)
// May return nil when nothing matches; type as any so WarnOnAny catches it.
return v.config.NtCache.FromType(anyType)
}
return v.error(node.Arguments[1], "predicate should has one input and one output param")

Expand All @@ -868,7 +866,8 @@ func (v *Checker) builtinNode(node *ast.BuiltinNode) Nature {
if !predicateOut.IsBool() && !predicateOut.IsUnknown(&v.config.NtCache) {
return v.error(node.Arguments[1], "predicate should return boolean (got %s)", predicateOut.String())
}
return v.config.NtCache.FromType(intType)
// Returns nil (not -1) when nothing matches; type as any so WarnOnAny catches it.
return v.config.NtCache.FromType(anyType)
}
return v.error(node.Arguments[1], "predicate should has one input and one output param")

Expand Down Expand Up @@ -992,12 +991,14 @@ func (v *Checker) checkBuiltinGet(node *ast.BuiltinNode) Nature {
if !prop.IsInteger && !prop.IsUnknown(&v.config.NtCache) {
return v.error(node.Arguments[1], "non-integer slice index %s", prop.String())
}
return base.Elem(&v.config.NtCache)
// Out-of-range get returns nil; type as any so WarnOnAny catches it.
return v.config.NtCache.FromType(anyType)
case reflect.Map:
if !prop.AssignableTo(base.Key(&v.config.NtCache)) && !prop.IsUnknown(&v.config.NtCache) {
return v.error(node.Arguments[1], "cannot use %s to get an element from %s", prop.String(), base.String())
}
return base.Elem(&v.config.NtCache)
// Missing key returns nil; type as any so WarnOnAny catches it.
return v.config.NtCache.FromType(anyType)
}
return v.error(node.Arguments[0], "type %v does not support indexing", base.String())
}
Expand Down
19 changes: 19 additions & 0 deletions checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1183,3 +1183,22 @@ func TestCheck_types(t *testing.T) {
})
}
}

func TestCheck_NilableBuiltinsWarnOnAny(t *testing.T) {
env := map[string]any{
"ints": []int{1, 2, 3},
"empty": []int{},
}
for _, src := range []string{
"find(ints, # > 5)",
"findIndex(ints, # > 5)",
"first(empty)",
"get(ints, 9)",
} {
t.Run(src, func(t *testing.T) {
_, err := expr.Compile(src, expr.Env(env), expr.AsInt(), expr.WarnOnAny())
require.Error(t, err)
require.Contains(t, err.Error(), "expected int")
})
}
}