hcl/hclwrite/parser_test.go
Martin Atkins 63e2897c12 hclsyntax: Source range of IndexExpr must cover whole expression
Some HCL callers make the (reasonable) assumption that the overall source
range of an expression will be a superset of all of the ranges of its
child expressions, for purposes such as extraction of source code
snippets, parse tree annotation in hclwrite, text editor analysis
functions like "go to reference", etc.

The IndexExpr type was not previously honoring that assumption, since its
source range was placed around only the bracket portion. That is a good
region to use when reporting errors relating to the index operation, but
it is not a faithful representation of the full extent of the expression.

In order to meet both of these requirements at once, IndexExpr now has
both SrcRange covering the entire expression and BracketRange covering
the index part delimited by brackets. We can then use BracketRange in
our error messages but return SrcRange as the result of the general
Range method that is common to all expression types.
2019-12-06 09:09:18 -08:00

1106 lines
19 KiB
Go

package hclwrite
import (
"fmt"
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/kylelemons/godebug/pretty"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func TestParse(t *testing.T) {
tests := []struct {
src string
want TestTreeNode
}{
{
"",
TestTreeNode{
Type: "Body",
},
},
{
"a = 1\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: " 1",
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"# aye aye aye\na = 1\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
Val: "# aye aye aye\n",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: " 1",
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"a = 1 # because it is\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: " 1",
},
},
},
{
Type: "comments",
Val: " # because it is\n",
},
},
},
},
},
},
{
"# bee bee bee\n\nb = 1\n", // two newlines separate the comment from the attribute
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Tokens", // Only lead/line comments attached to an object have type "comments"
Val: "# bee bee bee\n\n",
},
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "b",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: " 1",
},
},
},
{
Type: "comments",
Val: "",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"b {}\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Block",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "b",
},
{
Type: "Tokens",
Val: " {",
},
{
Type: "Body",
},
{
Type: "Tokens",
Val: "}",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"b label {}\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Block",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "b",
},
{
Type: "identifier",
Val: ` label`,
},
{
Type: "Tokens",
Val: " {",
},
{
Type: "Body",
},
{
Type: "Tokens",
Val: "}",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"b \"label\" {}\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Block",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "b",
},
{
Type: "quoted",
Val: ` "label"`,
},
{
Type: "Tokens",
Val: " {",
},
{
Type: "Body",
},
{
Type: "Tokens",
Val: "}",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"b {\n a = 1\n}\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Block",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "b",
},
{
Type: "Tokens",
Val: " {",
},
{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: "\n",
},
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: " a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: " 1",
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
{
Type: "Tokens",
Val: "}",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"a = foo\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Traversal",
Children: []TestTreeNode{
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "identifier",
Val: " foo",
},
},
},
},
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"a = foo.bar\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Traversal",
Children: []TestTreeNode{
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "identifier",
Val: " foo",
},
},
},
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: ".",
},
{
Type: "identifier",
Val: "bar",
},
},
},
},
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"a = foo[0]\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Traversal",
Children: []TestTreeNode{
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "identifier",
Val: " foo",
},
},
},
{
Type: "TraverseIndex",
Children: []TestTreeNode{
{
Type: "Tokens",
Val: "[",
},
{
Type: "number",
Val: "0",
},
{
Type: "Tokens",
Val: "]",
},
},
},
},
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"a = foo[bar]\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Traversal",
Children: []TestTreeNode{
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "identifier",
Val: " foo",
},
},
},
},
},
{
Type: "Tokens",
Val: "[",
},
{
Type: "Traversal",
Children: []TestTreeNode{
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "identifier",
Val: "bar",
},
},
},
},
},
{
Type: "Tokens",
Val: "]",
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
{
"a = foo[bar].baz\n",
TestTreeNode{
Type: "Body",
Children: []TestTreeNode{
{
Type: "Attribute",
Children: []TestTreeNode{
{
Type: "comments",
},
{
Type: "identifier",
Val: "a",
},
{
Type: "Tokens",
Val: " =",
},
{
Type: "Expression",
Children: []TestTreeNode{
{
Type: "Traversal",
Children: []TestTreeNode{
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "identifier",
Val: " foo",
},
},
},
},
},
{
Type: "Tokens",
Val: "[",
},
{
Type: "Traversal",
Children: []TestTreeNode{
{
Type: "TraverseName",
Children: []TestTreeNode{
{
Type: "identifier",
Val: "bar",
},
},
},
},
},
{
Type: "Tokens",
Val: "].baz",
},
},
},
{
Type: "comments",
},
{
Type: "Tokens",
Val: "\n",
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.src, func(t *testing.T) {
file, diags := parse([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) > 0 {
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
got := makeTestTree(file.body)
if !cmp.Equal(got, test.want) {
diff := cmp.Diff(got, test.want)
t.Errorf(
"wrong result\ninput:\n%s\n\ngot:\n%s\nwant:%s\n\ndiff:\n%s",
test.src,
spew.Sdump(got),
spew.Sdump(test.want),
diff,
)
}
})
}
}
func TestPartitionTokens(t *testing.T) {
tests := []struct {
tokens hclsyntax.Tokens
rng hcl.Range
wantStart int
wantEnd int
}{
{
hclsyntax.Tokens{},
hcl.Range{
Start: hcl.Pos{Byte: 0},
End: hcl.Pos{Byte: 0},
},
0,
0,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 0},
End: hcl.Pos{Byte: 4},
},
},
},
hcl.Range{
Start: hcl.Pos{Byte: 0},
End: hcl.Pos{Byte: 4},
},
0,
1,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 0},
End: hcl.Pos{Byte: 4},
},
},
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 4},
End: hcl.Pos{Byte: 8},
},
},
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 8},
End: hcl.Pos{Byte: 12},
},
},
},
hcl.Range{
Start: hcl.Pos{Byte: 4},
End: hcl.Pos{Byte: 8},
},
1,
2,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 0},
End: hcl.Pos{Byte: 4},
},
},
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 4},
End: hcl.Pos{Byte: 8},
},
},
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 8},
End: hcl.Pos{Byte: 12},
},
},
},
hcl.Range{
Start: hcl.Pos{Byte: 0},
End: hcl.Pos{Byte: 8},
},
0,
2,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 0},
End: hcl.Pos{Byte: 4},
},
},
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 4},
End: hcl.Pos{Byte: 8},
},
},
{
Type: hclsyntax.TokenIdent,
Range: hcl.Range{
Start: hcl.Pos{Byte: 8},
End: hcl.Pos{Byte: 12},
},
},
},
hcl.Range{
Start: hcl.Pos{Byte: 4},
End: hcl.Pos{Byte: 12},
},
1,
3,
},
}
prettyConfig := &pretty.Config{
Diffable: true,
IncludeUnexported: true,
PrintStringers: true,
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
gotStart, gotEnd := partitionTokens(test.tokens, test.rng)
if gotStart != test.wantStart || gotEnd != test.wantEnd {
t.Errorf(
"wrong result\ntokens: %s\nrange: %#v\ngot: %d, %d\nwant: %d, %d",
prettyConfig.Sprint(test.tokens), test.rng,
gotStart, test.wantStart,
gotEnd, test.wantEnd,
)
}
})
}
}
func TestPartitionLeadCommentTokens(t *testing.T) {
tests := []struct {
tokens hclsyntax.Tokens
wantStart int
}{
{
hclsyntax.Tokens{},
0,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenComment,
},
},
0,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenComment,
},
{
Type: hclsyntax.TokenComment,
},
},
0,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenComment,
},
{
Type: hclsyntax.TokenNewline,
},
},
2,
},
{
hclsyntax.Tokens{
{
Type: hclsyntax.TokenComment,
},
{
Type: hclsyntax.TokenNewline,
},
{
Type: hclsyntax.TokenComment,
},
},
2,
},
}
prettyConfig := &pretty.Config{
Diffable: true,
IncludeUnexported: true,
PrintStringers: true,
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
gotStart := partitionLeadCommentTokens(test.tokens)
if gotStart != test.wantStart {
t.Errorf(
"wrong result\ntokens: %s\ngot: %d\nwant: %d",
prettyConfig.Sprint(test.tokens),
gotStart, test.wantStart,
)
}
})
}
}
func TestLexConfig(t *testing.T) {
tests := []struct {
input string
want Tokens
}{
{
`a b `,
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`a`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`b`),
SpacesBefore: 2,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 1,
},
},
},
{
`
foo "bar" "baz" {
pizza = " cheese "
}
`,
Tokens{
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`bar`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`baz`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte(`{`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte("\n"),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`pizza`),
SpacesBefore: 4,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte(`=`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(` cheese `),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte("\n"),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte("\n"),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
}
prettyConfig := &pretty.Config{
Diffable: true,
IncludeUnexported: true,
PrintStringers: true,
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
got := lexConfig([]byte(test.input))
if !reflect.DeepEqual(got, test.want) {
diff := prettyConfig.Compare(test.want, got)
t.Errorf(
"wrong result\ninput: %s\ndiff: %s", test.input, diff,
)
}
})
}
}