1import { 2 ApplicableRefactorInfo, CancellationToken, ClassDeclaration, Debug, Diagnostics, emptyArray, EnumDeclaration, 3 ExportAssignment, ExportSpecifier, factory, FindAllReferences, findModifier, first, FunctionDeclaration, 4 getLocaleSpecificMessage, getParentNodeInSpan, getRefactorContextSpan, getSyntacticModifierFlags, 5 getTokenAtPosition, Identifier, ImportClause, ImportSpecifier, ImportTypeNode, InterfaceDeclaration, 6 InternalSymbolName, isAmbientModule, isExportAssignment, isExternalModuleAugmentation, isIdentifier, isModuleBlock, 7 isSourceFile, isStringLiteral, makeImport, ModifierFlags, NamespaceDeclaration, Node, NodeFlags, Program, 8 PropertyAccessExpression, QuotePreference, quotePreferenceFromString, RefactorContext, RefactorEditInfo, SourceFile, 9 Symbol, SyntaxKind, textChanges, TypeAliasDeclaration, TypeChecker, VariableStatement, 10} from "../_namespaces/ts"; 11import { isRefactorErrorInfo, RefactorErrorInfo, registerRefactor } from "../_namespaces/ts.refactor"; 12 13const refactorName = "Convert export"; 14 15const defaultToNamedAction = { 16 name: "Convert default export to named export", 17 description: Diagnostics.Convert_default_export_to_named_export.message, 18 kind: "refactor.rewrite.export.named" 19}; 20const namedToDefaultAction = { 21 name: "Convert named export to default export", 22 description: Diagnostics.Convert_named_export_to_default_export.message, 23 kind: "refactor.rewrite.export.default" 24}; 25 26registerRefactor(refactorName, { 27 kinds: [ 28 defaultToNamedAction.kind, 29 namedToDefaultAction.kind 30 ], 31 getAvailableActions: function getRefactorActionsToConvertBetweenNamedAndDefaultExports(context): readonly ApplicableRefactorInfo[] { 32 const info = getInfo(context, context.triggerReason === "invoked"); 33 if (!info) return emptyArray; 34 35 if (!isRefactorErrorInfo(info)) { 36 const action = info.wasDefault ? defaultToNamedAction : namedToDefaultAction; 37 return [{ name: refactorName, description: action.description, actions: [action] }]; 38 } 39 40 if (context.preferences.provideRefactorNotApplicableReason) { 41 return [ 42 { name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [ 43 { ...defaultToNamedAction, notApplicableReason: info.error }, 44 { ...namedToDefaultAction, notApplicableReason: info.error }, 45 ]} 46 ]; 47 } 48 49 return emptyArray; 50 }, 51 getEditsForAction: function getRefactorEditsToConvertBetweenNamedAndDefaultExports(context, actionName): RefactorEditInfo { 52 Debug.assert(actionName === defaultToNamedAction.name || actionName === namedToDefaultAction.name, "Unexpected action name"); 53 const info = getInfo(context); 54 Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info"); 55 const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, info, t, context.cancellationToken)); 56 return { edits, renameFilename: undefined, renameLocation: undefined }; 57 }, 58}); 59 60// If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name. 61type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement | ExportAssignment; 62interface ExportInfo { 63 readonly exportNode: ExportToConvert; 64 readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s. 65 readonly wasDefault: boolean; 66 readonly exportingModuleSymbol: Symbol; 67} 68 69function getInfo(context: RefactorContext, considerPartialSpans = true): ExportInfo | RefactorErrorInfo | undefined { 70 const { file, program } = context; 71 const span = getRefactorContextSpan(context); 72 const token = getTokenAtPosition(file, span.start); 73 const exportNode = !!(token.parent && getSyntacticModifierFlags(token.parent) & ModifierFlags.Export) && considerPartialSpans ? token.parent : getParentNodeInSpan(token, file, span); 74 if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) { 75 return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_export_statement) }; 76 } 77 78 const checker = program.getTypeChecker(); 79 const exportingModuleSymbol = getExportingModuleSymbol(exportNode, checker); 80 const flags = getSyntacticModifierFlags(exportNode) || ((isExportAssignment(exportNode) && !exportNode.isExportEquals) ? ModifierFlags.ExportDefault : ModifierFlags.None); 81 82 const wasDefault = !!(flags & ModifierFlags.Default); 83 // If source file already has a default export, don't offer refactor. 84 if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) { 85 return { error: getLocaleSpecificMessage(Diagnostics.This_file_already_has_a_default_export) }; 86 } 87 88 const noSymbolError = (id: Node) => 89 (isIdentifier(id) && checker.getSymbolAtLocation(id)) ? undefined 90 : { error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_named_export) }; 91 92 switch (exportNode.kind) { 93 case SyntaxKind.FunctionDeclaration: 94 case SyntaxKind.ClassDeclaration: 95 case SyntaxKind.InterfaceDeclaration: 96 case SyntaxKind.EnumDeclaration: 97 case SyntaxKind.TypeAliasDeclaration: 98 case SyntaxKind.ModuleDeclaration: { 99 const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration; 100 if (!node.name) return undefined; 101 return noSymbolError(node.name) 102 || { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol }; 103 } 104 case SyntaxKind.VariableStatement: { 105 const vs = exportNode as VariableStatement; 106 // Must be `export const x = something;`. 107 if (!(vs.declarationList.flags & NodeFlags.Const) || vs.declarationList.declarations.length !== 1) { 108 return undefined; 109 } 110 const decl = first(vs.declarationList.declarations); 111 if (!decl.initializer) return undefined; 112 Debug.assert(!wasDefault, "Can't have a default flag here"); 113 return noSymbolError(decl.name) 114 || { exportNode: vs, exportName: decl.name as Identifier, wasDefault, exportingModuleSymbol }; 115 } 116 case SyntaxKind.ExportAssignment: { 117 const node = exportNode as ExportAssignment; 118 if (node.isExportEquals) return undefined; 119 return noSymbolError(node.expression) 120 || { exportNode: node, exportName: node.expression as Identifier, wasDefault, exportingModuleSymbol }; 121 } 122 default: 123 return undefined; 124 } 125} 126 127function doChange(exportingSourceFile: SourceFile, program: Program, info: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { 128 changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); 129 changeImports(program, info, changes, cancellationToken); 130} 131 132function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: ExportInfo, changes: textChanges.ChangeTracker, checker: TypeChecker): void { 133 if (wasDefault) { 134 if (isExportAssignment(exportNode) && !exportNode.isExportEquals) { 135 const exp = exportNode.expression as Identifier; 136 const spec = makeExportSpecifier(exp.text, exp.text); 137 changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, factory.createNamedExports([spec]))); 138 } 139 else { 140 changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list")); 141 } 142 } 143 else { 144 const exportKeyword = Debug.checkDefined(findModifier(exportNode, SyntaxKind.ExportKeyword), "Should find an export keyword in modifier list"); 145 switch (exportNode.kind) { 146 case SyntaxKind.FunctionDeclaration: 147 case SyntaxKind.ClassDeclaration: 148 case SyntaxKind.InterfaceDeclaration: 149 changes.insertNodeAfter(exportingSourceFile, exportKeyword, factory.createToken(SyntaxKind.DefaultKeyword)); 150 break; 151 case SyntaxKind.VariableStatement: 152 // If 'x' isn't used in this file and doesn't have type definition, `export const x = 0;` --> `export default 0;` 153 const decl = first(exportNode.declarationList.declarations); 154 if (!FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile) && !decl.type) { 155 // We checked in `getInfo` that an initializer exists. 156 changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDefault(Debug.checkDefined(decl.initializer, "Initializer was previously known to be present"))); 157 break; 158 } 159 // falls through 160 case SyntaxKind.EnumDeclaration: 161 case SyntaxKind.TypeAliasDeclaration: 162 case SyntaxKind.ModuleDeclaration: 163 // `export type T = number;` -> `type T = number; export default T;` 164 changes.deleteModifier(exportingSourceFile, exportKeyword); 165 changes.insertNodeAfter(exportingSourceFile, exportNode, factory.createExportDefault(factory.createIdentifier(exportName.text))); 166 break; 167 default: 168 Debug.fail(`Unexpected exportNode kind ${(exportNode as ExportToConvert).kind}`); 169 } 170 } 171} 172 173function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { 174 const checker = program.getTypeChecker(); 175 const exportSymbol = Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol"); 176 FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => { 177 if (exportName === ref) return; 178 const importingSourceFile = ref.getSourceFile(); 179 if (wasDefault) { 180 changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text); 181 } 182 else { 183 changeNamedToDefaultImport(importingSourceFile, ref, changes); 184 } 185 }); 186} 187 188function changeDefaultToNamedImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker, exportName: string): void { 189 const { parent } = ref; 190 switch (parent.kind) { 191 case SyntaxKind.PropertyAccessExpression: 192 // `a.default` --> `a.foo` 193 changes.replaceNode(importingSourceFile, ref, factory.createIdentifier(exportName)); 194 break; 195 case SyntaxKind.ImportSpecifier: 196 case SyntaxKind.ExportSpecifier: { 197 const spec = parent as ImportSpecifier | ExportSpecifier; 198 // `default as foo` --> `foo`, `default as bar` --> `foo as bar` 199 changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text)); 200 break; 201 } 202 case SyntaxKind.ImportClause: { 203 const clause = parent as ImportClause; 204 Debug.assert(clause.name === ref, "Import clause name should match provided ref"); 205 const spec = makeImportSpecifier(exportName, ref.text); 206 const { namedBindings } = clause; 207 if (!namedBindings) { 208 // `import foo from "./a";` --> `import { foo } from "./a";` 209 changes.replaceNode(importingSourceFile, ref, factory.createNamedImports([spec])); 210 } 211 else if (namedBindings.kind === SyntaxKind.NamespaceImport) { 212 // `import foo, * as a from "./a";` --> `import * as a from ".a/"; import { foo } from "./a";` 213 changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) }); 214 const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : QuotePreference.Double; 215 const newImport = makeImport(/*default*/ undefined, [makeImportSpecifier(exportName, ref.text)], clause.parent.moduleSpecifier, quotePreference); 216 changes.insertNodeAfter(importingSourceFile, clause.parent, newImport); 217 } 218 else { 219 // `import foo, { bar } from "./a"` --> `import { bar, foo } from "./a";` 220 changes.delete(importingSourceFile, ref); 221 changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec); 222 } 223 break; 224 } 225 case SyntaxKind.ImportType: 226 const importTypeNode = parent as ImportTypeNode; 227 changes.replaceNode(importingSourceFile, parent, factory.createImportTypeNode(importTypeNode.argument, importTypeNode.assertions, factory.createIdentifier(exportName), importTypeNode.typeArguments, importTypeNode.isTypeOf)); 228 break; 229 default: 230 Debug.failBadSyntaxKind(parent); 231 } 232} 233 234function changeNamedToDefaultImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker): void { 235 const parent = ref.parent as PropertyAccessExpression | ImportSpecifier | ExportSpecifier; 236 switch (parent.kind) { 237 case SyntaxKind.PropertyAccessExpression: 238 // `a.foo` --> `a.default` 239 changes.replaceNode(importingSourceFile, ref, factory.createIdentifier("default")); 240 break; 241 case SyntaxKind.ImportSpecifier: { 242 // `import { foo } from "./a";` --> `import foo from "./a";` 243 // `import { foo as bar } from "./a";` --> `import bar from "./a";` 244 const defaultImport = factory.createIdentifier(parent.name.text); 245 if (parent.parent.elements.length === 1) { 246 changes.replaceNode(importingSourceFile, parent.parent, defaultImport); 247 } 248 else { 249 changes.delete(importingSourceFile, parent); 250 changes.insertNodeBefore(importingSourceFile, parent.parent, defaultImport); 251 } 252 break; 253 } 254 case SyntaxKind.ExportSpecifier: { 255 // `export { foo } from "./a";` --> `export { default as foo } from "./a";` 256 // `export { foo as bar } from "./a";` --> `export { default as bar } from "./a";` 257 // `export { foo as default } from "./a";` --> `export { default } from "./a";` 258 // (Because `export foo from "./a";` isn't valid syntax.) 259 changes.replaceNode(importingSourceFile, parent, makeExportSpecifier("default", parent.name.text)); 260 break; 261 } 262 default: 263 Debug.assertNever(parent, `Unexpected parent kind ${(parent as Node).kind}`); 264 } 265 266} 267 268function makeImportSpecifier(propertyName: string, name: string): ImportSpecifier { 269 return factory.createImportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)); 270} 271 272function makeExportSpecifier(propertyName: string, name: string): ExportSpecifier { 273 return factory.createExportSpecifier(/*isTypeOnly*/ false, propertyName === name ? undefined : factory.createIdentifier(propertyName), factory.createIdentifier(name)); 274} 275 276function getExportingModuleSymbol(node: Node, checker: TypeChecker) { 277 const parent = node.parent; 278 if (isSourceFile(parent)) { 279 return parent.symbol; 280 } 281 const symbol = parent.parent.symbol; 282 if (symbol.valueDeclaration && isExternalModuleAugmentation(symbol.valueDeclaration)) { 283 return checker.getMergedSymbol(symbol); 284 } 285 return symbol; 286} 287