From f27c5e3fbcd513a5ec6a0256f73bce54c7b54e81 Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Sat, 22 Aug 2026 22:01:34 +0000 Subject: [PATCH 1/2] Deduplicate SCIP symbol information Amp-Thread-ID: https://ampcode.com/threads/T-01a029e2-5095-710a-9cce-0c6d8645c173 --- src/FileIndexer.ts | 36 ++++++++++++++++++++++++++++++++++-- src/main.test.ts | 13 +++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/FileIndexer.ts b/src/FileIndexer.ts index 9b9f200c..878680d0 100644 --- a/src/FileIndexer.ts +++ b/src/FileIndexer.ts @@ -25,6 +25,8 @@ export class FileIndexer { private localCounter = new Counter() private propertyCounters: Map = new Map() private localSymbolTable: Map = new Map() + private symbolInformation: Map = + new Map() private workingDirectoryRegExp: RegExp constructor( public readonly checker: ts.TypeChecker, @@ -78,7 +80,7 @@ export class FileIndexer { ) const moduleName = this.sourceFile.moduleName || path.basename(this.sourceFile.fileName) - this.document.symbols.push( + this.pushSymbolInformation( new scip.scip.SymbolInformation({ symbol: symbol.value, documentation: ['```ts\nmodule "' + moduleName + '"\n```'], @@ -352,7 +354,7 @@ export class FileIndexer { documentation.push(ts.displayPartsToString(docstring)) } - this.document.symbols.push( + this.pushSymbolInformation( new scip.scip.SymbolInformation({ symbol: symbol.value, documentation, @@ -362,6 +364,36 @@ export class FileIndexer { ) } + private pushSymbolInformation(info: scip.scip.SymbolInformation): void { + const existing = this.symbolInformation.get(info.symbol) + if (!existing) { + this.symbolInformation.set(info.symbol, info) + this.document.symbols.push(info) + return + } + + // Overload declarations share one SCIP symbol but can contribute distinct + // signatures and relationships. SCIP permits only one SymbolInformation + // entry per symbol, so merge that metadata into the first entry. + for (const documentation of info.documentation) { + if (!existing.documentation.includes(documentation)) { + existing.documentation.push(documentation) + } + } + for (const relationship of info.relationships) { + const previous = existing.relationships.find( + candidate => candidate.symbol === relationship.symbol + ) + if (previous) { + previous.is_reference ||= relationship.is_reference + previous.is_implementation ||= relationship.is_implementation + previous.is_type_definition ||= relationship.is_type_definition + } else { + existing.relationships.push(relationship) + } + } + } + private pushOccurrence(occurrence: scip.scip.Occurrence): void { const lastOccurrence = this.document.occurrences.at(-1) if (lastOccurrence) { diff --git a/src/main.test.ts b/src/main.test.ts index d0d9a887..6114e291 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -75,6 +75,19 @@ for (const snapshotDirectory of snapshotDirectories) { throw new Error('empty LSIF index') } for (const document of index.documents) { + const symbols = new Set() + const duplicateSymbols: string[] = [] + for (const symbol of document.symbols) { + if (symbols.has(symbol.symbol)) { + duplicateSymbols.push(symbol.symbol) + } + symbols.add(symbol.symbol) + } + assert.equal( + duplicateSymbols, + [], + `${document.relative_path} should not contain duplicate SymbolInformation` + ) assert.ok( document.language, `${document.relative_path} should have a SCIP document language` From b6997da6fa8b7d84f470d5f642b30d0f45e191ec Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Sat, 22 Aug 2026 23:54:28 +0000 Subject: [PATCH 2/2] Snapshot merged overload information Amp-Thread-ID: https://ampcode.com/threads/T-01a029e2-5095-710a-9cce-0c6d8645c173 --- snapshots/input/syntax/src/overload.d.ts | 2 ++ snapshots/output/syntax/src/overload.d.ts | 20 +++++++++++++++++ src/FileIndexer.ts | 27 +++++++++++++---------- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/snapshots/input/syntax/src/overload.d.ts b/snapshots/input/syntax/src/overload.d.ts index f263ed41..901135ed 100644 --- a/snapshots/input/syntax/src/overload.d.ts +++ b/snapshots/input/syntax/src/overload.d.ts @@ -1,3 +1,5 @@ +// format-options: showDocs + export interface Overloader { onLiteral(param: 'a'): void onLiteral(param: 'b'): void diff --git a/snapshots/output/syntax/src/overload.d.ts b/snapshots/output/syntax/src/overload.d.ts index a9749c98..bd0bc9c6 100644 --- a/snapshots/output/syntax/src/overload.d.ts +++ b/snapshots/output/syntax/src/overload.d.ts @@ -1,11 +1,31 @@ // language TypeScript // < definition syntax 1.0.0 src/`overload.d.ts`/ +//documentation ```ts +// > module "overload.d.ts" +// > ``` + +// format-options: showDocs export interface Overloader { // ^^^^^^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader# +// documentation ```ts +// > interface Overloader +// > ``` onLiteral(param: 'a'): void //^^^^^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral(). +//documentation ```ts +// > (method) onLiteral(param: "a"): void +// > ``` +//documentation ```ts +// > (method) onLiteral(param: "b"): void +// > ``` // ^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral().(param) +// documentation ```ts +// > (parameter) param: "a" +// > ``` +// documentation ```ts +// > (parameter) param: "b" +// > ``` onLiteral(param: 'b'): void //^^^^^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral(). // ^^^^^ definition syntax 1.0.0 src/`overload.d.ts`/Overloader#onLiteral().(param) diff --git a/src/FileIndexer.ts b/src/FileIndexer.ts index 878680d0..9a7eac1b 100644 --- a/src/FileIndexer.ts +++ b/src/FileIndexer.ts @@ -346,7 +346,9 @@ export class FileIndexer { ): void { const documentation = [ '```ts\n' + - this.hideWorkingDirectory(this.signatureForDocumentation(node, sym)) + + this.hideWorkingDirectory( + this.signatureForDocumentation(node, sym, declaration) + ) + '\n```', ] const docstring = sym.getDocumentationComment(this.checker) @@ -385,6 +387,7 @@ export class FileIndexer { candidate => candidate.symbol === relationship.symbol ) if (previous) { + previous.is_definition ||= relationship.is_definition previous.is_reference ||= relationship.is_reference previous.is_implementation ||= relationship.is_implementation previous.is_type_definition ||= relationship.is_type_definition @@ -666,28 +669,28 @@ export class FileIndexer { return undefined } - private signatureForDocumentation(node: ts.Node, sym: ts.Symbol): string { + private signatureForDocumentation( + node: ts.Node, + sym: ts.Symbol, + declaration: ts.Node + ): string { const kind = scriptElementKind(node, sym) const type = (): string => this.checker.typeToString(this.checker.getTypeAtLocation(node)) const asSignatureDeclaration = ( node: ts.Node, - sym: ts.Symbol + declaration: ts.Node ): ts.SignatureDeclaration | undefined => { - const declaration = sym.declarations?.[0] - if (!declaration) { - return undefined - } return ts.isConstructorDeclaration(node) ? node - : ts.isFunctionDeclaration(declaration) + : ts.isFunctionDeclaration(declaration) || + ts.isMethodDeclaration(declaration) || + ts.isMethodSignature(declaration) ? declaration - : ts.isMethodDeclaration(declaration) - ? declaration - : undefined + : undefined } const signature = (): string | undefined => { - const signatureDeclaration = asSignatureDeclaration(node, sym) + const signatureDeclaration = asSignatureDeclaration(node, declaration) if (!signatureDeclaration) { return undefined }