diff --git a/Development.md b/Development.md index b6a437b6..8411dd8f 100644 --- a/Development.md +++ b/Development.md @@ -1,5 +1,7 @@ # Developing scip-typescript +Please note that the yarn version used by CI is `v1.22.19` - you should use this version as well to prevent lockfile conflicts. + ## References - VS Code is a good reference point for the "correct" behavior diff --git a/snapshots/input/syntax/src/constructor.ts b/snapshots/input/syntax/src/constructor.ts new file mode 100644 index 00000000..7bca4296 --- /dev/null +++ b/snapshots/input/syntax/src/constructor.ts @@ -0,0 +1,35 @@ +namespace Yay { + export class SuperConstructor { + constructor(public readonly property: number) {} + } + + export namespace Woo { + export class MyClass { + constructor() {} + } + } +} + +export class SuperConstructor2 { + constructor(public readonly property: number) {} +} + +export function useConstructor(): Yay.SuperConstructor { + return new Yay.SuperConstructor(10) +} + +export function useConstructor2(): SuperConstructor2 { + return new SuperConstructor2(10) +} + +export function useConstructor3(): Yay.Woo.MyClass { + return new Yay.Woo.MyClass() +} + +export class NoConstructor { + property: number +} + +export function useNoConstructor() { + return new NoConstructor() +} diff --git a/snapshots/output/syntax/src/class.ts b/snapshots/output/syntax/src/class.ts index 77dce898..67b33910 100644 --- a/snapshots/output/syntax/src/class.ts +++ b/snapshots/output/syntax/src/class.ts @@ -7,6 +7,8 @@ // ^^^^^^^^^^^^^ definition syntax 1.0.0 src/`class.ts`/Class#classProperty. // documentation ```ts\n(property) classProperty: string\n``` constructor(constructorParam: string) { +// ^^^^^^^^^^^ definition syntax 1.0.0 src/`class.ts`/Class#``(). +// documentation ```ts\nconstructor(constructorParam: string): Class\n``` // ^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`class.ts`/Class#``().(constructorParam) // documentation ```ts\n(parameter) constructorParam: string\n``` this.classProperty = constructorParam @@ -48,7 +50,7 @@ const instance = new Class(param).classProperty // ^^^^^^^^ definition local 2 // documentation ```ts\nvar instance: string\n``` -// ^^^^^ reference syntax 1.0.0 src/`class.ts`/Class# +// ^^^^^ reference syntax 1.0.0 src/`class.ts`/Class#``(). // ^^^^^ reference syntax 1.0.0 src/`class.ts`/newClass().(param) // ^^^^^^^^^^^^^ reference syntax 1.0.0 src/`class.ts`/Class#classProperty. const instance2 = Class.staticMethod(param) diff --git a/snapshots/output/syntax/src/constructor.ts b/snapshots/output/syntax/src/constructor.ts new file mode 100644 index 00000000..351bbdd5 --- /dev/null +++ b/snapshots/output/syntax/src/constructor.ts @@ -0,0 +1,83 @@ + namespace Yay { +// definition syntax 1.0.0 src/`constructor.ts`/ +//documentation ```ts\nmodule "constructor.ts"\n``` +// ^^^ definition syntax 1.0.0 src/`constructor.ts`/Yay/ +// documentation ```ts\nYay: typeof Yay\n``` + export class SuperConstructor { +// ^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/Yay/SuperConstructor# +// documentation ```ts\nclass SuperConstructor\n``` + constructor(public readonly property: number) {} +// ^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/Yay/SuperConstructor#``(). +// documentation ```ts\nconstructor(property: number): SuperConstructor\n``` +// ^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/Yay/SuperConstructor#``().(property) +// documentation ```ts\n(property) property: number\n``` + } + + export namespace Woo { +// ^^^ definition syntax 1.0.0 src/`constructor.ts`/Yay/Woo/ +// documentation ```ts\nWoo: typeof Woo\n``` + export class MyClass { +// ^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/Yay/Woo/MyClass# +// documentation ```ts\nclass MyClass\n``` + constructor() {} +// ^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/Yay/Woo/MyClass#``(). +// documentation ```ts\nconstructor(): MyClass\n``` + } + } + } + + export class SuperConstructor2 { +// ^^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/SuperConstructor2# +// documentation ```ts\nclass SuperConstructor2\n``` + constructor(public readonly property: number) {} +// ^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/SuperConstructor2#``(). +// documentation ```ts\nconstructor(property: number): SuperConstructor2\n``` +// ^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/SuperConstructor2#``().(property) +// documentation ```ts\n(property) property: number\n``` + } + + export function useConstructor(): Yay.SuperConstructor { +// ^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/useConstructor(). +// documentation ```ts\nfunction useConstructor(): SuperConstructor\n``` +// ^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/ +// ^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/SuperConstructor# + return new Yay.SuperConstructor(10) +// ^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/ +// ^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/SuperConstructor#``(). + } + + export function useConstructor2(): SuperConstructor2 { +// ^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/useConstructor2(). +// documentation ```ts\nfunction useConstructor2(): SuperConstructor2\n``` +// ^^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`constructor.ts`/SuperConstructor2# + return new SuperConstructor2(10) +// ^^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`constructor.ts`/SuperConstructor2#``(). + } + + export function useConstructor3(): Yay.Woo.MyClass { +// ^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/useConstructor3(). +// documentation ```ts\nfunction useConstructor3(): MyClass\n``` +// ^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/ +// ^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/Woo/ +// ^^^^^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/Woo/MyClass# + return new Yay.Woo.MyClass() +// ^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/ +// ^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/Woo/ +// ^^^^^^^ reference syntax 1.0.0 src/`constructor.ts`/Yay/Woo/MyClass#``(). + } + + export class NoConstructor { +// ^^^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/NoConstructor# +// documentation ```ts\nclass NoConstructor\n``` + property: number +// ^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/NoConstructor#property. +// documentation ```ts\n(property) property: number\n``` + } + + export function useNoConstructor() { +// ^^^^^^^^^^^^^^^^ definition syntax 1.0.0 src/`constructor.ts`/useNoConstructor(). +// documentation ```ts\nfunction useNoConstructor(): NoConstructor\n``` + return new NoConstructor() +// ^^^^^^^^^^^^^ reference syntax 1.0.0 src/`constructor.ts`/NoConstructor# + } + diff --git a/snapshots/output/syntax/src/import.ts b/snapshots/output/syntax/src/import.ts index 5a796855..b62558ba 100644 --- a/snapshots/output/syntax/src/import.ts +++ b/snapshots/output/syntax/src/import.ts @@ -22,7 +22,7 @@ // documentation ```ts\nfunction useEverything(): string\n``` return ( new Class('a').classProperty + -// ^^^^^ reference syntax 1.0.0 src/`class.ts`/Class# +// ^^^^^ reference syntax 1.0.0 src/`class.ts`/Class#``(). // ^^^^^^^^^^^^^ reference syntax 1.0.0 src/`class.ts`/Class#classProperty. renamedInterface().methodSignature('a') + // ^^^^^^^^^^^^^^^^ reference syntax 1.0.0 src/`interface.ts`/newInterface(). diff --git a/src/FileIndexer.ts b/src/FileIndexer.ts index a8ec4e3e..1f889d42 100644 --- a/src/FileIndexer.ts +++ b/src/FileIndexer.ts @@ -31,6 +31,7 @@ export class FileIndexer { public readonly input: Input, public readonly document: scip.scip.Document, public readonly globalSymbolTable: Map, + public readonly globalConstructorTable: Map, public readonly packages: Packages, public readonly sourceFile: ts.SourceFile ) { @@ -38,7 +39,7 @@ export class FileIndexer { } public index(): void { // Uncomment below if you want to skip certain files for local development. - // if (!this.sourceFile.fileName.includes('infer-relationship')) { + // if (!this.sourceFile.fileName.includes('constructor')) { // return // } this.emitSourceFileOccurrence() @@ -67,6 +68,7 @@ export class FileIndexer { } private visit(node: ts.Node): void { if ( + ts.isConstructorDeclaration(node) || ts.isIdentifier(node) || ts.isPrivateIdentifier(node) || ts.isStringLiteralLike(node) @@ -76,6 +78,7 @@ export class FileIndexer { this.visitSymbolOccurrence(node, sym) } } + ts.forEachChild(node, node => this.visit(node)) } @@ -84,7 +87,10 @@ export class FileIndexer { // // This code is directly based off src/services/goToDefinition.ts. private getTSSymbolAtLocation(node: ts.Node): ts.Symbol | undefined { - const symbol = this.checker.getSymbolAtLocation(node) + const rangeNode: ts.Node = ts.isConstructorDeclaration(node) + ? node.getFirstToken() ?? node + : node + const symbol = this.checker.getSymbolAtLocation(rangeNode) // If this is an alias, and the request came at the declaration location // get the aliased symbol instead. This allows for goto def on an import e.g. @@ -105,6 +111,23 @@ export class FileIndexer { return symbol } + private hasConstructor(classDeclaration: ts.ClassDeclaration): boolean { + const cached = this.globalConstructorTable.get(classDeclaration) + if (cached !== undefined) { + return cached + } + + for (const member of classDeclaration.members) { + if (ts.isConstructorDeclaration(member)) { + this.globalConstructorTable.set(classDeclaration, true) + return true + } + } + + this.globalConstructorTable.set(classDeclaration, false) + return false + } + private visitSymbolOccurrence(node: ts.Node, sym: ts.Symbol): void { const range = Range.fromNode(node).toLsif() let role = 0 @@ -112,7 +135,9 @@ export class FileIndexer { if (isDefinitionNode) { role |= scip.scip.SymbolRole.Definition } - const declarations = isDefinitionNode + const declarations = ts.isConstructorDeclaration(node) + ? [node] + : isDefinitionNode ? // Don't emit ambiguous definition at definition-site. You can reproduce // ambiguous results by triggering "Go to definition" in VS Code on `Conflict` // in the example below: @@ -123,7 +148,20 @@ export class FileIndexer { [node.parent] : sym?.declarations || [] for (const declaration of declarations) { - const scipSymbol = this.scipSymbol(declaration) + let scipSymbol = this.scipSymbol(declaration) + + if ( + ((ts.isIdentifier(node) && ts.isNewExpression(node.parent)) || + (ts.isPropertyAccessExpression(node.parent) && + ts.isNewExpression(node.parent.parent))) && + ts.isClassDeclaration(declaration) && + this.hasConstructor(declaration) + ) { + scipSymbol = ScipSymbol.global( + scipSymbol, + methodDescriptor('') + ) + } if (scipSymbol.isEmpty()) { // Skip empty symbols @@ -474,17 +512,24 @@ export class FileIndexer { const kind = scriptElementKind(node, sym) const type = (): string => this.checker.typeToString(this.checker.getTypeAtLocation(node)) - const signature = (): string | undefined => { + const asSignatureDeclaration = ( + node: ts.Node, + sym: ts.Symbol + ): ts.SignatureDeclaration | undefined => { const declaration = sym.declarations?.[0] if (!declaration) { return undefined } - const signatureDeclaration: ts.SignatureDeclaration | undefined = - ts.isFunctionDeclaration(declaration) - ? declaration - : ts.isMethodDeclaration(declaration) - ? declaration - : undefined + return ts.isConstructorDeclaration(node) + ? node + : ts.isFunctionDeclaration(declaration) + ? declaration + : ts.isMethodDeclaration(declaration) + ? declaration + : undefined + } + const signature = (): string | undefined => { + const signatureDeclaration = asSignatureDeclaration(node, sym) if (!signatureDeclaration) { return undefined } @@ -508,6 +553,9 @@ export class FileIndexer { return 'type ' + node.getText() case ts.ScriptElementKind.classElement: case ts.ScriptElementKind.localClassElement: + if (ts.isConstructorDeclaration(node)) { + return 'constructor' + (signature() || '') + } return 'class ' + node.getText() case ts.ScriptElementKind.interfaceElement: return 'interface ' + node.getText() @@ -769,5 +817,7 @@ function declarationName(node: ts.Node): ts.Node | undefined { * ^^^^^^^^^^^^^^^^^^^^^ node.parent */ function isDefinition(node: ts.Node): boolean { - return declarationName(node.parent) === node + return ( + declarationName(node.parent) === node || ts.isConstructorDeclaration(node) + ) } diff --git a/src/ProjectIndexer.ts b/src/ProjectIndexer.ts index 30a2313c..342c2285 100644 --- a/src/ProjectIndexer.ts +++ b/src/ProjectIndexer.ts @@ -72,6 +72,7 @@ export class ProjectIndexer { private program: ts.Program private checker: ts.TypeChecker private symbolCache: Map = new Map() + private hasConstructor: Map = new Map() private packages: Packages constructor( public readonly config: ts.ParsedCommandLine, @@ -140,6 +141,7 @@ export class ProjectIndexer { input, document, this.symbolCache, + this.hasConstructor, this.packages, sourceFile ) diff --git a/src/Range.ts b/src/Range.ts index c96918b4..b687d3eb 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -32,8 +32,11 @@ export class Range { } public static fromNode(node: ts.Node): Range { const sourceFile = node.getSourceFile() - const start = sourceFile.getLineAndCharacterOfPosition(node.getStart()) - const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd()) + const rangeNode: ts.Node = ts.isConstructorDeclaration(node) + ? node.getFirstToken() ?? node + : node + const start = sourceFile.getLineAndCharacterOfPosition(rangeNode.getStart()) + const end = sourceFile.getLineAndCharacterOfPosition(rangeNode.getEnd()) return new Range( new Position(start.line, start.character), new Position(end.line, end.character)