Skip to content

Commit 8ba4d45

Browse files
feat: Find Usages/References (#204)
* feat: Find Usages/References Uses Tanka to find the importers (`tk tool importers`), then runs through those files to find usages * lint * Update Tanka
1 parent c33dc2d commit 8ba4d45

File tree

7 files changed

+385
-18
lines changed

7 files changed

+385
-18
lines changed

go.mod

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
module github.com/grafana/jsonnet-language-server
22

3-
go 1.23.0
3+
go 1.24.0
44

5-
toolchain go1.23.2
5+
toolchain go1.24.2
66

77
require (
88
github.com/JohannesKaufmann/html-to-markdown v1.6.0
9-
github.com/google/go-jsonnet v0.20.0
10-
github.com/grafana/tanka v0.32.0
9+
github.com/google/go-jsonnet v0.21.0
10+
github.com/grafana/tanka v0.32.1-0.20250521123240-fa219d35d24f
1111
github.com/hexops/gotextdiff v1.0.3
1212
github.com/jdbaldry/go-language-server-protocol v0.0.0-20211013214444-3022da0884b2
1313
github.com/mitchellh/mapstructure v1.5.0
@@ -24,6 +24,7 @@ require (
2424
github.com/andybalholm/cascadia v1.3.2 // indirect
2525
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2626
github.com/fatih/color v1.18.0 // indirect
27+
github.com/gobwas/glob v0.2.3 // indirect
2728
github.com/google/uuid v1.6.0 // indirect
2829
github.com/huandu/xstrings v1.5.0 // indirect
2930
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -36,9 +37,9 @@ require (
3637
github.com/shopspring/decimal v1.4.0 // indirect
3738
github.com/spf13/cast v1.7.0 // indirect
3839
github.com/stretchr/objx v0.5.2 // indirect
39-
golang.org/x/crypto v0.35.0 // indirect
40+
golang.org/x/crypto v0.36.0 // indirect
4041
golang.org/x/net v0.25.0 // indirect
41-
golang.org/x/sys v0.32.0 // indirect
42+
golang.org/x/sys v0.33.0 // indirect
4243
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
4344
gopkg.in/yaml.v2 v2.4.0 // indirect
4445
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
2727
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2828
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2929
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
30-
github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g=
31-
github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA=
30+
github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA=
31+
github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ=
3232
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3333
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
34-
github.com/grafana/tanka v0.32.0 h1:F+xSc0ipvdeiyf2Fpl9dxcC3wpjVCMEgoc+RoyeGpNw=
35-
github.com/grafana/tanka v0.32.0/go.mod h1:djmXTGczYi6wMKyVpyR7nRBvNBbHtkkq7Q/40yNy12Q=
34+
github.com/grafana/tanka v0.32.1-0.20250521123240-fa219d35d24f h1:gP0r33Vy4+UwxSisoUvCT8H2QClu96X7SMakqG9iE6Q=
35+
github.com/grafana/tanka v0.32.1-0.20250521123240-fa219d35d24f/go.mod h1:NbGUZbqhwXwxpDUhsY7sfTrtPCZwcWhst8Wluk4LVIA=
3636
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
3737
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
3838
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
@@ -96,8 +96,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
9696
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
9797
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
9898
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
99-
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
100-
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
99+
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
100+
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
101101
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
102102
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
103103
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -128,8 +128,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
128128
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
129129
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
130130
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
131-
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
132-
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
131+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
132+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
133133
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
134134
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
135135
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

pkg/ast/processing/find_field.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,117 @@ func (p *Processor) findSelfObject(self *ast.Self) *ast.DesugaredObject {
311311
}
312312
return nil
313313
}
314+
315+
// findUsagesVisitor creates a visitor function that finds all usages of a given symbol
316+
func (p *Processor) findUsagesVisitor(symbolID ast.Identifier, symbol string, ranges *[]ObjectRange) func(node ast.Node) {
317+
return func(node ast.Node) {
318+
switch node := node.(type) {
319+
case *ast.Var:
320+
// For variables, check if the ID matches
321+
if node.Id == symbolID {
322+
*ranges = append(*ranges, ObjectRange{
323+
Filename: node.LocRange.FileName,
324+
SelectionRange: node.LocRange,
325+
FullRange: node.LocRange,
326+
})
327+
}
328+
case *ast.Index:
329+
// For field access, check if the index matches
330+
if litStr, ok := node.Index.(*ast.LiteralString); ok {
331+
if litStr.Value == symbol {
332+
*ranges = append(*ranges, ObjectRange{
333+
Filename: node.LocRange.FileName,
334+
SelectionRange: node.LocRange,
335+
FullRange: node.LocRange,
336+
})
337+
}
338+
}
339+
case *ast.Apply:
340+
if litStr, ok := node.Target.(*ast.LiteralString); ok {
341+
if litStr.Value == symbol {
342+
*ranges = append(*ranges, ObjectRange{
343+
Filename: node.LocRange.FileName,
344+
SelectionRange: node.LocRange,
345+
FullRange: node.LocRange,
346+
})
347+
}
348+
}
349+
}
350+
351+
// Visit all children
352+
switch node := node.(type) {
353+
case *ast.Apply:
354+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Target)
355+
for _, arg := range node.Arguments.Positional {
356+
p.findUsagesVisitor(symbolID, symbol, ranges)(arg.Expr)
357+
}
358+
for _, arg := range node.Arguments.Named {
359+
p.findUsagesVisitor(symbolID, symbol, ranges)(arg.Arg)
360+
}
361+
case *ast.Array:
362+
for _, element := range node.Elements {
363+
p.findUsagesVisitor(symbolID, symbol, ranges)(element.Expr)
364+
}
365+
case *ast.Binary:
366+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Left)
367+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Right)
368+
case *ast.Conditional:
369+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Cond)
370+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.BranchTrue)
371+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.BranchFalse)
372+
case *ast.DesugaredObject:
373+
for _, field := range node.Fields {
374+
p.findUsagesVisitor(symbolID, symbol, ranges)(field.Name)
375+
p.findUsagesVisitor(symbolID, symbol, ranges)(field.Body)
376+
}
377+
case *ast.Error:
378+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Expr)
379+
case *ast.Function:
380+
for _, param := range node.Parameters {
381+
if param.DefaultArg != nil {
382+
p.findUsagesVisitor(symbolID, symbol, ranges)(param.DefaultArg)
383+
}
384+
}
385+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Body)
386+
case *ast.Index:
387+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Target)
388+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Index)
389+
case *ast.Local:
390+
for _, bind := range node.Binds {
391+
p.findUsagesVisitor(symbolID, symbol, ranges)(bind.Body)
392+
}
393+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Body)
394+
case *ast.Object:
395+
for _, field := range node.Fields {
396+
p.findUsagesVisitor(symbolID, symbol, ranges)(field.Expr1)
397+
p.findUsagesVisitor(symbolID, symbol, ranges)(field.Expr2)
398+
}
399+
case *ast.SuperIndex:
400+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Index)
401+
case *ast.Unary:
402+
p.findUsagesVisitor(symbolID, symbol, ranges)(node.Expr)
403+
default:
404+
// No children to visit
405+
}
406+
}
407+
}
408+
409+
// FindUsages finds all usages of a symbol in the given files
410+
func (p *Processor) FindUsages(files []string, symbol string) ([]ObjectRange, error) {
411+
var ranges []ObjectRange
412+
symbolID := ast.Identifier(symbol)
413+
414+
// Create a visitor to find all usages
415+
visitor := p.findUsagesVisitor(symbolID, symbol, &ranges)
416+
417+
// Process each file
418+
for _, file := range files {
419+
rootNode, _, err := p.vm.ImportAST("", file)
420+
if err != nil {
421+
return nil, fmt.Errorf("failed to import AST for file %s: %w", file, err)
422+
}
423+
visitor(rootNode)
424+
}
425+
426+
return ranges, nil
427+
}

