From c3cbe9a9e2143e99cec25f37f3e4f3d226850e81 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Wed, 29 Jan 2020 14:21:48 +0900 Subject: [PATCH] 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. --- hclwrite/ast_block.go | 29 ++++ hclwrite/ast_block_test.go | 310 +++++++++++++++++++++++++++++++++++++ hclwrite/node.go | 30 ++++ 3 files changed, 369 insertions(+) diff --git a/hclwrite/ast_block.go b/hclwrite/ast_block.go index f7d3921..99fdb1d 100644 --- a/hclwrite/ast_block.go +++ b/hclwrite/ast_block.go @@ -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) + } +} diff --git a/hclwrite/ast_block_test.go b/hclwrite/ast_block_test.go index 1f7f969..5b66fd6 100644 --- a/hclwrite/ast_block_test.go +++ b/hclwrite/ast_block_test.go @@ -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) + } + }) + } +} diff --git a/hclwrite/node.go b/hclwrite/node.go index 45669f7..377490a 100644 --- a/hclwrite/node.go +++ b/hclwrite/node.go @@ -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