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()
|
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
|
// TemplateJoinExpr is used to convert tuples of strings produced by template
|
||||||
// constructs (i.e. for loops) into flat strings, by converting the values
|
// 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
|
// 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) {
|
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)
|
traversal, diags := hcl.AbsTraversalForExpr(expr)
|
||||||
if len(diags) != 0 {
|
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)
|
t.Fatalf("wrong traversal %#v; want length 3", traversal)
|
||||||
}
|
}
|
||||||
if traversal.RootName() != "a" {
|
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{})
|
expr, _ := ParseExpression([]byte("[0, a, true]"), "", hcl.Pos{})
|
||||||
exprs, diags := hcl.ExprList(expr)
|
exprs, diags := hcl.ExprList(expr)
|
||||||
if len(diags) != 0 {
|
if len(diags) != 0 {
|
||||||
t.Fatalf("unexpected diagnostics")
|
t.Fatalf("unexpected diagnostics:\n%s", diags.Error())
|
||||||
}
|
}
|
||||||
if len(exprs) != 3 {
|
if len(exprs) != 3 {
|
||||||
t.Fatalf("wrong result %#v; want length 3", exprs)
|
t.Fatalf("wrong result %#v; want length 3", exprs)
|
||||||
|
@ -853,6 +853,14 @@ Traversal:
|
|||||||
SrcRange: rng,
|
SrcRange: rng,
|
||||||
}
|
}
|
||||||
ret = makeRelativeTraversal(ret, step, 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 {
|
} else {
|
||||||
rng := hcl.RangeBetween(open.Range, close.Range)
|
rng := hcl.RangeBetween(open.Range, close.Range)
|
||||||
ret = &IndexExpr{
|
ret = &IndexExpr{
|
||||||
|
@ -36,7 +36,7 @@ func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
|
|||||||
&Diagnostic{
|
&Diagnostic{
|
||||||
Severity: DiagError,
|
Severity: DiagError,
|
||||||
Summary: "Invalid expression",
|
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(),
|
Subject: expr.Range().Ptr(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user