Skip to content

Avoid false unreachable and redundant-expr warnings in loops more robustly and efficiently, and avoid multiple revealed type notes for the same line. #19118

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

tyralla
Copy link
Collaborator

@tyralla tyralla commented May 20, 2025

Fixes #18606
Closes #18511
Improves #18991

This change is an improvement over 9685171. Besides fixing the regression reported in #18606 and removing the duplicates reported in #18511, it should significantly reduce the performance regression reported in #18991. At least running Measure-command {python runtests.py self} on my computer (with removed cache) is 10 % faster.

tyralla and others added 2 commits May 20, 2025 07:12
… robustly and efficiently, and avoid multiple `revealed type` notes for the same line.

This change is an improvement over 9685171.  Besides fixing the regression reported in python#18606 and removing the duplicates reported in python#18511, it should significantly reduce the performance regression reported in python#18991.  At least running `Measure-command {python runtests.py self}` on my computer (with removed cache) is 10 % faster.

This comment has been minimized.

@tyralla tyralla requested a review from JukkaL May 20, 2025 06:58
@tyralla
Copy link
Collaborator Author

tyralla commented May 20, 2025

@JukkaL: I thought that later responses to reveal_type are always better than earlier ones. However, this is not the case for testNewRedefineTryStatement test case. Maybe the most general approach would be to collect all the union items of the individual responses and to combine them in the final note. I could try this, if you think it's a good way to go.

Alternatively, I could just remove the reveal_type part of this pull request, as it seems less important than the other two addressed issues.

This comment has been minimized.

@tyralla tyralla requested review from hauntsaninja and A5rocks May 22, 2025 16:02
@tyralla
Copy link
Collaborator Author

tyralla commented May 22, 2025

I implemented a collection mechanism for the types revealed in different iteration steps. In the case of unions, the items are sorted alphabetically without differentiating upper and lower case letters.

