Refactor named imports to default instead of namespace when esModuleInterop is on and module is an export= (#47744)

This commit is contained in:
Andrew Branch 2022-02-04 17:11:25 -08:00 committed by GitHub
parent 8ddead50eb
commit 9c3b41d3cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 115 additions and 36 deletions

View File

@ -1252,11 +1252,11 @@ namespace ts {
return result;
}
export function getOwnValues<T>(sparseArray: T[]): T[] {
export function getOwnValues<T>(collection: MapLike<T> | T[]): T[] {
const values: T[] = [];
for (const key in sparseArray) {
if (hasOwnProperty.call(sparseArray, key)) {
values.push(sparseArray[key]);
for (const key in collection) {
if (hasOwnProperty.call(collection, key)) {
values.push((collection as MapLike<T>)[key]);
}
}

View File

@ -7123,6 +7123,10 @@
"category": "Message",
"code": 95169
},
"Convert named imports to default import": {
"category": "Message",
"code": 95170
},
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",

View File

@ -2,46 +2,48 @@
namespace ts.refactor {
const refactorName = "Convert import";
const namespaceToNamedAction = {
name: "Convert namespace import to named imports",
description: Diagnostics.Convert_namespace_import_to_named_imports.message,
kind: "refactor.rewrite.import.named",
};
const namedToNamespaceAction = {
name: "Convert named imports to namespace import",
description: Diagnostics.Convert_named_imports_to_namespace_import.message,
kind: "refactor.rewrite.import.namespace",
const actions = {
[ImportKind.Named]: {
name: "Convert namespace import to named imports",
description: Diagnostics.Convert_namespace_import_to_named_imports.message,
kind: "refactor.rewrite.import.named",
},
[ImportKind.Namespace]: {
name: "Convert named imports to namespace import",
description: Diagnostics.Convert_named_imports_to_namespace_import.message,
kind: "refactor.rewrite.import.namespace",
},
[ImportKind.Default]: {
name: "Convert named imports to default import",
description: Diagnostics.Convert_named_imports_to_default_import.message,
kind: "refactor.rewrite.import.default",
},
};
registerRefactor(refactorName, {
kinds: [
namespaceToNamedAction.kind,
namedToNamespaceAction.kind
],
kinds: getOwnValues(actions).map(a => a.kind),
getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndNamespacedImports(context): readonly ApplicableRefactorInfo[] {
const info = getImportToConvert(context, context.triggerReason === "invoked");
const info = getImportConversionInfo(context, context.triggerReason === "invoked");
if (!info) return emptyArray;
if (!isRefactorErrorInfo(info)) {
const namespaceImport = info.kind === SyntaxKind.NamespaceImport;
const action = namespaceImport ? namespaceToNamedAction : namedToNamespaceAction;
const action = actions[info.convertTo];
return [{ name: refactorName, description: action.description, actions: [action] }];
}
if (context.preferences.provideRefactorNotApplicableReason) {
return [
{ name: refactorName, description: namespaceToNamedAction.description,
actions: [{ ...namespaceToNamedAction, notApplicableReason: info.error }] },
{ name: refactorName, description: namedToNamespaceAction.description,
actions: [{ ...namedToNamespaceAction, notApplicableReason: info.error }] }
];
return getOwnValues(actions).map(action => ({
name: refactorName,
description: action.description,
actions: [{ ...action, notApplicableReason: info.error }]
}));
}
return emptyArray;
},
getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndNamespacedImports(context, actionName): RefactorEditInfo {
Debug.assert(actionName === namespaceToNamedAction.name || actionName === namedToNamespaceAction.name, "Unexpected action name");
const info = getImportToConvert(context);
Debug.assert(some(getOwnValues(actions), action => action.name === actionName), "Unexpected action name");
const info = getImportConversionInfo(context);
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info));
return { edits, renameFilename: undefined, renameLocation: undefined };
@ -49,7 +51,12 @@ namespace ts.refactor {
});
// Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`.
function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindings | RefactorErrorInfo | undefined {
type ImportConversionInfo =
| { convertTo: ImportKind.Default, import: NamedImports }
| { convertTo: ImportKind.Namespace, import: NamedImports }
| { convertTo: ImportKind.Named, import: NamespaceImport };
function getImportConversionInfo(context: RefactorContext, considerPartialSpans = true): ImportConversionInfo | RefactorErrorInfo | undefined {
const { file } = context;
const span = getRefactorContextSpan(context);
const token = getTokenAtPosition(file, span.start);
@ -69,16 +76,25 @@ namespace ts.refactor {
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) };
}
return importClause.namedBindings;
if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) {
return { convertTo: ImportKind.Named, import: importClause.namedBindings };
}
const compilerOptions = context.program.getCompilerOptions();
const shouldUseDefault = getAllowSyntheticDefaultImports(compilerOptions)
&& isExportEqualsModule(importClause.parent.moduleSpecifier, context.program.getTypeChecker());
return shouldUseDefault
? { convertTo: ImportKind.Default, import: importClause.namedBindings }
: { convertTo: ImportKind.Namespace, import: importClause.namedBindings };
}
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void {
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, info: ImportConversionInfo): void {
const checker = program.getTypeChecker();
if (toConvert.kind === SyntaxKind.NamespaceImport) {
doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
if (info.convertTo === ImportKind.Named) {
doChangeNamespaceToNamed(sourceFile, checker, changes, info.import, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
}
else {
doChangeNamedToNamespace(sourceFile, checker, changes, toConvert);
doChangeNamedToNamespaceOrDefault(sourceFile, checker, changes, info.import, info.convertTo === ImportKind.Default);
}
}
@ -137,7 +153,7 @@ namespace ts.refactor {
return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left;
}
function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports): void {
function doChangeNamedToNamespaceOrDefault(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports, shouldUseDefault: boolean) {
const importDecl = toConvert.parent.parent;
const { moduleSpecifier } = importDecl;
@ -188,7 +204,9 @@ namespace ts.refactor {
});
}
changes.replaceNode(sourceFile, toConvert, factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
changes.replaceNode(sourceFile, toConvert, shouldUseDefault
? factory.createIdentifier(namespaceImportName)
: factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
if (neededNamedImports.size) {
const newNamedImports: ImportSpecifier[] = arrayFrom(neededNamedImports.values()).map(element =>
factory.createImportSpecifier(element.isTypeOnly, element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
@ -196,6 +214,13 @@ namespace ts.refactor {
}
}
function isExportEqualsModule(moduleSpecifier: Expression, checker: TypeChecker) {
const externalModule = checker.resolveExternalModuleName(moduleSpecifier);
if (!externalModule) return false;
const exportEquals = checker.resolveExternalModuleSymbol(externalModule);
return externalModule !== exportEquals;
}
function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration {
return factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined,
factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? factory.createNamedImports(elements) : undefined), old.moduleSpecifier, /*assertClause*/ undefined);

View File

@ -0,0 +1,50 @@
/// <reference path="fourslash.ts" />
// @esModuleInterop: true
// @Filename: /process.d.ts
//// declare module "process" {
//// interface Process {
//// pid: number;
//// addListener(event: string, listener: (...args: any[]) => void): void;
//// }
//// var process: Process;
//// export = process;
//// }
// @Filename: /url.d.ts
//// declare module "url" {
//// export function parse(urlStr: string): any;
//// }
// @Filename: /index.ts
//// [|import { pid, addListener } from "process";|]
//// addListener("message", (m) => {
//// console.log(pid);
//// });
// @Filename: /a.ts
//// [|import { parse } from "url";|]
//// parse("https://www.typescriptlang.org");
goTo.selectRange(test.ranges()[0]);
edit.applyRefactor({
refactorName: "Convert import",
actionName: "Convert named imports to default import",
actionDescription: "Convert named imports to default import",
newContent: `import process from "process";
process.addListener("message", (m) => {
console.log(process.pid);
});`,
});
verify.not.refactorAvailable("Convert import", "Convert named imports to namespace import");
goTo.selectRange(test.ranges()[1]);
edit.applyRefactor({
refactorName: "Convert import",
actionName: "Convert named imports to namespace import",
actionDescription: "Convert named imports to namespace import",
newContent: `import * as url from "url";
url.parse("https://www.typescriptlang.org");`,
});
verify.not.refactorAvailable("Convert import", "Convert named imports to default import");