1/* @internal */ 2namespace ts.refactor { 3 const refactorName = "Convert import"; 4 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", 14 }; 15 16 registerRefactor(refactorName, { 17 kinds: [ 18 namespaceToNamedAction.kind, 19 namedToNamespaceAction.kind 20 ], 21 getAvailableActions(context): readonly ApplicableRefactorInfo[] { 22 const info = getImportToConvert(context, context.triggerReason === "invoked"); 23 if (!info) return emptyArray; 24 25 if (!isRefactorErrorInfo(info)) { 26 const namespaceImport = info.kind === SyntaxKind.NamespaceImport; 27 const action = namespaceImport ? namespaceToNamedAction : namedToNamespaceAction; 28 return [{ name: refactorName, description: action.description, actions: [action] }]; 29 } 30 31 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 ]; 38 } 39 40 return emptyArray; 41 }, 42 getEditsForAction(context, actionName): RefactorEditInfo { 43 Debug.assert(actionName === namespaceToNamedAction.name || actionName === namedToNamespaceAction.name, "Unexpected action name"); 44 const info = getImportToConvert(context); 45 Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); 46 const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info)); 47 return { edits, renameFilename: undefined, renameLocation: undefined }; 48 } 49 }); 50 51 // 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 { 53 const { file } = context; 54 const span = getRefactorContextSpan(context); 55 const token = getTokenAtPosition(file, span.start); 56 const importDecl = considerPartialSpans ? findAncestor(token, isImportDeclaration) : getParentNodeInSpan(token, file, span); 57 if (!importDecl || !isImportDeclaration(importDecl)) return { error: "Selection is not an import declaration." }; 58 if (importDecl.getEnd() < span.start + span.length) return undefined; 59 60 const { importClause } = importDecl; 61 if (!importClause) { 62 return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_import_clause) }; 63 } 64 65 if (!importClause.namedBindings) { 66 return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) }; 67 } 68 69 return importClause.namedBindings; 70 } 71 72 function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void { 73 const checker = program.getTypeChecker(); 74 if (toConvert.kind === SyntaxKind.NamespaceImport) { 75 doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions())); 76 } 77 else { 78 doChangeNamedToNamespace(sourceFile, checker, changes, toConvert); 79 } 80 } 81 82 function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void { 83 let usedAsNamespaceOrDefault = false; 84 85 const nodesToReplace: (PropertyAccessExpression | QualifiedName)[] = []; 86 const conflictingNames = new Map<string, true>(); 87 88 FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => { 89 if (!isPropertyAccessOrQualifiedName(id.parent)) { 90 usedAsNamespaceOrDefault = true; 91 } 92 else { 93 const exportName = getRightOfPropertyAccessOrQualifiedName(id.parent).text; 94 if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) { 95 conflictingNames.set(exportName, true); 96 } 97 Debug.assert(getLeftOfPropertyAccessOrQualifiedName(id.parent) === id, "Parent expression should match id"); 98 nodesToReplace.push(id.parent); 99 } 100 }); 101 102 // We may need to change `mod.x` to `_x` to avoid a name conflict. 103 const exportNameToImportName = new Map<string, string>(); 104 105 for (const propertyAccessOrQualifiedName of nodesToReplace) { 106 const exportName = getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName).text; 107 let importName = exportNameToImportName.get(exportName); 108 if (importName === undefined) { 109 exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName); 110 } 111 changes.replaceNode(sourceFile, propertyAccessOrQualifiedName, factory.createIdentifier(importName)); 112 } 113 114 const importSpecifiers: ImportSpecifier[] = []; 115 exportNameToImportName.forEach((name, propertyName) => { 116 importSpecifiers.push(factory.createImportSpecifier(name === propertyName ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name))); 117 }); 118 119 const importDecl = toConvert.parent.parent; 120 if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) { 121 // Need to leave the namespace import alone 122 changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers)); 123 } 124 else { 125 changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? factory.createIdentifier(toConvert.name.text) : undefined, importSpecifiers)); 126 } 127 } 128 129 function getRightOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) { 130 return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.name : propertyAccessOrQualifiedName.right; 131 } 132 133 function getLeftOfPropertyAccessOrQualifiedName(propertyAccessOrQualifiedName: PropertyAccessExpression | QualifiedName) { 134 return isPropertyAccessExpression(propertyAccessOrQualifiedName) ? propertyAccessOrQualifiedName.expression : propertyAccessOrQualifiedName.left; 135 } 136 137 function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports): void { 138 const importDecl = toConvert.parent.parent; 139 const { moduleSpecifier } = importDecl; 140 141 const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module"; 142 const namespaceNameConflicts = toConvert.elements.some(element => 143 FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => 144 !!checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true)) || false); 145 const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName; 146 147 const neededNamedImports: ImportSpecifier[] = []; 148 149 for (const element of toConvert.elements) { 150 const propertyName = (element.propertyName || element.name).text; 151 FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => { 152 const access = factory.createPropertyAccessExpression(factory.createIdentifier(namespaceImportName), propertyName); 153 if (isShorthandPropertyAssignment(id.parent)) { 154 changes.replaceNode(sourceFile, id.parent, factory.createPropertyAssignment(id.text, access)); 155 } 156 else if (isExportSpecifier(id.parent) && !id.parent.propertyName) { 157 if (!neededNamedImports.some(n => n.name === element.name)) { 158 neededNamedImports.push(factory.createImportSpecifier(element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text))); 159 } 160 } 161 else { 162 changes.replaceNode(sourceFile, id, access); 163 } 164 }); 165 } 166 167 changes.replaceNode(sourceFile, toConvert, factory.createNamespaceImport(factory.createIdentifier(namespaceImportName))); 168 if (neededNamedImports.length) { 169 changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports)); 170 } 171 } 172 173 function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration { 174 return factory.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, 175 factory.createImportClause(/*isTypeOnly*/ false, defaultImportName, elements && elements.length ? factory.createNamedImports(elements) : undefined), old.moduleSpecifier); 176 } 177} 178