diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 417cf7d1f5c40..66450ffcdbd48 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -67,7 +67,7 @@ jobs: cd .. - uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4" + uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf" ecosystem-analyzer \ --repository ruff \ diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 2078478505d78..ca4d28785d641 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -52,7 +52,7 @@ jobs: cd .. - uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4" + uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf" ecosystem-analyzer \ --verbose \ diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_(d3d47de65fb3bad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_(d3d47de65fb3bad).snap" index 6081e0f5d9193..6322ef6de6237 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_(d3d47de65fb3bad).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_(d3d47de65fb3bad).snap" @@ -19,12 +19,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, int].__setitem__(key: str, value: int, /) -> None` cannot be called with a key of type `Literal[0]` and a value of type `Literal[3]` on object of type `dict[str, int]` +error[invalid-assignment]: Invalid subscript assignment with key of type `Literal[0]` and value of type `Literal[3]` on object of type `dict[str, int]` --> src/mdtest_snippet.py:2:1 | 1 | config: dict[str, int] = {} 2 | config[0] = 3 # error: [invalid-assignment] - | ^^^^^^ + | ^^^^^^^-^^^^^ + | | + | Expected key of type `str`, got `Literal[0]` | info: rule `invalid-assignment` is enabled by default diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" index 2fbfb5323f087..9fa4cb2eaf9b8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" @@ -24,7 +24,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-key]: Cannot access `Config` with a key of type `Literal[0]`. Only string literals are allowed as keys on TypedDicts. +error[invalid-key]: TypedDict `Config` can only be subscripted with a string literal key, got key of type `Literal[0]`. --> src/mdtest_snippet.py:7:12 | 6 | def _(config: Config) -> None: diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_(f87bd015df018509).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_(f87bd015df018509).snap" index 125bcdfac1d87..6f44b42b5177a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_(f87bd015df018509).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_(f87bd015df018509).snap" @@ -19,12 +19,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, int].__setitem__(key: str, value: int, /) -> None` cannot be called with a key of type `Literal["retries"]` and a value of type `Literal["three"]` on object of type `dict[str, int]` +error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `Literal["three"]` on object of type `dict[str, int]` --> src/mdtest_snippet.py:2:1 | 1 | config: dict[str, int] = {} 2 | config["retries"] = "three" # error: [invalid-assignment] - | ^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^------- + | | + | Expected value of type `int`, got `Literal["three"]` | info: rule `invalid-assignment` is enabled by default diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" index f9c43e5882a26..0cccff66dc762 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" @@ -23,13 +23,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-assignment]: Cannot assign to a subscript on an object of type `ReadOnlyDict` with no `__setitem__` method +error[invalid-assignment]: Cannot assign to a subscript on an object of type `ReadOnlyDict` --> src/mdtest_snippet.py:6:1 | 5 | config = ReadOnlyDict() 6 | config["retries"] = 3 # error: [invalid-assignment] - | ^^^^^^ + | ^^^^^^^^^^^^^^^^^ | +help: Consider adding a `__setitem__` method to `ReadOnlyDict`. info: rule `invalid-assignment` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" index a12bb7c666b44..ce91a02c7093e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" @@ -19,14 +19,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-assignment]: Cannot assign to a subscript on an object of type `None` with no `__setitem__` method +error[invalid-assignment]: Cannot assign to a subscript on an object of type `None` --> src/mdtest_snippet.py:2:5 | 1 | def _(config: dict[str, int] | None) -> None: 2 | config["retries"] = 3 # error: [invalid-assignment] - | ^^^^^^ + | ^^^^^^^^^^^^^^^^^ | info: The full type of the subscripted object is `dict[str, int] | None` +info: `None` does not have a `__setitem__` method. info: rule `invalid-assignment` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" index 0f603931aa6ae..5df60709e7b89 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" @@ -19,12 +19,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, str].__setitem__(key: str, value: str, /) -> None` cannot be called with a key of type `Literal["retries"]` and a value of type `Literal[3]` on object of type `dict[str, str]` +error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `Literal[3]` on object of type `dict[str, str]` --> src/mdtest_snippet.py:2:5 | 1 | def _(config: dict[str, int] | dict[str, str]) -> None: 2 | config["retries"] = 3 # error: [invalid-assignment] - | ^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^- + | | + | Expected value of type `str`, got `Literal[3]` | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` info: rule `invalid-assignment` is enabled by default diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" index 635a402c9bba6..e5f29d9a10702 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" @@ -21,13 +21,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, int].__setitem__(key: str, value: int, /) -> None` cannot be called with a key of type `Literal["retries"]` and a value of type `float` on object of type `dict[str, int]` +error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `float` on object of type `dict[str, int]` --> src/mdtest_snippet.py:4:5 | 2 | # error: [invalid-assignment] 3 | # error: [invalid-assignment] 4 | config["retries"] = 3.0 - | ^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^--- + | | + | Expected value of type `int`, got `float` | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` info: rule `invalid-assignment` is enabled by default @@ -35,13 +37,15 @@ info: rule `invalid-assignment` is enabled by default ``` ``` -error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, str].__setitem__(key: str, value: str, /) -> None` cannot be called with a key of type `Literal["retries"]` and a value of type `float` on object of type `dict[str, str]` +error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `float` on object of type `dict[str, str]` --> src/mdtest_snippet.py:4:5 | 2 | # error: [invalid-assignment] 3 | # error: [invalid-assignment] 4 | config["retries"] = 3.0 - | ^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^--- + | | + | Expected value of type `str`, got `float` | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` info: rule `invalid-assignment` is enabled by default diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index 51b0f0ce69107..97c058d25820f 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -89,7 +89,7 @@ info: rule `invalid-key` is enabled by default ``` ``` -error[invalid-key]: Invalid key of type `str` for TypedDict `Person` +error[invalid-key]: TypedDict `Person` can only be subscripted with string literal keys, got key of type `str` --> src/mdtest_snippet.py:16:12 | 15 | def access_with_str_key(person: Person, str_key: str): @@ -146,7 +146,7 @@ info: rule `invalid-key` is enabled by default ``` ``` -error[invalid-key]: Cannot access `Person` with a key of type `str`. Only string literals are allowed as keys on TypedDicts. +error[invalid-key]: TypedDict `Person` can only be subscripted with a string literal key, got key of type `str`. --> src/mdtest_snippet.py:25:12 | 24 | def write_to_non_literal_string_key(person: Person, str_key: str): diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md index b15ec4abc1f4a..de96b7ac6c8d8 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md @@ -76,7 +76,7 @@ a[0] = 0 class NoSetitem: ... a = NoSetitem() -a[0] = 0 # error: "Cannot assign to a subscript on an object of type `NoSetitem` with no `__setitem__` method" +a[0] = 0 # error: "Cannot assign to a subscript on an object of type `NoSetitem`" ``` ## `__setitem__` not callable diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 422711b4c1aed..a6a6caa6b8901 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -69,7 +69,7 @@ def name_or_age() -> Literal["name", "age"]: carol: Person = {NAME: "Carol", AGE: 20} reveal_type(carol[NAME]) # revealed: str -# error: [invalid-key] "Invalid key of type `str` for TypedDict `Person`" +# error: [invalid-key] "TypedDict `Person` can only be subscripted with string literal keys, got key of type `str`" reveal_type(carol[non_literal()]) # revealed: Unknown reveal_type(carol[name_or_age()]) # revealed: str | int | None @@ -553,7 +553,7 @@ def _( # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing"" reveal_type(person["non_existing"]) # revealed: Unknown - # error: [invalid-key] "Invalid key of type `str` for TypedDict `Person`" + # error: [invalid-key] "TypedDict `Person` can only be subscripted with string literal keys, got key of type `str`" reveal_type(person[str_key]) # revealed: Unknown # No error here: @@ -631,10 +631,10 @@ def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any) person[union_of_keys] = None def _(person: Person, str_key: str, literalstr_key: LiteralString): - # error: [invalid-key] "Cannot access `Person` with a key of type `str`. Only string literals are allowed as keys on TypedDicts." + # error: [invalid-key] "TypedDict `Person` can only be subscripted with a string literal key, got key of type `str`." person[str_key] = None - # error: [invalid-key] "Cannot access `Person` with a key of type `LiteralString`. Only string literals are allowed as keys on TypedDicts." + # error: [invalid-key] "TypedDict `Person` can only be subscripted with a string literal key, got key of type `LiteralString`." person[literalstr_key] = None def _(person: Person, unknown_key: Any): diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 25c1efa27d25f..bdbc1feb9f692 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -3107,9 +3107,10 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( } _ => { let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid key of type `{}` for TypedDict `{}`", - key_ty.display(db), + "TypedDict `{}` can only be subscripted with string literal keys, \ + got key of type `{}`", typed_dict_ty.display(db), + key_ty.display(db), )); if let Some(full_object_ty) = full_object_ty { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 2c445f92eaa0a..5d96aa6ee072a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3557,10 +3557,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let slice_ty = self.infer_expression(slice, TypeContext::default()); self.validate_subscript_assignment_impl( - object.as_ref(), + target, None, object_ty, - slice.as_ref(), slice_ty, rhs_value, rhs_value_ty, @@ -3571,10 +3570,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { #[expect(clippy::too_many_arguments)] fn validate_subscript_assignment_impl( &self, - object_node: &'ast ast::Expr, + target: &'ast ast::ExprSubscript, full_object_ty: Option>, object_ty: Type<'db>, - slice_node: &'ast ast::Expr, slice_ty: Type<'db>, rhs_value_node: &'ast ast::Expr, rhs_value_ty: Type<'db>, @@ -3602,7 +3600,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); - let attach_original_type_info = |mut diagnostic: LintDiagnosticGuard| { + let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { if let Some(full_object_ty) = full_object_ty { diagnostic.info(format_args!( "The full type of the subscripted object is `{}`", @@ -3618,10 +3616,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut valid = true; for element_ty in union.elements(db) { valid &= self.validate_subscript_assignment_impl( - object_node, + target, full_object_ty.or(Some(object_ty)), *element_ty, - slice_node, slice_ty, rhs_value_node, rhs_value_ty, @@ -3636,10 +3633,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut valid = false; for element_ty in intersection.positive(db) { valid |= self.validate_subscript_assignment_impl( - object_node, + target, full_object_ty.or(Some(object_ty)), *element_ty, - slice_node, slice_ty, rhs_value_node, rhs_value_ty, @@ -3676,7 +3672,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // or a dynamic type like `Any`. We can do this by checking assignability to `LiteralString`, // but we need to exclude `LiteralString` itself. This check would technically allow weird key // types like `LiteralString & Any` to pass, but it does not need to be perfect. We would just - // fail to provide the "Only string literals are allowed" hint in that case. + // fail to provide the "can only be subscripted with string literal keys" hint in that case. if slice_ty.is_dynamic() { return true; @@ -3688,22 +3684,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if slice_ty.is_assignable_to(db, Type::LiteralString) && !slice_ty.is_equivalent_to(db, Type::LiteralString) { - if let Some(builder) = - self.context.report_lint(&INVALID_ASSIGNMENT, slice_node) + if let Some(builder) = self + .context + .report_lint(&INVALID_ASSIGNMENT, target.slice.as_ref()) { - let diagnostic = builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign value of type `{assigned_d}` to key of type `{}` on TypedDict `{value_d}`", slice_ty.display(db) )); - attach_original_type_info(diagnostic); + attach_original_type_info(&mut diagnostic); } } else { - if let Some(builder) = self.context.report_lint(&INVALID_KEY, slice_node) { - let diagnostic = builder.into_diagnostic(format_args!( - "Cannot access `{value_d}` with a key of type `{}`. Only string literals are allowed as keys on TypedDicts.", + if let Some(builder) = self + .context + .report_lint(&INVALID_KEY, target.slice.as_ref()) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "TypedDict `{value_d}` can only be subscripted with a string literal key, got key of type `{}`.", slice_ty.display(db) )); - attach_original_type_info(diagnostic); + attach_original_type_info(&mut diagnostic); } } @@ -3717,8 +3717,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { full_object_ty, key, rhs_value_ty, - object_node, - slice_node, + target.value.as_ref(), + target.slice.as_ref(), rhs_value_node, TypedDictAssignmentKind::Subscript, emit_diagnostic, @@ -3741,13 +3741,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if emit_diagnostic && let Some(builder) = self .context - .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, rhs_value_node) + .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) { - let diagnostic = builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` may be missing", object_ty.display(db), )); - attach_original_type_info(diagnostic); + attach_original_type_info(&mut diagnostic); } false } @@ -3755,17 +3755,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match call_error_kind { CallErrorKind::NotCallable => { if emit_diagnostic - && let Some(builder) = self - .context - .report_lint(&CALL_NON_CALLABLE, object_node) + && let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) { - let diagnostic = builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` is not callable \ on object of type `{}`", bindings.callable_type().display(db), object_ty.display(db), )); - attach_original_type_info(diagnostic); + attach_original_type_info(&mut diagnostic); } } CallErrorKind::BindingError => { @@ -3778,8 +3777,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { full_object_ty, key, rhs_value_ty, - object_node, - slice_node, + target.value.as_ref(), + target.slice.as_ref(), rhs_value_node, TypedDictAssignmentKind::Subscript, true, @@ -3787,35 +3786,77 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } else { if emit_diagnostic - && let Some(builder) = self - .context - .report_lint(&INVALID_ASSIGNMENT, object_node) + && let Some(builder) = self.context.report_lint( + &INVALID_ASSIGNMENT, + target.range.cover(rhs_value_node.range()), + ) { let assigned_d = rhs_value_ty.display(db); - let value_d = object_ty.display(db); + let object_d = object_ty.display(db); - let diagnostic = builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` cannot be called with \ - a key of type `{}` and a value of type `{assigned_d}` on object of type `{value_d}`", - bindings.callable_type().display(db), - slice_ty.display(db), - )); - attach_original_type_info(diagnostic); + // Special diagnostic for dictionaries + if let Some([expected_key_ty, expected_value_ty]) = + object_ty + .known_specialization(db, KnownClass::Dict) + .map(|s| s.types(db)) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid subscript assignment with key of type `{}` and value of \ + type `{assigned_d}` on object of type `{object_d}`", + slice_ty.display(db), + )); + + if !slice_ty.is_assignable_to(db, *expected_key_ty) + { + diagnostic.annotate( + self.context + .secondary(target.slice.as_ref()) + .message(format_args!( + "Expected key of type `{}`, got `{}`", + expected_key_ty.display(db), + slice_ty.display(db), + )), + ); + } + + if !rhs_value_ty + .is_assignable_to(db, *expected_value_ty) + { + diagnostic.annotate( + self.context + .secondary(rhs_value_node) + .message(format_args!( + "Expected value of type `{}`, got `{}`", + expected_value_ty.display(db), + rhs_value_ty.display(db), + )), + ); + } + + attach_original_type_info(&mut diagnostic); + } else { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__setitem__` of type `{}` cannot be called with \ + a key of type `{}` and a value of type `{assigned_d}` on object of type `{object_d}`", + bindings.callable_type().display(db), + slice_ty.display(db), + )); + attach_original_type_info(&mut diagnostic); + } } } } CallErrorKind::PossiblyNotCallable => { if emit_diagnostic - && let Some(builder) = self - .context - .report_lint(&CALL_NON_CALLABLE, object_node) + && let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) { - let diagnostic = builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` may not be callable on object of type `{}`", bindings.callable_type().display(db), object_ty.display(db), )); - attach_original_type_info(diagnostic); + attach_original_type_info(&mut diagnostic); } } } @@ -3824,13 +3865,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { CallDunderError::MethodNotAvailable => { if emit_diagnostic && let Some(builder) = - self.context.report_lint(&INVALID_ASSIGNMENT, object_node) + self.context.report_lint(&INVALID_ASSIGNMENT, target) { - let diagnostic = builder.into_diagnostic(format_args!( - "Cannot assign to a subscript on an object of type `{}` with no `__setitem__` method", + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot assign to a subscript on an object of type `{}`", object_ty.display(db), )); - attach_original_type_info(diagnostic); + attach_original_type_info(&mut diagnostic); + + // Use `KnownClass` as a crude proxy for checking if this is not a user-defined class. Otherwise, + // we end up suggesting things like "Consider adding a `__setitem__` method to `None`". + if object_ty + .as_nominal_instance() + .is_some_and(|instance| instance.class(db).known(db).is_some()) + { + diagnostic.info(format_args!( + "`{}` does not have a `__setitem__` method.", + object_ty.display(db), + )); + } else { + diagnostic.help(format_args!( + "Consider adding a `__setitem__` method to `{}`.", + object_ty.display(db), + )); + } } false }