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```",