Repository: python/mypy
Pull request: Fix class method type variables
Fixes https://github.com/python/mypy/issues/7846

The fix is mostly straightforward, instead of ad-hoc guessing of type variables from the return type, we pass the them all the way through intermediate helper functions.

The `ctypes` plugin change is because it used the bare representation of types (the one used by `reveal_type()`) for user errors. So after my change arrangement of stars changed, instead of adjusting it, I switched to the proper type formatting.

Problem statement:
Unsafe use of a generic class method of a generic class allowed
Mypy associates class type variables to generic class methods in some contexts, but this is inconsistent, which results in type unsafety. Here is an example that mypy is okay with but that is actually unsafe:

```py
from typing import TypeVar, Generic

T = TypeVar('T')
T2 = TypeVar('T2')

class C(Generic[T]):
    @classmethod
    def f(cls, x: T) -> T:
        return x

    @classmethod
    def g(cls, x: T2) -> T2:
        cls.f(x)  # This is accepted
        return x

class D(C[int]):
    @classmethod
    def f(cls, x: int) -> int:  # This override is accepted
        return x + 1

D.g('')   # Runtime error!
```

If the methods are instance methods, `T2` is not treated as compatible with `T`, however.

Hints from issue discussion:
> Mypy associates class type variables to generic class methods in some contexts

This is done to support alternative class constructors (a popular feature request in the past). I think this idea is not unsafe by itself but the implementation likely have some bugs.

I actually remember hitting something like this while working on some class method issue. I don't remember what was exactly the problem, but most likely it is related to what type `cls` is given. I vaguely remember this is because `cls` looks like `C`, not like `C[T]` at the point where the helper adding type variables is called. So that in your example `cls.f` has type `def [T] (T) -> T` instead of `def (T) -> T`.

There is a test case `testGenericClassMethodUnboundOnClassNonMatchingIdNonGeneric`. From it's name it is clear that the method should be non-generic but it doesn't work (see revealed type for `cls.foo`). I wanted to add a TODO here but probably forgot.
Yeah, the underlying feature looks useful. The unsafety is a bit worrying though, since it recently hid quite a few bad annotations in some code is was annotating. Increasing priority to high since the false negatives can be hard to track down.
While working on this I discovered there is the same issue for normal constructors:
```python
class C(Generic[T]):
    def __init__(self, item: T) -> None:
        self.item = item
    @classmethod
    def f(cls) -> None:
        cls(1)  # This is in fact unsafe, cls is def (T) -> T, not def [T] (T) -> T

class D(C[str]):
    def __init__(self, item: str) -> None:
        self.item = item + 'no way'

D('hm').f()
```
I can prohibit this too (for the same reason described above), but I feel like people may  be not happy about this. Probably we can test this against internal code bases to estimate fallout.

Commit message: Fix self-check

Patch to review:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index b86801e25f1b..ee03eba59dac 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -911,7 +911,12 @@ def analyze_type_type_callee(self, item: ProperType, context: Context) -> Type:
             res = type_object_type(item.type, self.named_type)
             if isinstance(res, CallableType):
                 res = res.copy_modified(from_type_type=True)
-            return expand_type_by_instance(res, item)
+            expanded = get_proper_type(expand_type_by_instance(res, item))
+            if isinstance(expanded, CallableType):
+                # Callee of the form Type[...] should never be generic, only
+                # proper class objects can be.
+                expanded = expanded.copy_modified(variables=[])
+            return expanded
         if isinstance(item, UnionType):
             return UnionType([self.analyze_type_type_callee(get_proper_type(tp), context)
                               for tp in item.relevant_items()], item.line)
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
index 859bd6afcc6d..acf6c0ea3e3b 100644
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -1,6 +1,6 @@
 """Type checking of attribute access"""
 
