Skip to content

Commit 5f0fd91

Browse files
authored
[ty] More type-variable default validation (#23639)
## Summary We have several checks at the moment which fire on legacy type variables with invalid defaults in a _class_ context, but which fail to check for legacy type variables with invalid defaults in a _function_ context. This PR adds those missing checks. The typing spec states that both legacy and PEP-695 type variables are not allowed to have defaults that: - reference type variables bound in outer scopes, or - reference type variables that are put into scope later on in the type parameter list or function signature The typing spec also states that a type variable with a default is not allowed to come before any type variables without defaults. ## Test Plan mdtests and snapshots
1 parent da13d62 commit 5f0fd91

10 files changed

+942
-65
lines changed

crates/ty_python_semantic/resources/mdtest/generics/scoping.md

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
python-version = "3.12"
66
```
77

8-
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
9-
spec.
8+
Most of these tests come from the [Scoping rules for type variables] section of the typing spec.
109

1110
## Typevar used outside of generic function or class
1211

@@ -410,6 +409,135 @@ class C[T]:
410409
ok2: Inner[T]
411410
```
412411

412+
## Type parameter defaults cannot reference outer-scope type parameters
413+
414+
```toml
415+
[environment]
416+
python-version = "3.13"
417+
```
418+
419+
Per the [typing spec][scoping rules], the default of a type parameter must not reference type
420+
parameters from an outer scope. Out-of-scope defaults on class type parameters are validated as part
421+
of `invalid-generic-class`; the tests here cover the remaining cases for PEP 695 function and type
422+
alias scopes, as well as legacy `TypeVar`s used in function/method signatures.
423+
424+
### Nested functions
425+
426+
<!-- snapshot-diagnostics -->
427+
428+
```py
429+
def outer[T]():
430+
# error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default"
431+
def inner[U = T](): ...
432+
def ok[U = int](): ... # OK
433+
```
434+
435+
### Function nested in class
436+
437+
<!-- snapshot-diagnostics -->
438+
439+
```py
440+
class C[T]:
441+
# error: [invalid-type-variable-default]
442+
def f[U = T](self): ...
443+
def g[U = int](self): ... # OK
444+
```
445+
446+
### Type alias nested in class
447+
448+
<!-- snapshot-diagnostics -->
449+
450+
```py
451+
class C[T]:
452+
# error: [invalid-type-variable-default]
453+
type Alias[U = T] = list[U]
454+
455+
type Ok[U = int] = list[U] # OK
456+
```
457+
458+
### Legacy TypeVar in method with outer-scope class TypeVar
459+
460+
<!-- snapshot-diagnostics -->
461+
462+
```py
463+
from typing import TypeVar, Generic
464+
465+
T1 = TypeVar("T1")
466+
T2 = TypeVar("T2", default=T1)
467+
468+
class Foo(Generic[T1]):
469+
# error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `T1`"
470+
def method(self, x: T2) -> T2:
471+
return x
472+
```
473+
474+
### Legacy TypeVar in nested function
475+
476+
<!-- snapshot-diagnostics -->
477+
478+
```py
479+
from typing import TypeVar, Generic
480+
481+
T = TypeVar("T")
482+
U = TypeVar("U", default=T)
483+
484+
def outer(x: T) -> T:
485+
# error: [invalid-type-variable-default]
486+
def inner(y: U) -> U:
487+
return y
488+
return x
489+
```
490+
491+
### Legacy TypeVar with default referring to later Typevar
492+
493+
<!-- snapshot-diagnostics -->
494+
495+
```py
496+
from typing import TypeVar, Generic
497+
498+
T = TypeVar("T", default=int)
499+
U = TypeVar("U", default=T)
500+
501+
# error: [invalid-type-variable-default]
502+
def bad(y: U, z: T) -> tuple[U, T]:
503+
return y, z
504+
505+
# OK, because the typevar with the default comes after the one without
506+
def fine(y: T, z: U) -> tuple[U, T]:
507+
return z, y
508+
```
509+
510+
### Legacy TypeVar ordering: default before non-default in function
511+
512+
<!-- snapshot-diagnostics -->
513+
514+
```py
515+
from typing import TypeVar
516+
517+
T1 = TypeVar("T1", default=int)
518+
T2 = TypeVar("T2")
519+
T3 = TypeVar("T3")
520+
DefaultStrT = TypeVar("DefaultStrT", default=str)
521+
522+
# error: [invalid-type-variable-default]
523+
def f(x: T1, y: T2) -> tuple[T1, T2]:
524+
return x, y
525+
526+
# error: [invalid-type-variable-default]
527+
def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]:
528+
return x, y, z
529+
530+
# error: [invalid-type-variable-default]
531+
def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]:
532+
return x, y, z, w
533+
534+
def ok(x: T2, y: T1) -> tuple[T2, T1]:
535+
return x, y
536+
537+
def ok2(x: T1, y: DefaultStrT) -> tuple[T1, DefaultStrT]:
538+
return x, y
539+
```
540+
413541
## Mixed-scope type parameters
414542

415543
Methods can have type parameters that are scoped to the method itself, while also referring to type
@@ -433,4 +561,5 @@ def f(x: type[Foo[T]]) -> T:
433561
raise NotImplementedError
434562
```
435563

436-
[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables
564+
[scoping rules]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules
565+
[scoping rules for type variables]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
assertion_line: 624
4+
expression: snapshot
5+
---
6+
7+
---
8+
mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Function nested in class
9+
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md
10+
---
11+
12+
# Python source files
13+
14+
## mdtest_snippet.py
15+
16+
```
17+
1 | class C[T]:
18+
2 | # error: [invalid-type-variable-default]
19+
3 | def f[U = T](self): ...
20+
4 | def g[U = int](self): ... # OK
21+
```
22+
23+
# Diagnostics
24+
25+
```
26+
error[invalid-type-variable-default]: Invalid default for type parameter `U`
27+
--> src/mdtest_snippet.py:1:9
28+
|
29+
1 | class C[T]:
30+
| - `T` defined here
31+
2 | # error: [invalid-type-variable-default]
32+
3 | def f[U = T](self): ...
33+
| ^ `T` is a type parameter bound in an outer scope
34+
4 | def g[U = int](self): ... # OK
35+
|
36+
info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules
37+
info: rule `invalid-type-variable-default` is enabled by default
38+
39+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
6+
---
7+
mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar in method with outer-scope class TypeVar
8+
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md
9+
---
10+
11+
# Python source files
12+
13+
## mdtest_snippet.py
14+
15+
```
16+
1 | from typing import TypeVar, Generic
17+
2 |
18+
3 | T1 = TypeVar("T1")
19+
4 | T2 = TypeVar("T2", default=T1)
20+
5 |
21+
6 | class Foo(Generic[T1]):
22+
7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `T1`"
23+
8 | def method(self, x: T2) -> T2:
24+
9 | return x
25+
```
26+
27+
# Diagnostics
28+
29+
```
30+
error[invalid-type-variable-default]: Invalid use of type variable `T2`
31+
--> src/mdtest_snippet.py:4:1
32+
|
33+
3 | T1 = TypeVar("T1")
34+
4 | T2 = TypeVar("T2", default=T1)
35+
| ------------------------------ `T2` defined here
36+
5 |
37+
6 | class Foo(Generic[T1]):
38+
7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable
39+
8 | def method(self, x: T2) -> T2:
40+
| ^^ Default of `T2` references out-of-scope type variable `T1`
41+
9 | return x
42+
|
43+
info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules
44+
info: rule `invalid-type-variable-default` is enabled by default
45+
46+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
assertion_line: 624
4+
expression: snapshot
5+
---
6+
7+
---
8+
mdtest name: scoping.md - Scoping rules for type variables - Type parameter defaults cannot reference outer-scope type parameters - Legacy TypeVar in nested function
9+
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md
10+
---
11+
12+
# Python source files
13+
14+
## mdtest_snippet.py
15+
16+
```
17+
1 | from typing import TypeVar, Generic
18+
2 |
19+
3 | T = TypeVar("T")
20+
4 | U = TypeVar("U", default=T)
21+
5 |
22+
6 | def outer(x: T) -> T:
23+
7 | # error: [invalid-type-variable-default]
24+
8 | def inner(y: U) -> U:
25+
9 | return y
26+
10 | return x
27+
```
28+
29+
# Diagnostics
30+
31+
```
32+
error[invalid-type-variable-default]: Invalid use of type variable `U`
33+
--> src/mdtest_snippet.py:4:1
34+
|
35+
3 | T = TypeVar("T")
36+
4 | U = TypeVar("U", default=T)
37+
| --------------------------- `U` defined here
38+
5 |
39+
6 | def outer(x: T) -> T:
40+
7 | # error: [invalid-type-variable-default]
41+
8 | def inner(y: U) -> U:
42+
| ^ Default of `U` references out-of-scope type variable `T`
43+
9 | return y
44+
10 | return x
45+
|
46+
info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules
47+
info: rule `invalid-type-variable-default` is enabled by default
48+
49+
```

0 commit comments

Comments
 (0)