Skip to content

Commit 628a057

Browse files
authored
Add support for local reference targets (#150)
1 parent ea0bcc6 commit 628a057

File tree

9 files changed

+437
-43
lines changed

9 files changed

+437
-43
lines changed

decoder/expression_candidates.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -478,22 +478,23 @@ func (d *PathDecoder) candidatesForTraversalConstraint(tc schema.TraversalExpr,
478478

479479
prefix, _ := d.bytesFromRange(prefixRng)
480480

481-
d.pathCtx.ReferenceTargets.MatchWalk(tc, string(prefix), func(ref reference.Target) error {
481+
d.pathCtx.ReferenceTargets.MatchWalk(tc, string(prefix), editRng, func(target reference.Target) error {
482482
// avoid suggesting references to block's own fields from within (for now)
483-
if ref.RangePtr != nil && outerBodyRng.Filename == ref.RangePtr.Filename &&
484-
(outerBodyRng.ContainsPos(ref.RangePtr.Start) ||
485-
posEqual(outerBodyRng.End, ref.RangePtr.End)) {
483+
// TODO: Reflect LocalAddr here
484+
if referenceTargetIsInRange(target, outerBodyRng) {
486485
return nil
487486
}
488487

488+
address := target.Address().String()
489+
489490
candidates = append(candidates, lang.Candidate{
490-
Label: ref.Addr.String(),
491-
Detail: ref.FriendlyName(),
492-
Description: ref.Description,
491+
Label: address,
492+
Detail: target.FriendlyName(),
493+
Description: target.Description,
493494
Kind: lang.TraversalCandidateKind,
494495
TextEdit: lang.TextEdit{
495-
NewText: ref.Addr.String(),
496-
Snippet: ref.Addr.String(),
496+
NewText: address,
497+
Snippet: address,
497498
Range: editRng,
498499
},
499500
})
@@ -503,6 +504,13 @@ func (d *PathDecoder) candidatesForTraversalConstraint(tc schema.TraversalExpr,
503504
return candidates
504505
}
505506

507+
func referenceTargetIsInRange(target reference.Target, bodyRange hcl.Range) bool {
508+
return target.RangePtr != nil &&
509+
bodyRange.Filename == target.RangePtr.Filename &&
510+
(bodyRange.ContainsPos(target.RangePtr.Start) ||
511+
posEqual(bodyRange.End, target.RangePtr.End))
512+
}
513+
506514
func newTextForConstraints(cons schema.ExprConstraints, isNested bool) string {
507515
for _, constraint := range cons {
508516
switch c := constraint.(type) {

decoder/hover.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ func (d *PathDecoder) hoverContentForTraversalExpr(traversal hcl.Traversal, tes
638638
return "", nil
639639
}
640640

641-
targets, ok := d.pathCtx.ReferenceTargets.Match(origin.Address(), origin.OriginConstraints())
641+
targets, ok := d.pathCtx.ReferenceTargets.Match(origin)
642642
if !ok {
643643
return "", &reference.NoTargetFound{}
644644
}
@@ -648,7 +648,7 @@ func (d *PathDecoder) hoverContentForTraversalExpr(traversal hcl.Traversal, tes
648648
}
649649

650650
func hoverContentForReferenceTarget(ref reference.Target) (string, error) {
651-
content := fmt.Sprintf("`%s`", ref.Addr.String())
651+
content := fmt.Sprintf("`%s`", ref.Address())
652652

653653
var friendlyName string
654654
if ref.Type != cty.NilType {

decoder/reference_targets.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (d *Decoder) ReferenceTargetsForOriginAtPos(path lang.Path, file string, po
5555
if !ok {
5656
continue
5757
}
58-
targets, ok := targetCtx.ReferenceTargets.Match(matchableOrigin.Address(), matchableOrigin.OriginConstraints())
58+
targets, ok := targetCtx.ReferenceTargets.Match(matchableOrigin)
5959
if !ok {
6060
// target not found
6161
continue

decoder/semantic_tokens.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func (d *PathDecoder) tokensForExpression(ctx context.Context, expr hclsyntax.Ex
207207
return tokens
208208
}
209209

210-
_, targetFound := d.pathCtx.ReferenceTargets.Match(origin.Address(), origin.OriginConstraints())
210+
_, targetFound := d.pathCtx.ReferenceTargets.Match(origin)
211211
if !targetFound {
212212
return tokens
213213
}

lang/address.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import (
99
type Address []AddressStep
1010

1111
func (a Address) Equals(addr Address) bool {
12+
// Empty address may come up in context where there are
13+
// two addresses for the same target and only is declared
14+
// (LocalAddr / Addr) in which case we don't want the empty
15+
// one to be treated as a match.
16+
if len(a) == 0 && len(addr) == 0 {
17+
return false
18+
}
1219
if len(a) != len(addr) {
1320
return false
1421
}

reference/origins.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ func (ro Origins) Match(localPath lang.Path, target Target, targetPath lang.Path
3737
for _, refOrigin := range ro {
3838
switch origin := refOrigin.(type) {
3939
case LocalOrigin:
40-
if localPath.Equals(targetPath) && target.Matches(origin.Address(), origin.OriginConstraints()) {
40+
if localPath.Equals(targetPath) && target.Matches(origin) {
4141
origins = append(origins, refOrigin)
4242
}
4343
case PathOrigin:
44-
if origin.TargetPath.Equals(targetPath) && target.Matches(origin.Address(), origin.OriginConstraints()) {
44+
if origin.TargetPath.Equals(targetPath) && target.Matches(origin) {
4545
origins = append(origins, refOrigin)
4646
}
4747
}

reference/target.go

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,28 @@ import (
88
)
99

1010
type Target struct {
11-
Addr lang.Address
11+
// Addr represents the address of the target, as available
12+
// elsewhere in the configuration
13+
Addr lang.Address
14+
15+
// LocalAddr represents the address of the target
16+
// as available *locally* (e.g. self.attr_name)
17+
LocalAddr lang.Address
18+
19+
// TargetableFromRangePtr defines where the target is targetable from.
20+
// This is considered when matching the target against origin.
21+
//
22+
// e.g. count.index is only available within the body of the block
23+
// where count is declared (and extension enabled)
24+
TargetableFromRangePtr *hcl.Range
25+
26+
// ScopeId provides scope for matching/filtering
27+
// (in addition to Type & Addr/LocalAddr).
28+
//
29+
// There should never be two targets with the same Type & address,
30+
// but there are contexts (e.g. completion) where we don't filter
31+
// by address and may not have type either (e.g. because targets
32+
// are type-unaware).
1233
ScopeId lang.ScopeId
1334

1435
// RangePtr represents range of the whole attribute or block
@@ -31,16 +52,38 @@ type Target struct {
3152
NestedTargets Targets
3253
}
3354

55+
// rangeOverlaps is a copy of hcl.Range.Overlaps
56+
// https://github.com/hashicorp/hcl/blob/v2.14.1/pos.go#L195-L212
57+
// which accounts for empty ranges that are common in the context of LS
58+
func rangeOverlaps(one, other hcl.Range) bool {
59+
switch {
60+
case one.Filename != other.Filename:
61+
// If the ranges are in different files then they can't possibly overlap
62+
return false
63+
case one.Empty() && other.Empty():
64+
// Empty ranges can never overlap
65+
return false
66+
case one.ContainsOffset(other.Start.Byte) || one.ContainsOffset(other.End.Byte):
67+
return true
68+
case other.ContainsOffset(one.Start.Byte) || other.ContainsOffset(one.End.Byte):
69+
return true
70+
default:
71+
return false
72+
}
73+
}
74+
3475
func (ref Target) Copy() Target {
3576
return Target{
36-
Addr: ref.Addr,
37-
ScopeId: ref.ScopeId,
38-
RangePtr: copyHclRangePtr(ref.RangePtr),
39-
DefRangePtr: copyHclRangePtr(ref.DefRangePtr),
40-
Type: ref.Type, // cty.Type is immutable by design
41-
Name: ref.Name,
42-
Description: ref.Description,
43-
NestedTargets: ref.NestedTargets.Copy(),
77+
Addr: ref.Addr,
78+
LocalAddr: ref.LocalAddr,
79+
TargetableFromRangePtr: copyHclRangePtr(ref.TargetableFromRangePtr),
80+
ScopeId: ref.ScopeId,
81+
RangePtr: copyHclRangePtr(ref.RangePtr),
82+
DefRangePtr: copyHclRangePtr(ref.DefRangePtr),
83+
Type: ref.Type, // cty.Type is immutable by design
84+
Name: ref.Name,
85+
Description: ref.Description,
86+
NestedTargets: ref.NestedTargets.Copy(),
4487
}
4588
}
4689

@@ -51,8 +94,16 @@ func copyHclRangePtr(rng *hcl.Range) *hcl.Range {
5194
return rng.Ptr()
5295
}
5396

97+
// Address returns any of the two non-empty addresses
98+
//
99+
// TODO: Return address based on context when we have both
54100
func (r Target) Address() lang.Address {
55-
return r.Addr
101+
addr := r.Addr
102+
if len(r.LocalAddr) > 0 {
103+
addr = r.LocalAddr
104+
}
105+
106+
return addr
56107
}
57108

58109
func (r Target) FriendlyName() string {
@@ -98,26 +149,32 @@ func (ref Target) ConformsToType(typ cty.Type) bool {
98149
return conformsToType || (typ == cty.NilType && ref.Type == cty.NilType)
99150
}
100151

101-
func (target Target) Matches(addr lang.Address, cons OriginConstraints) bool {
102-
if len(target.Addr) > len(addr) {
152+
func (target Target) Matches(origin MatchableOrigin) bool {
153+
if len(target.LocalAddr) > len(origin.Address()) && len(target.Addr) > len(origin.Address()) {
103154
return false
104155
}
105156

106-
originAddr := addr
157+
originAddr, localOriginAddr := origin.Address(), origin.Address()
107158

108159
matchesCons := false
109160

110-
if len(cons) == 0 && target.Type != cty.NilType {
111-
matchesCons = true
161+
// Unconstrained origins should be uncommon, but they match any target
162+
if len(origin.OriginConstraints()) == 0 {
163+
// As long as the target is type-aware. Type-unaware targets
164+
// generally don't have Type, so we avoid false positive here.
165+
if target.Type != cty.NilType {
166+
matchesCons = true
167+
}
112168
}
113169

114-
for _, cons := range cons {
170+
for _, cons := range origin.OriginConstraints() {
115171
if !target.MatchesScopeId(cons.OfScopeId) {
116172
continue
117173
}
118174

119175
if target.Type == cty.DynamicPseudoType {
120-
originAddr = addr.FirstSteps(uint(len(target.Addr)))
176+
originAddr = origin.Address().FirstSteps(uint(len(target.Addr)))
177+
localOriginAddr = origin.Address().FirstSteps(uint(len(target.LocalAddr)))
121178
matchesCons = true
122179
continue
123180
}
@@ -130,5 +187,10 @@ func (target Target) Matches(addr lang.Address, cons OriginConstraints) bool {
130187
}
131188
}
132189

133-
return target.Addr.Equals(originAddr) && matchesCons
190+
// Reject origin if it's outside the targetable range
191+
if target.TargetableFromRangePtr != nil && !rangeOverlaps(*target.TargetableFromRangePtr, origin.OriginRange()) {
192+
return false
193+
}
194+
195+
return (target.LocalAddr.Equals(localOriginAddr) || target.Addr.Equals(originAddr)) && matchesCons
134196
}

reference/targets.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"errors"
55
"strings"
66

7-
"github.com/hashicorp/hcl-lang/lang"
87
"github.com/hashicorp/hcl-lang/schema"
98
"github.com/hashicorp/hcl/v2"
109
)
@@ -29,7 +28,8 @@ func (r Targets) Len() int {
2928
}
3029

3130
func (r Targets) Less(i, j int) bool {
32-
return r[i].Addr.String() < r[j].Addr.String()
31+
return r[i].LocalAddr.String() < r[j].LocalAddr.String() ||
32+
r[i].Addr.String() < r[j].Addr.String()
3333
}
3434

3535
func (r Targets) Swap(i, j int) {
@@ -72,23 +72,36 @@ func (w refTargetDeepWalker) walk(refTargets Targets) {
7272
}
7373
}
7474

75-
func (refs Targets) MatchWalk(te schema.TraversalExpr, prefix string, f TargetWalkFunc) {
75+
func (refs Targets) MatchWalk(te schema.TraversalExpr, prefix string, originRng hcl.Range, f TargetWalkFunc) {
7676
for _, ref := range refs {
77-
if strings.HasPrefix(ref.Addr.String(), string(prefix)) {
77+
if len(ref.LocalAddr) > 0 && strings.HasPrefix(ref.LocalAddr.String(), prefix) {
78+
// Check if origin is inside the targetable range
79+
if ref.TargetableFromRangePtr == nil || rangeOverlaps(*ref.TargetableFromRangePtr, originRng) {
80+
nestedMatches := ref.NestedTargets.containsMatch(te, prefix)
81+
if ref.MatchesConstraint(te) || nestedMatches {
82+
f(ref)
83+
}
84+
}
85+
}
86+
if len(ref.Addr) > 0 && strings.HasPrefix(ref.Addr.String(), prefix) {
7887
nestedMatches := ref.NestedTargets.containsMatch(te, prefix)
7988
if ref.MatchesConstraint(te) || nestedMatches {
8089
f(ref)
8190
continue
8291
}
8392
}
8493

85-
ref.NestedTargets.MatchWalk(te, prefix, f)
94+
ref.NestedTargets.MatchWalk(te, prefix, originRng, f)
8695
}
8796
}
8897

8998
func (refs Targets) containsMatch(te schema.TraversalExpr, prefix string) bool {
9099
for _, ref := range refs {
91-
if strings.HasPrefix(ref.Addr.String(), string(prefix)) &&
100+
if strings.HasPrefix(ref.LocalAddr.String(), prefix) &&
101+
ref.MatchesConstraint(te) {
102+
return true
103+
}
104+
if strings.HasPrefix(ref.Addr.String(), prefix) &&
92105
ref.MatchesConstraint(te) {
93106
return true
94107
}
@@ -101,13 +114,14 @@ func (refs Targets) containsMatch(te schema.TraversalExpr, prefix string) bool {
101114
return false
102115
}
103116

104-
func (refs Targets) Match(addr lang.Address, cons OriginConstraints) (Targets, bool) {
117+
func (refs Targets) Match(origin MatchableOrigin) (Targets, bool) {
105118
matchingReferences := make(Targets, 0)
106119

107120
refs.deepWalk(func(ref Target) error {
108-
if ref.Matches(addr, cons) {
121+
if ref.Matches(origin) {
109122
matchingReferences = append(matchingReferences, ref)
110123
}
124+
111125
return nil
112126
}, InfiniteDepth)
113127

0 commit comments

Comments
 (0)