Skip to content

Commit f0227ec

Browse files
committed
Handle find all references for symbol merged with UMD module and global var
Fixes #29093
1 parent 3a2f6a3 commit f0227ec

File tree

3 files changed

+153
-21
lines changed

3 files changed

+153
-21
lines changed

src/harness/fourslash.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -939,8 +939,8 @@ namespace FourSlash {
939939
const startFile = this.activeFile.fileName;
940940
for (const fileName of files) {
941941
const searchFileNames = startFile === fileName ? [startFile] : [startFile, fileName];
942-
const highlights = this.getDocumentHighlightsAtCurrentPosition(searchFileNames)!;
943-
if (!highlights.every(dh => ts.contains(searchFileNames, dh.fileName))) {
942+
const highlights = this.getDocumentHighlightsAtCurrentPosition(searchFileNames);
943+
if (highlights && !highlights.every(dh => ts.contains(searchFileNames, dh.fileName))) {
944944
this.raiseError(`When asking for document highlights only in files ${searchFileNames}, got document highlights in ${unique(highlights, dh => dh.fileName)}`);
945945
}
946946
}

src/services/findAllReferences.ts

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ namespace ts.FindAllReferences {
111111
return flattenEntries(Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options, sourceFilesSet));
112112
}
113113

114-
function flattenEntries(referenceSymbols: SymbolAndEntries[] | undefined): ReadonlyArray<Entry> | undefined {
114+
function flattenEntries(referenceSymbols: ReadonlyArray<SymbolAndEntries> | undefined): ReadonlyArray<Entry> | undefined {
115115
return referenceSymbols && flatMap(referenceSymbols, r => r.references);
116116
}
117117

@@ -282,6 +282,11 @@ namespace ts.FindAllReferences {
282282
return createTextSpanFromBounds(start, end);
283283
}
284284

285+
export function getTextSpanOfEntry(entry: Entry) {
286+
return entry.kind === EntryKind.Span ? entry.textSpan :
287+
getTextSpan(entry.node, entry.node.getSourceFile());
288+
}
289+
285290
/** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */
286291
function isWriteAccessForReference(node: Node): boolean {
287292
const decl = getDeclarationFromName(node);
@@ -353,7 +358,7 @@ namespace ts.FindAllReferences {
353358
/* @internal */
354359
namespace ts.FindAllReferences.Core {
355360
/** Core find-all-references algorithm. Handles special cases before delegating to `getReferencedSymbolsForSymbol`. */
356-
export function getReferencedSymbolsForNode(position: number, node: Node, program: Program, sourceFiles: ReadonlyArray<SourceFile>, cancellationToken: CancellationToken, options: Options = {}, sourceFilesSet: ReadonlyMap<true> = arrayToSet(sourceFiles, f => f.fileName)): SymbolAndEntries[] | undefined {
361+
export function getReferencedSymbolsForNode(position: number, node: Node, program: Program, sourceFiles: ReadonlyArray<SourceFile>, cancellationToken: CancellationToken, options: Options = {}, sourceFilesSet: ReadonlyMap<true> = arrayToSet(sourceFiles, f => f.fileName)): ReadonlyArray<SymbolAndEntries> | undefined {
357362
if (isSourceFile(node)) {
358363
const reference = GoToDefinition.getReferenceAtPosition(node, position, program);
359364
const moduleSymbol = reference && program.getTypeChecker().getMergedSymbol(reference.file.symbol);
@@ -368,7 +373,7 @@ namespace ts.FindAllReferences.Core {
368373
}
369374

370375
const checker = program.getTypeChecker();
371-
let symbol = checker.getSymbolAtLocation(node);
376+
const symbol = checker.getSymbolAtLocation(node);
372377

373378
// Could not find a symbol e.g. unknown identifier
374379
if (!symbol) {
@@ -380,23 +385,92 @@ namespace ts.FindAllReferences.Core {
380385
return getReferencedSymbolsForModule(program, symbol.parent!, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet);
381386
}
382387

383-
let moduleReferences: SymbolAndEntries[] = emptyArray;
384-
const moduleSourceFile = isModuleSymbol(symbol);
385-
let referencedNode: Node | undefined = node;
386-
if (moduleSourceFile) {
387-
const exportEquals = symbol.exports!.get(InternalSymbolName.ExportEquals);
388-
// If !!exportEquals, we're about to add references to `import("mod")` anyway, so don't double-count them.
389-
moduleReferences = getReferencedSymbolsForModule(program, symbol, !!exportEquals, sourceFiles, sourceFilesSet);
390-
if (!exportEquals || !sourceFilesSet.has(moduleSourceFile.fileName)) return moduleReferences;
391-
// Continue to get references to 'export ='.
392-
symbol = skipAlias(exportEquals, checker);
393-
referencedNode = undefined;
388+
const moduleReferences = getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol, program, sourceFiles, cancellationToken, options, sourceFilesSet);
389+
if (moduleReferences && !(symbol.flags & SymbolFlags.Transient)) {
390+
return moduleReferences;
391+
}
392+
393+
const aliasedSymbol = getMergedAliasedSymbolOfNamespaceExportDeclaration(node, symbol, checker);
394+
const moduleReferencesOfExportTarget = aliasedSymbol &&
395+
getReferencedSymbolsForModuleIfDeclaredBySourceFile(aliasedSymbol, program, sourceFiles, cancellationToken, options, sourceFilesSet);
396+
397+
const references = getReferencedSymbolsForSymbol(symbol, node, sourceFiles, sourceFilesSet, checker, cancellationToken, options);
398+
return mergeReferences(program, moduleReferences, references, moduleReferencesOfExportTarget);
399+
}
400+
401+
function getMergedAliasedSymbolOfNamespaceExportDeclaration(node: Node, symbol: Symbol, checker: TypeChecker) {
402+
if (node.parent && isNamespaceExportDeclaration(node.parent)) {
403+
const aliasedSymbol = checker.getAliasedSymbol(symbol);
404+
const targetSymbol = checker.getMergedSymbol(aliasedSymbol);
405+
if (aliasedSymbol !== targetSymbol) {
406+
return targetSymbol;
407+
}
408+
}
409+
return undefined;
410+
}
411+
412+
function getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol: Symbol, program: Program, sourceFiles: ReadonlyArray<SourceFile>, cancellationToken: CancellationToken, options: Options, sourceFilesSet: ReadonlyMap<true>) {
413+
const moduleSourceFile = symbol.flags & SymbolFlags.Module ? find(symbol.declarations, isSourceFile) : undefined;
414+
if (!moduleSourceFile) return undefined;
415+
const exportEquals = symbol.exports!.get(InternalSymbolName.ExportEquals);
416+
// If !!exportEquals, we're about to add references to `import("mod")` anyway, so don't double-count them.
417+
const moduleReferences = getReferencedSymbolsForModule(program, symbol, !!exportEquals, sourceFiles, sourceFilesSet);
418+
if (!exportEquals || !sourceFilesSet.has(moduleSourceFile.fileName)) return moduleReferences;
419+
// Continue to get references to 'export ='.
420+
const checker = program.getTypeChecker();
421+
symbol = skipAlias(exportEquals, checker);
422+
return mergeReferences(program, moduleReferences, getReferencedSymbolsForSymbol(symbol, /*node*/ undefined, sourceFiles, sourceFilesSet, checker, cancellationToken, options));
423+
}
424+
425+
function mergeReferences(program: Program, ...referencesToMerge: (SymbolAndEntries[] | undefined)[]): SymbolAndEntries[] | undefined {
426+
let result: SymbolAndEntries[] | undefined;
427+
for (const references of referencesToMerge) {
428+
if (!references || !references.length) continue;
429+
if (!result) {
430+
result = references;
431+
continue;
432+
}
433+
for (const entry of references) {
434+
if (!entry.definition || entry.definition.type !== DefinitionKind.Symbol) {
435+
result.push(entry);
436+
continue;
437+
}
438+
const symbol = entry.definition.symbol;
439+
const refIndex = findIndex(result, ref => !!ref.definition &&
440+
ref.definition.type === DefinitionKind.Symbol &&
441+
ref.definition.symbol === symbol);
442+
if (refIndex === -1) {
443+
result.push(entry);
444+
continue;
445+
}
446+
447+
const reference = result[refIndex];
448+
result[refIndex] = {
449+
definition: reference.definition,
450+
references: reference.references.concat(entry.references).sort((entry1, entry2) => {
451+
const entry1File = getSourceFileIndexOfEntry(program, entry1);
452+
const entry2File = getSourceFileIndexOfEntry(program, entry2);
453+
if (entry1File !== entry2File) {
454+
return compareValues(entry1File, entry2File);
455+
}
456+
457+
const entry1Span = getTextSpanOfEntry(entry1);
458+
const entry2Span = getTextSpanOfEntry(entry2);
459+
return entry1Span.start !== entry2Span.start ?
460+
compareValues(entry1Span.start, entry2Span.start) :
461+
compareValues(entry1Span.length, entry2Span.length);
462+
})
463+
};
464+
}
394465
}
395-
return concatenate(moduleReferences, getReferencedSymbolsForSymbol(symbol, referencedNode, sourceFiles, sourceFilesSet, checker, cancellationToken, options));
466+
return result;
396467
}
397468

398-
function isModuleSymbol(symbol: Symbol): SourceFile | undefined {
399-
return symbol.flags & SymbolFlags.Module ? find(symbol.declarations, isSourceFile) : undefined;
469+
function getSourceFileIndexOfEntry(program: Program, entry: Entry) {
470+
const sourceFile = entry.kind === EntryKind.Span ?
471+
program.getSourceFile(entry.fileName)! :
472+
entry.node.getSourceFile();
473+
return program.getSourceFiles().indexOf(sourceFile);
400474
}
401475

402476
function getReferencedSymbolsForModule(program: Program, symbol: Symbol, excludeImportTypeOfExportEquals: boolean, sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>): SymbolAndEntries[] {
@@ -435,7 +509,7 @@ namespace ts.FindAllReferences.Core {
435509
break;
436510
default:
437511
// This may be merged with something.
438-
Debug.fail("Expected a module symbol to be declared by a SourceFile or ModuleDeclaration.");
512+
Debug.assert(!!(symbol.flags & SymbolFlags.Transient), "Expected a module symbol to be declared by a SourceFile or ModuleDeclaration.");
439513
}
440514
}
441515

@@ -551,6 +625,8 @@ namespace ts.FindAllReferences.Core {
551625
// If the symbol is declared as part of a declaration like `{ type: "a" } | { type: "b" }`, use the property on the union type to get more references.
552626
return firstDefined(symbol.declarations, decl => {
553627
if (!decl.parent) {
628+
// Ignore UMD module and global merge
629+
if (symbol.flags & SymbolFlags.Transient) return undefined;
554630
// Assertions for GH#21814. We should be handling SourceFile symbols in `getReferencedSymbolsForModule` instead of getting here.
555631
Debug.fail(`Unexpected symbol at ${Debug.showSyntaxKind(node)}: ${Debug.showSymbol(symbol)}`);
556632
}
@@ -588,6 +664,12 @@ namespace ts.FindAllReferences.Core {
588664
Class,
589665
}
590666

667+
function getNonModuleSymbolOfMergedModuleSymbol(symbol: Symbol) {
668+
if (!(symbol.flags & (SymbolFlags.Module | SymbolFlags.Transient))) return undefined;
669+
const decl = symbol.declarations && find(symbol.declarations, d => !isSourceFile(d) && !isModuleDeclaration(d));
670+
return decl && decl.symbol;
671+
}
672+
591673
/**
592674
* Holds all state needed for the finding references.
593675
* Unlike `Search`, there is only one `State`.
@@ -648,7 +730,7 @@ namespace ts.FindAllReferences.Core {
648730
// The other two forms seem to be handled downstream (e.g. in `skipPastExportOrImportSpecifier`), so special-casing the first form
649731
// here appears to be intentional).
650732
const {
651-
text = stripQuotes(unescapeLeadingUnderscores((getLocalSymbolForExportDefault(symbol) || symbol).escapedName)),
733+
text = stripQuotes(unescapeLeadingUnderscores((getLocalSymbolForExportDefault(symbol) || getNonModuleSymbolOfMergedModuleSymbol(symbol) || symbol).escapedName)),
652734
allSearchSymbols = [symbol],
653735
} = searchOptions;
654736
const escapedText = escapeLeadingUnderscores(text);
@@ -1573,6 +1655,13 @@ namespace ts.FindAllReferences.Core {
15731655
if (res2) return res2;
15741656
}
15751657

1658+
const aliasedSymbol = getMergedAliasedSymbolOfNamespaceExportDeclaration(location, symbol, checker);
1659+
if (aliasedSymbol) {
1660+
// In case of UMD module and global merging, search for global as well
1661+
const res = cbSymbol(aliasedSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.Node);
1662+
if (res) return res;
1663+
}
1664+
15761665
const res = fromRoot(symbol);
15771666
if (res) return res;
15781667

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: /node_modules/@types/three/three-core.d.ts
4+
////export class Vector3 {
5+
//// constructor(x?: number, y?: number, z?: number);
6+
//// x: number;
7+
//// y: number;
8+
////}
9+
10+
// @Filename: /node_modules/@types/three/index.d.ts
11+
////export * from "./three-core";
12+
////export as namespace [|{| "isWriteAccess": true, "isDefinition": true |}THREE|];
13+
14+
// @Filename: /typings/global.d.ts
15+
////import * as _THREE from '[|three|]';
16+
////declare global {
17+
//// const [|{| "isWriteAccess": true, "isDefinition": true |}THREE|]: typeof _THREE;
18+
////}
19+
20+
// @Filename: /src/index.ts
21+
////export const a = {};
22+
////let v = new [|THREE|].Vector2();
23+
24+
// @Filename: /tsconfig.json
25+
////{
26+
//// "compilerOptions": {
27+
//// "esModuleInterop": true,
28+
//// "outDir": "./build/js/",
29+
//// "noImplicitAny": true,
30+
//// "module": "es6",
31+
//// "target": "es6",
32+
//// "allowJs": true,
33+
//// "skipLibCheck": true,
34+
//// "lib": ["es2016", "dom"],
35+
//// "typeRoots": ["node_modules/@types/"],
36+
//// "types": ["three"]
37+
//// },
38+
//// "files": ["/src/index.ts", "typings/global.d.ts"]
39+
////}
40+
41+
// TODO:: this should be var THREE: typeof import instead of module name as var but thats existing issue and repros with quickInfo too.
42+
verify.singleReferenceGroup(`module "/node_modules/@types/three/index"
43+
var "/node_modules/@types/three/index": typeof import("/node_modules/@types/three/index")`);

0 commit comments

Comments
 (0)