From e96b87e5f3bb092f9ceccaa0b33742b0a5bd3949 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 2 Jun 2020 13:34:41 +0100 Subject: [PATCH] refactoring: Decouple generic HCL parsing logic This also helps keeping CompletionCandidate immutable and allows passing around already "trimmed" candidates as opposed to letting the lsp package handle the trimming. --- internal/hcl/hcl.go | 61 +++++++++--- internal/hcl/hcl_test.go | 13 ++- internal/hcl/types.go | 17 ++++ internal/lsp/completion.go | 18 ++-- internal/terraform/lang/config_block.go | 95 +++++++------------ internal/terraform/lang/config_block_test.go | 13 +-- internal/terraform/lang/datasource_block.go | 35 ++++--- .../terraform/lang/datasource_block_test.go | 18 +++- internal/terraform/lang/hcl_block.go | 16 ---- internal/terraform/lang/hcl_block_type.go | 3 +- internal/terraform/lang/hcl_parser.go | 37 ++++++-- internal/terraform/lang/hcl_parser_test.go | 35 ++----- internal/terraform/lang/parser.go | 56 ++++++----- internal/terraform/lang/parser_test.go | 24 ++--- internal/terraform/lang/provider_block.go | 28 +++--- .../terraform/lang/provider_block_test.go | 18 +++- internal/terraform/lang/resource_block.go | 31 +++--- .../terraform/lang/resource_block_test.go | 18 +++- internal/terraform/lang/types.go | 12 +-- internal/terraform/lang/utils.go | 24 +++-- langserver/handlers/complete.go | 26 +---- 21 files changed, 311 insertions(+), 287 deletions(-) create mode 100644 internal/hcl/types.go diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index 723214ece..b2ac3cab3 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -9,11 +9,6 @@ import ( "github.com/hashicorp/terraform-ls/internal/filesystem" ) -type File interface { - BlockTokensAtPosition(hcl.Pos) (hclsyntax.Tokens, error) - TokenAtPosition(hcl.Pos) (hclsyntax.Token, error) -} - type file struct { filename string content []byte @@ -25,13 +20,43 @@ type parsedFile struct { Tokens hclsyntax.Tokens } -func NewFile(f filesystem.File) File { +type parsedBlock struct { + tokens hclsyntax.Tokens +} + +func (pb *parsedBlock) Tokens() hclsyntax.Tokens { + return pb.tokens +} + +func (pb *parsedBlock) TokenAtPosition(pos hcl.Pos) (hclsyntax.Token, error) { + for _, t := range pb.tokens { + if rangeContainsOffset(t.Range, pos.Byte) { + return t, nil + } + } + + return hclsyntax.Token{}, &NoTokenFoundErr{pos} +} + +func NewFile(f filesystem.File) TokenizedFile { return &file{ filename: f.Filename(), content: []byte(f.Text()), } } +func NewTestFile(b []byte) TokenizedFile { + return &file{ + filename: "/test.tf", + content: b, + } +} + +func NewTestBlock(b []byte) (TokenizedBlock, error) { + f := NewTestFile(b) + return f.BlockAtPosition(hcllib.InitialPos) +} + func (f *file) parse() (*parsedFile, error) { if f.pf != nil { return f.pf, nil @@ -60,32 +85,42 @@ func (f *file) parse() (*parsedFile, error) { return f.pf, nil } -func (f *file) BlockTokensAtPosition(pos hcllib.Pos) (hclsyntax.Tokens, error) { +func (f *file) PosInBlock(pos hcl.Pos) bool { + _, err := f.BlockAtPosition(pos) + if IsNoBlockFoundErr(err) { + return false + } + + return true +} + +func (f *file) BlockAtPosition(pos hcllib.Pos) (TokenizedBlock, error) { pf, _ := f.parse() body, ok := pf.Body.(*hclsyntax.Body) if !ok { - return hclsyntax.Tokens{}, fmt.Errorf("unexpected body type (%T)", body) + return nil, fmt.Errorf("unexpected body type (%T)", body) } if body.SrcRange.Empty() && pos != hcllib.InitialPos { - return hclsyntax.Tokens{}, &InvalidHclPosErr{pos, body.SrcRange} + return nil, &InvalidHclPosErr{pos, body.SrcRange} } if !body.SrcRange.Empty() { if posIsEqual(body.SrcRange.End, pos) { - return pf.Tokens, &NoBlockFoundErr{pos} + return nil, &NoBlockFoundErr{pos} } if !body.SrcRange.ContainsPos(pos) { - return hclsyntax.Tokens{}, &InvalidHclPosErr{pos, body.SrcRange} + return nil, &InvalidHclPosErr{pos, body.SrcRange} } } for _, block := range body.Blocks { if block.Range().ContainsPos(pos) { - return definitionTokens(tokensInRange(pf.Tokens, block.Range())), nil + dt := definitionTokens(tokensInRange(pf.Tokens, block.Range())) + return &parsedBlock{dt}, nil } } - return pf.Tokens, &NoBlockFoundErr{pos} + return nil, &NoBlockFoundErr{pos} } func (f *file) TokenAtPosition(pos hcllib.Pos) (hclsyntax.Token, error) { diff --git a/internal/hcl/hcl_test.go b/internal/hcl/hcl_test.go index 1d3ebd543..c87de91f0 100644 --- a/internal/hcl/hcl_test.go +++ b/internal/hcl/hcl_test.go @@ -129,7 +129,7 @@ func TestFile_BlockAtPosition(t *testing.T) { }, &InvalidHclPosErr{ Pos: hcl.Pos{Line: -42, Column: -3, Byte: -46}, - InRange: hcl.Range{Filename: "test.tf", Start: hcl.InitialPos, End: hcl.InitialPos}, + InRange: hcl.Range{Filename: "/test.tf", Start: hcl.InitialPos, End: hcl.InitialPos}, }, nil, }, @@ -143,7 +143,7 @@ func TestFile_BlockAtPosition(t *testing.T) { }, &InvalidHclPosErr{ Pos: hcl.Pos{Line: 42, Column: 3, Byte: 46}, - InRange: hcl.Range{Filename: "test.tf", Start: hcl.InitialPos, End: hcl.InitialPos}, + InRange: hcl.Range{Filename: "/test.tf", Start: hcl.InitialPos, End: hcl.InitialPos}, }, nil, }, @@ -161,7 +161,7 @@ func TestFile_BlockAtPosition(t *testing.T) { &InvalidHclPosErr{ Pos: hcl.Pos{Line: 42, Column: 3, Byte: 46}, InRange: hcl.Range{ - Filename: "test.tf", + Filename: "/test.tf", Start: hcl.InitialPos, End: hcl.Pos{Column: 1, Line: 4, Byte: 20}, }, @@ -237,10 +237,9 @@ provider "aws" { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i+1, tc.name), func(t *testing.T) { - fsFile := filesystem.NewFile("test.tf", []byte(tc.content)) - f := NewFile(fsFile) + f := NewTestFile([]byte(tc.content)) - tokens, err := f.BlockTokensAtPosition(tc.pos) + tBlock, err := f.BlockAtPosition(tc.pos) if err != nil { if tc.expectedErr == nil { t.Fatal(err) @@ -254,7 +253,7 @@ provider "aws" { t.Fatalf("Expected error: %s", tc.expectedErr) } - if diff := cmp.Diff(hclsyntax.Tokens(tc.expectedTokens), tokens, opts...); diff != "" { + if diff := cmp.Diff(hclsyntax.Tokens(tc.expectedTokens), tBlock.Tokens(), opts...); diff != "" { t.Fatalf("Unexpected token difference: %s", diff) } diff --git a/internal/hcl/types.go b/internal/hcl/types.go new file mode 100644 index 000000000..22f539b5e --- /dev/null +++ b/internal/hcl/types.go @@ -0,0 +1,17 @@ +package hcl + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type TokenizedFile interface { + BlockAtPosition(hcl.Pos) (TokenizedBlock, error) + TokenAtPosition(hcl.Pos) (hclsyntax.Token, error) + PosInBlock(hcl.Pos) bool +} + +type TokenizedBlock interface { + TokenAtPosition(hcl.Pos) (hclsyntax.Token, error) + Tokens() hclsyntax.Tokens +} diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 64865b665..d15ef32ba 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -1,11 +1,12 @@ package lsp import ( + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform-ls/internal/terraform/lang" lsp "github.com/sourcegraph/go-lsp" ) -func CompletionList(candidates lang.CompletionCandidates, caps lsp.TextDocumentClientCapabilities) lsp.CompletionList { +func CompletionList(candidates lang.CompletionCandidates, pos hcl.Pos, caps lsp.TextDocumentClientCapabilities) lsp.CompletionList { snippetSupport := caps.Completion.CompletionItem.SnippetSupport list := lsp.CompletionList{} @@ -18,13 +19,13 @@ func CompletionList(candidates lang.CompletionCandidates, caps lsp.TextDocumentC list.IsIncomplete = !candidates.IsComplete() list.Items = make([]lsp.CompletionItem, len(cList)) for i, c := range cList { - list.Items[i] = CompletionItem(c, snippetSupport) + list.Items[i] = CompletionItem(c, pos, snippetSupport) } return list } -func CompletionItem(candidate lang.CompletionCandidate, snippetSupport bool) lsp.CompletionItem { +func CompletionItem(candidate lang.CompletionCandidate, pos hcl.Pos, snippetSupport bool) lsp.CompletionItem { // TODO: deprecated / tags? doc := "" @@ -33,7 +34,6 @@ func CompletionItem(candidate lang.CompletionCandidate, snippetSupport bool) lsp doc = c.Value() } - r := candidate.PrefixRange() if snippetSupport { return lsp.CompletionItem{ Label: candidate.Label(), @@ -43,8 +43,8 @@ func CompletionItem(candidate lang.CompletionCandidate, snippetSupport bool) lsp Documentation: doc, TextEdit: &lsp.TextEdit{ Range: lsp.Range{ - Start: lsp.Position{Line: r.Start.Line - 1, Character: r.Start.Column - 1}, - End: lsp.Position{Line: r.End.Line - 1, Character: r.End.Column - 1}, + Start: lsp.Position{Line: pos.Line - 1, Character: pos.Column - 1}, + End: lsp.Position{Line: pos.Line - 1, Character: pos.Column - 1}, }, NewText: candidate.Snippet(), }, @@ -59,10 +59,10 @@ func CompletionItem(candidate lang.CompletionCandidate, snippetSupport bool) lsp Documentation: doc, TextEdit: &lsp.TextEdit{ Range: lsp.Range{ - Start: lsp.Position{Line: r.Start.Line - 1, Character: r.Start.Column - 1}, - End: lsp.Position{Line: r.End.Line - 1, Character: r.End.Column - 1}, + Start: lsp.Position{Line: pos.Line - 1, Character: pos.Column - 1}, + End: lsp.Position{Line: pos.Line - 1, Character: pos.Column - 1}, }, - NewText: candidate.Label(), + NewText: candidate.PlainText(), }, } } diff --git a/internal/terraform/lang/config_block.go b/internal/terraform/lang/config_block.go index f201524e6..504fd40df 100644 --- a/internal/terraform/lang/config_block.go +++ b/internal/terraform/lang/config_block.go @@ -7,30 +7,30 @@ import ( "strings" hcl "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" tfjson "github.com/hashicorp/terraform-json" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" ) type configBlockFactory interface { - New(hclsyntax.Tokens) (ConfigBlock, error) + New(ihcl.TokenizedBlock) (ConfigBlock, error) LabelSchema() LabelSchema Documentation() MarkupContent } -type labelCandidates map[string][]CompletionCandidate +type labelCandidates map[string][]*labelCandidate type completableLabels struct { - logger *log.Logger - block Block - tokens hclsyntax.Tokens - labels labelCandidates + logger *log.Logger + parsedLabels []*ParsedLabel + tBlock ihcl.TokenizedBlock + labels labelCandidates } func (cl *completableLabels) completionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) { list := &completeList{ candidates: make([]CompletionCandidate, 0), } - l, ok := cl.block.LabelAtPos(pos) + l, ok := LabelAtPos(cl.parsedLabels, pos) if !ok { cl.logger.Printf("label not found at %#v", pos) return list, nil @@ -42,12 +42,14 @@ func (cl *completableLabels) completionCandidatesAtPos(pos hcl.Pos) (CompletionC } cl.logger.Printf("completing label %q ...", l.Name) - prefix := wordBeforePos(cl.tokens, pos) + + prefix := prefixAtPos(cl.tBlock, pos) + for _, c := range candidates { if !strings.HasPrefix(c.Label(), prefix) { continue } - c.SetPrefix(prefix) + c.prefix = prefix list.candidates = append(list.candidates, c) } list.Sort() @@ -58,9 +60,10 @@ func (cl *completableLabels) completionCandidatesAtPos(pos hcl.Pos) (CompletionC // completableBlock provides common completion functionality // for any Block implementation type completableBlock struct { - logger *log.Logger - block Block - tokens hclsyntax.Tokens + logger *log.Logger + parsedLabels []*ParsedLabel + tBlock ihcl.TokenizedBlock + schema *tfjson.SchemaBlock } func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) { @@ -68,18 +71,20 @@ func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCa candidates: make([]CompletionCandidate, 0), } - if !cb.block.PosInBody(pos) { + block := ParseBlock(cb.tBlock, cb.schema) + + if !block.PosInBody(pos) { cb.logger.Println("avoiding completion outside of block body") return nil, nil } - if cb.block.PosInAttribute(pos) { + if block.PosInAttribute(pos) { cb.logger.Println("avoiding completion in the middle of existing attribute") return nil, nil } // Completing the body (attributes and nested blocks) - b, ok := cb.block.BlockAtPos(pos) + b, ok := block.BlockAtPos(pos) if !ok { // This should never happen as the completion // should only be called on a block the "pos" points to @@ -87,7 +92,8 @@ func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCa return nil, nil } - prefix := wordBeforePos(cb.tokens, pos) + prefix := prefixAtPos(cb.tBlock, pos) + for name, attr := range b.Attributes() { if !strings.HasPrefix(name, prefix) { continue @@ -98,7 +104,6 @@ func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCa list.candidates = append(list.candidates, &attributeCandidate{ Name: name, Attr: attr, - Pos: pos, Prefix: prefix, }) } @@ -113,7 +118,6 @@ func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCa list.candidates = append(list.candidates, &nestedBlockCandidate{ Name: name, BlockType: block, - Pos: pos, Prefix: prefix, }) } @@ -150,7 +154,6 @@ type labelCandidate struct { label string detail string documentation MarkupContent - pos hcl.Pos prefix string } @@ -167,27 +170,16 @@ func (c *labelCandidate) Documentation() MarkupContent { } func (c *labelCandidate) Snippet() string { - return c.label -} - -func (c *labelCandidate) SetPrefix(prefix string) { - c.prefix = prefix + return c.PlainText() } -func (c *labelCandidate) PrefixRange() hcl.Range { - return hcl.Range{ - Start: hcl.Pos{ - Line: c.pos.Line, - Column: c.pos.Column - len(c.prefix), - }, - End: c.pos, - } +func (c *labelCandidate) PlainText() string { + return strings.TrimPrefix(c.label, c.prefix) } type attributeCandidate struct { Name string Attr *Attribute - Pos hcl.Pos Prefix string } @@ -216,27 +208,17 @@ func (c *attributeCandidate) Documentation() MarkupContent { } func (c *attributeCandidate) Snippet() string { - return fmt.Sprintf("%s = %s", c.Name, snippetForAttrType(0, c.Attr.Schema().AttributeType)) -} - -func (c *attributeCandidate) SetPrefix(prefix string) { - c.Prefix = prefix + name := strings.TrimPrefix(c.Name, c.Prefix) + return fmt.Sprintf("%s = %s", name, snippetForAttrType(0, c.Attr.Schema().AttributeType)) } -func (c *attributeCandidate) PrefixRange() hcl.Range { - return hcl.Range{ - Start: hcl.Pos{ - Line: c.Pos.Line, - Column: c.Pos.Column - len(c.Prefix), - }, - End: c.Pos, - } +func (c *attributeCandidate) PlainText() string { + return strings.TrimPrefix(c.Name, c.Prefix) } type nestedBlockCandidate struct { Name string BlockType *BlockType - Pos hcl.Pos Prefix string } @@ -259,19 +241,10 @@ func (c *nestedBlockCandidate) Documentation() MarkupContent { } func (c *nestedBlockCandidate) Snippet() string { - return snippetForNestedBlock(c.Name) + name := strings.TrimPrefix(c.Name, c.Prefix) + return snippetForNestedBlock(name) } -func (c *nestedBlockCandidate) SetPrefix(prefix string) { - c.Prefix = prefix -} - -func (c *nestedBlockCandidate) PrefixRange() hcl.Range { - return hcl.Range{ - Start: hcl.Pos{ - Line: c.Pos.Line, - Column: c.Pos.Column - len(c.Prefix), - }, - End: c.Pos, - } +func (c *nestedBlockCandidate) PlainText() string { + return strings.TrimPrefix(c.Name, c.Prefix) } diff --git a/internal/terraform/lang/config_block_test.go b/internal/terraform/lang/config_block_test.go index 6df682203..ac8f79d02 100644 --- a/internal/terraform/lang/config_block_test.go +++ b/internal/terraform/lang/config_block_test.go @@ -256,16 +256,13 @@ func TestCompletableBlock_CompletionCandidatesAtPos(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.src) - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.src)) - if err != nil { - t.Fatal(err) - } + tBlock := newTestBlock(t, tc.src) cb := &completableBlock{ - logger: testLogger(), - block: ParseBlock(block, []*ParsedLabel{}, tc.sb), - tokens: tokens, + logger: testLogger(), + parsedLabels: []*ParsedLabel{}, + schema: tc.sb, + tBlock: tBlock, } list, err := cb.completionCandidatesAtPos(tc.pos) diff --git a/internal/terraform/lang/datasource_block.go b/internal/terraform/lang/datasource_block.go index 3af4eb34b..548384707 100644 --- a/internal/terraform/lang/datasource_block.go +++ b/internal/terraform/lang/datasource_block.go @@ -7,6 +7,7 @@ import ( hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" tfjson "github.com/hashicorp/terraform-json" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) @@ -16,7 +17,7 @@ type datasourceBlockFactory struct { schemaReader schema.Reader } -func (f *datasourceBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) { +func (f *datasourceBlockFactory) New(tBlock ihcl.TokenizedBlock) (ConfigBlock, error) { if f.logger == nil { f.logger = discardLog() } @@ -25,7 +26,7 @@ func (f *datasourceBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, erro logger: f.logger, labelSchema: f.LabelSchema(), - tokens: tokens, + tBlock: tBlock, sr: f.schemaReader, }, nil } @@ -52,7 +53,7 @@ type datasourceBlock struct { labelSchema LabelSchema labels []*ParsedLabel - tokens hclsyntax.Tokens + tBlock ihcl.TokenizedBlock sr schema.Reader } @@ -81,7 +82,9 @@ func (r *datasourceBlock) Labels() []*ParsedLabel { if r.labels != nil { return r.labels } - labels := ParseLabels(r.tokens, r.labelSchema) + + labels := make([]*ParsedLabel, len(r.labelSchema)) + labels = ParseLabels(r.tBlock, r.labelSchema) r.labels = labels return r.labels @@ -96,7 +99,9 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand return nil, &noSchemaReaderErr{r.BlockType()} } - hclBlock, _ := hclsyntax.ParseBlockFromTokens(r.tokens) + // We ignore diags as we assume incomplete (invalid) configuration + hclBlock, _ := hclsyntax.ParseBlockFromTokens(r.tBlock.Tokens()) + if PosInLabels(hclBlock, pos) { dataSources, err := r.sr.DataSources() if err != nil { @@ -104,11 +109,11 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand } cl := &completableLabels{ - logger: r.logger, - block: ParseBlock(hclBlock, r.Labels(), nil), - tokens: r.tokens, + logger: r.logger, + parsedLabels: r.Labels(), + tBlock: r.tBlock, labels: labelCandidates{ - "type": dataSourceCandidates(dataSources, pos), + "type": dataSourceCandidates(dataSources), }, } @@ -120,15 +125,16 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand return nil, err } cb := &completableBlock{ - logger: r.logger, - block: ParseBlock(hclBlock, r.Labels(), rSchema.Block), - tokens: r.tokens, + logger: r.logger, + parsedLabels: r.Labels(), + schema: rSchema.Block, + tBlock: r.tBlock, } return cb.completionCandidatesAtPos(pos) } -func dataSourceCandidates(dataSources []schema.DataSource, pos hcl.Pos) []CompletionCandidate { - candidates := []CompletionCandidate{} +func dataSourceCandidates(dataSources []schema.DataSource) []*labelCandidate { + candidates := []*labelCandidate{} for _, ds := range dataSources { var desc MarkupContent = PlainText(ds.Description) if ds.DescriptionKind == tfjson.SchemaDescriptionKindMarkdown { @@ -139,7 +145,6 @@ func dataSourceCandidates(dataSources []schema.DataSource, pos hcl.Pos) []Comple label: ds.Name, detail: fmt.Sprintf("Data Source (%s)", ds.Provider), documentation: desc, - pos: pos, }) } return candidates diff --git a/internal/terraform/lang/datasource_block_test.go b/internal/terraform/lang/datasource_block_test.go index a524c9807..b33e68fe0 100644 --- a/internal/terraform/lang/datasource_block_test.go +++ b/internal/terraform/lang/datasource_block_test.go @@ -58,10 +58,10 @@ func TestDatasourceBlock_Name(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.src) + tBlock := newTestBlock(t, tc.src) pf := &datasourceBlockFactory{logger: log.New(os.Stdout, "", 0)} - p, err := pf.New(tokens) + p, err := pf.New(tBlock) if err != nil { if tc.expectedErr != nil && err.Error() == tc.expectedErr.Error() { @@ -145,6 +145,16 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) { }, nil, }, + { + "missing type", + `data "" "" { +}`, + simpleSchema, + nil, + hcl.Pos{Line: 2, Column: 1, Byte: 13}, + []renderedCandidate{}, + &schema.SchemaUnavailableErr{BlockType: "data", FullName: ""}, + }, { "schema reader error", `data "custom_ds" "name" { @@ -179,7 +189,7 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.src) + tBlock := newTestBlock(t, tc.src) pf := &datasourceBlockFactory{ logger: log.New(os.Stdout, "", 0), @@ -188,7 +198,7 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) { DataSourceSchemaErr: tc.readerErr, }, } - p, err := pf.New(tokens) + p, err := pf.New(tBlock) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/lang/hcl_block.go b/internal/terraform/lang/hcl_block.go index 352c8cff5..6e05595fb 100644 --- a/internal/terraform/lang/hcl_block.go +++ b/internal/terraform/lang/hcl_block.go @@ -7,7 +7,6 @@ import ( type parsedBlock struct { hclBlock *hclsyntax.Block - labels []*ParsedLabel AttributesMap map[string]*Attribute BlockTypesMap map[string]*BlockType @@ -49,21 +48,6 @@ func (b *parsedBlock) Range() hcl.Range { return b.hclBlock.Range() } -func (b *parsedBlock) PosInLabels(pos hcl.Pos) bool { - return PosInLabels(b.hclBlock, pos) -} - -func (b *parsedBlock) LabelAtPos(pos hcl.Pos) (*ParsedLabel, bool) { - for i, rng := range b.hclBlock.LabelRanges { - if rangeContainsOffset(rng, pos.Byte) { - // TODO: Guard against crashes when user sets label where we don't expect it - return b.labels[i], true - } - } - - return nil, false -} - func (b *parsedBlock) PosInBody(pos hcl.Pos) bool { for _, blockType := range b.BlockTypesMap { for _, b := range blockType.BlockList { diff --git a/internal/terraform/lang/hcl_block_type.go b/internal/terraform/lang/hcl_block_type.go index 6a48ef505..3d01815c1 100644 --- a/internal/terraform/lang/hcl_block_type.go +++ b/internal/terraform/lang/hcl_block_type.go @@ -62,8 +62,7 @@ func (bt BlockTypes) AddBlock(name string, block *hclsyntax.Block, typeSchema *t if block != nil { // SDK doesn't support named blocks yet, so we expect no labels here for now - labels := make([]*ParsedLabel, 0) - bt[name].BlockList = append(bt[name].BlockList, ParseBlock(block, labels, typeSchema.Block)) + bt[name].BlockList = append(bt[name].BlockList, parseBlock(block, typeSchema.Block)) } } diff --git a/internal/terraform/lang/hcl_parser.go b/internal/terraform/lang/hcl_parser.go index 0919e728c..6e140d236 100644 --- a/internal/terraform/lang/hcl_parser.go +++ b/internal/terraform/lang/hcl_parser.go @@ -6,12 +6,21 @@ import ( hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" tfjson "github.com/hashicorp/terraform-json" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" ) -func ParseBlock(block *hclsyntax.Block, labels []*ParsedLabel, schema *tfjson.SchemaBlock) Block { +// ParseBlock parses HCL block's tokens based on tfjson's SchemaBlock +// and keeps hold of all tfjson schema details on block or attribute level +func ParseBlock(tBlock ihcl.TokenizedBlock, schema *tfjson.SchemaBlock) Block { + // We ignore diags as we assume incomplete (invalid) configuration + hclBlock, _ := hclsyntax.ParseBlockFromTokens(tBlock.Tokens()) + + return parseBlock(hclBlock, schema) +} + +func parseBlock(block *hclsyntax.Block, schema *tfjson.SchemaBlock) Block { b := &parsedBlock{ hclBlock: block, - labels: labels, } if block == nil { return b @@ -33,30 +42,46 @@ func ParseBlock(block *hclsyntax.Block, labels []*ParsedLabel, schema *tfjson.Sc // ParseLabels parses HCL block's tokens based on LabelSchema, // returning labels as a slice of *ParsedLabel -func ParseLabels(tokens hclsyntax.Tokens, schema LabelSchema) []*ParsedLabel { +func ParseLabels(tBlock ihcl.TokenizedBlock, schema LabelSchema) []*ParsedLabel { // We ignore diags as we assume incomplete (invalid) configuration - hclBlock, _ := hclsyntax.ParseBlockFromTokens(tokens) + hclBlock, _ := hclsyntax.ParseBlockFromTokens(tBlock.Tokens()) - return parseLabels(hclBlock.Type, schema, hclBlock.Labels) + return parseLabels(hclBlock, schema) } -func parseLabels(blockType string, schema LabelSchema, parsed []string) []*ParsedLabel { +func parseLabels(block *hclsyntax.Block, schema LabelSchema) []*ParsedLabel { + parsed := block.Labels + labels := make([]*ParsedLabel, len(schema)) for i, l := range schema { var value string + var rng hcl.Range if len(parsed)-1 >= i { value = parsed[i] + rng = block.LabelRanges[i] } labels[i] = &ParsedLabel{ Name: l.Name, Value: value, + Range: rng, } } return labels } +func LabelAtPos(labels []*ParsedLabel, pos hcl.Pos) (*ParsedLabel, bool) { + for _, l := range labels { + if rangeContainsOffset(l.Range, pos.Byte) { + // TODO: Guard against crashes when user sets label where we don't expect it + return l, true + } + } + + return nil, false +} + func AsHCLSyntaxBlock(block *hcl.Block) (*hclsyntax.Block, error) { if block == nil { return nil, nil diff --git a/internal/terraform/lang/hcl_parser_test.go b/internal/terraform/lang/hcl_parser_test.go index c14910625..e84716442 100644 --- a/internal/terraform/lang/hcl_parser_test.go +++ b/internal/terraform/lang/hcl_parser_test.go @@ -22,13 +22,6 @@ func TestParseBlock_attributesAndBlockTypes(t *testing.T) { expectedAttributes map[string]*Attribute expectedBlockTypes map[string]*BlockType }{ - { - "empty cfg, nil schema", - "", - nil, - nil, - nil, - }, { "empty block, nil schema", `myblock {}`, @@ -302,12 +295,9 @@ func TestParseBlock_attributesAndBlockTypes(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.cfg)) - if err != nil { - t.Fatal(err) - } + tBlock := newTestBlock(t, tc.cfg) - b := ParseBlock(block, []*ParsedLabel{}, tc.schema) + b := ParseBlock(tBlock, tc.schema) if diff := cmp.Diff(tc.expectedAttributes, b.Attributes(), opts...); diff != "" { t.Fatalf("Attributes don't match.\n%s", diff) @@ -466,11 +456,8 @@ func TestBlock_BlockAtPos(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.cfg)) - if err != nil { - t.Fatal(err) - } - b := ParseBlock(block, []*ParsedLabel{}, schema) + tBlock := newTestBlock(t, tc.cfg) + b := ParseBlock(tBlock, schema) fBlock, _ := b.BlockAtPos(tc.pos) if diff := cmp.Diff(tc.expectedBlock, fBlock, opts...); diff != "" { @@ -626,11 +613,8 @@ func TestBlock_PosInBody(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.cfg)) - if err != nil { - t.Fatal(err) - } - b := ParseBlock(block, []*ParsedLabel{}, schema) + tBlock := newTestBlock(t, tc.cfg) + b := ParseBlock(tBlock, schema) isInBody := b.PosInBody(tc.pos) if tc.expected != isInBody { @@ -761,11 +745,8 @@ func TestBlock_PosInAttributes(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.cfg)) - if err != nil { - t.Fatal(err) - } - b := ParseBlock(block, []*ParsedLabel{}, schema) + tBlock := newTestBlock(t, tc.cfg) + b := ParseBlock(tBlock, schema) isInAttribute := b.PosInAttribute(tc.pos) if tc.expected != isInAttribute { diff --git a/internal/terraform/lang/parser.go b/internal/terraform/lang/parser.go index 054a1a7b6..5c520cc87 100644 --- a/internal/terraform/lang/parser.go +++ b/internal/terraform/lang/parser.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-version" hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" "github.com/hashicorp/terraform-ls/internal/terraform/errors" "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) @@ -93,14 +94,32 @@ func (p *parser) blockTypes() map[string]configBlockFactory { } } -func (p *parser) BlockTypeCandidates(tokens hclsyntax.Tokens, pos hcl.Pos) CompletionCandidates { +func (p *parser) CompletionCandidatesAtPos(file ihcl.TokenizedFile, pos hcl.Pos) (CompletionCandidates, error) { + if !file.PosInBlock(pos) { + return p.BlockTypeCandidates(file, pos), nil + } + + block, err := file.BlockAtPosition(pos) + if err != nil { + return nil, fmt.Errorf("finding HCL block failed: %#v", err) + } + + cfgBlock, err := p.ParseBlockFromTokens(block) + if err != nil { + return nil, fmt.Errorf("finding config block failed: %w", err) + } + + return cfgBlock.CompletionCandidatesAtPos(pos) +} + +func (p *parser) BlockTypeCandidates(file ihcl.TokenizedFile, pos hcl.Pos) CompletionCandidates { bTypes := p.blockTypes() list := &completeList{ candidates: make([]CompletionCandidate, 0), } - prefix := wordBeforePos(tokens, pos) + prefix := prefixAtPos(file, pos) for name, t := range bTypes { if !strings.HasPrefix(name, prefix) { continue @@ -110,7 +129,6 @@ func (p *parser) BlockTypeCandidates(tokens hclsyntax.Tokens, pos hcl.Pos) Compl LabelSchema: t.LabelSchema(), documentation: t.Documentation(), prefix: prefix, - pos: pos, }) } @@ -122,15 +140,19 @@ type completableBlockType struct { LabelSchema LabelSchema documentation MarkupContent prefix string - pos hcl.Pos } func (bt *completableBlockType) Label() string { return bt.TypeName } +func (bt *completableBlockType) PlainText() string { + return strings.TrimPrefix(bt.TypeName, bt.prefix) +} + func (bt *completableBlockType) Snippet() string { - return snippetForBlock(bt.TypeName, bt.LabelSchema) + typeName := strings.TrimPrefix(bt.TypeName, bt.prefix) + return snippetForBlock(typeName, bt.LabelSchema) } func (bt *completableBlockType) Detail() string { @@ -141,31 +163,13 @@ func (bt *completableBlockType) Documentation() MarkupContent { return bt.documentation } -func (bt *completableBlockType) SetPrefix(prefix string) { - bt.prefix = prefix -} - -func (bt *completableBlockType) PrefixRange() hcl.Range { - return hcl.Range{ - Start: hcl.Pos{ - Line: bt.pos.Line, - Column: bt.pos.Column - len(bt.prefix), - }, - End: bt.pos, - } -} - -func (p *parser) ParseBlockFromTokens(tokens hclsyntax.Tokens) (ConfigBlock, error) { - if len(tokens) == 0 { - return nil, EmptyConfigErr - } - +func (p *parser) ParseBlockFromTokens(tBlock ihcl.TokenizedBlock) (ConfigBlock, error) { // It is probably excessive to be parsing the whole block just for type // but there is no avoiding it without refactoring the upstream HCL parser // and it should not hurt the performance too much // // We ignore diags as we assume incomplete (invalid) configuration - block, _ := hclsyntax.ParseBlockFromTokens(tokens) + block, _ := hclsyntax.ParseBlockFromTokens(tBlock.Tokens()) p.logger.Printf("Parsed block type: %q", block.Type) @@ -174,7 +178,7 @@ func (p *parser) ParseBlockFromTokens(tokens hclsyntax.Tokens) (ConfigBlock, err return nil, &unknownBlockTypeErr{block.Type} } - cfgBlock, err := f.New(tokens) + cfgBlock, err := f.New(tBlock) if err != nil { return nil, fmt.Errorf("%s: %w", block.Type, err) } diff --git a/internal/terraform/lang/parser_test.go b/internal/terraform/lang/parser_test.go index f54e60a1d..342798071 100644 --- a/internal/terraform/lang/parser_test.go +++ b/internal/terraform/lang/parser_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" ) func TestParser_BlockTypeCandidates_len(t *testing.T) { @@ -24,8 +25,8 @@ provider "aws" { Column: 1, Byte: 0, } - tokens := lexConfig(t, content) - candidates := p.BlockTypeCandidates(tokens, pos) + tFile := ihcl.NewTestFile([]byte(content)) + candidates := p.BlockTypeCandidates(tFile, pos) if candidates.Len() < 3 { t.Fatalf("Expected >= 3 candidates, %d given", candidates.Len()) } @@ -42,8 +43,8 @@ provider "aws" { Column: 1, Byte: 0, } - tokens := lexConfig(t, content) - list := p.BlockTypeCandidates(tokens, pos) + tFile := ihcl.NewTestFile([]byte(content)) + list := p.BlockTypeCandidates(tFile, pos) rendered := renderCandidates(list, hcl.InitialPos) sortRenderedCandidates(rendered) @@ -98,10 +99,10 @@ func TestParser_ParseBlockFromTokens(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.cfg) + tBlock := newTestBlock(t, tc.cfg) p := newParser() - cfgBlock, err := p.ParseBlockFromTokens(tokens) + cfgBlock, err := p.ParseBlockFromTokens(tBlock) if err != nil { if errors.Is(err, tc.expectedErr) { return @@ -122,13 +123,12 @@ func TestParser_ParseBlockFromTokens(t *testing.T) { } } -func lexConfig(t *testing.T, src string) hclsyntax.Tokens { - tokens, diags := hclsyntax.LexConfig([]byte(src), "/test.tf", hcl.InitialPos) - if diags.HasErrors() { - t.Fatal(diags) +func newTestBlock(t *testing.T, src string) ihcl.TokenizedBlock { + b, err := ihcl.NewTestBlock([]byte(src)) + if err != nil { + t.Fatal(err) } - - return tokens + return b } func parseHclBlock(t *testing.T, src string) *hcl.Block { diff --git a/internal/terraform/lang/provider_block.go b/internal/terraform/lang/provider_block.go index c4828212a..5c0b89aab 100644 --- a/internal/terraform/lang/provider_block.go +++ b/internal/terraform/lang/provider_block.go @@ -5,6 +5,7 @@ import ( hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) @@ -14,7 +15,7 @@ type providerBlockFactory struct { schemaReader schema.Reader } -func (f *providerBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) { +func (f *providerBlockFactory) New(tBlock ihcl.TokenizedBlock) (ConfigBlock, error) { if f.logger == nil { f.logger = discardLog() } @@ -23,7 +24,7 @@ func (f *providerBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) logger: f.logger, labelSchema: f.LabelSchema(), - tokens: tokens, + tBlock: tBlock, sr: f.schemaReader, }, nil } @@ -49,7 +50,7 @@ type providerBlock struct { labelSchema LabelSchema labels []*ParsedLabel - tokens hclsyntax.Tokens + tBlock ihcl.TokenizedBlock sr schema.Reader } @@ -70,7 +71,7 @@ func (p *providerBlock) Labels() []*ParsedLabel { return p.labels } - labels := ParseLabels(p.tokens, p.labelSchema) + labels := ParseLabels(p.tBlock, p.labelSchema) p.labels = labels return p.labels @@ -85,7 +86,7 @@ func (p *providerBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid return nil, &noSchemaReaderErr{p.BlockType()} } - hclBlock, _ := hclsyntax.ParseBlockFromTokens(p.tokens) + hclBlock, _ := hclsyntax.ParseBlockFromTokens(p.tBlock.Tokens()) if PosInLabels(hclBlock, pos) { providers, err := p.sr.Providers() if err != nil { @@ -93,11 +94,11 @@ func (p *providerBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid } cl := &completableLabels{ - logger: p.logger, - block: ParseBlock(hclBlock, p.Labels(), nil), - tokens: p.tokens, + logger: p.logger, + parsedLabels: p.Labels(), + tBlock: p.tBlock, labels: labelCandidates{ - "name": providerCandidates(providers, pos), + "name": providerCandidates(providers), }, } @@ -110,19 +111,18 @@ func (p *providerBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid } cb := &completableBlock{ logger: p.logger, - block: ParseBlock(hclBlock, p.Labels(), pSchema.Block), - tokens: p.tokens, + schema: pSchema.Block, + tBlock: p.tBlock, } return cb.completionCandidatesAtPos(pos) } -func providerCandidates(names []string, pos hcl.Pos) []CompletionCandidate { - candidates := []CompletionCandidate{} +func providerCandidates(names []string) []*labelCandidate { + candidates := []*labelCandidate{} for _, name := range names { candidates = append(candidates, &labelCandidate{ label: name, detail: "provider", - pos: pos, }) } return candidates diff --git a/internal/terraform/lang/provider_block_test.go b/internal/terraform/lang/provider_block_test.go index 37bfc56a8..c2717b6de 100644 --- a/internal/terraform/lang/provider_block_test.go +++ b/internal/terraform/lang/provider_block_test.go @@ -51,10 +51,10 @@ func TestProviderBlock_Name(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.src) + tBlock := newTestBlock(t, tc.src) pf := &providerBlockFactory{logger: log.New(os.Stdout, "", 0)} - p, err := pf.New(tokens) + p, err := pf.New(tBlock) if err != nil { if tc.expectedErr != nil && err.Error() == tc.expectedErr.Error() { @@ -138,6 +138,16 @@ func TestProviderBlock_completionCandidatesAtPos(t *testing.T) { }, nil, }, + { + "missing type", + `provider "" { +}`, + simpleSchema, + nil, + hcl.Pos{Line: 2, Column: 1, Byte: 14}, + []renderedCandidate{}, + &schema.SchemaUnavailableErr{BlockType: "provider", FullName: ""}, + }, { "schema reader error", `provider "custom" { @@ -174,7 +184,7 @@ func TestProviderBlock_completionCandidatesAtPos(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.src) + tBlock := newTestBlock(t, tc.src) pf := &providerBlockFactory{ logger: log.New(os.Stdout, "", 0), @@ -183,7 +193,7 @@ func TestProviderBlock_completionCandidatesAtPos(t *testing.T) { ProviderSchemaErr: tc.readerErr, }, } - p, err := pf.New(tokens) + p, err := pf.New(tBlock) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/lang/resource_block.go b/internal/terraform/lang/resource_block.go index 4c3b148fa..2af489056 100644 --- a/internal/terraform/lang/resource_block.go +++ b/internal/terraform/lang/resource_block.go @@ -7,6 +7,7 @@ import ( hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" tfjson "github.com/hashicorp/terraform-json" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) @@ -16,7 +17,7 @@ type resourceBlockFactory struct { schemaReader schema.Reader } -func (f *resourceBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) { +func (f *resourceBlockFactory) New(tBlock ihcl.TokenizedBlock) (ConfigBlock, error) { if f.logger == nil { f.logger = discardLog() } @@ -25,7 +26,7 @@ func (f *resourceBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) logger: f.logger, labelSchema: f.LabelSchema(), - tokens: tokens, + tBlock: tBlock, sr: f.schemaReader, }, nil } @@ -52,7 +53,7 @@ type resourceBlock struct { labelSchema LabelSchema labels []*ParsedLabel - tokens hclsyntax.Tokens + tBlock ihcl.TokenizedBlock sr schema.Reader } @@ -81,7 +82,7 @@ func (r *resourceBlock) Labels() []*ParsedLabel { if r.labels != nil { return r.labels } - labels := ParseLabels(r.tokens, r.labelSchema) + labels := ParseLabels(r.tBlock, r.labelSchema) r.labels = labels return r.labels @@ -96,18 +97,18 @@ func (r *resourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid return nil, &noSchemaReaderErr{r.BlockType()} } - hclBlock, _ := hclsyntax.ParseBlockFromTokens(r.tokens) + hclBlock, _ := hclsyntax.ParseBlockFromTokens(r.tBlock.Tokens()) if PosInLabels(hclBlock, pos) { resources, err := r.sr.Resources() if err != nil { return nil, err } cl := &completableLabels{ - logger: r.logger, - block: ParseBlock(hclBlock, r.Labels(), nil), - tokens: r.tokens, + logger: r.logger, + parsedLabels: r.Labels(), + tBlock: r.tBlock, labels: labelCandidates{ - "type": resourceCandidates(resources, pos), + "type": resourceCandidates(resources), }, } @@ -119,15 +120,16 @@ func (r *resourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid return nil, err } cb := &completableBlock{ - logger: r.logger, - block: ParseBlock(hclBlock, r.Labels(), rSchema.Block), - tokens: r.tokens, + logger: r.logger, + parsedLabels: r.Labels(), + schema: rSchema.Block, + tBlock: r.tBlock, } return cb.completionCandidatesAtPos(pos) } -func resourceCandidates(resources []schema.Resource, pos hcl.Pos) []CompletionCandidate { - candidates := []CompletionCandidate{} +func resourceCandidates(resources []schema.Resource) []*labelCandidate { + candidates := []*labelCandidate{} for _, r := range resources { var desc MarkupContent = PlainText(r.Description) if r.DescriptionKind == tfjson.SchemaDescriptionKindMarkdown { @@ -138,7 +140,6 @@ func resourceCandidates(resources []schema.Resource, pos hcl.Pos) []CompletionCa label: r.Name, detail: fmt.Sprintf("Resource (%s)", r.Provider), documentation: desc, - pos: pos, }) } return candidates diff --git a/internal/terraform/lang/resource_block_test.go b/internal/terraform/lang/resource_block_test.go index 38abacee2..0b50428ad 100644 --- a/internal/terraform/lang/resource_block_test.go +++ b/internal/terraform/lang/resource_block_test.go @@ -58,10 +58,10 @@ func TestResourceBlock_Name(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.src) + tBlock := newTestBlock(t, tc.src) pf := &resourceBlockFactory{logger: log.New(os.Stdout, "", 0)} - p, err := pf.New(tokens) + p, err := pf.New(tBlock) if err != nil { if tc.expectedErr != nil && err.Error() == tc.expectedErr.Error() { @@ -145,6 +145,16 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) { }, nil, }, + { + "missing type", + `resource "" "" { +}`, + simpleSchema, + nil, + hcl.Pos{Line: 2, Column: 1, Byte: 17}, + []renderedCandidate{}, + &schema.SchemaUnavailableErr{BlockType: "resource", FullName: ""}, + }, { "schema reader error", `resource "custom_rs" "name" { @@ -181,7 +191,7 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - tokens := lexConfig(t, tc.src) + tBlock := newTestBlock(t, tc.src) pf := &resourceBlockFactory{ logger: log.New(os.Stdout, "", 0), @@ -190,7 +200,7 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) { ResourceSchemaErr: tc.readerErr, }, } - p, err := pf.New(tokens) + p, err := pf.New(tBlock) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/lang/types.go b/internal/terraform/lang/types.go index 23a112064..24947419f 100644 --- a/internal/terraform/lang/types.go +++ b/internal/terraform/lang/types.go @@ -4,8 +4,8 @@ import ( "log" hcl "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" tfjson "github.com/hashicorp/terraform-json" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" "github.com/hashicorp/terraform-ls/internal/mdplain" "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) @@ -15,8 +15,8 @@ import ( type Parser interface { SetLogger(*log.Logger) SetSchemaReader(schema.Reader) - BlockTypeCandidates(hclsyntax.Tokens, hcl.Pos) CompletionCandidates - ParseBlockFromTokens(hclsyntax.Tokens) (ConfigBlock, error) + BlockTypeCandidates(ihcl.TokenizedFile, hcl.Pos) CompletionCandidates + CompletionCandidatesAtPos(ihcl.TokenizedFile, hcl.Pos) (CompletionCandidates, error) } // ConfigBlock implements an abstraction above HCL block @@ -32,9 +32,7 @@ type ConfigBlock interface { // which keeps track of the related schema type Block interface { BlockAtPos(pos hcl.Pos) (Block, bool) - LabelAtPos(pos hcl.Pos) (*ParsedLabel, bool) Range() hcl.Range - PosInLabels(pos hcl.Pos) bool PosInBody(pos hcl.Pos) bool PosInAttribute(pos hcl.Pos) bool Attributes() map[string]*Attribute @@ -51,6 +49,7 @@ type Label struct { type ParsedLabel struct { Name string Value string + Range hcl.Range } type BlockType struct { @@ -78,8 +77,7 @@ type CompletionCandidate interface { Detail() string Documentation() MarkupContent Snippet() string - SetPrefix(string) - PrefixRange() hcl.Range + PlainText() string } // MarkupContent reflects lsp.MarkupContent diff --git a/internal/terraform/lang/utils.go b/internal/terraform/lang/utils.go index dbb7a1087..b2df239db 100644 --- a/internal/terraform/lang/utils.go +++ b/internal/terraform/lang/utils.go @@ -18,24 +18,22 @@ func PosInLabels(b *hclsyntax.Block, pos hcl.Pos) bool { return false } -func wordBeforePos(tokens hclsyntax.Tokens, pos hcl.Pos) string { - switch token := tokenAtPos(tokens, pos); token.Type { +func prefixAtPos(looker TokenAtPosLooker, pos hcl.Pos) string { + token, err := looker.TokenAtPosition(pos) + if err != nil { + return "" + } + + switch token.Type { case hclsyntax.TokenIdent, hclsyntax.TokenQuotedLit, hclsyntax.TokenStringLit: return string(token.Bytes[:pos.Byte-token.Range.Start.Byte]) - default: - return "" } + + return "" } -func tokenAtPos(tokens hclsyntax.Tokens, pos hcl.Pos) hclsyntax.Token { - for _, t := range tokens { - if rangeContainsOffset(t.Range, pos.Byte) { - return t - } - } - return hclsyntax.Token{ - Type: hclsyntax.TokenNil, - } +type TokenAtPosLooker interface { + TokenAtPosition(hcl.Pos) (hclsyntax.Token, error) } // rangeContainsOffset is a reimplementation of hcl.Range.ContainsOffset diff --git a/langserver/handlers/complete.go b/langserver/handlers/complete.go index 5700ff834..f8303db71 100644 --- a/langserver/handlers/complete.go +++ b/langserver/handlers/complete.go @@ -4,8 +4,6 @@ import ( "context" "fmt" - hcl "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" lsctx "github.com/hashicorp/terraform-ls/internal/context" ihcl "github.com/hashicorp/terraform-ls/internal/hcl" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" @@ -57,30 +55,10 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple p.SetLogger(h.logger) p.SetSchemaReader(sr) - tokens, err := hclFile.BlockTokensAtPosition(pos) - if err != nil { - if ihcl.IsNoBlockFoundErr(err) { - return ilsp.CompletionList(p.BlockTypeCandidates(tokens, pos), cc.TextDocument), nil - } - - return list, fmt.Errorf("finding HCL block failed: %#v", err) - } - - h.logger.Printf("HCL block found at HCL pos %#v", pos) - candidates, err := h.completeBlock(p, tokens, pos) + candidates, err := p.CompletionCandidatesAtPos(hclFile, pos) if err != nil { return list, fmt.Errorf("finding completion items failed: %w", err) } - return ilsp.CompletionList(candidates, cc.TextDocument), nil -} - -func (h *logHandler) completeBlock(p lang.Parser, tokens hclsyntax.Tokens, pos hcl.Pos) (lang.CompletionCandidates, error) { - cfgBlock, err := p.ParseBlockFromTokens(tokens) - if err != nil { - return nil, fmt.Errorf("finding config block failed: %w", err) - } - h.logger.Printf("Configuration block %q parsed", cfgBlock.BlockType()) - - return cfgBlock.CompletionCandidatesAtPos(pos) + return ilsp.CompletionList(candidates, pos, cc.TextDocument), nil }