Skip to content

Commit fbc91bb

Browse files
authored
Implement "Find implementations" for classes, properties, and methods (#136)
* Implement "Find implementations" for classes, properties, and methods Previously, scip-typescript did not support "Find implementations". Now, it supports this functionality for a few commonly used patterns where inheritance is used. This PR does not implement all locations where inheritance is used, but we can add that functionality later. * Address review feedback * s/parent/ancestor/ * Update snapshots - New test case for overloaded methods - Avoid emitting duplicate SCIP symbol relationships - Emit "is_reference=true" relationships for methods/properties
1 parent ab63601 commit fbc91bb

File tree

7 files changed

+255
-9
lines changed

7 files changed

+255
-9
lines changed

snapshots/input/syntax/src/inheritance.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,41 @@
1+
import { Overloader } from './overload'
2+
13
export interface Superinterface {
24
property: string
35
interfaceMethod(): string
46
}
7+
export interface IntermediateSuperinterface extends Superinterface {
8+
intermediateInterfaceMethod(): string
9+
}
510
export abstract class Superclass {
611
public abstract overrideMethod(): string
712
}
8-
export abstract class IntermediateSuperclass extends Superclass {}
9-
export class Subclass extends IntermediateSuperclass implements Superinterface {
13+
export abstract class IntermediateSuperclass extends Superclass {
14+
public override overrideMethod(): string {
15+
return 'this will get overridden'
16+
}
17+
public abstract intermediateOverrideMethod(): string
18+
}
19+
export class Subclass
20+
extends IntermediateSuperclass
21+
implements IntermediateSuperinterface, Overloader
22+
{
23+
public onLiteral(param: any): void {
24+
throw new Error('Method not implemented.' + param)
25+
}
1026
property = 'property'
1127
public overrideMethod(): string {
1228
throw new Error('Method not implemented.')
1329
}
30+
public intermediateOverrideMethod(): string {
31+
throw new Error('Method not implemented.')
32+
}
1433
public interfaceMethod(): string {
1534
throw new Error('Method not implemented.')
1635
}
36+
public intermediateInterfaceMethod(): string {
37+
throw new Error('Method not implemented.')
38+
}
1739
}
1840
export const objectLiteralImplementation: Superinterface = {
1941
property: 'property',
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface Overloader {
2+
onLiteral(param: 'a'): void
3+
onLiteral(param: 'b'): void
4+
}

snapshots/input/syntax/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"compilerOptions": {
3+
"outDir": "dist",
34
"target": "ES5"
45
}
56
}

snapshots/output/syntax/src/inheritance.ts

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { Overloader } from './overload'
2+
// ^^^^^^^^^^ reference syntax 1.0.0 src/`overload.d.ts`/Overloader#
3+
14
export interface Superinterface {
25
// ^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Superinterface#
36
// documentation ```ts\ninterface Superinterface\n```
@@ -7,6 +10,14 @@
710
interfaceMethod(): string
811
// ^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Superinterface#interfaceMethod().
912
// documentation ```ts\n(method) interfaceMethod() => string\n```
13+
}
14+
export interface IntermediateSuperinterface extends Superinterface {
15+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperinterface#
16+
// documentation ```ts\ninterface IntermediateSuperinterface\n```
17+
// ^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`inheritance.ts`/Superinterface#
18+
intermediateInterfaceMethod(): string
19+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperinterface#intermediateInterfaceMethod().
20+
// documentation ```ts\n(method) intermediateInterfaceMethod() => string\n```
1021
}
1122
export abstract class Superclass {
1223
// ^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Superclass#
@@ -15,28 +26,79 @@
1526
// ^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Superclass#overrideMethod().
1627
// documentation ```ts\n(method) overrideMethod(): string\n```
1728
}
18-
export abstract class IntermediateSuperclass extends Superclass {}
29+
export abstract class IntermediateSuperclass extends Superclass {
1930
// ^^^^^^^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#
2031
// documentation ```ts\nclass IntermediateSuperclass\n```
32+
// relationship implementation scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superclass#
2133
// ^^^^^^^^^^ reference syntax 1.0.0 src/`inheritance.ts`/Superclass#
22-
export class Subclass extends IntermediateSuperclass implements Superinterface {
34+
public override overrideMethod(): string {
35+
// ^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#overrideMethod().
36+
// documentation ```ts\n(method) overrideMethod(): string\n```
37+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superclass#overrideMethod().
38+
return 'this will get overridden'
39+
}
40+
public abstract intermediateOverrideMethod(): string
41+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#intermediateOverrideMethod().
42+
// documentation ```ts\n(method) intermediateOverrideMethod(): string\n```
43+
}
44+
export class Subclass
2345
// ^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#
2446
// documentation ```ts\nclass Subclass\n```
25-
// ^^^^^^^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#
26-
// ^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`inheritance.ts`/Superinterface#
47+
// relationship implementation scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#
48+
// relationship implementation scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperinterface#
49+
// relationship implementation scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superclass#
50+
// relationship implementation scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superinterface#
51+
// relationship implementation scip-typescript npm syntax 1.0.0 src/`overload.d.ts`/Overloader#
52+
extends IntermediateSuperclass
53+
// ^^^^^^^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#
54+
implements IntermediateSuperinterface, Overloader
55+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperinterface#
56+
// ^^^^^^^^^^ reference syntax 1.0.0 src/`overload.d.ts`/Overloader#
57+
{
58+
public onLiteral(param: any): void {
59+
// ^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#onLiteral().
60+
// documentation ```ts\n(method) onLiteral(param: any): void\n```
61+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral().
62+
// ^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#onLiteral().(param)
63+
// documentation ```ts\n(parameter) param: any\n```
64+
throw new Error('Method not implemented.' + param)
65+
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error#
66+
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error.
67+
// ^^^^^ reference syntax 1.0.0 src/`inheritance.ts`/Subclass#onLiteral().(param)
68+
}
2769
property = 'property'
2870
// ^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#property.
2971
// documentation ```ts\n(property) property: string\n```
72+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superinterface#property.
3073
public overrideMethod(): string {
3174
// ^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#overrideMethod().
3275
// documentation ```ts\n(method) overrideMethod(): string\n```
76+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#overrideMethod().
77+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superclass#overrideMethod().
78+
throw new Error('Method not implemented.')
79+
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error#
80+
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error.
81+
}
82+
public intermediateOverrideMethod(): string {
83+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#intermediateOverrideMethod().
84+
// documentation ```ts\n(method) intermediateOverrideMethod(): string\n```
85+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperclass#intermediateOverrideMethod().
3386
throw new Error('Method not implemented.')
3487
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error#
3588
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error.
3689
}
3790
public interfaceMethod(): string {
3891
// ^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#interfaceMethod().
3992
// documentation ```ts\n(method) interfaceMethod(): string\n```
93+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superinterface#interfaceMethod().
94+
throw new Error('Method not implemented.')
95+
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error#
96+
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error.
97+
}
98+
public intermediateInterfaceMethod(): string {
99+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/Subclass#intermediateInterfaceMethod().
100+
// documentation ```ts\n(method) intermediateInterfaceMethod(): string\n```
101+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/IntermediateSuperinterface#intermediateInterfaceMethod().
40102
throw new Error('Method not implemented.')
41103
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error#
42104
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error.
@@ -49,9 +111,11 @@
49111
property: 'property',
50112
// ^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/property0:
51113
// documentation ```ts\n(property) property: string\n```
114+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superinterface#property.
52115
interfaceMethod: (): string => {
53116
// ^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/interfaceMethod0:
54117
// documentation ```ts\n(property) interfaceMethod: () => string\n```
118+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superinterface#interfaceMethod().
55119
throw new Error('Function not implemented.')
56120
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error#
57121
// ^^^^^ reference typescript 4.6.2 lib/`lib.es5.d.ts`/Error.
@@ -71,9 +135,11 @@
71135
interfaceMethod: (): string => 'inferred',
72136
// ^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/interfaceMethod1:
73137
// documentation ```ts\n(property) interfaceMethod: () => string\n```
138+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superinterface#interfaceMethod().
74139
property: 'inferred',
75140
// ^^^^^^^^ definition syntax 1.0.0 src/`inheritance.ts`/property1:
76141
// documentation ```ts\n(property) property: string\n```
142+
// relationship implementation reference scip-typescript npm syntax 1.0.0 src/`inheritance.ts`/Superinterface#property.
77143
})
78144
}
79145

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export interface Overloader {
2+
// ^^^^^^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#
3+
// documentation ```ts\ninterface Overloader\n```
4+
onLiteral(param: 'a'): void
5+
// ^^^^^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral().
6+
// documentation ```ts\n(method) onLiteral{ (param: "a"): void; (param: "b"): void; }\n```
7+
// ^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral().(param)
8+
// documentation ```ts\n(parameter) param: "b"\n```
9+
onLiteral(param: 'b'): void
10+
// ^^^^^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral().
11+
// documentation ```ts\n(method) onLiteral{ (param: "a"): void; (param: "b"): void; }\n```
12+
// ^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral().(param)
13+
// documentation ```ts\n(parameter) param: "b"\n```
14+
}
15+

src/FileIndexer.ts

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Range } from './Range'
1919
import * as ts_inline from './TypeScriptInternal'
2020

2121
type Descriptor = lsif.lib.codeintel.lsiftyped.Descriptor
22+
type Relationship = lsif.lib.codeintel.lsiftyped.Relationship
2223

2324
export class FileIndexer {
2425
private localCounter = new Counter()
@@ -91,7 +92,7 @@ export class FileIndexer {
9192
})
9293
)
9394
if (isDefinition) {
94-
this.addSymbolInformation(identifier, sym, lsifSymbol)
95+
this.addSymbolInformation(identifier, sym, declaration, lsifSymbol)
9596
this.handleShorthandPropertyDefinition(declaration, range)
9697
// Only emit one symbol for definitions sites, see https://github.com/sourcegraph/lsif-typescript/issues/45
9798
break
@@ -139,6 +140,7 @@ export class FileIndexer {
139140
private addSymbolInformation(
140141
node: ts.Node,
141142
sym: ts.Symbol,
143+
declaration: ts.Node,
142144
symbol: LsifSymbol
143145
): void {
144146
const documentation = [
@@ -148,11 +150,68 @@ export class FileIndexer {
148150
if (docstring.length > 0) {
149151
documentation.push(ts.displayPartsToString(docstring))
150152
}
153+
151154
this.document.symbols.push(
152-
new lsiftyped.SymbolInformation({ symbol: symbol.value, documentation })
155+
new lsiftyped.SymbolInformation({
156+
symbol: symbol.value,
157+
documentation,
158+
relationships: this.relationships(declaration, symbol),
159+
})
153160
)
154161
}
155162

163+
private relationships(
164+
declaration: ts.Node,
165+
declarationSymbol: LsifSymbol
166+
): Relationship[] {
167+
const relationships: Relationship[] = []
168+
const isAddedSymbol = new Set<string>()
169+
const pushImplementation = (
170+
node: ts.NamedDeclaration,
171+
isReferences: boolean
172+
): void => {
173+
const symbol = this.lsifSymbol(node)
174+
if (symbol.isEmpty()) {
175+
return
176+
}
177+
if (symbol.value === declarationSymbol.value) {
178+
return
179+
}
180+
if (isAddedSymbol.has(symbol.value)) {
181+
// Avoid duplicate relationships. This can happen for overloaded methods
182+
// that have different ts.Symbol but the same SCIP symbol.
183+
return
184+
}
185+
isAddedSymbol.add(symbol.value)
186+
relationships.push(
187+
new lsiftyped.Relationship({
188+
symbol: symbol.value,
189+
is_implementation: true,
190+
is_reference: isReferences,
191+
})
192+
)
193+
}
194+
if (ts.isClassDeclaration(declaration)) {
195+
this.forEachAncestor(declaration, ancestor => {
196+
pushImplementation(ancestor, false)
197+
})
198+
} else if (
199+
ts.isMethodDeclaration(declaration) ||
200+
ts.isMethodSignature(declaration) ||
201+
ts.isPropertyAssignment(declaration) ||
202+
ts.isPropertyDeclaration(declaration)
203+
) {
204+
const declarationName = declaration.name.getText()
205+
this.forEachAncestor(declaration.parent, ancestor => {
206+
for (const member of ancestor.members) {
207+
if (declarationName === member.name?.getText()) {
208+
pushImplementation(member, true)
209+
}
210+
}
211+
})
212+
}
213+
return relationships
214+
}
156215
private declarationName(node: ts.Node): ts.Node | undefined {
157216
if (
158217
ts.isEnumDeclaration(node) ||
@@ -397,6 +456,84 @@ export class FileIndexer {
397456
}
398457
return node.getText() + ': ' + type()
399458
}
459+
460+
// Invokes the `onAncestor` callback for all "ancestors" of the provided node,
461+
// where "ancestor" is loosely defined as the superclass or superinterface of
462+
// that node. The callback is invoked on the `node` parameter itself if it's
463+
// class-like or an interface.
464+
private forEachAncestor(
465+
node: ts.Node,
466+
onAncestor: (
467+
ancestor: ts.ClassLikeDeclaration | ts.InterfaceDeclaration
468+
) => void
469+
): void {
470+
const isVisited = new Set<ts.Node>()
471+
const loop = (declaration: ts.Node): void => {
472+
if (isVisited.has(declaration)) {
473+
return
474+
}
475+
isVisited.add(declaration)
476+
if (
477+
ts.isClassLike(declaration) ||
478+
ts.isInterfaceDeclaration(declaration)
479+
) {
480+
onAncestor(declaration)
481+
}
482+
if (ts.isObjectLiteralExpression(declaration)) {
483+
const tpe = this.inferredTypeOfObjectLiteral(declaration)
484+
for (const symbolDeclaration of tpe.symbol?.declarations || []) {
485+
loop(symbolDeclaration)
486+
}
487+
} else if (
488+
ts.isClassLike(declaration) ||
489+
ts.isInterfaceDeclaration(declaration)
490+
) {
491+
for (const heritageClause of declaration?.heritageClauses || []) {
492+
for (const tpe of heritageClause.types) {
493+
const ancestorSymbol = this.getTSSymbolAtLocation(tpe.expression)
494+
if (ancestorSymbol) {
495+
for (const ancestorDecl of ancestorSymbol.declarations || []) {
496+
loop(ancestorDecl)
497+
}
498+
}
499+
}
500+
}
501+
}
502+
}
503+
loop(node)
504+
}
505+
506+
// Returns the "inferred" type of the provided object literal, where
507+
// "inferred" is loosely defined as the type that is expected in the position
508+
// where the object literal appears. For example, the object literal in
509+
// `const x: SomeInterface = {y: 42}` has the inferred type `SomeInterface`
510+
// even if `this.checker.getTypeAtLocation({y: 42})` does not return
511+
// `SomeInterface`. The object literal could satisfy many types, but in this
512+
// particular location must only satisfy `SomeInterface`.
513+
private inferredTypeOfObjectLiteral(
514+
node: ts.ObjectLiteralExpression
515+
): ts.Type {
516+
if (ts.isVariableDeclaration(node.parent)) {
517+
// Example, return `SomeInterface` from `const x: SomeInterface = {y: 42}`.
518+
return this.checker.getTypeAtLocation(node.parent.name)
519+
}
520+
521+
if (ts.isCallOrNewExpression(node.parent)) {
522+
// Example: return the type of the second parameter of `someMethod` from
523+
// the expression `someMethod(someParameter, {y: 42})`.
524+
const signature = this.checker.getResolvedSignature(node.parent)
525+
for (const [index, argument] of (node.parent.arguments || []).entries()) {
526+
if (argument === node) {
527+
const parameterSymbol = signature?.getParameters()[index]
528+
if (parameterSymbol) {
529+
return this.checker.getTypeOfSymbolAtLocation(parameterSymbol, node)
530+
}
531+
}
532+
}
533+
}
534+
535+
return this.checker.getTypeAtLocation(node)
536+
}
400537
}
401538

402539
function isAnonymousContainerOfSymbols(node: ts.Node): boolean {

src/SnapshotTesting.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function formatSnapshot(
6363
info.relationships.sort((a, b) => a.symbol.localeCompare(b.symbol))
6464
for (const relationship of info.relationships) {
6565
out.push(prefix)
66-
out.push('relationship ')
66+
out.push('relationship')
6767
if (relationship.is_implementation) {
6868
out.push(' implementation')
6969
}
@@ -73,6 +73,7 @@ export function formatSnapshot(
7373
if (relationship.is_type_definition) {
7474
out.push(' type_definition')
7575
}
76+
out.push(' ' + relationship.symbol)
7677
}
7778
out.push('\n')
7879
}

0 commit comments

Comments
 (0)