Skip to content

Commit a064794

Browse files
committed
Revert "chore: defer dependency-label support to a follow-up PR"
This reverts commit cecfe0e.
1 parent 1e4a36a commit a064794

6 files changed

Lines changed: 288 additions & 40 deletions

File tree

internal/tg/definition/definition.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,11 @@ func traversalDefinitionTarget(expr *hclsyntax.ScopeTraversalExpr) (string, stri
8383
return "", "", false
8484
}
8585

86-
if rootStep.Name == "local" {
86+
switch rootStep.Name {
87+
case "local":
8788
return attrStep.Name, DefinitionContextLocal, true
89+
case "dependency":
90+
return attrStep.Name, DefinitionContextDependency, true
8891
}
8992

9093
return "", "", false

internal/tg/references/references_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,28 @@ inputs = {
7171
assert.Nil(t, locs)
7272
})
7373
}
74+
75+
func TestGetReferences_DependencyLabel(t *testing.T) {
76+
t.Parallel()
77+
78+
tmpDir := t.TempDir()
79+
content := `dependency "vpc" {
80+
config_path = "../vpc"
81+
}
82+
83+
inputs = {
84+
vpc_id = dependency.vpc.outputs.id
85+
}
86+
`
87+
_, err := testutils.CreateFile(tmpDir, "terragrunt.hcl", content)
88+
require.NoError(t, err)
89+
90+
tgPath := filepath.Join(tmpDir, "terragrunt.hcl")
91+
l := testutils.NewTestLogger(t)
92+
s := tg.NewState()
93+
s.OpenDocument(t.Context(), l, uri.File(tgPath), content)
94+
95+
// Cursor on the reference (`vpc` in dependency.vpc.outputs.id).
96+
locs := references.GetReferences(l, s.Configs[tgPath], protocol.Position{Line: 5, Character: 23}, tgPath, s.Configs, true)
97+
require.Len(t, locs, 2, "label definition + outputs reference")
98+
}

internal/tg/rename/rename.go

Lines changed: 125 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const (
2222
// (`locals { name = ... }` and `local.name` references).
2323
RenameContextLocal = "local"
2424

25+
// RenameContextDependency is the context for renaming a dependency block
26+
// label (`dependency "name" {}` and `dependency.name.outputs.X` references).
27+
RenameContextDependency = "dependency"
28+
2529
// RenameContextNull means the cursor is not on a renameable identifier.
2630
RenameContextNull = "null"
2731
)
@@ -33,7 +37,8 @@ type RenameTarget struct {
3337
// Context is one of the RenameContext* constants.
3438
Context string
3539
// IdentRange is the LSP range covering only the identifier token, suitable
36-
// for use as the prepare-rename range.
40+
// for use as the prepare-rename range. For block labels this excludes the
41+
// surrounding quotes.
3742
IdentRange protocol.Range
3843
}
3944

@@ -83,31 +88,37 @@ func GetRenameTarget(l logger.Logger, st store.Store, position protocol.Position
8388
}
8489

8590
for cur := inode; cur != nil; cur = cur.Parent {
86-
attr, ok := cur.Node.(*hclsyntax.Attribute)
87-
if !ok {
88-
continue
89-
}
90-
91-
if !ast.IsLocalAttribute(cur) {
91+
switch n := cur.Node.(type) {
92+
case *hclsyntax.Block:
93+
if t, ok := blockLabelTarget(n, position); ok {
94+
return t
95+
}
96+
// We hit an enclosing block but the cursor isn't on its label.
9297
return null
93-
}
9498

95-
if !rangeContainsPosition(attr.NameRange, position) {
96-
return null
97-
}
99+
case *hclsyntax.Attribute:
100+
if !ast.IsLocalAttribute(cur) {
101+
return null
102+
}
103+
104+
if !rangeContainsPosition(n.NameRange, position) {
105+
return null
106+
}
98107

99-
return RenameTarget{
100-
Name: attr.Name,
101-
Context: RenameContextLocal,
102-
IdentRange: ast.FromHCLRange(attr.NameRange),
108+
return RenameTarget{
109+
Name: n.Name,
110+
Context: RenameContextLocal,
111+
IdentRange: ast.FromHCLRange(n.NameRange),
112+
}
103113
}
104114
}
105115

106116
return null
107117
}
108118

