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:
parent
ea60f7f2a6
commit
c3cbe9a9e2
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user