Hierarchical refactorings (#41975)

* add hierarchical refactoring strings

* fourslash tests

* extractSymbol filters returned actions

* move refactorKind check to utilities

* rename parameters

* messaging for addOrRemoveBracesToArrowFunction

* fix up inferFunctionReturnType

* fix up convertArrowFunctionOrFunctionExpression

* add preferences to fourslash method

* fix up convert string

* fix up moveToNewFile

* fix lint errors

* remove extra arrow braces diagnostics

* break out tests

* add refactor helpers

* refactor refactors

* keep list of actions

* address PR comments

* response protocol

* address more comments
This commit is contained in:
Jesse Trinity 2020-12-23 12:50:03 -08:00 committed by GitHub
parent d1ac4515c8
commit 8bbef818ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 627 additions and 325 deletions

View File

@ -6143,6 +6143,34 @@
"category": "Message",
"code": 95148
},
"Return type must be inferred from a function": {
"category": "Message",
"code": 95149
},
"Could not determine function return type": {
"category": "Message",
"code": 95150
},
"Could not convert to arrow function": {
"category": "Message",
"code": 95151
},
"Could not convert to named function": {
"category": "Message",
"code": 95152
},
"Could not convert to anonymous function": {
"category": "Message",
"code": 95153
},
"Can only convert string concatenation": {
"category": "Message",
"code": 95154
},
"Selection is not a valid statement or statements": {
"category": "Message",
"code": 95155
},
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",

View File

@ -3420,6 +3420,12 @@ namespace FourSlash {
}
}
public verifyRefactorKindsAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) {
const refactors = this.getApplicableRefactorsAtSelection("invoked", kind, preferences);
const availableKinds = ts.flatMap(refactors, refactor => refactor.actions).map(action => action.kind);
assert.deepEqual(availableKinds.sort(), expected.sort(), `Expected kinds to be equal`);
}
public verifyRefactorsAvailable(names: readonly string[]): void {
assert.deepEqual(unique(this.getApplicableRefactorsAtSelection(), r => r.name), names);
}
@ -3833,14 +3839,14 @@ namespace FourSlash {
test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved");
}
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit") {
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, ts.emptyOptions, triggerReason);
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) {
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind);
}
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit"): readonly ts.ApplicableRefactorInfo[] {
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason); // eslint-disable-line no-in-operator
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] {
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line no-in-operator
}
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason): readonly ts.ApplicableRefactorInfo[] {
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason) || ts.emptyArray;
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] {
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray;
}
public configurePlugin(pluginName: string, configuration: any): void {

View File

@ -215,6 +215,10 @@ namespace FourSlashInterface {
this.state.verifyRefactorAvailable(this.negative, triggerReason, name, actionName);
}
public refactorKindAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) {
this.state.verifyRefactorKindsAvailable(kind, expected, preferences);
}
public toggleLineComment(newFileContent: string) {
this.state.toggleLineComment(newFileContent);
}

View File

