Skip to content

Commit

Permalink
reference: refactor & fix matching (cyclical refs)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Nov 21, 2022
1 parent 898a478 commit ac45ab4
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 26 deletions.
6 changes: 0 additions & 6 deletions decoder/expression_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,12 +479,6 @@ 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 {
// avoid suggesting references to block's own fields from within (for now)
// TODO: Reflect LocalAddr here
if referenceTargetIsInRange(target, outermostBodyRng) {
return nil
}

address := target.Address(ctx).String()

candidates = append(candidates, lang.Candidate{
Expand Down
91 changes: 71 additions & 20 deletions reference/targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,39 +75,90 @@ func (w refTargetDeepWalker) walk(refTargets Targets) {

func (refs Targets) MatchWalk(ctx context.Context, te schema.TraversalExpr, prefix string, outermostBodyRng, originRng hcl.Range, f TargetWalkFunc) {
for _, ref := range refs {
if len(ref.LocalAddr) > 0 && strings.HasPrefix(ref.LocalAddr.String(), prefix) {
// Check if origin is inside the targetable range
if ref.TargetableFromRangePtr == nil || rangeOverlaps(*ref.TargetableFromRangePtr, originRng) {
nestedMatches := ref.NestedTargets.containsMatch(te, prefix)
if ref.MatchesConstraint(te) || nestedMatches {
f(ref)
}
}
if localTargetMatches(ctx, ref, te, prefix, outermostBodyRng, originRng) ||
absTargetMatches(ctx, ref, te, prefix, outermostBodyRng, originRng) {
f(ref)
continue
}
if len(ref.Addr) > 0 && strings.HasPrefix(ref.Addr.String(), prefix) {
nestedMatches := ref.NestedTargets.containsMatch(te, prefix)
if ref.MatchesConstraint(te) || nestedMatches {
f(ref)
continue

ref.NestedTargets.MatchWalk(ctx, te, prefix, outermostBodyRng, originRng, f)
}
}

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) {
hasNestedMatches := target.NestedTargets.containsMatch(ctx, te, prefix, outermostBodyRng, originRng)

// Avoid suggesting cyclical reference to the same attribute
// unless it has nested matches - i.e. still consider reference
// to the outside block/body as valid.
//
// For example, block { foo = self } where "self" refers to the "block"
// is considered valid. The use case this is important for is
// Terraform's self references inside nested block such as "connection".
if target.RangePtr != nil && !hasNestedMatches {
if rangeOverlaps(*target.RangePtr, originRng) {
return false
}
// We compare line in case the (incomplete) attribute
// ends w/ whitespace which wouldn't be included in the range
if target.RangePtr.Filename == originRng.Filename &&
target.RangePtr.End.Line == originRng.Start.Line {
return false
}
}

ref.NestedTargets.MatchWalk(ctx, te, prefix, outermostBodyRng, originRng, f)
// Reject origins which are outside the targetable range
if target.TargetableFromRangePtr != nil && !rangeOverlaps(*target.TargetableFromRangePtr, originRng) {
return false
}

if target.MatchesConstraint(te) || hasNestedMatches {
return true
}
}

return false
}

func absTargetMatches(ctx context.Context, target Target, te schema.TraversalExpr, prefix string, outermostBodyRng, originRng hcl.Range) bool {
if len(target.Addr) > 0 && strings.HasPrefix(target.Addr.String(), prefix) {
// Reject references to block's own fields from within the body
if referenceTargetIsInRange(target, outermostBodyRng) {
return false
}

if target.MatchesConstraint(te) || target.NestedTargets.containsMatch(ctx, te, prefix, outermostBodyRng, originRng) {
return true
}
}
return false
}

func referenceTargetIsInRange(target Target, bodyRange hcl.Range) bool {
return target.RangePtr != nil &&
bodyRange.Filename == target.RangePtr.Filename &&
(bodyRange.ContainsPos(target.RangePtr.Start) ||
posEqual(bodyRange.End, target.RangePtr.End))
}

func (refs Targets) containsMatch(te schema.TraversalExpr, prefix string) bool {
func posEqual(pos, other hcl.Pos) bool {
return pos.Line == other.Line &&
pos.Column == other.Column &&
pos.Byte == other.Byte
}

func (refs Targets) containsMatch(ctx context.Context, te schema.TraversalExpr, prefix string, outermostBodyRng, originRng hcl.Range) bool {
for _, ref := range refs {
if strings.HasPrefix(ref.LocalAddr.String(), prefix) &&
ref.MatchesConstraint(te) {
if localTargetMatches(ctx, ref, te, prefix, outermostBodyRng, originRng) {
return true
}
if strings.HasPrefix(ref.Addr.String(), prefix) &&
ref.MatchesConstraint(te) {
if absTargetMatches(ctx, ref, te, prefix, outermostBodyRng, originRng) {
return true
}

if len(ref.NestedTargets) > 0 {
if match := ref.NestedTargets.containsMatch(te, prefix); match {
if match := ref.NestedTargets.containsMatch(ctx, te, prefix, outermostBodyRng, originRng); match {
return true
}
}
Expand Down
102 changes: 102 additions & 0 deletions reference/targets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,108 @@ func TestTargets_MatchWalk_localRefs(t *testing.T) {
},
},
},
{
// ensure that e.g. count = count.index is mismatched, e.g. in
// resource "aws_alb" "test" {
// count = count.index
// }
"target pointing to the same attribute as origin",
Targets{
{
LocalAddr: lang.Address{
lang.RootStep{Name: "count"},
lang.AttrStep{Name: "index"},
},
DefRangePtr: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 3, Byte: 30},
End: hcl.Pos{Line: 2, Column: 8, Byte: 35},
},
RangePtr: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 3, Byte: 30},
End: hcl.Pos{Line: 2, Column: 10, Byte: 37},
},
TargetableFromRangePtr: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 28, Byte: 27},
End: hcl.Pos{Line: 3, Column: 1, Byte: 39},
},
},
},
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: 39},
},
hcl.Range{ // origin range
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 11, Byte: 38},
End: hcl.Pos{Line: 2, Column: 11, Byte: 38},
},
Targets{},
},
{
// ensure that e.g. foo = self is matched, e.g. in
// resource "aws_alb" "test" {
// foo = self
// }
"target pointing to outside body of an origin",
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: 3, Column: 1, Byte: 37},
},
TargetableFromRangePtr: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 28, Byte: 27},
End: hcl.Pos{Line: 3, Column: 1, Byte: 37},
},
NestedTargets: Targets{
{
LocalAddr: lang.Address{
lang.RootStep{Name: "self"},
lang.AttrStep{Name: "bar"},
},
RangePtr: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 28, Byte: 27},
End: hcl.Pos{Line: 3, Column: 1, Byte: 37},
},
TargetableFromRangePtr: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 28, Byte: 27},
End: hcl.Pos{Line: 3, 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: 2, Column: 9, Byte: 36},
End: hcl.Pos{Line: 2, Column: 9, Byte: 36},
},
Targets{},
},
}

for i, tc := range testCases {
Expand Down

0 comments on commit ac45ab4

Please sign in to comment.