From 01b4b960478c4ad844c048c050cfd274d15d3ead Mon Sep 17 00:00:00 2001 From: Eric Mark Martin Date: Wed, 22 Oct 2025 17:09:23 -0400 Subject: [PATCH] fix typeguard overriding logic --- .../resources/mdtest/narrow/type_guards.md | 28 ++++++++++++++++++- crates/ty_python_semantic/src/types/narrow.rs | 19 ++++++++++--- 2 files changed, 42 insertions(+), 5 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 9c88845f12..c2a5f8feb6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -359,7 +359,33 @@ def narrowed_type_must_be_exact(a: object, b: Baz): reveal_type(a) # revealed: Foo ``` -## Complex boolean logic with TypeGuard and TypeIs +## TypeGuard overrides normal constraints + +TypeGuard constraints override any previous narrowing, but additional "regular" constraints can be +added on to TypeGuard constraints. + +```py +from typing_extensions import TypeGuard, TypeIs + +class A: ... +class B: ... +class C: ... + +def f(x: object) -> TypeGuard[A]: + return True + +def g(x: object) -> TypeGuard[B]: + return True + +def h(x: object) -> TypeIs[C]: + return True + +def _(x: object): + if f(x) and g(x) and h(x): + reveal_type(x) # revealed: B & C +``` + +## Boolean logic with TypeGuard and TypeIs TypeGuard constraints need to properly distribute through boolean operations. diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 8b833e76a7..73afc9c713 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -313,8 +313,16 @@ impl<'db> Conjunction<'db> { /// Evaluate this conjunction to a single type. /// If there's a `TypeGuard` constraint, it replaces the regular constraint. /// Otherwise, returns the regular constraint. - fn evaluate_type_constraint(self) -> Type<'db> { - self.typeguard.unwrap_or(self.constraint) + fn evaluate_type_constraint(self, db: &'db dyn Db) -> Type<'db> { + self.typeguard.map_or_else( + || self.constraint, + |typeguard_constraint| { + IntersectionBuilder::new(db) + .add_positive(typeguard_constraint) + .add_positive(self.constraint) + .build() + }, + ) } } @@ -363,7 +371,7 @@ impl<'db> NarrowingConstraint<'db> { db, self.disjuncts .into_iter() - .map(Conjunction::evaluate_type_constraint), + .map(|disjunct| Conjunction::evaluate_type_constraint(disjunct, db)), ) } } @@ -1214,7 +1222,10 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { _ => return None, }; - Some(InternalConstraints::from_iter([(place, NarrowingConstraint::regular(narrowed_type))])) + Some(InternalConstraints::from_iter([( + place, + NarrowingConstraint::regular(narrowed_type), + )])) } fn evaluate_match_pattern_value(