Skip to content

Commit da4cc60

Browse files
author
Andy Hanson
committed
Handle declaration kinds that can't be default-exported
1 parent 91393e3 commit da4cc60

File tree

5 files changed

+144
-11
lines changed

5 files changed

+144
-11
lines changed

src/harness/fourslash.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,12 @@ namespace FourSlash {
451451
this.selectionEnd = end.position;
452452
}
453453

454+
public selectAllInFile(fileName: string) {
455+
this.openFile(fileName);
456+
this.goToPosition(0);
457+
this.selectionEnd = this.activeFile.content.length;
458+
}
459+
454460
public selectRange(range: Range): void {
455461
this.goToRangeStart(range);
456462
this.selectionEnd = range.end;
@@ -3969,6 +3975,10 @@ namespace FourSlashInterface {
39693975
this.state.select(startMarker, endMarker);
39703976
}
39713977

3978+
public selectAllInFile(fileName: string) {
3979+
this.state.selectAllInFile(fileName);
3980+
}
3981+
39723982
public selectRange(range: FourSlash.Range): void {
39733983
this.state.selectRange(range);
39743984
}

src/services/refactors/convertExport.ts

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ namespace ts.refactor {
1818
},
1919
});
2020

21-
type ExportToConvert = (FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration) & { readonly name: Identifier };
21+
// If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name.
22+
type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement;
2223
interface Info {
2324
readonly exportNode: ExportToConvert;
25+
readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s.
2426
readonly wasDefault: boolean;
2527
readonly exportingModuleSymbol: Symbol;
2628
}
@@ -48,35 +50,72 @@ namespace ts.refactor {
4850
case SyntaxKind.ClassDeclaration:
4951
case SyntaxKind.InterfaceDeclaration:
5052
case SyntaxKind.EnumDeclaration:
51-
case SyntaxKind.ModuleDeclaration:
52-
return isIdentifier((exportNode as ExportToConvert).name) ? { exportNode: exportNode as ExportToConvert, wasDefault, exportingModuleSymbol } : undefined;
53+
case SyntaxKind.TypeAliasDeclaration:
54+
case SyntaxKind.ModuleDeclaration: {
55+
const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration;
56+
return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined;
57+
}
58+
case SyntaxKind.VariableStatement: {
59+
const vs = exportNode as VariableStatement;
60+
// Must be `export const x = something;`.
61+
if (!(vs.declarationList.flags & NodeFlags.Const) || vs.declarationList.declarations.length !== 1) {
62+
return undefined;
63+
}
64+
const decl = first(vs.declarationList.declarations);
65+
if (!decl.initializer) return undefined;
66+
Debug.assert(!wasDefault);
67+
return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined;
68+
}
5369
default:
5470
return undefined;
5571
}
5672
}
5773

5874
function doChange(exportingSourceFile: SourceFile, program: Program, info: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
59-
changeExport(exportingSourceFile, info, changes);
75+
changeExport(exportingSourceFile, info, changes, program.getTypeChecker());
6076
changeImports(program, info, changes, cancellationToken);
6177
}
6278

63-
function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode }: Info, changes: textChanges.ChangeTracker): void {
79+
function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: Info, changes: textChanges.ChangeTracker, checker: TypeChecker): void {
6480
if (wasDefault) {
6581
changes.deleteNode(exportingSourceFile, Debug.assertDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword)));
6682
}
6783
else {
68-
changes.insertNodeAfter(exportingSourceFile, Debug.assertDefined(findModifier(exportNode, SyntaxKind.ExportKeyword)), createToken(SyntaxKind.DefaultKeyword));
84+
const exportKeyword = Debug.assertDefined(findModifier(exportNode, SyntaxKind.ExportKeyword));
85+
switch (exportNode.kind) {
86+
case SyntaxKind.FunctionDeclaration:
87+
case SyntaxKind.ClassDeclaration:
88+
case SyntaxKind.InterfaceDeclaration:
89+
changes.insertNodeAfter(exportingSourceFile, exportKeyword, createToken(SyntaxKind.DefaultKeyword));
90+
break;
91+
case SyntaxKind.VariableStatement:
92+
// If 'x' isn't used in this file, `export const x = 0;` --> `export default 0;`
93+
if (!FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile)) {
94+
// We checked in `getInfo` that an initializer exists.
95+
changes.replaceNode(exportingSourceFile, exportNode, createExportDefault(Debug.assertDefined(first(exportNode.declarationList.declarations).initializer)));
96+
break;
97+
}
98+
// falls through
99+
case SyntaxKind.EnumDeclaration:
100+
case SyntaxKind.TypeAliasDeclaration:
101+
case SyntaxKind.ModuleDeclaration:
102+
// `export type T = number;` -> `type T = number; export default T;`
103+
changes.deleteModifier(exportingSourceFile, exportKeyword);
104+
changes.insertNodeAfter(exportingSourceFile, exportNode, createExportDefault(createIdentifier(exportName.text)));
105+
break;
106+
default:
107+
Debug.assertNever(exportNode);
108+
}
69109
}
70110
}
71111

