diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99d5838e9..f9454d060 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,4 +99,4 @@ jobs: run: make fmt - name: Run tests - run: go test -cover -covermode=atomic -race ./... + run: go test -cover -covermode=atomic -timeout=5m -race ./... diff --git a/go.mod b/go.mod index 9805ce759..2f5281c6e 100644 --- a/go.mod +++ b/go.mod @@ -14,12 +14,12 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.2 github.com/hashicorp/go-version v1.3.0 - github.com/hashicorp/hcl-lang v0.0.0-20211029211837-70678d4f0419 + github.com/hashicorp/hcl-lang v0.0.0-20211123142056-191cd51dec5b github.com/hashicorp/hcl/v2 v2.10.1 github.com/hashicorp/terraform-exec v0.15.0 github.com/hashicorp/terraform-json v0.13.0 github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 - github.com/hashicorp/terraform-schema v0.0.0-20211021151419-21dfff199031 + github.com/hashicorp/terraform-schema v0.0.0-20211118125251-a89436f69539 github.com/kylelemons/godebug v1.1.0 // indirect github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 github.com/mitchellh/cli v1.1.2 diff --git a/go.sum b/go.sum index ded31d6e4..d75b57bac 100644 --- a/go.sum +++ b/go.sum @@ -191,9 +191,9 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl-lang v0.0.0-20210803155453-7c098e4940bc/go.mod h1:xzXU6Fn+TWVaZUFxV8CyAsObi2oMgSEFAmLvCx2ArzM= -github.com/hashicorp/hcl-lang v0.0.0-20211029211837-70678d4f0419 h1:4coi4Rt0qgJMrkVxMa/NcwmvX6CnFS5gluVbZElhsWI= -github.com/hashicorp/hcl-lang v0.0.0-20211029211837-70678d4f0419/go.mod h1:NQq9vfyCPpRTPS4L5xeJGxp32qqp83UkDAO37NyBGF8= +github.com/hashicorp/hcl-lang v0.0.0-20211118124824-da3a292c5d7a/go.mod h1:0W3+VP07azoS+fCX5hWk1KxwHnqf1s9J7oBg2cFXm1c= +github.com/hashicorp/hcl-lang v0.0.0-20211123142056-191cd51dec5b h1:UircH3n8tUnq881dS1EV/m+ZcIKjZq9jzl0M65PAdWU= +github.com/hashicorp/hcl-lang v0.0.0-20211123142056-191cd51dec5b/go.mod h1:0W3+VP07azoS+fCX5hWk1KxwHnqf1s9J7oBg2cFXm1c= github.com/hashicorp/hcl/v2 v2.10.1 h1:h4Xx4fsrRE26ohAk/1iGF/JBqRQbyUqu5Lvj60U54ys= github.com/hashicorp/hcl/v2 v2.10.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -207,8 +207,8 @@ github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9E github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co= github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 h1:R/I8ofvXuPcTNoc//N4ruvaHGZcShI/VuU2iXo875Lo= github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045/go.mod h1:anRyJbe12BZscpFgaeGu9gH12qfdBP094LYFtuAFzd4= -github.com/hashicorp/terraform-schema v0.0.0-20211021151419-21dfff199031 h1:HwQTGktZUBlRENcwb9MKm+cfqNcv0C5vagJnjKAqNKY= -github.com/hashicorp/terraform-schema v0.0.0-20211021151419-21dfff199031/go.mod h1:DlxWg9rEgltUs+FD5ElEgBoP985cjAeA9YHcYliAGVg= +github.com/hashicorp/terraform-schema v0.0.0-20211118125251-a89436f69539 h1:cKcbX33DsyhYo6niqnNuJxU/rj+U3ix/g+T6HevO3C8= +github.com/hashicorp/terraform-schema v0.0.0-20211118125251-a89436f69539/go.mod h1:8AxXLNebdxejfusRC5/sYkL7bx4ZpY1ZUjLOXMix3TM= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= @@ -383,7 +383,6 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zclconf/go-cty v1.9.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= diff --git a/internal/codelens/reference_count.go b/internal/codelens/reference_count.go index ede2f50fa..b4c6f0354 100644 --- a/internal/codelens/reference_count.go +++ b/internal/codelens/reference_count.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "github.com/hashicorp/hcl-lang/decoder" "github.com/hashicorp/hcl-lang/lang" @@ -17,12 +18,17 @@ func ReferenceCount(showReferencesCmdId string) lang.CodeLensFunc { return func(ctx context.Context, path lang.Path, file string) ([]lang.CodeLens, error) { lenses := make([]lang.CodeLens, 0) - pathCtx, err := decoder.PathCtx(ctx) + localCtx, err := decoder.PathCtx(ctx) if err != nil { return nil, err } - refTargets := pathCtx.ReferenceTargets.OutermostInFile(file) + pathReader, err := decoder.PathReaderFromContext(ctx) + if err != nil { + return nil, err + } + + refTargets := localCtx.ReferenceTargets.OutermostInFile(file) if err != nil { return nil, err } @@ -48,7 +54,14 @@ func ReferenceCount(showReferencesCmdId string) lang.CodeLensFunc { defRange = refTarget.DefRangePtr } - originCount += len(pathCtx.ReferenceOrigins.Targeting(refTarget)) + paths := pathReader.Paths(ctx) + for _, p := range paths { + pathCtx, err := pathReader.PathContext(p) + if err != nil { + continue + } + originCount += len(pathCtx.ReferenceOrigins.Match(p, refTarget, path)) + } } if originCount == 0 { @@ -74,6 +87,11 @@ func ReferenceCount(showReferencesCmdId string) lang.CodeLensFunc { }, }) } + + sort.SliceStable(lenses, func(i, j int) bool { + return lenses[i].Range.Start.Byte < lenses[j].Range.Start.Byte + }) + return lenses, nil } } diff --git a/internal/decoder/decoder.go b/internal/decoder/decoder.go index 538d635fb..c1e33307f 100644 --- a/internal/decoder/decoder.go +++ b/internal/decoder/decoder.go @@ -4,11 +4,13 @@ import ( "context" "github.com/hashicorp/hcl-lang/decoder" + "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform-ls/internal/codelens" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/state" + "github.com/hashicorp/terraform-ls/internal/terraform/ast" tfschema "github.com/hashicorp/terraform-schema/schema" ) @@ -26,11 +28,24 @@ func modulePathContext(mod *state.Module, schemaReader state.SchemaReader, modRe pathCtx := &decoder.PathContext{ Schema: schema, - ReferenceOrigins: mod.RefOrigins, - ReferenceTargets: mod.RefTargets, + ReferenceOrigins: make(reference.Origins, 0), + ReferenceTargets: make(reference.Targets, 0), Files: make(map[string]*hcl.File, 0), } + for _, origin := range mod.RefOrigins { + if ast.IsModuleFilename(origin.OriginRange().Filename) { + pathCtx.ReferenceOrigins = append(pathCtx.ReferenceOrigins, origin) + } + } + for _, target := range mod.RefTargets { + if target.RangePtr != nil && ast.IsModuleFilename(target.RangePtr.Filename) { + pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target) + } else if target.RangePtr == nil { + pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target) + } + } + for name, f := range mod.ParsedModuleFiles { pathCtx.Files[name.String()] = f } @@ -46,11 +61,22 @@ func varsPathContext(mod *state.Module) (*decoder.PathContext, error) { pathCtx := &decoder.PathContext{ Schema: schema, - ReferenceOrigins: mod.RefOrigins, - ReferenceTargets: mod.RefTargets, + ReferenceOrigins: make(reference.Origins, 0), + ReferenceTargets: make(reference.Targets, 0), Files: make(map[string]*hcl.File, 0), } + for _, origin := range mod.RefOrigins { + if ast.IsVarsFilename(origin.OriginRange().Filename) { + pathCtx.ReferenceOrigins = append(pathCtx.ReferenceOrigins, origin) + } + } + for _, target := range mod.RefTargets { + if target.RangePtr != nil && ast.IsVarsFilename(target.RangePtr.Filename) { + pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target) + } + } + for name, f := range mod.ParsedVarsFiles { pathCtx.Files[name.String()] = f } diff --git a/internal/langserver/handlers/code_lens.go b/internal/langserver/handlers/code_lens.go index a173f120c..ed1f50f47 100644 --- a/internal/langserver/handlers/code_lens.go +++ b/internal/langserver/handlers/code_lens.go @@ -3,6 +3,7 @@ package handlers import ( "context" + "github.com/hashicorp/hcl-lang/lang" lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" @@ -22,12 +23,12 @@ func (svc *service) TextDocumentCodeLens(ctx context.Context, params lsp.CodeLen return list, err } - d, err := svc.decoderForDocument(ctx, doc) - if err != nil { - return nil, err + path := lang.Path{ + Path: doc.Dir(), + LanguageID: doc.LanguageID(), } - lenses, err := d.CodeLensesForFile(ctx, doc.Filename()) + lenses, err := svc.decoder.CodeLensesForFile(ctx, path, doc.Filename()) if err != nil { return nil, err } diff --git a/internal/langserver/handlers/code_lens_test.go b/internal/langserver/handlers/code_lens_test.go index 18266d903..1c252c431 100644 --- a/internal/langserver/handlers/code_lens_test.go +++ b/internal/langserver/handlers/code_lens_test.go @@ -3,12 +3,15 @@ package handlers import ( "encoding/json" "fmt" + "path/filepath" "testing" + "time" "github.com/hashicorp/go-version" tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/langserver" "github.com/hashicorp/terraform-ls/internal/langserver/session" + "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) @@ -195,3 +198,159 @@ output "test" { ] }`) } + +func TestCodeLens_referenceCount_crossModule(t *testing.T) { + rootModPath, err := filepath.Abs(filepath.Join("testdata", "single-submodule")) + if err != nil { + t.Fatal(err) + } + + submodPath := filepath.Join(rootModPath, "application") + + rootModUri := lsp.FileHandlerFromDirPath(rootModPath) + submodUri := lsp.FileHandlerFromDirPath(submodPath) + + var testSchema tfjson.ProviderSchemas + err = json.Unmarshal([]byte(testModuleSchemaOutput), &testSchema) + if err != nil { + t.Fatal(err) + } + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + submodPath: validTfMockCalls(), + rootModPath: validTfMockCalls(), + }, + }})) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": { + "experimental": { + "showReferencesCommandId": "test.id" + } + }, + "rootUri": %q, + "processId": 12345 + }`, rootModUri.URI())}) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "version": 0, + "languageId": "terraform", + "text": %q, + "uri": "%s/main.tf" + } + }`, `variable "environment_name" { + type = string +} + +variable "app_prefix" { + type = string +} + +variable "instances" { + type = number +} +`, submodUri.URI())}) + // TODO remove once we support synchronous dependent tasks + // See https://github.com/hashicorp/terraform-ls/issues/719 + time.Sleep(2 * time.Second) + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/codeLens", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + } + }`, submodUri.URI()), + }, `{ + "jsonrpc": "2.0", + "id": 3, + "result": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 2, + "character": 1 + } + }, + "command": { + "title": "1 reference", + "command": "test.id", + "arguments": [ + { + "line": 0, + "character": 13 + }, + { + "includeDeclaration": false + } + ] + } + }, + { + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 6, + "character": 1 + } + }, + "command": { + "title": "1 reference", + "command": "test.id", + "arguments": [ + { + "line": 4, + "character": 10 + }, + { + "includeDeclaration": false + } + ] + } + }, + { + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 10, + "character": 1 + } + }, + "command": { + "title": "1 reference", + "command": "test.id", + "arguments": [ + { + "line": 8, + "character": 10 + }, + { + "includeDeclaration": false + } + ] + } + } + ] + }`) +} diff --git a/internal/langserver/handlers/complete_test.go b/internal/langserver/handlers/complete_test.go index 142e7f455..62cc0179c 100644 --- a/internal/langserver/handlers/complete_test.go +++ b/internal/langserver/handlers/complete_test.go @@ -717,11 +717,9 @@ output "test" { } }`, mainCfg, tmpDir.URI())}) - // module manifest-dependent tasks are scheduled & executed - // asynchronously and we currently have no way of waiting - // for them to complete. // TODO remove once we support synchronous dependent tasks - time.Sleep(1500 * time.Millisecond) + // See https://github.com/hashicorp/terraform-ls/issues/719 + time.Sleep(2 * time.Second) ls.CallAndExpectResponse(t, &langserver.CallRequest{ Method: "textDocument/completion", diff --git a/internal/langserver/handlers/go_to_ref_target.go b/internal/langserver/handlers/go_to_ref_target.go index d6f91333a..30be5f4fc 100644 --- a/internal/langserver/handlers/go_to_ref_target.go +++ b/internal/langserver/handlers/go_to_ref_target.go @@ -3,6 +3,7 @@ package handlers import ( "context" + "github.com/hashicorp/hcl-lang/lang" lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" @@ -24,20 +25,19 @@ func (svc *service) GoToReferenceTarget(ctx context.Context, params lsp.TextDocu return nil, err } - d, err := svc.decoderForDocument(ctx, doc) - if err != nil { - return nil, err - } - fPos, err := ilsp.FilePositionFromDocumentPosition(params, doc) if err != nil { return nil, err } - target, err := d.ReferenceTargetForOriginAtPos(doc.Filename(), fPos.Position()) + path := lang.Path{ + Path: doc.Dir(), + LanguageID: doc.LanguageID(), + } + targets, err := svc.decoder.ReferenceTargetsForOriginAtPos(path, doc.Filename(), fPos.Position()) if err != nil { return nil, err } - return ilsp.RefTargetToLocationLink(target, cc.TextDocument.Declaration.LinkSupport), nil + return ilsp.RefTargetsToLocationLinks(targets, cc.TextDocument.Declaration.LinkSupport), nil } diff --git a/internal/langserver/handlers/go_to_ref_target_test.go b/internal/langserver/handlers/go_to_ref_target_test.go index dfc18924e..64c137600 100644 --- a/internal/langserver/handlers/go_to_ref_target_test.go +++ b/internal/langserver/handlers/go_to_ref_target_test.go @@ -2,15 +2,18 @@ package handlers import ( "fmt" + "path/filepath" "testing" + "time" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-ls/internal/langserver" + "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) -func TestDefinition(t *testing.T) { +func TestDefinition_basic(t *testing.T) { tmpDir := TempDir(t) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ @@ -46,13 +49,13 @@ func TestDefinition(t *testing.T) { ls.Call(t, &langserver.CallRequest{ Method: "initialize", ReqParams: fmt.Sprintf(`{ - "capabilities": { - "definition": { - "linkSupport": true - } - }, - "rootUri": %q, - "processId": 12345 + "capabilities": { + "definition": { + "linkSupport": true + } + }, + "rootUri": %q, + "processId": 12345 }`, tmpDir.URI())}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", @@ -69,7 +72,7 @@ func TestDefinition(t *testing.T) { } output "foo" { - value = var.test + value = var.test }`)+`, "uri": "%s/main.tf" } @@ -87,7 +90,7 @@ output "foo" { }`, tmpDir.URI())}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, - "result": { + "result": [{ "uri":"%s/main.tf", "range": { "start": { @@ -99,11 +102,94 @@ output "foo" { "character": 1 } } - } + }] }`, tmpDir.URI())) } -func TestDeclaration(t *testing.T) { +func TestDefinition_moduleInputToVariable(t *testing.T) { + modPath, err := filepath.Abs(filepath.Join("testdata", "single-submodule")) + if err != nil { + t.Fatal(err) + } + modUri := lsp.FileHandlerFromDirPath(modPath) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + modPath: validTfMockCalls(), + }, + }, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": { + "definition": { + "linkSupport": true + } + }, + "rootUri": %q, + "processId": 12345 + }`, modUri.URI())}) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "version": 0, + "languageId": "terraform", + "text": `+fmt.Sprintf("%q", + `module "gorilla-app" { + source = "./application" + environment_name = "prod" + app_prefix = "protect-gorillas" + instances = 5 +} +`)+`, + "uri": "%s/main.tf" + } + }`, modUri.URI())}) + // TODO remove once we support synchronous dependent tasks + // See https://github.com/hashicorp/terraform-ls/issues/719 + time.Sleep(2 * time.Second) + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/definition", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + }, + "position": { + "line": 2, + "character": 6 + } + }`, modUri.URI())}, fmt.Sprintf(`{ + "jsonrpc": "2.0", + "id": 3, + "result": [ + { + "uri": "%s/application/main.tf", + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 2, + "character": 1 + } + } + } + ] + }`, modUri.URI())) +} + +func TestDeclaration_basic(t *testing.T) { tmpDir := TempDir(t) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ @@ -139,13 +225,13 @@ func TestDeclaration(t *testing.T) { ls.Call(t, &langserver.CallRequest{ Method: "initialize", ReqParams: fmt.Sprintf(`{ - "capabilities": { - "definition": { - "linkSupport": true - } - }, - "rootUri": %q, - "processId": 12345 + "capabilities": { + "definition": { + "linkSupport": true + } + }, + "rootUri": %q, + "processId": 12345 }`, tmpDir.URI())}) ls.Notify(t, &langserver.CallRequest{ Method: "initialized", @@ -162,7 +248,7 @@ func TestDeclaration(t *testing.T) { } output "foo" { - value = var.test + value = var.test }`)+`, "uri": "%s/main.tf" } @@ -180,7 +266,7 @@ output "foo" { }`, tmpDir.URI())}, fmt.Sprintf(`{ "jsonrpc": "2.0", "id": 3, - "result": { + "result": [{ "uri":"%s/main.tf", "range": { "start": { @@ -192,6 +278,6 @@ output "foo" { "character": 1 } } - } + }] }`, tmpDir.URI())) } diff --git a/internal/langserver/handlers/hooks_module.go b/internal/langserver/handlers/hooks_module.go index 4e61b3d18..7e6e4ea6f 100644 --- a/internal/langserver/handlers/hooks_module.go +++ b/internal/langserver/handlers/hooks_module.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/hashicorp/terraform-ls/internal/langserver/diagnostics" + "github.com/hashicorp/terraform-ls/internal/langserver/session" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/telemetry" "github.com/hashicorp/terraform-schema/backend" @@ -119,3 +120,22 @@ func updateDiagnostics(ctx context.Context, notifier *diagnostics.Notifier) stat } } } + +func refreshCodeLens(ctx context.Context, clientRequester session.ClientCaller) state.ModuleChangeHook { + return func(oldMod, newMod *state.Module) { + oldOrigins, oldTargets := 0, 0 + if oldMod != nil { + oldOrigins = len(oldMod.RefOrigins) + oldTargets = len(oldMod.RefTargets) + } + newOrigins, newTargets := 0, 0 + if newMod != nil { + newOrigins = len(newMod.RefOrigins) + newTargets = len(newMod.RefTargets) + } + + if oldOrigins != newOrigins || oldTargets != newTargets { + clientRequester.Callback(ctx, "workspace/codeLens/refresh", nil) + } + } +} diff --git a/internal/langserver/handlers/references.go b/internal/langserver/handlers/references.go index ffaf7fa62..16c37b680 100644 --- a/internal/langserver/handlers/references.go +++ b/internal/langserver/handlers/references.go @@ -3,6 +3,7 @@ package handlers import ( "context" + "github.com/hashicorp/hcl-lang/lang" lsctx "github.com/hashicorp/terraform-ls/internal/context" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" lsp "github.com/hashicorp/terraform-ls/internal/protocol" @@ -21,17 +22,17 @@ func (svc *service) References(ctx context.Context, params lsp.ReferenceParams) return list, err } - d, err := svc.decoderForDocument(ctx, doc) + fPos, err := ilsp.FilePositionFromDocumentPosition(params.TextDocumentPositionParams, doc) if err != nil { return list, err } - fPos, err := ilsp.FilePositionFromDocumentPosition(params.TextDocumentPositionParams, doc) - if err != nil { - return list, err + path := lang.Path{ + Path: doc.Dir(), + LanguageID: doc.LanguageID(), } - origins := d.ReferenceOriginsTargetingPos(doc.Filename(), fPos.Position()) + origins := svc.decoder.ReferenceOriginsTargetingPos(path, doc.Filename(), fPos.Position()) return ilsp.RefOriginsToLocations(origins), nil } diff --git a/internal/langserver/handlers/references_test.go b/internal/langserver/handlers/references_test.go index 334337482..051499f47 100644 --- a/internal/langserver/handlers/references_test.go +++ b/internal/langserver/handlers/references_test.go @@ -2,15 +2,18 @@ package handlers import ( "fmt" + "path/filepath" "testing" + "time" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-ls/internal/langserver" + "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/exec" "github.com/stretchr/testify/mock" ) -func TestReferences(t *testing.T) { +func TestReferences_basic(t *testing.T) { tmpDir := TempDir(t) ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ @@ -117,3 +120,95 @@ output "foo" { ] }`, tmpDir.URI(), tmpDir.URI())) } + +func TestReferences_variableToModuleInput(t *testing.T) { + rootModPath, err := filepath.Abs(filepath.Join("testdata", "single-submodule")) + if err != nil { + t.Fatal(err) + } + + submodPath := filepath.Join(rootModPath, "application") + + rootModUri := lsp.FileHandlerFromDirPath(rootModPath) + submodUri := lsp.FileHandlerFromDirPath(submodPath) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + submodPath: validTfMockCalls(), + }, + }, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": { + "definition": { + "linkSupport": true + } + }, + "rootUri": %q, + "processId": 12345 + }`, rootModUri.URI())}) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "version": 0, + "languageId": "terraform", + "text": `+fmt.Sprintf("%q", + `variable "environment_name" { + type = string +} + +variable "app_prefix" { + type = string +} + +variable "instances" { + type = number +} +`)+`, + "uri": "%s/main.tf" + } + }`, submodUri.URI())}) + // TODO remove once we support synchronous dependent tasks + // See https://github.com/hashicorp/terraform-ls/issues/719 + time.Sleep(2 * time.Second) + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/references", + ReqParams: fmt.Sprintf(`{ + "textDocument": { + "uri": "%s/main.tf" + }, + "position": { + "line": 0, + "character": 5 + } + }`, submodUri.URI())}, fmt.Sprintf(`{ + "jsonrpc": "2.0", + "id": 3, + "result": [ + { + "uri": "%s/main.tf", + "range": { + "start": { + "line": 2, + "character": 2 + }, + "end": { + "line": 2, + "character": 18 + } + } + } + ] + }`, rootModUri.URI())) +} diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index 606a61320..011ddce9d 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -446,6 +446,7 @@ func (svc *service) configureSessionDependencies(ctx context.Context, cfgOpts *s svc.stateStore.Modules.ChangeHooks = state.ModuleChangeHooks{ updateDiagnostics(svc.sessCtx, svc.diagsNotifier), sendModuleTelemetry(svc.sessCtx, svc.stateStore, svc.telemetry), + refreshCodeLens(svc.sessCtx, svc.server), } svc.modStore = svc.stateStore.Modules diff --git a/internal/langserver/handlers/testdata/single-submodule/.terraform/modules/modules.json b/internal/langserver/handlers/testdata/single-submodule/.terraform/modules/modules.json new file mode 100644 index 000000000..34f3138ef --- /dev/null +++ b/internal/langserver/handlers/testdata/single-submodule/.terraform/modules/modules.json @@ -0,0 +1 @@ +{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"gorilla-app","Source":"./application","Dir":"application"}]} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/single-submodule/application/main.tf b/internal/langserver/handlers/testdata/single-submodule/application/main.tf new file mode 100644 index 000000000..757a72eab --- /dev/null +++ b/internal/langserver/handlers/testdata/single-submodule/application/main.tf @@ -0,0 +1,18 @@ +variable "environment_name" { + type = string +} + +variable "app_prefix" { + type = string +} + +variable "instances" { + type = number +} + +resource "random_pet" "application" { + count = var.instances + keepers = { + unique = "${var.environment_name}-${var.app_prefix}" + } +} diff --git a/internal/langserver/handlers/testdata/single-submodule/main.tf b/internal/langserver/handlers/testdata/single-submodule/main.tf new file mode 100644 index 000000000..e0cbacf23 --- /dev/null +++ b/internal/langserver/handlers/testdata/single-submodule/main.tf @@ -0,0 +1,10 @@ +module "gorilla-app" { + source = "./application" + environment_name = "prod" + app_prefix = "protect-gorillas" + instances = var.instance_count +} + +variable "instance_count" { + default = 5 +} diff --git a/internal/langserver/handlers/testdata/single-submodule/terraform.tfvars b/internal/langserver/handlers/testdata/single-submodule/terraform.tfvars new file mode 100644 index 000000000..3b1241f57 --- /dev/null +++ b/internal/langserver/handlers/testdata/single-submodule/terraform.tfvars @@ -0,0 +1 @@ +instance_count = 3 diff --git a/internal/lsp/location_links.go b/internal/lsp/location_links.go index 674ec9af6..977b9a41e 100644 --- a/internal/lsp/location_links.go +++ b/internal/lsp/location_links.go @@ -8,23 +8,41 @@ import ( "github.com/hashicorp/terraform-ls/internal/uri" ) -func RefTargetToLocationLink(target *decoder.ReferenceTarget, linkSupport bool) interface{} { - targetUri := uri.FromPath(filepath.Join(target.Path.Path, target.Range.Filename)) - +func RefTargetsToLocationLinks(targets decoder.ReferenceTargets, linkSupport bool) interface{} { if linkSupport { - locLink := lsp.LocationLink{ - OriginSelectionRange: HCLRangeToLSP(target.OriginRange), - TargetURI: lsp.DocumentURI(targetUri), - TargetRange: HCLRangeToLSP(target.Range), + links := make([]lsp.LocationLink, 0) + for _, target := range targets { + links = append(links, refTargetToLocationLink(target)) } + return links + } - if target.DefRangePtr != nil { - locLink.TargetSelectionRange = HCLRangeToLSP(*target.DefRangePtr) - } + locations := make([]lsp.Location, 0) + for _, target := range targets { + locations = append(locations, refTargetToLocation(target)) + } + return locations +} + +func refTargetToLocationLink(target *decoder.ReferenceTarget) lsp.LocationLink { + targetUri := uri.FromPath(filepath.Join(target.Path.Path, target.Range.Filename)) + + locLink := lsp.LocationLink{ + OriginSelectionRange: HCLRangeToLSP(target.OriginRange), + TargetURI: lsp.DocumentURI(targetUri), + TargetRange: HCLRangeToLSP(target.Range), + } - return locLink + if target.DefRangePtr != nil { + locLink.TargetSelectionRange = HCLRangeToLSP(*target.DefRangePtr) } + return locLink +} + +func refTargetToLocation(target *decoder.ReferenceTarget) lsp.Location { + targetUri := uri.FromPath(filepath.Join(target.Path.Path, target.Range.Filename)) + return lsp.Location{ URI: lsp.DocumentURI(targetUri), Range: HCLRangeToLSP(target.Range), diff --git a/internal/terraform/module/module_loader.go b/internal/terraform/module/module_loader.go index 36816dd7c..dfb5fa859 100644 --- a/internal/terraform/module/module_loader.go +++ b/internal/terraform/module/module_loader.go @@ -144,7 +144,7 @@ func (ml *moduleLoader) nonPrioCapacity() int64 { } func (ml *moduleLoader) executeModuleOp(ctx context.Context, modOp ModuleOperation) { - ml.logger.Printf("executing %q for %s", modOp.Type, modOp.ModulePath) + ml.logger.Printf("ML: executing %q for %q", modOp.Type, modOp.ModulePath) // TODO: Report progress in % for each op based on queue length defer modOp.markAsDone() @@ -196,7 +196,7 @@ func (ml *moduleLoader) executeModuleOp(ctx context.Context, modOp ModuleOperati modOp.ModulePath, modOp.Type) return } - ml.logger.Printf("finished %q for %s", modOp.Type, modOp.ModulePath) + ml.logger.Printf("ML: finished %q for %q", modOp.Type, modOp.ModulePath) if modOp.Defer != nil { go modOp.Defer(opErr) @@ -209,56 +209,30 @@ func (ml *moduleLoader) EnqueueModuleOp(modOp ModuleOperation) error { return err } - ml.logger.Printf("ML: enqueing %q module operation: %s", modOp.Type, modOp.ModulePath) + ml.logger.Printf("ML: enqueing %q module operation: %q", modOp.Type, modOp.ModulePath) + + if operationState(mod, modOp.Type) == op.OpStateQueued { + // avoid enqueuing duplicate operation + modOp.markAsDone() + return nil + } switch modOp.Type { case op.OpTypeGetTerraformVersion: - if mod.TerraformVersionState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetTerraformVersionState(modOp.ModulePath, op.OpStateQueued) case op.OpTypeObtainSchema: - if mod.ProviderSchemaState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetProviderSchemaState(modOp.ModulePath, op.OpStateQueued) case op.OpTypeParseModuleConfiguration: - if mod.ModuleParsingState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetModuleParsingState(modOp.ModulePath, op.OpStateQueued) case op.OpTypeParseVariables: - if mod.VarsParsingState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetVarsParsingState(modOp.ModulePath, op.OpStateQueued) case op.OpTypeParseModuleManifest: - if mod.ModManifestState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetModManifestState(modOp.ModulePath, op.OpStateQueued) case op.OpTypeLoadModuleMetadata: - if mod.MetaState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetMetaState(modOp.ModulePath, op.OpStateQueued) case op.OpTypeDecodeReferenceTargets: - if mod.RefTargetsState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetReferenceTargetsState(modOp.ModulePath, op.OpStateQueued) case op.OpTypeDecodeReferenceOrigins: - if mod.RefOriginsState == op.OpStateQueued { - // avoid enqueuing duplicate operation - return nil - } ml.modStore.SetReferenceOriginsState(modOp.ModulePath, op.OpStateQueued) } @@ -268,6 +242,28 @@ func (ml *moduleLoader) EnqueueModuleOp(modOp ModuleOperation) error { return nil } +func operationState(mod *state.Module, opType op.OpType) op.OpState { + switch opType { + case op.OpTypeGetTerraformVersion: + return mod.TerraformVersionState + case op.OpTypeObtainSchema: + return mod.ProviderSchemaState + case op.OpTypeParseModuleConfiguration: + return mod.ModuleParsingState + case op.OpTypeParseVariables: + return mod.VarsParsingState + case op.OpTypeParseModuleManifest: + return mod.ModManifestState + case op.OpTypeLoadModuleMetadata: + return mod.MetaState + case op.OpTypeDecodeReferenceTargets: + return mod.RefTargetsState + case op.OpTypeDecodeReferenceOrigins: + return mod.RefOriginsState + } + return op.OpStateUnknown +} + func (ml *moduleLoader) DequeueModule(modPath string) { ml.queue.DequeueAllModuleOps(modPath) } diff --git a/internal/terraform/module/module_loader_test.go b/internal/terraform/module/module_loader_test.go index fb6b5fdf3..9b54235d2 100644 --- a/internal/terraform/module/module_loader_test.go +++ b/internal/terraform/module/module_loader_test.go @@ -79,7 +79,7 @@ func TestModuleLoader_referenceCollection(t *testing.T) { } expectedOrigins := reference.Origins{ - { + reference.LocalOrigin{ Addr: lang.Address{lang.RootStep{Name: "var"}, lang.AttrStep{Name: "count"}}, Range: hcl.Range{ Filename: "main.tf", diff --git a/internal/terraform/module/walker.go b/internal/terraform/module/walker.go index fced51005..93a50b6d6 100644 --- a/internal/terraform/module/walker.go +++ b/internal/terraform/module/walker.go @@ -265,6 +265,11 @@ func (w *Walker) walk(ctx context.Context, rootPath string) error { return err } + err = w.modMgr.EnqueueModuleOp(dir, op.OpTypeParseVariables, nil) + if err != nil { + return err + } + err = w.modMgr.EnqueueModuleOp(dir, op.OpTypeGetTerraformVersion, nil) if err != nil { return err @@ -272,12 +277,26 @@ func (w *Walker) walk(ctx context.Context, rootPath string) error { dataDir := datadir.WalkDataDirOfModule(w.fs, dir) if dataDir.ModuleManifestPath != "" { + // References are collected *after* manifest parsing + // so that we reflect any references to submodules. err = w.modMgr.EnqueueModuleOp(dir, op.OpTypeParseModuleManifest, decodeCalledModulesFunc(w.modMgr, w.watcher, dir)) if err != nil { return err } + } else { + // If there is no module manifest we still collect references + // as this module may also be called by other modules. + err = w.modMgr.EnqueueModuleOp(dir, op.OpTypeDecodeReferenceTargets, nil) + if err != nil { + return err + } + err = w.modMgr.EnqueueModuleOp(dir, op.OpTypeDecodeReferenceOrigins, nil) + if err != nil { + return err + } } + if dataDir.PluginLockFilePath != "" { err = w.modMgr.EnqueueModuleOp(dir, op.OpTypeObtainSchema, nil) if err != nil { diff --git a/internal/terraform/module/watcher.go b/internal/terraform/module/watcher.go index 1ad493885..cafcdc4b9 100644 --- a/internal/terraform/module/watcher.go +++ b/internal/terraform/module/watcher.go @@ -246,8 +246,8 @@ func decodeCalledModulesFunc(modMgr ModuleManager, w Watcher, modPath string) De modMgr.AddModule(mc.Path) modMgr.EnqueueModuleOp(mc.Path, op.OpTypeParseModuleConfiguration, nil) - modMgr.EnqueueModuleOp(mc.Path, op.OpTypeParseVariables, nil) modMgr.EnqueueModuleOp(mc.Path, op.OpTypeLoadModuleMetadata, nil) + modMgr.EnqueueModuleOp(mc.Path, op.OpTypeParseVariables, nil) modMgr.EnqueueModuleOp(mc.Path, op.OpTypeDecodeReferenceTargets, nil) modMgr.EnqueueModuleOp(mc.Path, op.OpTypeDecodeReferenceOrigins, nil) @@ -255,6 +255,9 @@ func decodeCalledModulesFunc(modMgr ModuleManager, w Watcher, modPath string) De w.AddModule(mc.Path) } } + + modMgr.EnqueueModuleOp(modPath, op.OpTypeDecodeReferenceTargets, nil) + modMgr.EnqueueModuleOp(modPath, op.OpTypeDecodeReferenceOrigins, nil) } }