@ -566,7 +566,8 @@ namespace ts.server.protocol {
arguments: GetApplicableRefactorsRequestArgs;
}
export type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & {
triggerReason?: RefactorTriggerReason
triggerReason?: RefactorTriggerReason;
kind?: string;
};
export type RefactorTriggerReason = "implicit" | "invoked";
@ -626,6 +627,11 @@ namespace ts.server.protocol {
* the current context.
*/
notApplicableReason?: string;
/**
* The hierarchical dotted name of the refactor action.
*/
kind?: string;
}
export interface GetEditsForRefactorRequest extends Request {

View File

@ -2129,7 +2129,7 @@ namespace ts.server {
private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file)!;
return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason);
return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason, args.kind);
}
private getEditsForRefactor(args: protocol.GetEditsForRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo {

View File

@ -4,7 +4,8 @@ namespace ts.codefix {
type AcceptedNameType = Identifier | StringLiteral;
type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression;
interface Info {
type Info = AccessorInfo | refactor.RefactorErrorInfo;
interface AccessorInfo {
readonly container: ContainerDeclaration;
readonly isStatic: boolean;
readonly isReadonly: boolean;
@ -16,20 +17,12 @@ namespace ts.codefix {
readonly renameAccessor: boolean;
}
type InfoOrError = {
info: Info,
error?: never
} | {
info?: never,
error: string
};
export function generateAccessorFromProperty(file: SourceFile, program: Program, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined {
const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, program, start, end);
if (!fieldInfo || !fieldInfo.info) return undefined;
if (!fieldInfo || refactor.isRefactorErrorInfo(fieldInfo)) return undefined;
const changeTracker = textChanges.ChangeTracker.fromContext(context);
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo.info;
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo;
suppressLeadingAndTrailingTrivia(fieldName);
suppressLeadingAndTrailingTrivia(accessorName);
@ -112,7 +105,7 @@ namespace ts.codefix {
return modifierFlags;
}
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): InfoOrError | undefined {
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): Info | undefined {
const node = getTokenAtPosition(file, start);
const cursorRequest = start === end && considerEmptySpans;
const declaration = findAncestor(node.parent, isAcceptedDeclaration);
@ -142,17 +135,15 @@ namespace ts.codefix {
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
return {
info: {
isStatic: hasStaticModifier(declaration),
isReadonly: hasEffectiveReadonlyModifier(declaration),
type: getDeclarationType(declaration, program),
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
originalName: (<AcceptedNameType>declaration.name).text,
declaration,
fieldName,
accessorName,
renameAccessor: startWithUnderscore
}
isStatic: hasStaticModifier(declaration),
isReadonly: hasEffectiveReadonlyModifier(declaration),
type: getDeclarationType(declaration, program),
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
originalName: (<AcceptedNameType>declaration.name).text,
declaration,
fieldName,
accessorName,
renameAccessor: startWithUnderscore
};
}

View File

@ -11,7 +11,9 @@ namespace ts.refactor {
export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] {
return arrayFrom(flatMapIterator(refactors.values(), refactor =>
context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context)));
context.cancellationToken && context.cancellationToken.isCancellationRequested() ||
!refactor.kinds?.some(kind => refactorKindBeginsWith(kind, context.kind)) ? undefined :
refactor.getAvailableActions(context)));
}
export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined {

View File

@ -2,45 +2,40 @@
namespace ts.refactor.addOrRemoveBracesToArrowFunction {
const refactorName = "Add or remove braces in an arrow function";
const refactorDescription = Diagnostics.Add_or_remove_braces_in_an_arrow_function.message;
const addBracesActionName = "Add braces to arrow function";
const removeBracesActionName = "Remove braces from arrow function";
const addBracesActionDescription = Diagnostics.Add_braces_to_arrow_function.message;
const removeBracesActionDescription = Diagnostics.Remove_braces_from_arrow_function.message;
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
interface Info {
const addBracesAction = {
name: "Add braces to arrow function",
description: Diagnostics.Add_braces_to_arrow_function.message,
kind: "refactor.rewrite.arrow.braces.add",
};
const removeBracesAction = {
name: "Remove braces from arrow function",
description: Diagnostics.Remove_braces_from_arrow_function.message,
kind: "refactor.rewrite.arrow.braces.remove"
};
registerRefactor(refactorName, {
kinds: [removeBracesAction.kind],
getEditsForAction,
getAvailableActions });
interface FunctionBracesInfo {
func: ArrowFunction;
expression: Expression | undefined;
returnStatement?: ReturnStatement;
addBraces: boolean;
}
type InfoOrError = {
info: Info,
error?: never
} | {
info?: never,
error: string
};
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition, triggerReason } = context;
const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked");
if (!info) return emptyArray;
if (info.error === undefined) {
if (!isRefactorErrorInfo(info)) {
return [{
name: refactorName,
description: refactorDescription,
actions: [
info.info.addBraces ?
{
name: addBracesActionName,
description: addBracesActionDescription
} : {
name: removeBracesActionName,
description: removeBracesActionDescription
}
info.addBraces ? addBracesAction : removeBracesAction
]
}];
}
@ -49,15 +44,10 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
return [{
name: refactorName,
description: refactorDescription,
actions: [{
name: addBracesActionName,
description: addBracesActionDescription,
notApplicableReason: info.error
}, {
name: removeBracesActionName,
description: removeBracesActionDescription,
notApplicableReason: info.error
}]
actions: [
{ ...addBracesAction, notApplicableReason: info.error },
{ ...removeBracesAction, notApplicableReason: info.error },
]
}];
}
@ -67,19 +57,19 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
const { file, startPosition } = context;
const info = getConvertibleArrowFunctionAtPosition(file, startPosition);
if (!info || !info.info) return undefined;
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
const { expression, returnStatement, func } = info.info;
const { expression, returnStatement, func } = info;
let body: ConciseBody;
if (actionName === addBracesActionName) {
if (actionName === addBracesAction.name) {
const returnStatement = factory.createReturnStatement(expression);
body = factory.createBlock([returnStatement], /* multiLine */ true);
suppressLeadingAndTrailingTrivia(body);
copyLeadingComments(expression!, returnStatement, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true);
}
else if (actionName === removeBracesActionName && returnStatement) {
else if (actionName === removeBracesAction.name && returnStatement) {
const actualExpression = expression || factory.createVoidZero();
body = needsParentheses(actualExpression) ? factory.createParenthesizedExpression(actualExpression) : actualExpression;
suppressLeadingAndTrailingTrivia(body);
@ -98,7 +88,7 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
return { renameFilename: undefined, renameLocation: undefined, edits };
}
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): InfoOrError | undefined {
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true, kind?: string): FunctionBracesInfo | RefactorErrorInfo | undefined {
const node = getTokenAtPosition(file, startPosition);
const func = getContainingFunction(node);
@ -118,26 +108,13 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
return undefined;
}
if (isExpression(func.body)) {
return {
info: {
func,
addBraces: true,
expression: func.body
}
};
if (refactorKindBeginsWith(addBracesAction.kind, kind) && isExpression(func.body)) {
return { func, addBraces: true, expression: func.body };
}
else if (func.body.statements.length === 1) {
else if (refactorKindBeginsWith(removeBracesAction.kind, kind) && isBlock(func.body) && func.body.statements.length === 1) {
const firstStatement = first(func.body.statements);
if (isReturnStatement(firstStatement)) {
return {
info: {
func,
addBraces: false,
expression: firstStatement.expression,
returnStatement: firstStatement
}
};
return { func, addBraces: false, expression: firstStatement.expression, returnStatement: firstStatement };
}
}
return undefined;

View File

@ -3,15 +3,30 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
const refactorName = "Convert arrow function or function expression";
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_arrow_function_or_function_expression);
const toAnonymousFunctionActionName = "Convert to anonymous function";
const toNamedFunctionActionName = "Convert to named function";
const toArrowFunctionActionName = "Convert to arrow function";
const toAnonymousFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function);
const toNamedFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_named_function);
const toArrowFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function);
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
const toAnonymousFunctionAction = {
name: "Convert to anonymous function",
description: getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function),
kind: "refactor.rewrite.function.anonymous",
};
const toNamedFunctionAction = {
name: "Convert to named function",
description: getLocaleSpecificMessage(Diagnostics.Convert_to_named_function),
kind: "refactor.rewrite.function.named",
};
const toArrowFunctionAction = {
name: "Convert to arrow function",
description: getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function),
kind: "refactor.rewrite.function.arrow",
};
registerRefactor(refactorName, {
kinds: [
toAnonymousFunctionAction.kind,
toNamedFunctionAction.kind,
toArrowFunctionAction.kind
],
getEditsForAction,
getAvailableActions
});
interface FunctionInfo {
readonly selectedVariableDeclaration: boolean;
@ -26,38 +41,50 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
}
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition, program } = context;
const { file, startPosition, program, kind } = context;
const info = getFunctionInfo(file, startPosition, program);
if (!info) return emptyArray;
const { selectedVariableDeclaration, func } = info;
const possibleActions: RefactorActionInfo[] = [];
if (selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent))) {
possibleActions.push({
name: toNamedFunctionActionName,
description: toNamedFunctionActionDescription
});
const errors: RefactorActionInfo[] = [];
if (refactorKindBeginsWith(toNamedFunctionAction.kind, kind)) {
const error = selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent)) ?
undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_named_function);
if (error) {
errors.push({ ...toNamedFunctionAction, notApplicableReason: error });
}
else {
possibleActions.push(toNamedFunctionAction);
}
}
if (!selectedVariableDeclaration && isArrowFunction(func)) {
possibleActions.push({
name: toAnonymousFunctionActionName,
description: toAnonymousFunctionActionDescription
});
if (refactorKindBeginsWith(toAnonymousFunctionAction.kind, kind)) {
const error = !selectedVariableDeclaration && isArrowFunction(func) ?
undefined: getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_anonymous_function);
if (error) {
errors.push({ ...toAnonymousFunctionAction, notApplicableReason: error });
}
else {
possibleActions.push(toAnonymousFunctionAction);
}
}
if (isFunctionExpression(func)) {
possibleActions.push({
name: toArrowFunctionActionName,
description: toArrowFunctionActionDescription
});
if (refactorKindBeginsWith(toArrowFunctionAction.kind, kind)) {
const error = isFunctionExpression(func) ? undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_arrow_function);
if (error) {
errors.push({ ...toArrowFunctionAction, notApplicableReason: error });
}
else {
possibleActions.push(toArrowFunctionAction);
}
}
return [{
name: refactorName,
description: refactorDescription,
actions: possibleActions
actions: possibleActions.length === 0 && context.preferences.provideRefactorNotApplicableReason ?
errors : possibleActions
}];
}
@ -70,18 +97,18 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
const edits: FileTextChanges[] = [];
switch (actionName) {
case toAnonymousFunctionActionName:
case toAnonymousFunctionAction.name:
edits.push(...getEditInfoForConvertToAnonymousFunction(context, func));
break;
case toNamedFunctionActionName:
case toNamedFunctionAction.name:
const variableInfo = getVariableInfo(func);
if (!variableInfo) return undefined;
edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo));
break;
case toArrowFunctionActionName:
case toArrowFunctionAction.name:
if (!isFunctionExpression(func)) return undefined;
edits.push(...getEditInfoForConvertToArrowFunction(context, func));
break;

