Skip to content

Commit 526bcca

Browse files
committed
Fix false positive bad-override when overriding methods using bounded TypeVars
When checking override compatibility between generic methods, type variables in the child method signature are instantiated with fresh inference variables and matched contravariantly against the parent parameters. When a parameter is a union like `T | None`, `is_subset_eq` previously attempted to check whether the `TypeVar` bound was a subtype of the union before splitting the RHS union. When the RHS union contained an unsolved inference variable, the speculative check succeeded under a snapshot and rolled back all variable assignments, leaving the child type variable unconstrained and causing override validation to fail. To fix this, check `Type::Quantified` against individual members of a RHS union first before falling back to testing whether the `TypeVar` bound or constraints as a whole satisfy the union. This allows inference variables in the RHS union to be properly constrained to the parent type variable, correctly validating method overrides.
1 parent 73f044a commit 526bcca

2 files changed

Lines changed: 71 additions & 58 deletions

File tree

pyrefly/lib/solver/subset.rs

Lines changed: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1867,52 +1867,6 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
18671867
{
18681868
Ok(())
18691869
}
1870-
// Given `A | B <: C | D` we must always split the LHS first, but a quantified might be hiding a LHS union in its bounds.
1871-
// Given (Quantified(bounds = A | B), A | B), we need to examine the bound _before_ splitting up the RHS union.
1872-
// But given (T@Quantified(bounds = ...), T | Something), we need to split the union.
1873-
// Therefore try these quantified cases, but only pick them if they work.
1874-
(Type::Quantified(q), u)
1875-
if let Restriction::Bound(bound) = q.restriction()
1876-
// A bare inference variable can preserve the quantified type itself. Expanding
1877-
// it to its bound here would make inference depend on which argument is checked
1878-
// first (https://github.com/facebook/pyrefly/issues/4187).
1879-
&& !matches!(u, Type::Union(union) if union.members.iter().any(|t| matches!(t, Type::Var(_))))
1880-
&& self
1881-
.solver
1882-
.with_snapshot(&u.collect_maybe_placeholder_vars(), || {
1883-
self.is_subset_eq(bound, u)
1884-
})
1885-
.is_ok() =>
1886-
{
1887-
Ok(())
1888-
}
1889-
(Type::Quantified(q), u)
1890-
if let Restriction::ShapeExtension(extension) = q.restriction()
1891-
&& self
1892-
.solver
1893-
.with_snapshot(&u.collect_maybe_placeholder_vars(), || {
1894-
self.is_subset_eq(
1895-
&extension.upper_bound(self.type_order.stdlib(), &self.solver.heap),
1896-
u,
1897-
)
1898-
})
1899-
.is_ok() =>
1900-
{
1901-
Ok(())
1902-
}
1903-
(Type::Quantified(q), u)
1904-
if let Restriction::Constraints(constraints) = q.restriction()
1905-
&& self
1906-
.solver
1907-
.with_snapshot(&u.collect_maybe_placeholder_vars(), || {
1908-
all(constraints.iter(), |constraint| {
1909-
self.is_subset_eq(constraint, u)
1910-
})
1911-
})
1912-
.is_ok() =>
1913-
{
1914-
Ok(())
1915-
}
19161870
(Type::Quantified(q), u @ Type::Tuple(_)) if q.is_type_var_tuple() => self
19171871
.is_subset_eq(
19181872
&self.solver.heap.mk_unbounded_tuple(
@@ -2084,6 +2038,25 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
20842038
all(members.iter(), |m| {
20852039
self.is_subset_eq(&Type::type_of(m.clone()), want)
20862040
})
2041+
} else if let Type::Quantified(q) = l {
2042+
// A quantified type parameter may hide a union in its bound or constraints
2043+
// (e.g. `T: (A, B)` or `T: A | B`). If per-member matching against the RHS
2044+
// union failed, check whether the bound or all constraints as a whole satisfy
2045+
// the RHS union.
2046+
match q.restriction() {
2047+
Restriction::Bound(bound) => self.is_subset_eq(bound, want),
2048+
Restriction::Constraints(constraints) => {
2049+
all(constraints.iter(), |constraint| {
2050+
self.is_subset_eq(constraint, want)
2051+
})
2052+
}
2053+
Restriction::ShapeExtension(extension) => {
2054+
let upper =
2055+
extension.upper_bound(self.type_order.stdlib(), &self.solver.heap);
2056+
self.is_subset_eq(&upper, want)
2057+
}
2058+
Restriction::Unrestricted => Err(error.unwrap_or(SubsetError::Other)),
2059+
}
20872060
} else {
20882061
Err(error.unwrap_or(SubsetError::Other))
20892062
}
@@ -2098,13 +2071,21 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
20982071
_ => result,
20992072
}
21002073
}
2101-
(Type::Quantified(q), u) if !q.restriction().is_restricted() => self.is_subset_eq(
2102-
&self
2103-
.solver
2104-
.heap
2105-
.mk_class_type(self.type_order.stdlib().object().clone()),
2106-
u,
2107-
),
2074+
(Type::Quantified(q), u) => match q.restriction() {
2075+
Restriction::Bound(bound) => self.is_subset_eq(bound, u),
2076+
Restriction::Constraints(constraints) => all(constraints.iter(), |constraint| {
2077+
self.is_subset_eq(constraint, u)
2078+
}),
2079+
Restriction::ShapeExtension(extension) => {
2080+
let upper = extension.upper_bound(self.type_order.stdlib(), &self.solver.heap);
2081+
self.is_subset_eq(&upper, u)
2082+
}
2083+
Restriction::Unrestricted => {
2084+
let upper = q.upper_bound(self.type_order.stdlib(), &self.solver.heap);
2085+
self.is_subset_eq(&upper, u)
2086+
}
2087+
},
2088+
21082089
(Type::Module(_), Type::ClassType(cls)) if cls.has_qname("types", "ModuleType") => {
21092090
Ok(())
21102091
}
@@ -2652,12 +2633,9 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
26522633
{
26532634
self.is_subset_literal_int_size(n, got, false)
26542635
}
2655-
(Type::Int(_) | Type::Quantified(_), Type::ClassType(cls))
2656-
if is_int_class_type(cls) =>
2657-
{
2658-
Ok(())
2659-
}
2636+
(Type::Int(_), Type::ClassType(cls)) if is_int_class_type(cls) => Ok(()),
26602637
(Type::QuantifiedValue(_), Type::ClassType(cls)) if is_int_class_type(cls) => Ok(()),
2638+
26612639
(Type::Literal(l_lit), Type::Literal(u_lit)) => {
26622640
ok_or(l_lit.value == u_lit.value, SubsetError::Other)
26632641
}

