feat: implement error when invalid typevar order

Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
This commit is contained in:
11happy 2025-12-04 10:14:26 +00:00 committed by 11happy
parent c7eea1f2e3
commit a1e88c31a0
5 changed files with 152 additions and 1 deletions

View File

@ -0,0 +1,24 @@
# Invalid Type Param Order
<!-- snapshot-diagnostics -->
```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
```

View File

@ -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
```

View File

@ -89,6 +89,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_TYPE_FORM); registry.register_lint(&INVALID_TYPE_FORM);
registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION); registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION);
registry.register_lint(&INVALID_TYPE_GUARD_CALL); registry.register_lint(&INVALID_TYPE_GUARD_CALL);
registry.register_lint(&INVALID_TYPE_PARAM_ORDER);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&MISSING_ARGUMENT); registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NO_MATCHING_OVERLOAD); 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! { declare_lint! {
/// ## What it does /// ## What it does
/// Checks for the creation of invalid `NewType`s /// 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>( pub(crate) fn report_rebound_typevar<'db>(
context: &InferContext<'db, '_>, context: &InferContext<'db, '_>,
typevar_name: &ast::name::Name, typevar_name: &ast::name::Name,

View File

@ -78,7 +78,7 @@ use crate::types::diagnostic::{
report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type, report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type,
report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
report_invalid_return_type, report_invalid_type_checking_constant, 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_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
report_possibly_missing_attribute, report_possibly_unresolved_reference, report_possibly_missing_attribute, report_possibly_unresolved_reference,
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, 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()); let scope = class.body_scope(self.db()).scope(self.db());
if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS)
&& let Some(parent) = scope.parent() && let Some(parent) = scope.parent()

10
ty.schema.json generated
View File

@ -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": { "invalid-type-variable-constraints": {
"title": "detects 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", "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",