-from typing import cast, Callable, Optional, Union
+from typing import cast, Callable, Optional, Union, List
 from typing_extensions import TYPE_CHECKING
 
 from mypy.types import (
@@ -240,7 +240,8 @@ def analyze_type_callable_member_access(name: str,
             # This check makes sure that when we encounter an operator, we skip looking up
             # the corresponding method in the current instance to avoid this edge case.
             # See https://github.com/python/mypy/pull/1787 for more info.
-            result = analyze_class_attribute_access(ret_type, name, mx)
+            result = analyze_class_attribute_access(ret_type, name, mx,
+                                                    original_vars=typ.items()[0].variables)
             if result:
                 return result
         # Look up from the 'type' type.
@@ -649,7 +650,9 @@ def f(self: S) -> T: ...
 def analyze_class_attribute_access(itype: Instance,
                                    name: str,
                                    mx: MemberContext,
-                                   override_info: Optional[TypeInfo] = None) -> Optional[Type]:
+                                   override_info: Optional[TypeInfo] = None,
+                                   original_vars: Optional[List[TypeVarDef]] = None
+                                   ) -> Optional[Type]:
     """original_type is the type of E in the expression E.var"""
     info = itype.type
     if override_info:
@@ -737,7 +740,7 @@ def analyze_class_attribute_access(itype: Instance,
         if isinstance(t, FunctionLike) and is_classmethod:
             t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg)
         result = add_class_tvars(t, itype, isuper, is_classmethod,
-                                 mx.builtin_type, mx.self_type)
+                                 mx.builtin_type, mx.self_type, original_vars=original_vars)
         if not mx.is_lvalue:
             result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type,
                                                mx.msg, mx.context, chk=mx.chk)
@@ -783,7 +786,8 @@ def analyze_class_attribute_access(itype: Instance,
 def add_class_tvars(t: ProperType, itype: Instance, isuper: Optional[Instance],
                     is_classmethod: bool,
                     builtin_type: Callable[[str], Instance],
-                    original_type: Type) -> Type:
+                    original_type: Type,
+                    original_vars: Optional[List[TypeVarDef]] = None) -> Type:
     """Instantiate type variables during analyze_class_attribute_access,
     e.g T and Q in the following:
 
@@ -799,7 +803,6 @@ class B(A[str]): pass
     component in case if a union (this is used to bind the self-types).
     """
     # TODO: verify consistency between Q and T
-    info = itype.type  # type: TypeInfo
     if is_classmethod:
         assert isuper is not None
         t = get_proper_type(expand_type_by_instance(t, isuper))
@@ -815,22 +818,15 @@ class B(A[str]): pass
     # This behaviour is useful for defining alternative constructors for generic classes.
     # To achieve such behaviour, we add the class type variables that are still free
     # (i.e. appear in the return type of the class object on which the method was accessed).
-    free_ids = {t.id for t in itype.args if isinstance(t, TypeVarType)}
-
     if isinstance(t, CallableType):
-        # NOTE: in practice either all or none of the variables are free, since
-        # visit_type_application() will detect any type argument count mismatch and apply
-        # a correct number of Anys.
-        tvars = [TypeVarDef(n, n, i + 1, [], builtin_type('builtins.object'), tv.variance)
-                 for (i, n), tv in zip(enumerate(info.type_vars), info.defn.type_vars)
-                 # use 'is' to avoid id clashes with unrelated variables
-                 if any(tv.id is id for id in free_ids)]
+        tvars = original_vars if original_vars is not None else []
         if is_classmethod:
             t = bind_self(t, original_type, is_classmethod=True)
         return t.copy_modified(variables=tvars + t.variables)
     elif isinstance(t, Overloaded):
         return Overloaded([cast(CallableType, add_class_tvars(item, itype, isuper, is_classmethod,
-                                                              builtin_type, original_type))
+                                                              builtin_type, original_type,
+                                                              original_vars=original_vars))
                            for item in t.items()])
     return t
 
diff --git a/mypy/plugins/ctypes.py b/mypy/plugins/ctypes.py
index ce943cec8850..88fb2f04769e 100644
--- a/mypy/plugins/ctypes.py
+++ b/mypy/plugins/ctypes.py
@@ -6,6 +6,7 @@
 import mypy.plugin
 from mypy import nodes
 from mypy.maptype import map_instance_to_supertype
