-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support for add or remove braces to arrow function #23423
Conversation
src/compiler/diagnosticMessages.json
Outdated
@@ -4165,5 +4165,17 @@ | |||
"Generate 'get' and 'set' accessors": { | |||
"category": "Message", | |||
"code": 95046 | |||
}, | |||
"Convert arrow function": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider extracting first(container.body.statements)
out and reusing it, will save you the cast later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
few nits.
@andy-ms can you please review |
@@ -0,0 +1,90 @@ | |||
/* @internal */ | |||
namespace ts.refactor.convertArrowFunction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would not that make it a lint feature at this point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
never mind.. all suggestions are lint features anyways..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the expression is {}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any value in preserving any comments that might have been attached to the return statement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 + 1 A: 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/compiler/scanner.ts
Outdated
@@ -727,7 +727,7 @@ namespace ts { | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What changed here? Is this a merge artifact?
src/services/utilities.ts
Outdated
@@ -1649,4 +1649,20 @@ namespace ts { | |||
Debug.assert(lastPos >= 0); | |||
return lastPos; | |||
} | |||
|
|||
export function copyComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile, explicitKind?: CommentKind, explicitHtnl?: boolean) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is "htnl"?
@@ -0,0 +1,13 @@ | |||
/// <reference path='fourslash.ts' /> | |||
|
|||
//// const foo = /*a*/a/*b*/ => (1, 2, 3); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it matter if the selection is empty?
|
||
interface Info { | ||
func: ArrowFunction; | ||
expression: Expression | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it matter whether this agrees with info.addBraces
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happened with this?
return isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.CommaToken || isObjectLiteralExpression(expression); | ||
} | ||
|
||
function updateBody(changeTracker: textChanges.ChangeTracker, file: SourceFile, container: ArrowFunction, body: ConciseBody) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to inline this?
I don't object strongly enough to block merging. |
@amcasey one more look? |
🆙 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've re-raised the only comment that I thought might be important. The others were about consistency.
Thank you! |
Fixes #23402