72-
function changeImports(program: Program, { wasDefault, exportNode, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
112+
function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
73113
const checker = program.getTypeChecker();
74-
const exportSymbol = Debug.assertDefined(checker.getSymbolAtLocation(exportNode.name));
75-
const exportName = exportNode.name.text;
76-
FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName, wasDefault, ref => {
114+
const exportSymbol = Debug.assertDefined(checker.getSymbolAtLocation(exportName));
115+
FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => {
77116
const importingSourceFile = ref.getSourceFile();
78117
if (wasDefault) {
79-
changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName);
118+
changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text);
80119
}
81120
else {
82121
changeNamedToDefaultImport(importingSourceFile, ref, changes);

src/services/textChanges.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ namespace ts.textChanges {
241241
return this;
242242
}
243243

244+
public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void {
245+
this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) });
246+
}
247+
244248
public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = {}) {
245249
const startPosition = getAdjustedStartPosition(sourceFile, startNode, options, Position.FullStart);
246250
const endPosition = getAdjustedEndPosition(sourceFile, endNode, options);

tests/cases/fourslash/fourslash.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ declare namespace FourSlashInterface {
139139
file(name: string, content?: string, scriptKindName?: string): any;
140140
select(startMarker: string, endMarker: string): void;
141141
selectRange(range: Range): void;
142+
selectAllInFile(fileName: string): void;
142143
}
143144
class verifyNegatable {
144145
private negative;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: /fn.ts
4+
////export function f() {}
5+
6+
// @Filename: /cls.ts
7+
////export class C {}
8+
9+
// @Filename: /interface.ts
10+
////export interface I {}
11+
12+
// @Filename: /enum.ts
13+
////export const enum E {}
14+
15+
// @Filename: /namespace.ts
16+
////export namespace N {}
17+
18+
// @Filename: /type.ts
19+
////export type T = number;
20+
21+
// @Filename: /var_unused.ts
22+
////export const x = 0;
23+
24+
// @Filename: /var_unused_noInitializer.ts
25+
////export const x;
26+
27+
// @Filename: /var_used.ts
28+
////export const x = 0;
29+
////x;
30+
31+
const tests: { [fileName: string]: string | undefined } = {
32+
fn: `export default function f() {}`,
33+
34+
cls: `export default class C {}`,
35+
36+
interface: `export default interface I {}`,
37+
38+
enum:
39+
`const enum E {}
40+
export default E;
41+
`,
42+
43+
namespace:
44+
`namespace N {}
45+
46+
export default N;
47+
`,
48+
49+
type:
50+
`type T = number;
51+
export default T;
52+
`,
53+
54+
var_unused: `export default 0;`,
55+
56+
var_unused_noInitializer: undefined,
57+
58+
var_used:
59+
`const x = 0;
60+
export default x;
61+
x;`,
62+
};
63+
64+
for (const name in tests) {
65+
const newContent = tests[name];
66+
const fileName = `/${name}.ts`;
67+
goTo.selectAllInFile(fileName);
68+
if (newContent === undefined) {
69+
verify.refactorsAvailable([]);
70+
}
71+
else {
72+
edit.applyRefactor({
73+
refactorName: "Convert export",
74+
actionName: "Convert named export to default export",
75+
actionDescription: "Convert named export to default export",
76+
newContent: { [fileName]: newContent },
77+
});
78+
}
79+
}

0 commit comments

Comments
 (0)