+from mypy.messages import format_type
 from mypy.subtypes import is_subtype
 from mypy.types import (
     AnyType, CallableType, Instance, NoneType, Type, TypeOfAny, UnionType,
@@ -125,18 +126,17 @@ def array_constructor_callback(ctx: 'mypy.plugin.FunctionContext') -> Type:
         for arg_num, (arg_kind, arg_type) in enumerate(zip(ctx.arg_kinds[0], ctx.arg_types[0]), 1):
             if arg_kind == nodes.ARG_POS and not is_subtype(arg_type, allowed):
                 ctx.api.msg.fail(
-                    'Array constructor argument {} of type "{}"'
-                    ' is not convertible to the array element type "{}"'
-                    .format(arg_num, arg_type, et),
-                    ctx.context)
+                    'Array constructor argument {} of type {}'
+                    ' is not convertible to the array element type {}'
+                    .format(arg_num, format_type(arg_type), format_type(et)), ctx.context)
             elif arg_kind == nodes.ARG_STAR:
                 ty = ctx.api.named_generic_type("typing.Iterable", [allowed])
                 if not is_subtype(arg_type, ty):
+                    it = ctx.api.named_generic_type("typing.Iterable", [et])
                     ctx.api.msg.fail(
-                        'Array constructor argument {} of type "{}"'
-                        ' is not convertible to the array element type "Iterable[{}]"'
-                        .format(arg_num, arg_type, et),
-                        ctx.context)
+                        'Array constructor argument {} of type {}'
+                        ' is not convertible to the array element type {}'
+                        .format(arg_num, format_type(arg_type), format_type(it)), ctx.context)
 
     return ctx.default_return_type
 
@@ -204,10 +204,9 @@ def array_value_callback(ctx: 'mypy.plugin.AttributeContext') -> Type:
                 types.append(_get_text_type(ctx.api))
             else:
                 ctx.api.msg.fail(
-                    'ctypes.Array attribute "value" is only available'
-                    ' with element type c_char or c_wchar, not "{}"'
-                    .format(et),
-                    ctx.context)
+                    'Array attribute "value" is only available'
+                    ' with element type "c_char" or "c_wchar", not {}'
+                    .format(format_type(et)), ctx.context)
         return make_simplified_union(types)
     return ctx.default_attr_type
 
@@ -223,9 +222,8 @@ def array_raw_callback(ctx: 'mypy.plugin.AttributeContext') -> Type:
                 types.append(_get_bytes_type(ctx.api))
             else:
                 ctx.api.msg.fail(
-                    'ctypes.Array attribute "raw" is only available'
-                    ' with element type c_char, not "{}"'
-                    .format(et),
-                    ctx.context)
+                    'Array attribute "raw" is only available'
+                    ' with element type "c_char", not {}'
+                    .format(format_type(et)), ctx.context)
         return make_simplified_union(types)
     return ctx.default_attr_type
diff --git a/test-data/unit/check-ctypes.test b/test-data/unit/check-ctypes.test
index a27011ca48a1..f6e55a451794 100644
--- a/test-data/unit/check-ctypes.test
+++ b/test-data/unit/check-ctypes.test
@@ -6,8 +6,8 @@ class MyCInt(ctypes.c_int):
 
 intarr4 = ctypes.c_int * 4
 a = intarr4(1, ctypes.c_int(2), MyCInt(3), 4)
-intarr4(1, 2, 3, "invalid")  # E: Array constructor argument 4 of type "builtins.str" is not convertible to the array element type "ctypes.c_int"
-reveal_type(a)  # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
+intarr4(1, 2, 3, "invalid")  # E: Array constructor argument 4 of type "str" is not convertible to the array element type "c_int"
+reveal_type(a)  # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
 reveal_type(a[0])  # N: Revealed type is 'builtins.int'
 reveal_type(a[1:3])  # N: Revealed type is 'builtins.list[builtins.int]'
 a[0] = 42
@@ -30,11 +30,11 @@ class MyCInt(ctypes.c_int):
 
 myintarr4 = MyCInt * 4
 mya = myintarr4(1, 2, MyCInt(3), 4)
