Skip to content

Commit 98740dd

Browse files
author
Andy Hanson
committed
Add refactor to convert named to default export and back
1 parent c437404 commit 98740dd

18 files changed

+357
-35
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4305,5 +4305,13 @@
43054305
"Convert named imports to namespace import": {
43064306
"category": "Message",
43074307
"code": 95057
4308+
},
4309+
"Convert default export to named export": {
4310+
"category": "Message",
4311+
"code": 95058
4312+
},
4313+
"Convert named export to default export": {
4314+
"category": "Message",
4315+
"code": 95059
43084316
}
43094317
}

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5643,7 +5643,7 @@ namespace ts {
56435643
// Keywords
56445644

56455645
/* @internal */
5646-
export function isModifierKind(token: SyntaxKind): boolean {
5646+
export function isModifierKind(token: SyntaxKind): token is Modifier["kind"] {
56475647
switch (token) {
56485648
case SyntaxKind.AbstractKeyword:
56495649
case SyntaxKind.AsyncKeyword:

src/harness/fourslash.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3050,6 +3050,10 @@ Actual: ${stringify(fullActual)}`);
30503050
}
30513051
}
30523052

3053+
public verifyRefactorsAvailable(names: ReadonlyArray<string>): void {
3054+
assert.deepEqual(unique(this.getApplicableRefactors(this.getSelection()), r => r.name), names);
3055+
}
3056+
30533057
public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) {
30543058
const actualRefactors = this.getApplicableRefactors(this.getSelection()).filter(r => r.name === name && r.actions.some(a => a.name === actionName));
30553059
this.assertObjectsEqual(actualRefactors, refactors);
@@ -3091,32 +3095,44 @@ Actual: ${stringify(fullActual)}`);
30913095
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
30923096
}
30933097

3094-
const { renamePosition, newContent } = parseNewContent();
3098+
let renameFilename: string | undefined;
3099+
let renamePosition: number | undefined;
30953100

3096-
this.verifyCurrentFileContent(newContent);
3101+
const newFileContents = typeof newContentWithRenameMarker === "string" ? { [this.activeFile.fileName]: newContentWithRenameMarker } : newContentWithRenameMarker;
3102+
for (const fileName in newFileContents) {
3103+
const { renamePosition: rp, newContent } = TestState.parseNewContent(newFileContents[fileName]);
3104+
if (renamePosition === undefined) {
3105+
renameFilename = fileName;
3106+
renamePosition = rp;
3107+
}
3108+
else {
3109+
ts.Debug.assert(rp === undefined);
3110+
}
3111+
this.verifyFileContent(fileName, newContent);
3112+
3113+
}
30973114

30983115
if (renamePosition === undefined) {
30993116
if (editInfo.renameLocation !== undefined) {
31003117
this.raiseError(`Did not expect a rename location, got ${editInfo.renameLocation}`);
31013118
}
31023119
}
31033120
else {
3104-
// TODO: test editInfo.renameFilename value
3105-
assert.isDefined(editInfo.renameFilename);
3121+
this.assertObjectsEqual(editInfo.renameFilename, renameFilename);
31063122
if (renamePosition !== editInfo.renameLocation) {
31073123
this.raiseError(`Expected rename position of ${renamePosition}, but got ${editInfo.renameLocation}`);
31083124
}
31093125
}
3126+
}
31103127

3111-
function parseNewContent(): { renamePosition: number | undefined, newContent: string } {
3112-
const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
3113-
if (renamePosition === -1) {
3114-
return { renamePosition: undefined, newContent: newContentWithRenameMarker };
3115-
}
3116-
else {
3117-
const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
3118-
return { renamePosition, newContent };
3119-
}
3128+
private static parseNewContent(newContentWithRenameMarker: string): { readonly renamePosition: number | undefined, readonly newContent: string } {
3129+
const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
3130+
if (renamePosition === -1) {
3131+
return { renamePosition: undefined, newContent: newContentWithRenameMarker };
3132+
}
3133+
else {
3134+
const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
3135+
return { renamePosition, newContent };
31203136
}
31213137
}
31223138

@@ -3820,7 +3836,7 @@ ${code}
38203836
}
38213837

38223838
/** Collects an array of unique outputs. */
3823-
function unique<T>(inputs: T[], getOutput: (t: T) => string): string[] {
3839+
function unique<T>(inputs: ReadonlyArray<T>, getOutput: (t: T) => string): string[] {
38243840
const set = ts.createMap<true>();
38253841
for (const input of inputs) {
38263842
const out = getOutput(input);
@@ -4111,6 +4127,10 @@ namespace FourSlashInterface {
41114127
this.state.verifyApplicableRefactorAvailableForRange(this.negative);
41124128
}
41134129

4130+
public refactorsAvailable(names: ReadonlyArray<string>): void {
4131+
this.state.verifyRefactorsAvailable(names);
4132+
}
4133+
41144134
public refactor(options: VerifyRefactorOptions) {
41154135
this.state.verifyRefactor(options);
41164136
}
@@ -4733,7 +4753,7 @@ namespace FourSlashInterface {
47334753
refactorName: string;
47344754
actionName: string;
47354755
actionDescription: string;
4736-
newContent: string;
4756+
newContent: NewFileContent;
47374757
}
47384758

47394759
export type ExpectedCompletionEntry = string | {
@@ -4795,9 +4815,11 @@ namespace FourSlashInterface {
47954815
filesToSearch?: ReadonlyArray<string>;
47964816
}
47974817

4818+
export type NewFileContent = string | { readonly [filename: string]: string };
4819+
47984820
export interface NewContentOptions {
47994821
// Exactly one of these should be defined.
4800-
newFileContent?: string | { readonly [filename: string]: string };
4822+
newFileContent?: NewFileContent;
48014823
newRangeContent?: string;
48024824
}
48034825

src/harness/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"../services/codefixes/useDefaultImport.ts",
121121
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
122122
"../services/codefixes/convertToMappedObjectType.ts",
123+
"../services/refactors/convertExport.ts",
123124
"../services/refactors/convertImport.ts",
124125
"../services/refactors/extractSymbol.ts",
125126
"../services/refactors/generateGetAccessorAndSetAccessor.ts",

src/server/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"../services/codefixes/useDefaultImport.ts",
117117
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
118118
"../services/codefixes/convertToMappedObjectType.ts",
119+
"../services/refactors/convertExport.ts",
119120
"../services/refactors/convertImport.ts",
120121
"../services/refactors/extractSymbol.ts",
121122
"../services/refactors/generateGetAccessorAndSetAccessor.ts",

src/server/tsconfig.library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"../services/codefixes/useDefaultImport.ts",
123123
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
124124
"../services/codefixes/convertToMappedObjectType.ts",
125+
"../services/refactors/convertExport.ts",
125126
"../services/refactors/convertImport.ts",
126127
"../services/refactors/extractSymbol.ts",
127128
"../services/refactors/generateGetAccessorAndSetAccessor.ts",

src/services/documentHighlights.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,8 @@ namespace ts.DocumentHighlights {
187187
});
188188
}
189189

190-
function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): Node[] {
191-
const modifierFlag = modifierToFlag(modifier);
192-
return mapDefined(getNodesToSearchForModifier(declaration, modifierFlag), node => {
193-
if (getModifierFlags(node) & modifierFlag) {
194-
const mod = find(node.modifiers!, m => m.kind === modifier);
195-
Debug.assert(!!mod);
196-
return mod;
197-
}
198-
});
190+
function getModifierOccurrences(modifier: Modifier["kind"], declaration: Node): Node[] {
191+
return mapDefined(getNodesToSearchForModifier(declaration, modifierToFlag(modifier)), node => findModifier(node, modifier));
199192
}
200193

201194
function getNodesToSearchForModifier(declaration: Node, modifierFlag: ModifierFlags): ReadonlyArray<Node> | undefined {

src/services/findAllReferences.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,30 @@ namespace ts.FindAllReferences.Core {
590590
}
591591
}
592592

593+
export function eachExportReference(
594+
sourceFiles: ReadonlyArray<SourceFile>,
595+
checker: TypeChecker,
596+
cancellationToken: CancellationToken | undefined,
597+
exportSymbol: Symbol,
598+
exportingModuleSymbol: Symbol,
599+
exportName: string,
600+
isDefaultExport: boolean,
601+
cb: (ref: Identifier) => void,
602+
): void {
603+
const importTracker = createImportTracker(sourceFiles, arrayToSet(sourceFiles, f => f.fileName), checker, cancellationToken);
604+
const { importSearches, indirectUsers } = importTracker(exportSymbol, { exportKind: isDefaultExport ? ExportKind.Default : ExportKind.Named, exportingModuleSymbol }, /*isForRename*/ false);
605+
for (const [importLocation] of importSearches) {
606+
cb(importLocation);
607+
}
608+
for (const indirectUser of indirectUsers) {
609+
for (const node of getPossibleSymbolReferenceNodes(indirectUser, isDefaultExport ? "default" : exportName)) {
610+
if (isIdentifier(node) && checker.getSymbolAtLocation(node) === exportSymbol) {
611+
cb(node);
612+
}
613+
}
614+
}
615+
}
616+
593617
function shouldAddSingleReference(singleRef: Identifier | StringLiteral, state: State): boolean {
594618
if (!hasMatchingMeaning(singleRef, state)) return false;
595619
if (!state.options.isForRename) return true;

src/services/importTracker.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace ts.FindAllReferences {
1212
export type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult;
1313

1414
/** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. */
15-
export function createImportTracker(sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken): ImportTracker {
15+
export function createImportTracker(sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker {
1616
const allDirectImports = getDirectImportsMap(sourceFiles, checker, cancellationToken);
1717
return (exportSymbol, exportInfo, isForRename) => {
1818
const { directImports, indirectUsers } = getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker, cancellationToken);
@@ -43,7 +43,7 @@ namespace ts.FindAllReferences {
4343
allDirectImports: Map<ImporterOrCallExpression[]>,
4444
{ exportingModuleSymbol, exportKind }: ExportInfo,
4545
checker: TypeChecker,
46-
cancellationToken: CancellationToken
46+
cancellationToken: CancellationToken | undefined,
4747
): { directImports: Importer[], indirectUsers: ReadonlyArray<SourceFile> } {
4848
const markSeenDirectImport = nodeSeenTracker<ImporterOrCallExpression>();
4949
const markSeenIndirectUser = nodeSeenTracker<SourceFileLike>();
@@ -80,7 +80,7 @@ namespace ts.FindAllReferences {
8080
continue;
8181
}
8282

83-
cancellationToken.throwIfCancellationRequested();
83+
if (cancellationToken) cancellationToken.throwIfCancellationRequested();
8484

8585
switch (direct.kind) {
8686
case SyntaxKind.CallExpression:
@@ -373,11 +373,11 @@ namespace ts.FindAllReferences {
373373
}
374374

375375
/** Returns a map from a module symbol Id to all import statements that directly reference the module. */
376-
function getDirectImportsMap(sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cancellationToken: CancellationToken): Map<ImporterOrCallExpression[]> {
376+
function getDirectImportsMap(sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cancellationToken: CancellationToken | undefined): Map<ImporterOrCallExpression[]> {
377377
const map = createMap<ImporterOrCallExpression[]>();
378378

379379
for (const sourceFile of sourceFiles) {
380-
cancellationToken.throwIfCancellationRequested();
380+
if (cancellationToken) cancellationToken.throwIfCancellationRequested();
381381
forEachImport(sourceFile, (importDecl, moduleSpecifier) => {
382382
const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
383383
if (moduleSymbol) {

0 commit comments

Comments
 (0)