At first glance, the reason for the Mypy primer diff (https://github.com/pydata/xarray) is not clear to me.

I asked for a review of those who opened the mentioned issues. Thanks in advance!

@sterliakov
Copy link
Collaborator

xarray is #19121, we have it on all current PRs

@tyralla
Copy link
Collaborator Author

tyralla commented May 23, 2025

@sterliakov: Thanks, good to know. (Anybody already working on it?)

@sterliakov
Copy link
Collaborator

@tyralla I'm a bit swamped, there are no related PRs open - so probably not yet? It is likely related to #18976, but I have no idea why it's inconsistent - any MRE extracted from that fails every time, not randomly. https://mypy-play.net/?mypy=master&python=3.12&flags=strict&gist=50f4ae811a650f9e90c6f52269f59d29

Copy link
Collaborator

@A5rocks A5rocks left a comment

Choose a reason for hiding this comment

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

I'm not immediately convinced this is robust!

mypy/checker.py Outdated
# We collect errors that indicate unreachability or redundant expressions
# in the first iteration and remove them in subsequent iterations if the
# related statement becomes reachable or non-redundant due to changes in
# partial types:
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if an unreachable statement is within another unreachable statement?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am not exactly sure what you are asking for.

Is this an example that fits your question?

[case testAvoidUnreachableCodeInUnreachableCodeWithPartialType]
# flags: --warn-unreachable --python-version 3.11

x = None
y = None
while x is None and y is None:
    reveal_type(x)  # N: Revealed type is "None"
    reveal_type(y)  # N: Revealed type is "None"
    if x is None and y is not None:
        x = 1  # E: Statement is unreachable
        if y is None:
            y = 1

[builtins fixtures/bool.pyi]

In this case, the results are the same for 1.15 and the current state of this PR.

Copy link
Collaborator

@A5rocks A5rocks May 23, 2025

Choose a reason for hiding this comment

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

something like this:

x = None
y = None
while True:
    if x is not None:
        print("a")
        if y is not None:
            print("b")  # this is unreachable
    x = 1

(I haven't run the PR on this so I'm not sure the result, but it should warn about unreachablity. mypy 1.15 does. I'm worried this change won't)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This example should print b. Is there a typo?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I edited it afterwards, it shouldn't anymore.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, you were totally right. The large temporarily unreachable code section shadowed the persistent unreachable code section. I added your test case and extended the current logic a bit. It's late now, so I'll postpone double-checking until tomorrow.

Can you see any more potential pitfalls?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I again looked at it. To me, the logic appears a little special, but clear and straightforward. Of course, I cannot exclude that there are more special cases my solution does not cover. So I wait for the next review.

@tyralla
Copy link
Collaborator Author

tyralla commented May 23, 2025

I'm not immediately convinced this is robust!

What kind of test case could convince you?

@A5rocks
Copy link
Collaborator

A5rocks commented May 23, 2025

Also BTW I notice your "restart CI" commit -- you could use git commit --allow-empty to avoid having to push a dummy change + a revert. (just a recommendation in case you didn't know)

@tyralla
Copy link
Collaborator Author

tyralla commented May 23, 2025

Also BTW I notice your "restart CI" commit -- you could use git commit --allow-empty to avoid having to push a dummy change + a revert. (just a recommendation in case you didn't know)

I didn't know. That's helpful, thanks!

…UnreachableLines` provided by @A5rocks and extend the current logic to fix it.
@tyralla tyralla requested a review from A5rocks May 24, 2025 00:41
Copy link
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Copy link
Collaborator

@A5rocks A5rocks left a comment

Choose a reason for hiding this comment

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

sorry, I think I misunderstood things.

for candidate in set(itertools.chain(*uselessness_errors)):
if all(
(candidate in errors) or (candidate[2] in lines)
for errors, lines in zip(uselessness_errors, unreachable_lines)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why can't this just use the last round of unreachability warnings?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is a good question. I initially implemented it so in 229e4a6. However, in the last function definition of test case testNewRedefineTryStatement, the first response to reveal_type does provide complete information, but the second (and final) response does not:

def f4() -> None:
    while int():
        try:
            x = 1
            if int():
                x = ""
                break
            if int():
                while int():
                    if int():
                        x = None
                        break
        finally:
            reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]" \
                           # N: Revealed type is "Union[builtins.int, None]"

I have no experience with the new --allow-redefinition-new option so far, and have not tried to investigate why this happens. I just extended my approach in c24c950.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't that case also allow something not allowed for str? Eg does that error for:

if x is not None:
  print(x + 5)

Above the reveal_type

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, after re-reading, it seems that I have not hit the point of your question. Another try: There might be some scope for optimisations. However, I am unsure what can possibly happen when running Mypy with different configurations (like in the f4 example discussed above), so I preferred to implement this part of the approach as robustly as possible.

Copy link
Collaborator

@A5rocks A5rocks May 25, 2025

Choose a reason for hiding this comment

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

On the other hand it's possible to be too robust: mypy only prints the errors for the last round right? So it's possible (through some bug elsewhere, like forgetting to propagate line numbers maybe?) to have unreachable code in the last run that isn't marked -- and so have no signal that the code there isn't checked.

On the other other hand, just using the last iteration's unreachability errors might be wrong in presence of bugs like in f4, but it's simpler (and implementation is trivial to get right) and could never mislead the user about what code could get errors.


Basically I think it's better to match the semantics of how error reporting happens (ie during the last round of checking) then to have only one type of error be correct in face of bugs.


Edit: actually I might be wrong about my assumptions here, let me play with your PR a bit and I'll update this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

well there seems to be a bug in your logic anyways (I can't see where, it looks right to me):

def f4() -> None:
    while int():
        try:
            x = 1
            if int():
                x = ""
                break
            if int():
                while int():
                    if int():
                        x = None
                        break
        finally:
            if isinstance(x, str):
                print("hello :)")  # E: Statement is unreachable
            reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]"

Anyways, turns out I was wrong about mypy only erroring in the final cycle. But the above bug should be enough for now and IMO a type becoming a subtype at the same place in the code seems like a bug.

@JukkaL since maybe some of my other assumptions are wrong. Do you think that looping over code multiple times but with subtypes should be considered a bug?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Are you certain? This is what I get:

# flags: --allow-redefinition-new --local-partial-types

def f4() -> None:
    while int():
        try:
            x = 1
            if int():
                x = ""
                break
            if int():
                while int():
                    if int():
                        x = None
                        break
        finally:
            if isinstance(x, str):
                reveal_type(x)  # N: Revealed type is "builtins.str"
            reveal_type(x)  # N: Revealed type is "Union[builtins.int, builtins.str, None]"

(I had to add isinstance to exception.pyi to get this running.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I just added that to a repro.py file and ran mypy repro.py --allow-redefinition-new --local-partial-types --warn-unreachable but maybe I checked your branch out incorrectly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

My bad; the -warn-unreachable flag was missing in the test case. Now I get both Statement is unreachable and Revealed type is "builtins.str". However, the same happens when I check the modified test with Mypy-Master. Therefore, I don't know if this actually points to a flaw in my current approach. I will investigate tomorrow....

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

...I had no time for a detailed investigation yet, but I tend to think the problem with the currently discussed test case has nothing to do with this PR, because the response to reveal_type and the unreachable error for the same line happen at the same iteration step.

Maybe the first hint on the cause for this discrepancy: expr.node.type = Union[builtins.int, builtins.str, None] but TypeChecker.lookup_type(expr) = Union[builtins.int, None].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[1.15 regression] (Temporary) false positive for reachability with unannotated partial type mypy duplicates reveal_type notes with partial types
3 participants