Add codefix for 'Cannot find name' diagnostic#28290
Conversation
ghost
left a comment
There was a problem hiding this comment.
Thanks for remembering to test on destructuring. Should also include an object destructuring test.
| @@ -0,0 +1,12 @@ | |||
| /// <reference path='fourslash.ts' /> | |||
|
|
|||
| ////[|for (x of []) {}|] | |||
There was a problem hiding this comment.
I think these [| |] ranges are unnecessary when you use newFileContent.
| @@ -0,0 +1,8 @@ | |||
| /// <reference path='fourslash.ts' /> | |||
|
|
|||
| ////[|for (x of []) {}|] | |||
There was a problem hiding this comment.
Just for completeness it would be nice to use for-in in one of these test cases.
|
|
||
| verify.codeFix({ | ||
| description: "Add const modifier to unresolved variable", | ||
| newRangeContent: "for (const x of []) {}" |
There was a problem hiding this comment.
It's silly to use newRangeContent when the range is the entire file, just use newFileContent.
| "category": "Message", | ||
| "code": 95068 | ||
| }, | ||
| "Add const modifier to unresolved variable": { |
There was a problem hiding this comment.
I would change this to Add 'const' to unresolved variable, since we don't call this a modifier in the compiler.
| const token = getTokenAtPosition(sourceFile, pos); | ||
| // fails on 'for ([x, y] of [[1,2]]) {}' when called for y | ||
| // since findPrecedingMatchingToken does not return the open paren here after iterating over another identifier (x) | ||
| const openParenToken = <Node>findPrecedingMatchingToken(token, SyntaxKind.OpenParenToken, sourceFile); |
There was a problem hiding this comment.
Since this error message appears in places other than for loops you need to be careful about checking the context, and don't return a codefix if this isn't a for loop.
function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number) {
const forInitializer = findAncestor(getTokenAtPosition(sourceFile, pos), node =>
isForInOrOfStatement(node.parent) ? node.parent.initializer === node
: isPossiblyPartOfDestructuring(node) ? false : "quit");
if (forInitializer) {
changeTracker.insertNodeBefore(sourceFile, forInitializer, createToken(SyntaxKind.ConstKeyword));
}
}
function isPossiblyPartOfDestructuring(node: Node): boolean {
switch (node.kind) {
case SyntaxKind.Identifier:
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.PropertyAssignment:
case SyntaxKind.ShorthandPropertyAssignment:
return true;
default:
return false;
}
}After calling makeChange you'll need to test changes.length when determining whether to return a code fix.
You'll also have to modify insertNodeBefore to handle this new situation.
|
I'm actually wondering if we should have a code fix at a naked |
|
I added tests for |
| //// function ...q) {}} f(10); | ||
|
|
||
| verify.not.codeFixAvailable(); | ||
| verify.not.codeFixAvailable([ |
There was a problem hiding this comment.
I do believe this test to check for the the specific codefix not being generated. However the test file contains a syntax error, which leads to a codefix being generated by this change. In order for this test to successfully run I assume that either the test fixture must be changed or the test expectation be widened to check for only the specific codefix not being generated.
There was a problem hiding this comment.
It's better to avoid verify.not.codeFixAvailable, since such a test could easily pass if the code fix does exist but has a slightly different description. Better to use verify.codeFixAvailable and list exactly the codefixes that exist.
There was a problem hiding this comment.
Since the new codefix is now only being generated inside for ... in/of loops I was able to revert the changes to this file completely.
| } | ||
|
|
||
| function alreadyContainsConstCodeFixForInitializer(changeTracker: textChanges.ChangeTracker, forInitializer: Node, sourceFile: SourceFile): boolean { | ||
| return changeTracker.getChanges().some(change => { |
There was a problem hiding this comment.
This will be brittle if the fix changes slightly. It would be better to keep a NodeSet of all the initializers and only do the change if tryAdd(forInitializer) succeeds.
There was a problem hiding this comment.
That sounds like a good idea. I am having trouble finding the correct scope to store this NodeSet in. Could you point me in the right direction, please?
There was a problem hiding this comment.
Create it at the start of getAllCodeActions before the codeFixAll loop. (Since this is irrelevant if we're only doing a single code fix.) See disableJsDiagnostics for an example (though that uses a set of numbers instead of a set of nodes).
There was a problem hiding this comment.
Thanks for the hint, I believe this allowed for a positive change regarding both stability and readability.
|
|
||
| ////export {}; | ||
| ////const { x, y } = o; | ||
| ////const { x, y } = [{}]; |
There was a problem hiding this comment.
Why this change? This makes the code wrong since it destructures an array to an object..
There was a problem hiding this comment.
Fixed, accidentally provided an array rather than an object.
|
CC @DanielRosenwasser to review, and to consider whether we should have this change on the statement |
9fd9566 to
1fd2371
Compare
|
Ping @DanielRosenwasser @Andy-MS |
…stInForLoop codefix
If @DanielRosenwasser has no opinion, this sounds reasonable to me. @rFlorian, sorry that this review went cold. Are you interested in continuing this? It looks like there are a few seemingly unnecessary/unrelated changes to some other fourslash tests that we’ll want to revert. I’d like to add the codefix for the standalone variable declaration statement, and I can help you look into the If you want to pick it back up, I’ll help get it to the finish line. Otherwise, I can just pick up where you left off (probably later this month). |
|
@andrewbranch The delay is no big deal, thanks for having a look at the PR now! If the changes required for the additional codefix are something I can handle I'll be happy to add them. I reverted the changes to the fourslash tests and could run them successfully locally afterwards. If I recall correctly they failed at an intermediate stage of this branch and I hadn't reverted them since. The What would be the best course of action to add the codefix for the How extensive should that additional codefix be? Some cases that I can come up with are:
|
|
TL;DR:
The examples after the first all depend on every would-be variable or binding element being an unresolved name, e.g.: let x;
[x, y] = [0, 1];wouldn’t be a candidate because Of those, all but the object destructuring produce a fairly tidy parse tree, so I think it shouldn’t be too messy to offer the quick fix there. { x } = { x: 2 }Here, the So, I think it would be lovely if |
…nd started implementing them
|
@andrewbranch I added fourslash test cases for the standalone and array-initialization cases. After that I also started implementing these changes and got the standalone case working as far as I can tell. My biggest problem to which I couldn't find a solution was a way to find all declared identifiers at the point where the error occurs. As you already pointed out that information is required to prevent false positives for changes where multiple identifiers are involved. I skipped the comma separated case for now because it requires the same information of available identifiers like arrays and also produces a slightly more complex parse tree. However I think it can be done by recursing through the A hint on how to retrieve available identifiers would be much appreciated. |
There is a checker function that does something like this ( I just played around with it and it seems like this works: if (isArrayLiteralExpression(parent)) {
// You’ll need to pass `context.program` into `makeChange`
const checker = program.getTypeChecker();
if (!every(parent.elements, element => arrayElementCouldBeVariableDeclaration(element, checker))) {
return;
}
return applyChange(changeTracker, parent, sourceFile, fixedNodes);
}
function arrayElementCouldBeVariableDeclaration(expression: Expression, checker: TypeChecker) {
const identifier =
isIdentifier(expression) ? expression :
isAssignmentExpression(expression, /*excludeCompoundAssignment*/ true) && isIdentifier(expression.left) ? expression.left :
undefined;
return !!identifier && !checker.getSymbolAtLocation(identifier);
}Note also that we’re being more specific about what syntax we can try to fix by making sure each array element could become a binding pattern—e.g. Other random note: I tried running the codefix inline in |
|
@andrewbranch Thanks for the quick reply and very useful hints. I added additional tests that check for existing indentation being untouched by this change. The root issue of indentation getting messed up with the old version lied in const fullStart = node.getFullStart();
const start = node.getStart(sourceFile);
if (fullStart === start) {
return start;
}
const fullStartLine = getLineStartPositionForPosition(fullStart, sourceFile);
const startLine = getLineStartPositionForPosition(start, sourceFile);
if (startLine === fullStartLine) {
return leadingTriviaOption === LeadingTriviaOption.IncludeAll ? fullStart : start;
}That function was previously called from I will have a look at the comma separated initializer case tomorrow as it now looks a lot more achievable to support as well. |
|
@andrewbranch It looks like I got the |
|
Looks like there are lint errors 🙈 Thanks for all your work on this! I look forward to reviewing it in full tomorrow ✨ |
|
Whoops forgot to remove a bit of whitespace 🙈 |
andrewbranch
left a comment
There was a problem hiding this comment.
So awesome that you jumped right back on this after 8 months of silence—thanks for your hard work and your patience 🌟
|
@andrewbranch It was a pleasure to push this PR over the finish line with your help 😄 |

Fixes #28275
All single-identifier cases of this issue are covered, however I am not sure how to correctly handle cases like
for ([x, y] of [[1,2]]) {}.Some help in how to approach finding and manipulating the correct nodes would be highly appreciated. My current implementation fails on the above case because
findPrecedingMatchingToken()fails when called foryin an attempt to find two opening paren after scanning overx.