View File

@ -1,54 +1,62 @@
/* @internal */
namespace ts.refactor {
const refactorName = "Convert export";
const actionNameDefaultToNamed = "Convert default export to named export";
const actionNameNamedToDefault = "Convert named export to default export";
const defaultToNamedAction = {
name: "Convert default export to named export",
description: Diagnostics.Convert_default_export_to_named_export.message,
kind: "refactor.rewrite.export.named"
};
const namedToDefaultAction = {
name: "Convert named export to default export",
description: Diagnostics.Convert_named_export_to_default_export.message,
kind: "refactor.rewrite.export.default"
};
registerRefactor(refactorName, {
kinds: [
defaultToNamedAction.kind,
namedToDefaultAction.kind
],
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
const info = getInfo(context, context.triggerReason === "invoked");
if (!info) return emptyArray;
if (info.error === undefined) {
const description = info.info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message;
const actionName = info.info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault;
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }];
if (!isRefactorErrorInfo(info)) {
const action = info.wasDefault ? defaultToNamedAction : namedToDefaultAction;
return [{ name: refactorName, description: action.description, actions: [action] }];
}
if (context.preferences.provideRefactorNotApplicableReason) {
return [
{ name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [{ name: actionNameDefaultToNamed, description: Diagnostics.Convert_default_export_to_named_export.message, notApplicableReason: info.error }] },
{ name: refactorName, description: Diagnostics.Convert_named_export_to_default_export.message, actions: [{ name: actionNameNamedToDefault, description: Diagnostics.Convert_named_export_to_default_export.message, notApplicableReason: info.error }] },
{ name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [
{ ...defaultToNamedAction, notApplicableReason: info.error },
{ ...namedToDefaultAction, notApplicableReason: info.error },
]}
];
}
return emptyArray;
},
getEditsForAction(context, actionName): RefactorEditInfo {
Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault, "Unexpected action name");
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context)?.info, "context must have info"), t, context.cancellationToken));
Debug.assert(actionName === defaultToNamedAction.name || actionName === namedToDefaultAction.name, "Unexpected action name");
const info = getInfo(context);
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, info, 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 {
interface ExportInfo {
readonly exportNode: ExportToConvert;
readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s.
readonly wasDefault: boolean;
readonly exportingModuleSymbol: Symbol;
}
type InfoOrError = {
info: Info,
error?: never
} | {
info?: never,
error: string
};
function getInfo(context: RefactorContext, considerPartialSpans = true): InfoOrError | undefined {
function getInfo(context: RefactorContext, considerPartialSpans = true): ExportInfo | RefactorErrorInfo | undefined {
const { file } = context;
const span = getRefactorContextSpan(context);
const token = getTokenAtPosition(file, span.start);
@ -74,7 +82,7 @@ namespace ts.refactor {
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.ModuleDeclaration: {
const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration;
return node.name && isIdentifier(node.name) ? { info: { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } } : undefined;
return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined;
}
case SyntaxKind.VariableStatement: {
const vs = exportNode as VariableStatement;
@ -85,19 +93,19 @@ namespace ts.refactor {
const decl = first(vs.declarationList.declarations);
if (!decl.initializer) return undefined;
Debug.assert(!wasDefault, "Can't have a default flag here");
return isIdentifier(decl.name) ? { info: { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } } : undefined;
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 {
function doChange(exportingSourceFile: SourceFile, program: Program, info: ExportInfo, 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 {
function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: ExportInfo, changes: textChanges.ChangeTracker, checker: TypeChecker): void {
if (wasDefault) {
changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list"));
}
@ -131,7 +139,7 @@ namespace ts.refactor {
}
}
function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
const checker = program.getTypeChecker();
const exportSymbol = Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol");
FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => {

View File

@ -1,46 +1,55 @@
/* @internal */
namespace ts.refactor {
const refactorName = "Convert import";
const actionNameNamespaceToNamed = "Convert namespace import to named imports";
const actionNameNamedToNamespace = "Convert named imports to namespace import";
type NamedImportBindingsOrError = {
info: NamedImportBindings,
error?: never
} | {
info?: never,
error: string
const namespaceToNamedAction = {
name: "Convert namespace import to named imports",
description: Diagnostics.Convert_namespace_import_to_named_imports.message,
kind: "refactor.rewrite.import.named",
};
const namedToNamespaceAction = {
name: "Convert named imports to namespace import",
description: Diagnostics.Convert_named_imports_to_namespace_import.message,
kind: "refactor.rewrite.import.namespace",
};
registerRefactor(refactorName, {
kinds: [
namespaceToNamedAction.kind,
namedToNamespaceAction.kind
],
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
const i = getImportToConvert(context, context.triggerReason === "invoked");
if (!i) return emptyArray;
const info = getImportToConvert(context, context.triggerReason === "invoked");
if (!info) return emptyArray;
if (i.error === undefined) {
const description = i.info.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message;
const actionName = i.info.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace;
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }];
if (!isRefactorErrorInfo(info)) {
const namespaceImport = info.kind === SyntaxKind.NamespaceImport;
const action = namespaceImport ? namespaceToNamedAction : namedToNamespaceAction;
return [{ name: refactorName, description: action.description, actions: [action] }];
}
if (context.preferences.provideRefactorNotApplicableReason) {
return [
{ name: refactorName, description: Diagnostics.Convert_namespace_import_to_named_imports.message, actions: [{ name: actionNameNamespaceToNamed, description: Diagnostics.Convert_namespace_import_to_named_imports.message, notApplicableReason: i.error }] },
{ name: refactorName, description: Diagnostics.Convert_named_imports_to_namespace_import.message, actions: [{ name: actionNameNamedToNamespace, description: Diagnostics.Convert_named_imports_to_namespace_import.message, notApplicableReason: i.error }] }
{ name: refactorName, description: namespaceToNamedAction.description,
actions: [{ ...namespaceToNamedAction, notApplicableReason: info.error }] },
{ name: refactorName, description: namedToNamespaceAction.description,
actions: [{ ...namedToNamespaceAction, notApplicableReason: info.error }] }
];
}
return emptyArray;
},
getEditsForAction(context, actionName): RefactorEditInfo {
Debug.assert(actionName === actionNameNamespaceToNamed || actionName === actionNameNamedToNamespace, "Unexpected action name");
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.checkDefined(getImportToConvert(context)?.info, "Context must provide an import to convert")));
Debug.assert(actionName === namespaceToNamedAction.name || actionName === namedToNamespaceAction.name, "Unexpected action name");
const info = getImportToConvert(context);
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info));
return { edits, renameFilename: undefined, renameLocation: undefined };
}
});
// Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`.
function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindingsOrError | undefined {
function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindings | RefactorErrorInfo | undefined {
const { file } = context;
const span = getRefactorContextSpan(context);
const token = getTokenAtPosition(file, span.start);
@ -57,7 +66,7 @@ namespace ts.refactor {
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) };
}
return { info: importClause.namedBindings };
return importClause.namedBindings;
}
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void {

View File

@ -2,8 +2,17 @@
namespace ts.refactor.addOrRemoveBracesToArrowFunction {
const refactorName = "Convert overload list to single signature";
const refactorDescription = Diagnostics.Convert_overload_list_to_single_signature.message;
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
const functionOverloadAction = {
name: refactorName,
description: refactorDescription,
kind: "refactor.rewrite.function.overloadList",
};
registerRefactor(refactorName, {
kinds: [functionOverloadAction.kind],
getEditsForAction,
getAvailableActions
});
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition, program } = context;
@ -13,10 +22,7 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
return [{
name: refactorName,
description: refactorDescription,
actions: [{
name: refactorName,
description: refactorDescription
}]
actions: [functionOverloadAction]
}];
}

View File

@ -2,8 +2,18 @@
namespace ts.refactor.convertParamsToDestructuredObject {
const refactorName = "Convert parameters to destructured object";
const minimumParameterLength = 2;
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object);
const toDestructuredAction = {
name: refactorName,
description: refactorDescription,
kind: "refactor.rewrite.parameters.toDestructured"
};
registerRefactor(refactorName, {
kinds: [toDestructuredAction.kind],
getEditsForAction,
getAvailableActions
});
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition } = context;
@ -12,14 +22,10 @@ namespace ts.refactor.convertParamsToDestructuredObject {
const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, context.program.getTypeChecker());
if (!functionDeclaration) return emptyArray;
const description = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object);
return [{
name: refactorName,
description,
actions: [{
name: refactorName,
description
}]
description: refactorDescription,
actions: [toDestructuredAction]
}];
}

View File

@ -3,7 +3,16 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
const refactorName = "Convert to template string";
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string);
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
const convertStringAction = {
name: refactorName,
description: refactorDescription,
kind: "refactor.rewrite.string"
};
registerRefactor(refactorName, {
kinds: [convertStringAction.kind],
getEditsForAction,
getAvailableActions
});
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition } = context;
@ -12,7 +21,13 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
const refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] };
if (isBinaryExpression(maybeBinary) && isStringConcatenationValid(maybeBinary)) {
refactorInfo.actions.push({ name: refactorName, description: refactorDescription });
refactorInfo.actions.push(convertStringAction);
return [refactorInfo];
}
else if (context.preferences.provideRefactorNotApplicableReason) {
refactorInfo.actions.push({ ...convertStringAction,
notApplicableReason: getLocaleSpecificMessage(Diagnostics.Can_only_convert_string_concatenation)
});
return [refactorInfo];
}
return emptyArray;

