Skip to content

Commit 11c8951

Browse files
authored
Merge branch 'master' into perf/scope-pooling
2 parents 9e6a2e6 + 94ec86d commit 11c8951

15 files changed

Lines changed: 185 additions & 24 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
go-versions: [ '1.18', '1.22', '1.24', '1.25' ]
14+
go-versions: [ '1.18', '1.22', '1.24', '1.25', '1.26' ]
1515
go-arch: [ '386' ]
1616
steps:
1717
- uses: actions/checkout@v3

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24', '1.25' ]
14+
go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24', '1.25', '1.26' ]
1515
steps:
1616
- uses: actions/checkout@v3
1717
- name: Setup Go ${{ matrix.go-version }}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ func main() {
170170
* [WunderGraph Cosmo](https://github.com/wundergraph/cosmo) - GraphQL Federeration Router uses Expr to customize Middleware behaviour
171171
* [SOLO](https://solo.one) uses Expr interally to allow dynamic code execution with custom defined functions.
172172
* [Naoma.AI](https://www.naoma.ai) uses Expr as a part of its call scoring engine.
173+
* [GlassFlow.dev](https://github.com/glassflow/clickhouse-etl) uses Expr to do realtime data transformation in ETL pipelines
173174

174175
[Add your company too](https://github.com/expr-lang/expr/edit/master/README.md)
175176

builtin/builtin_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ func TestBuiltin_errors(t *testing.T) {
283283
{`timezone(nil)`, "cannot use nil as argument (type string) to call timezone (1:10)"},
284284
{`flatten([1, 2], [3, 4])`, "invalid number of arguments (expected 1, got 2)"},
285285
{`flatten(1)`, "cannot flatten int"},
286+
{`fromJSON("5e2482")`, "cannot unmarshal number"},
286287
}
287288
for _, test := range errorTests {
288289
t.Run(test.input, func(t *testing.T) {
@@ -299,6 +300,15 @@ func TestBuiltin_errors(t *testing.T) {
299300
}
300301
}
301302

303+
func TestBuiltin_env_not_callable(t *testing.T) {
304+
code := `$env(''matches'i'?t:get().UTC())`
305+
env := map[string]any{"t": 1}
306+
307+
_, err := expr.Compile(code, expr.Env(env))
308+
require.Error(t, err)
309+
assert.Contains(t, err.Error(), "is not callable")
310+
}
311+
302312
func TestBuiltin_types(t *testing.T) {
303313
env := map[string]any{
304314
"num": 42,

builtin/lib.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ func flatten(arg reflect.Value, depth int) ([]any, error) {
564564
}
565565

566566
func get(params ...any) (out any, err error) {
567+
if len(params) < 2 {
568+
return nil, fmt.Errorf("invalid number of arguments (expected 2, got %d)", len(params))
569+
}
567570
from := params[0]
568571
i := params[1]
569572
v := reflect.ValueOf(from)

checker/checker.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,11 @@ func (v *Checker) callNode(node *ast.CallNode) Nature {
651651
return *node.Nature()
652652
}
653653

654+
// $env is not callable.
655+
if id, ok := node.Callee.(*ast.IdentifierNode); ok && id.Value == "$env" {
656+
return v.error(node, "%s is not callable", v.config.Env.String())
657+
}
658+
654659
nt := v.visit(node.Callee)
655660
if nt.IsUnknown(&v.config.NtCache) {
656661
return Nature{}

checker/checker_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,30 @@ invalid operation: > (mismatched types string and int) (1:30)
681681
invalid operation: + (mismatched types int and bool) (1:6)
682682
| 1; 2 + true; 3
683683
| .....^
684+
`,
685+
},
686+
{
687+
`$env()`,
688+
`
689+
mock.Env is not callable (1:1)
690+
| $env()
691+
| ^
692+
`,
693+
},
694+
{
695+
`$env(1)`,
696+
`
697+
mock.Env is not callable (1:1)
698+
| $env(1)
699+
| ^
700+
`,
701+
},
702+
{
703+
`$env(abs())`,
704+
`
705+
mock.Env is not callable (1:1)
706+
| $env(abs())
707+
| ^
684708
`,
685709
},
686710
}

expr.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,14 @@ func EnableBuiltin(name string) Option {
195195

196196
// WithContext passes context to all functions calls with a context.Context argument.
197197
func WithContext(name string) Option {
198-
return Patch(patcher.WithContext{
199-
Name: name,
200-
})
198+
return func(c *conf.Config) {
199+
c.Visitors = append(c.Visitors, patcher.WithContext{
200+
Name: name,
201+
Functions: c.Functions,
202+
Env: &c.Env,
203+
NtCache: &c.NtCache,
204+
})
205+
}
201206
}
202207

203208
// Timezone sets default timezone for date() and now() builtin functions.

parser/parser.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -575,10 +575,7 @@ func (p *Parser) parseCall(token Token, arguments []Node, checkOverrides bool) N
575575
}
576576
isOverridden = isOverridden && checkOverrides
577577

578-
if _, ok := predicates[token.Value]; ok && p.config != nil && p.config.Disabled[token.Value] && !isOverridden {
579-
// Disabled predicate without replacement - fail immediately
580-
p.error("unknown name %s", token.Value)
581-
} else if b, ok := predicates[token.Value]; ok && !isOverridden {
578+
if b, ok := predicates[token.Value]; ok && !isOverridden {
582579
p.expect(Bracket, "(")
583580

584581
// In case of the pipe operator, the first argument is the left-hand side
@@ -622,9 +619,6 @@ func (p *Parser) parseCall(token Token, arguments []Node, checkOverrides bool) N
622619
if node == nil {
623620
return nil
624621
}
625-
} else if _, ok := builtin.Index[token.Value]; ok && p.config != nil && p.config.Disabled[token.Value] && !isOverridden {
626-
// Disabled builtin without replacement - fail immediately
627-
p.error("unknown name %s", token.Value)
628622
} else if _, ok := builtin.Index[token.Value]; ok && (p.config == nil || !p.config.Disabled[token.Value]) && !isOverridden {
629623
node = p.createNode(&BuiltinNode{
630624
Name: token.Value,

patcher/with_context.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import (
44
"reflect"
55

66
"github.com/expr-lang/expr/ast"
7+
"github.com/expr-lang/expr/checker/nature"
8+
"github.com/expr-lang/expr/conf"
79
)
810

911
// WithContext adds WithContext.Name argument to all functions calls with a context.Context argument.
1012
type WithContext struct {
11-
Name string
13+
Name string
14+
Functions conf.FunctionsTable // Optional: used to look up function types when callee type is unknown.
15+
Env *nature.Nature // Optional: used to look up method types when callee type is unknown.
16+
NtCache *nature.Cache // Optional: cache for nature lookups.
1217
}
1318

1419
// Visit adds WithContext.Name argument to all functions calls with a context.Context argument.
@@ -19,6 +24,24 @@ func (w WithContext) Visit(node *ast.Node) {
1924
if fn == nil {
2025
return
2126
}
27+
// If callee type is interface{} (unknown), look up the function type from
28+
// the Functions table or Env. This handles cases where the checker returns early
29+
// without visiting nested call arguments (e.g., Date2() in Now2().After(Date2()))
30+
// because the outer call's type is unknown due to missing context arguments.
31+
if fn.Kind() == reflect.Interface {
32+
if ident, ok := call.Callee.(*ast.IdentifierNode); ok {
33+
if w.Functions != nil {
34+
if f, ok := w.Functions[ident.Value]; ok {
35+
fn = f.Type()
36+
}
37+
}
38+
if fn.Kind() == reflect.Interface && w.Env != nil {
39+
if m, ok := w.Env.MethodByName(w.NtCache, ident.Value); ok {
40+
fn = m.Type
41+
}
42+
}
43+
}
44+
}
2245
if fn.Kind() != reflect.Func {
2346
return
2447
}

0 commit comments

Comments
 (0)