Skip to content

Commit 40a6e62

Browse files
authored
Add reverse implementors index to the partial SCIP loader (#23)
## Description This PR adds a reverse implementors index to the partial SCIP loader that maps each abstract/interface symbol to its implementing symbols, built during index load by reading IsImplementation relationships. Also implemented the registry-level implementation handler to be used in "gotoImplementation" later. This handler resolves the symbol uses the reverse index to fetch implementors, resolves each implementor to its definition location, and returns those as LSP locations. If the reverse index has no entry (e.g., incomplete data), it falls back to scanning the symbol’s relationships to find implementors before resolving their locations. ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [X] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Refactoring (no functional changes) - [ ] Performance improvement - [ ] Test improvement ## Component(s) Affected - [X] Language Server (ULSP) - [ ] SCIP Generation (Python utilities) - [ ] VS Code/Cursor Extension - [ ] Java Aggregator - [ ] Build System (Bazel) - [ ] Documentation - [ ] Tests ## Testing - [X] I have added tests that prove my fix is effective or that my feature works - [X] New and existing unit tests pass locally with my changes (`bazel test //...`) - [ ] I have tested this manually with a real project ### Manual Testing Details Describe how you tested these changes: - IDE used for testing: - Project(s) tested against: - Specific features/scenarios verified: ## Checklist - [X] My code follows the existing code style and conventions - [X] I have performed a self-review of my own code - [X] I have commented my code, particularly in hard-to-understand areas - [X] I have made corresponding changes to the documentation - [X] I have updated BUILD.bazel files if I added new source files - [X] My changes generate no new warnings - [ ] Any dependent changes have been merged and published ## Screenshots/Logs (if applicable) Include any relevant screenshots, logs, or output that demonstrates the changes. ## Related Issues Fixes #(issue number) Closes #(issue number) Related to #(issue number) ## Additional Notes Any additional information that reviewers should know about this PR.
1 parent 4acbebc commit 40a6e62

File tree

5 files changed

+196
-8
lines changed

5 files changed

+196
-8
lines changed

src/scip-lib/partialloader/index.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type PartialIndex interface {
2424
GetSymbolInformation(symbol string) (*model.SymbolInformation, string, error)
2525
GetSymbolInformationFromDescriptors(descriptors []model.Descriptor, version string) (*model.SymbolInformation, string, error)
2626
References(symbol string) (map[string][]*model.Occurrence, error)
27+
Implementations(symbol string) ([]string, error)
2728
Tidy() error
2829
}
2930

@@ -57,19 +58,24 @@ type PartialLoadedIndex struct {
5758
indexFolder string
5859
pool *scanner.BufferPool
5960
onDocumentLoaded func(*model.Document)
61+
62+
// ImplementorsBySymbol maps abstract/interface symbol -> set of implementing symbols
63+
implementorsMu sync.RWMutex
64+
ImplementorsBySymbol map[string]map[string]struct{}
6065
}
6166

6267
// NewPartialLoadedIndex creates a new PartialLoadedIndex
6368
func NewPartialLoadedIndex(indexFolder string) PartialIndex {
6469
return &PartialLoadedIndex{
65-
PrefixTreeRoot: NewSymbolPrefixTree(),
66-
DocTreeNodes: make(map[string]*docNodes),
67-
LoadedDocuments: make(map[string]*model.Document),
68-
updatedDocs: make(map[string]int64),
69-
docToIndex: make(map[string]string, 0),
70-
indexFolder: indexFolder,
71-
pool: scanner.NewBufferPool(1024, 12),
72-
onDocumentLoaded: func(*model.Document) {},
70+
PrefixTreeRoot: NewSymbolPrefixTree(),
71+
DocTreeNodes: make(map[string]*docNodes),
72+
LoadedDocuments: make(map[string]*model.Document),
73+
updatedDocs: make(map[string]int64),
74+
docToIndex: make(map[string]string, 0),
75+
indexFolder: indexFolder,
76+
pool: scanner.NewBufferPool(1024, 12),
77+
onDocumentLoaded: func(*model.Document) {},
78+
ImplementorsBySymbol: make(map[string]map[string]struct{}),
7379
}
7480
}
7581

@@ -130,6 +136,7 @@ func (p *PartialLoadedIndex) LoadIndex(indexPath string, indexReader scanner.Sci
130136
localDocTreeNodes := make(map[string]*docNodes)
131137
localUpdatedDocs := make(map[string]int64)
132138
localDocToIndex := make(map[string]string)
139+
localImplementorsBySymbol := make(map[string]map[string]struct{})
133140

134141
loadScanner := &scanner.IndexScannerImpl{
135142
Pool: p.pool,
@@ -157,6 +164,17 @@ func (p *PartialLoadedIndex) LoadIndex(indexPath string, indexReader scanner.Sci
157164
modelInfo := mapper.ScipSymbolInformationToModelSymbolInformation(info)
158165
leafNode, isNew := localPrefixTree.AddSymbol(docPath, modelInfo, p.revision.Load())
159166

167+
// Populate reverse implementors for quick lookup (impl -> abs)
168+
for _, rel := range modelInfo.Relationships {
169+
if rel != nil && rel.IsImplementation {
170+
abs := rel.Symbol
171+
if localImplementorsBySymbol[abs] == nil {
172+
localImplementorsBySymbol[abs] = make(map[string]struct{})
173+
}
174+
localImplementorsBySymbol[abs][modelInfo.Symbol] = struct{}{}
175+
}
176+
}
177+
160178
if isNew {
161179
localDocTreeNodes[docPath].nodes = append(localDocTreeNodes[docPath].nodes, leafNode)
162180
}
@@ -179,6 +197,7 @@ func (p *PartialLoadedIndex) LoadIndex(indexPath string, indexReader scanner.Sci
179197
p.mergeDocTreeNodes(localDocTreeNodes)
180198
p.mergeUpdatedDocs(localUpdatedDocs)
181199
p.mergeDocToIndex(localDocToIndex)
200+
p.mergeImplementors(localImplementorsBySymbol)
182201
}()
183202
return loadScanner.ScanIndexReader(indexReader)
184203
}
@@ -280,6 +299,23 @@ func (p *PartialLoadedIndex) mergeDocToIndex(localDocToIndex map[string]string)
280299
}
281300
}
282301

302+
// mergeImplementors merges a local reverse implementors map into the main index
303+
func (p *PartialLoadedIndex) mergeImplementors(local map[string]map[string]struct{}) {
304+
if len(local) == 0 {
305+
return
306+
}
307+
p.implementorsMu.Lock()
308+
defer p.implementorsMu.Unlock()
309+
for abs, impls := range local {
310+
if p.ImplementorsBySymbol[abs] == nil {
311+
p.ImplementorsBySymbol[abs] = make(map[string]struct{})
312+
}
313+
for impl := range impls {
314+
p.ImplementorsBySymbol[abs][impl] = struct{}{}
315+
}
316+
}
317+
}
318+
283319
// LoadDocument loads a document into the PartialLoadedIndex
284320
func (p *PartialLoadedIndex) LoadDocument(relativeDocPath string) (*model.Document, error) {
285321
p.loadedDocsMu.RLock()
@@ -364,6 +400,21 @@ func (p *PartialLoadedIndex) loadDocumentFromIndexFolder(relativeDocPath string)
364400
return doc, nil
365401
}
366402

403+
// Implementations returns the list of implementing symbols for a given abstract/interface symbol
404+
func (p *PartialLoadedIndex) Implementations(symbol string) ([]string, error) {
405+
p.implementorsMu.RLock()
406+
defer p.implementorsMu.RUnlock()
407+
set := p.ImplementorsBySymbol[symbol]
408+
if set == nil {
409+
return []string{}, nil
410+
}
411+
res := make([]string, 0, len(set))
412+
for s := range set {
413+
res = append(res, s)
414+
}
415+
return res, nil
416+
}
417+
367418
// Tidy prunes nodes for documents that were updated in the current revision
368419
func (p *PartialLoadedIndex) Tidy() error {
369420
// Acquire the modification mutex to prevent new index loads during cleanup

src/scip-lib/partialloader/index_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,52 @@ func TestLoadIndexWithPreloadedDocument(t *testing.T) {
410410
assert.NotNil(t, info)
411411
assert.Equal(t, docPath, foundDocPath)
412412
}
413+
414+
// New tests for reverse implementors index
415+
func TestMergeImplementors(t *testing.T) {
416+
idx := &PartialLoadedIndex{
417+
ImplementorsBySymbol: make(map[string]map[string]struct{}),
418+
}
419+
local := map[string]map[string]struct{}{
420+
"abs#Symbol": {
421+
"impl#A": {},
422+
"impl#B": {},
423+
},
424+
}
425+
idx.mergeImplementors(local)
426+
set := idx.ImplementorsBySymbol["abs#Symbol"]
427+
if assert.NotNil(t, set) {
428+
_, okA := set["impl#A"]
429+
_, okB := set["impl#B"]
430+
assert.True(t, okA)
431+
assert.True(t, okB)
432+
}
433+
}
434+
435+
func TestImplementations(t *testing.T) {
436+
idx := &PartialLoadedIndex{
437+
ImplementorsBySymbol: make(map[string]map[string]struct{}),
438+
}
439+
idx.ImplementorsBySymbol["abs#Symbol"] = map[string]struct{}{
440+
"impl#B": {},
441+
"impl#A": {},
442+
}
443+
list, err := idx.Implementations("abs#Symbol")
444+
assert.NoError(t, err)
445+
assert.Contains(t, list, "impl#A")
446+
assert.Contains(t, list, "impl#B")
447+
448+
empty, err := idx.Implementations("unknown#Symbol")
449+
assert.NoError(t, err)
450+
assert.Equal(t, 0, len(empty))
451+
}
452+
453+
func TestLoadIndexWithImplementors(t *testing.T) {
454+
index := NewPartialLoadedIndex("../testdata")
455+
err := index.LoadIndexFile(filepath.Join("../testdata", "index.scip"))
456+
assert.NoError(t, err)
457+
symbol := "scip-go gomod code.uber.internal/devexp/test_management/tracing 0f67d80e60274b77875a241c43ef980bc9ffe0d8 `code.uber.internal/devexp/test_management/tracing`/PartialIndex#"
458+
implementors, err := index.Implementations(symbol)
459+
assert.NoError(t, err)
460+
assert.Equal(t, []string{"scip-go gomod code.uber.internal/devexp/test_management/tracing 0f67d80e60274b77875a241c43ef980bc9ffe0d8 `code.uber.internal/devexp/test_management/tracing`/index#"}, implementors)
461+
}

src/scip-lib/testdata/index.scip

597 Bytes
Binary file not shown.

src/ulsp/controller/scip/partial_registry.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,56 @@ func (p *partialScipRegistry) GetSymbolOccurrence(uri uri.URI, pos protocol.Posi
319319
return nil, errors.New("not implemented")
320320
}
321321

322+
// Implementation implements Registry.
323+
func (p *partialScipRegistry) Implementation(sourceURI uri.URI, pos protocol.Position) ([]protocol.Location, error) {
324+
doc, err := p.Index.LoadDocument(p.uriToRelativePath(sourceURI))
325+
if err != nil {
326+
p.logger.Errorf("failed to load document %s: %s", sourceURI, err)
327+
return nil, err
328+
}
329+
if doc == nil {
330+
return nil, nil
331+
}
332+
333+
sourceOccurrence := GetOccurrenceForPosition(doc.Occurrences, pos)
334+
if sourceOccurrence == nil {
335+
return nil, nil
336+
}
337+
338+
// Local symbols typically don't have implementation relationships
339+
if scip.IsLocalSymbol(sourceOccurrence.Symbol) {
340+
return []protocol.Location{}, nil
341+
}
342+
343+
locations := make([]protocol.Location, 0)
344+
345+
implementors, err := p.Index.Implementations(sourceOccurrence.Symbol)
346+
if err != nil {
347+
p.logger.Errorf("failed to get implementing symbols for %s: %s", sourceOccurrence.Symbol, err)
348+
return nil, err
349+
}
350+
for _, implSym := range implementors {
351+
implementingSymbol, err := model.ParseScipSymbol(implSym)
352+
if err != nil {
353+
p.logger.Errorf("failed to parse implementing symbol %s: %s", implSym, err)
354+
continue
355+
}
356+
implOcc, err := p.GetSymbolDefinitionOccurrence(
357+
mapper.ScipDescriptorsToModelDescriptors(implementingSymbol.Descriptors),
358+
implementingSymbol.Package.Version,
359+
)
360+
if err != nil {
361+
p.logger.Errorf("failed to get definition for implementing symbol %s: %s", implSym, err)
362+
continue
363+
}
364+
if implOcc != nil && implOcc.Occurrence != nil {
365+
locations = append(locations, *mapper.ScipOccurrenceToLocation(implOcc.Location, implOcc.Occurrence))
366+
}
367+
}
368+
369+
return locations, nil
370+
}
371+
322372
func (p *partialScipRegistry) uriToRelativePath(uri uri.URI) string {
323373
rel, err := filepath.Rel(p.WorkspaceRoot, uri.Filename())
324374
if err != nil {

src/ulsp/controller/scip/partial_registry_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,3 +1475,41 @@ func TestPartialScipRegistry_DocumentSymbols(t *testing.T) {
14751475
})
14761476
}
14771477
}
1478+
1479+
func TestPartialScipRegistry_Implementation_FastPath(t *testing.T) {
1480+
ctrl := gomock.NewController(t)
1481+
mockIndex := partialloadermock.NewMockPartialIndex(ctrl)
1482+
logger := zaptest.NewLogger(t).Sugar()
1483+
1484+
registry := &partialScipRegistry{
1485+
WorkspaceRoot: "/workspace",
1486+
Index: mockIndex,
1487+
logger: logger,
1488+
}
1489+
1490+
sourceURI := uri.File("/workspace/test.go")
1491+
pos := protocol.Position{Line: 1, Character: 1}
1492+
1493+
sourceOcc := &model.Occurrence{Symbol: tracingUUIDKey, Range: []int32{1, 1, 1, 2}}
1494+
mockIndex.EXPECT().LoadDocument("test.go").Return(&model.Document{
1495+
Occurrences: []*model.Occurrence{sourceOcc},
1496+
}, nil)
1497+
1498+
mockIndex.EXPECT().Implementations(tracingUUIDKey).Return([]string{
1499+
"scip-go gomod example/pkg v1.0.0 `example/pkg`/Foo#Bar.",
1500+
}, nil)
1501+
1502+
mockIndex.EXPECT().GetSymbolInformationFromDescriptors(gomock.Any(), gomock.Any()).Return(&model.SymbolInformation{Symbol: "impl#sym"}, "impl.go", nil)
1503+
mockIndex.EXPECT().LoadDocument("impl.go").Return(&model.Document{
1504+
Occurrences: []*model.Occurrence{
1505+
{Symbol: "impl#sym", SymbolRoles: int32(scipproto.SymbolRole_Definition), Range: []int32{10, 1, 10, 5}},
1506+
},
1507+
}, nil)
1508+
1509+
locs, err := registry.Implementation(sourceURI, pos)
1510+
require.NoError(t, err)
1511+
require.Equal(t, 1, len(locs))
1512+
assert.Equal(t, uri.File("/workspace/impl.go"), locs[0].URI)
1513+
assert.Equal(t, protocol.Position{Line: 10, Character: 1}, locs[0].Range.Start)
1514+
assert.Equal(t, protocol.Position{Line: 10, Character: 5}, locs[0].Range.End)
1515+
}

0 commit comments

Comments
 (0)