View File

@ -3,20 +3,26 @@ namespace ts.refactor.convertToOptionalChainExpression {
const refactorName = "Convert to optional chain expression";
const convertToOptionalChainExpressionMessage = getLocaleSpecificMessage(Diagnostics.Convert_to_optional_chain_expression);
registerRefactor(refactorName, { getAvailableActions, getEditsForAction });
const toOptionalChainAction = {
name: refactorName,
description: convertToOptionalChainExpressionMessage,
kind: "refactor.rewrite.expression.optionalChain",
};
registerRefactor(refactorName, {
kinds: [toOptionalChainAction.kind],
getAvailableActions,
getEditsForAction
});
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const info = getInfo(context, context.triggerReason === "invoked");
if (!info) return emptyArray;
if (!info.error) {
if (!isRefactorErrorInfo(info)) {
return [{
name: refactorName,
description: convertToOptionalChainExpressionMessage,
actions: [{
name: refactorName,
description: convertToOptionalChainExpressionMessage
}]
actions: [toOptionalChainAction],
}];
}
@ -24,11 +30,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
return [{
name: refactorName,
description: convertToOptionalChainExpressionMessage,
actions: [{
name: refactorName,
description: convertToOptionalChainExpressionMessage,
notApplicableReason: info.error
}]
actions: [{ ...toOptionalChainAction, notApplicableReason: info.error }],
}];
}
return emptyArray;
@ -36,24 +38,16 @@ namespace ts.refactor.convertToOptionalChainExpression {
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
const info = getInfo(context);
if (!info || !info.info) return undefined;
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
const edits = textChanges.ChangeTracker.with(context, t =>
doChange(context.file, context.program.getTypeChecker(), t, Debug.checkDefined(info.info, "context must have info"), actionName)
doChange(context.file, context.program.getTypeChecker(), t, info, actionName)
);
return { edits, renameFilename: undefined, renameLocation: undefined };
}
type InfoOrError = {
info: Info,
error?: never;
} | {
info?: never,
error: string;
};
type Occurrence = PropertyAccessExpression | ElementAccessExpression | Identifier;
interface Info {
interface OptionalChainInfo {
finalExpression: PropertyAccessExpression | ElementAccessExpression | CallExpression,
occurrences: Occurrence[],
expression: ValidExpression,
@ -83,7 +77,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
return isValidExpression(node) || isValidStatement(node);
}
function getInfo(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined {
function getInfo(context: RefactorContext, considerEmptySpans = true): OptionalChainInfo | RefactorErrorInfo | undefined {
const { file, program } = context;
const span = getRefactorContextSpan(context);
@ -103,7 +97,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
return isConditionalExpression(expression) ? getConditionalInfo(expression, checker) : getBinaryInfo(expression);
}
function getConditionalInfo(expression: ConditionalExpression, checker: TypeChecker): InfoOrError | undefined {
function getConditionalInfo(expression: ConditionalExpression, checker: TypeChecker): OptionalChainInfo | RefactorErrorInfo | undefined {
const condition = expression.condition;
const finalExpression = getFinalExpressionInChain(expression.whenTrue);
@ -113,16 +107,16 @@ namespace ts.refactor.convertToOptionalChainExpression {
if ((isPropertyAccessExpression(condition) || isIdentifier(condition))
&& getMatchingStart(condition, finalExpression.expression)) {
return { info: { finalExpression, occurrences: [condition], expression } };
return { finalExpression, occurrences: [condition], expression };
}
else if (isBinaryExpression(condition)) {
const occurrences = getOccurrencesInExpression(finalExpression.expression, condition);
return occurrences ? { info: { finalExpression, occurrences, expression } } :
return occurrences ? { finalExpression, occurrences, expression } :
{ error: getLocaleSpecificMessage(Diagnostics.Could_not_find_matching_access_expressions) };
}
}
function getBinaryInfo(expression: BinaryExpression): InfoOrError | undefined {
function getBinaryInfo(expression: BinaryExpression): OptionalChainInfo | RefactorErrorInfo | undefined {
if (expression.operatorToken.kind !== SyntaxKind.AmpersandAmpersandToken) {
return { error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_logical_AND_access_chains) };
};
@ -131,7 +125,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
if (!finalExpression) return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_convertible_access_expression) };
const occurrences = getOccurrencesInExpression(finalExpression.expression, expression.left);
return occurrences ? { info: { finalExpression, occurrences, expression } } :
return occurrences ? { finalExpression, occurrences, expression } :
{ error: getLocaleSpecificMessage(Diagnostics.Could_not_find_matching_access_expressions) };
}
@ -288,7 +282,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
return toConvert;
}
function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: Info, _actionName: string): void {
function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: OptionalChainInfo, _actionName: string): void {
const { finalExpression, occurrences, expression } = info;
const firstOccurrence = occurrences[occurrences.length - 1];
const convertedChain = convertOccurrences(checker, finalExpression, occurrences);

View File

@ -1,39 +1,56 @@
/* @internal */
namespace ts.refactor.extractSymbol {
const refactorName = "Extract Symbol";
registerRefactor(refactorName, { getAvailableActions, getEditsForAction });
const extractConstantAction = {
name: "Extract Constant",
description: getLocaleSpecificMessage(Diagnostics.Extract_constant),
kind: "refactor.extract.constant",
};
const extractFunctionAction = {
name: "Extract Function",
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
kind: "refactor.extract.function",
};
registerRefactor(refactorName, {
kinds: [
extractConstantAction.kind,
extractFunctionAction.kind
],
getAvailableActions,
getEditsForAction
});
/**
* Compute the associated code actions
* Exported for tests.
*/
export function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const requestedRefactor = context.kind;
const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context), context.triggerReason === "invoked");
const targetRange = rangeToExtract.targetRange;
if (targetRange === undefined) {
if (!rangeToExtract.errors || rangeToExtract.errors.length === 0 || !context.preferences.provideRefactorNotApplicableReason) {
return emptyArray;
}
return [{
name: refactorName,
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
actions: [{
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
name: "function_extract_error",
notApplicableReason: getStringError(rangeToExtract.errors)
}]
},
{
name: refactorName,
description: getLocaleSpecificMessage(Diagnostics.Extract_constant),
actions: [{
description: getLocaleSpecificMessage(Diagnostics.Extract_constant),
name: "constant_extract_error",
notApplicableReason: getStringError(rangeToExtract.errors)
}]
}];
const errors = [];
if (refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) {
errors.push({
name: refactorName,
description: extractFunctionAction.description,
actions: [{ ...extractFunctionAction, notApplicableReason: getStringError(rangeToExtract.errors) }]
});
}
if (refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) {
errors.push({
name: refactorName,
description: extractConstantAction.description,
actions: [{ ...extractConstantAction, notApplicableReason: getStringError(rangeToExtract.errors) }]
});
}
return errors;
}
const extractions = getPossibleExtractions(targetRange, context);
@ -53,46 +70,54 @@ namespace ts.refactor.extractSymbol {
let i = 0;
for (const { functionExtraction, constantExtraction } of extractions) {
const description = functionExtraction.description;
if (functionExtraction.errors.length === 0) {
// Don't issue refactorings with duplicated names.
// Scopes come back in "innermost first" order, so extractions will
// preferentially go into nearer scopes
if (!usedFunctionNames.has(description)) {
usedFunctionNames.set(description, true);
functionActions.push({
description,
name: `function_scope_${i}`
});
if(refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)){
if (functionExtraction.errors.length === 0) {
// Don't issue refactorings with duplicated names.
// Scopes come back in "innermost first" order, so extractions will
// preferentially go into nearer scopes
if (!usedFunctionNames.has(description)) {
usedFunctionNames.set(description, true);
functionActions.push({
description,
name: `function_scope_${i}`,
kind: extractFunctionAction.kind
});
}
}
else if (!innermostErrorFunctionAction) {
innermostErrorFunctionAction = {
description,
name: `function_scope_${i}`,
notApplicableReason: getStringError(functionExtraction.errors),
kind: extractFunctionAction.kind
};
}
}
else if (!innermostErrorFunctionAction) {
innermostErrorFunctionAction = {
description,
name: `function_scope_${i}`,
notApplicableReason: getStringError(functionExtraction.errors)
};
}
// Skip these since we don't have a way to report errors yet
if (constantExtraction.errors.length === 0) {
// Don't issue refactorings with duplicated names.
// Scopes come back in "innermost first" order, so extractions will
// preferentially go into nearer scopes
const description = constantExtraction.description;
if (!usedConstantNames.has(description)) {
usedConstantNames.set(description, true);
constantActions.push({
description,
name: `constant_scope_${i}`
});
if(refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) {
if (constantExtraction.errors.length === 0) {
// Don't issue refactorings with duplicated names.
// Scopes come back in "innermost first" order, so extractions will
// preferentially go into nearer scopes
const description = constantExtraction.description;
if (!usedConstantNames.has(description)) {
usedConstantNames.set(description, true);
constantActions.push({
description,
name: `constant_scope_${i}`,
kind: extractConstantAction.kind
});
}
}
else if (!innermostErrorConstantAction) {
innermostErrorConstantAction = {
description,
name: `constant_scope_${i}`,
notApplicableReason: getStringError(constantExtraction.errors),
kind: extractConstantAction.kind
};
}
}
else if (!innermostErrorConstantAction) {
innermostErrorConstantAction = {
description,
name: `constant_scope_${i}`,
notApplicableReason: getStringError(constantExtraction.errors)
};
}
// *do* increment i anyway because we'll look for the i-th scope
@ -106,7 +131,7 @@ namespace ts.refactor.extractSymbol {
infos.push({
name: refactorName,
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
actions: functionActions
actions: functionActions,
});
}
else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorFunctionAction) {

View File

@ -1,25 +1,39 @@
/* @internal */
namespace ts.refactor {
const refactorName = "Extract type";
const extractToTypeAlias = "Extract to type alias";
const extractToInterface = "Extract to interface";
const extractToTypeDef = "Extract to typedef";
const extractToTypeAliasAction = {
name: "Extract to type alias",
description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias),
kind: "refactor.extract.type",
};
const extractToInterfaceAction = {
name: "Extract to interface",
description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface),
kind: "refactor.extract.interface",
};
const extractToTypeDefAction = {
name: "Extract to typedef",
description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef),
kind: "refactor.extract.typedef"
};
registerRefactor(refactorName, {
kinds: [
extractToTypeAliasAction.kind,
extractToInterfaceAction.kind,
extractToTypeDefAction.kind
],
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
const info = getRangeToExtract(context, context.triggerReason === "invoked");
if (!info) return emptyArray;
if (info.error === undefined) {
if (!isRefactorErrorInfo(info)) {
return [{
name: refactorName,
description: getLocaleSpecificMessage(Diagnostics.Extract_type),
actions: info.info.isJS ? [{
name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef)
}] : append([{
name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias)
}], info.info.typeElements && {
name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface)
})
actions: info.isJS ?
[extractToTypeDefAction] : append([extractToTypeAliasAction], info.typeElements && extractToInterfaceAction)
}];
}
@ -28,9 +42,9 @@ namespace ts.refactor {
name: refactorName,
description: getLocaleSpecificMessage(Diagnostics.Extract_type),
actions: [
{ name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef), notApplicableReason: info.error },
{ name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias), notApplicableReason: info.error },
{ name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface), notApplicableReason: info.error },
{ ...extractToTypeDefAction, notApplicableReason: info.error },
{ ...extractToTypeAliasAction, notApplicableReason: info.error },
{ ...extractToInterfaceAction, notApplicableReason: info.error },
]
}];
}
@ -39,18 +53,19 @@ namespace ts.refactor {
},
getEditsForAction(context, actionName): RefactorEditInfo {
const { file, } = context;
const info = Debug.checkDefined(getRangeToExtract(context)?.info, "Expected to find a range to extract");
const info = getRangeToExtract(context);
Debug.assert(info && !isRefactorErrorInfo(info), "Expected to find a range to extract");
const name = getUniqueName("NewType", file);
const edits = textChanges.ChangeTracker.with(context, changes => {
switch (actionName) {
case extractToTypeAlias:
case extractToTypeAliasAction.name:
Debug.assert(!info.isJS, "Invalid actionName/JS combo");
return doTypeAliasChange(changes, file, name, info);
case extractToTypeDef:
case extractToTypeDefAction.name:
Debug.assert(info.isJS, "Invalid actionName/JS combo");
return doTypedefChange(changes, file, name, info);
case extractToInterface:
case extractToInterfaceAction.name:
Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo");
return doInterfaceChange(changes, file, name, info as InterfaceInfo);
default:
@ -72,16 +87,9 @@ namespace ts.refactor {
isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: readonly TypeParameterDeclaration[]; typeElements: readonly TypeElement[];
}
type Info = TypeAliasInfo | InterfaceInfo;
type InfoOrError = {
info: Info,
error?: never
} | {
info?: never,
error: string
};
type ExtractInfo = TypeAliasInfo | InterfaceInfo;
function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined {
function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): ExtractInfo | RefactorErrorInfo | undefined {
const { file, startPosition } = context;
const isJS = isSourceFileJS(file);
const current = getTokenAtPosition(file, startPosition);
@ -98,7 +106,7 @@ namespace ts.refactor {
if (!typeParameters) return { error: getLocaleSpecificMessage(Diagnostics.No_type_could_be_extracted_from_this_type_node) };
const typeElements = flattenTypeLiteralNodeReference(checker, selection);
return { info: { isJS, selection, firstStatement, typeParameters, typeElements } };
return { isJS, selection, firstStatement, typeParameters, typeElements };
}
function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): readonly TypeElement[] | undefined {
@ -209,7 +217,7 @@ namespace ts.refactor {
changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace });
}
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: Info) {
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: ExtractInfo) {
const { firstStatement, selection, typeParameters } = info;
const node = factory.createJSDocTypedefTag(

View File

@ -2,18 +2,25 @@
namespace ts.refactor.generateGetAccessorAndSetAccessor {
const actionName = "Generate 'get' and 'set' accessors";
const actionDescription = Diagnostics.Generate_get_and_set_accessors.message;
const generateGetSetAction = {
name: actionName,
description: actionDescription,
kind: "refactor.rewrite.property.generateAccessors",
};
registerRefactor(actionName, {
kinds: [generateGetSetAction.kind],
getEditsForAction(context, actionName) {
if (!context.endPosition) return undefined;
const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition);
if (!info || !info.info) return undefined;
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
const edits = codefix.generateAccessorFromProperty(context.file, context.program, context.startPosition, context.endPosition, context, actionName);
if (!edits) return undefined;
const renameFilename = context.file.fileName;
const nameNeedRename = info.info.renameAccessor ? info.info.accessorName : info.info.fieldName;
const nameNeedRename = info.renameAccessor ? info.accessorName : info.fieldName;
const renameLocationOffset = isIdentifier(nameNeedRename) ? 0 : -1;
const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.info.declaration));
const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.declaration));
return { renameFilename, renameLocation, edits };
},
@ -22,16 +29,11 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition, context.triggerReason === "invoked");
if (!info) return emptyArray;
if (!info.error) {
if (!isRefactorErrorInfo(info)) {
return [{
name: actionName,
description: actionDescription,
actions: [
{
name: actionName,
description: actionDescription
}
]
actions: [generateGetSetAction],
}];
}
@ -39,11 +41,7 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
return [{
name: actionName,
description: actionDescription,
actions: [{
name: actionName,
description: actionDescription,
notApplicableReason: info.error
}]
actions: [{ ...generateGetSetAction, notApplicableReason: info.error }],
}];
}

View File

@ -0,0 +1,25 @@
/* @internal */
namespace ts.refactor {
/**
* Returned by refactor funtions when some error message needs to be surfaced to users.
*/
export interface RefactorErrorInfo {
error: string;
};
/**
* Checks if some refactor info has refactor error info.
*/
export function isRefactorErrorInfo(info: unknown): info is RefactorErrorInfo {
return (info as RefactorErrorInfo).error !== undefined;
}
/**
* Checks if string "known" begins with string "requested".
* Used to match requested kinds with a known kind.
*/
export function refactorKindBeginsWith(known: string, requested: string | undefined): boolean {
if(!requested) return true;
return known.substr(0, requested.length) === requested;
}
}

View File

@ -2,11 +2,21 @@
namespace ts.refactor.inferFunctionReturnType {
const refactorName = "Infer function return type";
const refactorDescription = Diagnostics.Infer_function_return_type.message;
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
const inferReturnTypeAction = {
name: refactorName,
description: refactorDescription,
kind: "refactor.rewrite.function.returnType"
};
registerRefactor(refactorName, {
kinds: [inferReturnTypeAction.kind],
getEditsForAction,
getAvailableActions
});
function getEditsForAction(context: RefactorContext): RefactorEditInfo | undefined {
const info = getInfo(context);
if (info) {
if (info && !isRefactorErrorInfo(info)) {
const edits = textChanges.ChangeTracker.with(context, t =>
t.tryInsertTypeAnnotation(context.file, info.declaration, info.returnTypeNode));
return { renameFilename: undefined, renameLocation: undefined, edits };
@ -16,14 +26,19 @@ namespace ts.refactor.inferFunctionReturnType {
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const info = getInfo(context);
if (info) {
if (!info) return emptyArray;
if (!isRefactorErrorInfo(info)) {
return [{
name: refactorName,
description: refactorDescription,
actions: [{
name: refactorName,
description: refactorDescription
}]
actions: [inferReturnTypeAction]
}];
}
if (context.preferences.provideRefactorNotApplicableReason) {
return [{
name: refactorName,
description: refactorDescription,
actions: [{ ...inferReturnTypeAction, notApplicableReason: info.error }]
}];
}
return emptyArray;
@ -35,21 +50,25 @@ namespace ts.refactor.inferFunctionReturnType {
| ArrowFunction
| MethodDeclaration;
interface Info {
interface FunctionInfo {
declaration: ConvertibleDeclaration;
returnTypeNode: TypeNode;
}
function getInfo(context: RefactorContext): Info | undefined {
if (isInJSFile(context.file)) return;
function getInfo(context: RefactorContext): FunctionInfo | RefactorErrorInfo | undefined {
if (isInJSFile(context.file) || !refactorKindBeginsWith(inferReturnTypeAction.kind, context.kind)) return;
const token = getTokenAtPosition(context.file, context.startPosition);
const declaration = findAncestor(token, isConvertibleDeclaration);
if (!declaration || !declaration.body || declaration.type) return;
if (!declaration || !declaration.body || declaration.type) {
return { error: getLocaleSpecificMessage(Diagnostics.Return_type_must_be_inferred_from_a_function) };
}
const typeChecker = context.program.getTypeChecker();
const returnType = tryGetReturnType(typeChecker, declaration);
if (!returnType) return;
if (!returnType) {
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_determine_function_return_type) };
};
const returnTypeNode = typeChecker.typeToTypeNode(returnType, declaration, NodeBuilderFlags.NoTruncation);
if (returnTypeNode) {

View File

@ -1,11 +1,26 @@
/* @internal */
namespace ts.refactor {
const refactorName = "Move to a new file";
const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file);
const moveToNewFileAction = {
name: refactorName,
description,
kind: "refactor.move.newFile",
};
registerRefactor(refactorName, {
kinds: [moveToNewFileAction.kind],
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
if (!context.preferences.allowTextChangesInNewFiles || getStatementsToMove(context) === undefined) return emptyArray;
const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file);
return [{ name: refactorName, description, actions: [{ name: refactorName, description }] }];
const statements = getStatementsToMove(context);
if (context.preferences.allowTextChangesInNewFiles && statements) {
return [{ name: refactorName, description, actions: [moveToNewFileAction] }];
}
if (context.preferences.provideRefactorNotApplicableReason) {
return [{ name: refactorName, description, actions:
[{ ...moveToNewFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }]
}];
}
return emptyArray;
},
getEditsForAction(context, actionName): RefactorEditInfo {
Debug.assert(actionName === refactorName, "Wrong refactor invoked");

View File

@ -2451,7 +2451,7 @@ namespace ts {
return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options);
}
function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings, triggerReason?: RefactorTriggerReason): RefactorContext {
function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings, triggerReason?: RefactorTriggerReason, kind?: string): RefactorContext {
const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end];
return {
file,
@ -2463,6 +2463,7 @@ namespace ts {
cancellationToken,
preferences,
triggerReason,
kind
};
}
@ -2470,10 +2471,10 @@ namespace ts {
return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName));
}
function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason): ApplicableRefactorInfo[] {
function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason, kind: string): ApplicableRefactorInfo[] {
synchronizeHostData();
const file = getValidSourceFile(fileName);
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason));
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind));
}
function getEditsForRefactor(

View File

@ -116,6 +116,7 @@
"refactors/extractSymbol.ts",
"refactors/extractType.ts",
"refactors/generateGetAccessorAndSetAccessor.ts",
"refactors/helpers.ts",
"refactors/moveToNewFile.ts",
"refactors/addOrRemoveBracesToArrowFunction.ts",
"refactors/convertParamsToDestructuredObject.ts",

View File

@ -520,7 +520,7 @@ namespace ts {
/** @deprecated `fileName` will be ignored */
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason): ApplicableRefactorInfo[];
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
@ -792,6 +792,11 @@ namespace ts {
* the current context.
*/
notApplicableReason?: string;
/**
* The hierarchical dotted name of the refactor action.
*/
kind?: string;
}
/**
@ -1464,6 +1469,10 @@ namespace ts {
/** @internal */
export interface Refactor {
/** List of action kinds a refactor can provide.
* Used to skip unnecessary calculation when specific refactors are requested. */
kinds?: string[];
/** Compute the associated code actions */
getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined;
@ -1480,5 +1489,6 @@ namespace ts {
cancellationToken?: CancellationToken;
preferences: UserPreferences;
triggerReason?: RefactorTriggerReason;
kind?: string;
}
}

View File

@ -5570,7 +5570,7 @@ declare namespace ts {
applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
/** @deprecated `fileName` will be ignored */
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason): ApplicableRefactorInfo[];
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
@ -5796,6 +5796,10 @@ declare namespace ts {
* the current context.
*/
notApplicableReason?: string;
/**
* The hierarchical dotted name of the refactor action.
*/
kind?: string;
}
/**
* A set of edits to make in response to a refactor action, plus an optional
@ -6926,6 +6930,7 @@ declare namespace ts.server.protocol {
}
type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & {
triggerReason?: RefactorTriggerReason;
kind?: string;
};
type RefactorTriggerReason = "implicit" | "invoked";
/**
@ -6978,6 +6983,10 @@ declare namespace ts.server.protocol {
* the current context.
*/
notApplicableReason?: string;
/**
* The hierarchical dotted name of the refactor action.
*/
kind?: string;
}
interface GetEditsForRefactorRequest extends Request {
command: CommandTypes.GetEditsForRefactor;

View File

@ -5570,7 +5570,7 @@ declare namespace ts {
applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
/** @deprecated `fileName` will be ignored */
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason): ApplicableRefactorInfo[];
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
@ -5796,6 +5796,10 @@ declare namespace ts {
* the current context.
*/
notApplicableReason?: string;
/**
* The hierarchical dotted name of the refactor action.
*/
kind?: string;
}
/**
* A set of edits to make in response to a refactor action, plus an optional

View File

@ -244,6 +244,7 @@ declare namespace FourSlashInterface {
refactorAvailable(name: string, actionName?: string): void;
refactorAvailableForTriggerReason(triggerReason: RefactorTriggerReason, name: string, action?: string): void;
refactorKindAvailable(refactorKind: string, expected: string[], preferences?: {}): void;
}
class verify extends verifyNegatable {
assertHasRanges(ranges: Range[]): void;

View File

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
//// const foo: /*a*/string/*b*/ = /*c*/1/*d*/;
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.extract",
[
"refactor.extract.type"
]);
goTo.select("c", "d");
verify.refactorKindAvailable("refactor.extract",
[
"refactor.extract.constant",
"refactor.extract.function"
]);

View File

@ -0,0 +1,11 @@
/// <reference path='fourslash.ts' />
//// class A {
//// /*a*/public a: string;/*b*/
//// }
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite.property",
[
"refactor.rewrite.property.generateAccessors"
]);

View File

@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />
//// /*a*/const moveMe = 1;/*b*/
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.move",
[
"refactor.move.newFile"
],
{
allowTextChangesInNewFiles: true
});

