-
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
Codefix: add quick fix for missing 'new' operator #27019
Codefix: add quick fix for missing 'new' operator #27019
Conversation
src/compiler/diagnosticMessages.json
Outdated
"category": "Message", | ||
"code": 95066 | ||
}, | ||
"Add missing 'new' operator to caller": { |
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.
caller -> call
src/compiler/diagnosticMessages.json
Outdated
"category": "Message", | ||
"code": 95067 | ||
}, | ||
"Add missing 'new' operator to all callers": { |
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.
"to all calls"
errorCodes, | ||
getCodeActions(context) { | ||
const { sourceFile, span } = context; | ||
const identifierWithoutNew = getIdentifier(sourceFile, span.start); |
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 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.
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); |
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.
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.
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); |
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 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.
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
@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.
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)); |
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.
Debug.assertNode
return <Expression>token; | ||
} | ||
|
||
function addMissingNewOperator(changes: textChanges.ChangeTracker, sourceFile: SourceFile, missingNewExpression: Expression): void { |
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 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.
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.
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.
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.
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.
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.
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.
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?
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