diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 858f1f0c7c..4218eee1af 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -474,7 +474,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -599,7 +599,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -650,7 +650,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -679,7 +679,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -723,7 +723,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -762,11 +762,15 @@ Added in 0 **What it does** -Checks for subscript accesses with invalid keys. +Checks for subscript accesses with invalid keys and `TypedDict` construction with an +unknown key. **Why is this bad?** -Using an invalid key will raise a `KeyError` at runtime. +Subscripting with an invalid key will raise a `KeyError` at runtime. + +Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is +`closed=true` it also violates the expectations of the type. **Examples** @@ -779,6 +783,10 @@ class Person(TypedDict): alice = Person(name="Alice", age=30) alice["height"] # KeyError: 'height' + +bob: Person = { "name": "Bob", "age": 30 } # typo! + +carol = Person(name="Carol", age=25) # typo! ``` ## `invalid-legacy-type-variable` @@ -787,7 +795,7 @@ alice["height"] # KeyError: 'height' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -822,7 +830,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -888,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -938,7 +946,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -998,7 +1006,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1047,7 +1055,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1072,7 +1080,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1130,7 +1138,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1157,7 +1165,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1187,7 +1195,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1217,7 +1225,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1251,7 +1259,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1285,7 +1293,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1320,7 +1328,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1345,7 +1353,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1378,7 +1386,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1407,7 +1415,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1431,7 +1439,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1457,7 +1465,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1484,7 +1492,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1542,7 +1550,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1572,7 +1580,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1601,7 +1609,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1628,7 +1636,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1656,7 +1664,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1702,7 +1710,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1729,7 +1737,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1757,7 +1765,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1782,7 +1790,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1807,7 +1815,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1844,7 +1852,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1872,7 +1880,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2026,7 +2034,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2086,7 +2094,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2118,7 +2126,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2145,7 +2153,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2169,7 +2177,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2227,7 +2235,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2266,7 +2274,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2353,7 +2361,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source 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 b80700fa08..155b4ea618 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 @@ -37,20 +37,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md 23 | 24 | def write_to_non_literal_string_key(person: Person, str_key: str): 25 | person[str_key] = "Alice" # error: [invalid-key] -26 | from typing_extensions import ReadOnly -27 | -28 | class Employee(TypedDict): -29 | id: ReadOnly[int] -30 | name: str +26 | +27 | def create_with_invalid_string_key(): +28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] +29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] +30 | from typing_extensions import ReadOnly 31 | -32 | def write_to_readonly_key(employee: Employee): -33 | employee["id"] = 42 # error: [invalid-assignment] +32 | class Employee(TypedDict): +33 | id: ReadOnly[int] +34 | name: str +35 | +36 | def write_to_readonly_key(employee: Employee): +37 | employee["id"] = 42 # error: [invalid-assignment] ``` # Diagnostics ``` -error[invalid-key]: Invalid key access on TypedDict `Person` +error[invalid-key]: Invalid key for TypedDict `Person` --> src/mdtest_snippet.py:8:5 | 7 | def access_invalid_literal_string_key(person: Person): @@ -66,7 +70,7 @@ info: rule `invalid-key` is enabled by default ``` ``` -error[invalid-key]: Invalid key access on TypedDict `Person` +error[invalid-key]: Invalid key for TypedDict `Person` --> src/mdtest_snippet.py:13:5 | 12 | def access_invalid_key(person: Person): @@ -82,7 +86,7 @@ info: rule `invalid-key` is enabled by default ``` ``` -error[invalid-key]: TypedDict `Person` cannot be indexed with a key of type `str` +error[invalid-key]: Invalid key for TypedDict `Person` of type `str` --> src/mdtest_snippet.py:16:12 | 15 | def access_with_str_key(person: Person, str_key: str): @@ -123,7 +127,7 @@ info: rule `invalid-assignment` is enabled by default ``` ``` -error[invalid-key]: Invalid key access on TypedDict `Person` +error[invalid-key]: Invalid key for TypedDict `Person` --> src/mdtest_snippet.py:22:5 | 21 | def write_to_non_existing_key(person: Person): @@ -145,7 +149,39 @@ error[invalid-key]: Cannot access `Person` with a key of type `str`. Only string 24 | def write_to_non_literal_string_key(person: Person, str_key: str): 25 | person[str_key] = "Alice" # error: [invalid-key] | ^^^^^^^ -26 | from typing_extensions import ReadOnly +26 | +27 | def create_with_invalid_string_key(): + | +info: rule `invalid-key` is enabled by default + +``` + +``` +error[invalid-key]: Invalid key for TypedDict `Person` + --> src/mdtest_snippet.py:28:21 + | +27 | def create_with_invalid_string_key(): +28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] + | -----------------------------^^^^^^^^^-------- + | | | + | | Unknown key "unknown" + | TypedDict `Person` +29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] +30 | from typing_extensions import ReadOnly + | +info: rule `invalid-key` is enabled by default + +``` + +``` +error[invalid-key]: Invalid key for TypedDict `Person` + --> src/mdtest_snippet.py:29:11 + | +27 | def create_with_invalid_string_key(): +28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] +29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] + | ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown" +30 | from typing_extensions import ReadOnly | info: rule `invalid-key` is enabled by default @@ -153,21 +189,21 @@ info: rule `invalid-key` is enabled by default ``` error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee` - --> src/mdtest_snippet.py:33:5 + --> src/mdtest_snippet.py:37:5 | -32 | def write_to_readonly_key(employee: Employee): -33 | employee["id"] = 42 # error: [invalid-assignment] +36 | def write_to_readonly_key(employee: Employee): +37 | employee["id"] = 42 # error: [invalid-assignment] | -------- ^^^^ key is marked read-only | | | TypedDict `Employee` | info: Item declaration - --> src/mdtest_snippet.py:29:5 + --> src/mdtest_snippet.py:33:5 | -28 | class Employee(TypedDict): -29 | id: ReadOnly[int] +32 | class Employee(TypedDict): +33 | id: ReadOnly[int] | ----------------- Read-only item declared here -30 | name: str +34 | name: str | info: rule `invalid-assignment` is enabled by default diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index d810a79efe..042d6317a2 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 access on TypedDict `Person`: Unknown key "non_existing"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing"" 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 access on TypedDict `Person`: Unknown key "non_existing"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing"" reveal_type(bob["non_existing"]) # revealed: Unknown ``` @@ -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] "TypedDict `Person` cannot be indexed with a key of type `str`" +# error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`" reveal_type(carol[non_literal()]) # revealed: Unknown reveal_type(carol[name_or_age()]) # revealed: str | int | None @@ -81,7 +81,7 @@ def _(): CAPITALIZED_NAME = "Name" -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "Name" - did you mean "name"?" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "Name" - did you mean "name"?" # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20} @@ -104,9 +104,9 @@ eve2a: Person = {"age": 22} # error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor" eve2b = Person(age=22) -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" eve3a: Person = {"name": "Eve", "age": 25, "extra": True} -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" eve3b = Person(name="Eve", age=25, extra=True) ``` @@ -157,10 +157,10 @@ bob["name"] = None Assignments to non-existing keys are disallowed: ```py -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" alice["extra"] = True -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" bob["extra"] = True ``` @@ -185,10 +185,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 access on TypedDict `Inner`: Unknown key "non_existing"" +# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "non_existing"" reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown -# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "extra"" alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}} ``` @@ -267,22 +267,22 @@ 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 access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" alice4: Person = {"name": "Alice", "age": 30, "extra": True} -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" Person(name="Alice", age=30, extra=True) -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" Person({"name": "Alice", "age": 30, "extra": True}) -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" accepts_person({"name": "Alice", "age": 30, "extra": True}) # TODO: this should be an error house.owner = {"name": "Alice", "age": 30, "extra": True} a_person: Person -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" a_person = {"name": "Alice", "age": 30, "extra": True} -# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra"" (a_person := {"name": "Alice", "age": 30, "extra": True}) ``` @@ -323,7 +323,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 access on TypedDict `User`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `User`: Unknown key "extra"" user4 = User({"name": "Charlie", "age": 30, "extra": True}) ``` @@ -360,7 +360,7 @@ invalid = OptionalPerson(name=123) Extra fields are still not allowed, even with `total=False`: ```py -# error: [invalid-key] "Invalid key access on TypedDict `OptionalPerson`: Unknown key "extra"" +# error: [invalid-key] "Invalid key for TypedDict `OptionalPerson`: Unknown key "extra"" invalid_extra = OptionalPerson(name="George", extra=True) ``` @@ -503,10 +503,10 @@ def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age", reveal_type(person[union_of_keys]) # revealed: int | None | str - # error: [invalid-key] "Invalid key access on 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 - # error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`" + # error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`" reveal_type(person[str_key]) # revealed: Unknown # No error here: @@ -530,7 +530,7 @@ def _(person: Person): person["name"] = "Alice" person["age"] = 30 - # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "naem" - did you mean "name"?" + # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "naem" - did you mean "name"?" person["naem"] = "Alice" def _(person: Person): @@ -646,7 +646,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 access on TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?" + # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?" reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown ``` @@ -1015,6 +1015,10 @@ def write_to_non_existing_key(person: Person): def write_to_non_literal_string_key(person: Person, str_key: str): person[str_key] = "Alice" # error: [invalid-key] + +def create_with_invalid_string_key(): + alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key] + bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key] ``` Assignment to `ReadOnly` keys: diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 7db83b9b88..2dd75e57aa 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -572,10 +572,14 @@ declare_lint! { // Added in #19763. declare_lint! { /// ## What it does - /// Checks for subscript accesses with invalid keys. + /// Checks for subscript accesses with invalid keys and `TypedDict` construction with an + /// unknown key. /// /// ## Why is this bad? - /// Using an invalid key will raise a `KeyError` at runtime. + /// Subscripting with an invalid key will raise a `KeyError` at runtime. + /// + /// Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is + /// `closed=true` it also violates the expectations of the type. /// /// ## Examples /// ```python @@ -587,9 +591,13 @@ declare_lint! { /// /// alice = Person(name="Alice", age=30) /// alice["height"] # KeyError: 'height' + /// + /// bob: Person = { "name": "Bob", "age": 30 } # typo! + /// + /// carol = Person(name="Carol", age=25) # typo! /// ``` pub(crate) static INVALID_KEY = { - summary: "detects invalid subscript accesses", + summary: "detects invalid subscript accesses or TypedDict literal keys", status: LintStatus::stable("0.0.1-alpha.17"), default_level: Level::Error, } @@ -2966,7 +2974,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 access on TypedDict `{typed_dict_name}`", + "Invalid key for TypedDict `{typed_dict_name}`", )); diagnostic.annotate( @@ -2989,7 +2997,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( diagnostic } _ => builder.into_diagnostic(format_args!( - "TypedDict `{}` cannot be indexed with a key of type `{}`", + "Invalid key for TypedDict `{}` of type `{}`", typed_dict_ty.display(db), key_ty.display(db), )), diff --git a/ty.schema.json b/ty.schema.json index 270241fb28..55d5bdf996 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -584,8 +584,8 @@ ] }, "invalid-key": { - "title": "detects invalid subscript accesses", - "description": "## What it does\nChecks for subscript accesses with invalid keys.\n\n## Why is this bad?\nUsing an invalid key will raise a `KeyError` at runtime.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n```", + "title": "detects invalid subscript accesses or TypedDict literal keys", + "description": "## What it does\nChecks for subscript accesses with invalid keys and `TypedDict` construction with an\nunknown key.\n\n## Why is this bad?\nSubscripting with an invalid key will raise a `KeyError` at runtime.\n\nCreating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is\n`closed=true` it also violates the expectations of the type.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n\nbob: Person = { \"name\": \"Bob\", \"age\": 30 } # typo!\n\ncarol = Person(name=\"Carol\", age=25) # typo!\n```", "default": "error", "oneOf": [ {