Skip to content

Commit 8bbef81

Browse files
author
Jesse Trinity
authored
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
1 parent d1ac451 commit 8bbef81

37 files changed

+627
-325
lines changed

src/compiler/diagnosticMessages.json

+28
Original file line numberDiff line numberDiff line change
@@ -6143,6 +6143,34 @@
61436143
"category": "Message",
61446144
"code": 95148
61456145
},
6146+
"Return type must be inferred from a function": {
6147+
"category": "Message",
6148+
"code": 95149
6149+
},
6150+
"Could not determine function return type": {
6151+
"category": "Message",
6152+
"code": 95150
6153+
},
6154+
"Could not convert to arrow function": {
6155+
"category": "Message",
6156+
"code": 95151
6157+
},
6158+
"Could not convert to named function": {
6159+
"category": "Message",
6160+
"code": 95152
6161+
},
6162+
"Could not convert to anonymous function": {
6163+
"category": "Message",
6164+
"code": 95153
6165+
},
6166+
"Can only convert string concatenation": {
6167+
"category": "Message",
6168+
"code": 95154
6169+
},
6170+
"Selection is not a valid statement or statements": {
6171+
"category": "Message",
6172+
"code": 95155
6173+
},
61466174

61476175
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
61486176
"category": "Error",

src/harness/fourslashImpl.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -3420,6 +3420,12 @@ namespace FourSlash {
34203420
}
34213421
}
34223422

3423+
public verifyRefactorKindsAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) {
3424+
const refactors = this.getApplicableRefactorsAtSelection("invoked", kind, preferences);
3425+
const availableKinds = ts.flatMap(refactors, refactor => refactor.actions).map(action => action.kind);
3426+
assert.deepEqual(availableKinds.sort(), expected.sort(), `Expected kinds to be equal`);
3427+
}
3428+
34233429
public verifyRefactorsAvailable(names: readonly string[]): void {
34243430
assert.deepEqual(unique(this.getApplicableRefactorsAtSelection(), r => r.name), names);
34253431
}
@@ -3833,14 +3839,14 @@ namespace FourSlash {
38333839
test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved");
38343840
}
38353841

3836-
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit") {
3837-
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, ts.emptyOptions, triggerReason);
3842+
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) {
3843+
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind);
38383844
}
3839-
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit"): readonly ts.ApplicableRefactorInfo[] {
3840-
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason); // eslint-disable-line no-in-operator
3845+
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] {
3846+
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line no-in-operator
38413847
}
3842-
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason): readonly ts.ApplicableRefactorInfo[] {
3843-
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason) || ts.emptyArray;
3848+
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] {
3849+
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray;
38443850
}
38453851

38463852
public configurePlugin(pluginName: string, configuration: any): void {

src/harness/fourslashInterfaceImpl.ts

+4
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ namespace FourSlashInterface {
215215
this.state.verifyRefactorAvailable(this.negative, triggerReason, name, actionName);
216216
}
217217

218+
public refactorKindAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) {
219+
this.state.verifyRefactorKindsAvailable(kind, expected, preferences);
220+
}
221+
218222
public toggleLineComment(newFileContent: string) {
219223
this.state.toggleLineComment(newFileContent);
220224
}

src/server/protocol.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,8 @@ namespace ts.server.protocol {
566566
arguments: GetApplicableRefactorsRequestArgs;
567567
}
568568
export type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & {
569-
triggerReason?: RefactorTriggerReason
569+
triggerReason?: RefactorTriggerReason;
570+
kind?: string;
570571
};
571572

572573
export type RefactorTriggerReason = "implicit" | "invoked";
@@ -626,6 +627,11 @@ namespace ts.server.protocol {
626627
* the current context.
627628
*/
628629
notApplicableReason?: string;
630+
631+
/**
632+
* The hierarchical dotted name of the refactor action.
633+
*/
634+
kind?: string;
629635
}
630636

631637
export interface GetEditsForRefactorRequest extends Request {

src/server/session.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2129,7 +2129,7 @@ namespace ts.server {
21292129
private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] {
21302130
const { file, project } = this.getFileAndProject(args);
21312131
const scriptInfo = project.getScriptInfoForNormalizedPath(file)!;
2132-
return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason);
2132+
return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason, args.kind);
21332133
}
21342134

