mirror of https://github.com/astral-sh/ruff
[ty] Custom concise diagnostic messages (#21498)
## Summary This PR proposes that we add a new `set_concise_message` functionality to our `Diagnostic` construction API. When used, the concise message that is otherwise auto-generated from the main diagnostic message and the primary annotation will be overwritten with the custom message. To understand why this is desirable, let's look at the `invalid-key` diagnostic. This is how I *want* the full diagnostic to look like: <img width="620" height="282" alt="image" src="https://github.com/user-attachments/assets/3bf70f52-9d9f-4817-bc16-fb0ebf7c2113" /> However, without the change in this PR, the concise message would have the following form: ``` error[invalid-key]: Unknown key "Age" for TypedDict `Person`: Unknown key "Age" - did you mean "age"? ``` This duplication is why the full `invalid-key` diagnostic used a main diagnostic message that is only "Invalid key for TypedDict `Person`", to make that bearable: ``` error[invalid-key] Invalid key for TypedDict `Person`: Unknown key "Age" - did you mean "age"? ``` This is still less than ideal, *and* we had to make the "full" diagnostic worse. With the new API here, we have to make no such compromises. We need to do slightly more work (provide one additional custom-designed message), but we get to keep the "full" diagnostic that we actually want, and we can make the concise message more terse and readable: ``` error[invalid-key] Unknown key "Age" for TypedDict `Person` - did you mean "age"? ``` Similar problems exist for other diagnostics as well (I really want this for https://github.com/astral-sh/ruff/pull/21476). In this PR, I only changed `invalid-key` and `type-assertion-failure`. The PR here is somewhat related to the discussion in https://github.com/astral-sh/ty/issues/1418, but note that we are solving a problem that is unrelated to sub-diagnostics. ## Test Plan Updated tests
This commit is contained in:
parent
d5a95ec824
commit
7a739d6b76
|
|
@ -64,6 +64,7 @@ impl Diagnostic {
|
|||
id,
|
||||
severity,
|
||||
message: message.into_diagnostic_message(),
|
||||
custom_concise_message: None,
|
||||
annotations: vec![],
|
||||
subs: vec![],
|
||||
fix: None,
|
||||
|
|
@ -213,6 +214,10 @@ impl Diagnostic {
|
|||
/// cases, just converting it to a string (or printing it) will do what
|
||||
/// you want.
|
||||
pub fn concise_message(&self) -> ConciseMessage<'_> {
|
||||
if let Some(custom_message) = &self.inner.custom_concise_message {
|
||||
return ConciseMessage::Custom(custom_message.as_str());
|
||||
}
|
||||
|
||||
let main = self.inner.message.as_str();
|
||||
let annotation = self
|
||||
.primary_annotation()
|
||||
|
|
@ -226,6 +231,15 @@ impl Diagnostic {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set a custom message for the concise formatting of this diagnostic.
|
||||
///
|
||||
/// This overrides the default behavior of generating a concise message
|
||||
/// from the main diagnostic message and the primary annotation.
|
||||
pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) {
|
||||
Arc::make_mut(&mut self.inner).custom_concise_message =
|
||||
Some(message.into_diagnostic_message());
|
||||
}
|
||||
|
||||
/// Returns the severity of this diagnostic.
|
||||
///
|
||||
/// Note that this may be different than the severity of sub-diagnostics.
|
||||
|
|
@ -532,6 +546,7 @@ struct DiagnosticInner {
|
|||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
custom_concise_message: Option<DiagnosticMessage>,
|
||||
annotations: Vec<Annotation>,
|
||||
subs: Vec<SubDiagnostic>,
|
||||
fix: Option<Fix>,
|
||||
|
|
@ -1520,6 +1535,8 @@ pub enum ConciseMessage<'a> {
|
|||
/// This indicates that the diagnostic is probably using the old
|
||||
/// model.
|
||||
Empty,
|
||||
/// A custom concise message has been provided.
|
||||
Custom(&'a str),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConciseMessage<'_> {
|
||||
|
|
@ -1535,6 +1552,9 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
|||
write!(f, "{main}: {annotation}")
|
||||
}
|
||||
ConciseMessage::Empty => Ok(()),
|
||||
ConciseMessage::Custom(message) => {
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -458,12 +458,12 @@ b: TD | None = f([{"x": 0}, {"x": 1}])
|
|||
reveal_type(b) # revealed: TD
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
|
||||
# error: [invalid-key] "Invalid key for TypedDict `TD`: Unknown key "y""
|
||||
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
|
||||
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD`"
|
||||
c: TD = f([{"y": 0}, {"x": 1}])
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
|
||||
# error: [invalid-key] "Invalid key for TypedDict `TD`: Unknown key "y""
|
||||
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
|
||||
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`"
|
||||
c: TD | None = f([{"y": 0}, {"x": 1}])
|
||||
```
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ def if_else_isinstance_error(obj: A | B):
|
|||
elif isinstance(obj, C):
|
||||
pass
|
||||
else:
|
||||
# error: [type-assertion-failure] "Argument does not have asserted type `Never`"
|
||||
# error: [type-assertion-failure] "Type `B & ~A & ~C` is not equivalent to `Never`"
|
||||
assert_never(obj)
|
||||
|
||||
def if_else_singletons_success(obj: Literal[1, "a"] | None):
|
||||
|
|
@ -101,7 +101,7 @@ def if_else_singletons_error(obj: Literal[1, "a"] | None):
|
|||
elif obj is None:
|
||||
pass
|
||||
else:
|
||||
# error: [type-assertion-failure] "Argument does not have asserted type `Never`"
|
||||
# error: [type-assertion-failure] "Type `Literal["a"]` is not equivalent to `Never`"
|
||||
assert_never(obj)
|
||||
|
||||
def match_singletons_success(obj: Literal[1, "a"] | None):
|
||||
|
|
@ -125,7 +125,9 @@ def match_singletons_error(obj: Literal[1, "a"] | None):
|
|||
pass
|
||||
case _ as obj:
|
||||
# TODO: We should emit an error here, but the message should
|
||||
# show the type `Literal["a"]` instead of `@Todo(…)`.
|
||||
# error: [type-assertion-failure] "Argument does not have asserted type `Never`"
|
||||
# show the type `Literal["a"]` instead of `@Todo(…)`. We only
|
||||
# assert on the first part of the message because the `@Todo`
|
||||
# message is not available in release mode builds.
|
||||
# error: [type-assertion-failure] "Type `@Todo"
|
||||
assert_never(obj)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ from typing_extensions import assert_type
|
|||
|
||||
# Subtype does not count
|
||||
def _(x: bool):
|
||||
assert_type(x, int) # error: [type-assertion-failure]
|
||||
assert_type(x, int) # error: [type-assertion-failure] "Type `int` does not match asserted type `bool`"
|
||||
|
||||
def _(a: type[int], b: type[Any]):
|
||||
assert_type(a, type[Any]) # error: [type-assertion-failure]
|
||||
assert_type(b, type[int]) # error: [type-assertion-failure]
|
||||
assert_type(a, type[Any]) # error: [type-assertion-failure] "Type `type[Any]` does not match asserted type `type[int]`"
|
||||
assert_type(b, type[int]) # error: [type-assertion-failure] "Type `type[int]` does not match asserted type `type[Any]`"
|
||||
|
||||
# The expression constructing the type is not taken into account
|
||||
def _(a: type[int]):
|
||||
|
|
|
|||
|
|
@ -901,7 +901,7 @@ def color_name_misses_one_variant(color: Color) -> str:
|
|||
elif color is Color.GREEN:
|
||||
return "Green"
|
||||
else:
|
||||
assert_never(color) # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
|
||||
assert_never(color) # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`"
|
||||
|
||||
class Singleton(Enum):
|
||||
VALUE = 1
|
||||
|
|
@ -956,7 +956,7 @@ def color_name_misses_one_variant(color: Color) -> str:
|
|||
case Color.GREEN:
|
||||
return "Green"
|
||||
case _:
|
||||
assert_never(color) # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
|
||||
assert_never(color) # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`"
|
||||
|
||||
class Singleton(Enum):
|
||||
VALUE = 1
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Config`
|
||||
error[invalid-key]: Unknown key "Retries" for TypedDict `Config`
|
||||
--> src/mdtest_snippet.py:7:5
|
||||
|
|
||||
6 | def _(config: Config) -> None:
|
||||
7 | config["Retries"] = 30.0 # error: [invalid-key]
|
||||
| ------ ^^^^^^^^^ Unknown key "Retries" - did you mean "retries"?
|
||||
| ------ ^^^^^^^^^ Did you mean "retries"?
|
||||
| |
|
||||
| TypedDict `Config`
|
||||
|
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
error[invalid-key]: Unknown key "surname" for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:13:5
|
||||
|
|
||||
11 | # error: [invalid-key]
|
||||
12 | # error: [invalid-key]
|
||||
13 | being["surname"] = "unknown"
|
||||
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
|
||||
| ----- ^^^^^^^^^ Did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Person` in union type `Person | Animal`
|
||||
|
|
||||
|
|
@ -45,13 +45,13 @@ info: rule `invalid-key` is enabled by default
|
|||
```
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Animal`
|
||||
error[invalid-key]: Unknown key "surname" for TypedDict `Animal`
|
||||
--> src/mdtest_snippet.py:13:5
|
||||
|
|
||||
11 | # error: [invalid-key]
|
||||
12 | # error: [invalid-key]
|
||||
13 | being["surname"] = "unknown"
|
||||
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
|
||||
| ----- ^^^^^^^^^ Did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Animal` in union type `Person | Animal`
|
||||
|
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
error[invalid-key]: Unknown key "legs" for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
error[invalid-key]: Unknown key "naem" for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
|
|
||||
7 | def access_invalid_literal_string_key(person: Person):
|
||||
8 | person["naem"] # error: [invalid-key]
|
||||
| ------ ^^^^^^ Unknown key "naem" - did you mean "name"?
|
||||
| ------ ^^^^^^ Did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Person`
|
||||
9 |
|
||||
|
|
@ -73,7 +73,7 @@ info: rule `invalid-key` is enabled by default
|
|||
```
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
error[invalid-key]: Unknown key "naem" for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:13:5
|
||||
|
|
||||
12 | def access_invalid_key(person: Person):
|
||||
|
|
@ -130,12 +130,12 @@ info: rule `invalid-assignment` is enabled by default
|
|||
```
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
error[invalid-key]: Unknown key "naem" for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:22:5
|
||||
|
|
||||
21 | def write_to_non_existing_key(person: Person):
|
||||
22 | person["naem"] = "Alice" # error: [invalid-key]
|
||||
| ------ ^^^^^^ Unknown key "naem" - did you mean "name"?
|
||||
| ------ ^^^^^^ Did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Person`
|
||||
23 |
|
||||
|
|
@ -160,7 +160,7 @@ info: rule `invalid-key` is enabled by default
|
|||
```
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
error[invalid-key]: Unknown key "unknown" for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:29:21
|
||||
|
|
||||
27 | def create_with_invalid_string_key():
|
||||
|
|
@ -178,7 +178,7 @@ info: rule `invalid-key` is enabled by default
|
|||
```
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
error[invalid-key]: Unknown key "unknown" for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:32:11
|
||||
|
|
||||
31 | # error: [invalid-key]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ alice: Person = {"name": "Alice", "age": 30}
|
|||
reveal_type(alice["name"]) # revealed: str
|
||||
reveal_type(alice["age"]) # revealed: int | None
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
|
||||
# error: [invalid-key] "Unknown key "non_existing" for TypedDict `Person`"
|
||||
reveal_type(alice["non_existing"]) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ bob = Person(name="Bob", age=25)
|
|||
reveal_type(bob["name"]) # revealed: str
|
||||
reveal_type(bob["age"]) # revealed: int | None
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
|
||||
# error: [invalid-key] " key "non_existing" for TypedDict `Person`"
|
||||
reveal_type(bob["non_existing"]) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ def _():
|
|||
|
||||
CAPITALIZED_NAME = "Name"
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
|
||||
# error: [invalid-key] "Unknown key "Name" for TypedDict `Person` - did you mean "name"?"
|
||||
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
|
||||
dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20}
|
||||
|
||||
|
|
@ -112,10 +112,10 @@ eve2b = Person(age=22)
|
|||
reveal_type(eve2a) # revealed: Person
|
||||
reveal_type(eve2b) # revealed: Person
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
eve3b = Person(name="Eve", age=25, extra=True)
|
||||
|
||||
reveal_type(eve3a) # revealed: Person
|
||||
|
|
@ -169,10 +169,10 @@ bob["name"] = None
|
|||
Assignments to non-existing keys are disallowed:
|
||||
|
||||
```py
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
alice["extra"] = True
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
bob["extra"] = True
|
||||
```
|
||||
|
||||
|
|
@ -197,10 +197,10 @@ alice: Person = {"inner": {"name": "Alice", "age": 30}}
|
|||
reveal_type(alice["inner"]["name"]) # revealed: str
|
||||
reveal_type(alice["inner"]["age"]) # revealed: int | None
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "non_existing""
|
||||
# error: [invalid-key] "Unknown key "non_existing" for TypedDict `Inner`"
|
||||
reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Inner`"
|
||||
alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}}
|
||||
```
|
||||
|
||||
|
|
@ -289,16 +289,16 @@ a_person = {"name": None, "age": 30}
|
|||
All of these have an extra field that is not defined in the `TypedDict`:
|
||||
|
||||
```py
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
alice4: Person = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
Person(name="Alice", age=30, extra=True)
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
Person({"name": "Alice", "age": 30, "extra": True})
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
# error: [invalid-argument-type]
|
||||
accepts_person({"name": "Alice", "age": 30, "extra": True})
|
||||
|
||||
|
|
@ -307,10 +307,10 @@ accepts_person({"name": "Alice", "age": 30, "extra": True})
|
|||
house.owner = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
||||
a_person: Person
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
a_person = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
|
||||
(a_person := {"name": "Alice", "age": 30, "extra": True})
|
||||
```
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ user2 = User({"name": "Bob"})
|
|||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `User`: value of type `None`"
|
||||
user3 = User({"name": None, "age": 25})
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `User`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `User`"
|
||||
user4 = User({"name": "Charlie", "age": 30, "extra": True})
|
||||
```
|
||||
|
||||
|
|
@ -388,7 +388,7 @@ invalid = OptionalPerson(name=123)
|
|||
Extra fields are still not allowed, even with `total=False`:
|
||||
|
||||
```py
|
||||
# error: [invalid-key] "Invalid key for TypedDict `OptionalPerson`: Unknown key "extra""
|
||||
# error: [invalid-key] "Unknown key "extra" for TypedDict `OptionalPerson`"
|
||||
invalid_extra = OptionalPerson(name="George", extra=True)
|
||||
```
|
||||
|
||||
|
|
@ -550,7 +550,7 @@ def _(
|
|||
|
||||
reveal_type(person[union_of_keys]) # revealed: int | None | str
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
|
||||
# error: [invalid-key] "Unknown key "non_existing" for TypedDict `Person`"
|
||||
reveal_type(person["non_existing"]) # revealed: Unknown
|
||||
|
||||
# error: [invalid-key] "TypedDict `Person` can only be subscripted with a string literal key, got key of type `str`"
|
||||
|
|
@ -563,7 +563,7 @@ def _(
|
|||
|
||||
# TODO: A type of `int | None | Unknown` might be better here. The `str` is mixed in
|
||||
# because `Animal.__getitem__` can only return `str`.
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Animal`"
|
||||
# error: [invalid-key] "Unknown key "age" for TypedDict `Animal`"
|
||||
reveal_type(being["age"]) # revealed: int | None | str
|
||||
```
|
||||
|
||||
|
|
@ -589,7 +589,7 @@ def _(person: Person):
|
|||
person["name"] = "Alice"
|
||||
person["age"] = 30
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "naem" - did you mean "name"?"
|
||||
# error: [invalid-key] "Unknown key "naem" for TypedDict `Person` - did you mean "name"?"
|
||||
person["naem"] = "Alice"
|
||||
|
||||
def _(person: Person):
|
||||
|
|
@ -613,7 +613,7 @@ def _(being: Person | Animal):
|
|||
# error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Animal`: value of type `Literal[1]`"
|
||||
being["name"] = 1
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Animal`: Unknown key "surname" - did you mean "name"?"
|
||||
# error: [invalid-key] "Unknown key "surname" for TypedDict `Animal` - did you mean "name"?"
|
||||
being["surname"] = "unknown"
|
||||
|
||||
def _(centaur: Intersection[Person, Animal]):
|
||||
|
|
@ -621,7 +621,7 @@ def _(centaur: Intersection[Person, Animal]):
|
|||
centaur["age"] = 100
|
||||
centaur["legs"] = 4
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "unknown""
|
||||
# error: [invalid-key] "Unknown key "unknown" for TypedDict `Person`"
|
||||
centaur["unknown"] = "value"
|
||||
|
||||
def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any):
|
||||
|
|
@ -724,7 +724,7 @@ def _(p: Person) -> None:
|
|||
reveal_type(p.setdefault("name", "Alice")) # revealed: str
|
||||
reveal_type(p.setdefault("extra", "default")) # revealed: str
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
|
||||
# error: [invalid-key] "Unknown key "extraz" for TypedDict `Person` - did you mean "extra"?"
|
||||
reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -3147,7 +3147,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
|||
let typed_dict_name = typed_dict_ty.display(db);
|
||||
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Invalid key for TypedDict `{typed_dict_name}`",
|
||||
"Unknown key \"{key}\" for TypedDict `{typed_dict_name}`",
|
||||
));
|
||||
|
||||
diagnostic.annotate(if let Some(full_object_ty) = full_object_ty {
|
||||
|
|
@ -3167,15 +3167,21 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
|||
});
|
||||
|
||||
let existing_keys = items.iter().map(|(name, _)| name.as_str());
|
||||
|
||||
diagnostic.set_primary_message(format!(
|
||||
"Unknown key \"{key}\"{hint}",
|
||||
hint = if let Some(suggestion) = did_you_mean(existing_keys, key) {
|
||||
format!(" - did you mean \"{suggestion}\"?")
|
||||
if let Some(suggestion) = did_you_mean(existing_keys, key) {
|
||||
if key_node.is_expr_string_literal() {
|
||||
diagnostic
|
||||
.set_primary_message(format_args!("Did you mean \"{suggestion}\"?"));
|
||||
} else {
|
||||
String::new()
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Unknown key \"{key}\" - did you mean \"{suggestion}\"?",
|
||||
));
|
||||
}
|
||||
));
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Unknown key \"{key}\" for TypedDict `{typed_dict_name}` - did you mean \"{suggestion}\"?",
|
||||
));
|
||||
} else {
|
||||
diagnostic.set_primary_message(format_args!("Unknown key \"{key}\""));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
|
|
|
|||
|
|
@ -1495,6 +1495,12 @@ impl KnownFunction {
|
|||
asserted_type = asserted_ty.display(db),
|
||||
inferred_type = actual_ty.display(db),
|
||||
));
|
||||
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Type `{}` does not match asserted type `{}`",
|
||||
asserted_ty.display(db),
|
||||
actual_ty.display(db),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1520,6 +1526,11 @@ impl KnownFunction {
|
|||
"`Never` and `{inferred_type}` are not equivalent types",
|
||||
inferred_type = actual_ty.display(db),
|
||||
));
|
||||
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Type `{}` is not equivalent to `Never`",
|
||||
actual_ty.display(db),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue