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)
|
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.
|
// Labels returns the labels of the block.
|
||||||
func (b *Block) Labels() []string {
|
func (b *Block) Labels() []string {
|
||||||
labelNames := make([]string, 0, len(b.labels))
|
labelNames := make([]string, 0, len(b.labels))
|
||||||
@ -116,3 +123,25 @@ func (b *Block) Labels() []string {
|
|||||||
|
|
||||||
return labelNames
|
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"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBlockType(t *testing.T) {
|
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 {
|
func (ns *nodes) AppendUnstructuredTokens(tokens Tokens) *node {
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
Loading…
Reference in New Issue
Block a user