c3cbe9a9e2
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.
291 lines
5.9 KiB
Go
291 lines
5.9 KiB
Go
package hclwrite
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
)
|
|
|
|
// node represents a node in the AST.
|
|
type node struct {
|
|
content nodeContent
|
|
|
|
list *nodes
|
|
before, after *node
|
|
}
|
|
|
|
func newNode(c nodeContent) *node {
|
|
return &node{
|
|
content: c,
|
|
}
|
|
}
|
|
|
|
func (n *node) Equal(other *node) bool {
|
|
return cmp.Equal(n.content, other.content)
|
|
}
|
|
|
|
func (n *node) BuildTokens(to Tokens) Tokens {
|
|
return n.content.BuildTokens(to)
|
|
}
|
|
|
|
// Detach removes the receiver from the list it currently belongs to. If the
|
|
// node is not currently in a list, this is a no-op.
|
|
func (n *node) Detach() {
|
|
if n.list == nil {
|
|
return
|
|
}
|
|
if n.before != nil {
|
|
n.before.after = n.after
|
|
}
|
|
if n.after != nil {
|
|
n.after.before = n.before
|
|
}
|
|
if n.list.first == n {
|
|
n.list.first = n.after
|
|
}
|
|
if n.list.last == n {
|
|
n.list.last = n.before
|
|
}
|
|
n.list = nil
|
|
n.before = nil
|
|
n.after = nil
|
|
}
|
|
|
|
// ReplaceWith removes the receiver from the list it currently belongs to and
|
|
// inserts a new node with the given content in its place. If the node is not
|
|
// currently in a list, this function will panic.
|
|
//
|
|
// The return value is the newly-constructed node, containing the given content.
|
|
// After this function returns, the reciever is no longer attached to a list.
|
|
func (n *node) ReplaceWith(c nodeContent) *node {
|
|
if n.list == nil {
|
|
panic("can't replace node that is not in a list")
|
|
}
|
|
|
|
before := n.before
|
|
after := n.after
|
|
list := n.list
|
|
n.before, n.after, n.list = nil, nil, nil
|
|
|
|
nn := newNode(c)
|
|
nn.before = before
|
|
nn.after = after
|
|
nn.list = list
|
|
if before != nil {
|
|
before.after = nn
|
|
}
|
|
if after != nil {
|
|
after.before = nn
|
|
}
|
|
return nn
|
|
}
|
|
|
|
func (n *node) assertUnattached() {
|
|
if n.list != nil {
|
|
panic(fmt.Sprintf("attempt to attach already-attached node %#v", n))
|
|
}
|
|
}
|
|
|
|
// nodeContent is the interface type implemented by all AST content types.
|
|
type nodeContent interface {
|
|
walkChildNodes(w internalWalkFunc)
|
|
BuildTokens(to Tokens) Tokens
|
|
}
|
|
|
|
// nodes is a list of nodes.
|
|
type nodes struct {
|
|
first, last *node
|
|
}
|
|
|
|
func (ns *nodes) BuildTokens(to Tokens) Tokens {
|
|
for n := ns.first; n != nil; n = n.after {
|
|
to = n.BuildTokens(to)
|
|
}
|
|
return to
|
|
}
|
|
|
|
func (ns *nodes) Clear() {
|
|
ns.first = nil
|
|
ns.last = nil
|
|
}
|
|
|
|
func (ns *nodes) Append(c nodeContent) *node {
|
|
n := &node{
|
|
content: c,
|
|
}
|
|
ns.AppendNode(n)
|
|
n.list = ns
|
|
return n
|
|
}
|
|
|
|
func (ns *nodes) AppendNode(n *node) {
|
|
if ns.last != nil {
|
|
n.before = ns.last
|
|
ns.last.after = n
|
|
}
|
|
n.list = ns
|
|
ns.last = n
|
|
if ns.first == nil {
|
|
ns.first = n
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
n := newNode(tokens)
|
|
ns.AppendNode(n)
|
|
n.list = ns
|
|
return n
|
|
}
|
|
|
|
// FindNodeWithContent searches the nodes for a node whose content equals
|
|
// the given content. If it finds one then it returns it. Otherwise it returns
|
|
// nil.
|
|
func (ns *nodes) FindNodeWithContent(content nodeContent) *node {
|
|
for n := ns.first; n != nil; n = n.after {
|
|
if n.content == content {
|
|
return n
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nodeSet is an unordered set of nodes. It is used to describe a set of nodes
|
|
// that all belong to the same list that have some role or characteristic
|
|
// in common.
|
|
type nodeSet map[*node]struct{}
|
|
|
|
func newNodeSet() nodeSet {
|
|
return make(nodeSet)
|
|
}
|
|
|
|
func (ns nodeSet) Has(n *node) bool {
|
|
if ns == nil {
|
|
return false
|
|
}
|
|
_, exists := ns[n]
|
|
return exists
|
|
}
|
|
|
|
func (ns nodeSet) Add(n *node) {
|
|
ns[n] = struct{}{}
|
|
}
|
|
|
|
func (ns nodeSet) Remove(n *node) {
|
|
delete(ns, n)
|
|
}
|
|
|
|
func (ns nodeSet) List() []*node {
|
|
if len(ns) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ret := make([]*node, 0, len(ns))
|
|
|
|
// Determine which list we are working with. We assume here that all of
|
|
// the nodes belong to the same list, since that is part of the contract
|
|
// for nodeSet.
|
|
var list *nodes
|
|
for n := range ns {
|
|
list = n.list
|
|
break
|
|
}
|
|
|
|
// We recover the order by iterating over the whole list. This is not
|
|
// the most efficient way to do it, but our node lists should always be
|
|
// small so not worth making things more complex.
|
|
for n := list.first; n != nil; n = n.after {
|
|
if ns.Has(n) {
|
|
ret = append(ret, n)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// FindNodeWithContent searches the nodes for a node whose content equals
|
|
// the given content. If it finds one then it returns it. Otherwise it returns
|
|
// nil.
|
|
func (ns nodeSet) FindNodeWithContent(content nodeContent) *node {
|
|
for n := range ns {
|
|
if n.content == content {
|
|
return n
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type internalWalkFunc func(*node)
|
|
|
|
// inTree can be embedded into a content struct that has child nodes to get
|
|
// a standard implementation of the NodeContent interface and a record of
|
|
// a potential parent node.
|
|
type inTree struct {
|
|
parent *node
|
|
children *nodes
|
|
}
|
|
|
|
func newInTree() inTree {
|
|
return inTree{
|
|
children: &nodes{},
|
|
}
|
|
}
|
|
|
|
func (it *inTree) assertUnattached() {
|
|
if it.parent != nil {
|
|
panic(fmt.Sprintf("node is already attached to %T", it.parent.content))
|
|
}
|
|
}
|
|
|
|
func (it *inTree) walkChildNodes(w internalWalkFunc) {
|
|
for n := it.children.first; n != nil; n = n.after {
|
|
w(n)
|
|
}
|
|
}
|
|
|
|
func (it *inTree) BuildTokens(to Tokens) Tokens {
|
|
for n := it.children.first; n != nil; n = n.after {
|
|
to = n.BuildTokens(to)
|
|
}
|
|
return to
|
|
}
|
|
|
|
// leafNode can be embedded into a content struct to give it a do-nothing
|
|
// implementation of walkChildNodes
|
|
type leafNode struct {
|
|
}
|
|
|
|
func (n *leafNode) walkChildNodes(w internalWalkFunc) {
|
|
}
|