1/* @internal */ 2namespace ts.refactor { 3 const refactorName = "Convert import"; 4 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 }, 21 }; 22 23 registerRefactor(refactorName, { 24 kinds: getOwnValues(actions).map(a => a.kind), 25 getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndNamespacedImports(context): readonly ApplicableRefactorInfo[] { 26 const info = getImportConversionInfo(context, context.triggerReason === "invoked"); 27 if (!info) return emptyArray; 28 29 if (!isRefactorErrorInfo(info)) { 30 const action = actions[info.convertTo]; 31 return [{ name: refactorName, description: action.description, actions: [action] }]; 32 } 33 34 if (context.preferences.provideRefactorNotApplicableReason) { 35 return getOwnValues(actions).map(action => ({ 36 name: refactorName, 37 description: action.description, 38 actions: [{ ...action, notApplicableReason: info.error }] 39 })); 40 } 41 42 return emptyArray; 43 }, 44 getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndNamespacedImports(context, actionName): RefactorEditInfo { 45 Debug.assert(some(getOwnValues(actions), action => action.name === actionName), "Unexpected action name"); 46 const info = getImportConversionInfo(context); 47 Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); 48 const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info)); 49 return { edits, renameFilename: undefined, renameLocation: undefined }; 50 } 51 }); 52 53 // Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. 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 { 60 const { file } = context; 61 const span = getRefactorContextSpan(context); 62 const token = getTokenAtPosition(file, span.start); 63 const importDecl = considerPartialSpans ? findAncestor(token, isImportDeclaration) : getParentNodeInSpan(token, file, span); 64 if (!importDecl || !isImportDeclaration(importDecl)) return { error: "Selection is not an import declaration." }; 65 66 const end = span.start + span.length; 67 const nextToken = findNextToken(importDecl, importDecl.parent, file); 68 if (nextToken && end > nextToken.getStart()) return undefined; 69 70 const { importClause } = importDecl; 71 if (!importClause) { 72 return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_import_clause) }; 73 } 74 75 if (!importClause.namedBindings) { 76 return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) }; 77 } 78 79 if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { 80 return { convertTo: ImportKind.Named, import: importClause.namedBindings }; 81 } 82 const shouldUseDefault = getShouldUseDefault(context.program, importClause); 83 84 return shouldUseDefault 85 ? { convertTo: ImportKind.Default, import: importClause.namedBindings } 86 : { convertTo: ImportKind.Namespace, import: importClause.namedBindings }; 87 } 88 89 function getShouldUseDefault(program: Program, importClause: ImportClause) { 90 return getAllowSyntheticDefaultImports(program.getCompilerOptions()) 91 && isExportEqualsModule(importClause.parent.moduleSpecifier, program.getTypeChecker()); 92 } 93 94 function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, info: ImportConversionInfo): void { 95 const checker = program.getTypeChecker(); 96 if (info.convertTo === ImportKind.Named) { 97 doChangeNamespaceToNamed(sourceFile, checker, changes, info.import, getAllowSyntheticDefaultImports(program.getCompilerOptions())); 98 } 99 else { 100 doChangeNamedToNamespaceOrDefault(sourceFile, program, changes, info.import, info.convertTo === ImportKind.Default); 101 } 102 } 103 104 function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void { 105 let usedAsNamespaceOrDefault = false; 106 107 const nodesToReplace: (PropertyAccessExpression | QualifiedName)[] = []; 108 const conflictingNames = new Map<string, true>(); 109 110 FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => { 111 if (!isPropertyAccessOrQualifiedName(id.parent)) { 112 usedAsNamespaceOrDefault = true; 113 } 114 else { 115 const exportName = getRightOfPropertyAccessOrQualifiedName(id.parent).text; 116 if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) { 117 conflictingNames.set(exportName, true); 118 } 119 Debug.assert(getLeftOfPropertyAccessOrQualifiedName(id.parent) === id, "Parent expression should match id"); 120 nodesToReplace.push(id.parent); 121 } 122 }); 123 124 // We may need to change `mod.x` to `_x` to avoid a name conflict. 125 const exportNameToImportName = new Map<string, string>(); 126 127 for (const propertyAccessOrQualifiedName of nodesToReplace) { 128 const exportName = getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName).text; 129 let importName = exportNameToImportName.get(exportName); 130 if (importName === undefined) { 131 exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName); 132 } 133 changes.replaceNode(sourceFile, propertyAccessOrQualifiedName, factory.createIdentifier(importName)); 134 } 135 136 const importSpecifiers: ImportSpecifier[] = []; 137 exportNameToImportName.forEach((name, propertyName) => { 138 importSpecifiers.push(factory.createImportSpecifier(/*isTypeOnly*/ false, name === propertyName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name))); 139 }); 140 141 const importDecl = toConvert.parent.parent; 142 if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) { 143 // Need to leave the namespace import alone 144 changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers)); 145 } 146 else { 147 changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? factory.createIdentifier(toConvert.name.text) : undefined, importSpecifiers)); 148 } 149 } 150 151 function getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) { 152 return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.name : propertyAccessOrQualifiedName.right; 153 } 154 155 function getLeftOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) { 156 return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left; 157 } 158 159 export function doChangeNamedToNamespaceOrDefault(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImports, shouldUseDefault = getShouldUseDefault(program, toConvert.parent)): void { 160 const checker = program.getTypeChecker(); 161 const importDecl = toConvert.parent.parent; 162 const { moduleSpecifier } = importDecl; 163 164 const toConvertSymbols: Set<Symbol> = new Set(); 165 toConvert.elements.forEach(namedImport => { 166 const symbol = checker.getSymbolAtLocation(namedImport.name); 167 if (symbol) { 168 toConvertSymbols.add(symbol); 169 } 170 }); 171 const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module"; 172 function hasNamespaceNameConflict(namedImport: ImportSpecifier): boolean { 173 // We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict. 174 // A name conflict means that, in a scope where we would like to use the preferred namespace name, there already exists a symbol with that name in that scope. 175 // We are going to use the namespace name in the scopes the named imports being refactored are referenced, 176 // so we look for conflicts by looking at every reference to those named imports. 177 return !!FindAllReferences.Core.eachSymbolReferenceInFile(namedImport.name, checker, sourceFile, id => { 178 const symbol = checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true); 179 if (symbol) { // There already is a symbol with the same name as the preferred namespace name. 180 if (toConvertSymbols.has(symbol)) { // `preferredName` resolves to a symbol for one of the named import references we are going to transform into namespace import references... 181 return isExportSpecifier(id.parent); // ...but if this reference is an export specifier, it will not be transformed, so it is a conflict; otherwise, it will be renamed and is not a conflict. 182 } 183 return true; // `preferredName` resolves to any other symbol, which will be present in the refactored code and so poses a name conflict. 184 } 185 return false; // There is no symbol with the same name as the preferred namespace name, so no conflict. 186 }); 187 } 188 const namespaceNameConflicts = toConvert.elements.some(hasNamespaceNameConflict); 189 const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName; 190 191 // Imports that need to be kept as named imports in the refactored code, to avoid changing the semantics. 192 // More specifically, those are named imports that appear in named exports in the original code, e.g. `a` in `import { a } from "m"; export { a }`. 193 const neededNamedImports: Set<ImportSpecifier> = new Set(); 194 195 for (const element of toConvert.elements) { 196 const propertyName = (element.propertyName || element.name).text; 197 FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => { 198 const access = factory.createPropertyAccessExpression(factory.createIdentifier(namespaceImportName), propertyName); 199 if (isShorthandPropertyAssignment(id.parent)) { 200 changes.replaceNode(sourceFile, id.parent, factory.createPropertyAssignment(id.text, access)); 201 } 202 else if (isExportSpecifier(id.parent)) { 203 neededNamedImports.add(element); 204 } 205 else { 206 changes.replaceNode(sourceFile, id, access); 207 } 208 }); 209 } 210 211 changes.replaceNode(sourceFile, toConvert, shouldUseDefault 212 ? factory.createIdentifier(namespaceImportName) 213 : factory.createNamespaceImport(factory.createIdentifier(namespaceImportName))); 214 if (neededNamedImports.size) { 215 const newNamedImports: ImportSpecifier[] = arrayFrom(neededNamedImports.values()).map(element => 216 factory.createImportSpecifier(element.isTypeOnly, element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text))); 217 changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports)); 218 } 219 } 220 221 function isExportEqualsModule(moduleSpecifier: Expression, checker: TypeChecker) { 222 const externalModule = checker.resolveExternalModuleName(moduleSpecifier); 223 if (!externalModule) return false; 224 const exportEquals = checker.resolveExternalModuleSymbol(externalModule); 225 return externalModule !== exportEquals; 226 } 227 228 function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration { 229 return factory.createImportDeclaration(/*modifiers*/ undefined, 230 factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? factory.createNamedImports(elements) : undefined), old.moduleSpecifier, /*assertClause*/ undefined); 231 } 232} 233