[ty] Further improve subscript assignment diagnostics (#21411)

## Summary

Further improve subscript assignment diagnostics, especially for
`dict`s:

```py
config: dict[str, int] = {}

config["retries"] = "three"
```

<img width="1276" height="274" alt="image"
src="https://github.com/user-attachments/assets/9762c733-8d1c-4a57-8c8a-99825071dc7d"
/>

I have many more ideas, but this looks like a reasonable first step.
Thank you @AlexWaygood for some of the suggestions here.

## Test Plan

Update tests
This commit is contained in:
David Peter 2025-11-13 13:31:14 +01:00 committed by GitHub
parent 12e74ae894
commit 04ab9170d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 147 additions and 76 deletions

View file

@ -67,7 +67,7 @@ jobs:
cd .. 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 \ ecosystem-analyzer \
--repository ruff \ --repository ruff \

View file

@ -52,7 +52,7 @@ jobs:
cd .. 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 \ ecosystem-analyzer \
--verbose \ --verbose \

View file

@ -19,12 +19,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics # 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 --> src/mdtest_snippet.py:2:1
| |
1 | config: dict[str, int] = {} 1 | config: dict[str, int] = {}
2 | config[0] = 3 # error: [invalid-assignment] 2 | config[0] = 3 # error: [invalid-assignment]
| ^^^^^^ | ^^^^^^^-^^^^^
| |
| Expected key of type `str`, got `Literal[0]`
| |
info: rule `invalid-assignment` is enabled by default info: rule `invalid-assignment` is enabled by default

View file

@ -24,7 +24,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics # 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 --> src/mdtest_snippet.py:7:12
| |
6 | def _(config: Config) -> None: 6 | def _(config: Config) -> None:

View file

@ -19,12 +19,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics # 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 --> src/mdtest_snippet.py:2:1
| |
1 | config: dict[str, int] = {} 1 | config: dict[str, int] = {}
2 | config["retries"] = "three" # error: [invalid-assignment] 2 | config["retries"] = "three" # error: [invalid-assignment]
| ^^^^^^ | ^^^^^^^^^^^^^^^^^^^^-------
| |
| Expected value of type `int`, got `Literal["three"]`
| |
info: rule `invalid-assignment` is enabled by default info: rule `invalid-assignment` is enabled by default

View file

@ -23,13 +23,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics # 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 --> src/mdtest_snippet.py:6:1
| |
5 | config = ReadOnlyDict() 5 | config = ReadOnlyDict()
6 | config["retries"] = 3 # error: [invalid-assignment] 6 | config["retries"] = 3 # error: [invalid-assignment]
| ^^^^^^ | ^^^^^^^^^^^^^^^^^
| |
help: Consider adding a `__setitem__` method to `ReadOnlyDict`.
info: rule `invalid-assignment` is enabled by default info: rule `invalid-assignment` is enabled by default
``` ```

View file

@ -19,14 +19,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics # 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 --> src/mdtest_snippet.py:2:5
| |
1 | def _(config: dict[str, int] | None) -> None: 1 | def _(config: dict[str, int] | None) -> None:
2 | config["retries"] = 3 # error: [invalid-assignment] 2 | config["retries"] = 3 # error: [invalid-assignment]
| ^^^^^^ | ^^^^^^^^^^^^^^^^^
| |
info: The full type of the subscripted object is `dict[str, int] | None` 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 info: rule `invalid-assignment` is enabled by default
``` ```

View file

@ -19,12 +19,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics # 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 --> src/mdtest_snippet.py:2:5
| |
1 | def _(config: dict[str, int] | dict[str, str]) -> None: 1 | def _(config: dict[str, int] | dict[str, str]) -> None:
2 | config["retries"] = 3 # error: [invalid-assignment] 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: The full type of the subscripted object is `dict[str, int] | dict[str, str]`
info: rule `invalid-assignment` is enabled by default info: rule `invalid-assignment` is enabled by default

View file

@ -21,13 +21,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics # 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 --> src/mdtest_snippet.py:4:5
| |
2 | # error: [invalid-assignment] 2 | # error: [invalid-assignment]
3 | # error: [invalid-assignment] 3 | # error: [invalid-assignment]
4 | config["retries"] = 3.0 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: The full type of the subscripted object is `dict[str, int] | dict[str, str]`
info: rule `invalid-assignment` is enabled by default 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 --> src/mdtest_snippet.py:4:5
| |
2 | # error: [invalid-assignment] 2 | # error: [invalid-assignment]
3 | # error: [invalid-assignment] 3 | # error: [invalid-assignment]
4 | config["retries"] = 3.0 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: The full type of the subscripted object is `dict[str, int] | dict[str, str]`
info: rule `invalid-assignment` is enabled by default info: rule `invalid-assignment` is enabled by default

View file

@ -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 --> src/mdtest_snippet.py:16:12
| |
15 | def access_with_str_key(person: Person, str_key: str): 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 --> src/mdtest_snippet.py:25:12
| |
24 | def write_to_non_literal_string_key(person: Person, str_key: str): 24 | def write_to_non_literal_string_key(person: Person, str_key: str):

View file

@ -76,7 +76,7 @@ a[0] = 0
class NoSetitem: ... class NoSetitem: ...
a = 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 ## `__setitem__` not callable

View file

@ -69,7 +69,7 @@ def name_or_age() -> Literal["name", "age"]:
carol: Person = {NAME: "Carol", AGE: 20} carol: Person = {NAME: "Carol", AGE: 20}
reveal_type(carol[NAME]) # revealed: str 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[non_literal()]) # revealed: Unknown
reveal_type(carol[name_or_age()]) # revealed: str | int | None 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"" # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(person["non_existing"]) # revealed: Unknown 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 reveal_type(person[str_key]) # revealed: Unknown
# No error here: # No error here:
@ -631,10 +631,10 @@ def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any)
person[union_of_keys] = None person[union_of_keys] = None
def _(person: Person, str_key: str, literalstr_key: LiteralString): 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 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 person[literalstr_key] = None
def _(person: Person, unknown_key: Any): def _(person: Person, unknown_key: Any):

