| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package refactoring | |
| import ( | |
| "fmt" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // RemoveStatement is the fully-specified form of addrs.Remove | |
| type RemoveStatement struct { | |
| // From is the absolute address of the configuration object being removed. | |
| From addrs.ConfigMoveable | |
| // Destroy indicates that the resource should be destroyed, not just removed | |
| // from state. | |
| Destroy bool | |
| DeclRange tfdiags.SourceRange | |
| } | |
| // FindRemoveStatements recurses through the modules of the given configuration | |
| // and returns a set of all "removed" blocks defined within after deduplication | |
| // on the From address. | |
| // | |
| // Error diagnostics are returned if any resource or module targeted by a remove | |
| // block is still defined in configuration. | |
| // | |
| // A "removed" block in a parent module overrides a removed block in a child | |
| // module when both target the same configuration object. | |
| func FindRemoveStatements(rootCfg *configs.Config) (addrs.Map[addrs.ConfigMoveable, RemoveStatement], tfdiags.Diagnostics) { | |
| stmts := findRemoveStatements(rootCfg, addrs.MakeMap[addrs.ConfigMoveable, RemoveStatement]()) | |
| diags := validateRemoveStatements(rootCfg, stmts) | |
| return stmts, diags | |
| } | |
| func validateRemoveStatements(cfg *configs.Config, stmts addrs.Map[addrs.ConfigMoveable, RemoveStatement]) (diags tfdiags.Diagnostics) { | |
| for _, rst := range stmts.Keys() { | |
| switch rst := rst.(type) { | |
| case addrs.ConfigResource: | |
| m := cfg.Descendant(rst.Module) | |
| if m == nil { | |
| break | |
| } | |
| if r := m.Module.ResourceByAddr(rst.Resource); r != nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Removed resource still exists", | |
| Detail: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst), | |
| Subject: r.DeclRange.Ptr(), | |
| }) | |
| } | |
| case addrs.Module: | |
| if m := cfg.Descendant(rst); m != nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Removed module still exists", | |
| Detail: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst), | |
| Subject: m.CallRange.Ptr(), | |
| }) | |
| } | |
| } | |
| } | |
| return diags | |
| } | |
| func findRemoveStatements(cfg *configs.Config, into addrs.Map[addrs.ConfigMoveable, RemoveStatement]) addrs.Map[addrs.ConfigMoveable, RemoveStatement] { | |
| for _, mc := range cfg.Module.Removed { | |
| switch mc.From.ObjectKind() { | |
| case addrs.RemoveTargetResource: | |
| // First, stitch together the module path and the RelSubject to form | |
| // the absolute address of the config object being removed. | |
| res := mc.From.RelSubject.(addrs.ConfigResource) | |
| fromAddr := addrs.ConfigResource{ | |
| Module: append(cfg.Path, res.Module...), | |
| Resource: res.Resource, | |
| } | |
| // If we already have a remove statement for this ConfigResource, it | |
| // must have come from a parent module, because duplicate removed | |
| // blocks in the same module are ignored during parsing. | |
| // The removed block in the parent module overrides the block in the | |
| // child module. | |
| existingStatement, ok := into.GetOk(fromAddr) | |
| if ok { | |
| if existingResource, ok := existingStatement.From.(addrs.ConfigResource); ok && | |
| existingResource.Equal(fromAddr) { | |
| continue | |
| } | |
| } | |
| into.Put(fromAddr, RemoveStatement{ | |
| From: fromAddr, | |
| Destroy: mc.Destroy, | |
| DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange), | |
| }) | |
| case addrs.RemoveTargetModule: | |
| // First, stitch together the module path and the RelSubject to form | |
| // the absolute address of the config object being removed. | |
| mod := mc.From.RelSubject.(addrs.Module) | |
| absMod := append(cfg.Path, mod...) | |
| // If there is already a statement for this Module, it must | |
| // have come from a parent module, because duplicate removed blocks | |
| // in the same module are ignored during parsing. | |
| // The removed block in the parent module overrides the block in the | |
| // child module. | |
| existingStatement, ok := into.GetOk(mc.From.RelSubject) | |
| if ok { | |
| if existingModule, ok := existingStatement.From.(addrs.Module); ok && | |
| existingModule.Equal(absMod) { | |
| continue | |
| } | |
| } | |
| into.Put(absMod, RemoveStatement{ | |
| From: absMod, | |
| Destroy: mc.Destroy, | |
| DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange), | |
| }) | |
| default: | |
| panic("Unsupported remove target kind") | |
| } | |
| } | |
| for _, childCfg := range cfg.Children { | |
| into = findRemoveStatements(childCfg, into) | |
| } | |
| return into | |
| } |