addrs: MoveEndpointInModule

We previously built out addrs.UnifyMoveEndpoints with a different
implementation strategy in mind, but that design turns out to not be
viable because it forces us to move to AbsMoveable addresses too soon,
before we've done the analysis required to identify chained and nested
moves.

Instead, UnifyMoveEndpoints will return a new type MoveEndpointInModule
which conceptually represents a matching pattern which either matches or
doesn't match a particular AbsMoveable. It does this by just binding the
unified relative address from the MoveEndpoint to the module where it
was declared, and thus allows us to distinguish between the part of the
module path which applies to any instances of the given modules vs. the
user-specified part which must identify particular module instances.
This commit is contained in:
Martin Atkins 2021-07-12 12:23:10 -07:00
parent cd06572c4f
commit 22eee529e3
9 changed files with 367 additions and 209 deletions

View File

@ -53,7 +53,11 @@ func (c AbsModuleCall) absMoveableSigil() {
}
func (c AbsModuleCall) String() string {
return fmt.Sprintf("%s.%s", c.Module, c.Call.Name)
if len(c.Module) == 0 {
return "module." + c.Call.Name
}
return fmt.Sprintf("%s.module.%s", c.Module, c.Call.Name)
}
func (c AbsModuleCall) Instance(key InstanceKey) ModuleInstance {

View File

@ -39,6 +39,10 @@ type MoveEndpoint struct {
relSubject AbsMoveable
}
func (e *MoveEndpoint) ObjectKind() MoveEndpointKind {
return absMoveableEndpointKind(e.relSubject)
}
func (e *MoveEndpoint) String() string {
// Our internal pseudo-AbsMovable representing the relative
// address (either ModuleInstance or AbsResourceInstance) is
@ -73,7 +77,7 @@ func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool {
// address, because the rules for whether unify can succeed depend
// only on the relative part of the addresses, not on which module
// they were declared in.
from, to := UnifyMoveEndpoints(RootModuleInstance, e, other)
from, to := UnifyMoveEndpoints(RootModule, e, other)
return from != nil && to != nil
}
@ -147,11 +151,10 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost
// UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the
// "from" and "to" addresses in a moved block, and returns a pair of
// AbsMoveable addresses guaranteed to be of the same dynamic type
// MoveEndpointInModule addresses guaranteed to be of the same dynamic type
// that represent what the two MoveEndpoint addresses refer to.
//
// moduleAddr must be the address of the module instance where the move
// was declared.
// moduleAddr must be the address of the module where the move was declared.
//
// This function deals both with the conversion from relative to absolute
// addresses and with resolving the ambiguity between no-key instance
@ -163,7 +166,7 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost
// given addresses are incompatible then UnifyMoveEndpoints returns (nil, nil),
// in which case the caller should typically report an error to the user
// stating the unification constraints.
func UnifyMoveEndpoints(moduleAddr ModuleInstance, relFrom, relTo *MoveEndpoint) (absFrom, absTo AbsMoveable) {
func UnifyMoveEndpoints(moduleAddr Module, relFrom, relTo *MoveEndpoint) (modFrom, modTo *MoveEndpointInModule) {
// First we'll make a decision about which address type we're
// ultimately trying to unify to. For our internal purposes
@ -197,17 +200,17 @@ func UnifyMoveEndpoints(moduleAddr ModuleInstance, relFrom, relTo *MoveEndpoint)
panic("unhandled move address types")
}
absFrom = relFrom.prepareAbsMoveable(moduleAddr, wantType)
absTo = relTo.prepareAbsMoveable(moduleAddr, wantType)
if absFrom == nil || absTo == nil {
modFrom = relFrom.prepareMoveEndpointInModule(moduleAddr, wantType)
modTo = relTo.prepareMoveEndpointInModule(moduleAddr, wantType)
if modFrom == nil || modTo == nil {
// if either of them failed then they both failed, to make the
// caller's life a little easier.
return nil, nil
}
return absFrom, absTo
return modFrom, modTo
}
func (e *MoveEndpoint) prepareAbsMoveable(moduleAddr ModuleInstance, wantType TargetableAddrType) AbsMoveable {
func (e *MoveEndpoint) prepareMoveEndpointInModule(moduleAddr Module, wantType TargetableAddrType) *MoveEndpointInModule {
// relAddr can only be either AbsResourceInstance or ModuleInstance, the
// internal intermediate representation produced by ParseMoveEndpoint.
relAddr := e.relSubject
@ -216,40 +219,43 @@ func (e *MoveEndpoint) prepareAbsMoveable(moduleAddr ModuleInstance, wantType Ta
case ModuleInstance:
switch wantType {
case ModuleInstanceAddrType:
ret := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr))
ret = append(ret, moduleAddr...)
ret = append(ret, relAddr...)
return ret
// Since our internal representation is already a module instance,
// we can just rewrap this one.
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr,
}
case ModuleAddrType:
// NOTE: We're fudging a little here and using
// ModuleAddrType to represent AbsModuleCall rather
// than Module.
callerAddr := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr)-1)
callerAddr = append(callerAddr, moduleAddr...)
callerAddr = append(callerAddr, relAddr[:len(relAddr)-1]...)
return AbsModuleCall{
callerAddr, callAddr := relAddr.Call()
absCallAddr := AbsModuleCall{
Module: callerAddr,
Call: ModuleCall{
Name: relAddr[len(relAddr)-1].Name,
},
Call: callAddr,
}
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: absCallAddr,
}
default:
return nil // can't make any other types from a ModuleInstance
}
case AbsResourceInstance:
callerAddr := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr.Module))
callerAddr = append(callerAddr, moduleAddr...)
callerAddr = append(callerAddr, relAddr.Module...)
switch wantType {
case AbsResourceInstanceAddrType:
return AbsResourceInstance{
Module: callerAddr,
Resource: relAddr.Resource,
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr,
}
case AbsResourceAddrType:
return AbsResource{
Module: callerAddr,
Resource: relAddr.Resource.Resource,
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr.ContainingResource(),
}
default:
return nil // can't make any other types from an AbsResourceInstance

View File

@ -0,0 +1,33 @@
package addrs
import "fmt"
// MoveEndpointKind represents the different kinds of object that a movable
// address can refer to.
type MoveEndpointKind rune
//go:generate go run golang.org/x/tools/cmd/stringer -type MoveEndpointKind
const (
// MoveEndpointModule indicates that a move endpoint either refers to
// an individual module instance or to all instances of a particular
// module call.
MoveEndpointModule MoveEndpointKind = 'M'
// MoveEndpointResource indicates that a move endpoint either refers to
// an individual resource instance or to all instances of a particular
// resource.
MoveEndpointResource MoveEndpointKind = 'R'
)
func absMoveableEndpointKind(addr AbsMoveable) MoveEndpointKind {
switch addr := addr.(type) {
case ModuleInstance, AbsModuleCall:
return MoveEndpointModule
case AbsResourceInstance, AbsResource:
return MoveEndpointResource
default:
// The above should be exhaustive for all AbsMoveable types.
panic(fmt.Sprintf("unsupported address type %T", addr))
}
}

View File

@ -0,0 +1,101 @@
package addrs
import (
"strings"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// MoveEndpointInModule annotates a MoveEndpoint with the address of the
// module where it was declared, which is the form we use for resolving
// whether move statements chain from or are nested within other move
// statements.
type MoveEndpointInModule struct {
// SourceRange is the location of the physical endpoint address
// in configuration, if this MoveEndpoint was decoded from a
// configuration expresson.
SourceRange tfdiags.SourceRange
// The internals are unexported here because, as with MoveEndpoint,
// we're somewhat abusing AbsMoveable here to represent an address
// relative to the module, rather than as an absolute address.
// Conceptually, the following two fields represent a matching pattern
// for AbsMoveables where the elements of "module" behave as
// ModuleInstanceStep values with a wildcard instance key, because
// a moved block in a module affects all instances of that module.
// Unlike MoveEndpoint, relSubject in this case can be any of the
// address types that implement AbsMoveable.
module Module
relSubject AbsMoveable
}
func (e *MoveEndpointInModule) ObjectKind() MoveEndpointKind {
return absMoveableEndpointKind(e.relSubject)
}
// String produces a string representation of the object matching pattern
// represented by the reciever.
//
// Since there is no direct syntax for representing such an object matching
// pattern, this function uses a splat-operator-like representation to stand
// in for the wildcard instance keys.
func (e *MoveEndpointInModule) String() string {
if e == nil {
return ""
}
var buf strings.Builder
for _, name := range e.module {
buf.WriteString("module.")
buf.WriteString(name)
buf.WriteString("[*].")
}
buf.WriteString(e.relSubject.String())
// For consistency we'll also use the splat-like wildcard syntax to
// represent the final step being either a resource or module call
// rather than an instance, so we can more easily distinguish the two
// in the string representation.
switch e.relSubject.(type) {
case AbsModuleCall, AbsResource:
buf.WriteString("[*]")
}
return buf.String()
}
// SelectsMoveable returns true if the reciever directly selects the object
// represented by the given address, without any consideration of nesting.
//
// This is a good function to use for deciding whether a specific object
// found in the state should be acted on by a particular move statement.
func (e *MoveEndpointInModule) SelectsMoveable(addr AbsMoveable) bool {
// Only addresses of the same kind can possibly match. This guarantees
// that our logic below only needs to deal with combinations of resources
// and resource instances or with combinations of module calls and
// module instances.
if e.ObjectKind() != absMoveableEndpointKind(addr) {
return false
}
// TODO: implement
return false
}
// CanChainFrom returns true if the reciever describes an address that could
// potentially select an object that the other given address could select.
//
// In other words, this decides whether the move chaining rule applies, if
// the reciever is the "to" from one statement and the other given address
// is the "from" of another statement.
func (e *MoveEndpointInModule) CanChainFrom(other *MoveEndpointInModule) bool {
// TODO: implement
return false
}
// NestedWithin returns true if the reciever describes an address that is
// contained within one of the objects that the given other address could
// select.
func (e *MoveEndpointInModule) NestedWithin(other *MoveEndpointInModule) bool {
// TODO: implement
return false
}

View File

@ -5,7 +5,6 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
@ -339,245 +338,127 @@ func TestParseMoveEndpoint(t *testing.T) {
func TestUnifyMoveEndpoints(t *testing.T) {
tests := []struct {
InputFrom, InputTo string
Module ModuleInstance
WantFrom, WantTo AbsMoveable
Module Module
WantFrom, WantTo string
}{
{
InputFrom: `foo.bar`,
InputTo: `foo.baz`,
Module: RootModuleInstance,
WantFrom: AbsResource{
Module: RootModuleInstance,
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
WantTo: AbsResource{
Module: RootModuleInstance,
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "baz",
},
},
Module: RootModule,
WantFrom: `foo.bar[*]`,
WantTo: `foo.baz[*]`,
},
{
InputFrom: `foo.bar`,
InputTo: `foo.baz`,
Module: RootModuleInstance.Child("a", NoKey),
WantFrom: AbsResource{
Module: RootModuleInstance.Child("a", NoKey),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
WantTo: AbsResource{
Module: RootModuleInstance.Child("a", NoKey),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "baz",
},
},
Module: RootModule.Child("a"),
WantFrom: `module.a[*].foo.bar[*]`,
WantTo: `module.a[*].foo.baz[*]`,
},
{
InputFrom: `foo.bar`,
InputTo: `module.b[0].foo.baz`,
Module: RootModuleInstance.Child("a", NoKey),
WantFrom: AbsResource{
Module: RootModuleInstance.Child("a", NoKey),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
WantTo: AbsResource{
Module: RootModuleInstance.Child("a", NoKey).Child("b", IntKey(0)),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "baz",
},
},
Module: RootModule.Child("a"),
WantFrom: `module.a[*].foo.bar[*]`,
WantTo: `module.a[*].module.b[0].foo.baz[*]`,
},
{
InputFrom: `foo.bar`,
InputTo: `foo.bar["thing"]`,
Module: RootModuleInstance,
WantFrom: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
},
WantTo: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("thing"),
},
},
Module: RootModule,
WantFrom: `foo.bar`,
WantTo: `foo.bar["thing"]`,
},
{
InputFrom: `foo.bar["thing"]`,
InputTo: `foo.bar`,
Module: RootModuleInstance,
WantFrom: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("thing"),
},
},
WantTo: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
},
Module: RootModule,
WantFrom: `foo.bar["thing"]`,
WantTo: `foo.bar`,
},
{
InputFrom: `foo.bar["a"]`,
InputTo: `foo.bar["b"]`,
Module: RootModuleInstance,
WantFrom: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("a"),
},
},
WantTo: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("b"),
},
},
Module: RootModule,
WantFrom: `foo.bar["a"]`,
WantTo: `foo.bar["b"]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.bar`,
Module: RootModuleInstance,
WantFrom: AbsModuleCall{
Module: RootModuleInstance,
Call: ModuleCall{Name: "foo"},
},
WantTo: AbsModuleCall{
Module: RootModuleInstance,
Call: ModuleCall{Name: "bar"},
},
Module: RootModule,
WantFrom: `module.foo[*]`,
WantTo: `module.bar[*]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.bar.module.baz`,
Module: RootModuleInstance,
WantFrom: AbsModuleCall{
Module: RootModuleInstance,
Call: ModuleCall{Name: "foo"},
},
WantTo: AbsModuleCall{
Module: RootModuleInstance.Child("bar", NoKey),
Call: ModuleCall{Name: "baz"},
},
Module: RootModule,
WantFrom: `module.foo[*]`,
WantTo: `module.bar.module.baz[*]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.bar.module.baz`,
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
WantFrom: AbsModuleCall{
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
Call: ModuleCall{Name: "foo"},
},
WantTo: AbsModuleCall{
Module: RootModuleInstance.Child("bloop", StringKey("hi")).Child("bar", NoKey),
Call: ModuleCall{Name: "baz"},
},
Module: RootModule.Child("bloop"),
WantFrom: `module.bloop[*].module.foo[*]`,
WantTo: `module.bloop[*].module.bar.module.baz[*]`,
},
{
InputFrom: `module.foo[0]`,
InputTo: `module.foo["a"]`,
Module: RootModuleInstance,
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
Module: RootModule,
WantFrom: `module.foo[0]`,
WantTo: `module.foo["a"]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.foo["a"]`,
Module: RootModuleInstance,
WantFrom: RootModuleInstance.Child("foo", NoKey),
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
Module: RootModule,
WantFrom: `module.foo`,
WantTo: `module.foo["a"]`,
},
{
InputFrom: `module.foo[0]`,
InputTo: `module.foo`,
Module: RootModuleInstance,
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
WantTo: RootModuleInstance.Child("foo", NoKey),
Module: RootModule,
WantFrom: `module.foo[0]`,
WantTo: `module.foo`,
},
{
InputFrom: `module.foo[0]`,
InputTo: `module.foo`,
Module: RootModuleInstance.Child("bloop", NoKey),
WantFrom: RootModuleInstance.Child("bloop", NoKey).Child("foo", IntKey(0)),
WantTo: RootModuleInstance.Child("bloop", NoKey).Child("foo", NoKey),
Module: RootModule.Child("bloop"),
WantFrom: `module.bloop[*].module.foo[0]`,
WantTo: `module.bloop[*].module.foo`,
},
{
InputFrom: `module.foo`,
InputTo: `foo.bar`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module call with resource
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module call with resource
WantTo: ``,
},
{
InputFrom: `module.foo[0]`,
InputTo: `foo.bar`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module instance with resource
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module instance with resource
WantTo: ``,
},
{
InputFrom: `module.foo`,
InputTo: `foo.bar[0]`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module call with resource instance
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module call with resource instance
WantTo: ``,
},
{
InputFrom: `module.foo[0]`,
InputTo: `foo.bar[0]`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module instance with resource instance
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module instance with resource instance
WantTo: ``,
},
}
@ -604,13 +485,12 @@ func TestUnifyMoveEndpoints(t *testing.T) {
fromEp := parseInput(test.InputFrom)
toEp := parseInput(test.InputTo)
diffOpts := cmpopts.IgnoreUnexported(ModuleCall{})
gotFrom, gotTo := UnifyMoveEndpoints(test.Module, fromEp, toEp)
if diff := cmp.Diff(test.WantFrom, gotFrom, diffOpts); diff != "" {
t.Errorf("wrong 'from' address\n%s", diff)
if got, want := gotFrom.String(), test.WantFrom; got != want {
t.Errorf("wrong 'from' result\ngot: %s\nwant: %s", got, want)
}
if diff := cmp.Diff(test.WantTo, gotTo, diffOpts); diff != "" {
t.Errorf("wrong 'to' address\n%s", diff)
if got, want := gotTo.String(), test.WantTo; got != want {
t.Errorf("wrong 'to' result\ngot: %s\nwant: %s", got, want)
}
})
}

