hclwrite: Allow updating block type and labels

Fixes #338

Add methods to update block type and labels to enable us to refactor HCL
configurations such as renaming Terraform resources.

- `*Block.SetType(typeName string)`
- `*Block.SetLabels(labels []string)`

Some additional notes about SetLabels:

Since we cannot assume that old and new labels are equal in length,
remove old labels and insert new ones before TokenOBrace.

To implement this, I also added the following methods.

- `*nodes.Insert(pos *node, c nodeContent) *node`
- `*nodes.InsertNode(pos *node, n *node) *node`

They are similar to the existing Append / AppendNode,
but insert a node before a given position.
This commit is contained in:
Masayuki Morita 2020-01-29 14:21:48 +09:00 committed by Martin Atkins
parent ea60f7f2a6
commit c3cbe9a9e2
3 changed files with 369 additions and 0 deletions

View File

@ -79,6 +79,13 @@ func (b *Block) Type() string {
return string(typeNameObj.token.Bytes)
}
// SetType updates the type name of the block to a given name.
func (b *Block) SetType(typeName string) {
nameTok := newIdentToken(typeName)
nameObj := newIdentifier(nameTok)
b.typeName.ReplaceWith(nameObj)
}
// Labels returns the labels of the block.
func (b *Block) Labels() []string {
labelNames := make([]string, 0, len(b.labels))
@ -116,3 +123,25 @@ func (b *Block) Labels() []string {
return labelNames
}
// SetLabels updates the labels of the block to given labels.
// Since we cannot assume that old and new labels are equal in length,
// remove old labels and insert new ones before TokenOBrace.
func (b *Block) SetLabels(labels []string) {
// Remove old labels
for oldLabel := range b.labels {
oldLabel.Detach()
b.labels.Remove(oldLabel)
}
// Insert new labels before TokenOBrace.
for _, label := range labels {
labelToks := TokensForValue(cty.StringVal(label))
// Force a new label to use the quoted form even if the old one is unquoted.
// The unquoted form is supported in HCL 2 only for compatibility with some
// historical use in HCL 1.
labelObj := newQuoted(labelToks)
labelNode := b.children.Insert(b.open, labelObj)
b.labels.Add(labelNode)
}
}

View File

@ -6,7 +6,10 @@ import (
"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) {
@ -103,3 +106,310 @@ escape "\u0041" {
})
}
}
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 {}`,
"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)
}
})
}
}

View File

@ -130,6 +130,36 @@ func (ns *nodes) AppendNode(n *node) {
}
}
// Insert inserts a nodeContent at a given position.
// This is just a wrapper for InsertNode. See InsertNode for details.
func (ns *nodes) Insert(pos *node, c nodeContent) *node {
n := &node{
content: c,
}
ns.InsertNode(pos, n)
n.list = ns
return n
}
// InsertNode inserts a node at a given position.
// The first argument is a node reference before which to insert.
// To insert it to an empty list, set position to nil.
func (ns *nodes) InsertNode(pos *node, n *node) {
if pos == nil {
// inserts n to empty list.
ns.first = n
ns.last = n
} else {
// inserts n before pos.
pos.before.after = n
n.before = pos.before
pos.before = n
n.after = pos
}
n.list = ns
}
func (ns *nodes) AppendUnstructuredTokens(tokens Tokens) *node {
if len(tokens) == 0 {
return nil