pyrefly/lib/test/class_overrides.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,3 +2242,38 @@ class D(C):
22422242
from stub import C, D
22432243
"#,
22442244
);
2245+
2246+
testcase!(
2247+
test_override_module_level_typevar,
2248+
r#"
2249+
from typing import TypeVar
2250+
from typing_extensions import override
2251+
2252+
T = TypeVar("T", bound=int)
2253+
TConstrained = TypeVar("TConstrained", int, str)
2254+
2255+
class Base:
2256+
def method(self, x: T | None = None) -> T: ...
2257+
def method_no_opt(self, x: T) -> T: ...
2258+
def method_constrained(self, x: TConstrained | None = None) -> TConstrained: ...
2259+
2260+
class Derived(Base):
2261+
@override
2262+
def method(self, x: T | None = None) -> T:
2263+
raise NotImplementedError
2264+
@override
2265+
def method_no_opt(self, x: T) -> T:
2266+
raise NotImplementedError
2267+
@override
2268+
def method_constrained(self, x: TConstrained | None = None) -> TConstrained:
2269+
raise NotImplementedError
2270+
2271+
class BasePep:
2272+
def method[T: int](self, x: T | None = None) -> T: ...
2273+
2274+
class DerivedPep(BasePep):
2275+
@override
2276+
def method[T: int](self, x: T | None = None) -> T:
2277+
raise NotImplementedError
2278+
"#,
2279+
);

0 commit comments

Comments
 (0)