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