From 1d6ae8596a0acd2d84582a2c1cb29db4e89d505f Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 31 Oct 2025 10:58:09 -0400 Subject: [PATCH] [ty] Prefer exact matches when solving constrained type variables (#21165) ## Summary The solver is currently order-dependent, and will choose a supertype over the exact type if it appears earlier in the list of constraints. We could be smarter and try to choose the most precise subtype, but I imagine this is something the new constraint solver will fix anyways, and this fixes the issue showing up on https://github.com/astral-sh/ruff/pull/21070. --- .../mdtest/generics/legacy/functions.md | 25 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 8 ++++++ 2 files changed, 33 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 9745fdca21..2bbe85b5ec 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -545,3 +545,28 @@ def f(x: T, y: Not[T]) -> T: y = x # error: [invalid-assignment] return x ``` + +## Prefer exact matches for constrained typevars + +```py +from typing import TypeVar + +class Base: ... +class Sub(Base): ... + +# We solve to `Sub`, regardless of the order of constraints. +T = TypeVar("T", Base, Sub) +T2 = TypeVar("T2", Sub, Base) + +def f(x: T) -> list[T]: + return [x] + +def f2(x: T2) -> list[T2]: + return [x] + +x: list[Sub] = f(Sub()) +reveal_type(x) # revealed: list[Sub] + +y: list[Sub] = f2(Sub()) +reveal_type(y) # revealed: list[Sub] +``` diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 59216ca607..8485931ff2 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1483,6 +1483,14 @@ impl<'db> SpecializationBuilder<'db> { self.add_type_mapping(bound_typevar, ty); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // Prefer an exact match first. + for constraint in constraints.elements(self.db) { + if ty == *constraint { + self.add_type_mapping(bound_typevar, ty); + return Ok(()); + } + } + for constraint in constraints.elements(self.db) { if ty .when_assignable_to(self.db, *constraint, self.inferable)