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:
Andy 2018-06-25 10:34:24 -07:00 committed by GitHub
parent fefad791d3
commit 806a661be3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 526 additions and 39 deletions

View file

@ -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
}
}

View file

@ -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:

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;

View file

@ -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) {

View 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));
}
}

View file

@ -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";

View file

@ -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 {};

View file

@ -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",

View file

@ -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);

View file

@ -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 {
/**

View file

@ -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_;

View 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";`,
},
});

View file

@ -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();`,
},
});

View file

@ -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 },
});
}
}

View file

@ -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();`,
},
});

View file

@ -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([]);