diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index b7ad16fd..8f1644b6 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -300,19 +300,13 @@ func TestBuiltin_errors(t *testing.T) { } } -// The get() builtin must return an error when called with -// insufficient arguments at runtime, even if compile-time checks -// are bypassed (regression test for OSS-Fuzz #479270603). -func TestBuiltin_get_runtime_args_check(t *testing.T) { +func TestBuiltin_env_not_callable(t *testing.T) { code := `$env(''matches'i'?t:get().UTC())` env := map[string]any{"t": 1} - program, err := expr.Compile(code, expr.Env(env)) - require.NoError(t, err) - - _, err = expr.Run(program, env) + _, err := expr.Compile(code, expr.Env(env)) require.Error(t, err) - assert.Contains(t, err.Error(), "invalid number of arguments") + assert.Contains(t, err.Error(), "is not callable") } func TestBuiltin_types(t *testing.T) { diff --git a/checker/checker.go b/checker/checker.go index 23ed1725..3620f207 100644 --- a/checker/checker.go +++ b/checker/checker.go @@ -651,6 +651,11 @@ func (v *Checker) callNode(node *ast.CallNode) Nature { return *node.Nature() } + // $env is not callable. + if id, ok := node.Callee.(*ast.IdentifierNode); ok && id.Value == "$env" { + return v.error(node, "%s is not callable", v.config.Env.String()) + } + nt := v.visit(node.Callee) if nt.IsUnknown(&v.config.NtCache) { return Nature{} diff --git a/checker/checker_test.go b/checker/checker_test.go index 07047ff5..7a581612 100644 --- a/checker/checker_test.go +++ b/checker/checker_test.go @@ -681,6 +681,30 @@ invalid operation: > (mismatched types string and int) (1:30) invalid operation: + (mismatched types int and bool) (1:6) | 1; 2 + true; 3 | .....^ +`, + }, + { + `$env()`, + ` +mock.Env is not callable (1:1) + | $env() + | ^ +`, + }, + { + `$env(1)`, + ` +mock.Env is not callable (1:1) + | $env(1) + | ^ +`, + }, + { + `$env(abs())`, + ` +mock.Env is not callable (1:1) + | $env(abs()) + | ^ `, }, } diff --git a/vm/vm_test.go b/vm/vm_test.go index 6963803e..a9b09697 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -1512,20 +1512,44 @@ func TestVM_StackUnderflow(t *testing.T) { } } -func TestVM_OpCall_InvalidNumberOfArguments(t *testing.T) { - // This test ensures that calling a function with wrong number of arguments - // produces a clear error message instead of a panic. - // Regression test for clusterfuzz issue with expression: - // $env(''matches' '? :now().UTC(g).d)// - +func TestVM_EnvNotCallable(t *testing.T) { + // $env is the environment, not a function. env := map[string]any{ "ok": true, } code := `$env('' matches ' '? : now().UTC(g))` - program, err := expr.Compile(code, expr.Env(env)) + _, err := expr.Compile(code, expr.Env(env)) + require.Error(t, err) + require.Contains(t, err.Error(), "is not callable") +} + +func TestVM_OpCall_InvalidNumberOfArguments(t *testing.T) { + // Test that the VM validates argument count at runtime. + // Compile without Env() so compiler generates OpCall without type info. + program, err := expr.Compile(`fn(1, 2)`) require.NoError(t, err) + // Run with a function that has different arity + env := map[string]any{ + "fn": func(a int) int { return a }, + } + + _, err = expr.Run(program, env) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid number of arguments") +} + +func TestVM_OpCall_InvalidNumberOfArguments_Variadic(t *testing.T) { + // Test variadic function with too few arguments. + program, err := expr.Compile(`fn()`) + require.NoError(t, err) + + // Run with a variadic function that requires at least 1 argument + env := map[string]any{ + "fn": func(first int, rest ...int) int { return first }, + } + _, err = expr.Run(program, env) require.Error(t, err) require.Contains(t, err.Error(), "invalid number of arguments")