View file

@ -3107,9 +3107,10 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
} }
_ => { _ => {
let mut diagnostic = builder.into_diagnostic(format_args!( let mut diagnostic = builder.into_diagnostic(format_args!(
"Invalid key of type `{}` for TypedDict `{}`", "TypedDict `{}` can only be subscripted with string literal keys, \
key_ty.display(db), got key of type `{}`",
typed_dict_ty.display(db), typed_dict_ty.display(db),
key_ty.display(db),
)); ));
if let Some(full_object_ty) = full_object_ty { if let Some(full_object_ty) = full_object_ty {

View file

@ -3557,10 +3557,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let slice_ty = self.infer_expression(slice, TypeContext::default()); let slice_ty = self.infer_expression(slice, TypeContext::default());
self.validate_subscript_assignment_impl( self.validate_subscript_assignment_impl(
object.as_ref(), target,
None, None,
object_ty, object_ty,
slice.as_ref(),
slice_ty, slice_ty,
rhs_value, rhs_value,
rhs_value_ty, rhs_value_ty,
@ -3571,10 +3570,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
#[expect(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
fn validate_subscript_assignment_impl( fn validate_subscript_assignment_impl(
&self, &self,
object_node: &'ast ast::Expr, target: &'ast ast::ExprSubscript,
full_object_ty: Option<Type<'db>>, full_object_ty: Option<Type<'db>>,
object_ty: Type<'db>, object_ty: Type<'db>,
slice_node: &'ast ast::Expr,
slice_ty: Type<'db>, slice_ty: Type<'db>,
rhs_value_node: &'ast ast::Expr, rhs_value_node: &'ast ast::Expr,
rhs_value_ty: Type<'db>, rhs_value_ty: Type<'db>,
@ -3602,7 +3600,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let db = self.db(); 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 { if let Some(full_object_ty) = full_object_ty {
diagnostic.info(format_args!( diagnostic.info(format_args!(
"The full type of the subscripted object is `{}`", "The full type of the subscripted object is `{}`",
@ -3618,10 +3616,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut valid = true; let mut valid = true;
for element_ty in union.elements(db) { for element_ty in union.elements(db) {
valid &= self.validate_subscript_assignment_impl( valid &= self.validate_subscript_assignment_impl(
object_node, target,
full_object_ty.or(Some(object_ty)), full_object_ty.or(Some(object_ty)),
*element_ty, *element_ty,
slice_node,
slice_ty, slice_ty,
rhs_value_node, rhs_value_node,
rhs_value_ty, rhs_value_ty,
@ -3636,10 +3633,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut valid = false; let mut valid = false;
for element_ty in intersection.positive(db) { for element_ty in intersection.positive(db) {
valid |= self.validate_subscript_assignment_impl( valid |= self.validate_subscript_assignment_impl(
object_node, target,
full_object_ty.or(Some(object_ty)), full_object_ty.or(Some(object_ty)),
*element_ty, *element_ty,
slice_node,
slice_ty, slice_ty,
rhs_value_node, rhs_value_node,
rhs_value_ty, 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`, // 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 // 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 // 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() { if slice_ty.is_dynamic() {
return true; return true;
@ -3688,22 +3684,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if slice_ty.is_assignable_to(db, Type::LiteralString) if slice_ty.is_assignable_to(db, Type::LiteralString)
&& !slice_ty.is_equivalent_to(db, Type::LiteralString) && !slice_ty.is_equivalent_to(db, Type::LiteralString)
{ {
if let Some(builder) = if let Some(builder) = self
self.context.report_lint(&INVALID_ASSIGNMENT, slice_node) .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}`", "Cannot assign value of type `{assigned_d}` to key of type `{}` on TypedDict `{value_d}`",
slice_ty.display(db) slice_ty.display(db)
)); ));
attach_original_type_info(diagnostic); attach_original_type_info(&mut diagnostic);
} }
} else { } else {
if let Some(builder) = self.context.report_lint(&INVALID_KEY, slice_node) { if let Some(builder) = self
let diagnostic = builder.into_diagnostic(format_args!( .context
"Cannot access `{value_d}` with a key of type `{}`. Only string literals are allowed as keys on TypedDicts.", .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) 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, full_object_ty,
key, key,
rhs_value_ty, rhs_value_ty,
object_node, target.value.as_ref(),
slice_node, target.slice.as_ref(),
rhs_value_node, rhs_value_node,
TypedDictAssignmentKind::Subscript, TypedDictAssignmentKind::Subscript,
emit_diagnostic, emit_diagnostic,
@ -3741,13 +3741,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if emit_diagnostic if emit_diagnostic
&& let Some(builder) = self && let Some(builder) = self
.context .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", "Method `__setitem__` of type `{}` may be missing",
object_ty.display(db), object_ty.display(db),
)); ));
attach_original_type_info(diagnostic); attach_original_type_info(&mut diagnostic);
} }
false false
} }
@ -3755,17 +3755,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
match call_error_kind { match call_error_kind {
CallErrorKind::NotCallable => { CallErrorKind::NotCallable => {
if emit_diagnostic if emit_diagnostic
&& let Some(builder) = self && let Some(builder) =
.context self.context.report_lint(&CALL_NON_CALLABLE, target)
.report_lint(&CALL_NON_CALLABLE, object_node)
{ {
let diagnostic = builder.into_diagnostic(format_args!( let mut diagnostic = builder.into_diagnostic(format_args!(
"Method `__setitem__` of type `{}` is not callable \ "Method `__setitem__` of type `{}` is not callable \
on object of type `{}`", on object of type `{}`",
bindings.callable_type().display(db), bindings.callable_type().display(db),
object_ty.display(db), object_ty.display(db),
)); ));
attach_original_type_info(diagnostic); attach_original_type_info(&mut diagnostic);
} }
} }
CallErrorKind::BindingError => { CallErrorKind::BindingError => {
@ -3778,8 +3777,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
full_object_ty, full_object_ty,
key, key,
rhs_value_ty, rhs_value_ty,
object_node, target.value.as_ref(),
slice_node, target.slice.as_ref(),
rhs_value_node, rhs_value_node,
TypedDictAssignmentKind::Subscript, TypedDictAssignmentKind::Subscript,
true, true,
@ -3787,35 +3786,77 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} }
} else { } else {
if emit_diagnostic if emit_diagnostic
&& let Some(builder) = self && let Some(builder) = self.context.report_lint(
.context &INVALID_ASSIGNMENT,
.report_lint(&INVALID_ASSIGNMENT, object_node) target.range.cover(rhs_value_node.range()),
)
{ {
let assigned_d = rhs_value_ty.display(db); 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!( // 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 \ "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}`", a key of type `{}` and a value of type `{assigned_d}` on object of type `{object_d}`",
bindings.callable_type().display(db), bindings.callable_type().display(db),
slice_ty.display(db), slice_ty.display(db),
)); ));
attach_original_type_info(diagnostic); attach_original_type_info(&mut diagnostic);
}
} }
} }
} }
CallErrorKind::PossiblyNotCallable => { CallErrorKind::PossiblyNotCallable => {
if emit_diagnostic if emit_diagnostic
&& let Some(builder) = self && let Some(builder) =
.context self.context.report_lint(&CALL_NON_CALLABLE, target)
.report_lint(&CALL_NON_CALLABLE, object_node)
{ {
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 `{}`", "Method `__setitem__` of type `{}` may not be callable on object of type `{}`",
bindings.callable_type().display(db), bindings.callable_type().display(db),
object_ty.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 => { CallDunderError::MethodNotAvailable => {
if emit_diagnostic if emit_diagnostic
&& let Some(builder) = && 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!( let mut diagnostic = builder.into_diagnostic(format_args!(
"Cannot assign to a subscript on an object of type `{}` with no `__setitem__` method", "Cannot assign to a subscript on an object of type `{}`",
object_ty.display(db), 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 false
} }