View File

@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />
//// /*a*/export function f() {}/*b*/
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite.export",
[
"refactor.rewrite.export.default"
]);

View File

@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />
//// const arrow = () /*a*/=>/*b*/ 1;
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite",
[
"refactor.rewrite.arrow.braces.add",
"refactor.rewrite.function.named",
"refactor.rewrite.function.anonymous",
"refactor.rewrite.function.returnType"
]);

View File

@ -0,0 +1,10 @@
/// <reference path='fourslash.ts' />
//// /*a*/declare function foo(): void;
//// declare function foo(a: string): void;/*b*/
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite",
[
"refactor.rewrite.function.overloadList"
]);

View File

@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />
//// /*a*/import * as m from "m";/*b*/
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite",
[
"refactor.rewrite.import.named"
]);

View File

@ -0,0 +1,10 @@
/// <reference path='fourslash.ts' />
//// /*a*/foo && foo.bar/*b*/
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite",
[
"refactor.rewrite.expression.optionalChain"
]);

View File

@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />
//// function(/*a*/a: number, b: number/*b*/): number {}
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite",
[
"refactor.rewrite.parameters.toDestructured"
]);

View File

@ -0,0 +1,9 @@
/// <reference path='fourslash.ts' />
//// const foo = /*a*/"a" + bar/*b*/;
goTo.select("a", "b");
verify.refactorKindAvailable("refactor.rewrite",
[
"refactor.rewrite.string"
]);