Skip to content

Commit 3798c08

Browse files
authored
Merge branch 'master' into copilot/fix-bugs-in-code
2 parents 1cec25d + 40bda0b commit 3798c08

10 files changed

Lines changed: 92 additions & 25 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: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -300,19 +300,13 @@ func TestBuiltin_errors(t *testing.T) {
300300
}
301301
}
302302

303-
// The get() builtin must return an error when called with
304-
// insufficient arguments at runtime, even if compile-time checks
305-
// are bypassed (regression test for OSS-Fuzz #479270603).
306-
func TestBuiltin_get_runtime_args_check(t *testing.T) {
303+
func TestBuiltin_env_not_callable(t *testing.T) {
307304
code := `$env(''matches'i'?t:get().UTC())`
308305
env := map[string]any{"t": 1}
309306

310-
program, err := expr.Compile(code, expr.Env(env))
311-
require.NoError(t, err)
312-
313-
_, err = expr.Run(program, env)
307+
_, err := expr.Compile(code, expr.Env(env))
314308
require.Error(t, err)
315-
assert.Contains(t, err.Error(), "invalid number of arguments")
309+
assert.Contains(t, err.Error(), "is not callable")
316310
}
317311

318312
func TestBuiltin_types(t *testing.T) {

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
}

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,

test/fuzz/fuzz_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ func FuzzExpr(f *testing.F) {
3636
regexp.MustCompile(`unknown time zone`),
3737
regexp.MustCompile(`invalid location name`),
3838
regexp.MustCompile(`json: unsupported value`),
39+
regexp.MustCompile(`json: unsupported type`),
3940
regexp.MustCompile(`json: cannot unmarshal .* into Go value of type .*`),
4041
regexp.MustCompile(`unexpected end of JSON input`),
4142
regexp.MustCompile(`memory budget exceeded`),
4243
regexp.MustCompile(`using interface \{} as type .*`),
4344
regexp.MustCompile(`reflect.Value.MapIndex: value of type .* is not assignable to type .*`),
4445
regexp.MustCompile(`reflect: Call using .* as type .*`),
46+
regexp.MustCompile(`reflect: cannot use .* as type .* in .*`),
4547
regexp.MustCompile(`reflect: Call with too few input arguments`),
4648
regexp.MustCompile(`invalid number of arguments`),
4749
regexp.MustCompile(`reflect: call of reflect.Value.Call on .* Value`),

test/issues/924/issue_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package issue_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/expr-lang/expr"
7+
"github.com/expr-lang/expr/internal/testify/require"
8+
)
9+
10+
func TestIssue924_allow_disabling_builtins_and_providing_fn_at_runtime(t *testing.T) {
11+
// We disable the builtin "upper", but do not env information,
12+
// but we can provide a function at runtime.
13+
program, err := expr.Compile(`upper(1)`, expr.DisableBuiltin("upper"))
14+
require.NoError(t, err)
15+
16+
env := map[string]any{
17+
"upper": func(a int) int { return a },
18+
}
19+
20+
out, err := expr.Run(program, env)
21+
require.NoError(t, err)
22+
require.Equal(t, 1, out)
23+
}

vm/vm_test.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,20 +1512,44 @@ func TestVM_StackUnderflow(t *testing.T) {
15121512
}
15131513
}
15141514

1515-
func TestVM_OpCall_InvalidNumberOfArguments(t *testing.T) {
1516-
// This test ensures that calling a function with wrong number of arguments
1517-
// produces a clear error message instead of a panic.
1518-
// Regression test for clusterfuzz issue with expression:
1519-
// $env(''matches' '? :now().UTC(g).d)//
1520-
1515+
func TestVM_EnvNotCallable(t *testing.T) {
1516+
// $env is the environment, not a function.
15211517
env := map[string]any{
15221518
"ok": true,
15231519
}
15241520

15251521
code := `$env('' matches ' '? : now().UTC(g))`
1526-
program, err := expr.Compile(code, expr.Env(env))
1522+
_, err := expr.Compile(code, expr.Env(env))
1523+
require.Error(t, err)
1524+
require.Contains(t, err.Error(), "is not callable")
1525+
}
1526+
1527+
func TestVM_OpCall_InvalidNumberOfArguments(t *testing.T) {
1528+
// Test that the VM validates argument count at runtime.
1529+
// Compile without Env() so compiler generates OpCall without type info.
1530+
program, err := expr.Compile(`fn(1, 2)`)
15271531
require.NoError(t, err)
15281532

1533+
// Run with a function that has different arity
1534+
env := map[string]any{
1535+
"fn": func(a int) int { return a },
1536+
}
1537+
1538+
_, err = expr.Run(program, env)
1539+
require.Error(t, err)
1540+
require.Contains(t, err.Error(), "invalid number of arguments")
1541+
}
1542+
1543+
func TestVM_OpCall_InvalidNumberOfArguments_Variadic(t *testing.T) {
1544+
// Test variadic function with too few arguments.
1545+
program, err := expr.Compile(`fn()`)
1546+
require.NoError(t, err)
1547+
1548+
// Run with a variadic function that requires at least 1 argument
1549+
env := map[string]any{
1550+
"fn": func(first int, rest ...int) int { return first },
1551+
}
1552+
15291553
_, err = expr.Run(program, env)
15301554
require.Error(t, err)
15311555
require.Contains(t, err.Error(), "invalid number of arguments")

0 commit comments

Comments
 (0)