From 4fbf622b94c65e2d4a1156fb4168e4e9299f5e35 Mon Sep 17 00:00:00 2001 From: "k.molodyakov" Date: Sat, 7 Feb 2026 11:51:46 +0300 Subject: [PATCH] parser should not throw an error if predicate not present --- builtin/builtin_test.go | 84 +++++++++++++++++++++++++++++++++++++++++ expr_test.go | 37 ++++++++++++++++++ parser/parser.go | 8 +--- 3 files changed, 122 insertions(+), 7 deletions(-) diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index 8f1644b6..6077f157 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -460,6 +460,90 @@ func TestBuiltin_override_and_still_accessible(t *testing.T) { assert.Equal(t, true, out) } +func TestBuiltin_override(t *testing.T) { + env := map[string]any{ + "upper": func(a string) string { return strings.ToLower(a) }, + } + fn := expr.Function( + "upper", + func(params ...any) (any, error) { + s := params[0].(string) + return strings.ToUpper(s[:1]) + strings.ToLower(s[1:]), nil + }) + + t.Run("with compiled env", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, "str", out) + }) + + t.Run("with uncompiled env", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, "STR", out) + }) + + t.Run("with uncompiled env and disabled builtin", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`, expr.DisableBuiltin("upper")) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, "str", out) + }) + + t.Run("with function", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`, fn) + require.NoError(t, err) + + out, err := expr.Run(program, nil) + require.NoError(t, err) + assert.Equal(t, "Str", out) + }) + + t.Run("with function and compiled env", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`, fn, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, "Str", out) + }) + + t.Run("with function and compiled env and disabled builtin", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`, fn, expr.Env(env), expr.DisableBuiltin("upper")) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, "Str", out) + }) + + t.Run("with function and uncompiled env", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`, fn) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, "Str", out) + }) + + t.Run("with function and uncompiled env and disabled builtin", func(t *testing.T) { + program, err := expr.Compile(`upper("str")`, fn, expr.DisableBuiltin("upper")) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, "Str", out) + }) +} + func TestBuiltin_DisableBuiltin(t *testing.T) { t.Run("via env", func(t *testing.T) { for _, b := range builtin.Builtins { diff --git a/expr_test.go b/expr_test.go index 1bce3c8d..52f7e3ba 100644 --- a/expr_test.go +++ b/expr_test.go @@ -1659,6 +1659,43 @@ func TestExpr_call_float_arg_func_with_int(t *testing.T) { } } +func TestExpr_unknown_call_compiled_without_env(t *testing.T) { + t.Run("some function", func(t *testing.T) { + program, err := expr.Compile(`fn(1)`) + require.NoError(t, err) + + env := map[string]any{ + "fn": func(a int) int { return a }, + } + + _, err = expr.Run(program, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot fetch fn from") + + out, err := expr.Run(program, env) + require.NoError(t, err) + require.Equal(t, 1, out) + }) + + t.Run("disabled built-in function", func(t *testing.T) { + program, err := expr.Compile(`upper(1)`, expr.DisableBuiltin("upper")) + require.NoError(t, err) + + env := map[string]any{ + "upper": func(a int) int { return a }, + } + + _, err = expr.Run(program, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot fetch upper from") + + out, err := expr.Run(program, env) + require.NoError(t, err) + require.Equal(t, 1, out) + }) + +} + func TestConstExpr_error_panic(t *testing.T) { env := map[string]any{ "divide": func(a, b int) int { return a / b }, diff --git a/parser/parser.go b/parser/parser.go index a0c6d449..9e24a71e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -575,10 +575,7 @@ func (p *Parser) parseCall(token Token, arguments []Node, checkOverrides bool) N } isOverridden = isOverridden && checkOverrides - if _, ok := predicates[token.Value]; ok && p.config != nil && p.config.Disabled[token.Value] && !isOverridden { - // Disabled predicate without replacement - fail immediately - p.error("unknown name %s", token.Value) - } else if b, ok := predicates[token.Value]; ok && !isOverridden { + if b, ok := predicates[token.Value]; ok && !isOverridden { p.expect(Bracket, "(") // 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 if node == nil { return nil } - } else if _, ok := builtin.Index[token.Value]; ok && p.config != nil && p.config.Disabled[token.Value] && !isOverridden { - // Disabled builtin without replacement - fail immediately - p.error("unknown name %s", token.Value) } else if _, ok := builtin.Index[token.Value]; ok && (p.config == nil || !p.config.Disabled[token.Value]) && !isOverridden { node = p.createNode(&BuiltinNode{ Name: token.Value,