Skip to content

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

Merged
DanielRosenwasser merged 9 commits intomicrosoft:masterfrom
iliashkolyar:codefix_add_missing_new_operator
Nov 15, 2018
Merged

Codefix: add quick fix for missing 'new' operator#27019
DanielRosenwasser merged 9 commits intomicrosoft:masterfrom
iliashkolyar:codefix_add_missing_new_operator

Conversation

@iliashkolyar
Copy link
Copy Markdown
Contributor

Fixes #26580

@msftclas
Copy link
Copy Markdown

msftclas commented Sep 11, 2018

CLA assistant check
All CLA requirements met.

Comment thread src/compiler/diagnosticMessages.json Outdated
"category": "Message",
"code": 95066
},
"Add missing 'new' operator to caller": {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

caller -> call

Comment thread src/compiler/diagnosticMessages.json Outdated
"category": "Message",
"code": 95067
},
"Add missing 'new' operator to all callers": {
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown

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
Copy Markdown
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
Copy Markdown

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
Copy Markdown
Contributor Author

@Andy-MS
Thanks. Seems to work properly now.

@iliashkolyar
Copy link
Copy Markdown
Contributor Author

@DanielRosenwasser can you please take another look?

@iliashkolyar
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown

ghost commented Nov 7, 2018

@DanielRosenwasser Please review

@DanielRosenwasser
Copy link
Copy Markdown
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
Copy Markdown
Contributor Author

@DanielRosenwasser Ready.

@iliashkolyar
Copy link
Copy Markdown
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
@microsoft microsoft locked as resolved and limited conversation to collaborators Oct 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants