Ignore `TypeOfProtocolNeedsConcreteClass` error on bound cls args Summary: Fixes https://github.com/facebook/pyrefly/issues/3375. We usually allow only concrete classes to be assigned to protocols. This should not prevent calling a classmethod on a protocol. Test Plan: `./test.py` Reviewers: Subscribers: Tasks: Tags:
diff --git a/pyrefly/lib/alt/callable.rs b/pyrefly/lib/alt/callable.rs index 35bfd5b..0fb1687 100644 --- a/pyrefly/lib/alt/callable.rs +++ b/pyrefly/lib/alt/callable.rs
@@ -54,6 +54,7 @@ use crate::solver::solver::CallBoundary; use crate::solver::solver::CallContext; use crate::solver::solver::QuantifiedHandle; +use crate::solver::solver::SubsetError; use crate::solver::solver::TypeVarSpecializationError; use crate::types::callable::Callable; use crate::types::callable::Param; @@ -473,6 +474,7 @@ hint: &Type, param_name: Option<&Name>, vararg: bool, + is_self_arg: bool, range: TextRange, arg_errors: &ErrorCollector, call_errors: &ErrorCollector, @@ -490,12 +492,20 @@ match self { Self::Type(ty, done) => { *done = true; - solver.check_type_with_options( + let fresh_call_errors = solver.error_collector(); + let res = solver.check_type_with_options( ty, hint, range, - TypeCheckOptions::new(call_errors, tcc).with_call_context(call_context), + TypeCheckOptions::new(&fresh_call_errors, tcc).with_call_context(call_context), ); + // A protocol class may bind its own classmethods even though it cannot be passed + // explicitly as an argument. + if !(is_self_arg + && matches!(res, Some(SubsetError::TypeOfProtocolNeedsConcreteClass(_)))) + { + call_errors.extend(fresh_call_errors); + } solver.maybe_error_unknown_argument_type(ty, range, arg_errors); Some((*ty).clone()) } @@ -909,7 +919,11 @@ }; Ok(param_list_owner.push(ps).items().iter().rev().collect()) }; - for arg in self_arg.iter().chain(args.iter()) { + for (arg, is_self_arg) in self_arg + .iter() + .map(|arg| (arg, true)) + .chain(args.iter().map(|arg| (arg, false))) + { let mut arg_pre = arg.pre_eval(self, arg_errors); while arg_pre.step() { let param = if let Some(p) = rparams.last() { @@ -984,6 +998,7 @@ ty, name, false, + is_self_arg, arg.range(), arg_errors, call_errors, @@ -1024,6 +1039,7 @@ ty, name, true, + is_self_arg, arg.range(), arg_errors, call_errors, @@ -1610,6 +1626,7 @@ &hint, name, false, + false, range, arg_errors, call_errors,
diff --git a/pyrefly/lib/solver/subset.rs b/pyrefly/lib/solver/subset.rs index 6c057d3..98f1f21 100644 --- a/pyrefly/lib/solver/subset.rs +++ b/pyrefly/lib/solver/subset.rs
@@ -2458,18 +2458,21 @@ { Err(SubsetError::Other) } - (Type::ClassDef(got), Type::Type(inner)) - if let Type::ClassType(want_cls) = &**inner - && self.type_order.is_protocol(want_cls.class_object()) - && self.type_order.is_protocol(got) => - { - // We only allow concrete class names to be assigned to `type[T]` if `T` is a protocol - Err(SubsetError::TypeOfProtocolNeedsConcreteClass( - want_cls.name().clone(), - )) - } (Type::ClassDef(got), Type::Type(want)) => { - self.is_subset_eq(&self.type_order.promote_silently(got), want) + let res = self.is_subset_eq(&self.type_order.promote_silently(got), want); + if res.is_ok() + && got.is_protocol() + && let Type::ClassType(want_cls) = &**want + && want_cls.class_object().is_protocol() + { + // We only allow concrete class names to be assigned to `type[T]` if `T` is a protocol. + // We do this check after all other checks on these types so that callers in contexts + // in which this error isn't applicable can drop it without losing other errors. + return Err(SubsetError::TypeOfProtocolNeedsConcreteClass( + want_cls.name().clone(), + )); + } + res } (Type::Type(inner), Type::ClassDef(want)) if let Type::ClassType(got_cls) = &**inner =>
diff --git a/pyrefly/lib/test/protocol.rs b/pyrefly/lib/test/protocol.rs index fc18672..e33b4a6 100644 --- a/pyrefly/lib/test/protocol.rs +++ b/pyrefly/lib/test/protocol.rs
@@ -1471,6 +1471,18 @@ ); testcase!( + test_call_classmethod_on_protocol, + r#" +from typing import Protocol +class P(Protocol): + @classmethod + def f(cls) -> None: + return None +P.f() + "#, +); + +testcase!( test_runtime_checkable_unsafe_overlap, r#" from typing import Protocol, runtime_checkable