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

Codefix: add quick fix for missing 'new' operator #27019

Conversation

iliashkolyar
Copy link
Contributor

Fixes #26580

@msftclas
Copy link

msftclas commented Sep 11, 2018

CLA assistant check
All CLA requirements met.

"category": "Message",
"code": 95066
},
"Add missing 'new' operator to caller": {
Copy link
Member

Choose a reason for hiding this comment

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

caller -> call

"category": "Message",
"code": 95067
},
"Add missing 'new' operator to all callers": {
Copy link
Member

Choose a reason for hiding this comment

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

"to all calls"

errorCodes,
getCodeActions(context) {
const { sourceFile, span } = context;
const identifierWithoutNew = getIdentifier(sourceFile, span.start);
Copy link
Member

Choose a reason for hiding this comment

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

This is not guaranteed to be an identifier - you really have an arbitrary expression. For example:

class C {}
let x = (() => C)()();

Can you add a respective test that ensures it gets corrected to

let x = new ((() => C)())();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This indeed doesn't work at the moment.
I tried working with an expression here but it seems that the token isn't one:
Verbose Debug Information: Node OpenParenToken did not pass test 'isExpression'

When I tried working with its parent element, I received the following:
new (() => C)()() (Missing parentheses after the new addition).
Some help would be very appreciated.

}

function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifierWithoutNew: Identifier): void {
const newTypeNode = createNew(identifierWithoutNew, /*typeArguments*/ undefined, /*argumentsArray*/ undefined);
Copy link
Member

Choose a reason for hiding this comment

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

You should carry along the original type arguments and arguments. Can you add a test for these?

class C<T = number> {
    x?: T;
    constructor(x: T) { this.x = x; }
}

C(1, 2, 3);
C<string>("hello");
C<boolean>();

Copy link
Contributor Author

@iliashkolyar iliashkolyar Sep 11, 2018

Choose a reason for hiding this comment

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

Tests Added (without passing the arguments - see comment below).


function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, identifierWithoutNew: Identifier): void {
const newTypeNode = createNew(identifierWithoutNew, /*typeArguments*/ undefined, /*argumentsArray*/ undefined);
changes.replaceNode(sourceFile, identifierWithoutNew, newTypeNode);
Copy link
Member

Choose a reason for hiding this comment

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

I am so surprised that this is working. You should be replacing the CallExpression, not the identifier. The fact that this works is likely a bug.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it works because the identifier doesn't have the typeArguments and the arguments themselves here and therefore the new can replace it properly.

I guess it only works in the "simple" scenarios that I added, and indeed in the test without the identifier it doesn't.
Could you please advise on a better approach in which I use the CallExpression?
BTW, the CallExpression doesn't have typeArguments nor arguments, so how can I pass the original ones as you mentioned?

Thanks

@DanielRosenwasser
Copy link
Member

@andy-ms can you also take a look?

function getMissingNewExpression(sourceFile: SourceFile, pos: number): Expression {
const token = getTokenAtPosition(sourceFile, pos);
Debug.assert(isCallExpression(token.parent));
return <Expression>token;
Copy link
Member

Choose a reason for hiding this comment

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

The token isn't necessarily an expression (for example, opening parentheses). I think you should be returning the call expression itself.


function getMissingNewExpression(sourceFile: SourceFile, pos: number): Expression {
const token = getTokenAtPosition(sourceFile, pos);
Debug.assert(isCallExpression(token.parent));
Copy link
Member

Choose a reason for hiding this comment

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

Debug.assertNode

return <Expression>token;
}

function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, missingNewExpression: Expression): void {
Copy link
Member

Choose a reason for hiding this comment

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

If you return a call expression above, you now have a CallExpression to work with. From there, you can pass along the (optionaly present) typeArguments and arguments.

@ghost
Copy link

ghost commented Sep 12, 2018

textChanges doesn't actually need to generate a full source file to do its work -- it just makes local changes to the text. So instead of trying to convert a CallExpression to a NewExpression it's much simpler to just do this:

function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number): void {
    changes.insertNodeAt(sourceFile, pos, createToken(SyntaxKind.NewKeyword), { suffix: " " });
}

@iliashkolyar
Copy link
Contributor Author

@andy-ms
Thanks a lot, that is indeed a lot simpler.

I'm still stuck with the (() => C)()() expression which needs a new set of parentheses to wrap the execution of the anonymous function. (Returning new (() => C)()() instead of new ((() => C)())()).

I can detect that the token I'm currently at is not an expression, but I can't seem to find the logic that will give me the (() => C)() expression, which I assume I will then need to replace with a new new node.

Could you please point me as to the best approach for this?
Thanks

@ghost
Copy link

ghost commented Sep 12, 2018

function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, span: TextSpan): void {
    const call = cast(findAncestorMatchingSpan(sourceFile, span), isCallExpression);
    changes.insertNodeAt(sourceFile, span.start, createToken(SyntaxKind.NewKeyword), { suffix: " " });
    if (isCallExpression(call.expression)) {
        changes.insertNodeAt(sourceFile, call.expression.getStart(sourceFile), createToken(SyntaxKind.OpenParenToken));
        changes.insertNodeAt(sourceFile, call.expression.end, createToken(SyntaxKind.CloseParenToken));
    }
}

