From 04eb4784e56ae10c112906e677f7a46f6297c204 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 16 Apr 2021 16:56:07 +0100 Subject: [PATCH] Add support for traversals/references --- go.mod | 4 +- go.sum | 10 +- internal/decoder/decoder.go | 6 ++ internal/langserver/handlers/complete_test.go | 2 +- internal/langserver/handlers/did_change.go | 4 + internal/langserver/handlers/did_open.go | 1 + internal/lsp/completion.go | 2 + internal/lsp/token_encoder.go | 4 + internal/lsp/token_types.go | 1 + internal/state/module.go | 49 ++++++++++ internal/terraform/module/module_loader.go | 12 +++ internal/terraform/module/module_manager.go | 6 +- internal/terraform/module/module_ops.go | 92 +++++++++++++++++++ .../module/operation/op_type_string.go | 5 +- .../terraform/module/operation/operation.go | 1 + 15 files changed, 187 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 9eae342b7..90b117e59 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,12 @@ require ( github.com/hashicorp/go-memdb v1.3.2 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 - github.com/hashicorp/hcl-lang v0.0.0-20210419185146-8556dd730bc7 + github.com/hashicorp/hcl-lang v0.0.0-20210505191011-f735b3e2b548 github.com/hashicorp/hcl/v2 v2.10.0 github.com/hashicorp/terraform-exec v0.13.3 github.com/hashicorp/terraform-json v0.10.0 github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 - github.com/hashicorp/terraform-schema v0.0.0-20210428174709-ad0461b43827 + github.com/hashicorp/terraform-schema v0.0.0-20210505192708-22623ce70271 github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 github.com/mitchellh/cli v1.1.2 github.com/mitchellh/copystructure v1.1.2 diff --git a/go.sum b/go.sum index 68f303ddd..9d7059ef8 100644 --- a/go.sum +++ b/go.sum @@ -188,10 +188,9 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 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-20210419185146-8556dd730bc7 h1:6ajS9kFs/7UnRcy3dHThUnmicqU7VjGUn0RLOT6Ewq0= -github.com/hashicorp/hcl-lang v0.0.0-20210419185146-8556dd730bc7/go.mod h1:VRVfqufUmJSpWsoWDtYV/BejqCV+NNyS9V9vR0dcivs= +github.com/hashicorp/hcl-lang v0.0.0-20210505191011-f735b3e2b548 h1:UJcMJkdwVuDg7RSqrP5gqS2fPAT5WOENDMqzIDc0uxU= +github.com/hashicorp/hcl-lang v0.0.0-20210505191011-f735b3e2b548/go.mod h1:B3OEgaMz80/e+zVmTlJ8t2VL+k57up43LhGOcmOL5V4= github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= -github.com/hashicorp/hcl/v2 v2.9.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg= github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -206,8 +205,8 @@ github.com/hashicorp/terraform-json v0.10.0 h1:9syPD/Y5t+3uFjG8AiWVPu1bklJD8QB8i github.com/hashicorp/terraform-json v0.10.0/go.mod h1:3defM4kkMfttwiE7VakJDwCd4R+umhSQnvJwORXbprE= github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 h1:1FGtlkJw87UsTMg5s8jrekrHmUPUJaMcu6ELiVhQrNw= github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co= -github.com/hashicorp/terraform-schema v0.0.0-20210428174709-ad0461b43827 h1:wf2hrzKEi5WOZkp5U0GGLKsslbFrGob2GJ35rjwMiO0= -github.com/hashicorp/terraform-schema v0.0.0-20210428174709-ad0461b43827/go.mod h1:K3XjJOEolIgMNwNb/cVbSSO8nBxvO/JLDAuTOLbT7XQ= +github.com/hashicorp/terraform-schema v0.0.0-20210505192708-22623ce70271 h1:5PqDECk03F5c/HFIMuU1DSsmw/Umf/4JVGDYiYmgXdA= +github.com/hashicorp/terraform-schema v0.0.0-20210505192708-22623ce70271/go.mod h1:ceB2RvqFSFhsBU+VMq7qeFU+0x/RxPqN0tF8mKx0tv4= 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= @@ -379,7 +378,6 @@ github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.2.1/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.8.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.8.2/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.8.3 h1:48gwZXrdSADU2UW9eZKHprxAI7APZGW9XmExpJpSjT0= github.com/zclconf/go-cty v1.8.3/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= diff --git a/internal/decoder/decoder.go b/internal/decoder/decoder.go index 46438bf89..533fb923f 100644 --- a/internal/decoder/decoder.go +++ b/internal/decoder/decoder.go @@ -5,12 +5,18 @@ import ( "fmt" "github.com/hashicorp/hcl-lang/decoder" + "github.com/hashicorp/hcl-lang/lang" lsctx "github.com/hashicorp/terraform-ls/internal/context" "github.com/hashicorp/terraform-ls/internal/terraform/module" ) func DecoderForModule(ctx context.Context, mod module.Module) (*decoder.Decoder, error) { d := decoder.NewDecoder() + + d.SetReferenceReader(func() lang.References { + return mod.References + }) + d.SetUtmSource("terraform-ls") d.UseUtmContent(true) diff --git a/internal/langserver/handlers/complete_test.go b/internal/langserver/handlers/complete_test.go index dd360d951..0ad7d1c70 100644 --- a/internal/langserver/handlers/complete_test.go +++ b/internal/langserver/handlers/complete_test.go @@ -44,7 +44,7 @@ func TestCompletion_withValidData(t *testing.T) { ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ TerraformCalls: &exec.TerraformMockCalls{ PerWorkDir: map[string][]*mock.Call{ - tmpDir.Dir(): []*mock.Call{ + tmpDir.Dir(): { { Method: "Version", Repeatability: 1, diff --git a/internal/langserver/handlers/did_change.go b/internal/langserver/handlers/did_change.go index e9af61b8f..2dcde9f65 100644 --- a/internal/langserver/handlers/did_change.go +++ b/internal/langserver/handlers/did_change.go @@ -67,6 +67,10 @@ func TextDocumentDidChange(ctx context.Context, params lsp.DidChangeTextDocument if err != nil { return err } + err = modMgr.EnqueueModuleOpWait(mod.Path, op.OpTypeDecodeReferences) + if err != nil { + return err + } diags, err := lsctx.Diagnostics(ctx) if err != nil { diff --git a/internal/langserver/handlers/did_open.go b/internal/langserver/handlers/did_open.go index 525b410a7..33688dd30 100644 --- a/internal/langserver/handlers/did_open.go +++ b/internal/langserver/handlers/did_open.go @@ -48,6 +48,7 @@ func (lh *logHandler) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpe // TODO: Do this only if we can verify the file differs? modMgr.EnqueueModuleOpWait(mod.Path, op.OpTypeParseConfiguration) modMgr.EnqueueModuleOpWait(mod.Path, op.OpTypeLoadModuleMetadata) + modMgr.EnqueueModuleOpWait(mod.Path, op.OpTypeDecodeReferences) if mod.TerraformVersionState == op.OpStateUnknown { modMgr.EnqueueModuleOp(mod.Path, op.OpTypeGetTerraformVersion) diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 89422e08f..74127c396 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -48,6 +48,8 @@ func toCompletionItem(candidate lang.Candidate, caps lsp.CompletionClientCapabil kind = lsp.EnumCompletion case lang.MapCandidateKind, lang.ObjectCandidateKind: kind = lsp.StructCompletion + case lang.TraversalCandidateKind: + kind = lsp.VariableCompletion } // TODO: Omit item which uses kind unsupported by the client diff --git a/internal/lsp/token_encoder.go b/internal/lsp/token_encoder.go index e8a4cb470..2a4d1b454 100644 --- a/internal/lsp/token_encoder.go +++ b/internal/lsp/token_encoder.go @@ -47,6 +47,10 @@ func (te *TokenEncoder) encodeTokenOfIndex(i int) []float64 { tokenType = TokenTypeParameter case lang.TokenMapKey: tokenType = TokenTypeParameter + case lang.TokenKeyword: + tokenType = TokenTypeVariable + case lang.TokenTraversalStep: + tokenType = TokenTypeVariable default: return []float64{} diff --git a/internal/lsp/token_types.go b/internal/lsp/token_types.go index ca7d19cd1..cfb8d4ace 100644 --- a/internal/lsp/token_types.go +++ b/internal/lsp/token_types.go @@ -108,6 +108,7 @@ var ( TokenTypeKeyword, TokenTypeNumber, TokenTypeParameter, + TokenTypeVariable, } serverTokenModifiers = TokenModifiers{ TokenModifierDeprecated, diff --git a/internal/state/module.go b/internal/state/module.go index 48132084f..2cbabe43e 100644 --- a/internal/state/module.go +++ b/internal/state/module.go @@ -3,6 +3,7 @@ package state import ( "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl/v2" tfaddr "github.com/hashicorp/terraform-registry-address" tfmod "github.com/hashicorp/terraform-schema/module" @@ -32,6 +33,10 @@ type Module struct { ProviderSchemaErr error ProviderSchemaState op.OpState + References lang.References + ReferencesErr error + ReferencesState op.OpState + ParsedFiles map[string]*hcl.File ParsingErr error ParsingState op.OpState @@ -49,6 +54,7 @@ func newModule(modPath string) *Module { ModManifestState: op.OpStateUnknown, TerraformVersionState: op.OpStateUnknown, ProviderSchemaState: op.OpStateUnknown, + ReferencesState: op.OpStateUnknown, ParsingState: op.OpStateUnknown, MetaState: op.OpStateUnknown, } @@ -400,3 +406,46 @@ func (s *ModuleStore) UpdateDiagnostics(path string, diags map[string]hcl.Diagno txn.Commit() return nil } + +func (s *ModuleStore) SetReferencesState(path string, state op.OpState) error { + txn := s.db.Txn(true) + defer txn.Abort() + + mod, err := moduleByPath(txn, path) + if err != nil { + return err + } + + mod.ReferencesState = state + err = txn.Insert(s.tableName, mod) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +func (s *ModuleStore) UpdateReferences(path string, refs lang.References, rErr error) error { + txn := s.db.Txn(true) + txn.Defer(func() { + s.SetReferencesState(path, op.OpStateLoaded) + }) + defer txn.Abort() + + mod, err := moduleByPath(txn, path) + if err != nil { + return err + } + + mod.References = refs + mod.ReferencesErr = rErr + + err = txn.Insert(s.tableName, mod) + if err != nil { + return err + } + + txn.Commit() + return nil +} diff --git a/internal/terraform/module/module_loader.go b/internal/terraform/module/module_loader.go index 76fbfbf40..6f990cfa8 100644 --- a/internal/terraform/module/module_loader.go +++ b/internal/terraform/module/module_loader.go @@ -181,6 +181,12 @@ func (ml *moduleLoader) executeModuleOp(ctx context.Context, modOp ModuleOperati ml.logger.Printf("failed to load module metadata: %s", err) } return + case op.OpTypeDecodeReferences: + err := DecodeReferences(ml.modStore, ml.schemaStore, modOp.ModulePath) + if err != nil { + ml.logger.Printf("failed to decode references: %s", err) + } + return } ml.logger.Printf("%s: unknown operation (%#v) for module operation", @@ -226,6 +232,12 @@ func (ml *moduleLoader) EnqueueModuleOp(modOp ModuleOperation) error { return nil } ml.modStore.SetMetaState(modOp.ModulePath, op.OpStateQueued) + case op.OpTypeDecodeReferences: + if mod.MetaState == op.OpStateQueued { + // avoid enqueuing duplicate operation + return nil + } + ml.modStore.SetReferencesState(modOp.ModulePath, op.OpStateQueued) } ml.queue.PushOp(modOp) diff --git a/internal/terraform/module/module_manager.go b/internal/terraform/module/module_manager.go index 475843230..8b5e604ca 100644 --- a/internal/terraform/module/module_manager.go +++ b/internal/terraform/module/module_manager.go @@ -103,6 +103,10 @@ func (mm *moduleManager) SchemaForModule(modPath string) (*schema.BodySchema, er return nil, err } + return schemaForModule(mod, mm.schemaStore) +} + +func schemaForModule(mod *state.Module, schemaReader state.SchemaReader) (*schema.BodySchema, error) { var coreSchema *schema.BodySchema coreRequirements := make(version.Constraints, 0) if mod.TerraformVersion != nil { @@ -120,7 +124,7 @@ func (mm *moduleManager) SchemaForModule(modPath string) (*schema.BodySchema, er } sm := tfschema.NewSchemaMerger(coreSchema) - sm.SetSchemaReader(mm.schemaStore) + sm.SetSchemaReader(schemaReader) meta := &tfmodule.Meta{ Path: mod.Path, diff --git a/internal/terraform/module/module_ops.go b/internal/terraform/module/module_ops.go index 0b2995008..abb0d8c80 100644 --- a/internal/terraform/module/module_ops.go +++ b/internal/terraform/module/module_ops.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl-lang/decoder" + "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform-ls/internal/filesystem" @@ -17,6 +19,7 @@ import ( "github.com/hashicorp/terraform-schema/earlydecoder" "github.com/hashicorp/terraform-schema/module" tfschema "github.com/hashicorp/terraform-schema/schema" + "github.com/zclconf/go-cty/cty" ) type ModuleOperation struct { @@ -276,3 +279,92 @@ func LoadModuleMetadata(modStore *state.ModuleStore, modPath string) error { } return mErr } + +func DecodeReferences(modStore *state.ModuleStore, schemaReader state.SchemaReader, modPath string) error { + err := modStore.SetReferencesState(modPath, op.OpStateLoading) + if err != nil { + return err + } + + mod, err := modStore.ModuleByPath(modPath) + if err != nil { + return err + } + + d := decoder.NewDecoder() + for name, f := range mod.ParsedFiles { + err := d.LoadFile(name, f) + if err != nil { + return fmt.Errorf("failed to load a file: %w", err) + } + } + + fullSchema, schemaErr := schemaForModule(mod, schemaReader) + if schemaErr != nil { + sErr := modStore.UpdateReferences(modPath, lang.References{}, schemaErr) + if sErr != nil { + return sErr + } + return schemaErr + } + d.SetSchema(fullSchema) + + refs, rErr := d.DecodeReferences() + + bRefs := builtinReferences(modPath) + refs = append(refs, bRefs...) + + sErr := modStore.UpdateReferences(modPath, refs, rErr) + if sErr != nil { + return sErr + } + + return rErr +} + +var builtinScopeId = lang.ScopeId("builtin") + +func builtinReferences(modPath string) lang.References { + return lang.References{ + { + Addr: lang.Address{ + lang.RootStep{Name: "path"}, + lang.AttrStep{Name: "module"}, + }, + ScopeId: builtinScopeId, + Type: cty.String, + Description: lang.Markdown("The filesystem path of the module where the expression is placed\n\n" + + modPath), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "path"}, + lang.AttrStep{Name: "root"}, + }, + ScopeId: builtinScopeId, + Type: cty.String, + Description: lang.Markdown("The filesystem path of the root module of the configuration"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "path"}, + lang.AttrStep{Name: "cwd"}, + }, + ScopeId: builtinScopeId, + Type: cty.String, + Description: lang.Markdown("The filesystem path of the current working directory.\n\n" + + "In normal use of Terraform this is the same as `path.root`, " + + "but some advanced uses of Terraform run it from a directory " + + "other than the root module directory, causing these paths to be different."), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "terraform"}, + lang.AttrStep{Name: "workspace"}, + }, + ScopeId: builtinScopeId, + Type: cty.String, + Description: lang.Markdown("The name of the currently selected workspace"), + }, + } +} diff --git a/internal/terraform/module/operation/op_type_string.go b/internal/terraform/module/operation/op_type_string.go index ce8cd2dc0..e4a322288 100644 --- a/internal/terraform/module/operation/op_type_string.go +++ b/internal/terraform/module/operation/op_type_string.go @@ -14,11 +14,12 @@ func _() { _ = x[OpTypeParseConfiguration-3] _ = x[OpTypeParseModuleManifest-4] _ = x[OpTypeLoadModuleMetadata-5] + _ = x[OpTypeDecodeReferences-6] } -const _OpType_name = "OpTypeUnknownOpTypeGetTerraformVersionOpTypeObtainSchemaOpTypeParseConfigurationOpTypeParseModuleManifestOpTypeLoadModuleMetadata" +const _OpType_name = "OpTypeUnknownOpTypeGetTerraformVersionOpTypeObtainSchemaOpTypeParseConfigurationOpTypeParseModuleManifestOpTypeLoadModuleMetadataOpTypeDecodeReferences" -var _OpType_index = [...]uint8{0, 13, 38, 56, 80, 105, 129} +var _OpType_index = [...]uint8{0, 13, 38, 56, 80, 105, 129, 151} func (i OpType) String() string { if i >= OpType(len(_OpType_index)-1) { diff --git a/internal/terraform/module/operation/operation.go b/internal/terraform/module/operation/operation.go index 64d263eb4..bbe3ea6d1 100644 --- a/internal/terraform/module/operation/operation.go +++ b/internal/terraform/module/operation/operation.go @@ -20,4 +20,5 @@ const ( OpTypeParseConfiguration OpTypeParseModuleManifest OpTypeLoadModuleMetadata + OpTypeDecodeReferences )