Skip to content
103 changes: 54 additions & 49 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Invalid Order of Legacy Type Parameters

<!-- snapshot-diagnostics -->

```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],
): ...
```
Original file line number Diff line number Diff line change
Expand Up @@ -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-generic-class]
attr: Callable[PAnother, None]

# TODO: error
Expand Down
Original file line number Diff line number Diff line change
@@ -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

```
95 changes: 92 additions & 3 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -894,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
Expand Down Expand Up @@ -3695,6 +3700,90 @@ 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>,
node: &ast::StmtClassDef,
typevar_with_default: TypeVarInstance<'db>,
invalid_later_typevars: &[TypeVarInstance<'db>],
) {
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))),
);
}
}

pub(crate) fn report_rebound_typevar<'db>(
context: &InferContext<'db, '_>,
typevar_name: &ast::name::Name,
Expand Down
Loading
Loading