mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-02 04:48:07 +00:00
[ty] Diagnostic for generic classes that reference typevars in enclosing scope (#20822)
Generic classes are not allowed to bind or reference a typevar from an
enclosing scope:
```py
def f[T](x: T, y: T) -> None:
class Ok[S]: ...
# error: [invalid-generic-class]
class Bad1[T]: ...
# error: [invalid-generic-class]
class Bad2(Iterable[T]): ...
class C[T]:
class Ok1[S]: ...
# error: [invalid-generic-class]
class Bad1[T]: ...
# error: [invalid-generic-class]
class Bad2(Iterable[T]): ...
```
It does not matter if the class uses PEP 695 or legacy syntax. It does
not matter if the enclosing scope is a generic class or function. The
generic class cannot even _reference_ an enclosing typevar in its base
class list.
This PR adds diagnostics for these cases.
In addition, the PR adds better fallback behavior for generic classes
that violate this rule: any enclosing typevars are not included in the
class's generic context. (That ensures that we don't inadvertently try
to infer specializations for those typevars in places where we
shouldn't.) The `dulwich` ecosystem project has [examples of
this](d912eaaffd/dulwich/config.py (L251))
that were causing new false positives on #20677.
---------
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
83b497ce88
commit
aba0bd568e
10 changed files with 338 additions and 40 deletions
|
|
@ -71,6 +71,26 @@ reveal_type(generic_context(InheritedGenericPartiallySpecialized))
|
|||
reveal_type(generic_context(InheritedGenericFullySpecialized))
|
||||
```
|
||||
|
||||
In a nested class, references to typevars in an enclosing class are not allowed, but if they are
|
||||
present, they are not included in the class's generic context.
|
||||
|
||||
```py
|
||||
class OuterClass(Generic[T]):
|
||||
# error: [invalid-generic-class] "Generic class `InnerClass` must not reference type variables bound in an enclosing scope"
|
||||
class InnerClass(list[T]): ...
|
||||
# revealed: None
|
||||
reveal_type(generic_context(InnerClass))
|
||||
|
||||
def method(self):
|
||||
# error: [invalid-generic-class] "Generic class `InnerClassInMethod` must not reference type variables bound in an enclosing scope"
|
||||
class InnerClassInMethod(list[T]): ...
|
||||
# revealed: None
|
||||
reveal_type(generic_context(InnerClassInMethod))
|
||||
|
||||
# revealed: tuple[T@OuterClass]
|
||||
reveal_type(generic_context(OuterClass))
|
||||
```
|
||||
|
||||
If you don't specialize a generic base class, we use the default specialization, which maps each
|
||||
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
|
||||
the inheriting class generic.
|
||||
|
|
|
|||
|
|
@ -260,27 +260,31 @@ class C[T]:
|
|||
|
||||
### Generic class within generic function
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import Iterable
|
||||
|
||||
def f[T](x: T, y: T) -> None:
|
||||
class Ok[S]: ...
|
||||
# TODO: error for reuse of typevar
|
||||
# error: [invalid-generic-class]
|
||||
class Bad1[T]: ...
|
||||
# TODO: error for reuse of typevar
|
||||
# error: [invalid-generic-class]
|
||||
class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
### Generic class within generic class
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import Iterable
|
||||
|
||||
class C[T]:
|
||||
class Ok1[S]: ...
|
||||
# TODO: error for reuse of typevar
|
||||
# error: [invalid-generic-class]
|
||||
class Bad1[T]: ...
|
||||
# TODO: error for reuse of typevar
|
||||
# error: [invalid-generic-class]
|
||||
class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: scoping.md - Scoping rules for type variables - Nested formal typevars must be distinct - Generic class within generic function
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import Iterable
|
||||
2 |
|
||||
3 | def f[T](x: T, y: T) -> None:
|
||||
4 | class Ok[S]: ...
|
||||
5 | # error: [invalid-generic-class]
|
||||
6 | class Bad1[T]: ...
|
||||
7 | # error: [invalid-generic-class]
|
||||
8 | class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Generic class `Bad1` must not reference type variables bound in an enclosing scope
|
||||
--> src/mdtest_snippet.py:3:5
|
||||
|
|
||||
1 | from typing import Iterable
|
||||
2 |
|
||||
3 | def f[T](x: T, y: T) -> None:
|
||||
| ------------------------ Type variable `T` is bound in this enclosing scope
|
||||
4 | class Ok[S]: ...
|
||||
5 | # error: [invalid-generic-class]
|
||||
6 | class Bad1[T]: ...
|
||||
| ^^^^ `T` referenced in class definition here
|
||||
7 | # error: [invalid-generic-class]
|
||||
8 | class Bad2(Iterable[T]): ...
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Generic class `Bad2` must not reference type variables bound in an enclosing scope
|
||||
--> src/mdtest_snippet.py:3:5
|
||||
|
|
||||
1 | from typing import Iterable
|
||||
2 |
|
||||
3 | def f[T](x: T, y: T) -> None:
|
||||
| ------------------------ Type variable `T` is bound in this enclosing scope
|
||||
4 | class Ok[S]: ...
|
||||
5 | # error: [invalid-generic-class]
|
||||
6 | class Bad1[T]: ...
|
||||
7 | # error: [invalid-generic-class]
|
||||
8 | class Bad2(Iterable[T]): ...
|
||||
| ^^^^^^^^^^^^^^^^^ `T` referenced in class definition here
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: scoping.md - Scoping rules for type variables - Nested formal typevars must be distinct - Generic class within generic class
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import Iterable
|
||||
2 |
|
||||
3 | class C[T]:
|
||||
4 | class Ok1[S]: ...
|
||||
5 | # error: [invalid-generic-class]
|
||||
6 | class Bad1[T]: ...
|
||||
7 | # error: [invalid-generic-class]
|
||||
8 | class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Generic class `Bad1` must not reference type variables bound in an enclosing scope
|
||||
--> src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | from typing import Iterable
|
||||
2 |
|
||||
3 | class C[T]:
|
||||
| - Type variable `T` is bound in this enclosing scope
|
||||
4 | class Ok1[S]: ...
|
||||
5 | # error: [invalid-generic-class]
|
||||
6 | class Bad1[T]: ...
|
||||
| ^^^^ `T` referenced in class definition here
|
||||
7 | # error: [invalid-generic-class]
|
||||
8 | class Bad2(Iterable[T]): ...
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Generic class `Bad2` must not reference type variables bound in an enclosing scope
|
||||
--> src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | from typing import Iterable
|
||||
2 |
|
||||
3 | class C[T]:
|
||||
| - Type variable `T` is bound in this enclosing scope
|
||||
4 | class Ok1[S]: ...
|
||||
5 | # error: [invalid-generic-class]
|
||||
6 | class Bad1[T]: ...
|
||||
7 | # error: [invalid-generic-class]
|
||||
8 | class Bad2(Iterable[T]): ...
|
||||
| ^^^^^^^^^^^^^^^^^ `T` referenced in class definition here
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue