Skip to content

Commit

Permalink
terraform/lang: Formalize label parsing
Browse files Browse the repository at this point in the history
This also makes the parser less strict as we need to be able
to work with incomplete (invalid) declarations.
  • Loading branch information
radeksimko committed Apr 21, 2020
1 parent dcd8d79 commit 6fff231
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 128 deletions.
21 changes: 9 additions & 12 deletions internal/terraform/lang/config_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,37 @@ 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"
)

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
Expand Down
8 changes: 2 additions & 6 deletions internal/terraform/lang/config_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
73 changes: 49 additions & 24 deletions internal/terraform/lang/datasource_block.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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
}

Expand All @@ -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 "<unknown>"
}
if firstLabel == "" {
firstLabel = "<unknown>"
}
if secondLabel == "" {
secondLabel = "<unknown>"
}

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 {
Expand All @@ -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)
}
12 changes: 6 additions & 6 deletions internal/terraform/lang/datasource_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,24 @@ func TestDatasourceBlock_Name(t *testing.T) {
`data {
}
`,
"",
&invalidLabelsErr{"data", []string{}},
"<unknown>",
nil,
},
{
"invalid config - single label",
`data "aws_instance" {
}
`,
"",
&invalidLabelsErr{"data", []string{"aws_instance"}},
"aws_instance.<unknown>",
nil,
},
{
"invalid config - three labels",
`data "aws_instance" "name" "extra" {
}
`,
"",
&invalidLabelsErr{"data", []string{"aws_instance", "name", "extra"}},
"aws_instance.name",
nil,
},
{
"valid config",
Expand Down
13 changes: 0 additions & 13 deletions internal/terraform/lang/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions internal/terraform/lang/hcl_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

type parsedBlock struct {
hclBlock *hclsyntax.Block
labels []*Label
AttributesMap map[string]*Attribute
BlockTypesMap map[string]*BlockType

Expand Down
4 changes: 3 additions & 1 deletion internal/terraform/lang/hcl_block_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
3 changes: 2 additions & 1 deletion internal/terraform/lang/hcl_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions internal/terraform/lang/hcl_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions internal/terraform/lang/labels.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 2 additions & 5 deletions internal/terraform/lang/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}

Expand Down
Loading

0 comments on commit 6fff231

Please sign in to comment.