hclsyntax: Allow parens to force mapping key to be expression

Our error message for the ambiguous situation recommends doing this, but
the parser didn't actually previously allow it. Now we'll accept the form
that the error message recommends.

As before, we also accept a template with an interpolation sequence as
a disambiguation, but the error message doesn't mention that because it's
no longer idiomatic to use an inline string template containing just a
single interpolation sequence.
This commit is contained in:
Martin Atkins 2019-09-11 15:31:32 -07:00
parent ee38c67330
commit 22ba006718
3 changed files with 73 additions and 14 deletions

View File

@ -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() {

View File

@ -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,

View File

@ -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 {