// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package moduleref import ( "maps" "strings" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/modsdir" ) // Resolver is the struct responsible for finding all modules references in // Terraform configuration for a given internal module manifest. type Resolver struct { manifest *Manifest internalManifest modsdir.Manifest } // NewResolver creates a new Resolver, storing a copy of the internal manifest // that is passed. func NewResolver(internalManifest modsdir.Manifest) *Resolver { // Since maps are pointers, create a copy of the internal manifest to // prevent introducing side effects to the original internalManifestCopy := maps.Clone(internalManifest) // Remove the root module entry from the internal manifest as it is // never directly referenced. delete(internalManifestCopy, "") return &Resolver{ internalManifest: internalManifestCopy, manifest: &Manifest{ FormatVersion: FormatVersion, Records: Records{}, }, } } // Resolve will attempt to find all module references for the passed configuration // and return a new manifest encapsulating this information. func (r *Resolver) Resolve(cfg *configs.Config) *Manifest { // First find all the referenced modules. r.findAndTrimReferencedEntries(cfg, nil, nil) return r.manifest } // findAndTrimReferencedEntries will traverse a given Terraform configuration // and attempt find a caller for every entry in the internal module manifest. // If an entry is found, it will be removed from the internal manifest and // appended to the manifest that records this new information in a nested heirarchy. func (r *Resolver) findAndTrimReferencedEntries(cfg *configs.Config, parentRecord *Record, parentKey *string) { var name string var versionConstraints version.Constraints if parentKey != nil { for key := range cfg.Parent.Children { if key == *parentKey { name = key if cfg.Parent.Module.ModuleCalls[key] != nil { versionConstraints = cfg.Parent.Module.ModuleCalls[key].Version.Required } break } } } childRecord := &Record{ Key: name, Source: cfg.SourceAddr, VersionConstraints: versionConstraints, } key := strings.Join(cfg.Path, ".") for entryKey, entry := range r.internalManifest { if entryKey == key { // Use resolved version from manifest childRecord.Version = entry.Version if parentRecord.Source != nil { parentRecord.addChild(childRecord) } else { r.manifest.addModuleEntry(childRecord) } // "Trim" the entry from the internal manifest, saving us cycles // as we descend into the module tree. delete(r.internalManifest, entryKey) break } } // Traverse the child configurations for childKey, childCfg := range cfg.Children { r.findAndTrimReferencedEntries(childCfg, childRecord, &childKey) } }