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)
This commit is contained in:
parent
fefad791d3
commit
806a661be3
|
@ -4418,5 +4418,13 @@
|
|||
"Remove braces from arrow function": {
|
||||
"category": "Message",
|
||||
"code": 95060
|
||||
},
|
||||
"Convert default export to named export": {
|
||||
"category": "Message",
|
||||
"code": 95061
|
||||
},
|
||||
"Convert named export to default export": {
|
||||
"category": "Message",
|
||||
"code": 95062
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5844,7 +5844,7 @@ namespace ts {
|
|||
// Keywords
|
||||
|
||||
/* @internal */
|
||||
export function isModifierKind(token: SyntaxKind): boolean {
|
||||
export function isModifierKind(token: SyntaxKind): token is Modifier["kind"] {
|
||||
switch (token) {
|
||||
case SyntaxKind.AbstractKeyword:
|
||||
case SyntaxKind.AsyncKeyword:
|
||||
|
|
|
@ -451,6 +451,12 @@ namespace FourSlash {
|
|||
this.selectionEnd = end.position;
|
||||
}
|
||||
|
||||
public selectAllInFile(fileName: string) {
|
||||
this.openFile(fileName);
|
||||
this.goToPosition(0);
|
||||
this.selectionEnd = this.activeFile.content.length;
|
||||
}
|
||||
|
||||
public selectRange(range: Range): void {
|
||||
this.goToRangeStart(range);
|
||||
this.selectionEnd = range.end;
|
||||
|
@ -3090,9 +3096,22 @@ Actual: ${stringify(fullActual)}`);
|
|||
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
|
||||
}
|
||||
|
||||
const { renamePosition, newContent } = parseNewContent();
|
||||
let renameFilename: string | undefined;
|
||||
let renamePosition: number | undefined;
|
||||
|
||||
this.verifyCurrentFileContent(newContent);
|
||||
const newFileContents = typeof newContentWithRenameMarker === "string" ? { [this.activeFile.fileName]: newContentWithRenameMarker } : newContentWithRenameMarker;
|
||||
for (const fileName in newFileContents) {
|
||||
const { renamePosition: rp, newContent } = TestState.parseNewContent(newFileContents[fileName]);
|
||||
if (renamePosition === undefined) {
|
||||
renameFilename = fileName;
|
||||
renamePosition = rp;
|
||||
}
|
||||
else {
|
||||
ts.Debug.assert(rp === undefined);
|
||||
}
|
||||
this.verifyFileContent(fileName, newContent);
|
||||
|
||||
}
|
||||
|
||||
if (renamePosition === undefined) {
|
||||
if (editInfo.renameLocation !== undefined) {
|
||||
|
@ -3100,22 +3119,21 @@ Actual: ${stringify(fullActual)}`);
|
|||
}
|
||||
}
|
||||
else {
|
||||
// TODO: test editInfo.renameFilename value
|
||||
assert.isDefined(editInfo.renameFilename);
|
||||
this.assertObjectsEqual(editInfo.renameFilename, renameFilename);
|
||||
if (renamePosition !== editInfo.renameLocation) {
|
||||
this.raiseError(`Expected rename position of ${renamePosition}, but got ${editInfo.renameLocation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseNewContent(): { renamePosition: number | undefined, newContent: string } {
|
||||
const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
|
||||
if (renamePosition === -1) {
|
||||
return { renamePosition: undefined, newContent: newContentWithRenameMarker };
|
||||
}
|
||||
else {
|
||||
const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
|
||||
return { renamePosition, newContent };
|
||||
}
|
||||
private static parseNewContent(newContentWithRenameMarker: string): { readonly renamePosition: number | undefined, readonly newContent: string } {
|
||||
const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
|
||||
if (renamePosition === -1) {
|
||||
return { renamePosition: undefined, newContent: newContentWithRenameMarker };
|
||||
}
|
||||
else {
|
||||
const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
|
||||
return { renamePosition, newContent };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3966,6 +3984,10 @@ namespace FourSlashInterface {
|
|||
this.state.select(startMarker, endMarker);
|
||||
}
|
||||
|
||||
public selectAllInFile(fileName: string) {
|
||||
this.state.selectAllInFile(fileName);
|
||||
}
|
||||
|
||||
public selectRange(range: FourSlash.Range): void {
|
||||
this.state.selectRange(range);
|
||||
}
|
||||
|
@ -4736,7 +4758,7 @@ namespace FourSlashInterface {
|
|||
refactorName: string;
|
||||
actionName: string;
|
||||
actionDescription: string;
|
||||
newContent: string;
|
||||
newContent: NewFileContent;
|
||||
}
|
||||
|
||||
export type ExpectedCompletionEntry = string | {
|
||||
|
@ -4798,9 +4820,11 @@ namespace FourSlashInterface {
|
|||
filesToSearch?: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export type NewFileContent = string | { readonly [filename: string]: string };
|
||||
|
||||
export interface NewContentOptions {
|
||||
// Exactly one of these should be defined.
|
||||
newFileContent?: string | { readonly [filename: string]: string };
|
||||
newFileContent?: NewFileContent;
|
||||
newRangeContent?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -187,15 +187,8 @@ namespace ts.DocumentHighlights {
|
|||
});
|
||||
}
|
||||
|
||||
function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): Node[] {
|
||||
const modifierFlag = modifierToFlag(modifier);
|
||||
return mapDefined(getNodesToSearchForModifier(declaration, modifierFlag), node => {
|
||||
if (getModifierFlags(node) & modifierFlag) {
|
||||
const mod = find(node.modifiers!, m => m.kind === modifier);
|
||||
Debug.assert(!!mod);
|
||||
return mod;
|
||||
}
|
||||
});
|
||||
function getModifierOccurrences(modifier: Modifier["kind"], declaration: Node): Node[] {
|
||||
return mapDefined(getNodesToSearchForModifier(declaration, modifierToFlag(modifier)), node => findModifier(node, modifier));
|
||||
}
|
||||
|
||||
function getNodesToSearchForModifier(declaration: Node, modifierFlag: ModifierFlags): ReadonlyArray<Node> | undefined {
|
||||
|
|
|
@ -590,6 +590,30 @@ namespace ts.FindAllReferences.Core {
|
|||
}
|
||||
}
|
||||
|
||||
export function eachExportReference(
|
||||
sourceFiles: ReadonlyArray<SourceFile>,
|
||||
checker: TypeChecker,
|
||||
cancellationToken: CancellationToken | undefined,
|
||||
exportSymbol: Symbol,
|
||||
exportingModuleSymbol: Symbol,
|
||||
exportName: string,
|
||||
isDefaultExport: boolean,
|
||||
cb: (ref: Identifier) => void,
|
||||
): void {
|
||||
const importTracker = createImportTracker(sourceFiles, arrayToSet(sourceFiles, f => f.fileName), checker, cancellationToken);
|
||||
const { importSearches, indirectUsers } = importTracker(exportSymbol, { exportKind: isDefaultExport ? ExportKind.Default : ExportKind.Named, exportingModuleSymbol }, /*isForRename*/ false);
|
||||
for (const [importLocation] of importSearches) {
|
||||
cb(importLocation);
|
||||
}
|
||||
for (const indirectUser of indirectUsers) {
|
||||
for (const node of getPossibleSymbolReferenceNodes(indirectUser, isDefaultExport ? "default" : exportName)) {
|
||||
if (isIdentifier(node) && checker.getSymbolAtLocation(node) === exportSymbol) {
|
||||
cb(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAddSingleReference(singleRef: Identifier | StringLiteral, state: State): boolean {
|
||||
if (!hasMatchingMeaning(singleRef, state)) return false;
|
||||
if (!state.options.isForRename) return true;
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace ts.FindAllReferences {
|
|||
export type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult;
|
||||
|
||||
/** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. */
|
||||
export function createImportTracker(sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken): ImportTracker {
|
||||
export function createImportTracker(sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker {
|
||||
const allDirectImports = getDirectImportsMap(sourceFiles, checker, cancellationToken);
|
||||
return (exportSymbol, exportInfo, isForRename) => {
|
||||
const { directImports, indirectUsers } = getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker, cancellationToken);
|
||||
|
@ -43,7 +43,7 @@ namespace ts.FindAllReferences {
|
|||
allDirectImports: Map<ImporterOrCallExpression[]>,
|
||||
{ exportingModuleSymbol, exportKind }: ExportInfo,
|
||||
checker: TypeChecker,
|
||||
cancellationToken: CancellationToken
|
||||
cancellationToken: CancellationToken | undefined,
|
||||
): { directImports: Importer[], indirectUsers: ReadonlyArray<SourceFile> } {
|
||||
const markSeenDirectImport = nodeSeenTracker<ImporterOrCallExpression>();
|
||||
const markSeenIndirectUser = nodeSeenTracker<SourceFileLike>();
|
||||
|
@ -80,7 +80,7 @@ namespace ts.FindAllReferences {
|
|||
continue;
|
||||
}
|
||||
|
||||
cancellationToken.throwIfCancellationRequested();
|
||||
if (cancellationToken) cancellationToken.throwIfCancellationRequested();
|
||||
|
||||
switch (direct.kind) {
|
||||
case SyntaxKind.CallExpression:
|
||||
|
@ -363,11 +363,11 @@ namespace ts.FindAllReferences {
|
|||
}
|
||||
|
||||
/** Returns a map from a module symbol Id to all import statements that directly reference the module. */
|
||||
function getDirectImportsMap(sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cancellationToken: CancellationToken): Map<ImporterOrCallExpression[]> {
|
||||
function getDirectImportsMap(sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cancellationToken: CancellationToken | undefined): Map<ImporterOrCallExpression[]> {
|
||||
const map = createMap<ImporterOrCallExpression[]>();
|
||||
|
||||
for (const sourceFile of sourceFiles) {
|
||||
cancellationToken.throwIfCancellationRequested();
|
||||
if (cancellationToken) cancellationToken.throwIfCancellationRequested();
|
||||
forEachImport(sourceFile, (importDecl, moduleSpecifier) => {
|
||||
const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
|
||||
if (moduleSymbol) {
|
||||
|
|
212
src/services/refactors/convertExport.ts
Normal file
212
src/services/refactors/convertExport.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
/* @internal */
|
||||
namespace ts.refactor {
|
||||
const refactorName = "Convert export";
|
||||
const actionNameDefaultToNamed = "Convert default export to named export";
|
||||
const actionNameNamedToDefault = "Convert named export to default export";
|
||||
registerRefactor(refactorName, {
|
||||
getAvailableActions(context): ApplicableRefactorInfo[] | undefined {
|
||||
const info = getInfo(context);
|
||||
if (!info) return undefined;
|
||||
const description = info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message;
|
||||
const actionName = info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault;
|
||||
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }];
|
||||
},
|
||||
getEditsForAction(context, actionName): RefactorEditInfo {
|
||||
Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault);
|
||||
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.assertDefined(getInfo(context)), t, context.cancellationToken));
|
||||
return { edits, renameFilename: undefined, renameLocation: undefined };
|
||||
},
|
||||
});
|
||||
|
||||
// If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name.
|
||||
type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement;
|
||||
interface Info {
|
||||
readonly exportNode: ExportToConvert;
|
||||
readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s.
|
||||
readonly wasDefault: boolean;
|
||||
readonly exportingModuleSymbol: Symbol;
|
||||
}
|
||||
|
||||
function getInfo(context: RefactorContext): Info | undefined {
|
||||
const { file } = context;
|
||||
const span = getRefactorContextSpan(context);
|
||||
const token = getTokenAtPosition(file, span.start, /*includeJsDocComment*/ false);
|
||||
const exportNode = getParentNodeInSpan(token, file, span);
|
||||
if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const exportingModuleSymbol = isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol;
|
||||
|
||||
const flags = getModifierFlags(exportNode);
|
||||
const wasDefault = !!(flags & ModifierFlags.Default);
|
||||
// If source file already has a default export, don't offer refactor.
|
||||
if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (exportNode.kind) {
|
||||
case SyntaxKind.FunctionDeclaration:
|
||||
case SyntaxKind.ClassDeclaration:
|
||||
case SyntaxKind.InterfaceDeclaration:
|
||||
case SyntaxKind.EnumDeclaration:
|
||||
case SyntaxKind.TypeAliasDeclaration:
|
||||
case SyntaxKind.ModuleDeclaration: {
|
||||
const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration;
|
||||
return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined;
|
||||
}
|
||||
case SyntaxKind.VariableStatement: {
|
||||
const vs = exportNode as VariableStatement;
|
||||
// Must be `export const x = something;`.
|
||||
if (!(vs.declarationList.flags & NodeFlags.Const) || vs.declarationList.declarations.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const decl = first(vs.declarationList.declarations);
|
||||
if (!decl.initializer) return undefined;
|
||||
Debug.assert(!wasDefault);
|
||||
return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function doChange(exportingSourceFile: SourceFile, program: Program, info: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
|
||||
changeExport(exportingSourceFile, info, changes, program.getTypeChecker());
|
||||
changeImports(program, info, changes, cancellationToken);
|
||||
}
|
||||
|
||||
function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: Info, changes: textChanges.ChangeTracker, checker: TypeChecker): void {
|
||||
if (wasDefault) {
|
||||
changes.deleteNode(exportingSourceFile, Debug.assertDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword)));
|
||||
}
|
||||
else {
|
||||
const exportKeyword = Debug.assertDefined(findModifier(exportNode, SyntaxKind.ExportKeyword));
|
||||
switch (exportNode.kind) {
|
||||
case SyntaxKind.FunctionDeclaration:
|
||||
case SyntaxKind.ClassDeclaration:
|
||||
case SyntaxKind.InterfaceDeclaration:
|
||||
changes.insertNodeAfter(exportingSourceFile, exportKeyword, createToken(SyntaxKind.DefaultKeyword));
|
||||
break;
|
||||
case SyntaxKind.VariableStatement:
|
||||
// If 'x' isn't used in this file, `export const x = 0;` --> `export default 0;`
|
||||
if (!FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile)) {
|
||||
// We checked in `getInfo` that an initializer exists.
|
||||
changes.replaceNode(exportingSourceFile, exportNode, createExportDefault(Debug.assertDefined(first(exportNode.declarationList.declarations).initializer)));
|
||||
break;
|
||||
}
|
||||
// falls through
|
||||
case SyntaxKind.EnumDeclaration:
|
||||
case SyntaxKind.TypeAliasDeclaration:
|
||||
case SyntaxKind.ModuleDeclaration:
|
||||
// `export type T = number;` -> `type T = number; export default T;`
|
||||
changes.deleteModifier(exportingSourceFile, exportKeyword);
|
||||
changes.insertNodeAfter(exportingSourceFile, exportNode, createExportDefault(createIdentifier(exportName.text)));
|
||||
break;
|
||||
default:
|
||||
Debug.assertNever(exportNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
|
||||
const checker = program.getTypeChecker();
|
||||
const exportSymbol = Debug.assertDefined(checker.getSymbolAtLocation(exportName));
|
||||
FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => {
|
||||
const importingSourceFile = ref.getSourceFile();
|
||||
if (wasDefault) {
|
||||
changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text);
|
||||
}
|
||||
else {
|
||||
changeNamedToDefaultImport(importingSourceFile, ref, changes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function changeDefaultToNamedImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker, exportName: string): void {
|
||||
const { parent } = ref;
|
||||
switch (parent.kind) {
|
||||
case SyntaxKind.PropertyAccessExpression:
|
||||
// `a.default` --> `a.foo`
|
||||
changes.replaceNode(importingSourceFile, ref, createIdentifier(exportName));
|
||||
break;
|
||||
case SyntaxKind.ImportSpecifier:
|
||||
case SyntaxKind.ExportSpecifier: {
|
||||
const spec = parent as ImportSpecifier | ExportSpecifier;
|
||||
// `default as foo` --> `foo`, `default as bar` --> `foo as bar`
|
||||
changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text));
|
||||
break;
|
||||
}
|
||||
case SyntaxKind.ImportClause: {
|
||||
const clause = parent as ImportClause;
|
||||
Debug.assert(clause.name === ref);
|
||||
const spec = makeImportSpecifier(exportName, ref.text);
|
||||
const { namedBindings } = clause;
|
||||
if (!namedBindings) {
|
||||
// `import foo from "./a";` --> `import { foo } from "./a";`
|
||||
changes.replaceNode(importingSourceFile, ref, createNamedImports([spec]));
|
||||
}
|
||||
else if (namedBindings.kind === SyntaxKind.NamespaceImport) {
|
||||
// `import foo, * as a from "./a";` --> `import * as a from ".a/"; import { foo } from "./a";`
|
||||
changes.deleteNode(importingSourceFile, ref);
|
||||
const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : QuotePreference.Double;
|
||||
const newImport = makeImport(/*default*/ undefined, [makeImportSpecifier(exportName, ref.text)], clause.parent.moduleSpecifier, quotePreference);
|
||||
changes.insertNodeAfter(importingSourceFile, clause.parent, newImport);
|
||||
}
|
||||
else {
|
||||
// `import foo, { bar } from "./a"` --> `import { bar, foo } from "./a";`
|
||||
changes.deleteNode(importingSourceFile, ref);
|
||||
changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Debug.failBadSyntaxKind(parent);
|
||||
}
|
||||
}
|
||||
|
||||
function changeNamedToDefaultImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker): void {
|
||||
const { parent } = ref;
|
||||
switch (parent.kind) {
|
||||
case SyntaxKind.PropertyAccessExpression:
|
||||
// `a.foo` --> `a.default`
|
||||
changes.replaceNode(importingSourceFile, ref, createIdentifier("default"));
|
||||
break;
|
||||
case SyntaxKind.ImportSpecifier:
|
||||
case SyntaxKind.ExportSpecifier: {
|
||||
const spec = parent as ImportSpecifier | ExportSpecifier;
|
||||
if (spec.kind === SyntaxKind.ImportSpecifier) {
|
||||
// `import { foo } from "./a";` --> `import foo from "./a";`
|
||||
// `import { foo as bar } from "./a";` --> `import bar from "./a";`
|
||||
const defaultImport = createIdentifier(spec.name.text);
|
||||
if (spec.parent.elements.length === 1) {
|
||||
changes.replaceNode(importingSourceFile, spec.parent, defaultImport);
|
||||
}
|
||||
else {
|
||||
changes.deleteNodeInList(importingSourceFile, spec);
|
||||
changes.insertNodeBefore(importingSourceFile, spec.parent, defaultImport);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// `export { foo } from "./a";` --> `export { default as foo } from "./a";`
|
||||
// `export { foo as bar } from "./a";` --> `export { default as bar } from "./a";`
|
||||
// `export { foo as default } from "./a";` --> `export { default } from "./a";`
|
||||
// (Because `export foo from "./a";` isn't valid syntax.)
|
||||
changes.replaceNode(importingSourceFile, spec, makeExportSpecifier("default", spec.name.text));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Debug.failBadSyntaxKind(parent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function makeImportSpecifier(propertyName: string, name: string): ImportSpecifier {
|
||||
return createImportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name));
|
||||
}
|
||||
|
||||
function makeExportSpecifier(propertyName: string, name: string): ExportSpecifier {
|
||||
return createExportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name));
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/* @internal */
|
||||
namespace ts.refactor.generateGetAccessorAndSetAccessor {
|
||||
namespace ts.refactor {
|
||||
const refactorName = "Convert import";
|
||||
const actionNameNamespaceToNamed = "Convert namespace import to named imports";
|
||||
const actionNameNamedToNamespace = "Convert named imports to namespace import";
|
||||
|
|
|
@ -241,6 +241,10 @@ namespace ts.textChanges {
|
|||
return this;
|
||||
}
|
||||
|
||||
public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void {
|
||||
this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) });
|
||||
}
|
||||
|
||||
public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = {}) {
|
||||
const startPosition = getAdjustedStartPosition(sourceFile, startNode, options, Position.FullStart);
|
||||
const endPosition = getAdjustedEndPosition(sourceFile, endNode, options);
|
||||
|
@ -397,6 +401,9 @@ namespace ts.textChanges {
|
|||
else if (isParameter(before)) {
|
||||
return {};
|
||||
}
|
||||
else if (isStringLiteral(before) && isImportDeclaration(before.parent) || isNamedImports(before)) {
|
||||
return { suffix: ", " };
|
||||
}
|
||||
return Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it
|
||||
}
|
||||
|
||||
|
@ -465,6 +472,10 @@ namespace ts.textChanges {
|
|||
this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after));
|
||||
}
|
||||
|
||||
public insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray<Node>, newNode: Node): void {
|
||||
this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " });
|
||||
}
|
||||
|
||||
public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: ReadonlyArray<Node>): void {
|
||||
const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes));
|
||||
this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after));
|
||||
|
@ -502,6 +513,9 @@ namespace ts.textChanges {
|
|||
case SyntaxKind.PropertyAssignment:
|
||||
return { suffix: "," + this.newLineCharacter };
|
||||
|
||||
case SyntaxKind.ExportKeyword:
|
||||
return { prefix: " " };
|
||||
|
||||
case SyntaxKind.Parameter:
|
||||
return {};
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"codefixes/useDefaultImport.ts",
|
||||
"codefixes/fixAddModuleReferTypeMissingTypeof.ts",
|
||||
"codefixes/convertToMappedObjectType.ts",
|
||||
"refactors/convertExport.ts",
|
||||
"refactors/convertImport.ts",
|
||||
"refactors/extractSymbol.ts",
|
||||
"refactors/generateGetAccessorAndSetAccessor.ts",
|
||||
|
|
|
@ -1281,13 +1281,17 @@ namespace ts {
|
|||
|
||||
export const enum QuotePreference { Single, Double }
|
||||
|
||||
export function quotePreferenceFromString(str: StringLiteral, sourceFile: SourceFile): QuotePreference {
|
||||
return isStringDoubleQuoted(str, sourceFile) ? QuotePreference.Double : QuotePreference.Single;
|
||||
}
|
||||
|
||||
export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference {
|
||||
if (preferences.quotePreference) {
|
||||
return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double;
|
||||
}
|
||||
else {
|
||||
const firstModuleSpecifier = firstOrUndefined(sourceFile.imports);
|
||||
return !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile) ? QuotePreference.Single : QuotePreference.Double;
|
||||
const firstModuleSpecifier = sourceFile.imports && find(sourceFile.imports, isStringLiteral);
|
||||
return firstModuleSpecifier ? quotePreferenceFromString(firstModuleSpecifier, sourceFile) : QuotePreference.Double;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1384,6 +1388,10 @@ namespace ts {
|
|||
node.getEnd() <= textSpanEnd(span);
|
||||
}
|
||||
|
||||
export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined {
|
||||
return node.modifiers && find(node.modifiers, m => m.kind === kind);
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement): void {
|
||||
const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax);
|
||||
|
|
|
@ -5913,6 +5913,8 @@ declare namespace ts {
|
|||
Add_or_remove_braces_in_an_arrow_function: DiagnosticMessage;
|
||||
Add_braces_to_arrow_function: DiagnosticMessage;
|
||||
Remove_braces_from_arrow_function: DiagnosticMessage;
|
||||
Convert_default_export_to_named_export: DiagnosticMessage;
|
||||
Convert_named_export_to_default_export: DiagnosticMessage;
|
||||
};
|
||||
}
|
||||
declare namespace ts {
|
||||
|
@ -6947,7 +6949,7 @@ declare namespace ts {
|
|||
function isTemplateMiddleOrTemplateTail(node: Node): node is TemplateMiddle | TemplateTail;
|
||||
function isStringTextContainingNode(node: Node): node is StringLiteral | TemplateLiteralToken;
|
||||
function isGeneratedIdentifier(node: Node): node is GeneratedIdentifier;
|
||||
function isModifierKind(token: SyntaxKind): boolean;
|
||||
function isModifierKind(token: SyntaxKind): token is Modifier["kind"];
|
||||
function isParameterPropertyModifier(kind: SyntaxKind): boolean;
|
||||
function isClassMemberModifier(idToken: SyntaxKind): boolean;
|
||||
function isModifier(node: Node): node is Modifier;
|
||||
|
@ -10781,6 +10783,7 @@ declare namespace ts {
|
|||
Single = 0,
|
||||
Double = 1
|
||||
}
|
||||
function quotePreferenceFromString(str: StringLiteral, sourceFile: SourceFile): QuotePreference;
|
||||
function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference;
|
||||
function symbolNameNoDefault(symbol: Symbol): string | undefined;
|
||||
function symbolEscapedNameNoDefault(symbol: Symbol): __String | undefined;
|
||||
|
@ -10805,6 +10808,7 @@ declare namespace ts {
|
|||
some(pred: (node: Node) => boolean): boolean;
|
||||
}
|
||||
function getParentNodeInSpan(node: Node | undefined, file: SourceFile, span: TextSpan): Node | undefined;
|
||||
function findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined;
|
||||
function insertImport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, importDecl: Statement): void;
|
||||
}
|
||||
declare namespace ts {
|
||||
|
@ -10982,7 +10986,7 @@ declare namespace ts.FindAllReferences {
|
|||
}
|
||||
type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult;
|
||||
/** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. */
|
||||
function createImportTracker(sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken): ImportTracker;
|
||||
function createImportTracker(sourceFiles: ReadonlyArray<SourceFile>, sourceFilesSet: ReadonlyMap<true>, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker;
|
||||
/** Info about an exported symbol to perform recursive search on. */
|
||||
interface ExportInfo {
|
||||
exportingModuleSymbol: Symbol;
|
||||
|
@ -11085,6 +11089,7 @@ declare namespace ts.FindAllReferences {
|
|||
declare namespace ts.FindAllReferences.Core {
|
||||
/** Core find-all-references algorithm. Handles special cases before delegating to `getReferencedSymbolsForSymbol`. */
|
||||
function getReferencedSymbolsForNode(position: number, node: Node, program: Program, sourceFiles: ReadonlyArray<SourceFile>, cancellationToken: CancellationToken, options?: Options, sourceFilesSet?: ReadonlyMap<true>): SymbolAndEntries[] | undefined;
|
||||
function eachExportReference(sourceFiles: ReadonlyArray<SourceFile>, checker: TypeChecker, cancellationToken: CancellationToken | undefined, exportSymbol: Symbol, exportingModuleSymbol: Symbol, exportName: string, isDefaultExport: boolean, cb: (ref: Identifier) => void): void;
|
||||
/** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */
|
||||
function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile): boolean;
|
||||
function eachSymbolReferenceInFile<T>(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile, cb: (token: Identifier) => T): T | undefined;
|
||||
|
@ -11470,6 +11475,7 @@ declare namespace ts.textChanges {
|
|||
deleteRange(sourceFile: SourceFile, range: TextRange): this;
|
||||
/** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */
|
||||
deleteNode(sourceFile: SourceFile, node: Node, options?: ConfigurableStartEnd): this;
|
||||
deleteModifier(sourceFile: SourceFile, modifier: Modifier): void;
|
||||
deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options?: ConfigurableStartEnd): this;
|
||||
deleteNodeRangeExcludingEnd(sourceFile: SourceFile, startNode: Node, afterEndNode: Node | undefined, options?: ConfigurableStartEnd): void;
|
||||
deleteNodeInList(sourceFile: SourceFile, node: Node): this;
|
||||
|
@ -11501,6 +11507,7 @@ declare namespace ts.textChanges {
|
|||
private getInsertNodeAtClassStartPrefixSuffix;
|
||||
insertNodeAfterComma(sourceFile: SourceFile, after: Node, newNode: Node): void;
|
||||
insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void;
|
||||
insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray<Node>, newNode: Node): void;
|
||||
insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: ReadonlyArray<Node>): void;
|
||||
private insertNodeAfterWorker;
|
||||
private getInsertNodeAfterOptions;
|
||||
|
@ -11658,7 +11665,9 @@ declare namespace ts.codefix {
|
|||
}
|
||||
declare namespace ts.codefix {
|
||||
}
|
||||
declare namespace ts.refactor.generateGetAccessorAndSetAccessor {
|
||||
declare namespace ts.refactor {
|
||||
}
|
||||
declare namespace ts.refactor {
|
||||
}
|
||||
declare namespace ts.refactor.extractSymbol {
|
||||
/**
|
||||
|
|
|
@ -139,6 +139,7 @@ declare namespace FourSlashInterface {
|
|||
file(name: string, content?: string, scriptKindName?: string): any;
|
||||
select(startMarker: string, endMarker: string): void;
|
||||
selectRange(range: Range): void;
|
||||
selectAllInFile(fileName: string): void;
|
||||
}
|
||||
class verifyNegatable {
|
||||
private negative;
|
||||
|
@ -178,7 +179,7 @@ declare namespace FourSlashInterface {
|
|||
isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void;
|
||||
codeFix(options: {
|
||||
description: string,
|
||||
newFileContent?: string | { readonly [fileName: string]: string },
|
||||
newFileContent?: NewFileContent,
|
||||
newRangeContent?: string,
|
||||
errorCode?: number,
|
||||
index?: number,
|
||||
|
@ -336,7 +337,7 @@ declare namespace FourSlashInterface {
|
|||
getEditsForFileRename(options: {
|
||||
oldPath: string;
|
||||
newPath: string;
|
||||
newFileContents: { [fileName: string]: string };
|
||||
newFileContents: { readonly [fileName: string]: string };
|
||||
}): void;
|
||||
moveToNewFile(options: {
|
||||
readonly newFileContents: { readonly [fileName: string]: string };
|
||||
|
@ -357,7 +358,7 @@ declare namespace FourSlashInterface {
|
|||
enableFormatting(): void;
|
||||
disableFormatting(): void;
|
||||
|
||||
applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: string }): void;
|
||||
applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: NewFileContent }): void;
|
||||
}
|
||||
class debug {
|
||||
printCurrentParameterHelp(): void;
|
||||
|
@ -573,6 +574,7 @@ declare namespace FourSlashInterface {
|
|||
}
|
||||
|
||||
type ArrayOrSingle<T> = T | ReadonlyArray<T>;
|
||||
type NewFileContent = string | { readonly [fileName: string]: string };
|
||||
}
|
||||
declare function verifyOperationIsCancelled(f: any): void;
|
||||
declare var test: FourSlashInterface.test_;
|
||||
|
|
25
tests/cases/fourslash/refactorConvertExport_ambientModule.ts
Normal file
25
tests/cases/fourslash/refactorConvertExport_ambientModule.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @Filename: /foo.ts
|
||||
////declare module "foo" {
|
||||
//// /*a*/export default function foo(): void;/*b*/
|
||||
////}
|
||||
|
||||
// @Filename: /b.ts
|
||||
////import foo from "foo";
|
||||
|
||||
goTo.select("a", "b");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Convert export",
|
||||
actionName: "Convert default export to named export",
|
||||
actionDescription: "Convert default export to named export",
|
||||
newContent: {
|
||||
"/foo.ts":
|
||||
`declare module "foo" {
|
||||
export function foo(): void;
|
||||
}`,
|
||||
|
||||
"/b.ts":
|
||||
`import { foo } from "foo";`,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @Filename: /a.ts
|
||||
/////*a*/export default function f() {}/*b*/
|
||||
|
||||
// @Filename: /b.ts
|
||||
////import f from "./a";
|
||||
////import { default as f } from "./a";
|
||||
////import { default as g } from "./a";
|
||||
////import f, * as a from "./a"; // TODO: GH#24875
|
||||
////
|
||||
////export { default } from "./a";
|
||||
////export { default as f } from "./a";
|
||||
////export { default as i } from "./a";
|
||||
////
|
||||
////import * as a from "./a";
|
||||
////a.default();
|
||||
|
||||
goTo.select("a", "b");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Convert export",
|
||||
actionName: "Convert default export to named export",
|
||||
actionDescription: "Convert default export to named export",
|
||||
newContent: {
|
||||
"/a.ts":
|
||||
`export function f() {}`,
|
||||
|
||||
"/b.ts":
|
||||
`import { f } from "./a";
|
||||
import { f } from "./a";
|
||||
import { f as g } from "./a";
|
||||
import f, * as a from "./a"; // TODO: GH#24875
|
||||
|
||||
export { f as default } from "./a";
|
||||
export { f } from "./a";
|
||||
export { f as i } from "./a";
|
||||
|
||||
import * as a from "./a";
|
||||
a.f();`,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @Filename: /fn.ts
|
||||
////export function f() {}
|
||||
|
||||
// @Filename: /cls.ts
|
||||
////export class C {}
|
||||
|
||||
// @Filename: /interface.ts
|
||||
////export interface I {}
|
||||
|
||||
// @Filename: /enum.ts
|
||||
////export const enum E {}
|
||||
|
||||
// @Filename: /namespace.ts
|
||||
////export namespace N {}
|
||||
|
||||
// @Filename: /type.ts
|
||||
////export type T = number;
|
||||
|
||||
// @Filename: /var_unused.ts
|
||||
////export const x = 0;
|
||||
|
||||
// @Filename: /var_unused_noInitializer.ts
|
||||
////export const x;
|
||||
|
||||
// @Filename: /var_used.ts
|
||||
////export const x = 0;
|
||||
////x;
|
||||
|
||||
const tests: { [fileName: string]: string | undefined } = {
|
||||
fn: `export default function f() {}`,
|
||||
|
||||
cls: `export default class C {}`,
|
||||
|
||||
interface: `export default interface I {}`,
|
||||
|
||||
enum:
|
||||
`const enum E {}
|
||||
export default E;
|
||||
`,
|
||||
|
||||
namespace:
|
||||
`namespace N {}
|
||||
|
||||
export default N;
|
||||
`,
|
||||
|
||||
type:
|
||||
`type T = number;
|
||||
export default T;
|
||||
`,
|
||||
|
||||
var_unused: `export default 0;`,
|
||||
|
||||
var_unused_noInitializer: undefined,
|
||||
|
||||
var_used:
|
||||
`const x = 0;
|
||||
export default x;
|
||||
x;`,
|
||||
};
|
||||
|
||||
for (const name in tests) {
|
||||
const newContent = tests[name];
|
||||
const fileName = `/${name}.ts`;
|
||||
goTo.selectAllInFile(fileName);
|
||||
if (newContent === undefined) {
|
||||
verify.refactorsAvailable([]);
|
||||
}
|
||||
else {
|
||||
edit.applyRefactor({
|
||||
refactorName: "Convert export",
|
||||
actionName: "Convert named export to default export",
|
||||
actionDescription: "Convert named export to default export",
|
||||
newContent: { [fileName]: newContent },
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @Filename: /a.ts
|
||||
/////*a*/export function f() {}/*b*/
|
||||
|
||||
// @Filename: /b.ts
|
||||
////import { f } from "./a";
|
||||
////import { f as g } from "./a";
|
||||
////import { f, other } from "./a";
|
||||
////
|
||||
////export { f } from "./a";
|
||||
////export { f as i } from "./a";
|
||||
////export { f as default } from "./a";
|
||||
////
|
||||
////import * as a from "./a";
|
||||
////a.f();
|
||||
|
||||
goTo.select("a", "b");
|
||||
edit.applyRefactor({
|
||||
refactorName: "Convert export",
|
||||
actionName: "Convert named export to default export",
|
||||
actionDescription: "Convert named export to default export",
|
||||
newContent: {
|
||||
"/a.ts":
|
||||
`export default function f() {}`,
|
||||
|
||||
"/b.ts":
|
||||
`import f from "./a";
|
||||
import g from "./a";
|
||||
import f, { other } from "./a";
|
||||
|
||||
export { default as f } from "./a";
|
||||
export { default as i } from "./a";
|
||||
export { default } from "./a";
|
||||
|
||||
import * as a from "./a";
|
||||
a.default();`,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference path='fourslash.ts' />
|
||||
|
||||
// @Filename: /a.ts
|
||||
/////*a*/export function f() {}/*b*/
|
||||
////export default function g() {}
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorsAvailable([]);
|
Loading…
Reference in a new issue