diff --git a/globals/globals_test.go b/globals/globals_test.go index f5afa698c..d906e9ac9 100644 --- a/globals/globals_test.go +++ b/globals/globals_test.go @@ -2840,6 +2840,26 @@ func TestLoadGlobals(t *testing.T) { ), }, }, + { + name: "global with tm_ternary with branches using variables from for-loop", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `[for a in ["a", "b", "c"] : tm_ternary( + a == "a", + tm_upper(a), + "")]`), + ), + }, + }, + want: map[string]*hclwrite.Block{ + "/stack": Globals( + Expr("val", `["A", "", ""]`), + ), + }, + }, { name: "global with tm_ternary with different branch types", layout: []string{"s:stack"}, diff --git a/hcl/eval/eval.go b/hcl/eval/eval.go index 89359973e..0cf10c63a 100644 --- a/hcl/eval/eval.go +++ b/hcl/eval/eval.go @@ -31,9 +31,9 @@ const ( type ( // Context is the variables evaluator. Context struct { - scope project.Path - hclctx *hhcl.EvalContext - ns namespaces + scope project.Path + Internal *hhcl.EvalContext + ns namespaces evaluators map[string]Resolver } @@ -76,7 +76,7 @@ func New(scope project.Path, evaluators ...Resolver) *Context { } evalctx := &Context{ scope: scope, - hclctx: hclctx, + Internal: hclctx, evaluators: map[string]Resolver{}, ns: namespaces{}, } @@ -86,7 +86,7 @@ func New(scope project.Path, evaluators ...Resolver) *Context { } unsetVal := cty.CapsuleVal(unset, &struct{}{}) - evalctx.hclctx.Variables["unset"] = unsetVal + evalctx.Internal.Variables["unset"] = unsetVal return evalctx } @@ -108,14 +108,14 @@ func (c *Context) SetResolver(ev Resolver) { } } } else { - c.hclctx.Variables[ev.Name()] = prevalue + c.Internal.Variables[ev.Name()] = prevalue } } // DeleteResolver removes the resolver. func (c *Context) DeleteResolver(name string) { delete(c.evaluators, name) - delete(c.hclctx.Variables, name) + delete(c.Internal.Variables, name) } // Eval the given expr and all of its dependency references (if needed) @@ -207,7 +207,7 @@ func (c *Context) eval(expr hhcl.Expression, visited map[RefStr]hhcl.Expression) } } - val, diags := expr.Value(c.hclctx) + val, diags := expr.Value(c.Internal) if diags.HasErrors() { return cty.NilVal, errors.E(ErrEval, diags) } @@ -412,33 +412,33 @@ func (ns namespaces) Get(ref Ref) (value, bool) { // SetNamespace will set the given values inside the given namespace on the // evaluation context. func (c *Context) SetNamespace(name string, vals map[string]cty.Value) { - c.hclctx.Variables[name] = cty.ObjectVal(vals) + c.Internal.Variables[name] = cty.ObjectVal(vals) } // SetFunction sets the function in the context. func (c *Context) SetFunction(name string, fn function.Function) { - c.hclctx.Functions[name] = fn + c.Internal.Functions[name] = fn } // DeleteFunction deletes the given function from the context. func (c *Context) DeleteFunction(name string) { - delete(c.hclctx.Functions, name) + delete(c.Internal.Functions, name) } // SetFunctions sets the functions of the context. func (c *Context) SetFunctions(funcs map[string]function.Function) { - c.hclctx.Functions = funcs + c.Internal.Functions = funcs } // DeleteNamespace deletes the namespace name from the context. // If name is not in the context, it's a no-op. func (c *Context) DeleteNamespace(name string) { - delete(c.hclctx.Variables, name) + delete(c.Internal.Variables, name) } // HasNamespace returns true the evaluation context knows this namespace, false otherwise. func (c *Context) HasNamespace(name string) bool { - _, has := c.hclctx.Variables[name] + _, has := c.Internal.Variables[name] return has } @@ -459,8 +459,8 @@ func (c *Context) Copy() *Context { newctx := &hhcl.EvalContext{ Variables: map[string]cty.Value{}, } - newctx.Functions = c.hclctx.Functions - for k, v := range c.hclctx.Variables { + newctx.Functions = c.Internal.Functions + for k, v := range c.Internal.Variables { newctx.Variables[k] = v } return NewContextFrom(newctx) @@ -468,13 +468,13 @@ func (c *Context) Copy() *Context { // Unwrap returns the internal hhcl.EvalContext. func (c *Context) Unwrap() *hhcl.EvalContext { - return c.hclctx + return c.Internal } // NewContextFrom creates a new evaluator from the hashicorp EvalContext. func NewContextFrom(ctx *hhcl.EvalContext) *Context { return &Context{ - hclctx: ctx, + Internal: ctx, } } diff --git a/stdlib/ternary.go b/stdlib/ternary.go index ad8e5490f..9b4904455 100644 --- a/stdlib/ternary.go +++ b/stdlib/ternary.go @@ -55,9 +55,15 @@ func ternary(evalctx *eval.Context, cond cty.Value, val1, val2 cty.Value) (cty.V func evalTernaryBranch(evalctx *eval.Context, arg cty.Value) (cty.Value, error) { closure := customdecode.ExpressionClosureFromVal(arg) + + // some HCL language construct declare variables and pass them down the + // context tree, then we need to use the expression's own underlying EvalContext when available. + bk := evalctx.Internal + evalctx.Internal = closure.EvalContext newexpr, err := evalctx.PartialEval(&ast.CloneExpression{ Expression: closure.Expression.(hclsyntax.Expression), }) + evalctx.Internal = bk if err != nil { return cty.NilVal, errors.E(err, "evaluating tm_ternary branch") }