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