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