Skip to content

Commit

Permalink
feat: return an ExprSyntaxError for invalid references that end in a …
Browse files Browse the repository at this point in the history
…dot (commonly occurs in editors for completions)

Detect value expressions of the ExprSyntaxError type when parsing object constructor expressions and use them to add an item to the result even though we skip parsing the object due to recovery after the invalid expression.

This allows the Terraform language server to support completions for object attributes after a dot was typed.
  • Loading branch information
ansgarm committed Aug 21, 2024
1 parent 360ae57 commit 117baa8
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 3 deletions.
23 changes: 20 additions & 3 deletions hclsyntax/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -811,9 +811,16 @@ Traversal:
// will probably be misparsed until we hit something that
// allows us to re-sync.
//
// We will probably need to do something better here eventually
// in order to support autocomplete triggered by typing a
// period.
// Returning an ExprSyntaxError allows us to pass more information
// about the invalid expression to the caller, which can then
// use this for example for completions that happen after typing
// a dot in an editor.
ret = &ExprSyntaxError{
Placeholder: cty.DynamicVal,
ParseDiags: diags,
SrcRange: hcl.RangeBetween(from.Range(), dot.Range),
}

p.setRecovery()
}

Expand Down Expand Up @@ -1516,6 +1523,16 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
diags = append(diags, valueDiags...)

if p.recovery && valueDiags.HasErrors() {
// If the value is an ExprSyntaxError, we can add an item with it, even though we will recover afterwards
// This allows downstream consumers to still retrieve this first invalid item, even though following items
// won't be parsed. This is useful for supplying completions.
if exprSyntaxError, ok := value.(*ExprSyntaxError); ok {
items = append(items, ObjectConsItem{
KeyExpr: key,
ValueExpr: exprSyntaxError,
})
}

// If expression parsing failed then we are probably in a strange
// place in the token stream, so we'll bail out and try to reset
// to after our closing brace to allow parsing to continue.
Expand Down
81 changes: 81 additions & 0 deletions hclsyntax/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2672,6 +2672,87 @@ block "valid" {}
},
},
},
{
"a = { b = c. }",
1,
&Body{
Attributes: Attributes{
"a": {
Name: "a",
Expr: &ObjectConsExpr{
Items: []ObjectConsItem{
{
KeyExpr: &ObjectConsKeyExpr{
Wrapped: &ScopeTraversalExpr{
Traversal: hcl.Traversal{
hcl.TraverseRoot{
Name: "b",
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 7, Byte: 6},
End: hcl.Pos{Line: 1, Column: 8, Byte: 7},
},
},
},
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 7, Byte: 6},
End: hcl.Pos{Line: 1, Column: 8, Byte: 7},
},
},
},
ValueExpr: &ExprSyntaxError{
Placeholder: cty.DynamicVal,
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 11, Byte: 10},
End: hcl.Pos{Line: 1, Column: 13, Byte: 12},
},
ParseDiags: hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Invalid attribute name",
Detail: "An attribute name is required after a dot.",
Subject: &hcl.Range{
Start: hcl.Pos{Line: 1, Column: 14, Byte: 13},
End: hcl.Pos{Line: 1, Column: 15, Byte: 14},
},
},
},
},
},
},
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
End: hcl.Pos{Line: 1, Column: 15, Byte: 14},
},
OpenRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
End: hcl.Pos{Line: 1, Column: 6, Byte: 5},
},
},
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 15, Byte: 14},
},
NameRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 2, Byte: 1},
},
EqualsRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 3, Byte: 2},
End: hcl.Pos{Line: 1, Column: 4, Byte: 3},
},
},
},
Blocks: Blocks{},
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 15, Byte: 14},
},
EndRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 15, Byte: 14},
End: hcl.Pos{Line: 1, Column: 15, Byte: 14},
},
},
},
}

for _, test := range tests {
Expand Down

0 comments on commit 117baa8

Please sign in to comment.