diff --git a/hclsyntax/expression.go b/hclsyntax/expression.go index 14ced03..963ed77 100644 --- a/hclsyntax/expression.go +++ b/hclsyntax/expression.go @@ -804,7 +804,8 @@ func (e *ObjectConsExpr) ExprMap() []hcl.KeyValuePair { // which deals with the special case that a naked identifier in that position // must be interpreted as a literal string rather than evaluated directly. type ObjectConsKeyExpr struct { - Wrapped Expression + Wrapped Expression + ForceNonLiteral bool } func (e *ObjectConsKeyExpr) literalName() string { @@ -834,19 +835,21 @@ func (e *ObjectConsKeyExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnost // (This is handled at evaluation time rather than parse time because // an application using static analysis _can_ accept a naked multi-step // traversal here, if desired.) - if travExpr, isTraversal := e.Wrapped.(*ScopeTraversalExpr); isTraversal && len(travExpr.Traversal) > 1 { - var diags hcl.Diagnostics - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Ambiguous attribute key", - Detail: "If this expression is intended to be a reference, wrap it in parentheses. If it's instead intended as a literal name containing periods, wrap it in quotes to create a string literal.", - Subject: e.Range().Ptr(), - }) - return cty.DynamicVal, diags - } + if !e.ForceNonLiteral { + if travExpr, isTraversal := e.Wrapped.(*ScopeTraversalExpr); isTraversal && len(travExpr.Traversal) > 1 { + var diags hcl.Diagnostics + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ambiguous attribute key", + Detail: "If this expression is intended to be a reference, wrap it in parentheses. If it's instead intended as a literal name containing periods, wrap it in quotes to create a string literal.", + Subject: e.Range().Ptr(), + }) + return cty.DynamicVal, diags + } - if ln := e.literalName(); ln != "" { - return cty.StringVal(ln), nil + if ln := e.literalName(); ln != "" { + return cty.StringVal(ln), nil + } } return e.Wrapped.Value(ctx) } @@ -861,6 +864,12 @@ func (e *ObjectConsKeyExpr) StartRange() hcl.Range { // Implementation for hcl.AbsTraversalForExpr. func (e *ObjectConsKeyExpr) AsTraversal() hcl.Traversal { + // If we're forcing a non-literal then we can never be interpreted + // as a traversal. + if e.ForceNonLiteral { + return nil + } + // We can produce a traversal only if our wrappee can. st, diags := hcl.AbsTraversalForExpr(e.Wrapped) if diags.HasErrors() { diff --git a/hclsyntax/expression_test.go b/hclsyntax/expression_test.go index 34532a2..136d016 100644 --- a/hclsyntax/expression_test.go +++ b/hclsyntax/expression_test.go @@ -455,6 +455,46 @@ upper( cty.EmptyObjectVal, // (due to parser recovery behavior) 1, // Missing key/value separator; Expected an equals sign ("=") to mark the beginning of the attribute value. If you intended to given an attribute name containing periods or spaces, write the name in quotes to create a string literal. }, + { + `{var.greeting = "world"}`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "var": cty.ObjectVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + }), + }, + }, + cty.DynamicVal, + 1, // Ambiguous attribute key + }, + { + `{(var.greeting) = "world"}`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "var": cty.ObjectVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + }), + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, + { + `{"${var.greeting}" = "world"}`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "var": cty.ObjectVal(map[string]cty.Value{ + "greeting": cty.StringVal("hello"), + }), + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, { `{"hello" = "world", "goodbye" = "cruel world"}`, nil, diff --git a/hclsyntax/parser.go b/hclsyntax/parser.go index 34c18c8..d9880f8 100644 --- a/hclsyntax/parser.go +++ b/hclsyntax/parser.go @@ -1291,6 +1291,13 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) { break } + // Wrapping parens are not explicitly represented in the AST, but + // we want to use them here to disambiguate intepreting a mapping + // key as a full expression rather than just a name, and so + // we'll remember this was present and use it to force the + // behavior of our final ObjectConsKeyExpr. + forceNonLiteral := (p.Peek().Type == TokenOParen) + var key Expression var keyDiags hcl.Diagnostics key, keyDiags = p.ParseExpression() @@ -1307,7 +1314,10 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) { // We wrap up the key expression in a special wrapper that deals // with our special case that naked identifiers as object keys // are interpreted as literal strings. - key = &ObjectConsKeyExpr{Wrapped: key} + key = &ObjectConsKeyExpr{ + Wrapped: key, + ForceNonLiteral: forceNonLiteral, + } next = p.Peek() if next.Type != TokenEqual && next.Type != TokenColon {