[ty] Preserve quoting style when autofixing `TypedDict` keys (#21682)

This commit is contained in:
Alex Waygood 2025-11-28 18:40:34 +00:00 committed by GitHub
parent b5b4917d7f
commit 594b7b04d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 4 deletions

View File

@ -52,6 +52,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
38 | 38 |
39 | def write_to_readonly_key(employee: Employee): 39 | def write_to_readonly_key(employee: Employee):
40 | employee["id"] = 42 # error: [invalid-assignment] 40 | employee["id"] = 42 # error: [invalid-assignment]
41 | def write_to_non_existing_key_single_quotes(person: Person):
42 | # error: [invalid-key]
43 | person['naem'] = "Alice" # fmt: skip
``` ```
# Diagnostics # Diagnostics
@ -217,6 +220,8 @@ error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee`
| -------- ^^^^ key is marked read-only | -------- ^^^^ key is marked read-only
| | | |
| TypedDict `Employee` | TypedDict `Employee`
41 | def write_to_non_existing_key_single_quotes(person: Person):
42 | # error: [invalid-key]
| |
info: Item declaration info: Item declaration
--> src/mdtest_snippet.py:36:5 --> src/mdtest_snippet.py:36:5
@ -229,3 +234,24 @@ info: Item declaration
info: rule `invalid-assignment` is enabled by default info: rule `invalid-assignment` is enabled by default
``` ```
```
error[invalid-key]: Unknown key "naem" for TypedDict `Person`
--> src/mdtest_snippet.py:43:5
|
41 | def write_to_non_existing_key_single_quotes(person: Person):
42 | # error: [invalid-key]
43 | person['naem'] = "Alice" # fmt: skip
| ------ ^^^^^^ Did you mean 'name'?
| |
| TypedDict `Person`
|
info: rule `invalid-key` is enabled by default
40 | employee["id"] = 42 # error: [invalid-assignment]
41 | def write_to_non_existing_key_single_quotes(person: Person):
42 | # error: [invalid-key]
- person['naem'] = "Alice" # fmt: skip
43 + person['name'] = "Alice" # fmt: skip
note: This is an unsafe fix and may change runtime behavior
```

View File

@ -1502,6 +1502,14 @@ def write_to_readonly_key(employee: Employee):
employee["id"] = 42 # error: [invalid-assignment] employee["id"] = 42 # error: [invalid-assignment]
``` ```
If the key uses single quotes, the autofix preserves that quoting style:
```py
def write_to_non_existing_key_single_quotes(person: Person):
# error: [invalid-key]
person['naem'] = "Alice" # fmt: skip
```
## Import aliases ## Import aliases
`TypedDict` can be imported with aliases and should work correctly: `TypedDict` can be imported with aliases and should work correctly:

View File

@ -40,7 +40,7 @@ use ruff_db::{
use ruff_diagnostics::{Edit, Fix}; use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parentheses_iterator; use ruff_python_ast::parenthesize::parentheses_iterator;
use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_ast::{self as ast, AnyNodeRef, StringFlags};
use ruff_python_trivia::CommentRanges; use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -3456,11 +3456,15 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
let existing_keys = items.keys(); let existing_keys = items.keys();
if let Some(suggestion) = did_you_mean(existing_keys, key) { if let Some(suggestion) = did_you_mean(existing_keys, key) {
if key_node.is_expr_string_literal() { if let AnyNodeRef::ExprStringLiteral(literal) = key_node {
let quoted_suggestion = format!(
"{quote}{suggestion}{quote}",
quote = literal.value.first_literal_flags().quote_str()
);
diagnostic diagnostic
.set_primary_message(format_args!("Did you mean \"{suggestion}\"?")); .set_primary_message(format_args!("Did you mean {quoted_suggestion}?"));
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
format!("\"{suggestion}\""), quoted_suggestion,
key_node.range(), key_node.range(),
))); )));
} else { } else {