Skip to content

Add codefix for 'Cannot find name' diagnostic#28290

Merged
andrewbranch merged 19 commits into
microsoft:masterfrom
Kr4pper:add-codefix-cannot-find-name-in-for-loop
Jul 11, 2019
Merged

Add codefix for 'Cannot find name' diagnostic#28290
andrewbranch merged 19 commits into
microsoft:masterfrom
Kr4pper:add-codefix-cannot-find-name-in-for-loop

Conversation

@Kr4pper
Copy link
Copy Markdown
Contributor

@Kr4pper Kr4pper commented Nov 1, 2018

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 for y in an attempt to find two opening paren after scanning over x.

Copy link
Copy Markdown

@ghost ghost left a comment

Choose a reason for hiding this comment

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

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 []) {}|]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think these [| |] ranges are unnecessary when you use newFileContent.

@@ -0,0 +1,8 @@
/// <reference path='fourslash.ts' />

////[|for (x of []) {}|]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 []) {}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's silly to use newRangeContent when the range is the entire file, just use newFileContent.

Comment thread src/compiler/diagnosticMessages.json Outdated
"category": "Message",
"code": 95068
},
"Add const modifier to unresolved variable": {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@ghost ghost requested a review from DanielRosenwasser November 1, 2018 22:21
@ghost
Copy link
Copy Markdown

ghost commented Nov 1, 2018

I'm actually wondering if we should have a code fix at a naked x = 3; statement to change it to const x = 3;.

@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Nov 2, 2018

I added tests for for ... in and object destructuring. All errors that should come with this new codefix seem to now do so correctly.
However an existing fourslash test defined in incompleteFunctionCallCodefix3.ts and I fail to understand, how this change led to the test erroring. I have not yet commited the baselines due to this.

//// function ...q) {}} f(10);

verify.not.codeFixAvailable();
verify.not.codeFixAvailable([
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Forgot to remove .not

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 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

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 => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

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?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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).

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 hint, I believe this allowed for a positive change regarding both stability and readability.


////export {};
////const { x, y } = o;
////const { x, y } = [{}];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why this change? This makes the code wrong since it destructures an array to an object..

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.

Fixed, accidentally provided an array rather than an object.

@ghost
Copy link
Copy Markdown

ghost commented Nov 2, 2018

CC @DanielRosenwasser to review, and to consider whether we should have this change on the statement x = 0; where x is not defined.

@Kr4pper Kr4pper force-pushed the add-codefix-cannot-find-name-in-for-loop branch from 9fd9566 to 1fd2371 Compare November 15, 2018 22:06
@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Jan 20, 2019

Ping @DanielRosenwasser @Andy-MS
Just a friendly reminder, that this has been inactive for a while now.

@andrewbranch
Copy link
Copy Markdown
Member

to consider whether we should have this change on the statement x = 0; where x is not defined

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 for ([x, y] of [[1,2]]) {} case if you still need help with that.

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).

@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Jul 1, 2019

@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 for ([x, y] of [[1,2]]) {} cases are tested in codeFixAddMissingConstInForLoopWithArrayDestructuring1 and ...2 respectively and look to be working as expected.

What would be the best course of action to add the codefix for the x = 0 with x being undefined? The diagnostic messages themselves can probably be reused since they are not bound to the context of for-statements but I am assuming a new codefix will need to be created to handle these cases.

How extensive should that additional codefix be? Some cases that I can come up with are:

  • x = 0;
  • x = 1, y = 1;
  • {x} = {x: 2};
  • [x] = [3];
    Which of these cases and possibly which additional cases do you consider to be in scope?

@andrewbranch
Copy link
Copy Markdown
Member

TL;DR:

  • x = 0;—yes
  • x = 1, y = 1;—give it a shot
  • {x} = {x: 2};—perhaps not
  • [x] = [3];—give it a shot

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 x exists, so adding const before that would be an error for x even though it might be want you wanted for y.

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 = actually ends up in the trivia between two Block nodes, so detecting this particular situation would involve checking that the parse tree structure matches a very particular brand of “total garbage.” I would be hesitant to rely on the stability of that structure, as the exact results of garbage in → garbage out scenarios do change occasionally, especially in cases where we can actually improve the error messages associated with “garbage out” a bit.

So, I think it would be lovely if { x } = { x: 2 } worked, but my hunch is there’s not a clean way to do it that doesn’t assert a very specific invalid tree structure, and I would rather avoid that.

@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Jul 7, 2019

@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 ExpressionStatement node and collecting all identifiers along the way.

A hint on how to retrieve available identifiers would be much appreciated.

@andrewbranch
Copy link
Copy Markdown
Member

how to retrieve available identifiers

There is a checker function that does something like this (checker.getSymbolsInScope()), but that sounds like more work than you need to do. Rather than starting with “give me all valid identifiers” and seeing if every array element is one of them, it’s much more efficient to start with each array element and see if we can tell whether it exists. There are probably a few ways to do it, but my guess is that the cheapest is checker.getSymbolAtLocation(identifier)—if it returns undefined, then the binder wasn’t able to find a declaration for this identifier. I think it’s pretty good because the checker has to do very little work to get this info; it’s mostly looking up work the binder has already done.

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. [x, y = 0] is a syntactic candidate but [x, y()] isn’t. (My approach here is, for each element, check syntax and then check for a symbol; it might be more efficient to do, check every element for syntax, then if it all looks good, check every element again for a symbol. But I think symbol lookup should be cheap enough that either way is ok.)

Other random note: I tried running the codefix inline in addMissingConst.ts where I was indented a few tab stops, and it looks like some formatting is wonky—you should maybe add a test where the code to be fixed is indented:

insert-const

@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Jul 9, 2019

@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 ChangeTracker#getAdjustedStartPosition which due do the nature of the syntactically invalid identifier node interpreted that node as spanning multiple lines and thus added padding after it to 'match' the surroundings. Both of these checks fell through:

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 changeTracker.insertNodeBefore(...) which I could remove in favor of the simpler changeTracker.insertModifierBefore(...) which seems to be just the right tool for the job.

I will have a look at the comma separated initializer case tomorrow as it now looks a lot more achievable to support as well.

@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Jul 10, 2019

@andrewbranch It looks like I got the x = 0, y = 0 etc cases working. I dont like the way I am traversing the AST downwards to check for non-existence of all Identifiers contained within the leading ExpressionStatement but I could not find an available alternative and it seems to be doing the job fine for all test cases. As such I think the PR is now ready for review.

@andrewbranch
Copy link
Copy Markdown
Member

Looks like there are lint errors 🙈

Thanks for all your work on this! I look forward to reviewing it in full tomorrow ✨

@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Jul 10, 2019

Whoops forgot to remove a bit of whitespace 🙈

Copy link
Copy Markdown
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

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

So awesome that you jumped right back on this after 8 months of silence—thanks for your hard work and your patience 🌟

@andrewbranch andrewbranch merged commit 8eb3822 into microsoft:master Jul 11, 2019
@Kr4pper
Copy link
Copy Markdown
Contributor Author

Kr4pper commented Jul 11, 2019

@andrewbranch It was a pleasure to push this PR over the finish line with your help 😄

@Kr4pper Kr4pper deleted the add-codefix-cannot-find-name-in-for-loop branch July 11, 2019 20:15
@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.

Codefix for 'cannot find name' in for loop

5 participants