diff --git a/internal/addrs/module_call.go b/internal/addrs/module_call.go index c6af85f08..d80b35a58 100644 --- a/internal/addrs/module_call.go +++ b/internal/addrs/module_call.go @@ -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 { diff --git a/internal/addrs/move_endpoint.go b/internal/addrs/move_endpoint.go index b7c529dd5..2b44c5cd1 100644 --- a/internal/addrs/move_endpoint.go +++ b/internal/addrs/move_endpoint.go @@ -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 diff --git a/internal/addrs/move_endpoint_kind.go b/internal/addrs/move_endpoint_kind.go new file mode 100644 index 000000000..cd8adab8f --- /dev/null +++ b/internal/addrs/move_endpoint_kind.go @@ -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)) + } +} diff --git a/internal/addrs/move_endpoint_module.go b/internal/addrs/move_endpoint_module.go new file mode 100644 index 000000000..db07af2cd --- /dev/null +++ b/internal/addrs/move_endpoint_module.go @@ -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 +} diff --git a/internal/addrs/move_endpoint_test.go b/internal/addrs/move_endpoint_test.go index a54b79c85..6e117139b 100644 --- a/internal/addrs/move_endpoint_test.go +++ b/internal/addrs/move_endpoint_test.go @@ -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) } }) } diff --git a/internal/addrs/moveable.go b/internal/addrs/moveable.go index cac6eceb8..fd93b58dc 100644 --- a/internal/addrs/moveable.go +++ b/internal/addrs/moveable.go @@ -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() diff --git a/internal/addrs/moveendpointkind_string.go b/internal/addrs/moveendpointkind_string.go new file mode 100644 index 000000000..f706fb9ca --- /dev/null +++ b/internal/addrs/moveendpointkind_string.go @@ -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) + ")" + } +} diff --git a/internal/refactoring/move_execute.go b/internal/refactoring/move_execute.go new file mode 100644 index 000000000..3581793df --- /dev/null +++ b/internal/refactoring/move_execute.go @@ -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 +} diff --git a/internal/refactoring/move_statement.go b/internal/refactoring/move_statement.go new file mode 100644 index 000000000..cb2cff40c --- /dev/null +++ b/internal/refactoring/move_statement.go @@ -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 +}