Skip to content

Commit b3c87aa

Browse files
author
Andy
authored
Support find-all-references for default keyword (#17992)
* Support find-all-references for anonymous default exports * Also handle re-exported default exports * Add test for using `export =` with `--allowSyntheticDefaultExports`
1 parent 817c329 commit b3c87aa

8 files changed

+159
-44
lines changed

src/compiler/checker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22987,6 +22987,9 @@ namespace ts {
2298722987
: undefined;
2298822988
return objectType && getPropertyOfType(objectType, escapeLeadingUnderscores((node as StringLiteral | NumericLiteral).text));
2298922989

22990+
case SyntaxKind.DefaultKeyword:
22991+
return getSymbolOfNode(node.parent);
22992+
2299022993
default:
2299122994
return undefined;
2299222995
}

src/services/findAllReferences.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@ namespace ts.FindAllReferences {
176176
fileName: node.getSourceFile().fileName,
177177
textSpan: getTextSpan(node),
178178
isWriteAccess: isWriteAccess(node),
179-
isDefinition: isAnyDeclarationName(node) || isLiteralComputedPropertyDeclarationName(node),
179+
isDefinition: node.kind === SyntaxKind.DefaultKeyword
180+
|| isAnyDeclarationName(node)
181+
|| isLiteralComputedPropertyDeclarationName(node),
180182
isInString
181183
};
182184
}
@@ -243,7 +245,7 @@ namespace ts.FindAllReferences {
243245

244246
/** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */
245247
function isWriteAccess(node: Node): boolean {
246-
if (isAnyDeclarationName(node)) {
248+
if (node.kind === SyntaxKind.DefaultKeyword || isAnyDeclarationName(node)) {
247249
return true;
248250
}
249251

@@ -743,7 +745,7 @@ namespace ts.FindAllReferences.Core {
743745

744746
function isValidReferencePosition(node: Node, searchSymbolName: string): boolean {
745747
// Compare the length so we filter out strict superstrings of the symbol we are looking for
746-
switch (node && node.kind) {
748+
switch (node.kind) {
747749
case SyntaxKind.Identifier:
748750
return (node as Identifier).text.length === searchSymbolName.length;
749751

@@ -754,6 +756,9 @@ namespace ts.FindAllReferences.Core {
754756
case SyntaxKind.NumericLiteral:
755757
return isLiteralNameOfPropertyDeclarationOrIndexAccess(node as NumericLiteral) && (node as NumericLiteral).text.length === searchSymbolName.length;
756758

759+
case SyntaxKind.DefaultKeyword:
760+
return "default".length === searchSymbolName.length;
761+
757762
default:
758763
return false;
759764
}

src/services/importTracker.ts

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ namespace ts.FindAllReferences {
180180
* But re-exports will be placed in 'singleReferences' since they cannot be locally referenced.
181181
*/
182182
function getSearchesFromDirectImports(directImports: Importer[], exportSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker, isForRename: boolean): Pick<ImportsResult, "importSearches" | "singleReferences"> {
183-
const exportName = exportSymbol.escapedName;
184183
const importSearches: Array<[Identifier, Symbol]> = [];
185184
const singleReferences: Identifier[] = [];
186185
function addSearch(location: Identifier, symbol: Symbol): void {
@@ -218,12 +217,11 @@ namespace ts.FindAllReferences {
218217
return;
219218
}
220219

221-
if (!decl.importClause) {
220+
const { importClause } = decl;
221+
if (!importClause) {
222222
return;
223223
}
224224

225-
const { importClause } = decl;
226-
227225
const { namedBindings } = importClause;
228226
if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) {
229227
handleNamespaceImportLike(namedBindings.name);
@@ -245,7 +243,6 @@ namespace ts.FindAllReferences {
245243

246244
// 'default' might be accessed as a named import `{ default as foo }`.
247245
if (!isForRename && exportKind === ExportKind.Default) {
248-
Debug.assert(exportName === "default");
249246
searchForNamedImport(namedBindings as NamedImports | undefined);
250247
}
251248
}
@@ -258,36 +255,43 @@ namespace ts.FindAllReferences {
258255
*/
259256
function handleNamespaceImportLike(importName: Identifier): void {
260257
// Don't rename an import that already has a different name than the export.
261-
if (exportKind === ExportKind.ExportEquals && (!isForRename || importName.escapedText === exportName)) {
258+
if (exportKind === ExportKind.ExportEquals && (!isForRename || isNameMatch(importName.escapedText))) {
262259
addSearch(importName, checker.getSymbolAtLocation(importName));
263260
}
264261
}
265262

266263
function searchForNamedImport(namedBindings: NamedImportsOrExports | undefined): void {
267-
if (namedBindings) {
268-
for (const element of namedBindings.elements) {
269-
const { name, propertyName } = element;
270-
if ((propertyName || name).escapedText !== exportName) {
271-
continue;
272-
}
264+
if (!namedBindings) {
265+
return;
266+
}
273267

274-
if (propertyName) {
275-
// This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference.
276-
singleReferences.push(propertyName);
277-
if (!isForRename) { // If renaming `foo`, don't touch `bar`, just `foo`.
278-
// Search locally for `bar`.
279-
addSearch(name, checker.getSymbolAtLocation(name));
280-
}
281-
}
282-
else {
283-
const localSymbol = element.kind === SyntaxKind.ExportSpecifier && element.propertyName
284-
? checker.getExportSpecifierLocalTargetSymbol(element) // For re-exporting under a different name, we want to get the re-exported symbol.
285-
: checker.getSymbolAtLocation(name);
286-
addSearch(name, localSymbol);
268+
for (const element of namedBindings.elements) {
269+
const { name, propertyName } = element;
270+
if (!isNameMatch((propertyName || name).escapedText)) {
271+
continue;
272+
}
273+
274+
if (propertyName) {
275+
// This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference.
276+
singleReferences.push(propertyName);
277+
if (!isForRename) { // If renaming `foo`, don't touch `bar`, just `foo`.
278+
// Search locally for `bar`.
279+
addSearch(name, checker.getSymbolAtLocation(name));
287280
}
288281
}
282+
else {
283+
const localSymbol = element.kind === SyntaxKind.ExportSpecifier && element.propertyName
284+
? checker.getExportSpecifierLocalTargetSymbol(element) // For re-exporting under a different name, we want to get the re-exported symbol.
285+
: checker.getSymbolAtLocation(name);
286+
addSearch(name, localSymbol);
287+
}
289288
}
290289
}
290+
291+
function isNameMatch(name: __String): boolean {
292+
// Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports
293+
return name === exportSymbol.escapedName || exportKind !== ExportKind.Named && name === "default";
294+
}
291295
}
292296

293297
/** Returns 'true' is the namespace 'name' is re-exported from this module, and 'false' if it is only used locally. */
@@ -413,7 +417,7 @@ namespace ts.FindAllReferences {
413417
case SyntaxKind.ExternalModuleReference:
414418
return (decl as ExternalModuleReference).parent;
415419
default:
416-
Debug.fail(`Unexpected module specifier parent: ${decl.kind}`);
420+
Debug.fail("Unexpected module specifier parent: " + decl.kind);
417421
}
418422
}
419423

@@ -468,11 +472,11 @@ namespace ts.FindAllReferences {
468472
return exportInfo(symbol, getExportKindForDeclaration(exportNode));
469473
}
470474
}
471-
// If we are in `export = a;`, `parent` is the export assignment.
475+
// If we are in `export = a;` or `export default a;`, `parent` is the export assignment.
472476
else if (isExportAssignment(parent)) {
473477
return getExportAssignmentExport(parent);
474478
}
475-
// If we are in `export = class A {};` at `A`, `parent.parent` is the export assignment.
479+
// If we are in `export = class A {};` (or `export = class A {};`) at `A`, `parent.parent` is the export assignment.
476480
else if (isExportAssignment(parent.parent)) {
477481
return getExportAssignmentExport(parent.parent);
478482
}
@@ -489,7 +493,8 @@ namespace ts.FindAllReferences {
489493
// Get the symbol for the `export =` node; its parent is the module it's the export of.
490494
const exportingModuleSymbol = ex.symbol.parent;
491495
Debug.assert(!!exportingModuleSymbol);
492-
return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol, exportKind: ExportKind.ExportEquals } };
496+
const exportKind = ex.isExportEquals ? ExportKind.ExportEquals : ExportKind.Default;
497+
return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol, exportKind } };
493498
}
494499

495500
function getSpecialPropertyExport(node: ts.BinaryExpression, useLhsSymbol: boolean): ExportedSymbol | undefined {
@@ -525,7 +530,11 @@ namespace ts.FindAllReferences {
525530
importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker);
526531
}
527532

528-
if (symbolName(importedSymbol) === symbol.escapedName) { // If this is a rename import, do not continue searching.
533+
// If the import has a different name than the export, do not continue searching.
534+
// If `importedName` is undefined, do continue searching as the export is anonymous.
535+
// (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.)
536+
const importedName = symbolName(importedSymbol);
537+
if (importedName === undefined || importedName === "default" || importedName === symbol.escapedName) {
529538
return { kind: ImportExport.Import, symbol: importedSymbol, ...isImport };
530539
}
531540
}

tests/cases/fourslash/findAllRefsForDefaultExport04.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@
22

33
// @Filename: /a.ts
44
////const [|{| "isWriteAccess": true, "isDefinition": true |}a|] = 0;
5-
////export default [|a|];
5+
////export [|{| "isWriteAccess": true, "isDefinition": true |}default|] [|a|];
66

77
// @Filename: /b.ts
88
////import [|{| "isWriteAccess": true, "isDefinition": true |}a|] from "./a";
99
////[|a|];
1010

11-
const [r0, r1, r2, r3] = test.ranges();
12-
verify.referenceGroups([r0, r1], [
13-
{ definition: "const a: 0", ranges: [r0, r1] },
14-
{ definition: "import a", ranges: [r2, r3] }
11+
const [r0, r1, r2, r3, r4] = test.ranges();
12+
verify.referenceGroups([r0, r2], [
13+
{ definition: "const a: 0", ranges: [r0, r2] },
14+
{ definition: "import a", ranges: [r3, r4] }
15+
]);
16+
verify.referenceGroups(r1, [
17+
// TODO:GH#17990
18+
{ definition: "import default", ranges: [r1] },
19+
{ definition: "import a", ranges: [r3, r4] },
20+
]);
21+
verify.referenceGroups([r3, r4], [
22+
{ definition: "import a", ranges: [r3, r4] },
23+
// TODO:GH#17990
24+
{ definition: "import default", ranges: [r1] },
1525
]);
16-
verify.singleReferenceGroup("import a", [r2, r3]);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: /a.ts
4+
////export [|{| "isWriteAccess": true, "isDefinition": true |}default|] function() {}
5+
6+
// @Filename: /b.ts
7+
////import [|{| "isWriteAccess": true, "isDefinition": true |}f|] from "./a";
8+
9+
const [r0, r1] = test.ranges();
10+
verify.referenceGroups(r0, [
11+
{ definition: "function default(): void", ranges: [r0] },
12+
{ definition: "import f", ranges: [r1] },
13+
]);
14+
verify.referenceGroups(r1, [
15+
{ definition: "import f", ranges: [r1] },
16+
{ definition: "function default(): void", ranges: [r0] },
17+
]);
18+
19+
// Verify that it doesn't try to rename "default"
20+
goTo.rangeStart(r0);
21+
verify.renameInfoFailed();
22+
verify.renameLocations(r1, [r1]);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: /export.ts
4+
////const [|{| "isWriteAccess": true, "isDefinition": true |}foo|] = 1;
5+
////export default [|foo|];
6+
7+
// @Filename: /re-export.ts
8+
////export { [|{| "isWriteAccess": true, "isDefinition": true |}default|] } from "./export";
9+
10+
// @Filename: /re-export-dep.ts
11+
////import [|{| "isWriteAccess": true, "isDefinition": true |}fooDefault|] from "./re-export";
12+
13+
verify.noErrors();
14+
15+
const [r0, r1, r2, r3] = test.ranges();
16+
verify.referenceGroups([r0, r1], [
17+
{ definition: "const foo: 1", ranges: [r0, r1] },
18+
{ definition: "import default", ranges: [r2], },
19+
{ definition: "import fooDefault", ranges: [r3] },
20+
]);
21+
verify.referenceGroups(r2, [
22+
{ definition: "import default", ranges: [r2] },
23+
{ definition: "import fooDefault", ranges: [r3] },
24+
{ definition: "const foo: 1", ranges: [r0, r1] },
25+
]);
26+
verify.referenceGroups(r3, [
27+
{ definition: "import fooDefault", ranges: [r3] },
28+
{ definition: "import default", ranges: [r2] },
29+
{ definition: "const foo: 1", ranges: [r0, r1] },
30+
]);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowSyntheticDefaultImports: true
4+
5+
// @Filename: /export.ts
6+
////const [|{| "isWriteAccess": true, "isDefinition": true |}foo|] = 1;
7+
////export = [|foo|];
8+
9+
// @Filename: /re-export.ts
10+
////export { [|{| "isWriteAccess": true, "isDefinition": true |}default|] } from "./export";
11+
12+
// @Filename: /re-export-dep.ts
13+
////import [|{| "isWriteAccess": true, "isDefinition": true |}fooDefault|] from "./re-export";
14+
15+
verify.noErrors();
16+
17+
const [r0, r1, r2, r3] = test.ranges();
18+
verify.referenceGroups([r0, r1], [
19+
{ definition: "const foo: 1", ranges: [r0, r1] },
20+
{ definition: "import default", ranges: [r2], },
21+
{ definition: "import fooDefault", ranges: [r3] },
22+
]);
23+
verify.referenceGroups(r2, [
24+
{ definition: "import default", ranges: [r2] },
25+
{ definition: "import fooDefault", ranges: [r3] },
26+
{ definition: "const foo: 1", ranges: [r0, r1] },
27+
]);
28+
verify.referenceGroups(r3, [
29+
{ definition: "import fooDefault", ranges: [r3] },
30+
{ definition: "import default", ranges: [r2] },
31+
{ definition: "const foo: 1", ranges: [r0, r1] },
32+
]);

tests/cases/fourslash/findAllRefsReExports.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,19 @@ verify.referenceGroups(bar2, [{ ...eBar, definition: "(alias) bar(): void\nimpor
3939
verify.referenceGroups([defaultC], [c, d, eBoom, eBaz, eBang]);
4040
verify.referenceGroups(defaultD, [d, eBoom, a, b, eBar,c, eBaz, eBang]);
4141
verify.referenceGroups(defaultE, [c, d, eBoom, eBaz, eBang]);
42-
verify.referenceGroups(baz0, [eBaz]);
43-
verify.referenceGroups(baz1, [{ ...eBaz, definition: "(alias) baz(): void\nimport baz" }]);
42+
verify.referenceGroups(baz0, [eBaz, c, d, eBoom, eBang]);
43+
verify.referenceGroups(baz1, [
44+
{ ...eBaz, definition: "(alias) baz(): void\nimport baz" },
45+
c, d, eBoom, eBang,
46+
]);
4447

4548
verify.referenceGroups(bang0, [eBang]);
4649
verify.referenceGroups(bang1, [{ ...eBang, definition: "(alias) bang(): void\nimport bang" }]);
47-
48-
verify.referenceGroups(boom0, [eBoom]);
49-
verify.referenceGroups(boom1, [{ ...eBoom, definition: "(alias) boom(): void\nimport boom" }]);
50+
verify.referenceGroups(boom0, [eBoom, d, a, b, eBar, c, eBaz, eBang]);
51+
verify.referenceGroups(boom1, [
52+
{ ...eBoom, definition: "(alias) boom(): void\nimport boom" },
53+
d, a, b, eBar, c, eBaz, eBang,
54+
]);
5055

5156
test.rangesByText().forEach((ranges, text) => {
5257
if (text === "default") {

0 commit comments

Comments
 (0)