diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index 663d84c0c77a1..a9af3a7a9b7f7 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -257,3 +257,16 @@ f(5) # error: [invalid-argument-type] "Argument to function `f` is incorrect: E def g(x: float): f(x) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `int | float`" ``` + +## Invariant generic classes + +We show a special diagnostic hint for invariant generic classes. For more details, see the +[`invalid_assignment_details.md`](./invalid_assignment_details.md) test. + +```py +def modify(xs: list[int]): + xs.append(42) + +xs: list[bool] = [True, False] +modify(xs) # error: [invalid-argument-type] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md index cc5f2b3d04dff..058135594e679 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md @@ -248,3 +248,95 @@ class Incompatible: def _(source: Incompatible): target: SupportsCheck = source # error: [invalid-assignment] ``` + +## Invariant generic classes + +We show a special diagnostic hint for invariant generic classes. For example, if you try to assign a +`list[bool]` to a `list[int]`: + +```py +def _(source: list[bool]): + target: list[int] = source # error: [invalid-assignment] +``` + +We do the same for other invariant generic classes: + +```py +from collections import ChainMap, Counter, OrderedDict, defaultdict, deque +from collections.abc import MutableSequence, MutableMapping, MutableSet + +def _(source: set[bool]): + target: set[int] = source # error: [invalid-assignment] + +def _(source: dict[str, bool]): + target: dict[str, int] = source # error: [invalid-assignment] + +def _(source: dict[bool, str]): + target: dict[int, str] = source # error: [invalid-assignment] + +def _(source: dict[bool, bool]): + target: dict[int, int] = source # error: [invalid-assignment] + +def _(source: defaultdict[str, bool]): + target: defaultdict[str, int] = source # error: [invalid-assignment] + +def _(source: defaultdict[bool, str]): + target: defaultdict[int, str] = source # error: [invalid-assignment] + +def _(source: OrderedDict[str, bool]): + target: OrderedDict[str, int] = source # error: [invalid-assignment] + +def _(source: OrderedDict[bool, str]): + target: OrderedDict[int, str] = source # error: [invalid-assignment] + +def _(source: ChainMap[str, bool]): + target: ChainMap[str, int] = source # error: [invalid-assignment] + +def _(source: ChainMap[bool, str]): + target: ChainMap[int, str] = source # error: [invalid-assignment] + +def _(source: deque[bool]): + target: deque[int] = source # error: [invalid-assignment] + +def _(source: Counter[bool]): + target: Counter[int] = source # error: [invalid-assignment] + +def _(source: MutableSequence[bool]): + target: MutableSequence[int] = source # error: [invalid-assignment] +``` + +We also show this hint for custom invariant generic classes: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class MyContainer(Generic[T]): + value: T + +def _(source: MyContainer[bool]): + target: MyContainer[int] = source # error: [invalid-assignment] +``` + +We do *not* show this hint if the element types themselves wouldn't be assignable: + +```py +def _(source: list[int]): + target: list[str] = source # error: [invalid-assignment] +``` + +We do not emit any error if the collection types are covariant: + +```py +from collections.abc import Sequence + +def _(source: list[bool]): + target: Sequence[int] = source + +def _(source: frozenset[bool]): + target: frozenset[int] = source + +def _(source: tuple[bool, bool]): + target: tuple[int, int] = source +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Invariant_generic_cl\342\200\246_(7ff1d501c5f64fe9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Invariant_generic_cl\342\200\246_(7ff1d501c5f64fe9).snap" new file mode 100644 index 0000000000000..de0006d126d61 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Invariant_generic_cl\342\200\246_(7ff1d501c5f64fe9).snap" @@ -0,0 +1,45 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Invariant generic classes +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def modify(xs: list[int]): +2 | xs.append(42) +3 | +4 | xs: list[bool] = [True, False] +5 | modify(xs) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `modify` is incorrect + --> src/mdtest_snippet.py:5:8 + | +4 | xs: list[bool] = [True, False] +5 | modify(xs) # error: [invalid-argument-type] + | ^^ Expected `list[int]`, found `list[bool]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def modify(xs: list[int]): + | ^^^^^^ ------------- Parameter declared here +2 | xs.append(42) + | +info: `list` is invariant in its type parameter +info: Consider using the covariant supertype `collections.abc.Sequence` +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Invariant_generic_cl\342\200\246_(4083c269b4d4746f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Invariant_generic_cl\342\200\246_(4083c269b4d4746f).snap" new file mode 100644 index 0000000000000..e468b6786e2a6 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment_d\342\200\246_-_Invalid_assignment_d\342\200\246_-_Invariant_generic_cl\342\200\246_(4083c269b4d4746f).snap" @@ -0,0 +1,374 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Invariant generic classes +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def _(source: list[bool]): + 2 | target: list[int] = source # error: [invalid-assignment] + 3 | from collections import ChainMap, Counter, OrderedDict, defaultdict, deque + 4 | from collections.abc import MutableSequence, MutableMapping, MutableSet + 5 | + 6 | def _(source: set[bool]): + 7 | target: set[int] = source # error: [invalid-assignment] + 8 | + 9 | def _(source: dict[str, bool]): +10 | target: dict[str, int] = source # error: [invalid-assignment] +11 | +12 | def _(source: dict[bool, str]): +13 | target: dict[int, str] = source # error: [invalid-assignment] +14 | +15 | def _(source: dict[bool, bool]): +16 | target: dict[int, int] = source # error: [invalid-assignment] +17 | +18 | def _(source: defaultdict[str, bool]): +19 | target: defaultdict[str, int] = source # error: [invalid-assignment] +20 | +21 | def _(source: defaultdict[bool, str]): +22 | target: defaultdict[int, str] = source # error: [invalid-assignment] +23 | +24 | def _(source: OrderedDict[str, bool]): +25 | target: OrderedDict[str, int] = source # error: [invalid-assignment] +26 | +27 | def _(source: OrderedDict[bool, str]): +28 | target: OrderedDict[int, str] = source # error: [invalid-assignment] +29 | +30 | def _(source: ChainMap[str, bool]): +31 | target: ChainMap[str, int] = source # error: [invalid-assignment] +32 | +33 | def _(source: ChainMap[bool, str]): +34 | target: ChainMap[int, str] = source # error: [invalid-assignment] +35 | +36 | def _(source: deque[bool]): +37 | target: deque[int] = source # error: [invalid-assignment] +38 | +39 | def _(source: Counter[bool]): +40 | target: Counter[int] = source # error: [invalid-assignment] +41 | +42 | def _(source: MutableSequence[bool]): +43 | target: MutableSequence[int] = source # error: [invalid-assignment] +44 | from typing import Generic, TypeVar +45 | +46 | T = TypeVar("T") +47 | +48 | class MyContainer(Generic[T]): +49 | value: T +50 | +51 | def _(source: MyContainer[bool]): +52 | target: MyContainer[int] = source # error: [invalid-assignment] +53 | def _(source: list[int]): +54 | target: list[str] = source # error: [invalid-assignment] +55 | from collections.abc import Sequence +56 | +57 | def _(source: list[bool]): +58 | target: Sequence[int] = source +59 | +60 | def _(source: frozenset[bool]): +61 | target: frozenset[int] = source +62 | +63 | def _(source: tuple[bool, bool]): +64 | target: tuple[int, int] = source +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `list[bool]` is not assignable to `list[int]` + --> src/mdtest_snippet.py:2:13 + | +1 | def _(source: list[bool]): +2 | target: list[int] = source # error: [invalid-assignment] + | --------- ^^^^^^ Incompatible value of type `list[bool]` + | | + | Declared type +3 | from collections import ChainMap, Counter, OrderedDict, defaultdict, deque +4 | from collections.abc import MutableSequence, MutableMapping, MutableSet + | +info: `list` is invariant in its type parameter +info: Consider using the covariant supertype `collections.abc.Sequence` +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `set[bool]` is not assignable to `set[int]` + --> src/mdtest_snippet.py:7:13 + | +6 | def _(source: set[bool]): +7 | target: set[int] = source # error: [invalid-assignment] + | -------- ^^^^^^ Incompatible value of type `set[bool]` + | | + | Declared type +8 | +9 | def _(source: dict[str, bool]): + | +info: `set` is invariant in its type parameter +info: Consider using the covariant supertype `collections.abc.Set` +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `dict[str, bool]` is not assignable to `dict[str, int]` + --> src/mdtest_snippet.py:10:13 + | + 9 | def _(source: dict[str, bool]): +10 | target: dict[str, int] = source # error: [invalid-assignment] + | -------------- ^^^^^^ Incompatible value of type `dict[str, bool]` + | | + | Declared type +11 | +12 | def _(source: dict[bool, str]): + | +info: `dict` is invariant in its second type parameter +info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `dict[bool, str]` is not assignable to `dict[int, str]` + --> src/mdtest_snippet.py:13:13 + | +12 | def _(source: dict[bool, str]): +13 | target: dict[int, str] = source # error: [invalid-assignment] + | -------------- ^^^^^^ Incompatible value of type `dict[bool, str]` + | | + | Declared type +14 | +15 | def _(source: dict[bool, bool]): + | +info: `dict` is invariant in its first type parameter +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `dict[bool, bool]` is not assignable to `dict[int, int]` + --> src/mdtest_snippet.py:16:13 + | +15 | def _(source: dict[bool, bool]): +16 | target: dict[int, int] = source # error: [invalid-assignment] + | -------------- ^^^^^^ Incompatible value of type `dict[bool, bool]` + | | + | Declared type +17 | +18 | def _(source: defaultdict[str, bool]): + | +info: `dict` is invariant in its first and second type parameters +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `defaultdict[str, bool]` is not assignable to `defaultdict[str, int]` + --> src/mdtest_snippet.py:19:13 + | +18 | def _(source: defaultdict[str, bool]): +19 | target: defaultdict[str, int] = source # error: [invalid-assignment] + | --------------------- ^^^^^^ Incompatible value of type `defaultdict[str, bool]` + | | + | Declared type +20 | +21 | def _(source: defaultdict[bool, str]): + | +info: `defaultdict` is invariant in its second type parameter +info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `defaultdict[bool, str]` is not assignable to `defaultdict[int, str]` + --> src/mdtest_snippet.py:22:13 + | +21 | def _(source: defaultdict[bool, str]): +22 | target: defaultdict[int, str] = source # error: [invalid-assignment] + | --------------------- ^^^^^^ Incompatible value of type `defaultdict[bool, str]` + | | + | Declared type +23 | +24 | def _(source: OrderedDict[str, bool]): + | +info: `defaultdict` is invariant in its first type parameter +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `OrderedDict[str, bool]` is not assignable to `OrderedDict[str, int]` + --> src/mdtest_snippet.py:25:13 + | +24 | def _(source: OrderedDict[str, bool]): +25 | target: OrderedDict[str, int] = source # error: [invalid-assignment] + | --------------------- ^^^^^^ Incompatible value of type `OrderedDict[str, bool]` + | | + | Declared type +26 | +27 | def _(source: OrderedDict[bool, str]): + | +info: `OrderedDict` is invariant in its second type parameter +info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `OrderedDict[bool, str]` is not assignable to `OrderedDict[int, str]` + --> src/mdtest_snippet.py:28:13 + | +27 | def _(source: OrderedDict[bool, str]): +28 | target: OrderedDict[int, str] = source # error: [invalid-assignment] + | --------------------- ^^^^^^ Incompatible value of type `OrderedDict[bool, str]` + | | + | Declared type +29 | +30 | def _(source: ChainMap[str, bool]): + | +info: `OrderedDict` is invariant in its first type parameter +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `ChainMap[str, bool]` is not assignable to `ChainMap[str, int]` + --> src/mdtest_snippet.py:31:13 + | +30 | def _(source: ChainMap[str, bool]): +31 | target: ChainMap[str, int] = source # error: [invalid-assignment] + | ------------------ ^^^^^^ Incompatible value of type `ChainMap[str, bool]` + | | + | Declared type +32 | +33 | def _(source: ChainMap[bool, str]): + | +info: `ChainMap` is invariant in its second type parameter +info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `ChainMap[bool, str]` is not assignable to `ChainMap[int, str]` + --> src/mdtest_snippet.py:34:13 + | +33 | def _(source: ChainMap[bool, str]): +34 | target: ChainMap[int, str] = source # error: [invalid-assignment] + | ------------------ ^^^^^^ Incompatible value of type `ChainMap[bool, str]` + | | + | Declared type +35 | +36 | def _(source: deque[bool]): + | +info: `ChainMap` is invariant in its first type parameter +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `deque[bool]` is not assignable to `deque[int]` + --> src/mdtest_snippet.py:37:13 + | +36 | def _(source: deque[bool]): +37 | target: deque[int] = source # error: [invalid-assignment] + | ---------- ^^^^^^ Incompatible value of type `deque[bool]` + | | + | Declared type +38 | +39 | def _(source: Counter[bool]): + | +info: `deque` is invariant in its type parameter +info: Consider using the covariant supertype `collections.abc.Sequence` +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `Counter[bool]` is not assignable to `Counter[int]` + --> src/mdtest_snippet.py:40:13 + | +39 | def _(source: Counter[bool]): +40 | target: Counter[int] = source # error: [invalid-assignment] + | ------------ ^^^^^^ Incompatible value of type `Counter[bool]` + | | + | Declared type +41 | +42 | def _(source: MutableSequence[bool]): + | +info: `Counter` is invariant in its type parameter +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `MutableSequence[bool]` is not assignable to `MutableSequence[int]` + --> src/mdtest_snippet.py:43:13 + | +42 | def _(source: MutableSequence[bool]): +43 | target: MutableSequence[int] = source # error: [invalid-assignment] + | -------------------- ^^^^^^ Incompatible value of type `MutableSequence[bool]` + | | + | Declared type +44 | from typing import Generic, TypeVar + | +info: `MutableSequence` is invariant in its type parameter +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `MyContainer[bool]` is not assignable to `MyContainer[int]` + --> src/mdtest_snippet.py:52:13 + | +51 | def _(source: MyContainer[bool]): +52 | target: MyContainer[int] = source # error: [invalid-assignment] + | ---------------- ^^^^^^ Incompatible value of type `MyContainer[bool]` + | | + | Declared type +53 | def _(source: list[int]): +54 | target: list[str] = source # error: [invalid-assignment] + | +info: `MyContainer` is invariant in its type parameter +info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `list[int]` is not assignable to `list[str]` + --> src/mdtest_snippet.py:54:13 + | +52 | target: MyContainer[int] = source # error: [invalid-assignment] +53 | def _(source: list[int]): +54 | target: list[str] = source # error: [invalid-assignment] + | --------- ^^^^^^ Incompatible value of type `list[int]` + | | + | Declared type +55 | from collections.abc import Sequence + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 91bfabf8fc5ad..c2f38727e1e9a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1044,18 +1044,22 @@ impl<'db> Type<'db> { self.specialization_of_optional(db, None) } + /// If this type is a class instance, returns its class. + pub(crate) fn nominal_class(self, db: &'db dyn Db) -> Option> { + match self { + Type::NominalInstance(instance) => Some(instance.class(db)), + Type::ProtocolInstance(instance) => instance.to_nominal_instance().map(|i| i.class(db)), + Type::TypeAlias(alias) => alias.value_type(db).nominal_class(db), + _ => None, + } + } + fn specialization_of_optional( self, db: &'db dyn Db, expected_class: Option>, ) -> Option> { - let class_type = match self { - Type::NominalInstance(instance) => instance, - Type::ProtocolInstance(instance) => instance.to_nominal_instance()?, - Type::TypeAlias(alias) => alias.value_type(db).as_nominal_instance()?, - _ => return None, - } - .class(db); + let class_type = self.nominal_class(db)?; let (class_literal, specialization) = class_type.static_class_literal(db)?; if expected_class.is_some_and(|expected_class| expected_class != class_literal) { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 1926b2eec17f6..142dd9c6a50c4 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -32,7 +32,7 @@ use crate::types::diagnostic::{ CALL_NON_CALLABLE, CALL_TOP_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, INVALID_DATACLASS, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSITIONAL_ONLY_PARAMETER_AS_KWARG, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, - note_numbers_module_not_supported, + add_invariant_generic_hints, note_numbers_module_not_supported, }; use crate::types::enums::is_enum_class; use crate::types::function::{ @@ -5273,6 +5273,8 @@ impl<'db> BindingError<'db> { *provided_ty, ); } + + add_invariant_generic_hints(context.db(), &mut diag, *expected_ty, *provided_ty); } Self::InvalidKeyType { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index e311cf4f49cfa..7c04e5a4498d4 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -29,8 +29,8 @@ use crate::types::typed_dict::TypedDictSchema; use crate::types::typevar::TypeVarInstance; use crate::types::{ BoundTypeVarInstance, ClassType, DynamicType, LintDiagnosticGuard, Protocol, - ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, - protocol_class::ProtocolClass, + ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, TypeVarVariance, + binding_type, protocol_class::ProtocolClass, }; use crate::types::{KnownInstanceType, MemberLookupPolicy, UnionType}; use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; @@ -3447,6 +3447,109 @@ pub(super) fn note_numbers_module_not_supported<'db>( } } +fn covariant_supertype_hint<'db>( + class: ClassType<'db>, + db: &'db dyn Db, + mismatched_invariant_parameters: &[usize], +) -> Option<&'static str> { + match (class.known(db), mismatched_invariant_parameters) { + (Some(KnownClass::List | KnownClass::Deque), [0]) => { + Some("Consider using the covariant supertype `collections.abc.Sequence`") + } + (Some(KnownClass::Set), [0]) => { + Some("Consider using the covariant supertype `collections.abc.Set`") + } + ( + Some( + KnownClass::Dict + | KnownClass::DefaultDict + | KnownClass::OrderedDict + | KnownClass::ChainMap, + ), + [1], + ) => Some( + "Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type", + ), + _ => None, + } +} + +/// Add a diagnostic hint for cases like an invalid `list[bool]` to `list[int]` assignment, +/// that fails due to invariance. +pub(super) fn add_invariant_generic_hints<'db>( + db: &'db dyn Db, + diag: &mut Diagnostic, + expected_ty: Type<'db>, + provided_ty: Type<'db>, +) { + let Some(expected_class) = expected_ty.nominal_class(db) else { + return; + }; + let Some(provided_class) = provided_ty.nominal_class(db) else { + return; + }; + let Some(expected_specialization) = expected_ty.class_specialization(db) else { + return; + }; + let Some(provided_specialization) = provided_ty.class_specialization(db) else { + return; + }; + + if expected_class.class_literal(db) != provided_class.class_literal(db) { + return; + } + + let generic_context = expected_specialization.generic_context(db); + if generic_context != provided_specialization.generic_context(db) { + return; + } + + let mismatched_invariant_arguments = generic_context + .variables(db) + .zip(expected_specialization.types(db)) + .zip(provided_specialization.types(db)) + .enumerate() + .filter_map(|(index, ((bound_typevar, expected_arg), provided_arg))| { + (bound_typevar.variance(db) == TypeVarVariance::Invariant + && !expected_arg.is_equivalent_to(db, *provided_arg)) + .then_some((index, expected_arg, provided_arg)) + }); + + let mut mismatch_indices = Vec::new(); + for (index, expected_arg, provided_arg) in mismatched_invariant_arguments { + if !provided_arg.is_assignable_to(db, *expected_arg) { + return; + } + mismatch_indices.push(index); + } + + if mismatch_indices.is_empty() { + return; + } + + let class_name = expected_class.name(db); + let message = match (generic_context.len(db), mismatch_indices.as_slice()) { + (1, _) => { + format!("`{class_name}` is invariant in its type parameter") + } + (_, [0]) => format!("`{class_name}` is invariant in its first type parameter"), + (_, [1]) => format!("`{class_name}` is invariant in its second type parameter"), + (_, [2]) => format!("`{class_name}` is invariant in its third type parameter"), + (2, [0, 1]) => { + format!("`{class_name}` is invariant in its first and second type parameters") + } + _ => format!("`{class_name}` is invariant in (one of) its type parameters"), + }; + diag.info(message); + + if let Some(note) = covariant_supertype_hint(expected_class, db, &mismatch_indices) { + diag.info(note); + } + diag.info( + "For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics", + ); +} + pub(super) fn report_invalid_assignment<'db>( context: &InferContext<'db, '_>, target_node: AnyNodeRef, @@ -3531,6 +3634,7 @@ pub(super) fn report_invalid_assignment<'db>( // special case message note_numbers_module_not_supported(context.db(), &mut diag, target_ty, value_ty); + add_invariant_generic_hints(context.db(), &mut diag, target_ty, value_ty); } pub(super) fn report_invalid_attribute_assignment(