21352135
private getEditsForRefactor(args: protocol.GetEditsForRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo {

src/services/codefixes/generateAccessors.ts

+14-23
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ namespace ts.codefix {
44
type AcceptedNameType = Identifier | StringLiteral;
55
type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression;
66

7-
interface Info {
7+
type Info = AccessorInfo | refactor.RefactorErrorInfo;
8+
interface AccessorInfo {
89
readonly container: ContainerDeclaration;
910
readonly isStatic: boolean;
1011
readonly isReadonly: boolean;
@@ -16,20 +17,12 @@ namespace ts.codefix {
1617
readonly renameAccessor: boolean;
1718
}
1819

19-
type InfoOrError = {
20-
info: Info,
21-
error?: never
22-
} | {
23-
info?: never,
24-
error: string
25-
};
26-
2720
export function generateAccessorFromProperty(file: SourceFile, program: Program, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined {
2821
const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, program, start, end);
29-
if (!fieldInfo || !fieldInfo.info) return undefined;
22+
if (!fieldInfo || refactor.isRefactorErrorInfo(fieldInfo)) return undefined;
3023

3124
const changeTracker = textChanges.ChangeTracker.fromContext(context);
32-
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo.info;
25+
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo;
3326

3427
suppressLeadingAndTrailingTrivia(fieldName);
3528
suppressLeadingAndTrailingTrivia(accessorName);
@@ -112,7 +105,7 @@ namespace ts.codefix {
112105
return modifierFlags;
113106
}
114107

115-
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): InfoOrError | undefined {
108+
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): Info | undefined {
116109
const node = getTokenAtPosition(file, start);
117110
const cursorRequest = start === end && considerEmptySpans;
118111
const declaration = findAncestor(node.parent, isAcceptedDeclaration);
@@ -142,17 +135,15 @@ namespace ts.codefix {
142135
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
143136
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
144137
return {
145-
info: {
146-
isStatic: hasStaticModifier(declaration),
147-
isReadonly: hasEffectiveReadonlyModifier(declaration),
148-
type: getDeclarationType(declaration, program),
149-
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
150-
originalName: (<AcceptedNameType>declaration.name).text,
151-
declaration,
152-
fieldName,
153-
accessorName,
154-
renameAccessor: startWithUnderscore
155-
}
138+
isStatic: hasStaticModifier(declaration),
139+
isReadonly: hasEffectiveReadonlyModifier(declaration),
140+
type: getDeclarationType(declaration, program),
141+
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
142+
originalName: (<AcceptedNameType>declaration.name).text,
143+
declaration,
144+
fieldName,
145+
accessorName,
146+
renameAccessor: startWithUnderscore
156147
};
157148
}
158149

src/services/refactorProvider.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ namespace ts.refactor {
1111

1212
export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] {
1313
return arrayFrom(flatMapIterator(refactors.values(), refactor =>
14-
context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context)));
14+
context.cancellationToken && context.cancellationToken.isCancellationRequested() ||
15+
!refactor.kinds?.some(kind => refactorKindBeginsWith(kind, context.kind)) ? undefined :
16+
refactor.getAvailableActions(context)));
1517
}
1618

1719
export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined {

src/services/refactors/addOrRemoveBracesToArrowFunction.ts

+31-54
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,40 @@
22
namespace ts.refactor.addOrRemoveBracesToArrowFunction {
33
const refactorName = "Add or remove braces in an arrow function";
44
const refactorDescription = Diagnostics.Add_or_remove_braces_in_an_arrow_function.message;
5-
const addBracesActionName = "Add braces to arrow function";
6-
const removeBracesActionName = "Remove braces from arrow function";
7-
const addBracesActionDescription = Diagnostics.Add_braces_to_arrow_function.message;
8-
const removeBracesActionDescription = Diagnostics.Remove_braces_from_arrow_function.message;
9-
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
105

11-
interface Info {
6+
const addBracesAction = {
7+
name: "Add braces to arrow function",
8+
description: Diagnostics.Add_braces_to_arrow_function.message,
9+
kind: "refactor.rewrite.arrow.braces.add",
10+
};
11+
const removeBracesAction = {
12+
name: "Remove braces from arrow function",
13+
description: Diagnostics.Remove_braces_from_arrow_function.message,
14+
kind: "refactor.rewrite.arrow.braces.remove"
15+
};
16+
registerRefactor(refactorName, {
17+
kinds: [removeBracesAction.kind],
18+
getEditsForAction,
19+
getAvailableActions });
20+
21+
interface FunctionBracesInfo {
1222
func: ArrowFunction;
1323
expression: Expression | undefined;
1424
returnStatement?: ReturnStatement;
1525
addBraces: boolean;
1626
}
1727

18-
type InfoOrError = {
19-
info: Info,
20-
error?: never
21-
} | {
22-
info?: never,
23-
error: string
24-
};
25-
2628
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
2729
const { file, startPosition, triggerReason } = context;
2830
const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked");
2931
if (!info) return emptyArray;
3032

31-
if (info.error === undefined) {
33+
if (!isRefactorErrorInfo(info)) {
3234
return [{
3335
name: refactorName,
3436
description: refactorDescription,
3537
actions: [
36-
info.info.addBraces ?
37-
{
38-
name: addBracesActionName,
39-
description: addBracesActionDescription
40-
} : {
41-
name: removeBracesActionName,
42-
description: removeBracesActionDescription
43-
}
38+
info.addBraces ? addBracesAction : removeBracesAction
4439
]
4540
}];
4641
}
@@ -49,15 +44,10 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
4944
return [{
5045
name: refactorName,
5146
description: refactorDescription,
52-
actions: [{
53-
name: addBracesActionName,
54-
description: addBracesActionDescription,
55-
notApplicableReason: info.error
56-
}, {
57-
name: removeBracesActionName,
58-
description: removeBracesActionDescription,
59-
notApplicableReason: info.error
60-
}]
47+
actions: [
48+
{ ...addBracesAction, notApplicableReason: info.error },
49+
{ ...removeBracesAction, notApplicableReason: info.error },
50+
]
6151
}];
6252
}
6353

@@ -67,19 +57,19 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
6757
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
6858
const { file, startPosition } = context;
6959
const info = getConvertibleArrowFunctionAtPosition(file, startPosition);
70-
if (!info || !info.info) return undefined;
60+
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
7161

72-
const { expression, returnStatement, func } = info.info;
62+
const { expression, returnStatement, func } = info;
7363

7464
let body: ConciseBody;
7565

76-
if (actionName === addBracesActionName) {
66+
if (actionName === addBracesAction.name) {
7767
const returnStatement = factory.createReturnStatement(expression);
7868
body = factory.createBlock([returnStatement], /* multiLine */ true);
7969
suppressLeadingAndTrailingTrivia(body);
8070
copyLeadingComments(expression!, returnStatement, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true);
8171
}
82-
else if (actionName === removeBracesActionName && returnStatement) {
72+
else if (actionName === removeBracesAction.name && returnStatement) {
8373
const actualExpression = expression || factory.createVoidZero();
8474
body = needsParentheses(actualExpression) ? factory.createParenthesizedExpression(actualExpression) : actualExpression;
8575
suppressLeadingAndTrailingTrivia(body);
@@ -98,7 +88,7 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
9888
return { renameFilename: undefined, renameLocation: undefined, edits };
9989
}
10090

101-
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): InfoOrError | undefined {
91+
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true, kind?: string): FunctionBracesInfo | RefactorErrorInfo | undefined {
10292
const node = getTokenAtPosition(file, startPosition);
10393
const func = getContainingFunction(node);
10494

@@ -118,26 +108,13 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
118108
return undefined;
119109
}
120110

121-
if (isExpression(func.body)) {
122-
return {
123-
info: {
124-
func,
125-
addBraces: true,
126-
expression: func.body
127-
}
128-
};
111+
if (refactorKindBeginsWith(addBracesAction.kind, kind) && isExpression(func.body)) {
112+
return { func, addBraces: true, expression: func.body };
129113
}
130-
else if (func.body.statements.length === 1) {
114+
else if (refactorKindBeginsWith(removeBracesAction.kind, kind) && isBlock(func.body) && func.body.statements.length === 1) {
131115
const firstStatement = first(func.body.statements);
132116
if (isReturnStatement(firstStatement)) {
133-
return {
134-
info: {
135-
func,
136-
addBraces: false,
137-
expression: firstStatement.expression,
138-
returnStatement: firstStatement
139-
}
140-
};
117+
return { func, addBraces: false, expression: firstStatement.expression, returnStatement: firstStatement };
141118
}
142119
}
143120
return undefined;

0 commit comments

Comments
 (0)