From a24a326c3a08fee864893684518077defb14bc16 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 14 Feb 2026 18:13:09 -0800 Subject: [PATCH] [ty] support narrowing from Callable returning type guard --- .../resources/mdtest/narrow/type_guards.md | 12 ++++++++++++ crates/ty_python_semantic/src/types/narrow.rs | 19 +++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 5317d47a782749..f977248e3b3195 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -391,6 +391,18 @@ def _(a: Foo): reveal_type(a) # revealed: Foo & Bar ``` +Type guard narrowing also works when the callee has a `Callable` type: + +```py +from typing import Callable + +def _(x: Foo | Bar, is_bar: Callable[[object], TypeIs[Bar]]): + if is_bar(x): + reveal_type(x) # revealed: Bar + else: + reveal_type(x) # revealed: Foo & ~Bar +``` + For generics, we transform the argument passed into `TypeIs[]` from `X` to `Top[X]`. This helps especially when using various functions from typeshed that are annotated as returning `TypeIs[SomeCovariantGeneric[Any]]` to avoid false positives in other type checkers. For ty's diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 6315c8bfd4022f..4d62abcbf0bbd7 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1363,20 +1363,15 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { ) -> Option> { let inference = infer_expression_types(self.db, expression, TypeContext::default()); + if let Some(type_guard_call_constraints) = + self.evaluate_type_guard_call(inference, expr_call, is_positive) + { + return Some(type_guard_call_constraints); + } + let callable_ty = inference.expression_type(&*expr_call.func); match callable_ty { - Type::FunctionLiteral(function_type) - if matches!( - function_type.known(self.db), - None | Some(KnownFunction::RevealType) - ) => - { - self.evaluate_type_guard_call(inference, expr_call, is_positive) - } - Type::BoundMethod(_) => { - self.evaluate_type_guard_call(inference, expr_call, is_positive) - } // For the expression `len(E)`, we narrow the type based on whether len(E) is truthy // (i.e., whether E is non-empty). We only narrow the parts of the type where we know // `__bool__` and `__len__` are consistent (literals, tuples). Non-narrowable parts @@ -1464,7 +1459,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } // Helper to evaluate TypeGuard/TypeIs narrowing for a call expression. - // Used for both direct function calls and bound method calls. + // This is based on the call expression's return type, so it applies to any callable type. fn evaluate_type_guard_call( &mut self, inference: &ExpressionInference<'db>,