pkg/server/references.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
7+
"github.com/google/go-jsonnet/ast"
8+
"github.com/grafana/jsonnet-language-server/pkg/ast/processing"
9+
"github.com/grafana/jsonnet-language-server/pkg/cache"
10+
position "github.com/grafana/jsonnet-language-server/pkg/position_conversion"
11+
"github.com/grafana/jsonnet-language-server/pkg/utils"
12+
"github.com/jdbaldry/go-language-server-protocol/lsp/protocol"
13+
log "github.com/sirupsen/logrus"
14+
15+
tankaJsonnet "github.com/grafana/tanka/pkg/jsonnet"
16+
"github.com/grafana/tanka/pkg/jsonnet/jpath"
17+
)
18+
19+
// findSymbolAndFiles finds the symbol identifier and possible files where it might be used
20+
// based on the AST node at the given position.
21+
func (s *Server) findSymbolAndFiles(doc *cache.Document, params *protocol.ReferenceParams) (string, []string, error) {
22+
searchStack, _ := processing.FindNodeByPosition(doc.AST, position.ProtocolToAST(params.Position))
23+
24+
// Only match locals and obj fields, as we're trying to find usages of these
25+
possibleFiles := []string{}
26+
idOfSymbol := ""
27+
for !searchStack.IsEmpty() {
28+
deepestNode := searchStack.Pop()
29+
switch deepestNode := deepestNode.(type) {
30+
case *ast.Local:
31+
idOfSymbol = string(deepestNode.Binds[0].Variable)
32+
possibleFiles = []string{doc.Item.URI.SpanURI().Filename()} // Local variables are always used in the current file
33+
case *ast.DesugaredObject:
34+
// Find the field on the position
35+
for _, field := range deepestNode.Fields {
36+
if position.RangeASTToProtocol(field.LocRange).Start.Line == params.Position.Line {
37+
fieldName, ok := field.Name.(*ast.LiteralString)
38+
if !ok {
39+
return "", nil, utils.LogErrorf("References: field name is not a string")
40+
}
41+
idOfSymbol = fieldName.Value
42+
root, err := jpath.FindRoot(doc.Item.URI.SpanURI().Filename())
43+
if err != nil {
44+
log.Errorf("References: Error resolving Tanka root, using current directory: %v", err)
45+
root = filepath.Dir(doc.Item.URI.SpanURI().Filename())
46+
}
47+
possibleFiles, err = tankaJsonnet.FindTransitiveImportersForFile(root, []string{doc.Item.URI.SpanURI().Filename()})
48+
if err != nil {
49+
log.Errorf("References: Error finding transitive importers. Using current file only: %v", err)
50+
possibleFiles = []string{doc.Item.URI.SpanURI().Filename()}
51+
}
52+
break
53+
}
54+
}
55+
}
56+
if idOfSymbol != "" {
57+
break
58+
}
59+
}
60+
return idOfSymbol, possibleFiles, nil
61+
}
62+
63+
func (s *Server) References(_ context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
64+
doc, err := s.cache.Get(params.TextDocument.URI)
65+
if err != nil {
66+
return nil, utils.LogErrorf("References: %s: %w", errorRetrievingDocument, err)
67+
}
68+
69+
// Only find references if the line we're trying to find references for hasn't changed since last successful AST parse
70+
if doc.AST == nil {
71+
return nil, utils.LogErrorf("References: document was never successfully parsed, can't find references")
72+
}
73+
if doc.LinesChangedSinceAST[int(params.Position.Line)] {
74+
return nil, utils.LogErrorf("References: document line %d was changed since last successful parse, can't find references", params.Position.Line)
75+
}
76+
77+
vm := s.getVM(doc.Item.URI.SpanURI().Filename())
78+
processor := processing.NewProcessor(s.cache, vm)
79+
80+
idOfSymbol, possibleFiles, err := s.findSymbolAndFiles(doc, params)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
// Find all usages of the symbol
86+
objectRanges, err := processor.FindUsages(possibleFiles, idOfSymbol)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
// Convert ObjectRanges to protocol.Locations
92+
var locations []protocol.Location
93+
for _, r := range objectRanges {
94+
locations = append(locations, protocol.Location{
95+
URI: protocol.URIFromPath(r.Filename),
96+
Range: position.RangeASTToProtocol(r.SelectionRange),
97+
})
98+
}
99+
100+
return locations, nil
101+
}

0 commit comments

Comments
 (0)