Codefix: add quick fix for missing 'new' operator#27019
Codefix: add quick fix for missing 'new' operator#27019DanielRosenwasser merged 9 commits intomicrosoft:masterfrom
Conversation
| "category": "Message", | ||
| "code": 95066 | ||
| }, | ||
| "Add missing 'new' operator to caller": { |
| "category": "Message", | ||
| "code": 95067 | ||
| }, | ||
| "Add missing 'new' operator to all callers": { |
| errorCodes, | ||
| getCodeActions(context) { | ||
| const { sourceFile, span } = context; | ||
| const identifierWithoutNew = getIdentifier(sourceFile, span.start); |
There was a problem hiding this comment.
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)())();There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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>();
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
|
@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; |
There was a problem hiding this comment.
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)); |
| return <Expression>token; | ||
| } | ||
|
|
||
| function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, missingNewExpression: Expression): void { |
There was a problem hiding this comment.
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.
|
function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number): void {
changes.insertNodeAt(sourceFile, pos, createToken(SyntaxKind.NewKeyword), { suffix: " " });
} |
|
@Andy-MS I'm still stuck with the 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 Could you please point me as to the best approach for this? |
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;
} |
|
@Andy-MS |
|
@DanielRosenwasser can you please take another look? |
|
@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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
|
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. |
|
@DanielRosenwasser - I decided to use the |
|
@DanielRosenwasser can you please take another look? |
|
@DanielRosenwasser Please review |
|
Looks good! If you can address the merge conflicts and update the tests if necessary, we can pull this in for 3.2. |
|
@DanielRosenwasser Ready. |
|
@DanielRosenwasser Is there anything missing for the PR to be merged? |
Fixes #26580