hcl/hclwrite/ast_block_test.go
Alisdair McDiarmid 1818f36094 hclwrite: Allow blank quoted string block labels
The hclsyntax package permits block labels to be blank quoted strings,
and defers to the application to determine if this is valid or not. This
commit updates hclwrite to allow the same behaviour.

This is in response to an upstream bug report on Terraform, which uses
hclwrite to implement its fmt subcommand. Given a block with a blank
quoted string label, it currently deletes the label when formatting.
This is inconsistent with Terraform's behaviour when parsing HCL, which
treats empty labels differently from missing labels.
2020-11-18 09:21:23 -05:00

487 lines
9.5 KiB
Go

package hclwrite
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func TestBlockType(t *testing.T) {
tests := []struct {
src string
want string
}{
{
`
service {
attr0 = "val0"
}
`,
"service",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s", test.want), func(t *testing.T) {
f, diags := ParseConfig([]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")
}
block := f.Body().Blocks()[0]
got := string(block.Type())
if got != test.want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.want)
}
})
}
}
func TestBlockLabels(t *testing.T) {
tests := []struct {
src string
want []string
}{
{
`
nolabel {
}
`,
[]string{},
},
{
`
quoted "label1" {
}
`,
[]string{"label1"},
},
{
`
quoted "label1" "label2" {
}
`,
[]string{"label1", "label2"},
},
{
`
quoted "label1" /* foo */ "label2" {
}
`,
[]string{"label1", "label2"},
},
{
`
unquoted label1 {
}
`,
[]string{"label1"},
},
{
`
unquoted label1 /* foo */ label2 {
}
`,
[]string{"label1", "label2"},
},
{
`
mixed label1 "label2" {
}
`,
[]string{"label1", "label2"},
},
{
`
escape "\u0041" {
}
`,
[]string{"\u0041"},
},
{
`
blank "" {
}
`,
[]string{""},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s", strings.Join(test.want, " ")), func(t *testing.T) {
f, diags := ParseConfig([]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")
}
block := f.Body().Blocks()[0]
got := block.Labels()
if !reflect.DeepEqual(got, test.want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
}
})
}
}
func TestBlockSetType(t *testing.T) {
tests := []struct {
src string
oldTypeName string
newTypeName string
labels []string
want Tokens
}{
{
"foo {}",
"foo",
"bar",
nil,
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`bar`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s %s %s in %s", test.oldTypeName, test.newTypeName, test.labels, test.src), func(t *testing.T) {
f, diags := ParseConfig([]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")
}
b := f.Body().FirstMatchingBlock(test.oldTypeName, test.labels)
b.SetType(test.newTypeName)
got := f.BuildTokens(nil)
format(got)
if !reflect.DeepEqual(got, test.want) {
diff := cmp.Diff(test.want, got)
t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff)
}
})
}
}
func TestBlockSetLabels(t *testing.T) {
tests := []struct {
src string
typeName string
oldLabels []string
newLabels []string
want Tokens
}{
{
`foo "hoge" {}`,
"foo",
[]string{"hoge"},
[]string{"fuga"}, // update first label
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`fuga`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
`foo "hoge" "fuga" {}`,
"foo",
[]string{"hoge", "fuga"},
[]string{"hoge", "piyo"}, // update second label
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`hoge`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`piyo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
`foo {}`,
"foo",
[]string{},
[]string{"fuga"}, // insert a new label to empty list
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`fuga`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
`foo "hoge" {}`,
"foo",
[]string{"hoge"},
[]string{}, // remove all labels
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
`foo "hoge" /* fuga */ "piyo" {}`,
"foo",
[]string{"hoge", "piyo"},
[]string{"fuga"}, // force quoted form even if the old one is unquoted.
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`fuga`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
`foo "hoge" /* foo */ "" {}`,
"foo",
[]string{"hoge", ""},
[]string{"fuga"}, // force quoted form even if the old one is unquoted.
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte(`fuga`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenCQuote,
Bytes: []byte(`"`),
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte{'}'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s %s %s in %s", test.typeName, test.oldLabels, test.newLabels, test.src), func(t *testing.T) {
f, diags := ParseConfig([]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")
}
b := f.Body().FirstMatchingBlock(test.typeName, test.oldLabels)
b.SetLabels(test.newLabels)
got := f.BuildTokens(nil)
format(got)
if !reflect.DeepEqual(got, test.want) {
diff := cmp.Diff(test.want, got)
t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff)
}
})
}
}