Skip to content

Commit d421257

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

17 files changed

+368
-40
lines changed

src/harness/fourslash.ts

+38-16
Original file line numberDiff line numberDiff line change
@@ -3036,6 +3036,10 @@ Actual: ${stringify(fullActual)}`);
30363036
}
30373037
}
30383038

3039+
public verifyRefactorsAvailable(names: ReadonlyArray<string>): void {
3040+
assert.deepEqual(unique(this.getApplicableRefactors(this.getSelection()), r => r.name), names);
3041+
}
3042+
30393043
public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) {
30403044
const actualRefactors = this.getApplicableRefactors(this.getSelection()).filter(r => r.name === name && r.actions.some(a => a.name === actionName));
30413045
this.assertObjectsEqual(actualRefactors, refactors);
@@ -3077,32 +3081,44 @@ Actual: ${stringify(fullActual)}`);
30773081
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
30783082
}
30793083

3080-
const { renamePosition, newContent } = parseNewContent();
3084+
let renameFilename: string | undefined;
3085+
let renamePosition: number | undefined;
30813086

3082-
this.verifyCurrentFileContent(newContent);
3087+
const newFileContents = typeof newContentWithRenameMarker === "string" ? { [this.activeFile.fileName]: newContentWithRenameMarker } : newContentWithRenameMarker;
3088+
for (const fileName in newFileContents) {
3089+
const { renamePosition: rp, newContent } = TestState.parseNewContent(newFileContents[fileName]);
3090+
if (renamePosition === undefined) {
3091+
renameFilename = fileName;
3092+
renamePosition = rp;
3093+
}
3094+
else {
3095+
ts.Debug.assert(rp === undefined);
3096+
}
3097+
this.verifyFileContent(fileName, newContent);
3098+
3099+
}
30833100

30843101
if (renamePosition === undefined) {
30853102
if (editInfo.renameLocation !== undefined) {
30863103
this.raiseError(`Did not expect a rename location, got ${editInfo.renameLocation}`);
30873104
}
30883105
}
30893106
else {
3090-
// TODO: test editInfo.renameFilename value
3091-
assert.isDefined(editInfo.renameFilename);
3107+
this.assertObjectsEqual(editInfo.renameFilename, renameFilename);
30923108
if (renamePosition !== editInfo.renameLocation) {
30933109
this.raiseError(`Expected rename position of ${renamePosition}, but got ${editInfo.renameLocation}`);
30943110
}
30953111
}
3112+
}
30963113

3097-
function parseNewContent(): { renamePosition: number | undefined, newContent: string } {
3098-
const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
3099-
if (renamePosition === -1) {
3100-
return { renamePosition: undefined, newContent: newContentWithRenameMarker };
3101-
}
3102-
else {
3103-
const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
3104-
return { renamePosition, newContent };
3105-
}
3114+
private static parseNewContent(newContentWithRenameMarker: string): { readonly renamePosition: number | undefined, readonly newContent: string } {
3115+
const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
3116+
if (renamePosition === -1) {
3117+
return { renamePosition: undefined, newContent: newContentWithRenameMarker };
3118+
}
3119+
else {
3120+
const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
3121+
return { renamePosition, newContent };
31063122
}
31073123
}
31083124

@@ -3806,7 +3822,7 @@ ${code}
38063822
}
38073823

