diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 6bc89ae25eae5..cceaa2abd50b3 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1252,11 +1252,11 @@ namespace ts { return result; } - export function getOwnValues(sparseArray: T[]): T[] { + export function getOwnValues(collection: MapLike | 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)[key]); } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f00183c8ec8cb..b2bee635ae78c 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -7131,6 +7131,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", diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index fedeb986eddd8..3589e3006c1bf 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -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); diff --git a/tests/cases/fourslash/refactorConvertImport_namedToDefault1.ts b/tests/cases/fourslash/refactorConvertImport_namedToDefault1.ts new file mode 100644 index 0000000000000..ecc259c374566 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertImport_namedToDefault1.ts @@ -0,0 +1,50 @@ +/// + +// @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"); \ No newline at end of file