109119
// traversalTarget extracts a RenameTarget from a ScopeTraversalExpr if the
110-
// cursor is positioned on its first two traversal steps and the root is `local`.
120+
// cursor is positioned on its first two traversal steps and the root is a
121+
// supported kind.
111122
func traversalTarget(expr *hclsyntax.ScopeTraversalExpr, position protocol.Position) RenameTarget {
112123
null := RenameTarget{Context: RenameContextNull}
113124

@@ -125,11 +136,8 @@ func traversalTarget(expr *hclsyntax.ScopeTraversalExpr, position protocol.Posit
125136
return null
126137
}
127138

128-
if rootStep.Name != "local" {
129-
return null
130-
}
131-
132-
// Restrict cursor to the first two steps.
139+
// Restrict cursor to the first two steps so that, e.g., `dependency.vpc.outputs.x`
140+
// only triggers rename when the cursor is on `dependency` or `vpc`.
133141
firstTwo := hcl.Range{
134142
Filename: rootStep.SrcRange.Filename,
135143
Start: rootStep.SrcRange.Start,
@@ -139,13 +147,56 @@ func traversalTarget(expr *hclsyntax.ScopeTraversalExpr, position protocol.Posit
139147
return null
140148
}
141149

150+
context, ok := contextForRoot(rootStep.Name)
151+
if !ok {
152+
return null
153+
}
154+
142155
return RenameTarget{
143156
Name: attrStep.Name,
144-
Context: RenameContextLocal,
157+
Context: context,
145158
IdentRange: ast.FromHCLRange(ast.TraverseAttrIdentRange(attrStep)),
146159
}
147160
}
148161

162+
// blockLabelTarget returns a RenameTarget when the cursor is on the first
163+
// label of an `include` or `dependency` block.
164+
func blockLabelTarget(block *hclsyntax.Block, position protocol.Position) (RenameTarget, bool) {
165+
null := RenameTarget{Context: RenameContextNull}
166+
167+
context, ok := contextForRoot(block.Type)
168+
if !ok || context == RenameContextLocal {
169+
return null, false
170+
}
171+
172+
if len(block.Labels) == 0 || len(block.LabelRanges) == 0 {
173+
return null, false
174+
}
175+
176+
if !rangeContainsPosition(block.LabelRanges[0], position) {
177+
return null, false
178+
}
179+
180+
return RenameTarget{
181+
Name: block.Labels[0],
182+
Context: context,
183+
IdentRange: labelInnerRange(block.LabelRanges[0]),
184+
}, true
185+
}
186+
187+
// contextForRoot maps an HCL root identifier (the first traversal step or a
188+
// block type) to a rename context. Returns false for unsupported names.
189+
func contextForRoot(name string) (string, bool) {
190+
switch name {
191+
case "local":
192+
return RenameContextLocal, true
193+
case "dependency":
194+
return RenameContextDependency, true
195+
}
196+
197+
return "", false
198+
}
199+
149200
// FindAllOccurrences returns every rename occurrence of target across all
150201
// sibling .hcl files in the same directory as originFile (including originFile
151202
// itself). When a file has an entry in configs with a parsed AST, that AST is
@@ -175,7 +226,7 @@ func FindAllOccurrences(l logger.Logger, target RenameTarget, originFile string,
175226
continue
176227
}
177228

178-
occurrences = append(occurrences, definitionOccurrences(target, file, iast)...)
229+
occurrences = append(occurrences, definitionOccurrences(target, file, iast, body)...)
179230

180231
ast.WalkReferences(body, target.Context, target.Name, func(_ *hclsyntax.ScopeTraversalExpr, r hcl.Range) {
181232
occurrences = append(occurrences, Occurrence{
@@ -204,26 +255,50 @@ func FindAllOccurrences(l logger.Logger, target RenameTarget, originFile string,
204255
}
205256

206257
// definitionOccurrences finds the definition site(s) of target in the given file.
207-
func definitionOccurrences(target RenameTarget, file string, iast *ast.IndexedAST) []Occurrence {
208-
if target.Context != RenameContextLocal {
209-
return nil
210-
}
258+
func definitionOccurrences(target RenameTarget, file string, iast *ast.IndexedAST, body *hclsyntax.Body) []Occurrence {
259+
var occs []Occurrence
211260

212-
def, ok := iast.Locals[target.Name]
213-
if !ok {
214-
return nil
215-
}
261+
switch target.Context {
262+
case RenameContextLocal:
263+
def, ok := iast.Locals[target.Name]
264+
if !ok {
265+
return nil
266+
}
216267

217-
attr, ok := def.Node.(*hclsyntax.Attribute)
218-
if !ok {
219-
return nil
268+
attr, ok := def.Node.(*hclsyntax.Attribute)
269+
if !ok {
270+
return nil
271+
}
272+
273+
occs = append(occs, Occurrence{
274+
File: file,
275+
Range: ast.FromHCLRange(attr.NameRange),
276+
IsDefinition: true,
277+
})
278+
279+
case RenameContextDependency:
280+
for _, blk := range body.Blocks {
281+
if blk.Type != target.Context {
282+
continue
283+
}
284+
285+
if len(blk.Labels) == 0 || blk.Labels[0] != target.Name {
286+
continue
287+
}
288+
289+
if len(blk.LabelRanges) == 0 {
290+
continue
291+
}
292+
293+
occs = append(occs, Occurrence{
294+
File: file,
295+
Range: labelInnerRange(blk.LabelRanges[0]),
296+
IsDefinition: true,
297+
})
298+
}
220299
}
221300

222-
return []Occurrence{{
223-
File: file,
224-
Range: ast.FromHCLRange(attr.NameRange),
225-
IsDefinition: true,
226-
}}
301+
return occs
227302
}
228303

229304
// siblingHCLFiles returns absolute paths of *.hcl files in dir, excluding
@@ -300,6 +375,17 @@ func getOrParseAST(path string, configs map[string]store.Store, l logger.Logger)
300375
return iast
301376
}
302377

378+
// labelInnerRange returns the LSP range of a quoted label's contents, excluding
379+
// the surrounding double quotes.
380+
func labelInnerRange(r hcl.Range) protocol.Range {
381+
r.Start.Column++
382+
r.Start.Byte++
383+
r.End.Column--
384+
r.End.Byte--
385+
386+
return ast.FromHCLRange(r)
387+
}
388+
303389
// rangeContainsPosition reports whether p (LSP coordinates) is inside r (HCL
304390
// coordinates). The end of an HCL range is exclusive.
305391
func rangeContainsPosition(r hcl.Range, p protocol.Position) bool {

internal/tg/rename/rename_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ inputs = {
8787
expectedName: "foo",
8888
expectedContext: rename.RenameContextLocal,
8989
},
90+
{
91+
name: "cursor on dependency label",
92+
document: `dependency "vpc" {
93+
config_path = "../vpc"
94+
}`,
95+
position: protocol.Position{Line: 0, Character: 13},
96+
expectedName: "vpc",
97+
expectedContext: rename.RenameContextDependency,
98+
},
99+
{
100+
name: "cursor on dependency outputs reference",
101+
document: `inputs = {
102+
id = dependency.vpc.outputs.id
103+
}`,
104+
position: protocol.Position{Line: 1, Character: 19},
105+
expectedName: "vpc",
106+
expectedContext: rename.RenameContextDependency,
107+
},
108+
{
109+
name: "cursor on outputs step is not renameable",
110+
document: `inputs = {
111+
id = dependency.vpc.outputs.id
112+
}`,
113+
position: protocol.Position{Line: 1, Character: 24},
114+
expectedContext: rename.RenameContextNull,
115+
},
90116
{
91117
name: "cursor on unrelated traversal root",
92118
document: `inputs = {
@@ -284,3 +310,34 @@ func TestFindAllOccurrences_PrefersInMemoryAST(t *testing.T) {
284310
occs := rename.FindAllOccurrences(l, target, tgPath, configs)
285311
require.Len(t, occs, 2, "definition (from in-memory common) + reference")
286312
}
313+
314+
func TestFindAllOccurrences_DependencyLabel(t *testing.T) {
315+
t.Parallel()
316+
317+
tmpDir := t.TempDir()
318+
319+
content := `dependency "vpc" {
320+
config_path = "../vpc"
321+
}
322+
323+
inputs = {
324+
vpc_id = dependency.vpc.outputs.id
325+
}
326+
`
327+
_, err := testutils.CreateFile(tmpDir, "terragrunt.hcl", content)
328+
require.NoError(t, err)
329+
330+
tgPath := filepath.Join(tmpDir, "terragrunt.hcl")
331+
332+
l := testutils.NewTestLogger(t)
333+
s := tg.NewState()
334+
s.OpenDocument(t.Context(), l, uri.File(tgPath), content)
335+
336+
// Cursor on the dependency label "vpc".
337+
target := rename.GetRenameTarget(l, s.Configs[tgPath], protocol.Position{Line: 0, Character: 13})
338+
require.Equal(t, rename.RenameContextDependency, target.Context)
339+
require.Equal(t, "vpc", target.Name)
340+
341+
occs := rename.FindAllOccurrences(l, target, tgPath, s.Configs)
342+
require.Len(t, occs, 2, "definition label + outputs reference")
343+
}

internal/tg/state_definition_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tg_test
22

33
import (
4+
"os"
45
"path/filepath"
56
"testing"
67

@@ -107,3 +108,39 @@ func TestState_Definition_LocalReference_NotFound(t *testing.T) {
107108
assert.Equal(t, docURI, resp.Result.URI)
108109
assert.Equal(t, protocol.Position{Line: 1, Character: 18}, resp.Result.Range.Start)
109110
}
111+
112+
func TestState_Definition_DependencyTraversalReference(t *testing.T) {
113+
t.Parallel()
114+
115+
tmpDir := t.TempDir()
116+
117+
vpcDir := filepath.Join(tmpDir, "vpc")
118+
require.NoError(t, os.MkdirAll(vpcDir, 0o755))
119+
_, err := testutils.CreateFile(vpcDir, "terragrunt.hcl", "")
120+
require.NoError(t, err)
121+
122+
unitDir := filepath.Join(tmpDir, "app")
123+
require.NoError(t, os.MkdirAll(unitDir, 0o755))
124+
125+
content := `dependency "vpc" {
126+
config_path = "../vpc"
127+
}
128+
129+
inputs = {
130+
vpc_id = dependency.vpc.outputs.id
131+
}
132+
`
133+
unitPath := filepath.Join(unitDir, "terragrunt.hcl")
134+
_, err = testutils.CreateFile(unitDir, "terragrunt.hcl", content)
135+
require.NoError(t, err)
136+
137+
l := testutils.NewTestLogger(t)
138+
s := tg.NewState()
139+
s.OpenDocument(t.Context(), l, uri.File(unitPath), content)
140+
141+
// Cursor on `vpc` in `dependency.vpc.outputs.id`.
142+
resp := s.Definition(l, 1, uri.File(unitPath), protocol.Position{Line: 5, Character: 23})
143+
144+
expectedURI := uri.File(filepath.Join(vpcDir, "terragrunt.hcl"))
145+
assert.Equal(t, expectedURI, resp.Result.URI, "should jump to dependent unit's terragrunt.hcl")
146+
}

0 commit comments

Comments
 (0)