Skip to content
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

Don't assume direct parentage when emitting used-before-assignment #5582

Merged
merged 17 commits into from
Jan 10, 2022

Conversation

jacobtylerwalls
Copy link
Member

Type of Changes

Type
βœ“ πŸ› Bug fix

Description

Before
In Pylint 2.13.0 (unreleased) we are starting to be more confident in emitting used-before-assignment by making control flow inferences. (See e.g. #5384) We started cautiously, just checking direct parents of statements in try blocks to find out if that was the same TryExcept ancestor that was the parent of the node being visited in the except block. This left out possibly failing statements in the try block indented under if/with, etc.

Now
@DanielNoord suggested using get_node_first_ancestor_of_type() to be able to leapfrog over if and with statements to arrive at the relevant try ancestor. With this PR, we can now emit used-before-assignment even when the possibly failing statements in a try block are inside if/with statements.

One refactor commit and one behavior change commit.

Closes #4045

@coveralls
Copy link

coveralls commented Dec 21, 2021

Pull Request Test Coverage Report for Build 1678621581

  • 32 of 32 (100.0%) changed or added relevant lines in 2 files are covered.
  • 47 unchanged lines in 4 files lost coverage.
  • Overall coverage increased (+0.02%) to 93.715%

Files with Coverage Reduction New Missed Lines %
pylint/checkers/stdlib.py 1 93.53%
pylint/checkers/base.py 5 97.19%
pylint/checkers/classes/class_checker.py 16 94.32%
pylint/checkers/variables.py 25 96.2%
Totals Coverage Status
Change from base Build 1647269448: 0.02%
Covered Lines: 14388
Relevant Lines: 15353

πŸ’› - Coveralls

@jacobtylerwalls jacobtylerwalls marked this pull request as draft December 22, 2021 01:35
@jacobtylerwalls
Copy link
Member Author

jacobtylerwalls commented Dec 22, 2021

Need to add a test for

try:
    pass
except:
    if:
        name=
    else:
        name=
    print(name)

EDIT: done! βœ”οΈ

@jacobtylerwalls jacobtylerwalls marked this pull request as ready for review December 22, 2021 03:29
@jacobtylerwalls jacobtylerwalls changed the title Don't assume try ancestors are immediate parents when emitting used-before-assignment Don't assume direct parentage when emitting used-before-assignment Dec 22, 2021
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Looks very good! You're becoming/are the used-before expert πŸ˜„

pylint/checkers/variables.py Outdated Show resolved Hide resolved
pylint/checkers/variables.py Show resolved Hide resolved
pylint/checkers/variables.py Outdated Show resolved Hide resolved
pylint/checkers/variables.py Outdated Show resolved Hide resolved
tests/functional/u/use/used_before_assignment_issue2615.py Outdated Show resolved Hide resolved
Co-authored-by: DaniΓ«l van Noord <13665637+DanielNoord@users.noreply.github.com>
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

LGTM, a small remark regarding typing. This used-before-assignment check is getting a real boost thanks to you :)

