From a1e88c31a0cfc4e9e2d1faeb1fce53f057cb3040 Mon Sep 17 00:00:00 2001 From: 11happy Date: Thu, 4 Dec 2025 10:14:26 +0000 Subject: [PATCH 01/14] feat: implement error when invalid typevar order Signed-off-by: 11happy --- .../diagnostics/invalid_type_param_order.md | 24 ++++++++ ...ram_O\342\200\246_(8ff6f101710809cb).snap" | 56 +++++++++++++++++++ .../src/types/diagnostic.rs | 43 ++++++++++++++ .../src/types/infer/builder.rs | 20 ++++++- ty.schema.json | 10 ++++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md new file mode 100644 index 0000000000000..1f3d5cd5bb1e8 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md @@ -0,0 +1,24 @@ +# Invalid Type Param Order + + + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import TypeVar, Generic + +T1 = TypeVar("T1", default=int) +T2 = TypeVar("T2") +T3 = TypeVar("T3") + +# error [invalid-type-param-order] +class Foo(Generic[T1, T2]): + pass + +# error [invalid-type-param-order] +class Bar(Generic[T2, T1, T3]): + pass +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" new file mode 100644 index 0000000000000..399f0d0a1872f --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" @@ -0,0 +1,56 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_type_param_order.md - Invalid Type Param Order +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar, Generic + 2 | + 3 | T1 = TypeVar("T1", default=int) + 4 | T2 = TypeVar("T2") + 5 | T3 = TypeVar("T3") + 6 | + 7 | # error [invalid-type-param-order] + 8 | class Foo(Generic[T1, T2]): + 9 | pass +10 | +11 | # error [invalid-type-param-order] +12 | class Bar(Generic[T2, T1, T3]): +13 | pass +``` + +# Diagnostics + +``` +error[invalid-type-param-order]: Type parameter T2 without a default follows type parameter with a default + --> src/mdtest_snippet.py:8:7 + | +7 | # error [invalid-type-param-order] +8 | class Foo(Generic[T1, T2]): + | ^^^^^^^^^^^^^^^^^^^^ +9 | pass + | +info: rule `invalid-type-param-order` is enabled by default + +``` + +``` +error[invalid-type-param-order]: Type parameter T3 without a default follows type parameter with a default + --> src/mdtest_snippet.py:12:7 + | +11 | # error [invalid-type-param-order] +12 | class Bar(Generic[T2, T1, T3]): + | ^^^^^^^^^^^^^^^^^^^^^^^^ +13 | pass + | +info: rule `invalid-type-param-order` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 3acd7b0a64191..0f28ff05dcf16 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -89,6 +89,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_TYPE_FORM); registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION); registry.register_lint(&INVALID_TYPE_GUARD_CALL); + registry.register_lint(&INVALID_TYPE_PARAM_ORDER); registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); registry.register_lint(&MISSING_ARGUMENT); registry.register_lint(&NO_MATCHING_OVERLOAD); @@ -987,6 +988,34 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for type parameters without defaults that come after type parameters with defaults. + /// + /// ## Why is this bad? + /// Type parameters without defaults must come before type parameters with defaults. + /// + /// ## Example + /// + /// ```python + /// from typing import Generic, TypeVar + /// + /// T = TypeVar("T") + /// U = TypeVar("U") + /// # Error: T has no default but comes after U which has a default + /// class Foo(Generic[U = int, T]): ... + /// ``` + /// + /// ## References + /// - [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/) + + pub(crate) static INVALID_TYPE_PARAM_ORDER = { + summary: "detects invalid type parameter order", + status: LintStatus::stable("0.0.1-alpha.1"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for the creation of invalid `NewType`s @@ -3695,6 +3724,20 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>( } } +pub(crate) fn report_invalid_type_param_order<'db>( + context: &InferContext<'db, '_>, + class: ClassLiteral<'db>, + name: &str, +) { + if let Some(builder) = + context.report_lint(&INVALID_TYPE_PARAM_ORDER, class.header_range(context.db())) + { + builder.into_diagnostic(format_args!( + "Type parameter {name} without a default follows type parameter with a default", + )); + }; +} + pub(crate) fn report_rebound_typevar<'db>( context: &InferContext<'db, '_>, typevar_name: &ast::name::Name, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9ae325a9ae8fa..261aa2dc31680 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -78,7 +78,7 @@ use crate::types::diagnostic::{ report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, - report_named_tuple_field_with_leading_underscore, + report_named_tuple_field_with_leading_underscore, report_invalid_type_param_order, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, @@ -949,6 +949,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + let type_vars = class.typevars_referenced_in_definition(self.db()); + let mut seen_default = false; + for type_var in type_vars { + let has_default = type_var + .typevar(self.db()) + .default_type(self.db()) + .is_some(); + if seen_default && !has_default { + report_invalid_type_param_order( + &self.context, + class, + type_var.typevar(self.db()).name(self.db()).as_str(), + ); + } + if has_default { + seen_default = true; + } + } let scope = class.body_scope(self.db()).scope(self.db()); if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) && let Some(parent) = scope.parent() diff --git a/ty.schema.json b/ty.schema.json index 87feeb250775c..b228c1a8527a9 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -813,6 +813,16 @@ } ] }, + "invalid-type-param-order": { + "title": "detects invalid type parameter order", + "description": "## What it does\nChecks for type parameters without defaults that come after type parameters with defaults.\n\n## Why is this bad?\nType parameters without defaults must come before type parameters with defaults.\n\n## Example\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\")\n# Error: T has no default but comes after U which has a default\nclass Foo(Generic[U = int, T]): ...\n```\n\n## References\n- [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-type-variable-constraints": { "title": "detects invalid type variable constraints", "description": "## What it does\nChecks for constrained [type variables] with only one constraint.\n\n## Why is this bad?\nA constrained type variable must have at least two constraints.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar('T', str) # invalid constrained TypeVar\n```\n\nUse instead:\n```python\nT = TypeVar('T', str, int) # valid constrained TypeVar\n# or\nT = TypeVar('T', bound=str) # valid bound TypeVar\n```\n\n[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar", From 9aeb711f8dedee51490e6ffd785179b1fc2fb719 Mon Sep 17 00:00:00 2001 From: 11happy Date: Thu, 4 Dec 2025 10:21:43 +0000 Subject: [PATCH 02/14] style: fix clippy and fmt error Signed-off-by: 11happy --- crates/ty_python_semantic/src/types/diagnostic.rs | 2 +- crates/ty_python_semantic/src/types/infer/builder.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 0f28ff05dcf16..5af77348bee74 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -3735,7 +3735,7 @@ pub(crate) fn report_invalid_type_param_order<'db>( builder.into_diagnostic(format_args!( "Type parameter {name} without a default follows type parameter with a default", )); - }; + } } pub(crate) fn report_rebound_typevar<'db>( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 261aa2dc31680..88153dee804f1 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -78,7 +78,8 @@ use crate::types::diagnostic::{ report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, - report_named_tuple_field_with_leading_underscore, report_invalid_type_param_order, + report_invalid_type_param_order, + report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, From b7972df97cfa68a0c48f63b038cad1abbd4a6815 Mon Sep 17 00:00:00 2001 From: 11happy Date: Thu, 4 Dec 2025 10:33:46 +0000 Subject: [PATCH 03/14] test: add debugsnap & remove clippy error Signed-off-by: 11happy --- .../tests/e2e/snapshots/e2e__commands__debug_command.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index cf1b0afc645a6..5ab74d2a9df1b 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -78,6 +78,7 @@ Settings: Settings { "invalid-type-form": Error (Default), "invalid-type-guard-call": Error (Default), "invalid-type-guard-definition": Error (Default), + "invalid-type-param-order": Error (Default), "invalid-type-variable-constraints": Error (Default), "missing-argument": Error (Default), "missing-typed-dict-key": Error (Default), From 15469f8070234536a35c2cb358372efc6a608807 Mon Sep 17 00:00:00 2001 From: 11happy Date: Thu, 4 Dec 2025 10:41:55 +0000 Subject: [PATCH 04/14] update md files Signed-off-by: 11happy --- crates/ty/docs/rules.md | 182 ++++++++++++++++++++++++---------------- ty.schema.json | 2 +- 2 files changed, 109 insertions(+), 75 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index c2b450ae10bd3..852b06a2018fe 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,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 @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,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 @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -837,7 +837,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -870,7 +870,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -909,7 +909,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -944,7 +944,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -978,7 +978,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1085,7 +1085,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 @@ -1139,7 +1139,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1169,7 +1169,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 @@ -1219,7 +1219,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1245,7 +1245,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1276,7 +1276,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 @@ -1310,7 +1310,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 @@ -1359,7 +1359,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1384,7 +1384,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1442,7 +1442,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1469,7 +1469,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 @@ -1516,7 +1516,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1546,7 +1546,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1576,7 +1576,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 @@ -1610,7 +1610,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1638,13 +1638,47 @@ class C: def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self` ``` +## `invalid-type-param-order` + + +Default level: error · +Added in 0.0.1-alpha.1 · +Related issues · +View source + + + +**What it does** + +Checks for type parameters without defaults that come after type parameters with defaults. + +**Why is this bad?** + +Type parameters without defaults must come before type parameters with defaults. + +**Example** + + +```python +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") +# Error: T has no default but comes after U which has a default +class Foo(Generic[U = int, T]): ... +``` + +**References** + +- [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/) + ## `invalid-type-variable-constraints` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1679,7 +1713,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1704,7 +1738,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 @@ -1737,7 +1771,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1766,7 +1800,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1790,7 +1824,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1816,7 +1850,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1849,7 +1883,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1876,7 +1910,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1934,7 +1968,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1964,7 +1998,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 @@ -1993,7 +2027,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -2027,7 +2061,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2054,7 +2088,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2082,7 +2116,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2128,7 +2162,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2155,7 +2189,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 @@ -2183,7 +2217,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2208,7 +2242,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2233,7 +2267,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2270,7 +2304,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2298,7 +2332,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2323,7 +2357,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2364,7 +2398,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2452,7 +2486,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2480,7 +2514,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 @@ -2512,7 +2546,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2544,7 +2578,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2571,7 +2605,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2595,7 +2629,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2653,7 +2687,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2692,7 +2726,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2755,7 +2789,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2779,7 +2813,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/ty.schema.json b/ty.schema.json index b228c1a8527a9..755e14d6d43a1 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -815,7 +815,7 @@ }, "invalid-type-param-order": { "title": "detects invalid type parameter order", - "description": "## What it does\nChecks for type parameters without defaults that come after type parameters with defaults.\n\n## Why is this bad?\nType parameters without defaults must come before type parameters with defaults.\n\n## Example\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\")\n# Error: T has no default but comes after U which has a default\nclass Foo(Generic[U = int, T]): ...\n```\n\n## References\n- [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/)", + "description": "## What it does\nChecks for type parameters without defaults that come after type parameters with defaults.\n\n## Why is this bad?\nType parameters without defaults must come before type parameters with defaults.\n\n## Example\n\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\")\n# Error: T has no default but comes after U which has a default\nclass Foo(Generic[U = int, T]): ...\n```\n\n## References\n- [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/)", "default": "error", "oneOf": [ { From b49c1a8252c48ee9efe7908ca075e87dca1c6529 Mon Sep 17 00:00:00 2001 From: 11happy Date: Thu, 4 Dec 2025 10:55:00 +0000 Subject: [PATCH 05/14] update snap Signed-off-by: 11happy --- .../diagnostics/invalid_type_param_order.md | 6 ++-- ...ram_O\342\200\246_(8ff6f101710809cb).snap" | 30 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md index 1f3d5cd5bb1e8..d48dd684a5596 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md @@ -14,11 +14,9 @@ T1 = TypeVar("T1", default=int) T2 = TypeVar("T2") T3 = TypeVar("T3") -# error [invalid-type-param-order] -class Foo(Generic[T1, T2]): +class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] pass -# error [invalid-type-param-order] -class Bar(Generic[T2, T1, T3]): +class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] pass ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" index 399f0d0a1872f..51ef43c454e5c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" @@ -18,25 +18,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type 4 | T2 = TypeVar("T2") 5 | T3 = TypeVar("T3") 6 | - 7 | # error [invalid-type-param-order] - 8 | class Foo(Generic[T1, T2]): - 9 | pass -10 | -11 | # error [invalid-type-param-order] -12 | class Bar(Generic[T2, T1, T3]): -13 | pass + 7 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] + 8 | pass + 9 | +10 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] +11 | pass ``` # Diagnostics ``` error[invalid-type-param-order]: Type parameter T2 without a default follows type parameter with a default - --> src/mdtest_snippet.py:8:7 + --> src/mdtest_snippet.py:7:7 | -7 | # error [invalid-type-param-order] -8 | class Foo(Generic[T1, T2]): +5 | T3 = TypeVar("T3") +6 | +7 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] | ^^^^^^^^^^^^^^^^^^^^ -9 | pass +8 | pass | info: rule `invalid-type-param-order` is enabled by default @@ -44,12 +43,13 @@ info: rule `invalid-type-param-order` is enabled by default ``` error[invalid-type-param-order]: Type parameter T3 without a default follows type parameter with a default - --> src/mdtest_snippet.py:12:7 + --> src/mdtest_snippet.py:10:7 | -11 | # error [invalid-type-param-order] -12 | class Bar(Generic[T2, T1, T3]): + 8 | pass + 9 | +10 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] | ^^^^^^^^^^^^^^^^^^^^^^^^ -13 | pass +11 | pass | info: rule `invalid-type-param-order` is enabled by default From 5c19faaec040e95f109fc424d9c0422b10be481c Mon Sep 17 00:00:00 2001 From: 11happy Date: Thu, 4 Dec 2025 12:01:58 +0000 Subject: [PATCH 06/14] chore: minor fix Signed-off-by: 11happy --- .../src/types/diagnostic.rs | 1 - .../src/types/infer/builder.rs | 48 ++++++++++--------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 5af77348bee74..9fc714105778b 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1008,7 +1008,6 @@ declare_lint! { /// /// ## References /// - [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/) - pub(crate) static INVALID_TYPE_PARAM_ORDER = { summary: "detects invalid type parameter order", status: LintStatus::stable("0.0.1-alpha.1"), diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 88153dee804f1..022bd2f82cb0c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -56,13 +56,13 @@ use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, Meth use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; use crate::types::diagnostic::{ - self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, - CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, 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, + self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, + CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, 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_PARAM_ORDER, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, @@ -950,22 +950,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - let type_vars = class.typevars_referenced_in_definition(self.db()); - let mut seen_default = false; - for type_var in type_vars { - let has_default = type_var - .typevar(self.db()) - .default_type(self.db()) - .is_some(); - if seen_default && !has_default { - report_invalid_type_param_order( - &self.context, - class, - type_var.typevar(self.db()).name(self.db()).as_str(), - ); - } - if has_default { - seen_default = true; + if self.context.is_lint_enabled(&INVALID_TYPE_PARAM_ORDER) { + let type_vars = class.typevars_referenced_in_definition(self.db()); + let mut seen_default = false; + for type_var in type_vars { + let has_default = type_var + .typevar(self.db()) + .default_type(self.db()) + .is_some(); + if seen_default && !has_default { + report_invalid_type_param_order( + &self.context, + class, + type_var.typevar(self.db()).name(self.db()).as_str(), + ); + } + if has_default { + seen_default = true; + } } } let scope = class.body_scope(self.db()).scope(self.db()); From 5af1ac3b35873dfc81e2d0c08bc49f8d9613bbba Mon Sep 17 00:00:00 2001 From: 11happy Date: Thu, 4 Dec 2025 12:07:45 +0000 Subject: [PATCH 07/14] update rules Signed-off-by: 11happy --- crates/ty/docs/rules.md | 86 ++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 852b06a2018fe..fbfe6b23289e4 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -944,7 +944,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -978,7 +978,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1139,7 +1139,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1169,7 +1169,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 @@ -1219,7 +1219,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1310,7 +1310,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 @@ -1384,7 +1384,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1469,7 +1469,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 @@ -1516,7 +1516,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1546,7 +1546,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1576,7 +1576,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 @@ -1610,7 +1610,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1678,7 +1678,7 @@ class Foo(Generic[U = int, T]): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1713,7 +1713,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1738,7 +1738,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 @@ -1771,7 +1771,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1800,7 +1800,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1824,7 +1824,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1850,7 +1850,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1883,7 +1883,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1910,7 +1910,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1968,7 +1968,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1998,7 +1998,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 @@ -2027,7 +2027,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -2061,7 +2061,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2088,7 +2088,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2116,7 +2116,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2162,7 +2162,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2189,7 +2189,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 @@ -2217,7 +2217,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2242,7 +2242,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2267,7 +2267,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2304,7 +2304,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2332,7 +2332,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2486,7 +2486,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2546,7 +2546,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2578,7 +2578,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2605,7 +2605,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2629,7 +2629,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2726,7 +2726,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2813,7 +2813,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source From 7948d895f69fa92ceee1f098e562338be1fc45a3 Mon Sep 17 00:00:00 2001 From: 11happy Date: Wed, 10 Dec 2025 07:04:18 +0000 Subject: [PATCH 08/14] check only for generic context Signed-off-by: 11happy --- .../diagnostics/invalid_type_param_order.md | 7 +++ ...ram_O\342\200\246_(8ff6f101710809cb).snap" | 45 +++++++++++-------- .../src/types/infer/builder.rs | 33 +++++++------- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md index d48dd684a5596..2b5d956f9b810 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md @@ -13,6 +13,13 @@ from typing import TypeVar, Generic T1 = TypeVar("T1", default=int) T2 = TypeVar("T2") T3 = TypeVar("T3") +DefaultStrT = TypeVar("DefaultStrT", default=str) + +class SubclassMe(Generic[T1, DefaultStrT]): + x: DefaultStrT + +class Baz(SubclassMe[int, DefaultStrT]): + pass class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] pass diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" index 51ef43c454e5c..74cc600ed1d66 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" @@ -17,39 +17,46 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type 3 | T1 = TypeVar("T1", default=int) 4 | T2 = TypeVar("T2") 5 | T3 = TypeVar("T3") - 6 | - 7 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] - 8 | pass - 9 | -10 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] -11 | pass + 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) + 7 | + 8 | class SubclassMe(Generic[T1, DefaultStrT]): + 9 | x: DefaultStrT +10 | +11 | class Baz(SubclassMe[int, DefaultStrT]): +12 | pass +13 | +14 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] +15 | pass +16 | +17 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] +18 | pass ``` # Diagnostics ``` error[invalid-type-param-order]: Type parameter T2 without a default follows type parameter with a default - --> src/mdtest_snippet.py:7:7 - | -5 | T3 = TypeVar("T3") -6 | -7 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] - | ^^^^^^^^^^^^^^^^^^^^ -8 | pass - | + --> src/mdtest_snippet.py:14:7 + | +12 | pass +13 | +14 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] + | ^^^^^^^^^^^^^^^^^^^^ +15 | pass + | info: rule `invalid-type-param-order` is enabled by default ``` ``` error[invalid-type-param-order]: Type parameter T3 without a default follows type parameter with a default - --> src/mdtest_snippet.py:10:7 + --> src/mdtest_snippet.py:17:7 | - 8 | pass - 9 | -10 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] +15 | pass +16 | +17 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] | ^^^^^^^^^^^^^^^^^^^^^^^^ -11 | pass +18 | pass | info: rule `invalid-type-param-order` is enabled by default diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 022bd2f82cb0c..fc0712d67587e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -951,22 +951,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if self.context.is_lint_enabled(&INVALID_TYPE_PARAM_ORDER) { - let type_vars = class.typevars_referenced_in_definition(self.db()); - let mut seen_default = false; - for type_var in type_vars { - let has_default = type_var - .typevar(self.db()) - .default_type(self.db()) - .is_some(); - if seen_default && !has_default { - report_invalid_type_param_order( - &self.context, - class, - type_var.typevar(self.db()).name(self.db()).as_str(), - ); - } - if has_default { - seen_default = true; + if let Some(generic_context) = class.generic_context(self.db()) { + let mut seen_default = false; + + for bound_typevar in generic_context.variables(self.db()) { + let typevar = bound_typevar.typevar(self.db()); + let has_default = typevar.default_type(self.db()).is_some(); + + if seen_default && !has_default { + report_invalid_type_param_order( + &self.context, + class, + typevar.name(self.db()).as_str(), + ); + } + if has_default { + seen_default = true; + } } } } From 783e755fe33b3fa0c549f50adc85d2bcb54a0f16 Mon Sep 17 00:00:00 2001 From: 11happy Date: Wed, 10 Dec 2025 07:09:58 +0000 Subject: [PATCH 09/14] cargo fmt Signed-off-by: 11happy --- .../ty_python_semantic/src/types/infer/builder.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index fc0712d67587e..ef4e562734f8b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -56,13 +56,13 @@ use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, Meth use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; use crate::types::diagnostic::{ - self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, - CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, 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_PARAM_ORDER, + self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, + CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, 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_PARAM_ORDER, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, From 723b5149124aff67e68fb56046adcf5afb8867b9 Mon Sep 17 00:00:00 2001 From: 11happy Date: Wed, 10 Dec 2025 07:25:52 +0000 Subject: [PATCH 10/14] complete TODO Signed-off-by: 11happy --- .../resources/mdtest/generics/legacy/paramspec.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 7a365b64055bf..00f50240e41c5 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -424,9 +424,8 @@ p3 = ParamSpecWithDefault4[[int], [str]]() reveal_type(p3.attr1) # revealed: (int, /) -> None reveal_type(p3.attr2) # revealed: (str, /) -> None -# TODO: error # Un-ordered type variables as the default of `PAnother` is `P` -class ParamSpecWithDefault5(Generic[PAnother, P]): +class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-type-param-order] attr: Callable[PAnother, None] # TODO: error From eb6420c1a4252bf505e7c6119fe915e442cfcce4 Mon Sep 17 00:00:00 2001 From: 11happy Date: Sun, 14 Dec 2025 14:03:01 +0530 Subject: [PATCH 11/14] cargo fmt Signed-off-by: 11happy --- crates/ty_python_semantic/src/types/infer/builder.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ef4e562734f8b..b502f2f474ea3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -78,8 +78,7 @@ use crate::types::diagnostic::{ report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_type_checking_constant, - report_invalid_type_param_order, - report_named_tuple_field_with_leading_underscore, + report_invalid_type_param_order, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, From 2e5599fdc8a7cb4dde26440b1d75c544e0b81bf2 Mon Sep 17 00:00:00 2001 From: 11happy Date: Sun, 14 Dec 2025 14:25:48 +0530 Subject: [PATCH 12/14] update rules Signed-off-by: 11happy --- crates/ty/docs/rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index fbfe6b23289e4..86e1d127a8d7b 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -793,7 +793,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source From 7a8ffed28d797d93e36a66b79f062d154476a4b6 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 14 Dec 2025 19:26:48 +0000 Subject: [PATCH 13/14] several improvements --- crates/ty/docs/rules.md | 193 ++++++++---------- .../diagnostics/invalid_type_param_order.md | 29 --- .../invalid_type_parameter_order.md | 43 ++++ .../mdtest/generics/legacy/paramspec.md | 2 +- ...ram_O\342\200\246_(8ff6f101710809cb).snap" | 63 ------ ...f_Leg\342\200\246_(eaa359e8d6b3031d).snap" | 190 +++++++++++++++++ .../src/types/diagnostic.rs | 121 +++++++---- .../src/types/infer/builder.rs | 80 +++++--- 8 files changed, 449 insertions(+), 272 deletions(-) delete mode 100644 crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md create mode 100644 crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" create mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 86e1d127a8d7b..8b35be52a6e44 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,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 @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,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 @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -793,7 +793,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -837,7 +837,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -848,16 +848,21 @@ Checks for the creation of invalid generic classes **Why is this bad?** There are several requirements that you must follow when defining a generic class. +Many of these result in `TypeError` being raised at runtime if they are violated. **Examples** ```python -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar -T = TypeVar("T") # okay +T = TypeVar("T") +U = TypeVar("U", default=int) # error: class uses both PEP-695 syntax and legacy syntax class C[U](Generic[T]): ... + +# error: type parameter with default comes before type parameter without default +class D(Generic[U, T]): ... ``` **References** @@ -870,7 +875,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -909,7 +914,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -944,7 +949,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -978,7 +983,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1085,7 +1090,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 @@ -1139,7 +1144,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1169,7 +1174,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 @@ -1219,7 +1224,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1245,7 +1250,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1276,7 +1281,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 @@ -1310,7 +1315,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 @@ -1359,7 +1364,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1384,7 +1389,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1442,7 +1447,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1469,7 +1474,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 @@ -1516,7 +1521,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1546,7 +1551,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1576,7 +1581,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 @@ -1610,7 +1615,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1638,47 +1643,13 @@ class C: def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self` ``` -## `invalid-type-param-order` - - -Default level: error · -Added in 0.0.1-alpha.1 · -Related issues · -View source - - - -**What it does** - -Checks for type parameters without defaults that come after type parameters with defaults. - -**Why is this bad?** - -Type parameters without defaults must come before type parameters with defaults. - -**Example** - - -```python -from typing import Generic, TypeVar - -T = TypeVar("T") -U = TypeVar("U") -# Error: T has no default but comes after U which has a default -class Foo(Generic[U = int, T]): ... -``` - -**References** - -- [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/) - ## `invalid-type-variable-constraints` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1713,7 +1684,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1738,7 +1709,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 @@ -1771,7 +1742,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1800,7 +1771,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1824,7 +1795,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1850,7 +1821,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1883,7 +1854,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1910,7 +1881,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1968,7 +1939,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1998,7 +1969,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 @@ -2027,7 +1998,7 @@ class B(A): ... # Error raised here Default level: error · Preview (since 0.0.1-alpha.30) · Related issues · -View source +View source @@ -2061,7 +2032,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2088,7 +2059,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2116,7 +2087,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2162,7 +2133,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2189,7 +2160,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 @@ -2217,7 +2188,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2242,7 +2213,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2267,7 +2238,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2304,7 +2275,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2332,7 +2303,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2357,7 +2328,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2398,7 +2369,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2486,7 +2457,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2514,7 +2485,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 @@ -2546,7 +2517,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2578,7 +2549,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2605,7 +2576,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2629,7 +2600,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2687,7 +2658,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2726,7 +2697,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2789,7 +2760,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2813,7 +2784,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md deleted file mode 100644 index 2b5d956f9b810..0000000000000 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md +++ /dev/null @@ -1,29 +0,0 @@ -# Invalid Type Param Order - - - -```toml -[environment] -python-version = "3.13" -``` - -```py -from typing import TypeVar, Generic - -T1 = TypeVar("T1", default=int) -T2 = TypeVar("T2") -T3 = TypeVar("T3") -DefaultStrT = TypeVar("DefaultStrT", default=str) - -class SubclassMe(Generic[T1, DefaultStrT]): - x: DefaultStrT - -class Baz(SubclassMe[int, DefaultStrT]): - pass - -class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] - pass - -class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] - pass -``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md new file mode 100644 index 0000000000000..705ceae6b678f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md @@ -0,0 +1,43 @@ +# Invalid Order of Legacy Type Parameters + + + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import TypeVar, Generic, Protocol + +T1 = TypeVar("T1", default=int) + +T2 = TypeVar("T2") +T3 = TypeVar("T3") + +DefaultStrT = TypeVar("DefaultStrT", default=str) + +class SubclassMe(Generic[T1, DefaultStrT]): + x: DefaultStrT + +class Baz(SubclassMe[int, DefaultStrT]): + pass + +# error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default" +class Foo(Generic[T1, T2]): + pass + +class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class] + pass + +class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] + pass + +class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] + pass + +class VeryBad( + Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class] + Generic[T1, T2, DefaultStrT, T3], +): ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 00f50240e41c5..5e3cbe38881ed 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -425,7 +425,7 @@ reveal_type(p3.attr1) # revealed: (int, /) -> None reveal_type(p3.attr2) # revealed: (str, /) -> None # Un-ordered type variables as the default of `PAnother` is `P` -class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-type-param-order] +class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-generic-class] attr: Callable[PAnother, None] # TODO: error diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" deleted file mode 100644 index 74cc600ed1d66..0000000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o\342\200\246_-_Invalid_Type_Param_O\342\200\246_(8ff6f101710809cb).snap" +++ /dev/null @@ -1,63 +0,0 @@ ---- -source: crates/ty_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_type_param_order.md - Invalid Type Param Order -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing import TypeVar, Generic - 2 | - 3 | T1 = TypeVar("T1", default=int) - 4 | T2 = TypeVar("T2") - 5 | T3 = TypeVar("T3") - 6 | DefaultStrT = TypeVar("DefaultStrT", default=str) - 7 | - 8 | class SubclassMe(Generic[T1, DefaultStrT]): - 9 | x: DefaultStrT -10 | -11 | class Baz(SubclassMe[int, DefaultStrT]): -12 | pass -13 | -14 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] -15 | pass -16 | -17 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] -18 | pass -``` - -# Diagnostics - -``` -error[invalid-type-param-order]: Type parameter T2 without a default follows type parameter with a default - --> src/mdtest_snippet.py:14:7 - | -12 | pass -13 | -14 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order] - | ^^^^^^^^^^^^^^^^^^^^ -15 | pass - | -info: rule `invalid-type-param-order` is enabled by default - -``` - -``` -error[invalid-type-param-order]: Type parameter T3 without a default follows type parameter with a default - --> src/mdtest_snippet.py:17:7 - | -15 | pass -16 | -17 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order] - | ^^^^^^^^^^^^^^^^^^^^^^^^ -18 | pass - | -info: rule `invalid-type-param-order` is enabled by default - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" new file mode 100644 index 0000000000000..290fbb0ae06f0 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" @@ -0,0 +1,190 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_type_parameter_order.md - Invalid Order of Legacy Type Parameters +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar, Generic, Protocol + 2 | + 3 | T1 = TypeVar("T1", default=int) + 4 | + 5 | T2 = TypeVar("T2") + 6 | T3 = TypeVar("T3") + 7 | + 8 | DefaultStrT = TypeVar("DefaultStrT", default=str) + 9 | +10 | class SubclassMe(Generic[T1, DefaultStrT]): +11 | x: DefaultStrT +12 | +13 | class Baz(SubclassMe[int, DefaultStrT]): +14 | pass +15 | +16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default" +17 | class Foo(Generic[T1, T2]): +18 | pass +19 | +20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class] +21 | pass +22 | +23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] +24 | pass +25 | +26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] +27 | pass +28 | +29 | class VeryBad( +30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class] +31 | Generic[T1, T2, DefaultStrT, T3], +32 | ): ... +``` + +# Diagnostics + +``` +error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:17:19 + | +16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default" +17 | class Foo(Generic[T1, T2]): + | ^^^^^^ + | | + | Type variable `T2` does not have a default + | Earlier TypeVar `T1` does +18 | pass + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar, Generic, Protocol + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | + 5 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 6 | T3 = TypeVar("T3") + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:20:19 + | +18 | pass +19 | +20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class] + | ^^^^^^^^^^ + | | + | Type variable `T3` does not have a default + | Earlier TypeVar `T1` does +21 | pass + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar, Generic, Protocol + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | + 5 | T2 = TypeVar("T2") + 6 | T3 = TypeVar("T3") + | ------------------ `T3` defined here + 7 | + 8 | DefaultStrT = TypeVar("DefaultStrT", default=str) + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:23:20 + | +21 | pass +22 | +23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | | + | Type variables `T2` and `T3` do not have defaults + | Earlier TypeVar `T1` does +24 | pass + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar, Generic, Protocol + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | + 5 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 6 | T3 = TypeVar("T3") + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:26:20 + | +24 | pass +25 | +26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | | + | Type variables `T2` and `T3` do not have defaults + | Earlier TypeVar `T1` does +27 | pass + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar, Generic, Protocol + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | + 5 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 6 | T3 = TypeVar("T3") + | +info: rule `invalid-generic-class` is enabled by default + +``` + +``` +error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults + --> src/mdtest_snippet.py:30:14 + | +29 | class VeryBad( +30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | | + | Type variables `T2` and `T3` do not have defaults + | Earlier TypeVar `T1` does +31 | Generic[T1, T2, DefaultStrT, T3], +32 | ): ... + | + ::: src/mdtest_snippet.py:3:1 + | + 1 | from typing import TypeVar, Generic, Protocol + 2 | + 3 | T1 = TypeVar("T1", default=int) + | ------------------------------- `T1` defined here + 4 | + 5 | T2 = TypeVar("T2") + | ------------------ `T2` defined here + 6 | T3 = TypeVar("T3") + | +info: rule `invalid-generic-class` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 9fc714105778b..e136057d45bea 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -30,7 +30,7 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, protocol_class::ProtocolClass, }; -use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy}; +use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance}; use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint}; use itertools::Itertools; use ruff_db::{ @@ -89,7 +89,6 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_TYPE_FORM); registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION); registry.register_lint(&INVALID_TYPE_GUARD_CALL); - registry.register_lint(&INVALID_TYPE_PARAM_ORDER); registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); registry.register_lint(&MISSING_ARGUMENT); registry.register_lint(&NO_MATCHING_OVERLOAD); @@ -895,15 +894,20 @@ declare_lint! { /// /// ## Why is this bad? /// There are several requirements that you must follow when defining a generic class. + /// Many of these result in `TypeError` being raised at runtime if they are violated. /// /// ## Examples /// ```python - /// from typing import Generic, TypeVar + /// from typing_extensions import Generic, TypeVar /// - /// T = TypeVar("T") # okay + /// T = TypeVar("T") + /// U = TypeVar("U", default=int) /// /// # error: class uses both PEP-695 syntax and legacy syntax /// class C[U](Generic[T]): ... + /// + /// # error: type parameter with default comes before type parameter without default + /// class D(Generic[U, T]): ... /// ``` /// /// ## References @@ -988,33 +992,6 @@ declare_lint! { } } -declare_lint! { - /// ## What it does - /// Checks for type parameters without defaults that come after type parameters with defaults. - /// - /// ## Why is this bad? - /// Type parameters without defaults must come before type parameters with defaults. - /// - /// ## Example - /// - /// ```python - /// from typing import Generic, TypeVar - /// - /// T = TypeVar("T") - /// U = TypeVar("U") - /// # Error: T has no default but comes after U which has a default - /// class Foo(Generic[U = int, T]): ... - /// ``` - /// - /// ## References - /// - [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/) - pub(crate) static INVALID_TYPE_PARAM_ORDER = { - summary: "detects invalid type parameter order", - status: LintStatus::stable("0.0.1-alpha.1"), - default_level: Level::Error, - } -} - declare_lint! { /// ## What it does /// Checks for the creation of invalid `NewType`s @@ -3726,14 +3703,84 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>( pub(crate) fn report_invalid_type_param_order<'db>( context: &InferContext<'db, '_>, class: ClassLiteral<'db>, - name: &str, + node: &ast::StmtClassDef, + typevar_with_default: TypeVarInstance<'db>, + invalid_later_typevars: &[TypeVarInstance<'db>], ) { - if let Some(builder) = - context.report_lint(&INVALID_TYPE_PARAM_ORDER, class.header_range(context.db())) - { - builder.into_diagnostic(format_args!( - "Type parameter {name} without a default follows type parameter with a default", + let db = context.db(); + + let base_index = class + .explicit_bases(db) + .iter() + .position(|base| { + matches!( + base, + Type::KnownInstance( + KnownInstanceType::SubscriptedProtocol(_) + | KnownInstanceType::SubscriptedGeneric(_) + ) + ) + }) + .expect( + "It should not be possible for a class to have a legacy generic context \ + if it does not inherit from `Protocol[]` or `Generic[]`", + ); + + let base_node = &node.bases()[base_index]; + + let primary_diagnostic_range = base_node + .as_subscript_expr() + .map(|subscript| &*subscript.slice) + .unwrap_or(base_node) + .range(); + + let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, primary_diagnostic_range) + else { + return; + }; + + let mut diagnostic = builder.into_diagnostic( + "Type parameters without defaults cannot follow type parameters with defaults", + ); + + diagnostic.set_concise_message(format_args!( + "Type parameter `{}` without a default cannot follow earlier parameter `{}` with a default", + invalid_later_typevars[0].name(db), + typevar_with_default.name(db), + )); + + if let [single_typevar] = invalid_later_typevars { + diagnostic.set_primary_message(format_args!( + "Type variable `{}` does not have a default", + single_typevar.name(db), )); + } else { + let later_typevars = + format_enumeration(invalid_later_typevars.iter().map(|tv| tv.name(db))); + diagnostic.set_primary_message(format_args!( + "Type variables {later_typevars} do not have defaults", + )); + } + + diagnostic.annotate( + Annotation::primary(Span::from(context.file()).with_range(primary_diagnostic_range)) + .message(format_args!( + "Earlier TypeVar `{}` does", + typevar_with_default.name(db) + )), + ); + + for tvar in [typevar_with_default, invalid_later_typevars[0]] { + let Some(definition) = tvar.definition(db) else { + continue; + }; + let file = definition.file(db); + diagnostic.annotate( + Annotation::secondary(Span::from( + definition.full_range(db, &parsed_module(db, file).load(db)), + )) + .message(format_args!("`{}` defined here", tvar.name(db))), + ); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b502f2f474ea3..723e89519c739 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -62,7 +62,7 @@ use crate::types::diagnostic::{ 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_PARAM_ORDER, + INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, @@ -949,44 +949,62 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - if self.context.is_lint_enabled(&INVALID_TYPE_PARAM_ORDER) { - if let Some(generic_context) = class.generic_context(self.db()) { - let mut seen_default = false; + if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) { + if !class.has_pep_695_type_params(self.db()) + && let Some(generic_context) = class.legacy_generic_context(self.db()) + { + struct State<'db> { + typevar_with_default: TypeVarInstance<'db>, + invalid_later_tvars: Vec>, + } + + let mut state: Option> = None; for bound_typevar in generic_context.variables(self.db()) { let typevar = bound_typevar.typevar(self.db()); let has_default = typevar.default_type(self.db()).is_some(); - if seen_default && !has_default { - report_invalid_type_param_order( - &self.context, - class, - typevar.name(self.db()).as_str(), - ); - } - if has_default { - seen_default = true; + if let Some(state) = state.as_mut() { + if !has_default { + state.invalid_later_tvars.push(typevar); + } + } else if has_default { + state = Some(State { + typevar_with_default: typevar, + invalid_later_tvars: vec![], + }); } } + + if let Some(state) = state + && !state.invalid_later_tvars.is_empty() + { + report_invalid_type_param_order( + &self.context, + class, + class_node, + state.typevar_with_default, + &state.invalid_later_tvars, + ); + } } - } - let scope = class.body_scope(self.db()).scope(self.db()); - if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) - && let Some(parent) = scope.parent() - { - for self_typevar in class.typevars_referenced_in_definition(self.db()) { - let self_typevar_name = self_typevar.typevar(self.db()).name(self.db()); - for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) { - if let Some(other_typevar) = - enclosing.binds_named_typevar(self.db(), self_typevar_name) - { - report_rebound_typevar( - &self.context, - self_typevar_name, - class, - class_node, - other_typevar, - ); + + let scope = class.body_scope(self.db()).scope(self.db()); + if let Some(parent) = scope.parent() { + for self_typevar in class.typevars_referenced_in_definition(self.db()) { + let self_typevar_name = self_typevar.typevar(self.db()).name(self.db()); + for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) { + if let Some(other_typevar) = + enclosing.binds_named_typevar(self.db(), self_typevar_name) + { + report_rebound_typevar( + &self.context, + self_typevar_name, + class, + class_node, + other_typevar, + ); + } } } } From ceea5c54f1ee31de8ad1ba7596abff25e31eb695 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 14 Dec 2025 19:29:53 +0000 Subject: [PATCH 14/14] generated files --- .../e2e/snapshots/e2e__commands__debug_command.snap | 1 - ty.schema.json | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 5ab74d2a9df1b..cf1b0afc645a6 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -78,7 +78,6 @@ Settings: Settings { "invalid-type-form": Error (Default), "invalid-type-guard-call": Error (Default), "invalid-type-guard-definition": Error (Default), - "invalid-type-param-order": Error (Default), "invalid-type-variable-constraints": Error (Default), "missing-argument": Error (Default), "missing-typed-dict-key": Error (Default), diff --git a/ty.schema.json b/ty.schema.json index 755e14d6d43a1..74c142ec4cb44 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -595,7 +595,7 @@ }, "invalid-generic-class": { "title": "detects invalid generic classes", - "description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", + "description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\nMany of these result in `TypeError` being raised at runtime if they are violated.\n\n## Examples\n```python\nfrom typing_extensions import Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\", default=int)\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n\n# error: type parameter with default comes before type parameter without default\nclass D(Generic[U, T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", "default": "error", "oneOf": [ { @@ -813,16 +813,6 @@ } ] }, - "invalid-type-param-order": { - "title": "detects invalid type parameter order", - "description": "## What it does\nChecks for type parameters without defaults that come after type parameters with defaults.\n\n## Why is this bad?\nType parameters without defaults must come before type parameters with defaults.\n\n## Example\n\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\")\n# Error: T has no default but comes after U which has a default\nclass Foo(Generic[U = int, T]): ...\n```\n\n## References\n- [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/)", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, "invalid-type-variable-constraints": { "title": "detects invalid type variable constraints", "description": "## What it does\nChecks for constrained [type variables] with only one constraint.\n\n## Why is this bad?\nA constrained type variable must have at least two constraints.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar('T', str) # invalid constrained TypeVar\n```\n\nUse instead:\n```python\nT = TypeVar('T', str, int) # valid constrained TypeVar\n# or\nT = TypeVar('T', bound=str) # valid bound TypeVar\n```\n\n[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar",