Repository: python/mypy (Python)

Issue:
False negative with protocols containins classvars
**Bug Report**

Some uses of protocols with ClassVars pass typechecking when they shouldn't. I've included a snippet with comments below.

**To Reproduce**

```python
from typing import Any, Protocol, ClassVar


class TestProtocol(Protocol):
    a: ClassVar[Any]


class TestClass:
    """Satisfies TestProtocol."""
    a: ClassVar = 1


def takes_instance(inst: TestProtocol) -> None:
    inst.a
    type(inst).a

takes_instance(TestClass())  # Typechecks, as it should
takes_instance(TestClass)  # Typechecks, but it blows up at runtime
```

**Expected Behavior**

The last line should not type-check.

**Actual Behavior**

The last line type-checks.

**Your Environment**

- Mypy version used: `master` branch, commit `09b8b550abe3982726047cc0ed68a9b6fa91745c`
- Mypy command-line flags:
- Mypy configuration options from `mypy.ini` (and other config files):
- Python version used: 3.10.1
- Operating system and version: macOs


Hints:
Hm, yeah, I think I missed this case. Should be a trivial fix.

Patch:
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -54,6 +54,7 @@
     IS_CLASS_OR_STATIC,
     IS_CLASSVAR,
     IS_SETTABLE,
+    IS_VAR,
     find_member,
     get_member_flags,
     is_same_type,
@@ -1959,13 +1960,25 @@ def report_protocol_problems(
                     context,
                     code=code,
                 )
-            if class_obj and IS_SETTABLE in superflags and IS_CLASSVAR not in subflags:
+            if (
+                class_obj
+                and IS_VAR in superflags
+                and (IS_VAR in subflags and IS_CLASSVAR not in subflags)
+            ):
                 self.note(
                     "Only class variables allowed for class object access on protocols,"
                     ' {} is an instance variable of "{}"'.format(name, subtype.type.name),
                     context,
                     code=code,
                 )
+            if class_obj and IS_CLASSVAR in superflags:
+                self.note(
+                    "ClassVar protocol member {}.{} can never be matched by a class object".format(
+                        supertype.type.name, name
+                    ),
+                    context,
+                    code=code,
+                )
         self.print_more(conflict_flags, context, OFFSET, MAX_ITEMS, code=code)
 
     def pretty_overload(
@@ -2600,8 +2613,10 @@ def get_bad_protocol_flags(
             or IS_CLASS_OR_STATIC in superflags
             and IS_CLASS_OR_STATIC not in subflags
             or class_obj
-            and IS_SETTABLE in superflags
+            and IS_VAR in superflags
             and IS_CLASSVAR not in subflags
+            or class_obj
+            and IS_CLASSVAR in superflags
         ):
             bad_flags.append((name, subflags, superflags))
     return bad_flags
diff --git a/mypy/subtypes.py b/mypy/subtypes.py
--- a/mypy/subtypes.py
+++ b/mypy/subtypes.py
@@ -68,6 +68,7 @@
 IS_SETTABLE: Final = 1
 IS_CLASSVAR: Final = 2
 IS_CLASS_OR_STATIC: Final = 3
+IS_VAR: Final = 4
 
 TypeParameterChecker: _TypeAlias = Callable[[Type, Type, int, bool, "SubtypeContext"], bool]
 
@@ -1020,9 +1021,12 @@ def named_type(fullname: str) -> Instance:
                 if (IS_CLASSVAR in subflags) != (IS_CLASSVAR in superflags):
                     return False
             else:
-                if IS_SETTABLE in superflags and IS_CLASSVAR not in subflags:
+                if IS_VAR in superflags and IS_CLASSVAR not in subflags:
                     # Only class variables are allowed for class object access.
                     return False
+                if IS_CLASSVAR in superflags:
+                    # This can be never matched by a class object.
+                    return False
             if IS_SETTABLE in superflags and IS_SETTABLE not in subflags:
                 return False
             # This rule is copied from nominal check in checker.py
@@ -1118,13 +1122,17 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set
         if isinstance(method, Decorator):
             if method.var.is_staticmethod or method.var.is_classmethod:
                 return {IS_CLASS_OR_STATIC}
+            elif method.var.is_property:
+                return {IS_VAR}
         elif method.is_property:  # this could be settable property
             assert isinstance(method, OverloadedFuncDef)
             dec = method.items[0]
             assert isinstance(dec, Decorator)
             if dec.var.is_settable_property or setattr_meth:
-                return {IS_SETTABLE}
-        return set()
+                return {IS_VAR, IS_SETTABLE}
+            else:
+                return {IS_VAR}
+        return set()  # Just a regular method
     node = info.get(name)
     if not node:
         if setattr_meth:
@@ -1137,8 +1145,10 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set
         return set()
     v = node.node
     # just a variable
-    if isinstance(v, Var) and not v.is_property:
-        flags = set()
+    if isinstance(v, Var):
+        if v.is_property:
+            return {IS_VAR}
+        flags = {IS_VAR}
         if not v.is_final:
             flags.add(IS_SETTABLE)
         if v.is_classvar:


Test patch:
diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test
--- a/test-data/unit/check-protocols.test
+++ b/test-data/unit/check-protocols.test
@@ -3182,8 +3182,21 @@ test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P"
 test(D)  # E: Argument 1 to "test" has incompatible type "Type[D]"; expected "P" \
          # N: Only class variables allowed for class object access on protocols, foo is an instance variable of "D"
 
+[case testProtocolClassObjectClassVarRejected]
+from typing import ClassVar, Protocol
+
+class P(Protocol):
+    foo: ClassVar[int]
+
+class B:
+    foo: ClassVar[int]
+
+def test(arg: P) -> None: ...
+test(B)  # E: Argument 1 to "test" has incompatible type "Type[B]"; expected "P" \
+         # N: ClassVar protocol member P.foo can never be matched by a class object
+
 [case testProtocolClassObjectPropertyRejected]
-from typing import Protocol
+from typing import ClassVar, Protocol
 
 class P(Protocol):
     @property
@@ -3192,12 +3205,20 @@ class P(Protocol):
 class B:
     @property
     def foo(self) -> int: ...
+class C:
+    foo: int
+class D:
+    foo: ClassVar[int]
 
 def test(arg: P) -> None: ...
-# TODO: give better diagnostics in this case.
+# TODO: skip type mismatch diagnostics in this case.
 test(B)  # E: Argument 1 to "test" has incompatible type "Type[B]"; expected "P" \
          # N: Following member(s) of "B" have conflicts: \
-         # N:     foo: expected "int", got "Callable[[B], int]"
+         # N:     foo: expected "int", got "Callable[[B], int]" \
+         # N: Only class variables allowed for class object access on protocols, foo is an instance variable of "B"
+test(C)  # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
+         # N: Only class variables allowed for class object access on protocols, foo is an instance variable of "C"
+test(D)  # OK
 [builtins fixtures/property.pyi]
 
 [case testProtocolClassObjectInstanceMethod]