diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index b039ba94f7..730692f9ef 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -95,7 +95,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -126,7 +126,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -190,7 +190,7 @@ class B(A): ...
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -218,7 +218,7 @@ type B = A
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -245,7 +245,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -357,7 +357,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -387,7 +387,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -502,7 +502,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -557,7 +557,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -627,7 +627,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -678,7 +678,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -707,7 +707,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -751,7 +751,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -793,7 +793,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -826,7 +826,7 @@ class C[U](Generic[T]): ...
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -934,7 +934,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1020,7 +1020,7 @@ and `__ne__` methods accept `object` as their second argument.
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1052,7 +1052,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1082,7 +1082,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1132,7 +1132,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1158,7 +1158,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1189,7 +1189,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1223,7 +1223,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1272,7 +1272,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1297,7 +1297,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1355,7 +1355,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1376,13 +1376,60 @@ IntOrStr = TypeAliasType("IntOrStr", int | str) # okay
NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal
```
+## `invalid-type-arguments`
+
+
+Default level: error ·
+Added in 0.0.1-alpha.29 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for invalid type arguments in explicit type specialization.
+
+**Why is this bad?**
+
+Providing the wrong number of type arguments or type arguments that don't
+satisfy the type variable's bounds or constraints will lead to incorrect
+type inference and may indicate a misunderstanding of the generic type's
+interface.
+
+**Examples**
+
+
+Using legacy type variables:
+```python
+from typing import Generic, TypeVar
+
+T1 = TypeVar('T1', int, str)
+T2 = TypeVar('T2', bound=int)
+
+class Foo1(Generic[T1]): ...
+class Foo2(Generic[T2]): ...
+
+Foo1[bytes] # error: bytes does not satisfy T1's constraints
+Foo2[str] # error: str does not satisfy T2's bound
+```
+
+Using PEP 695 type variables:
+```python
+class Foo[T]: ...
+class Bar[T, U]: ...
+
+Foo[int, str] # error: too many arguments
+Bar[int] # error: too few arguments
+```
+
## `invalid-type-checking-constant`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1412,7 +1459,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1442,7 +1489,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1476,7 +1523,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1510,7 +1557,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1545,7 +1592,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1570,7 +1617,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1603,7 +1650,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1632,7 +1679,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1656,7 +1703,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1682,7 +1729,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1709,7 +1756,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1767,7 +1814,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1797,7 +1844,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1826,7 +1873,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1853,7 +1900,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1881,7 +1928,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1927,7 +1974,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1954,7 +2001,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1982,7 +2029,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2007,7 +2054,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2032,7 +2079,7 @@ print(x) # NameError: name 'x' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2069,7 +2116,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2097,7 +2144,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2122,7 +2169,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2163,7 +2210,7 @@ class SubProto(BaseProto, Protocol):
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -2251,7 +2298,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2279,7 +2326,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2311,7 +2358,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2343,7 +2390,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2370,7 +2417,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2394,7 +2441,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2452,7 +2499,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2491,7 +2538,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2554,7 +2601,7 @@ def foo(x: int | str) -> int | str:
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -2578,7 +2625,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
index 9d6cd6ded7..66f39a3aa4 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
@@ -145,7 +145,7 @@ reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]]
The specialization must match the generic types:
```py
-# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
+# error: [invalid-type-arguments] "Too many type arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
```
@@ -164,12 +164,10 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `str`"
+# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`"
reveal_type(Bounded[str]()) # revealed: Unknown
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `int | str`"
+# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `BoundedT@Bounded`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
@@ -197,8 +195,7 @@ reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument to class `Constrained` is incorrect: Expected `int | str`, found `object`"
+# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `ConstrainedT@Constrained`"
reveal_type(Constrained[object]()) # revealed: Unknown
```
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md
index d32029311a..50df561dbc 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md
@@ -61,7 +61,7 @@ def _(a: C[int], b: C[Literal[5]]):
The specialization must match the generic types:
```py
-# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
+# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
reveal_type(C[int, int]) # revealed: Unknown
```
@@ -88,12 +88,10 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]) # revealed: Bounded[IntSubclass]
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `str`"
+# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[str]) # revealed: Unknown
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `int | str`"
+# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[int | str]) # revealed: Unknown
reveal_type(BoundedByUnion[int]) # revealed: BoundedByUnion[int]
@@ -119,8 +117,7 @@ reveal_type(Constrained[str]) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]) # revealed: Constrained[int | str]
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument is incorrect: Expected `int | str`, found `object`"
+# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
reveal_type(Constrained[object]) # revealed: Unknown
```
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
index 1f3d69e01b..418596b083 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
@@ -135,7 +135,7 @@ reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]]
The specialization must match the generic types:
```py
-# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
+# error: [invalid-type-arguments] "Too many type arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
```
@@ -149,12 +149,10 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `str`"
+# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[str]()) # revealed: Unknown
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `int | str`"
+# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
@@ -180,8 +178,7 @@ reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
-# TODO: update this diagnostic to talk about type parameters and specializations
-# error: [invalid-argument-type] "Argument to class `Constrained` is incorrect: Expected `int | str`, found `object`"
+# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
reveal_type(Constrained[object]()) # revealed: Unknown
```
diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs
index 280aa7984d..2d4b284be1 100644
--- a/crates/ty_python_semantic/src/types/call/bind.rs
+++ b/crates/ty_python_semantic/src/types/call/bind.rs
@@ -3586,12 +3586,15 @@ impl CallableBindingSnapshotter {
/// Describes a callable for the purposes of diagnostics.
#[derive(Debug)]
pub(crate) struct CallableDescription<'a> {
- name: &'a str,
- kind: &'a str,
+ pub(crate) name: &'a str,
+ pub(crate) kind: &'a str,
}
impl<'db> CallableDescription<'db> {
- fn new(db: &'db dyn Db, callable_type: Type<'db>) -> Option> {
+ pub(crate) fn new(
+ db: &'db dyn Db,
+ callable_type: Type<'db>,
+ ) -> Option> {
match callable_type {
Type::FunctionLiteral(function) => Some(CallableDescription {
kind: "function",
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 0738c41330..a3490337b0 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -84,6 +84,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_NAMED_TUPLE);
registry.register_lint(&INVALID_RAISE);
registry.register_lint(&INVALID_SUPER_ARGUMENT);
+ registry.register_lint(&INVALID_TYPE_ARGUMENTS);
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
registry.register_lint(&INVALID_TYPE_FORM);
registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION);
@@ -1406,6 +1407,47 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for invalid type arguments in explicit type specialization.
+ ///
+ /// ## Why is this bad?
+ /// Providing the wrong number of type arguments or type arguments that don't
+ /// satisfy the type variable's bounds or constraints will lead to incorrect
+ /// type inference and may indicate a misunderstanding of the generic type's
+ /// interface.
+ ///
+ /// ## Examples
+ ///
+ /// Using legacy type variables:
+ /// ```python
+ /// from typing import Generic, TypeVar
+ ///
+ /// T1 = TypeVar('T1', int, str)
+ /// T2 = TypeVar('T2', bound=int)
+ ///
+ /// class Foo1(Generic[T1]): ...
+ /// class Foo2(Generic[T2]): ...
+ ///
+ /// Foo1[bytes] # error: bytes does not satisfy T1's constraints
+ /// Foo2[str] # error: str does not satisfy T2's bound
+ /// ```
+ ///
+ /// Using PEP 695 type variables:
+ /// ```python
+ /// class Foo[T]: ...
+ /// class Bar[T, U]: ...
+ ///
+ /// Foo[int, str] # error: too many arguments
+ /// Bar[int] # error: too few arguments
+ /// ```
+ pub(crate) static INVALID_TYPE_ARGUMENTS = {
+ summary: "detects invalid type arguments in generic specialization",
+ status: LintStatus::stable("0.0.1-alpha.29"),
+ default_level: Level::Error,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Checks for objects that are not iterable but are used in a context that requires them to be.
diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs
index df15fdafc0..47b9b51cd7 100644
--- a/crates/ty_python_semantic/src/types/generics.rs
+++ b/crates/ty_python_semantic/src/types/generics.rs
@@ -13,7 +13,7 @@ use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::constraints::ConstraintSet;
use crate::types::instance::{Protocol, ProtocolInstanceType};
-use crate::types::signatures::{Parameter, Parameters, Signature};
+use crate::types::signatures::Parameters;
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
use crate::types::{
@@ -411,39 +411,6 @@ impl<'db> GenericContext<'db> {
self.variables_inner(db).len()
}
- pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> {
- let parameters = Parameters::new(
- self.variables(db)
- .map(|typevar| Self::parameter_from_typevar(db, typevar)),
- );
- Signature::new(parameters, None)
- }
-
- fn parameter_from_typevar(
- db: &'db dyn Db,
- bound_typevar: BoundTypeVarInstance<'db>,
- ) -> Parameter<'db> {
- let typevar = bound_typevar.typevar(db);
- let mut parameter = Parameter::positional_only(Some(typevar.name(db).clone()));
- match typevar.bound_or_constraints(db) {
- Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
- // TODO: This should be a type form.
- parameter = parameter.with_annotated_type(bound);
- }
- Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
- // TODO: This should be a new type variant where only these exact types are
- // assignable, and not subclasses of them, nor a union of them.
- parameter = parameter
- .with_annotated_type(UnionType::from_elements(db, constraints.elements(db)));
- }
- None => {}
- }
- if let Some(default_ty) = bound_typevar.default_type(db) {
- parameter = parameter.with_default_type(default_ty);
- }
- parameter
- }
-
pub(crate) fn default_specialization(
self,
db: &'db dyn Db,
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index f5d4f36a30..229a872bc2 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -1,6 +1,6 @@
use std::iter;
-use itertools::{Either, Itertools};
+use itertools::{Either, EitherOrBoth, Itertools};
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_db::parsed::ParsedModuleRef;
@@ -49,7 +49,7 @@ use crate::semantic_index::{
ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table,
};
use crate::subscript::{PyIndex, PySlice};
-use crate::types::call::bind::MatchingOverloadIndex;
+use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
use crate::types::context::{InNoTypeCheck, InferContext};
@@ -60,12 +60,13 @@ use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE,
INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD,
- INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM,
- INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
- NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
- POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
- UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
- USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
+ INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS,
+ INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
+ IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
+ POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
+ UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
+ UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
+ hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
@@ -106,9 +107,9 @@ use crate::types::{
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, ParameterForm, SpecialFormType, SubclassOfType, TrackedConstraintSet,
Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
- TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
- TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
- UnionTypeInstance, binding_type, infer_scope_types, liskov, todo_type,
+ TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
+ TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder,
+ UnionType, UnionTypeInstance, binding_type, infer_scope_types, liskov, todo_type,
};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
use crate::unpack::{EvaluationMode, UnpackPosition};
@@ -11220,45 +11221,173 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
generic_context: GenericContext<'db>,
specialize: impl FnOnce(&[Option>]) -> Type<'db>,
) -> Type<'db> {
+ let db = self.db();
let slice_node = subscript.slice.as_ref();
- let call_argument_types = match slice_node {
+
+ // Extract type arguments from the subscript
+ let type_arguments: Vec> = match slice_node {
ast::Expr::Tuple(tuple) => {
- let arguments = CallArguments::positional(
- tuple.elts.iter().map(|elt| self.infer_type_expression(elt)),
- );
+ let types: Vec<_> = tuple
+ .elts
+ .iter()
+ .map(|elt| self.infer_type_expression(elt))
+ .collect();
self.store_expression_type(
slice_node,
- Type::heterogeneous_tuple(self.db(), arguments.iter_types()),
+ Type::heterogeneous_tuple(db, types.iter().copied()),
);
- arguments
+ types
}
- _ => CallArguments::positional([self.infer_type_expression(slice_node)]),
+ _ => vec![self.infer_type_expression(slice_node)],
};
- let binding = Binding::single(value_ty, generic_context.signature(self.db()));
- let bindings = match Bindings::from(binding)
- .match_parameters(self.db(), &call_argument_types)
- .check_types(
- self.db(),
- &call_argument_types,
- TypeContext::default(),
- &self.dataclass_field_specifiers,
- ) {
- Ok(bindings) => bindings,
- Err(CallError(_, bindings)) => {
- bindings.report_diagnostics(&self.context, subscript.into());
- return Type::unknown();
- }
- };
- let callable = bindings
- .into_iter()
- .next()
- .expect("valid bindings should have one callable");
- let (_, overload) = callable
- .matching_overloads()
- .next()
- .expect("valid bindings should have matching overload");
- specialize(overload.parameter_types())
+ let typevars = generic_context.variables(db);
+ let typevars_len = typevars.len();
+
+ let mut specialization_types = Vec::with_capacity(typevars_len);
+ let mut typevar_with_defaults = 0;
+ let mut missing_typevars = vec![];
+ let mut first_excess_type_argument_index = None;
+
+ // Helper to get the AST node corresponding to the type argument at `index`.
+ let get_node = |index: usize| -> ast::AnyNodeRef<'_> {
+ match slice_node {
+ ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
+ .get(index)
+ .expect("type argument index should not be out of range")
+ .into(),
+ _ => slice_node.into(),
+ }
+ };
+
+ let mut has_error = false;
+
+ for (index, item) in typevars.zip_longest(type_arguments.iter()).enumerate() {
+ match item {
+ EitherOrBoth::Both(typevar, &provided_type) => {
+ if typevar.default_type(db).is_some() {
+ typevar_with_defaults += 1;
+ }
+ match typevar.typevar(db).bound_or_constraints(db) {
+ Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
+ if provided_type
+ .when_assignable_to(db, bound, InferableTypeVars::None)
+ .is_never_satisfied(db)
+ {
+ let node = get_node(index);
+ if let Some(builder) =
+ self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node)
+ {
+ builder.into_diagnostic(format_args!(
+ "Type `{}` is not assignable to upper bound `{}` \
+ of type variable `{}`",
+ provided_type.display(db),
+ bound.display(db),
+ typevar.identity(db).display(db),
+ ));
+ }
+ has_error = true;
+ continue;
+ }
+ }
+ Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
+ if provided_type
+ .when_assignable_to(
+ db,
+ Type::Union(constraints),
+ InferableTypeVars::None,
+ )
+ .is_never_satisfied(db)
+ {
+ let node = get_node(index);
+ if let Some(builder) =
+ self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node)
+ {
+ builder.into_diagnostic(format_args!(
+ "Type `{}` does not satisfy constraints `{}` \
+ of type variable `{}`",
+ provided_type.display(db),
+ constraints
+ .elements(db)
+ .iter()
+ .map(|c| c.display(db))
+ .format("`, `"),
+ typevar.identity(db).display(db),
+ ));
+ }
+ has_error = true;
+ continue;
+ }
+ }
+ None => {}
+ }
+ specialization_types.push(Some(provided_type));
+ }
+ EitherOrBoth::Left(typevar) => {
+ if typevar.default_type(db).is_none() {
+ missing_typevars.push(typevar);
+ } else {
+ typevar_with_defaults += 1;
+ }
+ specialization_types.push(None);
+ }
+ EitherOrBoth::Right(_) => {
+ first_excess_type_argument_index.get_or_insert(index);
+ }
+ }
+ }
+
+ if !missing_typevars.is_empty() {
+ if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, subscript) {
+ let description = CallableDescription::new(db, value_ty);
+ let s = if missing_typevars.len() > 1 { "s" } else { "" };
+ builder.into_diagnostic(format_args!(
+ "No type argument{s} provided for required type variable{s} `{}`{}",
+ missing_typevars
+ .iter()
+ .map(|tv| tv.typevar(db).name(db))
+ .format("`, `"),
+ if let Some(CallableDescription { kind, name }) = description {
+ format!(" of {kind} `{name}`")
+ } else {
+ String::new()
+ }
+ ));
+ }
+ has_error = true;
+ }
+
+ if let Some(first_excess_type_argument_index) = first_excess_type_argument_index {
+ let node = get_node(first_excess_type_argument_index);
+ if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) {
+ let description = CallableDescription::new(db, value_ty);
+ builder.into_diagnostic(format_args!(
+ "Too many type arguments{}: expected {}, got {}",
+ if let Some(CallableDescription { kind, name }) = description {
+ format!(" to {kind} `{name}`")
+ } else {
+ String::new()
+ },
+ if typevar_with_defaults == 0 {
+ format!("{typevars_len}")
+ } else {
+ format!(
+ "between {} and {}",
+ typevars_len - typevar_with_defaults,
+ typevars_len
+ )
+ },
+ type_arguments.len(),
+ ));
+ }
+ has_error = true;
+ }
+
+ if has_error {
+ return Type::unknown();
+ }
+
+ specialize(&specialization_types)
}
fn infer_subscript_expression_types(
diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
index b6c245a91f..0676f15fd7 100644
--- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
+++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
@@ -72,6 +72,7 @@ Settings: Settings {
"invalid-super-argument": Error (Default),
"invalid-syntax-in-forward-annotation": Error (Default),
"invalid-type-alias-type": Error (Default),
+ "invalid-type-arguments": Error (Default),
"invalid-type-checking-constant": Error (Default),
"invalid-type-form": Error (Default),
"invalid-type-guard-call": Error (Default),
diff --git a/ty.schema.json b/ty.schema.json
index edc493f2e3..aef426eb23 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -753,6 +753,16 @@
}
]
},
+ "invalid-type-arguments": {
+ "title": "detects invalid type arguments in generic specialization",
+ "description": "## What it does\nChecks for invalid type arguments in explicit type specialization.\n\n## Why is this bad?\nProviding the wrong number of type arguments or type arguments that don't\nsatisfy the type variable's bounds or constraints will lead to incorrect\ntype inference and may indicate a misunderstanding of the generic type's\ninterface.\n\n## Examples\n\nUsing legacy type variables:\n```python\nfrom typing import Generic, TypeVar\n\nT1 = TypeVar('T1', int, str)\nT2 = TypeVar('T2', bound=int)\n\nclass Foo1(Generic[T1]): ...\nclass Foo2(Generic[T2]): ...\n\nFoo1[bytes] # error: bytes does not satisfy T1's constraints\nFoo2[str] # error: str does not satisfy T2's bound\n```\n\nUsing PEP 695 type variables:\n```python\nclass Foo[T]: ...\nclass Bar[T, U]: ...\n\nFoo[int, str] # error: too many arguments\nBar[int] # error: too few arguments\n```",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"invalid-type-checking-constant": {
"title": "detects invalid `TYPE_CHECKING` constant assignments",
"description": "## What it does\nChecks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an\nannotation not assignable from `bool`.\n\n## Why is this bad?\nThe name `TYPE_CHECKING` is reserved for a flag that can be used to provide conditional\ncode seen only by the type checker, and not at runtime. Normally this flag is imported from\n`typing` or `typing_extensions`, but it can also be defined locally. If defined locally, it\nmust be assigned the value `False` at runtime; the type checker will consider its value to\nbe `True`. If annotated, it must be annotated as a type that can accept `bool` values.\n\n## Examples\n```python\nTYPE_CHECKING: str\nTYPE_CHECKING = ''\n```",