Skip to content

Fix TypeGuard with call on temporary object #19577

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 12 commits into
base: master
Choose a base branch
from

Conversation

saulshanabrook
Copy link

@saulshanabrook saulshanabrook commented Aug 3, 2025

Fixes #19575 by adding support for TypeGaurd/TypeIs when they are used on methods off of classes which were not saved to a variable.

Solution adapted from copilot answer here and then refined: saulshanabrook#1

  • Add test case for generic classes as well like X[int](y)
  • See if we can reduce code duplication

This comment has been minimized.

@saulshanabrook saulshanabrook marked this pull request as ready for review August 3, 2025 05:56

This comment has been minimized.

@A5rocks
Copy link
Collaborator

A5rocks commented Aug 3, 2025

I'm quite confused both by the changes in this PR and by the original code. In my mind, typeguards here should be an algorithm of:

  1. resolve the type of what is being called
  2. check if the return type of that is a TypeGuard

Without any of this special casing for RefExpr or CallExpr or whatever. What if I do (a + b)("this is a special string")? Am I wrong here, or could this be significantly simplified?

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Aug 3, 2025

Thanks for the PR! What do you think of the following additional diff.

This takes into account A5rocks' suggestion, avoids exceptions for control flow (which is slow in mypyc), and makes a change to look at CallableType results as well. Looks like we can't quite get rid of the RefExpr case without some tests involving overloads and generics failing...

diff --git a/mypy/checker.py b/mypy/checker.py
index afe7c7740..1460e844a 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -6183,23 +6183,20 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi):
                 attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1]))
                 if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1:
                     return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0])
-            elif isinstance(node.callee, (RefExpr, CallExpr)):
-                # We support both named callables (RefExpr) and temporaries (CallExpr).
-                # For temporaries (e.g., E()(x)), we extract type_is/type_guard from the __call__ method.
-                # For named callables (e.g., is_int(x)), we extract type_is/type_guard directly from the RefExpr.
+            else:
                 type_is, type_guard = None, None
-                try:
-                    called_type = get_proper_type(self.lookup_type(node.callee))
-                except KeyError:
-                    called_type = None
-                # TODO: there are some more cases in check_call() to handle.
-                # If the callee is an instance, try to extract TypeGuard/TypeIs from its __call__ method.
-                if called_type and isinstance(called_type, Instance):
-                    call = find_member("__call__", called_type, called_type, is_operator=True)
-                    if call is not None:
-                        called_type = get_proper_type(call)
-                        if isinstance(called_type, CallableType):
-                            type_is, type_guard = called_type.type_is, called_type.type_guard
+                called_type = self.lookup_type_or_none(node.callee)
+                if called_type is not None:
+                    called_type = get_proper_type(called_type)
+                    # TODO: there are some more cases in check_call() to handle.
+                    # If the callee is an instance, try to extract TypeGuard/TypeIs from its __call__ method.
+                    if isinstance(called_type, Instance):
+                        call = find_member("__call__", called_type, called_type, is_operator=True)
+                        if call is not None:
+                            called_type = get_proper_type(call)
+                    if isinstance(called_type, CallableType):
+                        type_is, type_guard = called_type.type_is, called_type.type_guard
+
                 # If the callee is a RefExpr, extract TypeGuard/TypeIs directly.
                 if isinstance(node.callee, RefExpr):
                     type_is, type_guard = node.callee.type_is, node.callee.type_guard

@saulshanabrook
Copy link
Author

This is my first time contributing to the MyPy codebase so I am not sure if a larger refactor is needed to support this more idiomatically.

Thanks @hauntsaninja for the diff! That does seem more general and I applied it and pushed it.

Copy link
Contributor

github-actions bot commented Aug 4, 2025

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

Copy link
Collaborator

@hauntsaninja hauntsaninja left a comment

Choose a reason for hiding this comment

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

Thank you!

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

Successfully merging this pull request may close these issues.

TypeGuard/TypeIs broken on __call__ with fresh class
3 participants