From 9a676bbeb778138dd57f0293fa8b432a6510a462 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 13 Jan 2026 16:55:46 -0500 Subject: [PATCH] [ty] Add diagnostic to catch generic enums (#22482) ## Summary Closes https://github.com/astral-sh/ty/issues/2416. --------- Co-authored-by: Alex Waygood --- crates/ty/docs/rules.md | 207 +++++++++++------- .../resources/mdtest/enums.md | 102 +++++++++ .../src/types/diagnostic.rs | 43 ++++ .../src/types/infer/builder.rs | 51 +++-- ty.schema.json | 10 + 5 files changed, 315 insertions(+), 98 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index d1173e0013..4f79aafd9f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -80,7 +80,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -135,7 +135,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -167,7 +167,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -198,7 +198,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -230,7 +230,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -262,7 +262,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -290,7 +290,7 @@ type B = A Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -346,7 +346,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -373,7 +373,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -529,7 +529,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -559,7 +559,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -674,7 +674,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -729,7 +729,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -763,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -799,7 +799,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -850,7 +850,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -879,7 +879,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -923,7 +923,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -965,7 +965,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1041,6 +1041,55 @@ class D(Generic[U, T]): ... - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) +## `invalid-generic-enum` + + +Default level: error · +Added in 0.0.12 · +Related issues · +View source + + + +**What it does** + +Checks for enum classes that are also generic. + +**Why is this bad?** + +Enum classes cannot be generic. Python does not support generic enums: +attempting to create one will either result in an immediate `TypeError` +at runtime, or will create a class that cannot be specialized in the way +that a normal generic class can. + +**Examples** + +```python +from enum import Enum +from typing import Generic, TypeVar + +T = TypeVar("T") + +# error: enum class cannot be generic (class creation fails with `TypeError`) +class E[T](Enum): + A = 1 + +# error: enum class cannot be generic (class creation fails with `TypeError`) +class F(Enum, Generic[T]): + A = 1 + +# error: enum class cannot be generic -- the class creation does not immediately fail... +class G(Generic[T], Enum): + A = 1 + +# ...but this raises `KeyError`: +x: G[int] +``` + +**References** + +- [Python documentation: Enum](https://docs.python.org/3/library/enum.html) + ## `invalid-ignore-comment` @@ -1077,7 +1126,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1116,7 +1165,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1151,7 +1200,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1185,7 +1234,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1292,7 +1341,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1346,7 +1395,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1376,7 +1425,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1426,7 +1475,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1452,7 +1501,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1483,7 +1532,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1517,7 +1566,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1566,7 +1615,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1591,7 +1640,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1687,7 +1736,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -1733,7 +1782,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1760,7 +1809,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1807,7 +1856,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1837,7 +1886,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1867,7 +1916,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1901,7 +1950,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1935,7 +1984,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1970,7 +2019,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2001,7 +2050,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2026,7 +2075,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2059,7 +2108,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2088,7 +2137,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2114,7 +2163,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2138,7 +2187,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2171,7 +2220,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2198,7 +2247,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2225,7 +2274,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2253,7 +2302,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2285,7 +2334,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2322,7 +2371,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2386,7 +2435,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2413,7 +2462,7 @@ cast(int, f()) # Redundant Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2443,7 +2492,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2472,7 +2521,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -2506,7 +2555,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2533,7 +2582,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2561,7 +2610,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2607,7 +2656,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2631,7 +2680,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2658,7 +2707,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2686,7 +2735,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2744,7 +2793,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2769,7 +2818,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2794,7 +2843,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2833,7 +2882,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2870,7 +2919,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Preview (since 1.0.0) · Related issues · -View source +View source @@ -2911,7 +2960,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2975,7 +3024,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3038,7 +3087,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 8809f41350..c3b1e55c53 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -1016,6 +1016,108 @@ class Color(Enum): reveal_type(Color.RED != Color.RED) # revealed: bool ``` +## Generic enums are invalid + +Enum classes cannot be generic. Python does not support generic enums, and attempting to create one +will result in a `TypeError` at runtime. + +### PEP 695 syntax + +Using PEP 695 type parameters on an enum is invalid: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from enum import Enum + +# error: [invalid-generic-enum] "Enum class `E` cannot be generic" +class E[T](Enum): + A = 1 + B = 2 +``` + +### Legacy `Generic` base class + +Inheriting from both `Enum` and `Generic[T]` is also invalid: + +```py +from enum import Enum +from typing import Generic, TypeVar + +T = TypeVar("T") + +# error: [invalid-generic-enum] "Enum class `F` cannot be generic" +class F(Enum, Generic[T]): + A = 1 + B = 2 +``` + +### Swapped order (`Generic` first) + +The order of bases doesn't matter; it's still invalid: + +```py +from enum import Enum +from typing import Generic, TypeVar + +T = TypeVar("T") + +# error: [invalid-generic-enum] "Enum class `G` cannot be generic" +class G(Generic[T], Enum): + A = 1 + B = 2 +``` + +### Enum subclasses + +Subclasses of enum base classes also cannot be generic: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from enum import Enum, IntEnum +from typing import Generic, TypeVar + +T = TypeVar("T") + +# error: [invalid-generic-enum] "Enum class `MyIntEnum` cannot be generic" +class MyIntEnum[T](IntEnum): + A = 1 + +# error: [invalid-generic-enum] "Enum class `MyFlagEnum` cannot be generic" +class MyFlagEnum(IntEnum, Generic[T]): + A = 1 +``` + +### Custom enum base class + +Even with custom enum subclasses that don't have members, they cannot be made generic: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from enum import Enum +from typing import Generic, TypeVar + +T = TypeVar("T") + +class MyEnumBase(Enum): + def some_method(self) -> None: ... + +# error: [invalid-generic-enum] "Enum class `MyEnum` cannot be generic" +class MyEnum[T](MyEnumBase): + A = 1 +``` + ## References - Typing spec: diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 36302200bf..e1bed92bc3 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -73,6 +73,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_CONTEXT_MANAGER); registry.register_lint(&INVALID_DECLARATION); registry.register_lint(&INVALID_EXCEPTION_CAUGHT); + registry.register_lint(&INVALID_GENERIC_ENUM); registry.register_lint(&INVALID_GENERIC_CLASS); registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE); registry.register_lint(&INVALID_PARAMSPEC); @@ -956,6 +957,48 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for enum classes that are also generic. + /// + /// ## Why is this bad? + /// Enum classes cannot be generic. Python does not support generic enums: + /// attempting to create one will either result in an immediate `TypeError` + /// at runtime, or will create a class that cannot be specialized in the way + /// that a normal generic class can. + /// + /// ## Examples + /// ```python + /// from enum import Enum + /// from typing import Generic, TypeVar + /// + /// T = TypeVar("T") + /// + /// # error: enum class cannot be generic (class creation fails with `TypeError`) + /// class E[T](Enum): + /// A = 1 + /// + /// # error: enum class cannot be generic (class creation fails with `TypeError`) + /// class F(Enum, Generic[T]): + /// A = 1 + /// + /// # error: enum class cannot be generic -- the class creation does not immediately fail... + /// class G(Generic[T], Enum): + /// A = 1 + /// + /// # ...but this raises `KeyError`: + /// x: G[int] + /// ``` + /// + /// ## References + /// - [Python documentation: Enum](https://docs.python.org/3/library/enum.html) + pub(crate) static INVALID_GENERIC_ENUM = { + summary: "detects generic enum classes", + status: LintStatus::stable("0.0.12"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for the creation of invalid generic classes diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 1d2300fb73..214584521c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -65,15 +65,15 @@ use crate::types::diagnostic::{ CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, - INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, - INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, - INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, - INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, - NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, - POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, - UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, - UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, - hint_if_stdlib_attribute_exists_on_other_versions, + INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, + INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, + INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, + INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, + IncompatibleBases, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, + POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, + TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, + UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, + USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict, @@ -92,6 +92,7 @@ use crate::types::diagnostic::{ report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, report_unsupported_binary_operation, report_unsupported_comparison, }; +use crate::types::enums::is_enum_class_by_inheritance; use crate::types::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, is_implicit_classmethod, is_implicit_staticmethod, @@ -636,10 +637,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } + // (2) Check that the class is not an enum and generic + if is_enum_class_by_inheritance(self.db(), class) + && class.generic_context(self.db()).is_some() + { + if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_ENUM, class_node) { + builder.into_diagnostic(format_args!( + "Enum class `{}` cannot be generic", + class.name(self.db()) + )); + } + } + let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class.into(), None); - // (2) If it's a `NamedTuple` class, check that no field without a default value + // (3) If it's a `NamedTuple` class, check that no field without a default value // appears after a field with a default value. if is_named_tuple { let mut field_with_default_encountered = None; @@ -680,7 +693,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut disjoint_bases = IncompatibleBases::default(); - // (3) Iterate through the class's explicit bases to check for various possible errors: + // (4) Iterate through the class's explicit bases to check for various possible errors: // - Check for inheritance from plain `Generic`, // - Check for inheritance from a `@final` classes // - If the class is a protocol class: check for inheritance from a non-protocol class @@ -794,7 +807,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (4) Check that the class's MRO is resolvable + // (5) Check that the class's MRO is resolvable match class.try_mro(self.db(), None) { Err(mro_error) => match mro_error.reason() { StaticMroErrorKind::DuplicateBases(duplicates) => { @@ -865,7 +878,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (5) Check that @total_ordering has a valid ordering method in the MRO + // (6) Check that @total_ordering has a valid ordering method in the MRO if class.total_ordering(self.db()) && !class.has_ordering_method_in_mro(self.db(), None) { // Find the @total_ordering decorator to report the diagnostic at its location @@ -884,7 +897,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (6) Check that the class's metaclass can be determined without error. + // (7) Check that the class's metaclass can be determined without error. if let Err(metaclass_error) = class.try_metaclass(self.db()) { match metaclass_error.reason() { MetaclassErrorKind::Cycle => { @@ -960,7 +973,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (7) Check that the class arguments matches the arguments of the + // (8) Check that the class arguments matches the arguments of the // base class `__init_subclass__` method. if let Some(args) = class_node.arguments.as_deref() { let call_args: CallArguments = args @@ -1000,7 +1013,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (8) If the class is generic, verify that its generic context does not violate any of + // (9) If the class is generic, verify that its generic context does not violate any of // the typevar scoping rules. if let (Some(legacy), Some(inherited)) = ( class.legacy_generic_context(self.db()), @@ -1079,7 +1092,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (9) Check that a dataclass does not have more than one `KW_ONLY`. + // (10) Check that a dataclass does not have more than one `KW_ONLY`. if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) = CodeGeneratorKind::from_class(self.db(), class.into(), None) { @@ -1114,7 +1127,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // (10) Check for violations of the Liskov Substitution Principle, + // (11) Check for violations of the Liskov Substitution Principle, // and for violations of other rules relating to invalid overrides of some sort. overrides::check_class(&self.context, class); @@ -1122,7 +1135,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { protocol.validate_members(&self.context); } - // (11) If it's a `TypedDict` class, check that it doesn't include any invalid + // (12) If it's a `TypedDict` class, check that it doesn't include any invalid // statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax // // The body of the class definition defines the items of the `TypedDict` type. It diff --git a/ty.schema.json b/ty.schema.json index acdf95b7ef..969aeb31a9 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -640,6 +640,16 @@ } ] }, + "invalid-generic-enum": { + "title": "detects generic enum classes", + "description": "## What it does\nChecks for enum classes that are also generic.\n\n## Why is this bad?\nEnum classes cannot be generic. Python does not support generic enums:\nattempting to create one will either result in an immediate `TypeError`\nat runtime, or will create a class that cannot be specialized in the way\nthat a normal generic class can.\n\n## Examples\n```python\nfrom enum import Enum\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\n\n# error: enum class cannot be generic (class creation fails with `TypeError`)\nclass E[T](Enum):\n A = 1\n\n# error: enum class cannot be generic (class creation fails with `TypeError`)\nclass F(Enum, Generic[T]):\n A = 1\n\n# error: enum class cannot be generic -- the class creation does not immediately fail...\nclass G(Generic[T], Enum):\n A = 1\n\n# ...but this raises `KeyError`:\nx: G[int]\n```\n\n## References\n- [Python documentation: Enum](https://docs.python.org/3/library/enum.html)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-ignore-comment": { "title": "detects ignore comments that use invalid syntax", "description": "## What it does\nChecks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```",