add support for add or remove braces to arrow function#23423
add support for add or remove braces to arrow function#23423mhegazy merged 12 commits intomicrosoft:masterfrom
Conversation
src/compiler/diagnosticMessages.json
Outdated
| "category": "Message", | ||
| "code": 95046 | ||
| }, | ||
| "Convert arrow function": { |
There was a problem hiding this comment.
nit. Though no one sees this message, i think it should this be Add or remove braces in an arrow function
| const info = getConvertibleArrowFunctionAtPosition(file, startPosition); | ||
| if (!info) return undefined; | ||
|
|
||
| const actions: RefactorActionInfo[] = [ |
There was a problem hiding this comment.
consider inlining this into the return statement
| expression: container.body | ||
| }; | ||
| } | ||
| else if (container.body.statements.length === 1 && isReturnStatement(first(container.body.statements))) { |
There was a problem hiding this comment.
consider extracting first(container.body.statements) out and reusing it, will save you the cast later.
|
@Andy-MS can you please review |
| @@ -0,0 +1,90 @@ | |||
| /* @internal */ | |||
| namespace ts.refactor.convertArrowFunction { | |||
There was a problem hiding this comment.
I would have thought this would be a suggestion diagnostic/code-fix so that it could be discovered via the lightbulb.
There was a problem hiding this comment.
would not that make it a lint feature at this point?
There was a problem hiding this comment.
never mind.. all suggestions are lint features anyways..
There was a problem hiding this comment.
why this could be a diagnostic/code-fix...
I don't want to see this lightbulb in the code except when I want to convert
There was a problem hiding this comment.
and it also could convert back 😂
| const info = getConvertibleArrowFunctionAtPosition(file, startPosition); | ||
| if (!info) return undefined; | ||
|
|
||
| const { addBraces, expression, container } = info; |
There was a problem hiding this comment.
Is it possible addBraces could disagree with _actionName. If that does happen, should we bail?
| return { | ||
| container, | ||
| addBraces: false, | ||
| expression: (<ReturnStatement>first(container.body.statements)).expression |
There was a problem hiding this comment.
What happens if the expression is {}?
There was a problem hiding this comment.
Or a comma expression. Feels like there might be some hidden complications here.
| } | ||
|
|
||
| function updateBraces(changeTracker: textChanges.ChangeTracker, file: SourceFile, container: ArrowFunction, expression: Expression, addBraces: boolean) { | ||
| const body = addBraces ? createBlock([createReturn(expression)]) : expression; |
There was a problem hiding this comment.
Is there any value in preserving any comments that might have been attached to the return statement?
amcasey
left a comment
There was a problem hiding this comment.
Mostly, I'm concerned about the fact that this is a refactoring. Secondarily, I think there are cases where we'll synthesize syntactically invalid code (good to document with tests, but not the end of the world).
|
need some help: |
| }]; | ||
| } | ||
|
|
||
| function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined { |
There was a problem hiding this comment.
Don't begin a name with _ unless it's unused.
| } | ||
|
|
||
| function removeBraces(changeTracker: textChanges.ChangeTracker, file: SourceFile, container: ArrowFunction, expression: Expression) { | ||
| if (!isLiteralExpression(expression) && !isIdentifier(expression) && !isParenthesizedExpression(expression) && expression.kind !== SyntaxKind.NullKeyword) { |
There was a problem hiding this comment.
Should have a needsParentheses utility function that uses a switch statement. There are probably some unhandled cases here -- a property access shouldn't need parentheses, nor should a function call. Nor should true or false or undefined.
(By the way, I don't think it would be worth it to add a test for every single one of these cases. I notice there are a lot of tests related to parenthesizing already -- maybe they should be combined into one file.)
| if (!info) return undefined; | ||
|
|
||
| const { expression, container } = info; | ||
| const changeTracker = textChanges.ChangeTracker.fromContext(context); |
There was a problem hiding this comment.
Both branches end up calling the same method on the change tracker. So this can be written as:
function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined {
const { file, startPosition } = context;
const info = getConvertibleArrowFunctionAtPosition(file, startPosition);
if (!info) return undefined;
const { expression, container } = info;
const newBody = _actionName === addBracesActionName ? createBlock([createReturn(expression)])
: _actionName === removeBracesActionName ? needsParentheses(expression) ? createParen(expression) : expression
: Debug.fail("invalid action");
const edits = textChanges.ChangeTracker.with(context, t => t.replaceNode(file, container, updateBody(container, newBody)));
return { renameFilename: undefined, renameLocation: undefined, edits };
}
function updateBody(node: ArrowFunction, body: ConciseBody): ArrowFunction {
return updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, body);
}
function needsParentheses(expression: Expression): boolean {
if (isLiteralExpression(expression)) {
return false;
}
switch (expression.kind) {
case SyntaxKind.Identifier:
case SyntaxKind.NullKeyword:
case SyntaxKind.TrueKeyword:
case SyntaxKind.FalseKeyword:
case SyntaxKind.UndefinedKeyword:
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
case SyntaxKind.CallExpression:
case SyntaxKind.NewExpression:
return false;
default:
return true;
}
}There was a problem hiding this comment.
Maybe needsParentheses should be written backwards -- what are the expressions that do need parentheses? BinaryExpression where the operator is ,, ObjectLiteralExpression, any others?
| registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); | ||
|
|
||
| interface Info { | ||
| container: ArrowFunction; |
There was a problem hiding this comment.
container seems like the wrong name. It comes from getContainingFunction, but that's irrelevant to the rest of the code ... might just call it fun.
|
If we do this, we'll presumably want to implement add/remove braces for A problem with this being implemented as a code fix is performance -- we would have to add a suggestion diagnostic to every single arrow function. I looked at how IntelliJ does this and they only show the suggestion when you're at a particular position, which is more like a refactor in our design. (Contrast with the underscore on I think the criteria we currently use for this is overly broad though -- |
|
@Kingwl There is a |
|
something need to consider: const a = () => /* test */ a + 1A: const a = () => { /* test */ return a + 1; }B: const a = () => {
// test
return a + 1;
}C: const a = () => {
/* test */
return a + 1;
}the question is:
|
|
My opinion would be that adding braces implies an intent to make it multiline, so option C. But it's not a big deal if that would be hard to implement. |
|
@Andy-MS need some help!😢 |
|
It looks like in services, we're using a function |
| if (!info) return undefined; | ||
|
|
||
| const { expression, func } = info; | ||
| const changeTracker = textChanges.ChangeTracker.fromContext(context); |
There was a problem hiding this comment.
This could be moved closer to its use:
const edits = textChanges.ChangeTracker.with(context, t => updateBody(t, file, func, body));
return { renameFilename: undefined, renameLocation: undefined, edits };There was a problem hiding this comment.
Also, we should consider replacing only the body node instead of the entire arrow function? Though that doesn't fix the duplicated comments issue.
| } | ||
|
|
||
| function needsParentheses(expression: Expression) { | ||
| if (isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.CommaToken) return true; |
There was a problem hiding this comment.
if (a) return true; if (b) return true; return false; is just a || b.
|
update body only and the duplicated comments increased again😂 |
|
ping @weswigham |
|
Indeed it is. Setting |
|
any progress here? |
|
@Kingwl I know it's not your fault that this has been sitting for so long, but could you possibly push an update that resolves the merge conflicts and build issues (which might just be expirations?) before we take another look? Thanks! |
|
@Kingwl a few tests need to be updated. |
amcasey
left a comment
There was a problem hiding this comment.
I still think it would be more natural, both in code and for users, to express this as a code fix. For example, that would allow it to be suppressed or applied automatically at multiple locations. It would also make it discoverable and eliminate possible confusion about what range needs to be selected to have the change offered.
| export function forEachLeadingCommentRange<U>(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; | ||
| export function forEachLeadingCommentRange<T, U>(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; | ||
| export function forEachLeadingCommentRange<T, U>(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { | ||
| return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ false, cb, state); |
There was a problem hiding this comment.
What changed here? Is this a merge artifact?
src/services/utilities.ts
Outdated
| return lastPos; | ||
| } | ||
|
|
||
| export function copyComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile, explicitKind?: CommentKind, explicitHtnl?: boolean) { |
| @@ -0,0 +1,13 @@ | |||
| /// <reference path='fourslash.ts' /> | |||
|
|
|||
| //// const foo = /*a*/a/*b*/ => (1, 2, 3); | |||
There was a problem hiding this comment.
All of these tests seem to use the same selection range. What if the parameter has more than one character and only part of it is selected? What if a different part of the arrow function is selected?
There was a problem hiding this comment.
Does it matter if the selection is empty?
|
|
||
| interface Info { | ||
| func: ArrowFunction; | ||
| expression: Expression | undefined; |
There was a problem hiding this comment.
I'm not sure I understand why expression is required, but can be undefined, whereas returnStatement is optional. Should they follow the same pattern?
| const { expression, returnStatement, func } = info; | ||
|
|
||
| let body: ConciseBody; | ||
| if (actionName === addBracesActionName) { |
There was a problem hiding this comment.
Does it matter whether this agrees with info.addBraces?
| return isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.CommaToken || isObjectLiteralExpression(expression); | ||
| } | ||
|
|
||
| function updateBody(changeTracker: textChanges.ChangeTracker, file: SourceFile, container: ArrowFunction, body: ConciseBody) { |
|
I don't object strongly enough to block merging. |
|
@amcasey one more look? |
|
🆙 |
amcasey
left a comment
There was a problem hiding this comment.
I've re-raised the only comment that I thought might be important. The others were about consistency.
|
Thank you! |

Fixes #23402