diff --git a/decoder/candidates.go b/decoder/candidates.go index f9df622b..b9454ed4 100644 --- a/decoder/candidates.go +++ b/decoder/candidates.go @@ -55,7 +55,10 @@ func (d *PathDecoder) completionAtPos(ctx context.Context, body *hclsyntax.Body, filename := body.Range().Filename for _, attr := range body.Attributes { - if d.isPosInsideAttrExpr(attr, pos) { + // TODO: handle nil Expr in all nested calls instead which allows us + // to recover incomplete calls to provider defined functions (which have no expression + // as they are deemed invalid by hcl while they are not completed yet) + if attr.Expr != nil && d.isPosInsideAttrExpr(attr, pos) { if bodySchema.Extensions != nil && bodySchema.Extensions.SelfRefs { ctx = schema.WithActiveSelfRefs(ctx) } @@ -82,6 +85,12 @@ func (d *PathDecoder) completionAtPos(ctx context.Context, body *hclsyntax.Body, if attr.EqualsRange.ContainsPos(pos) { return lang.ZeroCandidates(), nil } + + // There is a partial (aka invalid) namespaced function call in an attribute + // We abort here as we don't want to complete body schema candidates + if attr.Expr == nil { + return lang.ZeroCandidates(), nil + } } rng := hcl.Range{ diff --git a/decoder/candidates_test.go b/decoder/candidates_test.go index 60ca4d37..79be38e5 100644 --- a/decoder/candidates_test.go +++ b/decoder/candidates_test.go @@ -1325,6 +1325,40 @@ resource "random_resource" "test" { } } +func TestDecoder_CompletionAtPos_nil_expr(t *testing.T) { + ctx := context.Background() + + // provider:: is not a traversal expression, so hcl will return a nil expression which needs to be + // handled gracefully + f, _ := hclsyntax.ParseConfig([]byte(`attr = provider::`), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "attr": {Constraint: schema.AnyExpression{OfType: cty.DynamicPseudoType}}, + }, + }, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + pos := hcl.Pos{Line: 1, Column: 18, Byte: 17} + + candidates, err := d.CompletionAtPos(ctx, "test.tf", pos) + if err != nil { + t.Fatal(err) + } + + expectedCandidates := lang.CompleteCandidates([]lang.Candidate{}) + + diff := cmp.Diff(expectedCandidates, candidates, ctydebug.CmpOptions) + if diff != "" { + t.Fatalf("unexpected schema for %s: %s", stringPos(pos), diff) + } + +} + func TestDecoder_CompletionAtPos_AnyAttribute(t *testing.T) { ctx := context.Background() providersSchema := &schema.BlockSchema{ diff --git a/decoder/hover.go b/decoder/hover.go index 24e8dbb6..5ddf6a28 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -80,7 +80,7 @@ func (d *PathDecoder) hoverAtPos(ctx context.Context, body *hclsyntax.Body, body }, nil } - if attr.Expr.Range().ContainsPos(pos) { + if attr.Expr != nil && attr.Expr.Range().ContainsPos(pos) { return d.newExpression(attr.Expr, aSchema.Constraint).HoverAtPos(ctx, pos), nil } } diff --git a/decoder/hover_test.go b/decoder/hover_test.go index 5e132953..5c93210f 100644 --- a/decoder/hover_test.go +++ b/decoder/hover_test.go @@ -661,6 +661,35 @@ func TestDecoder_HoverAtPos_basic(t *testing.T) { } } +func TestDecoder_HoverAtPos_nil_expr(t *testing.T) { + // provider:: is not a traversal expression, so hcl will return a nil expression which needs to be + // handled gracefully + f, _ := hclsyntax.ParseConfig([]byte(`attr = provider::`), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "attr": {Constraint: schema.AnyExpression{OfType: cty.DynamicPseudoType}}, + }, + }, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + ctx := context.Background() + _, err := d.HoverAtPos(ctx, "test.tf", hcl.Pos{Line: 1, Column: 16, Byte: 15}) + + if err == nil { + t.Fatal("expected error") + } + + positionalErr := &PositionalError{} + if !errors.As(err, &positionalErr) { + t.Fatal("expected PositionalError for invalid expression") + } +} + func TestDecoder_HoverAtPos_URL(t *testing.T) { resourceLabelSchema := []*schema.LabelSchema{ {Name: "type", IsDepKey: true}, diff --git a/decoder/reference_origins.go b/decoder/reference_origins.go index 19099a24..fcc5cd17 100644 --- a/decoder/reference_origins.go +++ b/decoder/reference_origins.go @@ -161,6 +161,11 @@ func (d *PathDecoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.B } } + // skip if the attribute Expr is nil as all following origins are based on the expression + if attr.Expr == nil { + continue + } + if aSchema.IsDepKey && bodySchema.Targets != nil { origins = append(origins, reference.DirectOrigin{ Range: attr.Expr.Range(), diff --git a/decoder/reference_origins_test.go b/decoder/reference_origins_test.go index 6d5fd53b..598e3fd6 100644 --- a/decoder/reference_origins_test.go +++ b/decoder/reference_origins_test.go @@ -11,7 +11,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" ) @@ -763,3 +765,29 @@ func TestReferenceOriginsTargetingPos(t *testing.T) { }) } } + +func TestCollectReferenceOrigins_nil_expr(t *testing.T) { + // provider:: is not a traversal expression, so hcl will return a nil expression which needs to be + // handled gracefully + f, _ := hclsyntax.ParseConfig([]byte(`attr = provider::`), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "attr": {Constraint: schema.AnyExpression{OfType: cty.DynamicPseudoType}}, + }, + }, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + targets, err := d.CollectReferenceOrigins() + if err != nil { + t.Fatal("unexpected error when collecting reference origins while there was no expr in one of them") + } + + if len(targets) != 0 { + t.Fatalf("expected no targets, got %d", len(targets)) + } +} diff --git a/decoder/reference_targets.go b/decoder/reference_targets.go index 9d8c912e..020f7466 100644 --- a/decoder/reference_targets.go +++ b/decoder/reference_targets.go @@ -275,6 +275,11 @@ func (d *PathDecoder) decodeReferenceTargetsForAttribute(attr *hcl.Attribute, at ctx := context.Background() + // Early return as we don't attempt to recover reference targets for invalid expressions + if attr.Expr == nil { + return refs + } + expr := d.newExpression(attr.Expr, attrSchema.Constraint) if eType, ok := expr.(ReferenceTargetsExpression); ok { var targetCtx *TargetContext diff --git a/decoder/reference_targets_test.go b/decoder/reference_targets_test.go index 07ffbecc..ef8e93e1 100644 --- a/decoder/reference_targets_test.go +++ b/decoder/reference_targets_test.go @@ -12,7 +12,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" ) @@ -352,3 +354,28 @@ func TestReferenceTargetForOriginAtPos(t *testing.T) { }) } } + +func TestCollectReferenceTargets_nil_expr(t *testing.T) { + // provider:: is not a traversal expression, so hcl will return a nil expression which needs to be + // handled gracefully + f, _ := hclsyntax.ParseConfig([]byte(`attr = provider::`), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "attr": {Constraint: schema.AnyExpression{OfType: cty.DynamicPseudoType}}, + }, + }, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal("unexpected error when collecting reference targets while there was no expr in one of them") + } + + if len(targets) != 0 { + t.Fatalf("expected no targets, got %d", len(targets)) + } +} diff --git a/decoder/semantic_tokens.go b/decoder/semantic_tokens.go index 42b10cd0..08ca83ab 100644 --- a/decoder/semantic_tokens.go +++ b/decoder/semantic_tokens.go @@ -79,7 +79,10 @@ func (d *PathDecoder) tokensForBody(ctx context.Context, body *hclsyntax.Body, b Range: attr.NameRange, }) - tokens = append(tokens, d.newExpression(attr.Expr, attrSchema.Constraint).SemanticTokens(ctx)...) + // Invalid expressions may be nil, and we don't want to panic further down the line + if attr.Expr != nil { + tokens = append(tokens, d.newExpression(attr.Expr, attrSchema.Constraint).SemanticTokens(ctx)...) + } } for _, block := range body.Blocks { diff --git a/decoder/semantic_tokens_test.go b/decoder/semantic_tokens_test.go index 6a95d5f5..9718b738 100644 --- a/decoder/semantic_tokens_test.go +++ b/decoder/semantic_tokens_test.go @@ -320,6 +320,55 @@ resource "vault_auth_backend" "blah" { } } +func TestDecoder_SemanticTokensInFile_nil_expr(t *testing.T) { + // provider:: is not a traversal expression, so hcl will return a nil expression which needs to be + // handled gracefully + f, _ := hclsyntax.ParseConfig([]byte(`attr = provider::`), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "attr": {Constraint: schema.AnyExpression{OfType: cty.DynamicPseudoType}}, + }, + }, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + ctx := context.Background() + + tokens, err := d.SemanticTokensInFile(ctx, "test.tf") + if err != nil { + t.Fatal(err) + } + + expectedTokens := []lang.SemanticToken{ + { + Type: "hcl-attrName", + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 5, + Byte: 4, + }, + }, + }, + } + + diff := cmp.Diff(expectedTokens, tokens) + if diff != "" { + t.Fatalf("unexpected tokens: %s", diff) + } +} + func TestDecoder_SemanticTokensInFile_dependentSchema(t *testing.T) { bodySchema := &schema.BodySchema{ Blocks: map[string]*schema.BlockSchema{ diff --git a/go.mod b/go.mod index 5c21f18f..76370782 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21.0 require ( github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/hcl/v2 v2.19.1 + github.com/hashicorp/hcl/v2 v2.19.2-0.20240226153113-c152f81a277f github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 github.com/zclconf/go-cty v1.14.2 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b diff --git a/go.sum b/go.sum index 1ceb41b6..77a46da5 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= -github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +github.com/hashicorp/hcl/v2 v2.19.2-0.20240226153113-c152f81a277f h1:NhMIDxWIF21iTWPHNgMObGU4smTzfzwZbNxsqoCjzug= +github.com/hashicorp/hcl/v2 v2.19.2-0.20240226153113-c152f81a277f/go.mod h1:WmcD/Ym72MDOOx5F62Ly+leloeu6H7m0pG7VBiU6pQk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=