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 11 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
Copy link
Contributor

github-actions bot commented Aug 3, 2025

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

@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

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.

TypeGuard/TypeIs broken on __call__ with fresh class
3 participants