From d424744d218a32ced3bee128d825d518d3ed7157 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Thu, 28 May 2020 11:37:10 +0100 Subject: [PATCH] refactoring(parser): Pass around tokens instead of blocks --- internal/hcl/hcl.go | 125 ++++++++++++++---- internal/hcl/hcl_test.go | 83 ++++++++++-- internal/terraform/lang/config_block.go | 2 +- internal/terraform/lang/config_block_test.go | 5 +- internal/terraform/lang/datasource_block.go | 15 ++- .../terraform/lang/datasource_block_test.go | 14 +- internal/terraform/lang/hcl_block_type.go | 2 +- internal/terraform/lang/hcl_parser.go | 34 ++++- internal/terraform/lang/hcl_parser_test.go | 18 +-- internal/terraform/lang/labels.go | 18 --- internal/terraform/lang/parser.go | 19 +-- internal/terraform/lang/parser_test.go | 15 ++- internal/terraform/lang/provider_block.go | 15 ++- .../terraform/lang/provider_block_test.go | 14 +- internal/terraform/lang/resource_block.go | 15 ++- .../terraform/lang/resource_block_test.go | 14 +- internal/terraform/lang/types.go | 3 +- langserver/handlers/complete.go | 9 +- 18 files changed, 288 insertions(+), 132 deletions(-) delete mode 100644 internal/terraform/lang/labels.go diff --git a/internal/hcl/hcl.go b/internal/hcl/hcl.go index d9e4c4751..83b5f575f 100644 --- a/internal/hcl/hcl.go +++ b/internal/hcl/hcl.go @@ -1,19 +1,27 @@ package hcl import ( + "fmt" + + "github.com/hashicorp/hcl/v2" hcllib "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform-ls/internal/filesystem" ) type File interface { - BlockAtPosition(filesystem.FilePosition) (*hcllib.Block, hcllib.Pos, error) + BlockTokensAtPosition(filesystem.FilePosition) (hclsyntax.Tokens, hcllib.Pos, error) } type file struct { filename string content []byte - f *hcllib.File + pf *parsedFile +} + +type parsedFile struct { + Body hcllib.Body + Tokens hclsyntax.Tokens } func NewFile(f filesystem.File) File { @@ -23,51 +31,116 @@ func NewFile(f filesystem.File) File { } } -func (f *file) ast() (*hcllib.File, error) { - if f.f != nil { - return f.f, nil +func (f *file) parse() (*parsedFile, error) { + if f.pf != nil { + return f.pf, nil + } + + var parseDiags hcllib.Diagnostics + + tokens, diags := hclsyntax.LexConfig(f.content, f.filename, hcllib.InitialPos) + if diags.HasErrors() { + parseDiags = append(parseDiags, diags...) } - hf, err := hclsyntax.ParseConfig(f.content, f.filename, hcllib.InitialPos) - f.f = hf + body, diags := hclsyntax.ParseBodyFromTokens(tokens, hclsyntax.TokenEOF) + if diags.HasErrors() { + parseDiags = append(parseDiags, diags...) + } - return f.f, err + f.pf = &parsedFile{ + Tokens: tokens, + Body: body, + } + + if parseDiags.HasErrors() { + return f.pf, parseDiags + } + return f.pf, nil } -func (f *file) BlockAtPosition(filePos filesystem.FilePosition) (*hcllib.Block, hcllib.Pos, error) { +func (f *file) BlockTokensAtPosition(filePos filesystem.FilePosition) (hclsyntax.Tokens, hcllib.Pos, error) { pos := filePos.Position() b, err := f.blockAtPosition(pos) if err != nil { - return nil, pos, err + return hclsyntax.Tokens{}, pos, err } return b, pos, nil } -func (f *file) blockAtPosition(pos hcllib.Pos) (*hcllib.Block, error) { - ast, _ := f.ast() +func (f *file) blockAtPosition(pos hcllib.Pos) (hclsyntax.Tokens, error) { + pf, _ := f.parse() - if body, ok := ast.Body.(*hclsyntax.Body); ok { - if body.SrcRange.Empty() && pos != hcllib.InitialPos { - return nil, &InvalidHclPosErr{pos, body.SrcRange} + body, ok := pf.Body.(*hclsyntax.Body) + if !ok { + return hclsyntax.Tokens{}, fmt.Errorf("unexpected body type (%T)", body) + } + if body.SrcRange.Empty() && pos != hcllib.InitialPos { + return hclsyntax.Tokens{}, &InvalidHclPosErr{pos, body.SrcRange} + } + if !body.SrcRange.Empty() { + if posIsEqual(body.SrcRange.End, pos) { + return hclsyntax.Tokens{}, &NoBlockFoundErr{pos} } - if !body.SrcRange.Empty() { - if posIsEqual(body.SrcRange.End, pos) { - return nil, &NoBlockFoundErr{pos} - } - if !body.SrcRange.ContainsPos(pos) { - return nil, &InvalidHclPosErr{pos, body.SrcRange} - } + if !body.SrcRange.ContainsPos(pos) { + return hclsyntax.Tokens{}, &InvalidHclPosErr{pos, body.SrcRange} } } - block := ast.OutermostBlockAtPos(pos) - if block == nil { - return nil, &NoBlockFoundErr{pos} + for _, block := range body.Blocks { + wholeRange := hcllib.RangeBetween(block.TypeRange, block.CloseBraceRange) + if wholeRange.ContainsPos(pos) { + return tokensInRange(f.pf.Tokens, block.Range()), nil + } + } + + return nil, &NoBlockFoundErr{pos} +} + +func tokensInRange(tokens hclsyntax.Tokens, rng hcllib.Range) hclsyntax.Tokens { + var ts hclsyntax.Tokens + + for _, t := range tokens { + // We could check more strictly if the whole token is in given range + // but we just assume block always contains whole tokens + if rng.Overlaps(t.Range) { + ts = append(ts, t) + } + } + + if len(ts) > 0 { + lastToken := ts[len(ts)-1] + if lastToken.Type != hclsyntax.TokenEOF && + lastToken.Type != hclsyntax.TokenNewline { + tRng := lastToken.Range + + // Parser expects some form of closing token, so we attach a newline + trailingNewLine := hclsyntax.Token{ + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + Range: hcl.Range{ + Filename: tRng.Filename, + Start: tRng.End, + End: hcl.Pos{ + Byte: tRng.End.Byte + 1, + Column: 1, + Line: tRng.End.Line + 1, + }, + }, + } + + ts = append(ts, trailingNewLine) + } + + if ts[0].Type == hclsyntax.TokenNewline { + // Remove initial newline + return ts[1:] + } } - return block, nil + return ts } func posIsEqual(a, b hcllib.Pos) bool { diff --git a/internal/hcl/hcl_test.go b/internal/hcl/hcl_test.go index f0275ead0..a2a27c854 100644 --- a/internal/hcl/hcl_test.go +++ b/internal/hcl/hcl_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform-ls/internal/filesystem" ) @@ -17,8 +18,8 @@ func TestFile_BlockAtPosition(t *testing.T) { content string pos hcl.Pos - expectedErr error - expectedBlock *hcl.Block + expectedErr error + expectedTokens []hclsyntax.Token }{ { "invalid config", @@ -29,9 +30,31 @@ func TestFile_BlockAtPosition(t *testing.T) { Byte: 0, }, nil, // Expect errors to be ignored - &hcl.Block{ - Type: "provider", - Labels: []string{"aws"}, + []hclsyntax.Token{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte("provider"), + }, + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte(`"`), + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte("aws"), + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte(`"`), + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte("{"), + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + }, }, }, { @@ -46,9 +69,43 @@ func TestFile_BlockAtPosition(t *testing.T) { Byte: 17, }, nil, - &hcl.Block{ - Type: "provider", - Labels: []string{"aws"}, + []hclsyntax.Token{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte("provider"), + }, + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte(`"`), + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte("aws"), + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte(`"`), + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte("{"), + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte("}"), + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + }, }, }, { @@ -128,8 +185,8 @@ func TestFile_BlockAtPosition(t *testing.T) { } opts := cmp.Options{ - cmpopts.IgnoreFields(hcl.Block{}, - "Body", "DefRange", "TypeRange", "LabelRanges"), + cmpopts.IgnoreFields(hclsyntax.Token{}, + "Range"), cmpopts.IgnoreFields(hcl.Diagnostic{}, "Subject"), } @@ -142,7 +199,7 @@ func TestFile_BlockAtPosition(t *testing.T) { pos: tc.pos, } - block, _, err := f.BlockAtPosition(fp) + tokens, _, err := f.BlockTokensAtPosition(fp) if err != nil { if tc.expectedErr == nil { t.Fatal(err) @@ -156,8 +213,8 @@ func TestFile_BlockAtPosition(t *testing.T) { t.Fatalf("Expected error: %s", tc.expectedErr) } - if diff := cmp.Diff(tc.expectedBlock, block, opts...); diff != "" { - t.Fatalf("Unexpected block difference: %s", diff) + if diff := cmp.Diff(hclsyntax.Tokens(tc.expectedTokens), tokens, opts...); diff != "" { + t.Fatalf("Unexpected token difference: %s", diff) } }) diff --git a/internal/terraform/lang/config_block.go b/internal/terraform/lang/config_block.go index 4ae49781a..2d4e4478a 100644 --- a/internal/terraform/lang/config_block.go +++ b/internal/terraform/lang/config_block.go @@ -11,7 +11,7 @@ import ( ) type configBlockFactory interface { - New(*hclsyntax.Block) (ConfigBlock, error) + New(hclsyntax.Tokens) (ConfigBlock, error) LabelSchema() LabelSchema Documentation() MarkupContent } diff --git a/internal/terraform/lang/config_block_test.go b/internal/terraform/lang/config_block_test.go index b8efe6d65..7a1656bf1 100644 --- a/internal/terraform/lang/config_block_test.go +++ b/internal/terraform/lang/config_block_test.go @@ -256,14 +256,15 @@ func TestCompletableBlock_CompletionCandidatesAtPos(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.src)) + tokens := lexConfig(t, tc.src) + block, err := ParseBlock(tokens, []*ParsedLabel{}, tc.sb) if err != nil { t.Fatal(err) } cb := &completableBlock{ logger: testLogger(), - block: ParseBlock(block, []*ParsedLabel{}, tc.sb), + block: block, } list, err := cb.completionCandidatesAtPos(tc.pos) diff --git a/internal/terraform/lang/datasource_block.go b/internal/terraform/lang/datasource_block.go index 899fc203f..435fa5e49 100644 --- a/internal/terraform/lang/datasource_block.go +++ b/internal/terraform/lang/datasource_block.go @@ -16,7 +16,7 @@ type datasourceBlockFactory struct { schemaReader schema.Reader } -func (f *datasourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) { +func (f *datasourceBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) { if f.logger == nil { f.logger = discardLog() } @@ -25,7 +25,7 @@ func (f *datasourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error logger: f.logger, labelSchema: f.LabelSchema(), - hclBlock: block, + tokens: tokens, sr: f.schemaReader, }, nil } @@ -52,7 +52,7 @@ type datasourceBlock struct { labelSchema LabelSchema labels []*ParsedLabel - hclBlock *hclsyntax.Block + tokens hclsyntax.Tokens sr schema.Reader } @@ -81,7 +81,9 @@ func (r *datasourceBlock) Labels() []*ParsedLabel { if r.labels != nil { return r.labels } - r.labels = parseLabels(r.BlockType(), r.labelSchema, r.hclBlock.Labels) + labels, _ := ParseLabels(r.tokens, r.labelSchema) + r.labels = labels + return r.labels } @@ -102,7 +104,10 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand } schemaBlock = rSchema.Block } - block := ParseBlock(r.hclBlock, r.Labels(), schemaBlock) + block, err := ParseBlock(r.tokens, r.Labels(), schemaBlock) + if err != nil { + return nil, err + } if block.PosInLabels(pos) { dataSources, err := r.sr.DataSources() diff --git a/internal/terraform/lang/datasource_block_test.go b/internal/terraform/lang/datasource_block_test.go index 0e123edd0..9b203432e 100644 --- a/internal/terraform/lang/datasource_block_test.go +++ b/internal/terraform/lang/datasource_block_test.go @@ -58,13 +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) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.src)) - if err != nil { - t.Fatal(err) - } + tokens := lexConfig(t, tc.src) pf := &datasourceBlockFactory{logger: log.New(os.Stdout, "", 0)} - p, err := pf.New(block) + p, err := pf.New(tokens) if err != nil { if tc.expectedErr != nil && err.Error() == tc.expectedErr.Error() { @@ -193,10 +190,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) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.src)) - if err != nil { - t.Fatal(err) - } + tokens := lexConfig(t, tc.src) pf := &datasourceBlockFactory{ logger: log.New(os.Stdout, "", 0), @@ -205,7 +199,7 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) { DataSourceSchemaErr: tc.readerErr, }, } - p, err := pf.New(block) + p, err := pf.New(tokens) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/lang/hcl_block_type.go b/internal/terraform/lang/hcl_block_type.go index 6a48ef505..e788f5390 100644 --- a/internal/terraform/lang/hcl_block_type.go +++ b/internal/terraform/lang/hcl_block_type.go @@ -63,7 +63,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, labels, typeSchema.Block)) } } diff --git a/internal/terraform/lang/hcl_parser.go b/internal/terraform/lang/hcl_parser.go index 502f2a710..5d07d1552 100644 --- a/internal/terraform/lang/hcl_parser.go +++ b/internal/terraform/lang/hcl_parser.go @@ -2,6 +2,7 @@ package lang import ( "fmt" + "log" hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -10,7 +11,14 @@ import ( // ParseBlock parses HCL configuration based on tfjson's SchemaBlock // and keeps hold of all tfjson schema details on block or attribute level -func ParseBlock(block *hclsyntax.Block, labels []*ParsedLabel, schema *tfjson.SchemaBlock) Block { +func ParseBlock(tokens hclsyntax.Tokens, labels []*ParsedLabel, schema *tfjson.SchemaBlock) (Block, error) { + log.Printf("Parsing block...") + hclBlock, _ := hclsyntax.ParseBlockFromTokens(tokens) + + return parseBlock(hclBlock, labels, schema), nil +} + +func parseBlock(block *hclsyntax.Block, labels []*ParsedLabel, schema *tfjson.SchemaBlock) Block { b := &parsedBlock{ hclBlock: block, labels: labels, @@ -33,6 +41,30 @@ func ParseBlock(block *hclsyntax.Block, labels []*ParsedLabel, schema *tfjson.Sc return b } +func ParseLabels(tokens hclsyntax.Tokens, schema LabelSchema) ([]*ParsedLabel, error) { + log.Printf("Parsing labels...") + hclBlock, _ := hclsyntax.ParseBlockFromTokens(tokens) + + return parseLabels(hclBlock.Type, schema, hclBlock.Labels), nil +} + +func parseLabels(blockType string, schema LabelSchema, parsed []string) []*ParsedLabel { + labels := make([]*ParsedLabel, len(schema)) + + for i, l := range schema { + var value string + if len(parsed)-1 >= i { + value = parsed[i] + } + labels[i] = &ParsedLabel{ + Name: l.Name, + Value: value, + } + } + + return labels +} + 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 5e9b9c4f1..57e25d783 100644 --- a/internal/terraform/lang/hcl_parser_test.go +++ b/internal/terraform/lang/hcl_parser_test.go @@ -302,13 +302,13 @@ 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)) + tokens := lexConfig(t, tc.cfg) + + b, err := ParseBlock(tokens, []*ParsedLabel{}, tc.schema) if err != nil { t.Fatal(err) } - b := ParseBlock(block, []*ParsedLabel{}, tc.schema) - if diff := cmp.Diff(tc.expectedAttributes, b.Attributes(), opts...); diff != "" { t.Fatalf("Attributes don't match.\n%s", diff) } @@ -466,12 +466,12 @@ 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)) + tokens := lexConfig(t, tc.cfg) + b, err := ParseBlock(tokens, []*ParsedLabel{}, schema) if err != nil { t.Fatal(err) } - b := ParseBlock(block, []*ParsedLabel{}, schema) fBlock, _ := b.BlockAtPos(tc.pos) if diff := cmp.Diff(tc.expectedBlock, fBlock, opts...); diff != "" { t.Fatalf("Block doesn't match.\n%s", diff) @@ -626,12 +626,12 @@ 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)) + tokens := lexConfig(t, tc.cfg) + b, err := ParseBlock(tokens, []*ParsedLabel{}, schema) if err != nil { t.Fatal(err) } - b := ParseBlock(block, []*ParsedLabel{}, schema) isInBody := b.PosInBody(tc.pos) if tc.expected != isInBody { if tc.expected { @@ -761,12 +761,12 @@ 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)) + tokens := lexConfig(t, tc.cfg) + b, err := ParseBlock(tokens, []*ParsedLabel{}, schema) if err != nil { t.Fatal(err) } - b := ParseBlock(block, []*ParsedLabel{}, schema) isInAttribute := b.PosInAttribute(tc.pos) if tc.expected != isInAttribute { if tc.expected { diff --git a/internal/terraform/lang/labels.go b/internal/terraform/lang/labels.go deleted file mode 100644 index 31e5ccc2b..000000000 --- a/internal/terraform/lang/labels.go +++ /dev/null @@ -1,18 +0,0 @@ -package lang - -func parseLabels(blockType string, schema LabelSchema, parsed []string) []*ParsedLabel { - labels := make([]*ParsedLabel, len(schema)) - - for i, l := range schema { - var value string - if len(parsed)-1 >= i { - value = parsed[i] - } - labels[i] = &ParsedLabel{ - Name: l.Name, - Value: value, - } - } - - return labels -} diff --git a/internal/terraform/lang/parser.go b/internal/terraform/lang/parser.go index 4bdc329d4..03d4df673 100644 --- a/internal/terraform/lang/parser.go +++ b/internal/terraform/lang/parser.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/go-version" hcl "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform-ls/internal/terraform/errors" "github.com/hashicorp/terraform-ls/internal/terraform/schema" ) @@ -131,26 +132,28 @@ func (bt *completableBlockType) Documentation() MarkupContent { return bt.documentation } -func (p *parser) ParseBlockFromHCL(block *hcl.Block) (ConfigBlock, error) { - if block == nil { +func (p *parser) ParseBlockFromTokens(tokens hclsyntax.Tokens) (ConfigBlock, error) { + if len(tokens) == 0 { return nil, EmptyConfigErr } + // 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 + block, _ := hclsyntax.ParseBlockFromTokens(tokens) + + p.logger.Printf("Parsed block type: %q", block.Type) + f, ok := p.blockTypes()[block.Type] if !ok { return nil, &unknownBlockTypeErr{block.Type} } - hsBlock, err := AsHCLSyntaxBlock(block) + cfgBlock, err := f.New(tokens) if err != nil { return nil, fmt.Errorf("%s: %w", block.Type, err) } - cfgBlock, err := f.New(hsBlock) - if err != nil { - return nil, fmt.Errorf("%s: %w", hsBlock.Type, err) - } - return cfgBlock, nil } diff --git a/internal/terraform/lang/parser_test.go b/internal/terraform/lang/parser_test.go index 2b6144ce9..bb059389c 100644 --- a/internal/terraform/lang/parser_test.go +++ b/internal/terraform/lang/parser_test.go @@ -47,7 +47,7 @@ func TestParser_BlockTypeCandidates_snippet(t *testing.T) { } } -func TestParser_ParseBlockFromHCL(t *testing.T) { +func TestParser_ParseBlockFromTokens(t *testing.T) { testCases := []struct { name string cfg string @@ -80,10 +80,10 @@ func TestParser_ParseBlockFromHCL(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - hclBlock := parseHclBlock(t, tc.cfg) + tokens := lexConfig(t, tc.cfg) p := newParser() - cfgBlock, err := p.ParseBlockFromHCL(hclBlock) + cfgBlock, err := p.ParseBlockFromTokens(tokens) if err != nil { if errors.Is(err, tc.expectedErr) { return @@ -104,6 +104,15 @@ func TestParser_ParseBlockFromHCL(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) + } + + return tokens +} + func parseHclBlock(t *testing.T, src string) *hcl.Block { f, diags := hclsyntax.ParseConfig([]byte(src), "/test.tf", hcl.InitialPos) if diags.HasErrors() { diff --git a/internal/terraform/lang/provider_block.go b/internal/terraform/lang/provider_block.go index ef55abcec..a9af6580e 100644 --- a/internal/terraform/lang/provider_block.go +++ b/internal/terraform/lang/provider_block.go @@ -15,7 +15,7 @@ type providerBlockFactory struct { schemaReader schema.Reader } -func (f *providerBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) { +func (f *providerBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) { if f.logger == nil { f.logger = discardLog() } @@ -24,7 +24,7 @@ func (f *providerBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) logger: f.logger, labelSchema: f.LabelSchema(), - hclBlock: block, + tokens: tokens, sr: f.schemaReader, }, nil } @@ -50,7 +50,7 @@ type providerBlock struct { labelSchema LabelSchema labels []*ParsedLabel - hclBlock *hclsyntax.Block + tokens hclsyntax.Tokens sr schema.Reader } @@ -70,7 +70,9 @@ func (p *providerBlock) Labels() []*ParsedLabel { if p.labels != nil { return p.labels } - p.labels = parseLabels(p.BlockType(), p.labelSchema, p.hclBlock.Labels) + labels, _ := ParseLabels(p.tokens, p.labelSchema) + p.labels = labels + return p.labels } @@ -91,7 +93,10 @@ func (p *providerBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid } schemaBlock = pSchema.Block } - block := ParseBlock(p.hclBlock, p.Labels(), schemaBlock) + block, err := ParseBlock(p.tokens, p.Labels(), schemaBlock) + if err != nil { + return nil, err + } if block.PosInLabels(pos) { providers, err := p.sr.Providers() diff --git a/internal/terraform/lang/provider_block_test.go b/internal/terraform/lang/provider_block_test.go index 9f892a1d0..05ca1d867 100644 --- a/internal/terraform/lang/provider_block_test.go +++ b/internal/terraform/lang/provider_block_test.go @@ -51,13 +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) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.src)) - if err != nil { - t.Fatal(err) - } + tokens := lexConfig(t, tc.src) pf := &providerBlockFactory{logger: log.New(os.Stdout, "", 0)} - p, err := pf.New(block) + p, err := pf.New(tokens) if err != nil { if tc.expectedErr != nil && err.Error() == tc.expectedErr.Error() { @@ -188,10 +185,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) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.src)) - if err != nil { - t.Fatal(err) - } + tokens := lexConfig(t, tc.src) pf := &providerBlockFactory{ logger: log.New(os.Stdout, "", 0), @@ -200,7 +194,7 @@ func TestProviderBlock_completionCandidatesAtPos(t *testing.T) { ProviderSchemaErr: tc.readerErr, }, } - p, err := pf.New(block) + p, err := pf.New(tokens) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/lang/resource_block.go b/internal/terraform/lang/resource_block.go index f7202c43e..efa168dee 100644 --- a/internal/terraform/lang/resource_block.go +++ b/internal/terraform/lang/resource_block.go @@ -16,7 +16,7 @@ type resourceBlockFactory struct { schemaReader schema.Reader } -func (f *resourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) { +func (f *resourceBlockFactory) New(tokens hclsyntax.Tokens) (ConfigBlock, error) { if f.logger == nil { f.logger = discardLog() } @@ -25,7 +25,7 @@ func (f *resourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) logger: f.logger, labelSchema: f.LabelSchema(), - hclBlock: block, + tokens: tokens, sr: f.schemaReader, }, nil } @@ -52,7 +52,7 @@ type resourceBlock struct { labelSchema LabelSchema labels []*ParsedLabel - hclBlock *hclsyntax.Block + tokens hclsyntax.Tokens sr schema.Reader } @@ -81,7 +81,9 @@ func (r *resourceBlock) Labels() []*ParsedLabel { if r.labels != nil { return r.labels } - r.labels = parseLabels(r.BlockType(), r.labelSchema, r.hclBlock.Labels) + labels, _ := ParseLabels(r.tokens, r.labelSchema) + r.labels = labels + return r.labels } @@ -102,7 +104,10 @@ func (r *resourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid } schemaBlock = rSchema.Block } - block := ParseBlock(r.hclBlock, r.Labels(), schemaBlock) + block, err := ParseBlock(r.tokens, r.Labels(), schemaBlock) + if err != nil { + return nil, err + } if block.PosInLabels(pos) { resources, err := r.sr.Resources() diff --git a/internal/terraform/lang/resource_block_test.go b/internal/terraform/lang/resource_block_test.go index 3d9caf184..53b187f86 100644 --- a/internal/terraform/lang/resource_block_test.go +++ b/internal/terraform/lang/resource_block_test.go @@ -58,13 +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) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.src)) - if err != nil { - t.Fatal(err) - } + tokens := lexConfig(t, tc.src) pf := &resourceBlockFactory{logger: log.New(os.Stdout, "", 0)} - p, err := pf.New(block) + p, err := pf.New(tokens) if err != nil { if tc.expectedErr != nil && err.Error() == tc.expectedErr.Error() { @@ -195,10 +192,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) { - block, err := AsHCLSyntaxBlock(parseHclBlock(t, tc.src)) - if err != nil { - t.Fatal(err) - } + tokens := lexConfig(t, tc.src) pf := &resourceBlockFactory{ logger: log.New(os.Stdout, "", 0), @@ -207,7 +201,7 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) { ResourceSchemaErr: tc.readerErr, }, } - p, err := pf.New(block) + p, err := pf.New(tokens) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/lang/types.go b/internal/terraform/lang/types.go index 37f178377..cc15418a9 100644 --- a/internal/terraform/lang/types.go +++ b/internal/terraform/lang/types.go @@ -4,6 +4,7 @@ import ( "log" hcl "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/mdplain" "github.com/hashicorp/terraform-ls/internal/terraform/schema" @@ -15,7 +16,7 @@ type Parser interface { SetLogger(*log.Logger) SetSchemaReader(schema.Reader) BlockTypeCandidates() CompletionCandidates - ParseBlockFromHCL(*hcl.Block) (ConfigBlock, error) + ParseBlockFromTokens(hclsyntax.Tokens) (ConfigBlock, error) } // ConfigBlock implements an abstraction above HCL block diff --git a/langserver/handlers/complete.go b/langserver/handlers/complete.go index d90815440..2e9df08eb 100644 --- a/langserver/handlers/complete.go +++ b/langserver/handlers/complete.go @@ -5,6 +5,7 @@ import ( "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" @@ -54,7 +55,7 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple p.SetLogger(h.logger) p.SetSchemaReader(sr) - hclBlock, hclPos, err := hclFile.BlockAtPosition(fPos) + tokens, hclPos, err := hclFile.BlockTokensAtPosition(fPos) if err != nil { if ihcl.IsNoBlockFoundErr(err) { return ilsp.CompletionList(p.BlockTypeCandidates(), fPos.Position(), cc.TextDocument), nil @@ -64,7 +65,7 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple } h.logger.Printf("HCL block found at HCL pos %#v", hclPos) - candidates, err := h.completeBlock(p, hclBlock, hclPos) + candidates, err := h.completeBlock(p, tokens, hclPos) if err != nil { return list, fmt.Errorf("finding completion items failed: %w", err) } @@ -72,8 +73,8 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple return ilsp.CompletionList(candidates, fPos.Position(), cc.TextDocument), nil } -func (h *logHandler) completeBlock(p lang.Parser, block *hcl.Block, pos hcl.Pos) (lang.CompletionCandidates, error) { - cfgBlock, err := p.ParseBlockFromHCL(block) +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) }