Skip to content

Commit e7e69ad

Browse files
author
Andy
authored
Fix bug in importTracker: default and namespace imports are not exclusive (microsoft#24965)
1 parent 581d2e8 commit e7e69ad

File tree

3 files changed

+46
-39
lines changed

3 files changed

+46
-39
lines changed

src/services/importTracker.ts

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -103,23 +103,17 @@ namespace ts.FindAllReferences {
103103
break; // TODO: GH#23879
104104

105105
case SyntaxKind.ImportEqualsDeclaration:
106-
handleNamespaceImport(direct, direct.name, hasModifier(direct, ModifierFlags.Export));
106+
handleNamespaceImport(direct, direct.name, hasModifier(direct, ModifierFlags.Export), /*alreadyAddedDirect*/ false);
107107
break;
108108

109109
case SyntaxKind.ImportDeclaration:
110+
directImports.push(direct);
110111
const namedBindings = direct.importClause && direct.importClause.namedBindings;
111112
if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) {
112-
handleNamespaceImport(direct, namedBindings.name);
113+
handleNamespaceImport(direct, namedBindings.name, /*isReExport*/ false, /*alreadyAddedDirect*/ true);
113114
}
114-
else if (isDefaultImport(direct)) {
115-
const sourceFileLike = getSourceFileLikeForImportDeclaration(direct);
116-
if (!isAvailableThroughGlobal) {
117-
addIndirectUser(sourceFileLike); // Add a check for indirect uses to handle synthetic default imports
118-
}
119-
directImports.push(direct);
120-
}
121-
else {
122-
directImports.push(direct);
115+
else if (!isAvailableThroughGlobal && isDefaultImport(direct)) {
116+
addIndirectUser(getSourceFileLikeForImportDeclaration(direct)); // Add a check for indirect uses to handle synthetic default imports
123117
}
124118
break;
125119

@@ -145,10 +139,10 @@ namespace ts.FindAllReferences {
145139
}
146140
}
147141

148-
function handleNamespaceImport(importDeclaration: ImportEqualsDeclaration | ImportDeclaration, name: Identifier, isReExport?: boolean): void {
142+
function handleNamespaceImport(importDeclaration: ImportEqualsDeclaration | ImportDeclaration, name: Identifier, isReExport: boolean, alreadyAddedDirect: boolean): void {
149143
if (exportKind === ExportKind.ExportEquals) {
150144
// This is a direct import, not import-as-namespace.
151-
directImports.push(importDeclaration);
145+
if (!alreadyAddedDirect) directImports.push(importDeclaration);
152146
}
153147
else if (!isAvailableThroughGlobal) {
154148
const sourceFileLike = getSourceFileLikeForImportDeclaration(importDeclaration);
@@ -247,34 +241,30 @@ namespace ts.FindAllReferences {
247241
return;
248242
}
249243

250-
const { importClause } = decl;
251-
if (!importClause) {
252-
return;
253-
}
244+
const { name, namedBindings } = decl.importClause || { name: undefined, namedBindings: undefined };
254245

255-
const { namedBindings } = importClause;
256-
if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) {
257-
handleNamespaceImportLike(namedBindings.name);
258-
return;
259-
}
260-
261-
if (exportKind === ExportKind.Named) {
262-
searchForNamedImport(namedBindings as NamedImports | undefined); // tslint:disable-line no-unnecessary-type-assertion (TODO: GH#18217)
263-
}
264-
else {
265-
// `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals
266-
const { name } = importClause;
267-
// If a default import has the same name as the default export, allow to rename it.
268-
// Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that.
269-
if (name && (!isForRename || name.escapedText === symbolEscapedNameNoDefault(exportSymbol))) {
270-
const defaultImportAlias = checker.getSymbolAtLocation(name)!;
271-
addSearch(name, defaultImportAlias);
246+
if (namedBindings) {
247+
switch (namedBindings.kind) {
248+
case SyntaxKind.NamespaceImport:
249+
handleNamespaceImportLike(namedBindings.name);
250+
break;
251+
case SyntaxKind.NamedImports:
252+
// 'default' might be accessed as a named import `{ default as foo }`.
253+
if (exportKind === ExportKind.Named || exportKind === ExportKind.Default) {
254+
searchForNamedImport(namedBindings);
255+
}
256+
break;
257+
default:
258+
Debug.assertNever(namedBindings);
272259
}
260+
}
273261

274-
// 'default' might be accessed as a named import `{ default as foo }`.
275-
if (exportKind === ExportKind.Default) {
276-
searchForNamedImport(namedBindings);
277-
}
262+
// `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals.
263+
// If a default import has the same name as the default export, allow to rename it.
264+
// Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that.
265+
if (name && (exportKind === ExportKind.Default || exportKind === ExportKind.ExportEquals) && (!isForRename || name.escapedText === symbolEscapedNameNoDefault(exportSymbol))) {
266+
const defaultImportAlias = checker.getSymbolAtLocation(name)!;
267+
addSearch(name, defaultImportAlias);
278268
}
279269
}
280270

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /a.ts
4+
////export default function [|{| "isWriteAccess": true, "isDefinition": true |}a|]() {}
5+
6+
// @Filename: /b.ts
7+
////import [|{| "isWriteAccess": true, "isDefinition": true |}a|], * as ns from "./a";
8+
9+
const [r0, r1] = test.ranges();
10+
const a: FourSlashInterface.ReferenceGroup = { definition: "function a(): void", ranges: [r0] };
11+
const b: FourSlashInterface.ReferenceGroup = { definition: "(alias) function a(): void\nimport a", ranges: [r1] };
12+
verify.referenceGroups(r0, [a, b]);
13+
verify.referenceGroups(r1, [b, a]);

tests/cases/fourslash/fourslash.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ declare namespace FourSlashInterface {
255255
* For each of starts, asserts the ranges that are referenced from there.
256256
* This uses the 'findReferences' command instead of 'getReferencesAtPosition', so references are grouped by their definition.
257257
*/
258-
referenceGroups(starts: ArrayOrSingle<string> | ArrayOrSingle<Range>, parts: Array<{ definition: ReferencesDefinition, ranges: Range[] }>): void;
258+
referenceGroups(starts: ArrayOrSingle<string> | ArrayOrSingle<Range>, parts: Array<FourSlashInterface.ReferenceGroup>): void;
259259
singleReferenceGroup(definition: ReferencesDefinition, ranges?: Range[]): void;
260260
rangesAreOccurrences(isWriteAccess?: boolean): void;
261261
rangesWithSameTextAreRenameLocations(): void;
@@ -511,6 +511,10 @@ declare namespace FourSlashInterface {
511511
text: string;
512512
range: Range;
513513
}
514+
interface ReferenceGroup {
515+
readonly definition: ReferencesDefinition;
516+
readonly ranges: ReadonlyArray<Range>;
517+
}
514518
interface Diagnostic {
515519
message: string;
516520
/** @default `test.ranges()[0]` */

0 commit comments

Comments
 (0)