function findAncestorMatchingSpan(sourceFile: SourceFile, span: TextSpan): Node {
    let token = getTokenAtPosition(sourceFile, span.start);
    const end = textSpanEnd(span);
    while (token.end < end) {
        token = token.parent;
    }
    return token;
}

@iliashkolyar
Copy link
Contributor Author

@andy-ms
Thanks. Seems to work properly now.

@iliashkolyar
Copy link
Contributor Author

@DanielRosenwasser can you please take another look?

@iliashkolyar
Copy link
Contributor Author

@DanielRosenwasser - ping.

function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, span: TextSpan): void {
const call = cast(findAncestorMatchingSpan(sourceFile, span), isCallExpression);
changes.insertNodeAt(sourceFile, span.start, createToken(SyntaxKind.NewKeyword), { suffix: " " });
if (isCallExpression(call.expression)) {
Copy link
Member

Choose a reason for hiding this comment

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

I really feel this is masking a problem with the change tracker API. We shouldn't have to have special checks, the change tracker should do it automatically when replacing nodes, much like how our factories/printers automatically do this.

Copy link

Choose a reason for hiding this comment

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

The problem is that if we replace a node with another it triggers re-formatting of the whole body (and can mess with comments), which could be annoying to users who just expected the codefix to make one local change.

Copy link
Member

@DanielRosenwasser DanielRosenwasser Oct 1, 2018

Choose a reason for hiding this comment

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

Great example of where this won't work:

var foo;

foo()!()

Here, you'll end up with

new foo()!()

instead of

new (foo()!)()

We already have a parenthesizeForNew function in factory.ts: https://github.com/Microsoft/TypeScript/blob/7c875465b5565e9d92a046ec24939c115c8e34cb/src/compiler/factory.ts#L4171-L4184

So the change tracker should be doing something similar here.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see, that's a good point. My worry is that this won't be the last time we do something like this though. Maybe for now the right fix is to expose parenthesizeForNew in compiler/utilities.ts and use it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback!

I'm trying to implement your suggestion but I don't think I fully understand the internals needed for it to work.

According to @andy-ms, we should not use replaceNode and stick with the insertNodeAt so we won't generate the source file again.
Using the insertNodeAt doesn't really create a new node, but rather adds a new token to the start of the text span (without any change to the existing nodes or the text span).

On the other-hand, you are suggesting to use parenthesizeForNew, which receives an expression and returns it after wrapping it in parentheses if needed.

As I understand it, to work with the parenthesizeForNew API I'll need to create a NewExpression (similarly to what occurs in createNew), which brings us back to the replaceNode logic that we decided to avoid.

The only solution that aligns with both requirements is having a similar logic to the switch case in parenthesizeForNew in my code, that adds the parentheses (using changes.insertNodeAt) in these cases only (replacing the current isCallExpression check).
But again, i'm not sure that this is what you guys are targeting for.

What do you think?

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Oct 1, 2018

P.S. thanks so much for being patient. I want us to get the right fix in and I'm sure we can do that.

@iliashkolyar
Copy link
Contributor Author

@DanielRosenwasser - I decided to use the createNew API as it already contains the parenthesizeForNew logic.
I saw that most of the code fixes currently use replaceNode in their logic, so i'm not sure why this code fix is any different and should avoid using it (as @andy-ms indicated).

@iliashkolyar
Copy link
Contributor Author

@DanielRosenwasser can you please take another look?

@iliashkolyar iliashkolyar deleted the codefix_add_missing_new_operator branch October 28, 2018 21:15
@iliashkolyar iliashkolyar restored the codefix_add_missing_new_operator branch October 28, 2018 21:16
@iliashkolyar iliashkolyar reopened this Oct 28, 2018
@ghost
Copy link

ghost commented Nov 7, 2018

@DanielRosenwasser Please review

@DanielRosenwasser
Copy link
Member

Looks good! If you can address the merge conflicts and update the tests if necessary, we can pull this in for 3.2.

@iliashkolyar
Copy link
Contributor Author

@DanielRosenwasser Ready.

@iliashkolyar
Copy link
Contributor Author

@DanielRosenwasser Is there anything missing for the PR to be merged?

@DanielRosenwasser DanielRosenwasser merged commit fe26370 into microsoft:master Nov 15, 2018
@iliashkolyar iliashkolyar deleted the codefix_add_missing_new_operator branch November 15, 2018 17:42
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.

4 participants