• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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