f1f3985230
So far the expression parentheses syntax has been handled entirely in the parser and has been totally invisible in the AST. That's fine for typical expression evaluation, but over the years it's led to a few quirky behaviors in less common situations where we've assumed that all expressions are covered by the AST itself or by the source ranges that the AST captures. In particular, hclwrite assumes that all expressions will have source ranges that cover their tokens, and it generates an incorrect physical syntax tree when the AST doesn't uphold that. After resisting through a few other similar bugs, this commit finally introduces an explicit AST node for parentheses, which makes the parentheses explicit in the AST and captures the larger source range that includes the TokenOParen and the TokenCParen. This means that parentheses will now be visible as a distinct node when walking the AST, as reflected in the updated tests here. That may cause downstream applications that traverse the tree to exhibit different behaviors but we're not considering that as a "breaking change" because the Walk function doesn't make any guarantees about the specific AST shape.
336 lines
6.4 KiB
Go
336 lines
6.4 KiB
Go
package hclsyntax
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
)
|
|
|
|
func TestBlocksAtPos(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Src string
|
|
Pos hcl.Pos
|
|
WantTypes []string
|
|
}{
|
|
"empty": {
|
|
``,
|
|
hcl.Pos{Byte: 0},
|
|
nil,
|
|
},
|
|
"spaces": {
|
|
` `,
|
|
hcl.Pos{Byte: 1},
|
|
nil,
|
|
},
|
|
"single in header": {
|
|
`foo {}`,
|
|
hcl.Pos{Byte: 1},
|
|
[]string{"foo"},
|
|
},
|
|
"single in body": {
|
|
`foo { }`,
|
|
hcl.Pos{Byte: 7},
|
|
[]string{"foo"},
|
|
},
|
|
"single in body with unselected nested": {
|
|
`
|
|
foo {
|
|
|
|
bar {
|
|
|
|
}
|
|
}
|
|
`,
|
|
hcl.Pos{Byte: 10},
|
|
[]string{"foo"},
|
|
},
|
|
"single in body with unselected sibling": {
|
|
`
|
|
foo { }
|
|
bar { }
|
|
`,
|
|
hcl.Pos{Byte: 10},
|
|
[]string{"foo"},
|
|
},
|
|
"selected nested two levels": {
|
|
`
|
|
foo {
|
|
bar {
|
|
|
|
}
|
|
}
|
|
`,
|
|
hcl.Pos{Byte: 20},
|
|
[]string{"foo", "bar"},
|
|
},
|
|
"selected nested three levels": {
|
|
`
|
|
foo {
|
|
bar {
|
|
baz {
|
|
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
hcl.Pos{Byte: 31},
|
|
[]string{"foo", "bar", "baz"},
|
|
},
|
|
"selected nested three levels with unselected sibling after": {
|
|
`
|
|
foo {
|
|
bar {
|
|
baz {
|
|
|
|
}
|
|
}
|
|
not_wanted {}
|
|
}
|
|
`,
|
|
hcl.Pos{Byte: 31},
|
|
[]string{"foo", "bar", "baz"},
|
|
},
|
|
"selected nested three levels with unselected sibling before": {
|
|
`
|
|
foo {
|
|
not_wanted {}
|
|
bar {
|
|
baz {
|
|
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
hcl.Pos{Byte: 49},
|
|
[]string{"foo", "bar", "baz"},
|
|
},
|
|
"unterminated": {
|
|
`foo { `,
|
|
hcl.Pos{Byte: 7},
|
|
[]string{"foo"},
|
|
},
|
|
"unterminated nested": {
|
|
`
|
|
foo {
|
|
bar {
|
|
}
|
|
`,
|
|
hcl.Pos{Byte: 16},
|
|
[]string{"foo", "bar"},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
f, diags := ParseConfig([]byte(test.Src), "", hcl.Pos{Line: 1, Column: 1})
|
|
for _, diag := range diags {
|
|
// We intentionally ignore diagnostics here because we should be
|
|
// able to work with the incomplete configuration that results
|
|
// when the parser does its recovery behavior. However, we do
|
|
// log them in case it's helpful to someone debugging a failing
|
|
// test.
|
|
t.Logf(diag.Error())
|
|
}
|
|
|
|
blocks := f.BlocksAtPos(test.Pos)
|
|
outermost := f.OutermostBlockAtPos(test.Pos)
|
|
innermost := f.InnermostBlockAtPos(test.Pos)
|
|
|
|
gotTypes := make([]string, len(blocks))
|
|
for i, block := range blocks {
|
|
gotTypes[i] = block.Type
|
|
}
|
|
|
|
if len(test.WantTypes) == 0 {
|
|
if len(gotTypes) != 0 {
|
|
t.Errorf("wrong block types\ngot: %#v\nwant: (none)", gotTypes)
|
|
}
|
|
if outermost != nil {
|
|
t.Errorf("wrong outermost type\ngot: %#v\nwant: (none)", outermost.Type)
|
|
}
|
|
if innermost != nil {
|
|
t.Errorf("wrong innermost type\ngot: %#v\nwant: (none)", innermost.Type)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !reflect.DeepEqual(gotTypes, test.WantTypes) {
|
|
if len(gotTypes) != 0 {
|
|
t.Errorf("wrong block types\ngot: %#v\nwant: %#v", gotTypes, test.WantTypes)
|
|
}
|
|
}
|
|
if got, want := outermost.Type, test.WantTypes[0]; got != want {
|
|
t.Errorf("wrong outermost type\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
if got, want := innermost.Type, test.WantTypes[len(test.WantTypes)-1]; got != want {
|
|
t.Errorf("wrong innermost type\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAttributeAtPos(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Src string
|
|
Pos hcl.Pos
|
|
WantName string
|
|
}{
|
|
"empty": {
|
|
``,
|
|
hcl.Pos{Byte: 0},
|
|
"",
|
|
},
|
|
"top-level": {
|
|
`foo = 1`,
|
|
hcl.Pos{Byte: 0},
|
|
"foo",
|
|
},
|
|
"top-level with ignored sibling after": {
|
|
`
|
|
foo = 1
|
|
bar = 2
|
|
`,
|
|
hcl.Pos{Byte: 6},
|
|
"foo",
|
|
},
|
|
"top-level ignored sibling before": {
|
|
`
|
|
foo = 1
|
|
bar = 2
|
|
`,
|
|
hcl.Pos{Byte: 17},
|
|
"bar",
|
|
},
|
|
"nested": {
|
|
`
|
|
foo {
|
|
bar = 2
|
|
}
|
|
`,
|
|
hcl.Pos{Byte: 17},
|
|
"bar",
|
|
},
|
|
"nested in unterminated block": {
|
|
`
|
|
foo {
|
|
bar = 2
|
|
`,
|
|
hcl.Pos{Byte: 17},
|
|
"bar",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
f, diags := ParseConfig([]byte(test.Src), "", hcl.Pos{Line: 1, Column: 1})
|
|
for _, diag := range diags {
|
|
// We intentionally ignore diagnostics here because we should be
|
|
// able to work with the incomplete configuration that results
|
|
// when the parser does its recovery behavior. However, we do
|
|
// log them in case it's helpful to someone debugging a failing
|
|
// test.
|
|
t.Logf(diag.Error())
|
|
}
|
|
|
|
got := f.AttributeAtPos(test.Pos)
|
|
|
|
if test.WantName == "" {
|
|
if got != nil {
|
|
t.Errorf("wrong attribute name\ngot: %#v\nwant: (none)", got.Name)
|
|
}
|
|
return
|
|
}
|
|
|
|
if got == nil {
|
|
t.Fatalf("wrong attribute name\ngot: (none)\nwant: %#v", test.WantName)
|
|
}
|
|
|
|
if got.Name != test.WantName {
|
|
t.Errorf("wrong attribute name\ngot: %#v\nwant: %#v", got.Name, test.WantName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOutermostExprAtPos(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Src string
|
|
Pos hcl.Pos
|
|
WantSrc string
|
|
}{
|
|
"empty": {
|
|
``,
|
|
hcl.Pos{Byte: 0},
|
|
``,
|
|
},
|
|
"simple bool": {
|
|
`a = true`,
|
|
hcl.Pos{Byte: 6},
|
|
`true`,
|
|
},
|
|
"simple reference": {
|
|
`a = blah`,
|
|
hcl.Pos{Byte: 6},
|
|
`blah`,
|
|
},
|
|
"attribute reference": {
|
|
`a = blah.foo`,
|
|
hcl.Pos{Byte: 6},
|
|
`blah.foo`,
|
|
},
|
|
"parens": {
|
|
`a = (1 + 1)`,
|
|
hcl.Pos{Byte: 6},
|
|
`(1 + 1)`,
|
|
},
|
|
"tuple cons": {
|
|
`a = [1, 2, 3]`,
|
|
hcl.Pos{Byte: 5},
|
|
`[1, 2, 3]`,
|
|
},
|
|
"function call": {
|
|
`a = foom("a")`,
|
|
hcl.Pos{Byte: 10},
|
|
`foom("a")`,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
inputSrc := []byte(test.Src)
|
|
f, diags := ParseConfig(inputSrc, "", hcl.Pos{Line: 1, Column: 1})
|
|
for _, diag := range diags {
|
|
// We intentionally ignore diagnostics here because we should be
|
|
// able to work with the incomplete configuration that results
|
|
// when the parser does its recovery behavior. However, we do
|
|
// log them in case it's helpful to someone debugging a failing
|
|
// test.
|
|
t.Logf(diag.Error())
|
|
}
|
|
|
|
gotExpr := f.OutermostExprAtPos(test.Pos)
|
|
var gotSrc string
|
|
if gotExpr != nil {
|
|
rng := gotExpr.Range()
|
|
gotSrc = string(rng.SliceBytes(inputSrc))
|
|
}
|
|
|
|
if test.WantSrc == "" {
|
|
if gotExpr != nil {
|
|
t.Errorf("wrong expression source\ngot: %s\nwant: (none)", gotSrc)
|
|
}
|
|
return
|
|
}
|
|
|
|
if gotExpr == nil {
|
|
t.Fatalf("wrong expression source\ngot: (none)\nwant: %s", test.WantSrc)
|
|
}
|
|
|
|
if gotSrc != test.WantSrc {
|
|
t.Errorf("wrong expression source\ngot: %#v\nwant: %#v", gotSrc, test.WantSrc)
|
|
}
|
|
})
|
|
}
|
|
}
|