// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 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", }, }, }, }, }, }, { "a = (\n 1 + 2\n)\nb = 3\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: " (\n 1 + 2\n)", }, }, }, {Type: "comments"}, { Type: "Tokens", Val: "\n", }, }, }, { Type: "Attribute", Children: []TestTreeNode{ {Type: "comments"}, { Type: "identifier", Val: "b", }, { Type: "Tokens", Val: " =", }, { Type: "Expression", Children: []TestTreeNode{ { Type: "Tokens", Val: " 3", }, }, }, {Type: "comments"}, { Type: "Tokens", Val: "\n", }, }, }, }, }, }, { "b {}\n", TestTreeNode{ Type: "Body", Children: []TestTreeNode{ { Type: "Block", Children: []TestTreeNode{ { Type: "comments", }, { Type: "identifier", Val: "b", }, { Type: "blockLabels", }, { 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: "blockLabels", Children: []TestTreeNode{ { 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: "blockLabels", Children: []TestTreeNode{ { Type: "quoted", Val: ` "label"`, }, }, }, { Type: "Tokens", Val: " {", }, { Type: "Body", }, { Type: "Tokens", Val: "}", }, { Type: "Tokens", Val: "\n", }, }, }, }, }, }, { "b \"label1\" /* foo */ \"label2\" {}\n", TestTreeNode{ Type: "Body", Children: []TestTreeNode{ { Type: "Block", Children: []TestTreeNode{ { Type: "comments", }, { Type: "identifier", Val: "b", }, { Type: "blockLabels", Children: []TestTreeNode{ { Type: "quoted", Val: ` "label1"`, }, { // The comment between the labels just // becomes an "unstructured tokens" // node, because this isn't a place // where we expect comments to attach // to a particular object as // documentation. Type: "Tokens", Val: ` /* foo */`, }, { Type: "quoted", Val: ` "label2"`, }, }, }, { 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: "blockLabels", }, { 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.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: "comments", }, { 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: "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: ".*.bar", }, }, }, { Type: "comments", }, { 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: "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: "[*].bar", }, }, }, { 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: "TraverseName", Children: []TestTreeNode{ { Type: "Tokens", Val: ".", }, { Type: "identifier", Val: "baz", }, }, }, }, }, { 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, ) } }) } }