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:
parent
4fba5e1a75
commit
0b64543c96
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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{
|
||||
|
@ -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(),
|
||||
},
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user