From 7a739d6b764139c920c689e0404f766f06a449e4 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 18 Nov 2025 09:35:40 +0100 Subject: [PATCH] [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: image 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 --- crates/ruff_db/src/diagnostic/mod.rs | 20 ++++++++ .../mdtest/assignment/annotations.md | 4 +- .../mdtest/directives/assert_never.md | 10 ++-- .../mdtest/directives/assert_type.md | 6 +-- .../resources/mdtest/enums.md | 4 +- ..._Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap | 4 +- ..._Unknown_key_for_all_…_(1c685d9d10678263).snap | 8 ++-- ..._Unknown_key_for_one_…_(b515711c0a451a86).snap | 2 +- ...ict`_-_Diagnostics_(e5289abf5c570c29).snap | 14 +++--- .../resources/mdtest/typed_dict.md | 46 +++++++++---------- .../src/types/diagnostic.rs | 22 +++++---- .../ty_python_semantic/src/types/function.rs | 11 +++++ 12 files changed, 95 insertions(+), 56 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 9d9ff2dbc3..0761c96acb 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -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, annotations: Vec, subs: Vec, fix: Option, @@ -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}") + } } } } diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index efdbbab36b..99cacd8193 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -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}]) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index 89c35aae5b..67047850be 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -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) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index fd7c4d1649..840e8ce4b1 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -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]): diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index bdd2a92995..8ae0dce62f 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap index e6036f32e0..ca95ce1675 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap @@ -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` | diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(1c685d9d10678263).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(1c685d9d10678263).snap index 2e7bbcfe4d..6c9e4de308 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(1c685d9d10678263).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(1c685d9d10678263).snap @@ -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` | diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_one_…_(b515711c0a451a86).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_one_…_(b515711c0a451a86).snap index 6c919e6937..4ca40015cd 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_one_…_(b515711c0a451a86).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_one_…_(b515711c0a451a86).snap @@ -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: 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 492729672e..697188d2be 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 @@ -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] diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 6de15b3be1..ad24b03f7c 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -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 ``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 82c983f99b..07ad837655 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -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!( diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 085bbaa60f..5438f7ff4e 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -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), + )); } }