From 6fff231f08a2884612ea65772e0e74e6c9acf5a1 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 20 Apr 2020 12:30:33 +0100 Subject: [PATCH] terraform/lang: Formalize label parsing This also makes the parser less strict as we need to be able to work with incomplete (invalid) declarations. --- internal/terraform/lang/config_block.go | 21 +++--- internal/terraform/lang/config_block_test.go | 8 +- internal/terraform/lang/datasource_block.go | 73 +++++++++++++------ .../terraform/lang/datasource_block_test.go | 12 +-- internal/terraform/lang/errors.go | 13 ---- internal/terraform/lang/hcl_block.go | 1 + internal/terraform/lang/hcl_block_type.go | 4 +- internal/terraform/lang/hcl_parser.go | 3 +- internal/terraform/lang/hcl_parser_test.go | 8 +- internal/terraform/lang/labels.go | 18 +++++ internal/terraform/lang/parser_test.go | 7 +- internal/terraform/lang/provider_block.go | 65 +++++++++++------ .../terraform/lang/provider_block_test.go | 8 +- internal/terraform/lang/resource_block.go | 73 +++++++++++++------ .../terraform/lang/resource_block_test.go | 12 +-- internal/terraform/lang/types.go | 8 ++ 16 files changed, 206 insertions(+), 128 deletions(-) create mode 100644 internal/terraform/lang/labels.go diff --git a/internal/terraform/lang/config_block.go b/internal/terraform/lang/config_block.go index f2febfd6..215b9c8b 100644 --- a/internal/terraform/lang/config_block.go +++ b/internal/terraform/lang/config_block.go @@ -6,7 +6,6 @@ import ( hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - tfjson "github.com/hashicorp/terraform-json" lsp "github.com/sourcegraph/go-lsp" ) @@ -14,32 +13,30 @@ type configBlockFactory interface { New(*hclsyntax.Block) (ConfigBlock, error) } +// completableBlock provides common completion functionality +// for any Block implementation type completableBlock struct { - logger *log.Logger - caps lsp.TextDocumentClientCapabilities - hclBlock *hclsyntax.Block - schema *tfjson.SchemaBlock + logger *log.Logger + caps lsp.TextDocumentClientCapabilities + + block Block } func (cb *completableBlock) completionItemsAtPos(pos hcl.Pos) (lsp.CompletionList, error) { list := lsp.CompletionList{} - cb.logger.Printf("block: %#v", cb.hclBlock) - - block := ParseBlock(cb.hclBlock, cb.schema) - - if !block.PosInBody(pos) { + if !cb.block.PosInBody(pos) { // Avoid autocompleting outside of body, for now cb.logger.Println("avoiding completion outside of block body") return list, nil } - if block.PosInAttribute(pos) { + if cb.block.PosInAttribute(pos) { cb.logger.Println("avoiding completion in the middle of existing attribute") return list, nil } - b, ok := block.BlockAtPos(pos) + b, ok := cb.block.BlockAtPos(pos) if !ok { // This should never happen as the completion // should only be called on a block the "pos" points to diff --git a/internal/terraform/lang/config_block_test.go b/internal/terraform/lang/config_block_test.go index 25cb54b8..8f0ad016 100644 --- a/internal/terraform/lang/config_block_test.go +++ b/internal/terraform/lang/config_block_test.go @@ -270,18 +270,14 @@ func TestCompletableBlock_CompletionItemsAtPos(t *testing.T) { } cb := &completableBlock{ - logger: testLogger(), - hclBlock: block, + logger: testLogger(), + block: ParseBlock(block, []*Label{}, tc.sb), } if tc.caps != nil { cb.caps = *caps } - if tc.sb != nil { - cb.schema = tc.sb - } - list, err := cb.completionItemsAtPos(tc.pos) if err != nil { if tc.expectedErr != nil && err.Error() == tc.expectedErr.Error() { diff --git a/internal/terraform/lang/datasource_block.go b/internal/terraform/lang/datasource_block.go index dd85bace..5730ad12 100644 --- a/internal/terraform/lang/datasource_block.go +++ b/internal/terraform/lang/datasource_block.go @@ -1,11 +1,12 @@ package lang import ( + "fmt" "log" - "strings" hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/terraform/schema" lsp "github.com/sourcegraph/go-lsp" ) @@ -22,16 +23,13 @@ func (f *datasourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error f.logger = discardLog() } - labels := block.Labels - if len(labels) != 2 { - return nil, &invalidLabelsErr{f.BlockType(), labels} - } - return &datasourceBlock{ - hclBlock: block, - logger: f.logger, - caps: f.caps, - sr: f.schemaReader, + logger: f.logger, + caps: f.caps, + + labelSchema: LabelSchema{"type", "name"}, + hclBlock: block, + sr: f.schemaReader, }, nil } @@ -40,18 +38,42 @@ func (r *datasourceBlockFactory) BlockType() string { } type datasourceBlock struct { - logger *log.Logger - caps lsp.TextDocumentClientCapabilities - hclBlock *hclsyntax.Block - sr schema.Reader + logger *log.Logger + caps lsp.TextDocumentClientCapabilities + + labelSchema LabelSchema + labels []*Label + hclBlock *hclsyntax.Block + sr schema.Reader } func (r *datasourceBlock) Type() string { - return r.hclBlock.Labels[0] + return r.Labels()[0].Value } func (r *datasourceBlock) Name() string { - return strings.Join(r.hclBlock.Labels, ".") + firstLabel := r.Labels()[0].Value + secondLabel := r.Labels()[1].Value + + if firstLabel == "" && secondLabel == "" { + return "" + } + if firstLabel == "" { + firstLabel = "" + } + if secondLabel == "" { + secondLabel = "" + } + + return fmt.Sprintf("%s.%s", firstLabel, secondLabel) +} + +func (r *datasourceBlock) Labels() []*Label { + if r.labels != nil { + return r.labels + } + r.labels = parseLabels(r.BlockType(), r.labelSchema, r.hclBlock.Labels) + return r.labels } func (r *datasourceBlock) BlockType() string { @@ -65,17 +87,20 @@ func (r *datasourceBlock) CompletionItemsAtPos(pos hcl.Pos) (lsp.CompletionList, return list, &noSchemaReaderErr{r.BlockType()} } - rSchema, err := r.sr.DataSourceSchema(r.Type()) - if err != nil { - return list, err + cb := &completableBlock{ + logger: r.logger, + caps: r.caps, } - cb := &completableBlock{ - logger: r.logger, - caps: r.caps, - hclBlock: r.hclBlock, - schema: rSchema.Block, + var schemaBlock *tfjson.SchemaBlock + if r.Type() != "" { + rSchema, err := r.sr.DataSourceSchema(r.Type()) + if err != nil { + return list, err + } + schemaBlock = rSchema.Block } + cb.block = ParseBlock(r.hclBlock, r.Labels(), schemaBlock) return cb.completionItemsAtPos(pos) } diff --git a/internal/terraform/lang/datasource_block_test.go b/internal/terraform/lang/datasource_block_test.go index 5003ccda..f128ac45 100644 --- a/internal/terraform/lang/datasource_block_test.go +++ b/internal/terraform/lang/datasource_block_test.go @@ -20,24 +20,24 @@ func TestDatasourceBlock_Name(t *testing.T) { `data { } `, - "", - &invalidLabelsErr{"data", []string{}}, + "", + nil, }, { "invalid config - single label", `data "aws_instance" { } `, - "", - &invalidLabelsErr{"data", []string{"aws_instance"}}, + "aws_instance.", + nil, }, { "invalid config - three labels", `data "aws_instance" "name" "extra" { } `, - "", - &invalidLabelsErr{"data", []string{"aws_instance", "name", "extra"}}, + "aws_instance.name", + nil, }, { "valid config", diff --git a/internal/terraform/lang/errors.go b/internal/terraform/lang/errors.go index ff49c62e..5ea7f222 100644 --- a/internal/terraform/lang/errors.go +++ b/internal/terraform/lang/errors.go @@ -16,19 +16,6 @@ func (e *emptyCfgErr) Error() string { var EmptyConfigErr = &emptyCfgErr{} -type invalidLabelsErr struct { - BlockType string - Labels []string -} - -func (e *invalidLabelsErr) Is(target error) bool { - return reflect.DeepEqual(e, target) -} - -func (e *invalidLabelsErr) Error() string { - return fmt.Sprintf("invalid labels for %s block: %q", e.BlockType, e.Labels) -} - type unknownBlockTypeErr struct { BlockType string } diff --git a/internal/terraform/lang/hcl_block.go b/internal/terraform/lang/hcl_block.go index abebee6d..cd7f8c9a 100644 --- a/internal/terraform/lang/hcl_block.go +++ b/internal/terraform/lang/hcl_block.go @@ -7,6 +7,7 @@ import ( type parsedBlock struct { hclBlock *hclsyntax.Block + labels []*Label AttributesMap map[string]*Attribute BlockTypesMap map[string]*BlockType diff --git a/internal/terraform/lang/hcl_block_type.go b/internal/terraform/lang/hcl_block_type.go index ed4eb089..8edc80df 100644 --- a/internal/terraform/lang/hcl_block_type.go +++ b/internal/terraform/lang/hcl_block_type.go @@ -61,7 +61,9 @@ func (bt BlockTypes) AddBlock(name string, block *hclsyntax.Block, typeSchema *t } if block != nil { - bt[name].BlockList = append(bt[name].BlockList, ParseBlock(block, typeSchema.Block)) + // SDK doesn't support named blocks yet, so we expect no labels here for now + labels := make([]*Label, 0) + 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 14733490..bdb94072 100644 --- a/internal/terraform/lang/hcl_parser.go +++ b/internal/terraform/lang/hcl_parser.go @@ -10,9 +10,10 @@ 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, schema *tfjson.SchemaBlock) Block { +func ParseBlock(block *hclsyntax.Block, labels []*Label, schema *tfjson.SchemaBlock) Block { b := &parsedBlock{ hclBlock: block, + labels: labels, } if block == nil { return b diff --git a/internal/terraform/lang/hcl_parser_test.go b/internal/terraform/lang/hcl_parser_test.go index 24831b83..de95cb8a 100644 --- a/internal/terraform/lang/hcl_parser_test.go +++ b/internal/terraform/lang/hcl_parser_test.go @@ -307,7 +307,7 @@ func TestParseBlock_attributesAndBlockTypes(t *testing.T) { t.Fatal(err) } - b := ParseBlock(block, tc.schema) + b := ParseBlock(block, []*Label{}, tc.schema) if diff := cmp.Diff(tc.expectedAttributes, b.Attributes(), opts...); diff != "" { t.Fatalf("Attributes don't match.\n%s", diff) @@ -471,7 +471,7 @@ func TestBlock_BlockAtPos(t *testing.T) { t.Fatal(err) } - b := ParseBlock(block, schema) + b := ParseBlock(block, []*Label{}, schema) fBlock, _ := b.BlockAtPos(tc.pos) if diff := cmp.Diff(tc.expectedBlock, fBlock, opts...); diff != "" { t.Fatalf("Block doesn't match.\n%s", diff) @@ -631,7 +631,7 @@ func TestBlock_PosInBody(t *testing.T) { t.Fatal(err) } - b := ParseBlock(block, schema) + b := ParseBlock(block, []*Label{}, schema) isInBody := b.PosInBody(tc.pos) if tc.expected != isInBody { if tc.expected { @@ -766,7 +766,7 @@ func TestBlock_PosInAttributes(t *testing.T) { t.Fatal(err) } - b := ParseBlock(block, schema) + b := ParseBlock(block, []*Label{}, 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 new file mode 100644 index 00000000..4193e95a --- /dev/null +++ b/internal/terraform/lang/labels.go @@ -0,0 +1,18 @@ +package lang + +func parseLabels(blockType string, schema LabelSchema, parsed []string) []*Label { + labels := make([]*Label, len(schema)) + + for i, labelName := range schema { + var value string + if len(parsed)-1 >= i { + value = parsed[i] + } + labels[i] = &Label{ + Name: labelName, + Value: value, + } + } + + return labels +} diff --git a/internal/terraform/lang/parser_test.go b/internal/terraform/lang/parser_test.go index 85f814a3..29edcc4d 100644 --- a/internal/terraform/lang/parser_test.go +++ b/internal/terraform/lang/parser_test.go @@ -38,11 +38,8 @@ func TestParser_ParseBlockFromHCL(t *testing.T) { "error from factory", `provider "currywurst" "extra" { }`, - "", - &invalidLabelsErr{ - BlockType: "provider", - Labels: []string{"currywurst", "extra"}, - }, + "provider", + nil, }, } diff --git a/internal/terraform/lang/provider_block.go b/internal/terraform/lang/provider_block.go index d6b55bf7..ef5b3592 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" + tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/terraform/schema" lsp "github.com/sourcegraph/go-lsp" ) @@ -21,16 +22,13 @@ func (f *providerBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) f.logger = discardLog() } - labels := block.Labels - if len(labels) != 1 { - return nil, &invalidLabelsErr{f.BlockType(), labels} - } - return &providerBlock{ - hclBlock: block, - logger: f.logger, - caps: f.caps, - sr: f.schemaReader, + logger: f.logger, + caps: f.caps, + + labelSchema: LabelSchema{"name"}, + hclBlock: block, + sr: f.schemaReader, }, nil } @@ -39,14 +37,33 @@ func (f *providerBlockFactory) BlockType() string { } type providerBlock struct { - logger *log.Logger - caps lsp.TextDocumentClientCapabilities - hclBlock *hclsyntax.Block - sr schema.Reader + logger *log.Logger + caps lsp.TextDocumentClientCapabilities + + labelSchema LabelSchema + labels []*Label + hclBlock *hclsyntax.Block + sr schema.Reader } func (p *providerBlock) Name() string { - return p.hclBlock.Labels[0] + firstLabel := p.RawName() + if firstLabel == "" { + return "" + } + return firstLabel +} + +func (p *providerBlock) RawName() string { + return p.Labels()[0].Value +} + +func (p *providerBlock) Labels() []*Label { + if p.labels != nil { + return p.labels + } + p.labels = parseLabels(p.BlockType(), p.labelSchema, p.hclBlock.Labels) + return p.labels } func (p *providerBlock) BlockType() string { @@ -60,16 +77,20 @@ func (p *providerBlock) CompletionItemsAtPos(pos hcl.Pos) (lsp.CompletionList, e return list, &noSchemaReaderErr{p.BlockType()} } - pSchema, err := p.sr.ProviderConfigSchema(p.Name()) - if err != nil { - return list, err + cb := &completableBlock{ + logger: p.logger, + caps: p.caps, } - cb := &completableBlock{ - logger: p.logger, - caps: p.caps, - hclBlock: p.hclBlock, - schema: pSchema.Block, + var schemaBlock *tfjson.SchemaBlock + if p.RawName() != "" { + pSchema, err := p.sr.ProviderConfigSchema(p.RawName()) + if err != nil { + return list, err + } + schemaBlock = pSchema.Block } + cb.block = ParseBlock(p.hclBlock, p.Labels(), schemaBlock) + return cb.completionItemsAtPos(pos) } diff --git a/internal/terraform/lang/provider_block_test.go b/internal/terraform/lang/provider_block_test.go index 9d84d659..83a56c57 100644 --- a/internal/terraform/lang/provider_block_test.go +++ b/internal/terraform/lang/provider_block_test.go @@ -20,16 +20,16 @@ func TestProviderBlock_Name(t *testing.T) { `provider "aws" "extra" { } `, - "", - &invalidLabelsErr{"provider", []string{"aws", "extra"}}, + "aws", + nil, }, { "invalid config - no labels", `provider { } `, - "", - &invalidLabelsErr{"provider", []string{}}, + "", + nil, }, { "valid config", diff --git a/internal/terraform/lang/resource_block.go b/internal/terraform/lang/resource_block.go index cd4fbfdb..a6226b99 100644 --- a/internal/terraform/lang/resource_block.go +++ b/internal/terraform/lang/resource_block.go @@ -1,11 +1,12 @@ package lang import ( + "fmt" "log" - "strings" hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-ls/internal/terraform/schema" lsp "github.com/sourcegraph/go-lsp" ) @@ -22,16 +23,13 @@ func (f *resourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) f.logger = discardLog() } - labels := block.Labels - if len(labels) != 2 { - return nil, &invalidLabelsErr{f.BlockType(), labels} - } - return &resourceBlock{ - hclBlock: block, - logger: f.logger, - caps: f.caps, - sr: f.schemaReader, + logger: f.logger, + caps: f.caps, + + labelSchema: LabelSchema{"type", "name"}, + hclBlock: block, + sr: f.schemaReader, }, nil } @@ -40,18 +38,42 @@ func (r *resourceBlockFactory) BlockType() string { } type resourceBlock struct { - logger *log.Logger - caps lsp.TextDocumentClientCapabilities - hclBlock *hclsyntax.Block - sr schema.Reader + logger *log.Logger + caps lsp.TextDocumentClientCapabilities + + labelSchema LabelSchema + labels []*Label + hclBlock *hclsyntax.Block + sr schema.Reader } func (r *resourceBlock) Type() string { - return r.hclBlock.Labels[0] + return r.Labels()[0].Value } func (r *resourceBlock) Name() string { - return strings.Join(r.hclBlock.Labels, ".") + firstLabel := r.Labels()[0].Value + secondLabel := r.Labels()[1].Value + + if firstLabel == "" && secondLabel == "" { + return "" + } + if firstLabel == "" { + firstLabel = "" + } + if secondLabel == "" { + secondLabel = "" + } + + return fmt.Sprintf("%s.%s", firstLabel, secondLabel) +} + +func (r *resourceBlock) Labels() []*Label { + if r.labels != nil { + return r.labels + } + r.labels = parseLabels(r.BlockType(), r.labelSchema, r.hclBlock.Labels) + return r.labels } func (r *resourceBlock) BlockType() string { @@ -65,17 +87,20 @@ func (r *resourceBlock) CompletionItemsAtPos(pos hcl.Pos) (lsp.CompletionList, e return list, &noSchemaReaderErr{r.BlockType()} } - rSchema, err := r.sr.ResourceSchema(r.Type()) - if err != nil { - return list, err + cb := &completableBlock{ + logger: r.logger, + caps: r.caps, } - cb := &completableBlock{ - logger: r.logger, - caps: r.caps, - hclBlock: r.hclBlock, - schema: rSchema.Block, + var schemaBlock *tfjson.SchemaBlock + if r.Type() != "" { + rSchema, err := r.sr.ResourceSchema(r.Type()) + if err != nil { + return list, err + } + schemaBlock = rSchema.Block } + cb.block = ParseBlock(r.hclBlock, r.Labels(), schemaBlock) return cb.completionItemsAtPos(pos) } diff --git a/internal/terraform/lang/resource_block_test.go b/internal/terraform/lang/resource_block_test.go index 068cf362..9b9738fd 100644 --- a/internal/terraform/lang/resource_block_test.go +++ b/internal/terraform/lang/resource_block_test.go @@ -20,24 +20,24 @@ func TestResourceBlock_Name(t *testing.T) { `resource { } `, - "", - &invalidLabelsErr{"resource", []string{}}, + "", + nil, }, { "invalid config - single label", `resource "aws_instance" { } `, - "", - &invalidLabelsErr{"resource", []string{"aws_instance"}}, + "aws_instance.", + nil, }, { "invalid config - three labels", `resource "aws_instance" "name" "extra" { } `, - "", - &invalidLabelsErr{"resource", []string{"aws_instance", "name", "extra"}}, + "aws_instance.name", + nil, }, { "valid config", diff --git a/internal/terraform/lang/types.go b/internal/terraform/lang/types.go index cfc2b3d0..0475c4e7 100644 --- a/internal/terraform/lang/types.go +++ b/internal/terraform/lang/types.go @@ -24,6 +24,7 @@ type ConfigBlock interface { CompletionItemsAtPos(pos hcl.Pos) (lsp.CompletionList, error) Name() string BlockType() string + Labels() []*Label } // Block represents a decoded HCL block (by a Parser) @@ -37,6 +38,13 @@ type Block interface { BlockTypes() map[string]*BlockType } +type LabelSchema []string + +type Label struct { + Name string + Value string +} + type BlockType struct { BlockList []Block schema *tfjson.SchemaBlockType