-myintarr4(1, ctypes.c_int(2), MyCInt(3), "invalid")  # E: Array constructor argument 2 of type "ctypes.c_int" is not convertible to the array element type "__main__.MyCInt" \
-                                                     # E: Array constructor argument 4 of type "builtins.str" is not convertible to the array element type "__main__.MyCInt"
-reveal_type(mya)  # N: Revealed type is 'ctypes.Array[__main__.MyCInt]'
-reveal_type(mya[0])  # N: Revealed type is '__main__.MyCInt'
-reveal_type(mya[1:3])  # N: Revealed type is 'builtins.list[__main__.MyCInt]'
+myintarr4(1, ctypes.c_int(2), MyCInt(3), "invalid")  # E: Array constructor argument 2 of type "c_int" is not convertible to the array element type "MyCInt" \
+                                                     # E: Array constructor argument 4 of type "str" is not convertible to the array element type "MyCInt"
+reveal_type(mya)  # N: Revealed type is 'ctypes.Array[__main__.MyCInt*]'
+reveal_type(mya[0])  # N: Revealed type is '__main__.MyCInt*'
+reveal_type(mya[1:3])  # N: Revealed type is 'builtins.list[__main__.MyCInt*]'
 mya[0] = 42
 mya[1] = ctypes.c_int(42)  # E: No overload variant of "__setitem__" of "Array" matches argument types "int", "c_int" \
                            # N: Possible overload variants: \
@@ -106,7 +106,7 @@ import ctypes
 
 wca = (ctypes.c_wchar * 4)('a', 'b', 'c', '\x00')
 reveal_type(wca.value)  # N: Revealed type is 'builtins.str'
-wca.raw  # E: ctypes.Array attribute "raw" is only available with element type c_char, not "ctypes.c_wchar"
+wca.raw  # E: Array attribute "raw" is only available with element type "c_char", not "c_wchar"
 [builtins fixtures/floatdict.pyi]
 
 [case testCtypesWcharArrayAttrsPy2]
@@ -115,7 +115,7 @@ import ctypes
 
 wca = (ctypes.c_wchar * 4)(u'a', u'b', u'c', u'\x00')
 reveal_type(wca.value)  # N: Revealed type is 'builtins.unicode'
-wca.raw  # E: ctypes.Array attribute "raw" is only available with element type c_char, not "ctypes.c_wchar"
+wca.raw  # E: Array attribute "raw" is only available with element type "c_char", not "c_wchar"
 [builtins_py2 fixtures/floatdict_python2.pyi]
 
 [case testCtypesCharUnionArrayAttrs]
@@ -124,7 +124,7 @@ from typing import Union
 
 cua: ctypes.Array[Union[ctypes.c_char, ctypes.c_wchar]]
 reveal_type(cua.value)  # N: Revealed type is 'Union[builtins.bytes, builtins.str]'
-cua.raw  # E: ctypes.Array attribute "raw" is only available with element type c_char, not "Union[ctypes.c_char, ctypes.c_wchar]"
+cua.raw  # E: Array attribute "raw" is only available with element type "c_char", not "Union[c_char, c_wchar]"
 [builtins fixtures/floatdict.pyi]
 
 [case testCtypesAnyUnionArrayAttrs]
@@ -141,8 +141,8 @@ import ctypes
 from typing import Union
 
 cua: ctypes.Array[Union[ctypes.c_char, ctypes.c_int]]
-cua.value  # E: ctypes.Array attribute "value" is only available with element type c_char or c_wchar, not "Union[ctypes.c_char, ctypes.c_int]"
-cua.raw  # E: ctypes.Array attribute "raw" is only available with element type c_char, not "Union[ctypes.c_char, ctypes.c_int]"
+cua.value  # E: Array attribute "value" is only available with element type "c_char" or "c_wchar", not "Union[c_char, c_int]"
+cua.raw  # E: Array attribute "raw" is only available with element type "c_char", not "Union[c_char, c_int]"
 [builtins fixtures/floatdict.pyi]
 
 [case testCtypesAnyArrayAttrs]
@@ -157,8 +157,8 @@ reveal_type(aa.raw)  # N: Revealed type is 'builtins.bytes'
 import ctypes
 
 oa = (ctypes.c_int * 4)(1, 2, 3, 4)
-oa.value  # E: ctypes.Array attribute "value" is only available with element type c_char or c_wchar, not "ctypes.c_int"
-oa.raw  # E: ctypes.Array attribute "raw" is only available with element type c_char, not "ctypes.c_int"
+oa.value  # E: Array attribute "value" is only available with element type "c_char" or "c_wchar", not "c_int"
+oa.raw  # E: Array attribute "raw" is only available with element type "c_char", not "c_int"
 [builtins fixtures/floatdict.pyi]
 
 [case testCtypesArrayConstructorStarargs]
