Skip to content

Commit 68122ea

Browse files
author
Andy
authored
Support find-all-references for export = of class expression (#16327)
* Support find-all-references for `export =` of class expression * Add comments
1 parent 747d01c commit 68122ea

File tree

5 files changed

+103
-29
lines changed

5 files changed

+103
-29
lines changed

src/services/findAllReferences.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,10 @@ namespace ts.FindAllReferences.Core {
496496
const { text = stripQuotes(getDeclaredName(this.checker, symbol, location)), allSearchSymbols = undefined } = searchOptions;
497497
const escapedText = escapeIdentifier(text);
498498
const parents = this.options.implementations && getParentSymbolsOfPropertyAccess(location, symbol, this.checker);
499-
return { location, symbol, comingFrom, text, escapedText, parents, includes };
500-
501-
function includes(referenceSymbol: Symbol): boolean {
502-
return allSearchSymbols ? contains(allSearchSymbols, referenceSymbol) : referenceSymbol === symbol;
503-
}
499+
return {
500+
location, symbol, comingFrom, text, escapedText, parents,
501+
includes: referenceSymbol => allSearchSymbols ? contains(allSearchSymbols, referenceSymbol) : referenceSymbol === symbol,
502+
};
504503
}
505504

506505
private readonly symbolIdToReferences: Entry[][] = [];

src/services/importTracker.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,8 @@ namespace ts.FindAllReferences {
436436
if (parent.kind === SyntaxKind.PropertyAccessExpression) {
437437
// When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use.
438438
// So check that we are at the declaration.
439-
return symbol.declarations.some(d => d === parent) && parent.parent.kind === ts.SyntaxKind.BinaryExpression
440-
? getSpecialPropertyExport(parent.parent as ts.BinaryExpression, /*useLhsSymbol*/ false)
439+
return symbol.declarations.some(d => d === parent) && isBinaryExpression(parent.parent)
440+
? getSpecialPropertyExport(parent.parent, /*useLhsSymbol*/ false)
441441
: undefined;
442442
}
443443
else {
@@ -449,31 +449,41 @@ namespace ts.FindAllReferences {
449449
else {
450450
const exportNode = getExportNode(parent);
451451
if (exportNode && hasModifier(exportNode, ModifierFlags.Export)) {
452-
if (exportNode.kind === SyntaxKind.ImportEqualsDeclaration && (exportNode as ImportEqualsDeclaration).moduleReference === node) {
452+
if (isImportEqualsDeclaration(exportNode) && exportNode.moduleReference === node) {
453453
// We're at `Y` in `export import X = Y`. This is not the exported symbol, the left-hand-side is. So treat this as an import statement.
454454
if (comingFromExport) {
455455
return undefined;
456456
}
457457

458-
const lhsSymbol = checker.getSymbolAtLocation((exportNode as ImportEqualsDeclaration).name);
458+
const lhsSymbol = checker.getSymbolAtLocation(exportNode.name);
459459
return { kind: ImportExport.Import, symbol: lhsSymbol, isNamedImport: false };
460460
}
461461
else {
462462
return exportInfo(symbol, getExportKindForDeclaration(exportNode));
463463
}
464464
}
465-
else if (parent.kind === SyntaxKind.ExportAssignment) {
466-
// Get the symbol for the `export =` node; its parent is the module it's the export of.
467-
const exportingModuleSymbol = parent.symbol.parent;
468-
Debug.assert(!!exportingModuleSymbol);
469-
return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol, exportKind: ExportKind.ExportEquals } };
465+
// If we are in `export = a;`, `parent` is the export assignment.
466+
else if (isExportAssignment(parent)) {
467+
return getExportAssignmentExport(parent);
470468
}
471-
else if (parent.kind === ts.SyntaxKind.BinaryExpression) {
472-
return getSpecialPropertyExport(parent as ts.BinaryExpression, /*useLhsSymbol*/ true);
469+
// If we are in `export = class A {};` at `A`, `parent.parent` is the export assignment.
470+
else if (isExportAssignment(parent.parent)) {
471+
return getExportAssignmentExport(parent.parent);
473472
}
474-
else if (parent.parent.kind === SyntaxKind.BinaryExpression) {
475-
return getSpecialPropertyExport(parent.parent as ts.BinaryExpression, /*useLhsSymbol*/ true);
473+
// Similar for `module.exports =` and `exports.A =`.
474+
else if (isBinaryExpression(parent)) {
475+
return getSpecialPropertyExport(parent, /*useLhsSymbol*/ true);
476476
}
477+
else if (isBinaryExpression(parent.parent)) {
478+
return getSpecialPropertyExport(parent.parent, /*useLhsSymbol*/ true);
479+
}
480+
}
481+
482+
function getExportAssignmentExport(ex: ExportAssignment): ExportedSymbol {
483+
// Get the symbol for the `export =` node; its parent is the module it's the export of.
484+
const exportingModuleSymbol = ex.symbol.parent;
485+
Debug.assert(!!exportingModuleSymbol);
486+
return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol, exportKind: ExportKind.ExportEquals } };
477487
}
478488

479489
function getSpecialPropertyExport(node: ts.BinaryExpression, useLhsSymbol: boolean): ExportedSymbol | undefined {
@@ -496,21 +506,21 @@ namespace ts.FindAllReferences {
496506

497507
function getImport(): ImportedSymbol | undefined {
498508
const isImport = isNodeImport(node);
499-
if (!isImport) return;
509+
if (!isImport) return undefined;
500510

501511
// A symbol being imported is always an alias. So get what that aliases to find the local symbol.
502512
let importedSymbol = checker.getImmediateAliasedSymbol(symbol);
503-
if (importedSymbol) {
504-
// Search on the local symbol in the exporting module, not the exported symbol.
505-
importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker);
506-
// Similarly, skip past the symbol for 'export ='
507-
if (importedSymbol.name === "export=") {
508-
importedSymbol = checker.getImmediateAliasedSymbol(importedSymbol);
509-
}
513+
if (!importedSymbol) return undefined;
510514

511-
if (symbolName(importedSymbol) === symbol.name) { // If this is a rename import, do not continue searching.
512-
return { kind: ImportExport.Import, symbol: importedSymbol, ...isImport };
513-
}
515+
// Search on the local symbol in the exporting module, not the exported symbol.
516+
importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker);
517+
// Similarly, skip past the symbol for 'export ='
518+
if (importedSymbol.name === "export=") {
519+
importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker);
520+
}
521+
522+
if (symbolName(importedSymbol) === symbol.name) { // If this is a rename import, do not continue searching.
523+
return { kind: ImportExport.Import, symbol: importedSymbol, ...isImport };
514524
}
515525
}
516526

@@ -525,6 +535,22 @@ namespace ts.FindAllReferences {
525535
}
526536
}
527537

538+
function getExportEqualsLocalSymbol(importedSymbol: Symbol, checker: TypeChecker): Symbol {
539+
if (importedSymbol.flags & SymbolFlags.Alias) {
540+
return checker.getImmediateAliasedSymbol(importedSymbol);
541+
}
542+
543+
const decl = importedSymbol.valueDeclaration;
544+
if (isExportAssignment(decl)) { // `export = class {}`
545+
return decl.expression.symbol;
546+
}
547+
else if (isBinaryExpression(decl)) { // `module.exports = class {}`
548+
return decl.right.symbol;
549+
}
550+
Debug.fail();
551+
}
552+
553+
// If a reference is a class expression, the exported node would be its parent.
528554
// If a reference is a variable declaration, the exported node would be the variable statement.
529555
function getExportNode(parent: Node): Node | undefined {
530556
if (parent.kind === SyntaxKind.VariableDeclaration) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: /a.ts
4+
////export = class [|{| "isWriteAccess": true, "isDefinition": true |}A|] {
5+
//// m() { [|A|]; }
6+
////};
7+
8+
// @Filename: /b.ts
9+
////import [|{| "isWriteAccess": true, "isDefinition": true |}A|] = require("./a");
10+
////[|A|];
11+
12+
const [r0, r1, r2, r3] = test.ranges();
13+
const defs = { definition: "(local class) A", ranges: [r0, r1] };
14+
const imports = { definition: 'import A = require("./a")', ranges: [r2, r3] };
15+
verify.referenceGroups([r0, r1], [defs, imports]);
16+
verify.referenceGroups([r2, r3], [imports, defs]);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
5+
// @Filename: /a.js
6+
////module.exports = class [|{| "isWriteAccess": true, "isDefinition": true |}A|] {};
7+
8+
// @Filename: /b.js
9+
////import [|{| "isWriteAccess": true, "isDefinition": true |}A|] = require("./a");
10+
////[|A|];
11+
12+
const [r0, r1, r2] = test.ranges();
13+
const defs = { definition: "(local class) A", ranges: [r0] };
14+
const imports = { definition: 'import A = require("./a")', ranges: [r1, r2] };
15+
verify.referenceGroups([r0], [defs, imports]);
16+
verify.referenceGroups([r1, r2], [imports, defs]);
17+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
5+
// @Filename: /a.js
6+
////exports.[|{| "isWriteAccess": true, "isDefinition": true |}A|] = class {};
7+
8+
// @Filename: /b.js
9+
////import { [|{| "isWriteAccess": true, "isDefinition": true |}A|] } from "./a";
10+
////[|A|];
11+
12+
const [r0, r1, r2] = test.ranges();
13+
const defs = { definition: "(property) A: typeof (Anonymous class)", ranges: [r0] };
14+
const imports = { definition: "import A", ranges: [r1, r2] };
15+
verify.referenceGroups([r0], [defs, imports]);
16+
verify.referenceGroups([r1, r2], [imports, defs]);

0 commit comments

Comments
 (0)