pylint/checkers/variables.py Outdated Show resolved Hide resolved
(no typing exists for result of statement()`
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

I still have some question, I think I don't understand the functional test well.

pylint/checkers/variables.py Outdated Show resolved Hide resolved
Comment on lines +35 to +46
def consecutive_except_blocks():
"""An assignment assumed to execute in one TryExcept should continue to be
assumed to execute in a consecutive TryExcept.
"""
try:
res = 100
except ZeroDivisionError:
pass
try:
pass
except ValueError:
print(res)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think I understand everything here.

If the first except is raised and the function do not return, it means in the second one res won't be defined ?

Suggested change
def consecutive_except_blocks():
"""An assignment assumed to execute in one TryExcept should continue to be
assumed to execute in a consecutive TryExcept.
"""
try:
res = 100
except ZeroDivisionError:
pass
try:
pass
except ValueError:
print(res)
def consecutive_except_blocks():
try:
res = 100
except ZeroDivisionError:
pass
try:
pass
except ValueError:
print(res) # [used-before-assignment]
def consecutive_except_blocks_with_return():
try:
res = 100
except ZeroDivisionError:
return
try:
pass
except ValueError:
print(res)

Copy link
Member Author

Choose a reason for hiding this comment

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

Same here, we want to avoid annoying users, and so outside a TryExcept we assume the try statements executed. By that same logic we assume that outside a TryExcept its except statements did not execute -- because it would be a bit backwards to assume the try statements never execute and thus that assignments in the except always happen.

Copy link
Member

Choose a reason for hiding this comment

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

Didn't we say that like Pylance we consider that everything in a try/except can fail ? (#5384). Also, if we do not raise or return in the except it means we can reach line 43 from one branch or another and we would need to consider both branches in order to be correct.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, that wasn't my understanding, and that's not what Pylance does. The yellow underline is Pylance, showing that the checker we just worked on is in agreement with Pylance:
Screen Shot 2021-12-23 at 10 44 31 AM

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess a good way of putting it is that's why there are different private methods in this checker now:

_uncertain_nodes_in_except_blocks
_uncertain_nodes_in_try_blocks_when_evaluating_except_blocks
_uncertain_nodes_in_try_blocks_when_evaluating_finally_blocks

We make different control flow inferences based on where the name is being used (try, except, finally, or outside).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with the points you're making here. Especially the last example is something of which I can easily imagine complaints coming from. Code with requests and databases seem to often really on such patterns.

Perhaps you could add a summary of this explanation to the issue you mentioned? That way we don't lose this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Especially the last example is something of which I can easily imagine complaints coming from.

My last example was not quite right 😬 -- I neglected that subsequent handlers can't swallow exceptions raised in earlier handlers, so ValueError can't be passed down to a subsequent handler. I'll edit it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps you could add a summary of this explanation to the issue you mentioned? That way we don't lose this.

I think this discussion is getting at when an else clause should be used in a try/except/else. If you can depend on exhaustive handling by your except handlers, you don't need to guard your print(a) after the try/except. If it's not exhaustively handled, then your print(a) should be in an else.

I guess this could even be its own checker, so it could be enabled/disabled separately. (It would probably be easier to implement that way, given how much new logic would go into the exhaustiveness checking.)

Copy link
Member

Choose a reason for hiding this comment

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

I think relying on everyone knowing that library_function_that_might_sysexit will implicitly exit all the time is an anti-pattern. Personally I would initialize the variable to be sure, or at least to add a comment stating explicitly that there's an exit and it's ok to do that if there's a good reason to do that like performance. (a pylint disable is not that much more work then),

But I get that this might be a little opinionated and that we'd get complaints from this, thank you for explaining.

Since we're talking about addressing false negatives, not false positives, that seems like a smart way of iterating to me.

πŸ‘

Copy link
Member Author

Choose a reason for hiding this comment

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

This discussion helped clarify for me that beefing up the checker to assume try statements always fail is worthy of discussion, but would need to be solved in tandem with #5524, so I suggest we keep discussing there for 2.14 or later.

Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Just did a quick review of things I noticed immediately. I'll do a full review once @Pierre-Sassoulas is finished as not to duplicate work/comments.

pylint/checkers/utils.py Outdated Show resolved Hide resolved
pylint/checkers/variables.py Outdated Show resolved Hide resolved
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

I think this discussion must be resolved before we review the change in details.

@DanielNoord
Copy link
Collaborator

Moving this to 2.14 as there seems to be consensus that we want to wait for feedback on recent changes to this message. See #5582 (comment)

@DanielNoord DanielNoord modified the milestones: 2.13.0, 2.14.0 Jan 10, 2022
@jacobtylerwalls
Copy link
Member Author

jacobtylerwalls commented Jan 10, 2022

Moving this to 2.14 as there seems to be consensus that we want to wait for feedback on recent changes to this message. See #5582 (comment)

I guess that's not quite what I gathered from your comment at #5582 (comment). I thought the suggestion was to wait for 2.14 before potentially beefing up the checker to assume try statements always fail -- something that I think after more discussion we intend to track and discuss in #5524.

On the contrary, this PR and its partner #5583 merely fix false negatives that one would currently assume would work given how the release notes for 2.13 already describe the partial assumptions we are making. So it's not a blocker -- we can ship with false negatives, but it's not either that we are consciously deciding to wait to ship fixes for false negatives regarding assumptions that have already been merged to main in other PRs.

Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Misunderstood @jacobtylerwalls!

I only have one comment πŸ˜„

pylint/checkers/variables.py Outdated Show resolved Hide resolved
@DanielNoord DanielNoord modified the milestones: 2.14.0, 2.13.0 Jan 10, 2022
@jacobtylerwalls
Copy link
Member Author

Not a problem @DanielNoord, if I could go back in time I would have done this in the first set of control flow PRs, but live and learn!

@DanielNoord
Copy link
Collaborator

DanielNoord commented Jan 10, 2022

Not a problem @DanielNoord, if I could go back in time I would have done this in the first set of control flow PRs, but live and learn!

We've come a long way since your initial PR and my refactor of that document. I think it's still a bit of a mess but it is so much better than previously and also catches a lot more! All thanks to you πŸ˜„

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

Great work @jacobtylerwalls, thank you for the discussion too, doing false negative later makes a lot of sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
False Negative πŸ¦‹ No message is emitted but something is wrong with the code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Example where pylint does not detect uninitialized variable
4 participants