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 0000000000..1f3d5cd5bb --- /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…_-_Invalid_Type_Param_O…_(8ff6f101710809cb).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o…_-_Invalid_Type_Param_O…_(8ff6f101710809cb).snap new file mode 100644 index 0000000000..399f0d0a18 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o…_-_Invalid_Type_Param_O…_(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 3acd7b0a64..0f28ff05dc 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 9ae325a9ae..261aa2dc31 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 87feeb2507..b228c1a852 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",