From d65542c05eb46166814e3bfb02968da3125d1ef7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 6 Jan 2026 10:47:04 -0500 Subject: [PATCH] [ty] Make tuple intersection a fallible operation (#22094) ## Summary This PR attempts to address a TODO in https://github.com/astral-sh/ruff/pull/21965#discussion_r2635378498. --- crates/ty_python_semantic/src/types.rs | 8 ++- crates/ty_python_semantic/src/types/tuple.rs | 62 +++++++++----------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8babf31355..c24f375b6a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4645,7 +4645,13 @@ impl<'db> Type<'db> { let first_spec = specs_iter.next()?; let mut builder = TupleSpecBuilder::from(&*first_spec); for spec in specs_iter { - builder = builder.intersect(db, &spec); + // Two tuples cannot have incompatible specs unless the tuples themselves + // are disjoint. `IntersectionBuilder` eagerly simplifies such + // intersections to `Never`, so this should always return `Some`. + let Some(intersected) = builder.intersect(db, &spec) else { + return Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))); + }; + builder = intersected; } Some(Cow::Owned(builder.build())) } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 96568fd518..9b2d13aad4 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -1931,12 +1931,14 @@ impl<'db> TupleSpecBuilder<'db> { } } - /// Return a new tuple-spec builder that reflects the intersection of this tuple and another tuple. + /// Return a new tuple-spec builder that reflects the intersection of this tuple and another + /// tuple, or `None` if the intersection is impossible (e.g., two fixed-length tuples with + /// different lengths). /// /// For example, if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a /// tuple-spec for `tuple[object, object]`, the result will be a tuple-spec builder for /// `tuple[int, str]` (since `int & object` simplifies to `int`, and `str & object` to `str`). - pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Option { match (&mut self, other) { // Both fixed-length with the same length: element-wise intersection. (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) @@ -1945,24 +1947,23 @@ impl<'db> TupleSpecBuilder<'db> { for (existing, new) in our_elements.iter_mut().zip(new_elements.all_elements()) { *existing = IntersectionType::from_elements(db, [*existing, *new]); } - return self; + Some(self) } - (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Variable(var)) => { - if let Ok(tuple) = var.resize(db, TupleLength::Fixed(our_elements.len())) { - return self.intersect(db, &tuple); - } - } + // Fixed-length tuples with different lengths cannot intersect. + (TupleSpecBuilder::Fixed(_), TupleSpec::Fixed(_)) => None, - (TupleSpecBuilder::Variable { .. }, TupleSpec::Fixed(fixed)) => { - if let Ok(tuple) = self - .clone() - .build() - .resize(db, TupleLength::Fixed(fixed.len())) - { - return TupleSpecBuilder::from(&tuple).intersect(db, other); - } - } + (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Variable(var)) => var + .resize(db, TupleLength::Fixed(our_elements.len())) + .ok() + .and_then(|tuple| self.intersect(db, &tuple)), + + (TupleSpecBuilder::Variable { .. }, TupleSpec::Fixed(fixed)) => self + .clone() + .build() + .resize(db, TupleLength::Fixed(fixed.len())) + .ok() + .and_then(|tuple| TupleSpecBuilder::from(&tuple).intersect(db, other)), ( TupleSpecBuilder::Variable { @@ -1982,29 +1983,20 @@ impl<'db> TupleSpecBuilder<'db> { for (existing, new) in suffix.iter_mut().zip(var.suffix_elements()) { *existing = IntersectionType::from_elements(db, [*existing, *new]); } - return self; + return Some(self); } let self_built = self.clone().build(); let self_len = self_built.len(); - if let Ok(resized) = var.resize(db, self_len) { - return self.intersect(db, &resized); - } else if let Ok(resized) = self_built.resize(db, var.len()) { - return TupleSpecBuilder::from(&resized).intersect(db, other); - } + var.resize(db, self_len) + .ok() + .and_then(|resized| self.intersect(db, &resized)) + .or_else(|| { + self_built.resize(db, var.len()).ok().and_then(|resized| { + TupleSpecBuilder::from(&resized).intersect(db, other) + }) + }) } - - _ => {} - } - - // TODO: probably incorrect? `tuple[int, str] & tuple[int, str, bytes]` should resolve to `Never`. - // So maybe this function should be fallible (return an `Option`)? - let intersected = - IntersectionType::from_elements(db, self.all_elements().chain(other.all_elements())); - TupleSpecBuilder::Variable { - prefix: vec![], - variable: intersected, - suffix: vec![], } }