hclsyntax: Parse indexing brackets with a string literal as a traversal

A sequence like "foo" is represented in the AST as a TemplateExpr with a
single string literal inside rather than as a string literal node
directly, so we need to recognize that situation during parsing and treat
it as a special case so we can get the intended behavior of representing
that index as a traversal step rather than as a dynamic index operation.

Most of the time this distinction doesn't matter, but it's important for
static analysis use-cases. In particular, hcl.AbsTraversalForExpr will now
accept an expression like foo["bar"] where before it would've rejected it.

This also includes a better error message for when an expression cannot be
recognized as a single traversal. There isn't really any context here to
return a direct reference to the construct that was problematic, which is
what we'd ideally do, but at least this new message includes a summary
of what is allowed and some examples of things that are not allowed as an
aid to understanding what "static variable reference" means.
This commit is contained in:
Martin Atkins 2019-06-17 15:50:56 -07:00
parent 4fba5e1a75
commit 0b64543c96
4 changed files with 55 additions and 6 deletions

View File

@ -89,6 +89,26 @@ func (e *TemplateExpr) StartRange() hcl.Range {
return e.Parts[0].StartRange()
}
// IsStringLiteral returns true if and only if the template consists only of
// single string literal, as would be created for a simple quoted string like
// "foo".
//
// If this function returns true, then calling Value on the same expression
// with a nil EvalContext will return the literal value.
//
// Note that "${"foo"}", "${1}", etc aren't considered literal values for the
// purposes of this method, because the intent of this method is to identify
// situations where the user seems to be explicitly intending literal string
// interpretation, not situations that result in literals as a technicality
// of the template expression unwrapping behavior.
func (e *TemplateExpr) IsStringLiteral() bool {
if len(e.Parts) != 1 {
return false
}
_, ok := e.Parts[0].(*LiteralValueExpr)
return ok
}
// TemplateJoinExpr is used to convert tuples of strings produced by template
// constructs (i.e. for loops) into flat strings, by converting the values
// tos strings and joining them. This AST node is not used directly; it's

View File

@ -1558,16 +1558,37 @@ func TestFunctionCallExprValue(t *testing.T) {
}
func TestExpressionAsTraversal(t *testing.T) {
expr, _ := ParseExpression([]byte("a.b[0]"), "", hcl.Pos{})
expr, _ := ParseExpression([]byte("a.b[0][\"c\"]"), "", hcl.Pos{})
traversal, diags := hcl.AbsTraversalForExpr(expr)
if len(diags) != 0 {
t.Fatalf("unexpected diagnostics")
t.Fatalf("unexpected diagnostics:\n%s", diags.Error())
}
if len(traversal) != 3 {
if len(traversal) != 4 {
t.Fatalf("wrong traversal %#v; want length 3", traversal)
}
if traversal.RootName() != "a" {
t.Fatalf("wrong root name %q; want %q", traversal.RootName(), "a")
t.Errorf("wrong root name %q; want %q", traversal.RootName(), "a")
}
if step, ok := traversal[1].(hcl.TraverseAttr); ok {
if got, want := step.Name, "b"; got != want {
t.Errorf("wrong name %q for step 1; want %q", got, want)
}
} else {
t.Errorf("wrong type %T for step 1; want %T", traversal[1], step)
}
if step, ok := traversal[2].(hcl.TraverseIndex); ok {
if got, want := step.Key, cty.Zero; !want.RawEquals(got) {
t.Errorf("wrong name %#v for step 2; want %#v", got, want)
}
} else {
t.Errorf("wrong type %T for step 2; want %T", traversal[2], step)
}
if step, ok := traversal[3].(hcl.TraverseIndex); ok {
if got, want := step.Key, cty.StringVal("c"); !want.RawEquals(got) {
t.Errorf("wrong name %#v for step 3; want %#v", got, want)
}
} else {
t.Errorf("wrong type %T for step 3; want %T", traversal[3], step)
}
}
@ -1575,7 +1596,7 @@ func TestStaticExpressionList(t *testing.T) {
expr, _ := ParseExpression([]byte("[0, a, true]"), "", hcl.Pos{})
exprs, diags := hcl.ExprList(expr)
if len(diags) != 0 {
t.Fatalf("unexpected diagnostics")
t.Fatalf("unexpected diagnostics:\n%s", diags.Error())
}
if len(exprs) != 3 {
t.Fatalf("wrong result %#v; want length 3", exprs)

View File

@ -853,6 +853,14 @@ Traversal:
SrcRange: rng,
}
ret = makeRelativeTraversal(ret, step, rng)
} else if tmpl, isTmpl := keyExpr.(*TemplateExpr); isTmpl && tmpl.IsStringLiteral() {
litKey, _ := tmpl.Value(nil)
rng := hcl.RangeBetween(open.Range, close.Range)
step := hcl.TraverseIndex{
Key: litKey,
SrcRange: rng,
}
ret = makeRelativeTraversal(ret, step, rng)
} else {
rng := hcl.RangeBetween(open.Range, close.Range)
ret = &IndexExpr{

View File

@ -36,7 +36,7 @@ func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
&Diagnostic{
Severity: DiagError,
Summary: "Invalid expression",
Detail: "A static variable reference is required.",
Detail: "A single static variable reference is required: only attribute access and indexing with constant keys. No calculations, function calls, template expressions, etc are allowed here.",
Subject: expr.Range().Ptr(),
},
}