@@ -168,13 +168,13 @@ intarr4 = ctypes.c_int * 4
 intarr6 = ctypes.c_int * 6
 int_values = [1, 2, 3, 4]
 c_int_values = [ctypes.c_int(1), ctypes.c_int(2), ctypes.c_int(3), ctypes.c_int(4)]
-reveal_type(intarr4(*int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
-reveal_type(intarr4(*c_int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
-reveal_type(intarr6(1, ctypes.c_int(2), *int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
-reveal_type(intarr6(1, ctypes.c_int(2), *c_int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int]'
+reveal_type(intarr4(*int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
+reveal_type(intarr4(*c_int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
+reveal_type(intarr6(1, ctypes.c_int(2), *int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
+reveal_type(intarr6(1, ctypes.c_int(2), *c_int_values))  # N: Revealed type is 'ctypes.Array[ctypes.c_int*]'
 
 float_values = [1.0, 2.0, 3.0, 4.0]
-intarr4(*float_values) # E: Array constructor argument 1 of type "builtins.list[builtins.float*]" is not convertible to the array element type "Iterable[ctypes.c_int]"
+intarr4(*float_values) # E: Array constructor argument 1 of type "List[float]" is not convertible to the array element type "Iterable[c_int]"
 [builtins fixtures/floatdict.pyi]
 
 [case testCtypesArrayConstructorKwargs]
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index 3a5e68c90ed5..c4e5e6d6d3f1 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -484,9 +484,12 @@ class A(Generic[T]):
   @classmethod
   def foo(cls) -> None:
       reveal_type(cls)  # N: Revealed type is 'Type[__main__.A[T`1]]'
-      reveal_type(cls(1))  # N: Revealed type is '__main__.A[builtins.int*]'
-      reveal_type(cls('wooooo'))  # N: Revealed type is '__main__.A[builtins.str*]'
+      reveal_type(cls(cls.x))  # N: Revealed type is '__main__.A[T`1]'
 
+  @classmethod
+  def other(cls, x: T) -> A[T]: ...
+
+reveal_type(A(0).other)  # N: Revealed type is 'def (x: builtins.int*) -> __main__.A[builtins.int*]'
 [builtins fixtures/classmethod.pyi]
 
 [case testDataclassesForwardRefs]
diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test
index 09a3616fb3a2..140add167fb2 100644
--- a/test-data/unit/check-generics.test
+++ b/test-data/unit/check-generics.test
@@ -1928,6 +1928,89 @@ reveal_type(E.make_one)  # N: Revealed type is 'def [S] (x: Tuple[S`1, builtins.
 reveal_type(E[int].make_one)  # N: Revealed type is 'def (x: Tuple[builtins.int*, builtins.str*]) -> __main__.C[Tuple[builtins.int*, builtins.str*]]'
 [builtins fixtures/classmethod.pyi]
 
+[case testGenericClassClsNonGeneric]
+from typing import TypeVar, Generic
+
+T = TypeVar('T')
+
+class C(Generic[T]):
+    @classmethod
+    def f(cls, x: T) -> T:
+        return x
+
+    @classmethod
+    def other(cls) -> None:
+        reveal_type(C)  # N: Revealed type is 'def [T] () -> __main__.C[T`1]'
+        reveal_type(C[T])  # N: Revealed type is 'def () -> __main__.C[T`1]'
+        reveal_type(C.f)  # N: Revealed type is 'def [T] (x: T`1) -> T`1'
+        reveal_type(C[T].f)  # N: Revealed type is 'def (x: T`1) -> T`1'
+        reveal_type(cls.f)  # N: Revealed type is 'def (x: T`1) -> T`1'
+[builtins fixtures/classmethod.pyi]
+
+[case testGenericClassUnrelatedVars]
+from typing import TypeVar, Generic
+
+T = TypeVar('T')
+T2 = TypeVar('T2')
+
+class C(Generic[T]):
+    @classmethod
+    def f(cls, x: T) -> T:
+        return x
+
+    @classmethod
+    def g(cls, x: T2) -> T2:
+        cls.f(x)  # E: Argument 1 to "f" of "C" has incompatible type "T2"; expected "T"
+        return x
+[builtins fixtures/classmethod.pyi]
+
+[case testGenericClassInGenericFunction]
+from typing import TypeVar, Generic
+
+T = TypeVar('T')
+
+class C(Generic[T]):
+    def __init__(self, item: T) -> None: ...
+    @classmethod
+    def f(cls, x: T) -> T:
+        return x
+
+def foo(x: T, y: int) -> T:
+    C(y)  # OK
+    C[T](y)  # E: Argument 1 to "C" has incompatible type "int"; expected "T"
+    C[T].f(y)  # E: Argument 1 to "f" of "C" has incompatible type "int"; expected "T"
+    return x
+[builtins fixtures/classmethod.pyi]
+
+[case testGenericClassDirectCall]
+from typing import TypeVar, Generic
+
+T = TypeVar('T')
+
+class C(Generic[T]):
+    def __init__(self, item: T) -> None: ...
+    @classmethod
+    def f(cls) -> None:
+        cls(1)  # E: Argument 1 to "C" has incompatible type "int"; expected "T"
+[builtins fixtures/classmethod.pyi]
+
+[case testGenericClassAlternativeConstructorPrecise]
+from typing import Generic, TypeVar, Type, Tuple, Any
+
+T = TypeVar('T')
+
+class Base(Generic[T]):
+    Q = TypeVar('Q', bound=Base[T])
+
+    def __init__(self, item: T) -> None: ...
+
+    @classmethod
+    def make_pair(cls: Type[Q], item: T) -> Tuple[Q, Q]:
+        if bool():
+            return cls(0), cls(0)  # E: Argument 1 to "Base" has incompatible type "int"; expected "T"
+        return cls(item), cls(item)
+[builtins fixtures/classmethod.pyi]
+
 [case testGenericClassMethodUnboundOnClassNonMatchingIdNonGeneric]
 from typing import Generic, TypeVar, Any, Tuple, Type
 
@@ -1944,7 +2027,7 @@ class B(A[T], Generic[T, S]):
         reveal_type(A[T].foo)  # N: Revealed type is 'def () -> Tuple[T`1, __main__.A*[T`1]]'
     @classmethod
     def other(cls) -> None:
-        reveal_type(cls.foo)  # N: Revealed type is 'def [T, S] () -> Tuple[T`1, __main__.B*[T`1, S`2]]'
+        reveal_type(cls.foo)  # N: Revealed type is 'def () -> Tuple[T`1, __main__.B*[T`1, S`2]]'
 reveal_type(B.foo)  # N: Revealed type is 'def [T, S] () -> Tuple[T`1, __main__.B*[T`1, S`2]]'
 [builtins fixtures/classmethod.pyi]
 


Code review comments:
diff_hunk: @@ -484,9 +484,12 @@ class A(Generic[T]):
   @classmethod
   def foo(cls) -> None:
       reveal_type(cls)  # N: Revealed type is 'Type[__main__.A[T`1]]'
-      reveal_type(cls(1))  # N: Revealed type is '__main__.A[builtins.int*]'
-      reveal_type(cls('wooooo'))  # N: Revealed type is '__main__.A[builtins.str*]'
+      reveal_type(cls(cls.x))  # N: Revealed type is '__main__.A[T`1]'
path: test-data/unit/check-dataclasses.test
text: @user1:
I think that this should actually be an error, since we refer to a generic attribute using the type object.

@author:
There is exclusion to specifically support a common pattern like this:
```python
class Base(Generic[T]):
    impl_class: Type[T]  # to be defined on subclasses
    @user2
    def make(cls) -> T:
        return cls.impl_class()
```
IIUC you want to prohibit this:
```python
class C(Generic[T]):
    def __init__(self, x: T) -> None:
        self.x = x
    @user2
    def meth(cls) -> None:
        cls.x  # This should be banned as ambiguous
```
If yes, then I can try this (although may require a bit of special-casing for dataclasses where the variable appears in class body).

@user1:
Yeah, the first example looks reasonable, while the second one is not. Not sure what's the best way to deal with it.

@author:
OK, I added the check for this an no tests failed, if people will complain about this it will be easy to revert, the change is very simple.