hclsyntax: Allow Terraform-style legacy index form

Terraform allowed indexing like foo.0.bar to work around HIL limitations,
and so we'll permit that as a pragmatic way to accept existing Terraform
configurations.

However, we can't support this fully because our parser thinks that
chained number indexes, like foo.0.0.bar, are single numbers. Since that
usage in Terraform is very rare (there are very few lists of lists) we
will mark that situation as an error with a helpful message suggesting
to use the modern index syntax instead.

This also turned up a similar bug in the existing legacy index handling
we were doing for splat expressions, which is now handled in the same
way.
This commit is contained in:
Martin Atkins 2018-03-03 09:02:29 -08:00
parent 061412b83a
commit 074b73b8b5
3 changed files with 161 additions and 0 deletions

View File

@ -762,6 +762,15 @@ upper(
}),
0,
},
{
`[[[{name:"foo"}]], [[{name:"bar"}], [{name:"baz"}]]].*.0.0.name`,
nil,
cty.TupleVal([]cty.Value{
cty.DynamicVal,
cty.DynamicVal,
}),
1, // can't chain legacy index syntax together, like .0.0 (because 0.0 parses as a single number)
},
{
// For an "attribute-only" splat, an index operator applies to
// the splat result as a whole, rather than being incorporated
@ -787,6 +796,24 @@ upper(
cty.StringVal("hello"),
0,
},
{
`["hello"].0`,
nil,
cty.StringVal("hello"),
0,
},
{
`[["hello"]].0.0`,
nil,
cty.DynamicVal,
1, // can't chain legacy index syntax together (because 0.0 parses as 0)
},
{
`[{greeting = "hello"}].0.greeting`,
nil,
cty.StringVal("hello"),
0,
},
{
`[][0]`,
nil,

View File

@ -477,6 +477,53 @@ Traversal:
ret = makeRelativeTraversal(ret, step, rng)
case TokenNumberLit:
// This is a weird form we inherited from HIL, allowing numbers
// to be used as attributes as a weird way of writing [n].
// This was never actually a first-class thing in HIL, but
// HIL tolerated sequences like .0. in its variable names and
// calling applications like Terraform exploited that to
// introduce indexing syntax where none existed.
numTok := p.Read() // eat token
attrTok = numTok
// This syntax is ambiguous if multiple indices are used in
// succession, like foo.0.1.baz: that actually parses as
// a fractional number 0.1. Since we're only supporting this
// syntax for compatibility with legacy Terraform
// configurations, and Terraform does not tend to have lists
// of lists, we'll choose to reject that here with a helpful
// error message, rather than failing later because the index
// isn't a whole number.
if dotIdx := bytes.IndexByte(numTok.Bytes, '.'); dotIdx >= 0 {
first := numTok.Bytes[:dotIdx]
second := numTok.Bytes[dotIdx+1:]
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid legacy index syntax",
Detail: fmt.Sprintf("When using the legacy index syntax, chaining two indexes together is not permitted. Use the proper index syntax instead, like [%s][%s].", first, second),
Subject: &attrTok.Range,
})
rng := hcl.RangeBetween(dot.Range, numTok.Range)
step := hcl.TraverseIndex{
Key: cty.DynamicVal,
SrcRange: rng,
}
ret = makeRelativeTraversal(ret, step, rng)
break
}
numVal, numDiags := p.numberLitValue(numTok)
diags = append(diags, numDiags...)
rng := hcl.RangeBetween(dot.Range, numTok.Range)
step := hcl.TraverseIndex{
Key: numVal,
SrcRange: rng,
}
ret = makeRelativeTraversal(ret, step, rng)
case TokenStar:
// "Attribute-only" splat expression.
// (This is a kinda weird construct inherited from HIL, which
@ -497,6 +544,27 @@ Traversal:
// into a list, for expressions like:
// foo.bar.*.baz.0.foo
numTok := p.Read()
// Weird special case if the user writes something
// like foo.bar.*.baz.0.0.foo, where 0.0 parses
// as a number.
if dotIdx := bytes.IndexByte(numTok.Bytes, '.'); dotIdx >= 0 {
first := numTok.Bytes[:dotIdx]
second := numTok.Bytes[dotIdx+1:]
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid legacy index syntax",
Detail: fmt.Sprintf("When using the legacy index syntax, chaining two indexes together is not permitted. Use the proper index syntax with a full splat expression [*] instead, like [%s][%s].", first, second),
Subject: &attrTok.Range,
})
trav = append(trav, hcl.TraverseIndex{
Key: cty.DynamicVal,
SrcRange: hcl.RangeBetween(dot.Range, numTok.Range),
})
lastRange = numTok.Range
continue
}
numVal, numDiags := p.numberLitValue(numTok)
diags = append(diags, numDiags...)
trav = append(trav, hcl.TraverseIndex{

View File

@ -1263,6 +1263,72 @@ block "valid" {}
},
},
},
{
"a = foo.0.1.baz\n",
1, // Chaining legacy index syntax is not supported
&Body{
Attributes: Attributes{
"a": {
Name: "a",
Expr: &ScopeTraversalExpr{
Traversal: hcl.Traversal{
hcl.TraverseRoot{
Name: "foo",
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
End: hcl.Pos{Line: 1, Column: 8, Byte: 7},
},
},
hcl.TraverseIndex{
Key: cty.DynamicVal,
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
},
hcl.TraverseAttr{
Name: "baz",
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
},
},
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
End: hcl.Pos{Line: 1, Column: 16, Byte: 15},
},
},
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 16, Byte: 15},
},
NameRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 2, Byte: 1},
},
EqualsRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 3, Byte: 2},
End: hcl.Pos{Line: 1, Column: 4, Byte: 3},
},
},
},
Blocks: Blocks{},
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 1, Byte: 16},
},
EndRange: hcl.Range{
Start: hcl.Pos{Line: 2, Column: 1, Byte: 16},
End: hcl.Pos{Line: 2, Column: 1, Byte: 16},
},
},
},
{
"a = \"${var.public_subnets[count.index]}\"\n",
0,