diff --git a/decoder/body_extensions_test.go b/decoder/body_extensions_test.go index c517ad7e..f754c912 100644 --- a/decoder/body_extensions_test.go +++ b/decoder/body_extensions_test.go @@ -15,7 +15,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" ) -func TestCompletionAtPos_BodySchema_Extensions(t *testing.T) { +func TestCompletionAtPos_BodySchema_Extensions_Count(t *testing.T) { ctx := context.Background() testCases := []struct { @@ -297,16 +297,42 @@ func TestCompletionAtPos_BodySchema_Extensions(t *testing.T) { }, Type: cty.Number, Description: lang.PlainText("The distinct index number (starting with 0) corresponding to the instance"), + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 34, + }, + End: hcl.Pos{ + Line: 2, + Column: 12, + Byte: 43, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 34, + }, + End: hcl.Pos{ + Line: 2, + Column: 8, + Byte: 39, + }, + }, }, }, `resource "aws_instance" "foo" { - count = 4 - cpu_count = + count = 4 + cpu_count = }`, hcl.Pos{ Line: 3, - Column: 14, - Byte: 55, + Column: 15, + Byte: 57, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -321,13 +347,13 @@ func TestCompletionAtPos_BodySchema_Extensions(t *testing.T) { Filename: "test.tf", Start: hcl.Pos{ Line: 3, - Column: 13, - Byte: 55, + Column: 15, + Byte: 58, }, End: hcl.Pos{ Line: 3, - Column: 13, - Byte: 55, + Column: 15, + Byte: 58, }, }, NewText: "count.index", @@ -602,6 +628,43 @@ variable "test" { }, }), }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: tc.bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + ReferenceTargets: tc.referenceTargets, + }) + + candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" { + t.Fatalf("unexpected candidates: %s", diff) + } + }) + } +} + +func TestCompletionAtPos_BodySchema_Extensions_ForEach(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + testName string + bodySchema *schema.BodySchema + referenceTargets reference.Targets + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ { "foreach attribute completion", &schema.BodySchema{ @@ -1104,7 +1167,526 @@ for_each = } } -func TestCompletionAtPos_BodySchema_DynamicBlock_Extensions(t *testing.T) { +func TestCompletionAtPos_BodySchema_Extensions_SelfRef(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + testName string + bodySchema *schema.BodySchema + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + // nested block + { + "target self addr enabled but no extension enabled", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + {Name: "name"}, + }, + Body: schema.NewBodySchema(), + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "cpu_count": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + "fox": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + Address: &schema.BlockAddrSchema{ + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + }, + }, + }, + }, + `resource "aws_instance" "foo" { + cpu_count = 4 + fox = +}`, + hcl.Pos{Line: 3, Column: 8, Byte: 55}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "target self addr enabled and extension enabled", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + SelfRefs: true, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "cpu_count": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + "fox": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + Address: &schema.BlockAddrSchema{ + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + }, + }, + }, + }, + `resource "aws_instance" "foo" { + cpu_count = 4 + fox = +}`, + hcl.Pos{Line: 3, Column: 8, Byte: 55}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "self", + Detail: "object", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 8, Byte: 55}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 55}, + }, + + NewText: "self", + Snippet: "self", + }, + Kind: lang.TraversalCandidateKind, + }, + }), + }, + { + "target self addr disabled and extension enabled", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + SelfRefs: true, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "cpu_count": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + "fox": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + Address: &schema.BlockAddrSchema{ + DependentBodyAsData: true, + InferDependentBody: true, + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + }, + }, + }, + }, + `resource "aws_instance" "foo" { + cpu_count = 4 + fox = +}`, + hcl.Pos{Line: 3, Column: 8, Byte: 55}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "no cyclical completion (attr = self.attr)", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + SelfRefs: true, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "cpu_count": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + Address: &schema.BlockAddrSchema{ + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + }, + }, + }, + }, + `resource "aws_instance" "foo" { + cpu_count = +}`, + hcl.Pos{Line: 2, Column: 15, Byte: 46}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "completion with prefix", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + SelfRefs: true, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "cpu_count": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + "fox": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + Address: &schema.BlockAddrSchema{ + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + }, + }, + }, + }, + `resource "aws_instance" "foo" { + cpu_count = 4 + fox = self. +}`, + hcl.Pos{Line: 3, Column: 14, Byte: 61}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "self.cpu_count", + Detail: "number", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 9, Byte: 56}, + End: hcl.Pos{Line: 3, Column: 14, Byte: 61}, + }, + NewText: "self.cpu_count", + Snippet: "self.cpu_count", + }, + Kind: lang.TraversalCandidateKind, + }, + }), + }, + { + "target self addr enabled and extension enabled within a block", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "animal": { + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + SelfRefs: true, + }, + Attributes: map[string]*schema.AttributeSchema{ + "fox": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "cpu_count": { + IsOptional: true, + Expr: schema.ExprConstraints{ + schema.TraversalExpr{ + OfType: cty.Number, + }, + schema.LiteralTypeExpr{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + Address: &schema.BlockAddrSchema{ + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + }, + }, + }, + }, + `resource "aws_instance" "foo" { + cpu_count = 4 + animal { + fox = + } +}`, + hcl.Pos{Line: 4, Column: 10, Byte: 68}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "self", + Detail: "object", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 4, Column: 10, Byte: 68}, + End: hcl.Pos{Line: 4, Column: 10, Byte: 68}, + }, + + NewText: "self", + Snippet: "self", + }, + Kind: lang.TraversalCandidateKind, + }, + }), + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: tc.bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + d = testPathDecoder(t, &PathContext{ + Schema: tc.bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + ReferenceTargets: targets, + }) + + candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" { + t.Fatalf("unexpected candidates: %s", diff) + } + }) + } +} + +func TestCompletionAtPos_BodySchema_Extensions_DynamicBlock(t *testing.T) { ctx := context.Background() testCases := []struct { diff --git a/decoder/candidates.go b/decoder/candidates.go index ea215449..64a04559 100644 --- a/decoder/candidates.go +++ b/decoder/candidates.go @@ -70,6 +70,9 @@ func (d *PathDecoder) candidatesAtPos(ctx context.Context, body *hclsyntax.Body, for _, attr := range body.Attributes { if d.isPosInsideAttrExpr(attr, pos) { + if bodySchema.Extensions != nil && bodySchema.Extensions.SelfRefs { + ctx = schema.WithActiveSelfRefs(ctx) + } if bodySchema.Extensions != nil && bodySchema.Extensions.Count && attr.Name == "count" { return d.attrValueCandidatesAtPos(ctx, attr, countAttributeSchema(), outerBodyRng, pos) } diff --git a/decoder/expression_candidates.go b/decoder/expression_candidates.go index 83426ab7..9353c580 100644 --- a/decoder/expression_candidates.go +++ b/decoder/expression_candidates.go @@ -472,7 +472,7 @@ func (d *PathDecoder) candidatesForTraversalConstraint(ctx context.Context, tc s prefix, _ := d.bytesFromRange(prefixRng) d.pathCtx.ReferenceTargets.MatchWalk(ctx, tc, string(prefix), outermostBodyRng, editRng, func(target reference.Target) error { - address := target.Address(ctx).String() + address := target.Address(ctx, editRng.Start).String() candidates = append(candidates, lang.Candidate{ Label: address, diff --git a/decoder/hover.go b/decoder/hover.go index 2d12f286..b05b0c81 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -64,6 +64,10 @@ func (d *PathDecoder) hoverAtPos(ctx context.Context, body *hclsyntax.Body, body for name, attr := range body.Attributes { if attr.Range().ContainsPos(pos) { var aSchema *schema.AttributeSchema + if bodySchema.Extensions != nil && bodySchema.Extensions.SelfRefs { + ctx = schema.WithActiveSelfRefs(ctx) + } + if bodySchema.Extensions != nil && bodySchema.Extensions.Count && name == "count" { aSchema = countAttributeSchema() } else if bodySchema.Extensions != nil && bodySchema.Extensions.ForEach && name == "for_each" { @@ -643,14 +647,14 @@ func (d *PathDecoder) hoverContentForTraversalExpr(ctx context.Context, traversa } // TODO: Reflect additional found targets here? - return hoverContentForReferenceTarget(ctx, targets[0]) + return hoverContentForReferenceTarget(ctx, targets[0], pos) } return "", &reference.NoTargetFound{} } -func hoverContentForReferenceTarget(ctx context.Context, ref reference.Target) (string, error) { - content := fmt.Sprintf("`%s`", ref.Address(ctx)) +func hoverContentForReferenceTarget(ctx context.Context, ref reference.Target, pos hcl.Pos) (string, error) { + content := fmt.Sprintf("`%s`", ref.Address(ctx, pos)) var friendlyName string if ref.Type != cty.NilType { diff --git a/decoder/reference_origins.go b/decoder/reference_origins.go index 2f07ea05..9e9bbf7a 100644 --- a/decoder/reference_origins.go +++ b/decoder/reference_origins.go @@ -156,7 +156,11 @@ func (d *PathDecoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.B }) } - origins = append(origins, d.findOriginsInExpression(attr.Expr, aSchema.Expr)...) + allowSelfRefs := false + if bodySchema.Extensions != nil && bodySchema.Extensions.SelfRefs { + allowSelfRefs = true + } + origins = append(origins, d.findOriginsInExpression(attr.Expr, aSchema.Expr, allowSelfRefs)...) } for _, block := range content.Blocks { @@ -180,7 +184,7 @@ func (d *PathDecoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.B return origins, impliedOrigins } -func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.ExprConstraints) reference.Origins { +func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.ExprConstraints, allowSelfRefs bool) reference.Origins { origins := make(reference.Origins, 0) switch eType := expr.(type) { @@ -188,7 +192,7 @@ func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.Exp tce, ok := ExprConstraints(ec).TupleConsExpr() if ok { for _, elemExpr := range eType.ExprList() { - origins = append(origins, d.findOriginsInExpression(elemExpr, tce.AnyElem)...) + origins = append(origins, d.findOriginsInExpression(elemExpr, tce.AnyElem, allowSelfRefs)...) } break } @@ -196,7 +200,7 @@ func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.Exp le, ok := ExprConstraints(ec).ListExpr() if ok { for _, elemExpr := range eType.ExprList() { - origins = append(origins, d.findOriginsInExpression(elemExpr, le.Elem)...) + origins = append(origins, d.findOriginsInExpression(elemExpr, le.Elem, allowSelfRefs)...) } break } @@ -204,7 +208,7 @@ func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.Exp se, ok := ExprConstraints(ec).SetExpr() if ok { for _, elemExpr := range eType.ExprList() { - origins = append(origins, d.findOriginsInExpression(elemExpr, se.Elem)...) + origins = append(origins, d.findOriginsInExpression(elemExpr, se.Elem, allowSelfRefs)...) } break } @@ -215,7 +219,7 @@ func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.Exp if len(tue.Elems) < i+1 { break } - origins = append(origins, d.findOriginsInExpression(elemExpr, tue.Elems[i])...) + origins = append(origins, d.findOriginsInExpression(elemExpr, tue.Elems[i], allowSelfRefs)...) } } case *hclsyntax.ObjectConsExpr: @@ -235,14 +239,14 @@ func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.Exp continue } - origins = append(origins, d.findOriginsInExpression(item.ValueExpr, attr.Expr)...) + origins = append(origins, d.findOriginsInExpression(item.ValueExpr, attr.Expr, allowSelfRefs)...) } } me, ok := ExprConstraints(ec).MapExpr() if ok { for _, item := range eType.Items { - origins = append(origins, d.findOriginsInExpression(item.ValueExpr, me.Elem)...) + origins = append(origins, d.findOriginsInExpression(item.ValueExpr, me.Elem, allowSelfRefs)...) } } case *hclsyntax.AnonSymbolExpr, @@ -266,7 +270,7 @@ func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.Exp // see https://github.com/hashicorp/terraform-ls/issues/496 tes, ok := ExprConstraints(ec).TraversalExprs() if ok { - origins = append(origins, reference.TraversalsToLocalOrigins(expr.Variables(), tes)...) + origins = append(origins, reference.TraversalsToLocalOrigins(expr.Variables(), tes, allowSelfRefs)...) } case *hclsyntax.LiteralValueExpr: // String constant may also be a traversal in some cases, but currently not recognized @@ -279,7 +283,7 @@ func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.Exp // This may result in less accurate decoding where even origins // which do not actually conform to the constraints are recognized. // TODO: https://github.com/hashicorp/terraform-ls/issues/675 - origins = append(origins, reference.TraversalsToLocalOrigins(expr.Variables(), schema.TraversalExprs{})...) + origins = append(origins, reference.TraversalsToLocalOrigins(expr.Variables(), schema.TraversalExprs{}, allowSelfRefs)...) } return origins @@ -297,7 +301,11 @@ func (d *PathDecoder) referenceOriginAtPos(body *hclsyntax.Body, bodySchema *sch aSchema = bodySchema.AnyAttribute } - for _, origin := range d.findOriginsInExpression(attr.Expr, aSchema.Expr) { + allowSelfRefs := false + if bodySchema.Extensions != nil && bodySchema.Extensions.SelfRefs { + allowSelfRefs = true + } + for _, origin := range d.findOriginsInExpression(attr.Expr, aSchema.Expr, allowSelfRefs) { if origin.OriginRange().ContainsPos(pos) { return &origin, nil } diff --git a/decoder/reference_targets.go b/decoder/reference_targets.go index 7d7b3223..5ce68034 100644 --- a/decoder/reference_targets.go +++ b/decoder/reference_targets.go @@ -181,7 +181,7 @@ func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock * if bSchema.Address.InferBody && bSchema.Body != nil { bodyRef.NestedTargets = append(bodyRef.NestedTargets, - d.collectInferredReferenceTargetsForBody(addr, bSchema.Address.ScopeId, blk.Body, bSchema.Body)...) + d.collectInferredReferenceTargetsForBody(addr, bSchema.Address, blk.Body, bSchema.Body, nil, lang.Address{})...) } bodyRef.Type = bodyToDataType(bSchema.Type, bSchema.Body) @@ -214,8 +214,17 @@ func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock * bodyRef.Type = bodyToDataType(bSchema.Type, fullSchema) if bSchema.Address.InferDependentBody && len(bSchema.DependentBody) > 0 { + if bSchema.Address.DependentBodySelfRef { + bodyRef.LocalAddr = lang.Address{ + lang.RootStep{Name: "self"}, + } + bodyRef.TargetableFromRangePtr = blk.Range.Ptr() + } else { + bodyRef.LocalAddr = lang.Address{} + } + bodyRef.NestedTargets = append(bodyRef.NestedTargets, - d.collectInferredReferenceTargetsForBody(addr, bSchema.Address.ScopeId, blk.Body, fullSchema)...) + d.collectInferredReferenceTargetsForBody(addr, bSchema.Address, blk.Body, fullSchema, nil, bodyRef.LocalAddr)...) } if !bSchema.Address.BodyAsData { @@ -652,10 +661,15 @@ func bodySchemaAsAttrTypes(bodySchema *schema.BodySchema) map[string]cty.Type { return attrTypes } -func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, scopeId lang.ScopeId, body hcl.Body, bodySchema *schema.BodySchema) reference.Targets { +func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, bAddrSchema *schema.BlockAddrSchema, body hcl.Body, bodySchema *schema.BodySchema, selfRefBodyRangePtr *hcl.Range, selfRefAddr lang.Address) reference.Targets { refs := make(reference.Targets, 0) content := decodeBody(body, bodySchema) + // We don't get body range for JSON here + // TODO? calculate or implement upstream + if bAddrSchema.DependentBodySelfRef && content.RangePtr != nil && selfRefBodyRangePtr == nil { + selfRefBodyRangePtr = content.RangePtr + } for name, aSchema := range bodySchema.Attributes { attrType, ok := exprConstraintToDataType(aSchema.Expr) @@ -668,12 +682,21 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, ref := reference.Target{ Addr: attrAddr, - ScopeId: scopeId, + ScopeId: bAddrSchema.ScopeId, Type: attrType, Description: aSchema.Description, RangePtr: body.MissingItemRange().Ptr(), } + if bAddrSchema.DependentBodySelfRef && selfRefBodyRangePtr != nil { + localAddr := make(lang.Address, len(selfRefAddr)) + copy(localAddr, selfRefAddr) + localAddr = append(localAddr, lang.AttrStep{Name: name}) + + ref.LocalAddr = localAddr + ref.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + } + var attrExpr hcl.Expression if attr, ok := content.Attributes[name]; ok { ref.RangePtr = attr.Range.Ptr() @@ -683,7 +706,7 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, if attrExpr != nil && !attrType.IsPrimitiveType() { ref.NestedTargets = make(reference.Targets, 0) - ref.NestedTargets = append(ref.NestedTargets, decodeReferenceTargetsForComplexTypeExpr(attrAddr, attrExpr, attrType, scopeId)...) + ref.NestedTargets = append(ref.NestedTargets, decodeReferenceTargetsForComplexTypeExpr(attrAddr, attrExpr, attrType, bAddrSchema.ScopeId)...) } refs = append(refs, ref) @@ -700,14 +723,21 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, blockRef := reference.Target{ Addr: blockAddr, - ScopeId: scopeId, + LocalAddr: make(lang.Address, len(selfRefAddr)), + ScopeId: bAddrSchema.ScopeId, Type: cty.Object(bodySchemaAsAttrTypes(bCollection.Schema.Body)), Description: bCollection.Schema.Description, DefRangePtr: blk.DefRange.Ptr(), RangePtr: blk.Range.Ptr(), - NestedTargets: d.collectInferredReferenceTargetsForBody( - blockAddr, scopeId, blk.Body, bCollection.Schema.Body), } + if bAddrSchema.DependentBodySelfRef && selfRefBodyRangePtr != nil { + copy(blockRef.LocalAddr, selfRefAddr) + blockRef.LocalAddr = append(blockRef.LocalAddr, lang.AttrStep{Name: bType}) + blockRef.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + } + blockRef.NestedTargets = d.collectInferredReferenceTargetsForBody( + blockAddr, bAddrSchema, blk.Body, bCollection.Schema.Body, selfRefBodyRangePtr, blockRef.LocalAddr) + sort.Sort(blockRef.NestedTargets) refs = append(refs, blockRef) } @@ -719,11 +749,17 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, blockRef := reference.Target{ Addr: blockAddr, - ScopeId: scopeId, + LocalAddr: make(lang.Address, len(selfRefAddr)), + ScopeId: bAddrSchema.ScopeId, Type: cty.List(cty.Object(bodySchemaAsAttrTypes(bCollection.Schema.Body))), Description: bCollection.Schema.Description, RangePtr: body.MissingItemRange().Ptr(), } + if bAddrSchema.DependentBodySelfRef && selfRefBodyRangePtr != nil { + copy(blockRef.LocalAddr, selfRefAddr) + blockRef.LocalAddr = append(blockRef.LocalAddr, lang.AttrStep{Name: bType}) + blockRef.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + } for i, b := range bCollection.Blocks { elemAddr := make(lang.Address, len(blockAddr)) @@ -734,14 +770,25 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, elemRef := reference.Target{ Addr: elemAddr, - ScopeId: scopeId, + LocalAddr: make(lang.Address, len(blockRef.LocalAddr)), + ScopeId: bAddrSchema.ScopeId, Type: cty.Object(bodySchemaAsAttrTypes(bCollection.Schema.Body)), Description: bCollection.Schema.Description, DefRangePtr: b.DefRange.Ptr(), RangePtr: b.Range.Ptr(), - NestedTargets: d.collectInferredReferenceTargetsForBody( - elemAddr, scopeId, b.Body, bCollection.Schema.Body), } + + if bAddrSchema.DependentBodySelfRef && selfRefBodyRangePtr != nil { + copy(elemRef.LocalAddr, blockRef.LocalAddr) + elemRef.LocalAddr = append(elemRef.LocalAddr, lang.IndexStep{ + Key: cty.NumberIntVal(int64(i)), + }) + elemRef.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + } + + elemRef.NestedTargets = d.collectInferredReferenceTargetsForBody( + elemAddr, bAddrSchema, b.Body, bCollection.Schema.Body, selfRefBodyRangePtr, elemRef.LocalAddr) + sort.Sort(elemRef.NestedTargets) blockRef.NestedTargets = append(blockRef.NestedTargets, elemRef) @@ -771,11 +818,17 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, blockRef := reference.Target{ Addr: blockAddr, - ScopeId: scopeId, + LocalAddr: make(lang.Address, len(selfRefAddr)), + ScopeId: bAddrSchema.ScopeId, Type: cty.Set(cty.Object(bodySchemaAsAttrTypes(bCollection.Schema.Body))), Description: bCollection.Schema.Description, RangePtr: body.MissingItemRange().Ptr(), } + if bAddrSchema.DependentBodySelfRef && selfRefBodyRangePtr != nil { + copy(blockRef.LocalAddr, selfRefAddr) + blockRef.LocalAddr = append(blockRef.LocalAddr, lang.AttrStep{Name: bType}) + blockRef.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + } for i, b := range bCollection.Blocks { if i == 0 { @@ -803,11 +856,17 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, blockRef := reference.Target{ Addr: blockAddr, - ScopeId: scopeId, + LocalAddr: make(lang.Address, len(selfRefAddr)), + ScopeId: bAddrSchema.ScopeId, Type: cty.Map(cty.Object(bodySchemaAsAttrTypes(bCollection.Schema.Body))), Description: bCollection.Schema.Description, RangePtr: body.MissingItemRange().Ptr(), } + if bAddrSchema.DependentBodySelfRef && selfRefBodyRangePtr != nil { + copy(blockRef.LocalAddr, selfRefAddr) + blockRef.LocalAddr = append(blockRef.LocalAddr, lang.AttrStep{Name: bType}) + blockRef.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + } for i, b := range bCollection.Blocks { elemAddr := make(lang.Address, len(blockAddr)) @@ -820,14 +879,23 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, elemRef := reference.Target{ Addr: elemAddr, - ScopeId: scopeId, + LocalAddr: make(lang.Address, len(blockRef.LocalAddr)), + ScopeId: bAddrSchema.ScopeId, Type: refType, Description: bCollection.Schema.Description, RangePtr: b.Range.Ptr(), DefRangePtr: b.DefRange.Ptr(), - NestedTargets: d.collectInferredReferenceTargetsForBody( - elemAddr, scopeId, b.Body, bCollection.Schema.Body), } + if bAddrSchema.DependentBodySelfRef && selfRefBodyRangePtr != nil { + copy(elemRef.LocalAddr, blockRef.LocalAddr) + elemRef.LocalAddr = append(elemRef.LocalAddr, lang.IndexStep{ + Key: cty.StringVal(b.Labels[0]), + }) + elemRef.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + } + + elemRef.NestedTargets = d.collectInferredReferenceTargetsForBody( + elemAddr, bAddrSchema, b.Body, bCollection.Schema.Body, selfRefBodyRangePtr, elemRef.LocalAddr) sort.Sort(elemRef.NestedTargets) blockRef.NestedTargets = append(blockRef.NestedTargets, elemRef) diff --git a/decoder/reference_targets_collect_extensions_hcl_test.go b/decoder/reference_targets_collect_extensions_hcl_test.go new file mode 100644 index 00000000..c07fc2f7 --- /dev/null +++ b/decoder/reference_targets_collect_extensions_hcl_test.go @@ -0,0 +1,1182 @@ +package decoder + +import ( + "fmt" + "testing" + + "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" +) + +func TestCollectReferenceTargets_extension_hcl(t *testing.T) { + testCases := []struct { + name string + schema *schema.BodySchema + cfg string + expectedRefs reference.Targets + }{ + { + "self references collection - attributes", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Address: &schema.BlockAddrSchema{ + Steps: schema.Address{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + }, + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + }, + { + Name: "name", + }, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "static": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.String)}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "bar": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.Number)}, + "foo": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.String)}, + }, + }, + }, + }, + }, + }, + `resource "aws_instance" "blah" { + static = "test" + foo = "test" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 5, + Column: 2, + Byte: 78, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 31, + Byte: 30, + }, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 5, + Column: 2, + Byte: 78, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "bar": cty.Number, + "foo": cty.String, + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "bar"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 3, + Byte: 68, + }, + End: hcl.Pos{ + Line: 4, + Column: 11, + Byte: 76, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 3, + Byte: 68, + }, + End: hcl.Pos{ + Line: 4, + Column: 6, + Byte: 71, + }, + }, + Type: cty.Number, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 5, + Column: 2, + Byte: 78, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 3, + Column: 15, + Byte: 65, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 3, + Column: 6, + Byte: 56, + }, + }, + Type: cty.String, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 5, + Column: 2, + Byte: 78, + }, + }, + }, + }, + }, + }, + }, + { + "self references collection - object block", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Address: &schema.BlockAddrSchema{ + Steps: schema.Address{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + }, + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + }, + { + Name: "name", + }, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "static": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.String)}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Blocks: map[string]*schema.BlockSchema{ + "foo": { + Type: schema.BlockTypeObject, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "bar": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.Number)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + `resource "aws_instance" "blah" { + static = "test" + foo { + bar = 42 + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 31, + Byte: 30, + }, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.Object(map[string]cty.Type{ + "bar": cty.Number, + }), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 5, + Column: 4, + Byte: 75, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 3, + Column: 6, + Byte: 56, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "bar": cty.Number, + }), + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + lang.AttrStep{Name: "bar"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 5, + Byte: 63, + }, + End: hcl.Pos{ + Line: 4, + Column: 13, + Byte: 71, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 5, + Byte: 63, + }, + End: hcl.Pos{ + Line: 4, + Column: 8, + Byte: 66, + }, + }, + Type: cty.Number, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "self references collection - list block", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Address: &schema.BlockAddrSchema{ + Steps: schema.Address{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + }, + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + }, + { + Name: "name", + }, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "static": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.String)}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Blocks: map[string]*schema.BlockSchema{ + "foo": { + Type: schema.BlockTypeList, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "bar": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.Number)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + `resource "aws_instance" "blah" { + static = "test" + foo { + bar = 42 + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 31, + Byte: 30, + }, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.List(cty.Object(map[string]cty.Type{ + "bar": cty.Number, + })), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 5, + Column: 4, + Byte: 75, + }, + }, + Type: cty.List(cty.Object(map[string]cty.Type{ + "bar": cty.Number, + })), + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 5, + Column: 4, + Byte: 75, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 3, + Column: 6, + Byte: 56, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "bar": cty.Number, + }), + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.AttrStep{Name: "bar"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 5, + Byte: 63, + }, + End: hcl.Pos{ + Line: 4, + Column: 13, + Byte: 71, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 5, + Byte: 63, + }, + End: hcl.Pos{ + Line: 4, + Column: 8, + Byte: 66, + }, + }, + Type: cty.Number, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "self references collection - set block", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Address: &schema.BlockAddrSchema{ + Steps: schema.Address{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + }, + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + }, + { + Name: "name", + }, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "static": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.String)}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Blocks: map[string]*schema.BlockSchema{ + "foo": { + Type: schema.BlockTypeSet, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "bar": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.Number)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + `resource "aws_instance" "blah" { + static = "test" + foo { + bar = 42 + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 31, + Byte: 30, + }, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.Set(cty.Object(map[string]cty.Type{ + "bar": cty.Number, + })), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 5, + Column: 4, + Byte: 75, + }, + }, + Type: cty.Set(cty.Object(map[string]cty.Type{ + "bar": cty.Number, + })), + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 77, + }, + }, + }, + }, + }, + }, + }, + { + "self references collection - map block", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Address: &schema.BlockAddrSchema{ + Steps: schema.Address{ + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + }, + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + }, + { + Name: "name", + }, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "static": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.String)}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "aws_instance", + }, + }, + }): { + Blocks: map[string]*schema.BlockSchema{ + "foo": { + Type: schema.BlockTypeMap, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "bar": {IsOptional: true, Expr: schema.LiteralTypeOnly(cty.Number)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + `resource "aws_instance" "blah" { + static = "test" + foo "dog" { + bar = 42 + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 83, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 31, + Byte: 30, + }, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 83, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.Map(cty.Object(map[string]cty.Type{ + "bar": cty.Number, + })), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 5, + Column: 4, + Byte: 81, + }, + }, + Type: cty.Map(cty.Object(map[string]cty.Type{ + "bar": cty.Number, + })), + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 83, + }, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.StringVal("dog")}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.StringVal("dog")}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 5, + Column: 4, + Byte: 81, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 53, + }, + End: hcl.Pos{ + Line: 3, + Column: 12, + Byte: 62, + }, + }, + Type: cty.Object(map[string]cty.Type{ + "bar": cty.Number, + }), + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 83, + }, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "blah"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.StringVal("dog")}, + lang.AttrStep{Name: "bar"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "foo"}, + lang.IndexStep{Key: cty.StringVal("dog")}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 5, + Byte: 69, + }, + End: hcl.Pos{ + Line: 4, + Column: 13, + Byte: 77, + }, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 4, + Column: 5, + Byte: 69, + }, + End: hcl.Pos{ + Line: 4, + Column: 8, + Byte: 72, + }, + }, + Type: cty.Number, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 32, + Byte: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 83, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { + f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + + d := testPathDecoder(t, &PathContext{ + Schema: tc.schema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + refs, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefs, refs, ctydebug.CmpOptions); diff != "" { + t.Fatalf("mismatch of references: %s", diff) + } + }) + } +} diff --git a/decoder/reference_targets_collect_hcl_test.go b/decoder/reference_targets_collect_hcl_test.go index e6ba125d..ad8055c5 100644 --- a/decoder/reference_targets_collect_hcl_test.go +++ b/decoder/reference_targets_collect_hcl_test.go @@ -1677,6 +1677,7 @@ provider "test" { Addr: lang.Address{ lang.RootStep{Name: "aws"}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "attr": cty.Number, "name": cty.String, @@ -2133,6 +2134,7 @@ provider "test" { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "objblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -2377,6 +2379,7 @@ provider "test" { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "listblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -2403,6 +2406,7 @@ provider "test" { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(0)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -2515,6 +2519,7 @@ provider "test" { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(1)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -2763,6 +2768,7 @@ provider "test" { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "setblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -2924,6 +2930,7 @@ provider "test" { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "listblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -2950,6 +2957,7 @@ provider "test" { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(0)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -3062,6 +3070,7 @@ provider "test" { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(1)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -3312,6 +3321,7 @@ provider "test" { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "listener"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -3338,6 +3348,7 @@ provider "test" { lang.AttrStep{Name: "listener"}, lang.IndexStep{Key: cty.StringVal("http")}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "port": cty.Number, "protocol": cty.String, @@ -3450,6 +3461,7 @@ provider "test" { lang.AttrStep{Name: "listener"}, lang.IndexStep{Key: cty.StringVal("https")}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "port": cty.Number, "protocol": cty.String, @@ -4234,6 +4246,7 @@ module "different" { lang.RootStep{Name: "module"}, lang.AttrStep{Name: "test"}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "attr": cty.String, }), diff --git a/decoder/reference_targets_collect_json_test.go b/decoder/reference_targets_collect_json_test.go index 06c61341..8a0e2fd2 100644 --- a/decoder/reference_targets_collect_json_test.go +++ b/decoder/reference_targets_collect_json_test.go @@ -1473,6 +1473,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { Addr: lang.Address{ lang.RootStep{Name: "aws"}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "attr": cty.Number, "name": cty.String, @@ -1826,6 +1827,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "objblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -2074,6 +2076,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "listblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -2100,6 +2103,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(0)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -2212,6 +2216,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(1)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -2464,6 +2469,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "setblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -2629,6 +2635,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "listblock"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -2655,6 +2662,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(0)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -2767,6 +2775,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "listblock"}, lang.IndexStep{Key: cty.NumberIntVal(1)}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -3023,6 +3032,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "aws"}, lang.AttrStep{Name: "listener"}, }, + LocalAddr: lang.Address{}, RangePtr: &hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{ @@ -3049,6 +3059,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "listener"}, lang.IndexStep{Key: cty.StringVal("http")}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "port": cty.Number, "protocol": cty.String, @@ -3161,6 +3172,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.AttrStep{Name: "listener"}, lang.IndexStep{Key: cty.StringVal("https")}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "port": cty.Number, "protocol": cty.String, @@ -3954,6 +3966,7 @@ func TestCollectReferenceTargets_json(t *testing.T) { lang.RootStep{Name: "module"}, lang.AttrStep{Name: "test"}, }, + LocalAddr: lang.Address{}, Type: cty.Object(map[string]cty.Type{ "attr": cty.String, }), diff --git a/reference/target.go b/reference/target.go index dd9f4a86..f015a7b2 100644 --- a/reference/target.go +++ b/reference/target.go @@ -98,13 +98,23 @@ func copyHclRangePtr(rng *hcl.Range) *hcl.Range { // Address returns any of the two non-empty addresses // depending on the provided context -func (r Target) Address(ctx context.Context) lang.Address { - addr := r.Addr +func (r Target) Address(ctx context.Context, pos hcl.Pos) lang.Address { if len(r.LocalAddr) > 0 { - addr = r.LocalAddr + // If the target has only local address, use it + if len(r.Addr) == 0 { + return r.LocalAddr + } + + // If the target has local self address & self is active + if r.LocalAddr[0].String() == "self" && schema.ActiveSelfRefsFromContext(ctx) { + // and we targeting it from the expected range + if r.TargetableFromRangePtr != nil && r.TargetableFromRangePtr.ContainsPos(pos) { + return r.LocalAddr + } + } } - return addr + return r.Addr } func (r Target) FriendlyName() string { diff --git a/reference/target_test.go b/reference/target_test.go new file mode 100644 index 00000000..f83853ce --- /dev/null +++ b/reference/target_test.go @@ -0,0 +1,160 @@ +package reference + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty-debug/ctydebug" +) + +func TestTarget_Address(t *testing.T) { + testCases := []struct { + name string + pos hcl.Pos + activeSelfRefs bool + target Target + expectedAddress lang.Address + }{ + { + "absolute address and no local address", + hcl.InitialPos, + false, + Target{ + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + }, + lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + }, + { + "local address and no absolute address", + hcl.InitialPos, + false, + Target{ + LocalAddr: lang.Address{ + lang.RootStep{Name: "count"}, + lang.AttrStep{Name: "index"}, + }, + }, + lang.Address{ + lang.RootStep{Name: "count"}, + lang.AttrStep{Name: "index"}, + }, + }, + { + "self address with active self and matching range", + hcl.Pos{Line: 2, Column: 2, Byte: 2}, + true, + Target{ + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "instance_size"}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 10}, + }, + }, + lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "instance_size"}, + }, + }, + { + "self address without active self but matching range", + hcl.Pos{Line: 2, Column: 2, Byte: 2}, + false, + Target{ + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "instance_size"}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 10}, + }, + }, + lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + }, + { + "self address with active self but no matching range", + hcl.Pos{Line: 5, Column: 2, Byte: 15}, + true, + Target{ + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "instance_size"}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 10}, + }, + }, + lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + }, + { + "self address with active self and missing targetable", + hcl.Pos{Line: 5, Column: 2, Byte: 15}, + true, + Target{ + Addr: lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "instance_size"}, + }, + }, + lang.Address{ + lang.RootStep{Name: "aws_instance"}, + lang.AttrStep{Name: "instance_size"}, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { + ctx := context.Background() + + if tc.activeSelfRefs { + ctx = schema.WithActiveSelfRefs(ctx) + } + + address := tc.target.Address(ctx, tc.pos) + if diff := cmp.Diff(tc.expectedAddress, address, ctydebug.CmpOptions); diff != "" { + t.Fatalf("mismatch of address: %s", diff) + } + }) + } +} diff --git a/reference/targets.go b/reference/targets.go index 07d32da4..4d2e4ecb 100644 --- a/reference/targets.go +++ b/reference/targets.go @@ -87,6 +87,11 @@ func (refs Targets) MatchWalk(ctx context.Context, te schema.TraversalExpr, pref func localTargetMatches(ctx context.Context, target Target, te schema.TraversalExpr, prefix string, outermostBodyRng, originRng hcl.Range) bool { if len(target.LocalAddr) > 0 && strings.HasPrefix(target.LocalAddr.String(), prefix) { + // reject self references if not enabled + if !schema.ActiveSelfRefsFromContext(ctx) && target.LocalAddr[0].String() == "self" { + return false + } + hasNestedMatches := target.NestedTargets.containsMatch(ctx, te, prefix, outermostBodyRng, originRng) // Avoid suggesting cyclical reference to the same attribute diff --git a/reference/targets_test.go b/reference/targets_test.go index 72e9b6e3..5931784a 100644 --- a/reference/targets_test.go +++ b/reference/targets_test.go @@ -881,6 +881,7 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { prefix string outermostBodyRng hcl.Range originRng hcl.Range + activeSelfRefs bool expectedTargets Targets }{ { @@ -898,6 +899,7 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { Start: hcl.InitialPos, End: hcl.InitialPos, }, + false, Targets{}, }, { @@ -928,6 +930,7 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { Start: hcl.InitialPos, End: hcl.InitialPos, }, + false, Targets{ { LocalAddr: lang.Address{ @@ -978,6 +981,7 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { Start: hcl.InitialPos, End: hcl.InitialPos, }, + false, Targets{ { LocalAddr: lang.Address{ @@ -1044,6 +1048,7 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { Start: hcl.Pos{Line: 5, Column: 1, Byte: 25}, End: hcl.Pos{Line: 5, Column: 10, Byte: 35}, }, + false, Targets{ { LocalAddr: lang.Address{ @@ -1099,6 +1104,7 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { Start: hcl.Pos{Line: 2, Column: 11, Byte: 38}, End: hcl.Pos{Line: 2, Column: 11, Byte: 38}, }, + false, Targets{}, }, { @@ -1158,6 +1164,172 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { Start: hcl.Pos{Line: 2, Column: 9, Byte: 36}, End: hcl.Pos{Line: 2, Column: 9, Byte: 36}, }, + true, + Targets{}, + }, + { + "self only matches when enabled", + Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_alb"}, + lang.AttrStep{Name: "test"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + NestedTargets: Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_alb"}, + lang.AttrStep{Name: "test"}, + lang.AttrStep{Name: "bar"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 30}, + End: hcl.Pos{Line: 2, Column: 20, Byte: 35}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + }, + }, + }, + }, + schema.TraversalExpr{}, + "", + hcl.Range{ // outermost body range + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 37}, + }, + hcl.Range{ // origin range + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 2, Byte: 36}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 36}, + }, + true, + Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_alb"}, + lang.AttrStep{Name: "test"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + NestedTargets: Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_alb"}, + lang.AttrStep{Name: "test"}, + lang.AttrStep{Name: "bar"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 30}, + End: hcl.Pos{Line: 2, Column: 20, Byte: 35}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + }, + }, + }, + }, + }, + { + "self doesn't match when disabled", + Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_alb"}, + lang.AttrStep{Name: "test"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + NestedTargets: Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws_alb"}, + lang.AttrStep{Name: "test"}, + lang.AttrStep{Name: "bar"}, + }, + LocalAddr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 30}, + End: hcl.Pos{Line: 2, Column: 20, Byte: 35}, + }, + TargetableFromRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 37}, + }, + }, + }, + }, + }, + schema.TraversalExpr{}, + "", + hcl.Range{ // outermost body range + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 37}, + }, + hcl.Range{ // origin range + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 2, Byte: 36}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 36}, + }, + false, Targets{}, }, } @@ -1166,6 +1338,9 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { targets := make(Targets, 0) ctx := context.Background() + if tc.activeSelfRefs { + ctx = schema.WithActiveSelfRefs(ctx) + } tc.targets.MatchWalk(ctx, tc.traversalConst, tc.prefix, tc.outermostBodyRng, tc.originRng, func(t Target) error { targets = append(targets, t) return nil diff --git a/reference/traversal.go b/reference/traversal.go index 21207429..5fad7133 100644 --- a/reference/traversal.go +++ b/reference/traversal.go @@ -6,9 +6,16 @@ import ( "github.com/hashicorp/hcl/v2" ) -func TraversalsToLocalOrigins(traversals []hcl.Traversal, tes schema.TraversalExprs) Origins { +func TraversalsToLocalOrigins(traversals []hcl.Traversal, tes schema.TraversalExprs, allowSelfRefs bool) Origins { origins := make(Origins, 0) for _, traversal := range traversals { + // traversal should not be relative here, since the input to this + // function `expr.Variables()` only returns absolute traversals + if !traversal.IsRelative() && traversal.RootName() == "self" && !allowSelfRefs { + // Only if a block allows the usage of self.* we create a origin, + // else just continue + continue + } origin, err := TraversalToLocalOrigin(traversal, tes) if err != nil { continue diff --git a/reference/traversal_test.go b/reference/traversal_test.go index 1d073a01..08c07675 100644 --- a/reference/traversal_test.go +++ b/reference/traversal_test.go @@ -98,3 +98,81 @@ func TestTraversalToOrigin(t *testing.T) { }) } } + +func TestTraversalsToOrigin(t *testing.T) { + testCases := []struct { + testName string + rawTraversals []string + traversalExprs schema.TraversalExprs + allowSelfRefs bool + expectedOrigins Origins + }{ + { + "origin collection without self refs", + []string{"foo.bar", "self.bar"}, + schema.TraversalExprs{}, + false, + Origins{ + LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + lang.AttrStep{Name: "bar"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + { + "origin collection with self refs", + []string{"foo.bar", "self.bar"}, + schema.TraversalExprs{}, + true, + Origins{ + LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + lang.AttrStep{Name: "bar"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "self"}, + lang.AttrStep{Name: "bar"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + traversals := make([]hcl.Traversal, 0) + for _, rawTraversal := range tc.rawTraversals { + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(rawTraversal), "test.tf", hcl.InitialPos) + if len(diags) > 0 { + t.Fatal(diags) + } + traversals = append(traversals, traversal) + } + + origins := TraversalsToLocalOrigins(traversals, tc.traversalExprs, tc.allowSelfRefs) + if diff := cmp.Diff(tc.expectedOrigins, origins, ctydebug.CmpOptions); diff != "" { + t.Fatalf("origin mismatch: %s", diff) + } + }) + } +} diff --git a/schema/block_schema.go b/schema/block_schema.go index 2dcbbea5..6dcac56c 100644 --- a/schema/block_schema.go +++ b/schema/block_schema.go @@ -69,6 +69,16 @@ type BlockAddrSchema struct { // blocks and attributes are also walked // and their addresses inferred as data InferDependentBody bool + + // DependentBodySelfRef instructs collection of reference + // targets with an additional self.* LocalAddr and + // makes those targetable by origins within the block body + // via reference.Target.TargetableFromRangePtr. + // + // The targetting (matching w/ origins) is further limited by + // BodySchema.Extensions.SelfRef, where only self.* origins + // within a body w/ SelfRef:true will be collected. + DependentBodySelfRef bool } type BlockAsTypeOf struct { @@ -99,6 +109,10 @@ func (bas *BlockAddrSchema) Validate() error { return errors.New("InferDependentBody requires DependentBodyAsData") } + if bas.DependentBodySelfRef && !bas.InferDependentBody { + return errors.New("DependentBodySelfRef requires InferDependentBody") + } + return nil } @@ -108,15 +122,16 @@ func (bas *BlockAddrSchema) Copy() *BlockAddrSchema { } newBas := &BlockAddrSchema{ - FriendlyName: bas.FriendlyName, - ScopeId: bas.ScopeId, - AsReference: bas.AsReference, - AsTypeOf: bas.AsTypeOf.Copy(), - BodyAsData: bas.BodyAsData, - InferBody: bas.InferBody, - DependentBodyAsData: bas.DependentBodyAsData, - InferDependentBody: bas.InferDependentBody, - Steps: bas.Steps.Copy(), + FriendlyName: bas.FriendlyName, + ScopeId: bas.ScopeId, + AsReference: bas.AsReference, + AsTypeOf: bas.AsTypeOf.Copy(), + BodyAsData: bas.BodyAsData, + InferBody: bas.InferBody, + DependentBodyAsData: bas.DependentBodyAsData, + InferDependentBody: bas.InferDependentBody, + DependentBodySelfRef: bas.DependentBodySelfRef, + Steps: bas.Steps.Copy(), } return newBas diff --git a/schema/body_schema.go b/schema/body_schema.go index 129a3356..f972da2a 100644 --- a/schema/body_schema.go +++ b/schema/body_schema.go @@ -54,6 +54,7 @@ type BodyExtensions struct { Count bool // count attribute + count.index refs ForEach bool // for_each attribute + each.* refs DynamicBlocks bool // dynamic "block-name" w/ content & for_each inside + SelfRefs bool // self.* refs } func (be *BodyExtensions) Copy() *BodyExtensions { @@ -65,6 +66,7 @@ func (be *BodyExtensions) Copy() *BodyExtensions { Count: be.Count, ForEach: be.ForEach, DynamicBlocks: be.DynamicBlocks, + SelfRefs: be.SelfRefs, } } diff --git a/schema/schema.go b/schema/schema.go index d2d8c840..6109f7c3 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -33,6 +33,16 @@ func ActiveForEachFromContext(ctx context.Context) bool { return ctx.Value(bodyActiveForEachCtxKey{}) != nil } +type bodyActiveSelfRefsCtxKey struct{} + +func WithActiveSelfRefs(ctx context.Context) context.Context { + return context.WithValue(ctx, bodyActiveSelfRefsCtxKey{}, true) +} + +func ActiveSelfRefsFromContext(ctx context.Context) bool { + return ctx.Value(bodyActiveSelfRefsCtxKey{}) != nil +} + type bodyActiveDynamicBlockCtxKey struct{} func WithActiveDynamicBlock(ctx context.Context) context.Context {