Skip to content

Commit 9c3b41d

Browse files
authored
Refactor named imports to default instead of namespace when esModuleInterop is on and module is an export= (#47744)
1 parent 8ddead5 commit 9c3b41d

File tree

4 files changed

+115
-36
lines changed

4 files changed

+115
-36
lines changed

src/compiler/core.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,11 +1252,11 @@ namespace ts {
12521252
return result;
12531253
}
12541254

1255-
export function getOwnValues<T>(sparseArray: T[]): T[] {
1255+
export function getOwnValues<T>(collection: MapLike<T> | T[]): T[] {
12561256
const values: T[] = [];
1257-
for (const key in sparseArray) {
1258-
if (hasOwnProperty.call(sparseArray, key)) {
1259-
values.push(sparseArray[key]);
1257+
for (const key in collection) {
1258+
if (hasOwnProperty.call(collection, key)) {
1259+
values.push((collection as MapLike<T>)[key]);
12601260
}
12611261
}
12621262

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7123,6 +7123,10 @@
71237123
"category": "Message",
71247124
"code": 95169
71257125
},
7126+
"Convert named imports to default import": {
7127+
"category": "Message",
7128+
"code": 95170
7129+
},
71267130

71277131
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
71287132
"category": "Error",

src/services/refactors/convertImport.ts

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,61 @@
22
namespace ts.refactor {
33
const refactorName = "Convert import";
44

5-
const namespaceToNamedAction = {
6-
name: "Convert namespace import to named imports",
7-
description: Diagnostics.Convert_namespace_import_to_named_imports.message,
8-
kind: "refactor.rewrite.import.named",
9-
};
10-
const namedToNamespaceAction = {
11-
name: "Convert named imports to namespace import",
12-
description: Diagnostics.Convert_named_imports_to_namespace_import.message,
13-
kind: "refactor.rewrite.import.namespace",
5+
const actions = {
6+
[ImportKind.Named]: {
7+
name: "Convert namespace import to named imports",
8+
description: Diagnostics.Convert_namespace_import_to_named_imports.message,
9+
kind: "refactor.rewrite.import.named",
10+
},
11+
[ImportKind.Namespace]: {
12+
name: "Convert named imports to namespace import",
13+
description: Diagnostics.Convert_named_imports_to_namespace_import.message,
14+
kind: "refactor.rewrite.import.namespace",
15+
},
16+
[ImportKind.Default]: {
17+
name: "Convert named imports to default import",
18+
description: Diagnostics.Convert_named_imports_to_default_import.message,
19+
kind: "refactor.rewrite.import.default",
20+
},
1421
};
1522

1623
registerRefactor(refactorName, {
17-
kinds: [
18-
namespaceToNamedAction.kind,
19-
namedToNamespaceAction.kind
20-
],
24+
kinds: getOwnValues(actions).map(a => a.kind),
2125
getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndNamespacedImports(context): readonly ApplicableRefactorInfo[] {
22-
const info = getImportToConvert(context, context.triggerReason === "invoked");
26+
const info = getImportConversionInfo(context, context.triggerReason === "invoked");
2327
if (!info) return emptyArray;
2428

2529
if (!isRefactorErrorInfo(info)) {
26-
const namespaceImport = info.kind === SyntaxKind.NamespaceImport;
27-
const action = namespaceImport ? namespaceToNamedAction : namedToNamespaceAction;
30+
const action = actions[info.convertTo];
2831
return [{ name: refactorName, description: action.description, actions: [action] }];
2932
}
3033

3134
if (context.preferences.provideRefactorNotApplicableReason) {
32-
return [
33-
{ name: refactorName, description: namespaceToNamedAction.description,
34-
actions: [{ ...namespaceToNamedAction, notApplicableReason: info.error }] },
35-
{ name: refactorName, description: namedToNamespaceAction.description,
36-
actions: [{ ...namedToNamespaceAction, notApplicableReason: info.error }] }
37-
];
35+
return getOwnValues(actions).map(action => ({
36+
name: refactorName,
37+
description: action.description,
38+
actions: [{ ...action, notApplicableReason: info.error }]
39+
}));
3840
}
3941

4042
return emptyArray;
4143
},
4244
getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndNamespacedImports(context, actionName): RefactorEditInfo {
43-
Debug.assert(actionName === namespaceToNamedAction.name || actionName === namedToNamespaceAction.name, "Unexpected action name");
44-
const info = getImportToConvert(context);
45+
Debug.assert(some(getOwnValues(actions), action => action.name === actionName), "Unexpected action name");
46+
const info = getImportConversionInfo(context);
4547
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
4648
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info));
4749
return { edits, renameFilename: undefined, renameLocation: undefined };
4850
}
4951
});
5052

5153
// Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`.
52-
function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindings | RefactorErrorInfo | undefined {
54+
type ImportConversionInfo =
55+
| { convertTo: ImportKind.Default, import: NamedImports }
56+
| { convertTo: ImportKind.Namespace, import: NamedImports }
57+
| { convertTo: ImportKind.Named, import: NamespaceImport };
58+
59+
function getImportConversionInfo(context: RefactorContext, considerPartialSpans = true): ImportConversionInfo | RefactorErrorInfo | undefined {
5360
const { file } = context;
5461
const span = getRefactorContextSpan(context);
5562
const token = getTokenAtPosition(file, span.start);
@@ -69,16 +76,25 @@ namespace ts.refactor {
6976
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) };
7077
}
7178

72-
return importClause.namedBindings;
79+
if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) {
80+
return { convertTo: ImportKind.Named, import: importClause.namedBindings };
81+
}
82+
const compilerOptions = context.program.getCompilerOptions();
83+
const shouldUseDefault = getAllowSyntheticDefaultImports(compilerOptions)
84+
&& isExportEqualsModule(importClause.parent.moduleSpecifier, context.program.getTypeChecker());
85+
86+
return shouldUseDefault
87+
? { convertTo: ImportKind.Default, import: importClause.namedBindings }
88+
: { convertTo: ImportKind.Namespace, import: importClause.namedBindings };
7389
}
7490

75-
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void {
91+
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, info: ImportConversionInfo): void {
7692
const checker = program.getTypeChecker();
77-
if (toConvert.kind === SyntaxKind.NamespaceImport) {
78-
doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
93+
if (info.convertTo === ImportKind.Named) {
94+
doChangeNamespaceToNamed(sourceFile, checker, changes, info.import, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
7995
}
8096
else {
81-
doChangeNamedToNamespace(sourceFile, checker, changes, toConvert);
97+
doChangeNamedToNamespaceOrDefault(sourceFile, checker, changes, info.import, info.convertTo === ImportKind.Default);
8298
}
8399
}
84100

@@ -137,7 +153,7 @@ namespace ts.refactor {
137153
return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left;
138154
}
139155

140-
function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports): void {
156+
function doChangeNamedToNamespaceOrDefault(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports, shouldUseDefault: boolean) {
141157
const importDecl = toConvert.parent.parent;
142158
const { moduleSpecifier } = importDecl;
143159

@@ -188,14 +204,23 @@ namespace ts.refactor {
188204
});
189205
}
190206

191-
changes.replaceNode(sourceFile, toConvert, factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
207+
changes.replaceNode(sourceFile, toConvert, shouldUseDefault
208+
? factory.createIdentifier(namespaceImportName)
209+
: factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
192210
if (neededNamedImports.size) {
193211
const newNamedImports: ImportSpecifier[] = arrayFrom(neededNamedImports.values()).map(element =>
194212
factory.createImportSpecifier(element.isTypeOnly, element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
195213
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports));
196214
}
197215
}
198216

217+
function isExportEqualsModule(moduleSpecifier: Expression, checker: TypeChecker) {
218+
const externalModule = checker.resolveExternalModuleName(moduleSpecifier);
219+
if (!externalModule) return false;
220+
const exportEquals = checker.resolveExternalModuleSymbol(externalModule);
221+
return externalModule !== exportEquals;
222+
}
223+
199224
function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration {
200225
return factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined,
201226
factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? factory.createNamedImports(elements) : undefined), old.moduleSpecifier, /*assertClause*/ undefined);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @esModuleInterop: true
4+
5+
// @Filename: /process.d.ts
6+
//// declare module "process" {
7+
//// interface Process {
8+
//// pid: number;
9+
//// addListener(event: string, listener: (...args: any[]) => void): void;
10+
//// }
11+
//// var process: Process;
12+
//// export = process;
13+
//// }
14+
15+
// @Filename: /url.d.ts
16+
//// declare module "url" {
17+
//// export function parse(urlStr: string): any;
18+
//// }
19+
20+
// @Filename: /index.ts
21+
//// [|import { pid, addListener } from "process";|]
22+
//// addListener("message", (m) => {
23+
//// console.log(pid);
24+
//// });
25+
26+
// @Filename: /a.ts
27+
//// [|import { parse } from "url";|]
28+
//// parse("https://www.typescriptlang.org");
29+
30+
goTo.selectRange(test.ranges()[0]);
31+
edit.applyRefactor({
32+
refactorName: "Convert import",
33+
actionName: "Convert named imports to default import",
34+
actionDescription: "Convert named imports to default import",
35+
newContent: `import process from "process";
36+
process.addListener("message", (m) => {
37+
console.log(process.pid);
38+
});`,
39+
});
40+
verify.not.refactorAvailable("Convert import", "Convert named imports to namespace import");
41+
42+
goTo.selectRange(test.ranges()[1]);
43+
edit.applyRefactor({
44+
refactorName: "Convert import",
45+
actionName: "Convert named imports to namespace import",
46+
actionDescription: "Convert named imports to namespace import",
47+
newContent: `import * as url from "url";
48+
url.parse("https://www.typescriptlang.org");`,
49+
});
50+
verify.not.refactorAvailable("Convert import", "Convert named imports to default import");

0 commit comments

Comments
 (0)