Skip to content

Commit aa10d51

Browse files
committed
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
1 parent b90e77c commit aa10d51

3 files changed

Lines changed: 74 additions & 5 deletions

File tree

checker/checker.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ func (v *Checker) identifierNode(node *ast.IdentifierNode) Nature {
252252
return v.varScopes[i].nature
253253
}
254254
}
255-
if node.Value == "$env" {
255+
if node.Value == "$env" && !v.config.Disabled["$env"] {
256256
return Nature{}
257257
}
258258

@@ -514,7 +514,7 @@ func (v *Checker) chainNode(node *ast.ChainNode) Nature {
514514

515515
func (v *Checker) memberNode(node *ast.MemberNode) Nature {
516516
// $env variable
517-
if an, ok := node.Node.(*ast.IdentifierNode); ok && an.Value == "$env" {
517+
if an, ok := node.Node.(*ast.IdentifierNode); ok && an.Value == "$env" && !v.config.Disabled["$env"] {
518518
if name, ok := node.Property.(*ast.StringNode); ok {
519519
strict := v.config.Strict
520520
if node.Optional {
@@ -652,7 +652,7 @@ func (v *Checker) callNode(node *ast.CallNode) Nature {
652652
}
653653

654654
// $env is not callable.
655-
if id, ok := node.Callee.(*ast.IdentifierNode); ok && id.Value == "$env" {
655+
if id, ok := node.Callee.(*ast.IdentifierNode); ok && id.Value == "$env" && !v.config.Disabled["$env"] {
656656
return v.error(node, "%s is not callable", v.config.Env.String())
657657
}
658658

@@ -965,7 +965,7 @@ func (v *Checker) checkBuiltinGet(node *ast.BuiltinNode) Nature {
965965
prop := v.visit(node.Arguments[1])
966966
prop = prop.Deref(&v.config.NtCache)
967967

968-
if id, ok := node.Arguments[0].(*ast.IdentifierNode); ok && id.Value == "$env" {
968+
if id, ok := node.Arguments[0].(*ast.IdentifierNode); ok && id.Value == "$env" && !v.config.Disabled["$env"] {
969969
if s, ok := node.Arguments[1].(*ast.StringNode); ok {
970970
if nt, ok := v.config.Env.Get(&v.config.NtCache, s.Value); ok {
971971
return nt

compiler/compiler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ func (c *compiler) IdentifierNode(node *ast.IdentifierNode) {
302302
c.emit(OpLoadVar, index)
303303
return
304304
}
305-
if node.Value == "$env" {
305+
if node.Value == "$env" && (c.config == nil || !c.config.Disabled["$env"]) {
306306
c.emit(OpLoadEnv)
307307
return
308308
}

test/issues/710/issue_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package issue_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/expr-lang/expr"
7+
"github.com/expr-lang/expr/internal/testify/assert"
8+
"github.com/expr-lang/expr/internal/testify/require"
9+
)
10+
11+
func TestIssue710_disable_env_builtin(t *testing.T) {
12+
env := map[string]any{
13+
"foo": 42,
14+
}
15+
16+
t.Run("$env disabled in strict mode", func(t *testing.T) {
17+
_, err := expr.Compile(`$env`, expr.Env(env), expr.DisableBuiltin("$env"))
18+
require.Error(t, err)
19+
assert.Contains(t, err.Error(), "unknown name $env")
20+
})
21+
22+
t.Run("$env.field disabled in strict mode", func(t *testing.T) {
23+
_, err := expr.Compile(`$env.foo`, expr.Env(env), expr.DisableBuiltin("$env"))
24+
require.Error(t, err)
25+
assert.Contains(t, err.Error(), "unknown name $env")
26+
})
27+
28+
t.Run("get($env, field) disabled in strict mode", func(t *testing.T) {
29+
_, err := expr.Compile(`get($env, "foo")`, expr.Env(env), expr.DisableBuiltin("$env"))
30+
require.Error(t, err)
31+
assert.Contains(t, err.Error(), "unknown name $env")
32+
})
33+
34+
t.Run("$env() disabled in strict mode", func(t *testing.T) {
35+
_, err := expr.Compile(`$env()`, expr.Env(env), expr.DisableBuiltin("$env"))
36+
require.Error(t, err)
37+
assert.Contains(t, err.Error(), "unknown name $env")
38+
})
39+
40+
t.Run("$env disabled without strict mode", func(t *testing.T) {
41+
// Without expr.Env(), the checker runs in non-strict mode.
42+
// Disabled $env falls through to normal identifier resolution,
43+
// which returns nil for unknown names in non-strict mode.
44+
program, err := expr.Compile(`$env`, expr.DisableBuiltin("$env"))
45+
require.NoError(t, err)
46+
47+
out, err := expr.Run(program, map[string]any{})
48+
require.NoError(t, err)
49+
assert.Nil(t, out)
50+
})
51+
52+
t.Run("$env enabled by default", func(t *testing.T) {
53+
program, err := expr.Compile(`$env.foo`, expr.Env(env))
54+
require.NoError(t, err)
55+
56+
out, err := expr.Run(program, env)
57+
require.NoError(t, err)
58+
assert.Equal(t, 42, out)
59+
})
60+
61+
t.Run("$env standalone enabled by default", func(t *testing.T) {
62+
program, err := expr.Compile(`$env`, expr.Env(env))
63+
require.NoError(t, err)
64+
65+
out, err := expr.Run(program, env)
66+
require.NoError(t, err)
67+
assert.Equal(t, env, out)
68+
})
69+
}

0 commit comments

Comments
 (0)