38083824
/** Collects an array of unique outputs. */
3809-
function unique<T>(inputs: T[], getOutput: (t: T) => string): string[] {
3825+
function unique<T>(inputs: ReadonlyArray<T>, getOutput: (t: T) => string): string[] {
38103826
const set = ts.createMap<true>();
38113827
for (const input of inputs) {
38123828
const out = getOutput(input);
@@ -4097,6 +4113,10 @@ namespace FourSlashInterface {
40974113
this.state.verifyApplicableRefactorAvailableForRange(this.negative);
40984114
}
40994115

4116+
public refactorsAvailable(names: ReadonlyArray<string>): void {
4117+
this.state.verifyRefactorsAvailable(names);
4118+
}
4119+
41004120
public refactor(options: VerifyRefactorOptions) {
41014121
this.state.verifyRefactor(options);
41024122
}
@@ -4719,7 +4739,7 @@ namespace FourSlashInterface {
47194739
refactorName: string;
47204740
actionName: string;
47214741
actionDescription: string;
4722-
newContent: string;
4742+
newContent: NewFileContent;
47234743
}
47244744

47254745
export type ExpectedCompletionEntry = string | {
@@ -4781,9 +4801,11 @@ namespace FourSlashInterface {
47814801
filesToSearch?: ReadonlyArray<string>;
47824802
}
47834803

4804+
export type NewFileContent = string | { readonly [filename: string]: string };
4805+
47844806
export interface NewContentOptions {
47854807
// Exactly one of these should be defined.
4786-
newFileContent?: string | { readonly [filename: string]: string };
4808+
newFileContent?: NewFileContent;
47874809
newRangeContent?: string;
47884810
}
47894811

src/parser/diagnosticInformationMap.generated.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// <auto-generated />
2-
// generated from './diagnosticInformationMap.generated.ts' by 'src\parser'
2+
// generated from './diagnosticInformationMap.generated.ts' by 'src/parser'
33
/* @internal */
44
namespace ts {
55
function diag(code: number, category: DiagnosticCategory, key: string, message: string, reportsUnnecessary?: {}): DiagnosticMessage {
@@ -1106,5 +1106,7 @@ namespace ts {
11061106
Add_or_remove_braces_in_an_arrow_function: diag(95058, DiagnosticCategory.Message, "Add_or_remove_braces_in_an_arrow_function_95058", "Add or remove braces in an arrow function"),
11071107
Add_braces_to_arrow_function: diag(95059, DiagnosticCategory.Message, "Add_braces_to_arrow_function_95059", "Add braces to arrow function"),
11081108
Remove_braces_from_arrow_function: diag(95060, DiagnosticCategory.Message, "Remove_braces_from_arrow_function_95060", "Remove braces from arrow function"),
1109+
Convert_default_export_to_named_export: diag(95061, DiagnosticCategory.Message, "Convert_default_export_to_named_export_95061", "Convert default export to named export"),
1110+
Convert_named_export_to_default_export: diag(95062, DiagnosticCategory.Message, "Convert_named_export_to_default_export_95062", "Convert named export to default export"),
11091111
};
11101112
}

src/parser/diagnosticMessages.generated.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -1097,5 +1097,7 @@
10971097
"Convert_named_imports_to_namespace_import_95057" : "Convert named imports to namespace import",
10981098
"Add_or_remove_braces_in_an_arrow_function_95058" : "Add or remove braces in an arrow function",
10991099
"Add_braces_to_arrow_function_95059" : "Add braces to arrow function",
1100-
"Remove_braces_from_arrow_function_95060" : "Remove braces from arrow function"
1100+
"Remove_braces_from_arrow_function_95060" : "Remove braces from arrow function",
1101+
"Convert_default_export_to_named_export_95061" : "Convert default export to named export",
1102+
"Convert_named_export_to_default_export_95062" : "Convert named export to default export"
11011103
}

src/parser/diagnosticMessages.json

+9-1
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/parser/utilities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5813,7 +5813,7 @@ namespace ts {
58135813
// Keywords
58145814

58155815
/* @internal */
5816-
export function isModifierKind(token: SyntaxKind): boolean {
5816+
export function isModifierKind(token: SyntaxKind): token is Modifier["kind"] {
58175817
switch (token) {
58185818
case SyntaxKind.AbstractKeyword:
58195819
case SyntaxKind.AsyncKeyword:

src/services/documentHighlights.ts

+2-9
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

+24
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

+5-5
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)