Skip to content
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

Merged
merged 12 commits into from
Jun 11, 2018

Conversation

Kingwl
Copy link
Contributor

@Kingwl Kingwl commented Apr 16, 2018

Fixes #23402

@@ -4165,5 +4165,17 @@
"Generate 'get' and 'set' accessors": {
"category": "Message",
"code": 95046
},
"Convert arrow function": {
Copy link
Contributor

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[] = [
Copy link
Contributor

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))) {
Copy link
Contributor

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.

Copy link
Contributor

@mhegazy mhegazy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few nits.

@mhegazy mhegazy requested review from amcasey and a user April 17, 2018 00:18
@mhegazy
Copy link
Contributor

mhegazy commented Apr 17, 2018

@andy-ms can you please review

@@ -0,0 +1,90 @@
/* @internal */
namespace ts.refactor.convertArrowFunction {
Copy link
Member

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.

Copy link
Contributor

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?

Copy link
Contributor

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..

Copy link
Contributor Author

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

Copy link
Contributor Author

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;
Copy link
Member

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
Copy link
Member

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 {}?

Copy link
Member

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;
Copy link
Member

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?

Copy link
Member

@amcasey amcasey left a 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).

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 17, 2018

need some help:
how could i move the comment which attach to the return statement...

}];
}

function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined {
Copy link

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) {
Copy link

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);
Copy link

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

Copy link

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;
Copy link

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.

@ghost
Copy link

ghost commented Apr 17, 2018

If we do this, we'll presumably want to implement add/remove braces for if, for, and while.

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.

j

(Contrast with the underscore on true which is always visible, since that is considered a problem.)

I think the criteria we currently use for this is overly broad though -- getContainingFunction will return the arrow function if we're anywhere inside its body. I would recommend only providing the refactor if we're at one of its tokens -- the parentheses (or first parameter if no parentheses), the arrow, or the braces (if they already exist).

@ghost
Copy link

ghost commented Apr 17, 2018

@Kingwl There is a copyComments function in convertFunctionToEs6Class which could be moved to utilities.

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 18, 2018

something need to consider:
what should follow code been convert to?

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:

  1. should the code add braces multi line
  2. what kind of comment should be generate after convert

@ghost
Copy link

ghost commented Apr 24, 2018

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.

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 25, 2018

@andy-ms need some help!😢
how could i remove expression comment with suppressLeadingAndTrailingTrivia
i tried returnStatement body expression and they have no effect

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 25, 2018

#23611

@ghost
Copy link

ghost commented Apr 25, 2018

It looks like in services, we're using a function forEachLeadingCommentRange, but the emitter is using a completely different function forEachLeadingCommentToEmit, so it's possible that those aren't aligned with each other. CC @rbuckton

if (!info) return undefined;

const { expression, func } = info;
const changeTracker = textChanges.ChangeTracker.fromContext(context);
Copy link

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

Copy link

@ghost ghost Apr 25, 2018

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;
Copy link

@ghost ghost Apr 25, 2018

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.

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 26, 2018

update body only and the duplicated comments increased again😂

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 27, 2018

ping @weswigham
does those two duplicated comments is the TrailingComments for the => token? 😢

@weswigham
Copy link
Member

Indeed it is. Setting EmitFlags.NoComments on the arrow function should, I believe, also disable the comments in the inner tokens? We had a similar issue before with another refactoring @amcasey maybe remembers the specifics? All the modifications to comment traversal you've got here just seem a tad excessive.

@Kingwl
Copy link
Contributor Author

Kingwl commented May 22, 2018

any progress here?
I manually performed dozens of conversions and forget the return statement 😅

@amcasey
Copy link
Member

amcasey commented May 22, 2018

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

@mhegazy
Copy link
Contributor

mhegazy commented May 23, 2018

@Kingwl a few tests need to be updated.

Copy link
Member

@amcasey amcasey left a 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.

@@ -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);
Copy link
Member

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?

@@ -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) {
Copy link
Member

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);
Copy link
Member

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?

Copy link
Member

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;
Copy link
Member

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) {
Copy link
Member

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?

Copy link
Member

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) {
Copy link
Member

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?

@amcasey
Copy link
Member

amcasey commented May 24, 2018

I don't object strongly enough to block merging.

@mhegazy
Copy link
Contributor

mhegazy commented May 24, 2018

I will take on the codefix vs refactoring discussion with the VSCode team. there are some usability issues we need to figure out.. once i have that concluded i can convert the refactor to a code fix..

@Kingwl please address the remaining comments from @amcasey and we should be good to go.

@mhegazy
Copy link
Contributor

mhegazy commented May 25, 2018

@amcasey one more look?

@mhegazy
Copy link
Contributor

mhegazy commented May 25, 2018

@Kingwl can you look at the remaining comments from @amcasey

@Kingwl
Copy link
Contributor Author

Kingwl commented Jun 8, 2018

🆙

Copy link
Member

@amcasey amcasey left a 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.

@mhegazy mhegazy merged commit e07e2e0 into microsoft:master Jun 11, 2018
@Kingwl Kingwl deleted the add-braces branch June 11, 2018 23:10
@DanielRosenwasser
Copy link
Member

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants