From aa10d5195aa5d8c0ea60d188ad4e480c5e402a58 Mon Sep 17 00:00:00 2001 From: jv Date: Thu, 26 Mar 2026 15:35:04 -0700 Subject: [PATCH] feat: support DisableBuiltin("$env") to hide the $env variable $env is a hardcoded special variable, not a regular builtin, so DisableBuiltin("$env") previously had no effect. Check config.Disabled before each $env special-case handler in the checker and compiler. When disabled, $env falls through to normal identifier resolution (errors in strict mode as "unknown name $env"), consistent with how DisableBuiltin works for regular builtins. Fixes #710 --- checker/checker.go | 8 ++-- compiler/compiler.go | 2 +- test/issues/710/issue_test.go | 69 +++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 test/issues/710/issue_test.go diff --git a/checker/checker.go b/checker/checker.go index 3620f207..21d2f9b5 100644 --- a/checker/checker.go +++ b/checker/checker.go @@ -252,7 +252,7 @@ func (v *Checker) identifierNode(node *ast.IdentifierNode) Nature { return v.varScopes[i].nature } } - if node.Value == "$env" { + if node.Value == "$env" && !v.config.Disabled["$env"] { return Nature{} } @@ -514,7 +514,7 @@ func (v *Checker) chainNode(node *ast.ChainNode) Nature { func (v *Checker) memberNode(node *ast.MemberNode) Nature { // $env variable - if an, ok := node.Node.(*ast.IdentifierNode); ok && an.Value == "$env" { + if an, ok := node.Node.(*ast.IdentifierNode); ok && an.Value == "$env" && !v.config.Disabled["$env"] { if name, ok := node.Property.(*ast.StringNode); ok { strict := v.config.Strict if node.Optional { @@ -652,7 +652,7 @@ func (v *Checker) callNode(node *ast.CallNode) Nature { } // $env is not callable. - if id, ok := node.Callee.(*ast.IdentifierNode); ok && id.Value == "$env" { + if id, ok := node.Callee.(*ast.IdentifierNode); ok && id.Value == "$env" && !v.config.Disabled["$env"] { return v.error(node, "%s is not callable", v.config.Env.String()) } @@ -965,7 +965,7 @@ func (v *Checker) checkBuiltinGet(node *ast.BuiltinNode) Nature { prop := v.visit(node.Arguments[1]) prop = prop.Deref(&v.config.NtCache) - if id, ok := node.Arguments[0].(*ast.IdentifierNode); ok && id.Value == "$env" { + if id, ok := node.Arguments[0].(*ast.IdentifierNode); ok && id.Value == "$env" && !v.config.Disabled["$env"] { if s, ok := node.Arguments[1].(*ast.StringNode); ok { if nt, ok := v.config.Env.Get(&v.config.NtCache, s.Value); ok { return nt diff --git a/compiler/compiler.go b/compiler/compiler.go index f66cf9ed..f714a933 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -302,7 +302,7 @@ func (c *compiler) IdentifierNode(node *ast.IdentifierNode) { c.emit(OpLoadVar, index) return } - if node.Value == "$env" { + if node.Value == "$env" && (c.config == nil || !c.config.Disabled["$env"]) { c.emit(OpLoadEnv) return } diff --git a/test/issues/710/issue_test.go b/test/issues/710/issue_test.go new file mode 100644 index 00000000..b083580e --- /dev/null +++ b/test/issues/710/issue_test.go @@ -0,0 +1,69 @@ +package issue_test + +import ( + "testing" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/internal/testify/assert" + "github.com/expr-lang/expr/internal/testify/require" +) + +func TestIssue710_disable_env_builtin(t *testing.T) { + env := map[string]any{ + "foo": 42, + } + + t.Run("$env disabled in strict mode", func(t *testing.T) { + _, err := expr.Compile(`$env`, expr.Env(env), expr.DisableBuiltin("$env")) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown name $env") + }) + + t.Run("$env.field disabled in strict mode", func(t *testing.T) { + _, err := expr.Compile(`$env.foo`, expr.Env(env), expr.DisableBuiltin("$env")) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown name $env") + }) + + t.Run("get($env, field) disabled in strict mode", func(t *testing.T) { + _, err := expr.Compile(`get($env, "foo")`, expr.Env(env), expr.DisableBuiltin("$env")) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown name $env") + }) + + t.Run("$env() disabled in strict mode", func(t *testing.T) { + _, err := expr.Compile(`$env()`, expr.Env(env), expr.DisableBuiltin("$env")) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown name $env") + }) + + t.Run("$env disabled without strict mode", func(t *testing.T) { + // Without expr.Env(), the checker runs in non-strict mode. + // Disabled $env falls through to normal identifier resolution, + // which returns nil for unknown names in non-strict mode. + program, err := expr.Compile(`$env`, expr.DisableBuiltin("$env")) + require.NoError(t, err) + + out, err := expr.Run(program, map[string]any{}) + require.NoError(t, err) + assert.Nil(t, out) + }) + + t.Run("$env enabled by default", func(t *testing.T) { + program, err := expr.Compile(`$env.foo`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, 42, out) + }) + + t.Run("$env standalone enabled by default", func(t *testing.T) { + program, err := expr.Compile(`$env`, expr.Env(env)) + require.NoError(t, err) + + out, err := expr.Run(program, env) + require.NoError(t, err) + assert.Equal(t, env, out) + }) +}