diff --git a/.changes/unreleased/ENHANCEMENTS-20240712-120348.yaml b/.changes/unreleased/ENHANCEMENTS-20240712-120348.yaml new file mode 100644 index 000000000..9ced54bbc --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20240712-120348.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: Enable completion for all locally installed remote modules +time: 2024-07-12T12:03:48.33405+02:00 +custom: + Issue: "1760" + Repository: terraform-ls diff --git a/go.mod b/go.mod index 0a3ec3da8..fc42d245d 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/terraform-exec v0.21.0 github.com/hashicorp/terraform-json v0.22.1 github.com/hashicorp/terraform-registry-address v0.2.3 - github.com/hashicorp/terraform-schema v0.0.0-20240705124631-2b8abc4e4619 + github.com/hashicorp/terraform-schema v0.0.0-20240712095857-6b121d850a0f github.com/mcuadros/go-defaults v1.2.0 github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 github.com/mitchellh/cli v1.1.5 diff --git a/go.sum b/go.sum index 862b63930..50d31fb1e 100644 --- a/go.sum +++ b/go.sum @@ -229,8 +229,8 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7 github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= -github.com/hashicorp/terraform-schema v0.0.0-20240705124631-2b8abc4e4619 h1:4FTKovXqTafesi5NGi5XudAUDHAae5yHqnp23fR/WRA= -github.com/hashicorp/terraform-schema v0.0.0-20240705124631-2b8abc4e4619/go.mod h1:ar787Bv/qD6tlnjtzH8fQ1Yi6c/B5LsnpFlO8c92Atg= +github.com/hashicorp/terraform-schema v0.0.0-20240712095857-6b121d850a0f h1:gwHfuCSPO/myZHMDgNyvbHue6fvTK7sV2dWVBt2J8XI= +github.com/hashicorp/terraform-schema v0.0.0-20240712095857-6b121d850a0f/go.mod h1:ar787Bv/qD6tlnjtzH8fQ1Yi6c/B5LsnpFlO8c92Atg= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo= diff --git a/internal/features/modules/decoder/decoder_test.go b/internal/features/modules/decoder/decoder_test.go index f96b11c18..cec32ff65 100644 --- a/internal/features/modules/decoder/decoder_test.go +++ b/internal/features/modules/decoder/decoder_test.go @@ -37,6 +37,10 @@ func (r RootReaderMock) TerraformVersion(modPath string) *version.Version { return nil } +func (r RootReaderMock) InstalledModulePath(rootPath string, normalizedSource string) (string, bool) { + return "", false +} + func TestDecoder_CodeLensesForFile_concurrencyBug(t *testing.T) { globalStore, err := globalState.NewStateStore() if err != nil { diff --git a/internal/features/modules/decoder/path_reader.go b/internal/features/modules/decoder/path_reader.go index 73a0d6832..c2bc56f54 100644 --- a/internal/features/modules/decoder/path_reader.go +++ b/internal/features/modules/decoder/path_reader.go @@ -30,6 +30,7 @@ type StateReader interface { type RootReader interface { InstalledModuleCalls(modPath string) (map[string]tfmod.InstalledModuleCall, error) TerraformVersion(modPath string) *version.Version + InstalledModulePath(rootPath string, normalizedSource string) (string, bool) } type CombinedReader struct { diff --git a/internal/features/modules/events.go b/internal/features/modules/events.go index 97b345dce..d59e1643c 100644 --- a/internal/features/modules/events.go +++ b/internal/features/modules/events.go @@ -20,6 +20,7 @@ import ( globalState "github.com/hashicorp/terraform-ls/internal/state" globalAst "github.com/hashicorp/terraform-ls/internal/terraform/ast" op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" + tfaddr "github.com/hashicorp/terraform-registry-address" tfmod "github.com/hashicorp/terraform-schema/module" ) @@ -183,14 +184,30 @@ func (f *ModulesFeature) decodeDeclaredModuleCalls(ctx context.Context, dir docu var errs *multierror.Error - f.logger.Printf("indexing declared module calls for %q: %d", dir.URI, len(declared)) for _, mc := range declared { - // TODO! handle installed module calls - localSource, ok := mc.SourceAddr.(tfmod.LocalSourceAddr) - if !ok { + var mcPath string + switch source := mc.SourceAddr.(type) { + // For local module sources, we can construct the path directly from the configuration + case tfmod.LocalSourceAddr: + mcPath = filepath.Join(dir.Path(), filepath.FromSlash(source.String())) + // For registry modules, we need to find the local installation path (if installed) + case tfaddr.Module: + installedDir, ok := f.rootFeature.InstalledModulePath(dir.Path(), source.String()) + if !ok { + continue + } + mcPath = filepath.Join(dir.Path(), filepath.FromSlash(installedDir)) + // For other remote modules, we need to find the local installation path (if installed) + case tfmod.RemoteSourceAddr: + installedDir, ok := f.rootFeature.InstalledModulePath(dir.Path(), source.String()) + if !ok { + continue + } + mcPath = filepath.Join(dir.Path(), filepath.FromSlash(installedDir)) + default: + // Unknown source address, we can't resolve the path continue } - mcPath := filepath.Join(dir.Path(), filepath.FromSlash(localSource.String())) fi, err := os.Stat(mcPath) if err != nil || !fi.IsDir() { diff --git a/internal/features/modules/jobs/validation_test.go b/internal/features/modules/jobs/validation_test.go index a553ca9fc..8c937a322 100644 --- a/internal/features/modules/jobs/validation_test.go +++ b/internal/features/modules/jobs/validation_test.go @@ -28,6 +28,10 @@ func (r RootReaderMock) TerraformVersion(modPath string) *version.Version { return nil } +func (r RootReaderMock) InstalledModulePath(rootPath string, normalizedSource string) (string, bool) { + return "", false +} + func TestSchemaModuleValidation_FullModule(t *testing.T) { ctx := context.Background() gs, err := globalState.NewStateStore() diff --git a/internal/features/modules/state/module_store.go b/internal/features/modules/state/module_store.go index 7dd875958..976495b96 100644 --- a/internal/features/modules/state/module_store.go +++ b/internal/features/modules/state/module_store.go @@ -171,11 +171,12 @@ func (s *ModuleStore) DeclaredModuleCalls(modPath string) (map[string]tfmod.Decl declared := make(map[string]tfmod.DeclaredModuleCall) for _, mc := range mod.Meta.ModuleCalls { declared[mc.LocalName] = tfmod.DeclaredModuleCall{ - LocalName: mc.LocalName, - SourceAddr: mc.SourceAddr, - Version: mc.Version, - InputNames: mc.InputNames, - RangePtr: mc.RangePtr, + LocalName: mc.LocalName, + RawSourceAddr: mc.RawSourceAddr, + SourceAddr: mc.SourceAddr, + Version: mc.Version, + InputNames: mc.InputNames, + RangePtr: mc.RangePtr, } } diff --git a/internal/features/rootmodules/root_modules_feature.go b/internal/features/rootmodules/root_modules_feature.go index 24c153718..c51bcc0a1 100644 --- a/internal/features/rootmodules/root_modules_feature.go +++ b/internal/features/rootmodules/root_modules_feature.go @@ -188,3 +188,22 @@ func (f *RootModulesFeature) Telemetry(path string) map[string]interface{} { return properties } + +// InstalledModulePath checks the installed modules in the given root module +// for the given normalized source address. +// +// If the module is installed, it returns the path to the module installation +// directory on disk. +func (f *RootModulesFeature) InstalledModulePath(rootPath string, normalizedSource string) (string, bool) { + record, err := f.Store.RootRecordByPath(rootPath) + if err != nil { + return "", false + } + + dir, ok := record.InstalledModules[normalizedSource] + if !ok { + return "", false + } + + return dir, true +} diff --git a/internal/features/rootmodules/state/installed_modules.go b/internal/features/rootmodules/state/installed_modules.go new file mode 100644 index 000000000..876409851 --- /dev/null +++ b/internal/features/rootmodules/state/installed_modules.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package state + +import "github.com/hashicorp/terraform-ls/internal/terraform/datadir" + +// InstalledModules is a map of normalized source addresses from the +// manifest to the path of the local directory where the module is installed +type InstalledModules map[string]string + +func InstalledModulesFromManifest(manifest *datadir.ModuleManifest) InstalledModules { + if manifest == nil { + return nil + } + + installedModules := make(InstalledModules, len(manifest.Records)) + + // TODO: To support multiple versions of the same module, we need to + // look into resolving the version constraints to a specific version. + for _, v := range manifest.Records { + installedModules[v.RawSourceAddr] = v.Dir + } + + return installedModules +} diff --git a/internal/features/rootmodules/state/root_record.go b/internal/features/rootmodules/state/root_record.go index fdc5a0dec..7e4599328 100644 --- a/internal/features/rootmodules/state/root_record.go +++ b/internal/features/rootmodules/state/root_record.go @@ -23,6 +23,10 @@ type RootRecord struct { ModManifestErr error ModManifestState op.OpState + // InstalledModules is a map of normalized source addresses from the + // manifest to the path of the local directory where the module is installed + InstalledModules InstalledModules + TerraformVersion *version.Version TerraformVersionErr error TerraformVersionState op.OpState @@ -56,13 +60,20 @@ func (m *RootRecord) Copy() *RootRecord { } if m.InstalledProviders != nil { - newRecord.InstalledProviders = make(InstalledProviders, 0) + newRecord.InstalledProviders = make(InstalledProviders, len(m.InstalledProviders)) for addr, pv := range m.InstalledProviders { // version.Version is practically immutable once parsed newRecord.InstalledProviders[addr] = pv } } + if m.InstalledModules != nil { + newRecord.InstalledModules = make(InstalledModules, len(m.InstalledModules)) + for source, dir := range m.InstalledModules { + newRecord.InstalledModules[source] = dir + } + } + return newRecord } diff --git a/internal/features/rootmodules/state/root_store.go b/internal/features/rootmodules/state/root_store.go index 57878231c..7d87e09ae 100644 --- a/internal/features/rootmodules/state/root_store.go +++ b/internal/features/rootmodules/state/root_store.go @@ -258,6 +258,7 @@ func (s *RootStore) UpdateModManifest(path string, manifest *datadir.ModuleManif record.ModManifest = manifest record.ModManifestErr = mErr + record.InstalledModules = InstalledModulesFromManifest(manifest) err = txn.Insert(s.tableName, record) if err != nil {