Skip to content

Commit d2fdc78

Browse files
author
Andy Hanson
committed
Add refactor to convert named to default export and back
1 parent f512011 commit d2fdc78

18 files changed

+358
-36
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3636,7 +3636,7 @@
36363636
"category": "Message",
36373637
"code": 6353
36383638
},
3639-
3639+
36403640
"Project '{0}' is up to date with .d.ts files from its dependencies": {
36413641
"category": "Message",
36423642
"code": 6354
@@ -4414,5 +4414,13 @@
44144414
"Remove braces from arrow function": {
44154415
"category": "Message",
44164416
"code": 95060
4417+
},
4418+
"Convert default export to named export": {
4419+
"category": "Message",
4420+
"code": 95061
4421+
},
4422+
"Convert named export to default export": {
4423+
"category": "Message",
4424+
"code": 95062
44174425
}
44184426
}

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5665,7 +5665,7 @@ namespace ts {
56655665
// Keywords
56665666

56675667
/* @internal */
5668-
export function isModifierKind(token: SyntaxKind): boolean {
5668+
export function isModifierKind(token: SyntaxKind): token is Modifier["kind"] {
56695669
switch (token) {
56705670
case SyntaxKind.AbstractKeyword:
56715671
case SyntaxKind.AsyncKeyword:

src/harness/fourslash.ts

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

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

3095-
const { renamePosition, newContent } = parseNewContent();
3099+
let renameFilename: string | undefined;
3100+
let renamePosition: number | undefined;
30963101

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

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

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

@@ -3821,7 +3837,7 @@ ${code}
38213837
}
38223838

38233839
/** Collects an array of unique outputs. */
3824-
function unique<T>(inputs: T[], getOutput: (t: T) => string): string[] {
3840+
function unique<T>(inputs: ReadonlyArray<T>, getOutput: (t: T) => string): string[] {
38253841
const set = ts.createMap<true>();
38263842
for (const input of inputs) {
38273843
const out = getOutput(input);
@@ -4112,6 +4128,10 @@ namespace FourSlashInterface {
41124128
this.state.verifyApplicableRefactorAvailableForRange(this.negative);
41134129
}
41144130

4131+
public refactorsAvailable(names: ReadonlyArray<string>): void {
4132+
this.state.verifyRefactorsAvailable(names);
4133+
}
4134+
41154135
public refactor(options: VerifyRefactorOptions) {
41164136
this.state.verifyRefactor(options);
41174137
}
@@ -4734,7 +4754,7 @@ namespace FourSlashInterface {
47344754
refactorName: string;
47354755
actionName: string;
47364756
actionDescription: string;
4737-
newContent: string;
4757+
newContent: NewFileContent;
47384758
}
47394759

47404760
export type ExpectedCompletionEntry = string | {
@@ -4796,9 +4816,11 @@ namespace FourSlashInterface {
47964816
filesToSearch?: ReadonlyArray<string>;
47974817
}
47984818

4819+
export type NewFileContent = string | { readonly [filename: string]: string };
4820+
47994821
export interface NewContentOptions {
48004822
// Exactly one of these should be defined.
4801-
newFileContent?: string | { readonly [filename: string]: string };
4823+
newFileContent?: NewFileContent;
48024824
newRangeContent?: string;
48034825
}
48044826

src/harness/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"../services/codefixes/useDefaultImport.ts",
122122
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
123123
"../services/codefixes/convertToMappedObjectType.ts",
124+
"../services/refactors/convertExport.ts",
124125
"../services/refactors/convertImport.ts",
125126
"../services/refactors/extractSymbol.ts",
126127
"../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)