From 6c7b548b0ef58f5cb8edd5beb360771c4b29c7b3 Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 29 Jun 2026 16:38:13 +0200 Subject: [PATCH] Fix overloads with a generic `self:` dropped when matching a `Protocol` --- pyrefly/lib/alt/class/class_field.rs | 16 ++++++-- pyrefly/lib/test/protocol.rs | 61 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index a11b111150..555d56d741 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -2840,9 +2840,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { .signatures .iter() .filter(|sig| { - let func = match sig { - OverloadType::Function(f) => f, - OverloadType::Forall(forall) => &forall.body, + let (func, tparams) = match sig { + OverloadType::Function(f) => (f, None), + OverloadType::Forall(forall) => (&forall.body, Some(&forall.tparams)), }; // Only instance methods have a `self` first parameter; static and // class methods' first parameter is a regular argument. @@ -2850,6 +2850,16 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { return true; } func.signature.get_first_param().is_none_or(|p| { + // Replace the overload's own type params in `self:` with `Any` + // (e.g. `self: Array[S, T]` -> `Array[Any, Any]`), matching against a gradual + // `self:` rather than a rigid, unsolvable variable. + let p = match tparams { + Some(tparams) => { + let any = self.heap.mk_any_implicit(); + p.subst(&tparams.iter().map(|q| (q, &any)).collect()) + } + None => p, + }; // A non-protocol `self:` can't re-enter protocol conformance, so check // it directly. A protocol-typed `self:`, however, makes // `is_subset_eq(self_type, p)` re-enter this same filtering on the same diff --git a/pyrefly/lib/test/protocol.rs b/pyrefly/lib/test/protocol.rs index 8d9c66aa93..b1188c6700 100644 --- a/pyrefly/lib/test/protocol.rs +++ b/pyrefly/lib/test/protocol.rs @@ -1126,3 +1126,64 @@ def f(x: HasCallNoExtra[int, str]) -> Lens[int, str]: return x # E: not assignable to declared return type "#, ); + +testcase!( + test_protocol_overload_typevar_self_kept, + r#" +from typing import Protocol, overload + +class C: + @overload + def f[S](self: S, x: int) -> S: ... + @overload + def f(self, x: str) -> str: ... + def f(self, x: int | str) -> object: ... + +class P(Protocol): + def f(self, x: int) -> object: ... + +p: P = C() # the `self: S` overload binds S=C, satisfying P +"#, +); + +testcase!( + test_protocol_overload_parameterized_self_kept, + r#" +from typing import Protocol, overload + +class Arr[S, T]: + @overload + def astype[S2, T2](self: Arr[S2, T2], dtype: int) -> Arr[S2, T2]: ... + @overload + def astype(self, dtype: str) -> bytes: ... + def astype(self, dtype: object) -> object: ... + +class HasAstype(Protocol): + def astype(self, dtype: int) -> object: ... + +def g(a: Arr[int, float]) -> HasAstype: + return a # the `self: Arr[S2, T2]` overload binds to the receiver, satisfying P +"#, +); + +testcase!( + test_protocol_overload_partially_concrete_self_filtered, + r#" +from typing import Protocol, overload + +class Arr[S, T]: + @overload + def m[S2](self: Arr[S2, int], x: int) -> int: ... + @overload + def m(self, x: str) -> str: ... + def m(self, x: object) -> object: ... + +class HasM(Protocol): + def m(self, x: int) -> int: ... + +def g(a: Arr[float, str]) -> HasM: + # `self: Arr[S2, int]` pins T=int; receiver has T=str, so that overload does not + # apply and `Arr[float, str]` does not satisfy `HasM`. + return a # E: not assignable to declared return type +"#, +);