View File

@ -9,7 +9,6 @@ package addrs
// of the configuration, which is different than the direct representation
// of these in configuration where the author gives an address relative to
// the current module where the address is defined. The type MoveEndpoint
type AbsMoveable interface {
absMoveableSigil()

View File

@ -0,0 +1,29 @@
// Code generated by "stringer -type MoveEndpointKind"; DO NOT EDIT.
package addrs
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MoveEndpointModule-77]
_ = x[MoveEndpointResource-82]
}
const (
_MoveEndpointKind_name_0 = "MoveEndpointModule"
_MoveEndpointKind_name_1 = "MoveEndpointResource"
)
func (i MoveEndpointKind) String() string {
switch {
case i == 77:
return _MoveEndpointKind_name_0
case i == 82:
return _MoveEndpointKind_name_1
default:
return "MoveEndpointKind(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View File

@ -0,0 +1,63 @@
package refactoring
import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/states"
)
type MoveResult struct {
From, To addrs.AbsResourceInstance
}
// ApplyMoves modifies in-place the given state object so that any existing
// objects that are matched by a "from" argument of one of the move statements
// will be moved to instead appear at the "to" argument of that statement.
//
// The result is a map from the unique key of each absolute address that was
// either the source or destination of a move to a MoveResult describing
// what happened at that address.
//
// ApplyMoves does not have any error situations itself, and will instead just
// ignore any unresolvable move statements. Validation of a set of moves is
// a separate concern applied to the configuration, because validity of
// moves is always dependent only on the configuration, not on the state.
func ApplyMoves(stmts []MoveStatement, state *states.State) map[addrs.UniqueKey]MoveResult {
// The methodology here is to construct a small graph of all of the move
// statements where the edges represent where a particular statement
// is either chained from or nested inside the effect of another statement.
// That then means we can traverse the graph in topological sort order
// to gradually move objects through potentially multiple moves each.
g := &dag.AcyclicGraph{}
for _, stmt := range stmts {
// The graph nodes are pointers to the actual statements directly.
g.Add(&stmt)
}
// Now we'll add the edges representing chaining and nesting relationships.
// We assume that a reasonable configuration will have at most tens of
// move statements and thus this N*M algorithm is acceptable.
for _, depender := range stmts {
for _, dependee := range stmts {
dependeeTo := dependee.To
dependerFrom := depender.From
if dependerFrom.CanChainFrom(dependeeTo) || dependerFrom.NestedWithin(dependeeTo) {
g.Connect(dag.BasicEdge(depender, dependee))
}
}
}
// If there are any cycles in the graph then we'll not take any action
// at all. The separate validation step should detect this and return
// an error.
if len(g.Cycles()) != 0 {
return nil
}
// The starting nodes are the ones that don't depend on any other nodes.
//startNodes := make(dag.Set, len(stmts))
//g.DepthFirstWalk()
return nil
}

View File

@ -0,0 +1,43 @@
package refactoring
import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type MoveStatement struct {
From, To *addrs.MoveEndpointInModule
DeclRange tfdiags.SourceRange
}
// FindMoveStatements recurses through the modules of the given configuration
// and returns a flat set of all "moved" blocks defined within, in a
// deterministic but undefined order.
func FindMoveStatements(rootCfg *configs.Config) []MoveStatement {
return findMoveStatements(rootCfg, nil)
}
func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
modAddr := cfg.Path
for _, mc := range cfg.Module.Moved {
fromAddr, toAddr := addrs.UnifyMoveEndpoints(modAddr, mc.From, mc.To)
if fromAddr == nil || toAddr == nil {
// Invalid combination should get caught by our separate
// validation rules elsewhere.
continue
}
into = append(into, MoveStatement{
From: fromAddr,
To: toAddr,
DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
})
}
for _, childCfg := range cfg.Children {
into = findMoveStatements(childCfg, into)
}
return into
}