Skip to content

Commit 806a661

Browse files
author
Andy
authored
Add refactor to convert named to default export and back (#24878)
* Add refactor to convert named to default export and back * Support ambient module * Handle declaration kinds that can't be default-exported * Update API (#24966)
1 parent fefad79 commit 806a661

18 files changed

+526
-39
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4418,5 +4418,13 @@
44184418
"Remove braces from arrow function": {
44194419
"category": "Message",
44204420
"code": 95060
4421+
},
4422+
"Convert default export to named export": {
4423+
"category": "Message",
4424+
"code": 95061
4425+
},
4426+
"Convert named export to default export": {
4427+
"category": "Message",
4428+
"code": 95062
44214429
}
44224430
}

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5844,7 +5844,7 @@ namespace ts {
58445844
// Keywords
58455845

58465846
/* @internal */
5847-
export function isModifierKind(token: SyntaxKind): boolean {
5847+
export function isModifierKind(token: SyntaxKind): token is Modifier["kind"] {
58485848
switch (token) {
58495849
case SyntaxKind.AbstractKeyword:
58505850
case SyntaxKind.AsyncKeyword:

src/harness/fourslash.ts

Lines changed: 39 additions & 15 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;
@@ -3090,32 +3096,44 @@ Actual: ${stringify(fullActual)}`);
30903096
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
30913097
}
30923098

3093-
const { renamePosition, newContent } = parseNewContent();
3099+
let renameFilename: string | undefined;
3100+
let renamePosition: number | undefined;
3101+
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);
30943113

3095-
this.verifyCurrentFileContent(newContent);
3114+
}
30963115

30973116
if (renamePosition === undefined) {
30983117
if (editInfo.renameLocation !== undefined) {
30993118
this.raiseError(`Did not expect a rename location, got ${editInfo.renameLocation}`);
31003119
}
31013120
}
31023121
else {
3103-
// TODO: test editInfo.renameFilename value
3104-
assert.isDefined(editInfo.renameFilename);
3122+
this.assertObjectsEqual(editInfo.renameFilename, renameFilename);
31053123
if (renamePosition !== editInfo.renameLocation) {
31063124
this.raiseError(`Expected rename position of ${renamePosition}, but got ${editInfo.renameLocation}`);
31073125
}
31083126
}
3127+
}
31093128

3110-
function parseNewContent(): { renamePosition: number | undefined, newContent: string } {
3111-
const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
3112-
if (renamePosition === -1) {
3113-
return { renamePosition: undefined, newContent: newContentWithRenameMarker };
3114-
}
3115-
else {
3116-
const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
3117-
return { renamePosition, newContent };
3118-
}
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 };
31193137
}
31203138
}
31213139

@@ -3966,6 +3984,10 @@ namespace FourSlashInterface {
39663984
this.state.select(startMarker, endMarker);
39673985
}
39683986

3987+
public selectAllInFile(fileName: string) {
3988+
this.state.selectAllInFile(fileName);
3989+
}
3990+
39693991
public selectRange(range: FourSlash.Range): void {
39703992
this.state.selectRange(range);
39713993
}
@@ -4736,7 +4758,7 @@ namespace FourSlashInterface {
47364758
refactorName: string;
47374759
actionName: string;
47384760
actionDescription: string;
4739-
newContent: string;
4761+
newContent: NewFileContent;
47404762
}
47414763

47424764
export type ExpectedCompletionEntry = string | {
@@ -4798,9 +4820,11 @@ namespace FourSlashInterface {
47984820
filesToSearch?: ReadonlyArray<string>;
47994821
}
48004822

4823+
export type NewFileContent = string | { readonly [filename: string]: string };
4824+
48014825
export interface NewContentOptions {
48024826
// Exactly one of these should be defined.
4803-
newFileContent?: string | { readonly [filename: string]: string };
4827+
newFileContent?: NewFileContent;
48044828
newRangeContent?: string;
48054829
}
48064830

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:
@@ -363,11 +363,11 @@ namespace ts.FindAllReferences {
363363
}
364364

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

369369
for (const sourceFile of sourceFiles) {
370-
cancellationToken.throwIfCancellationRequested();
370+
if (cancellationToken) cancellationToken.throwIfCancellationRequested();
371371
forEachImport(sourceFile, (importDecl, moduleSpecifier) => {
372372
const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
373373
if (moduleSymbol) {

0 commit comments

Comments
 (0)