diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 8de896c0c6..ae4f3d5009 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -8,7 +8,7 @@
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -80,7 +80,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.7 ·
Related issues ·
-View source
+View source
@@ -135,7 +135,7 @@ def f(x: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -167,7 +167,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -198,7 +198,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -230,7 +230,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -262,7 +262,7 @@ class B(A): ...
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -290,7 +290,7 @@ type B = A
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -346,7 +346,7 @@ false positives it can produce.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -373,7 +373,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -559,7 +559,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -674,7 +674,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -729,7 +729,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -763,7 +763,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
@@ -799,7 +799,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -850,7 +850,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -879,7 +879,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -923,7 +923,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -965,7 +965,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1077,7 +1077,7 @@ a = 20 / 0 # type: ignore
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -1116,7 +1116,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1151,7 +1151,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1185,7 +1185,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1292,7 +1292,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1346,7 +1346,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1376,7 +1376,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1426,7 +1426,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1452,7 +1452,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1483,7 +1483,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1517,7 +1517,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
@@ -1566,7 +1566,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1591,7 +1591,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1687,7 +1687,7 @@ class C: ...
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1714,7 +1714,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1761,7 +1761,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1791,7 +1791,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1821,7 +1821,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
@@ -1855,7 +1855,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1889,7 +1889,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1918,13 +1918,44 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar
+## `invalid-typed-dict-statement`
+
+
+Default level: error ·
+Added in 0.0.9 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Detects statements other than annotated declarations in `TypedDict` class bodies.
+
+**Why is this bad?**
+
+`TypedDict` class bodies aren't allowed to contain any other types of statements. For
+example, method definitions and field values aren't allowed. None of these will be
+available on "instances of the `TypedDict`" at runtime (as `dict` is the runtime class of
+all "`TypedDict` instances").
+
+**Example**
+
+```python
+from typing import TypedDict
+
+class Foo(TypedDict):
+ def bar(self): # error: [invalid-typed-dict-statement]
+ pass
+```
+
## `missing-argument`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1949,7 +1980,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
@@ -1982,7 +2013,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2011,7 +2042,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2037,7 +2068,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
@@ -2061,7 +2092,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2094,7 +2125,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2121,7 +2152,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2148,7 +2179,7 @@ f(x=1) # Error raised here
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2176,7 +2207,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2208,7 +2239,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: ignore ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2245,7 +2276,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2309,7 +2340,7 @@ def test(): -> "int":
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2336,7 +2367,7 @@ cast(int, f()) # Redundant
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2366,7 +2397,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
@@ -2395,7 +2426,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -2429,7 +2460,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2456,7 +2487,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2484,7 +2515,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2530,7 +2561,7 @@ class A:
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2554,7 +2585,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2581,7 +2612,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
@@ -2609,7 +2640,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2667,7 +2698,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2692,7 +2723,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2717,7 +2748,7 @@ print(x) # NameError: name 'x' is not defined
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2756,7 +2787,7 @@ class D(C): ... # error: [unsupported-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2793,7 +2824,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2852,7 +2883,7 @@ a = 20 / 2
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2915,7 +2946,7 @@ def foo(x: int | str) -> int | str:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md
index adc966aa28..0cf810c418 100644
--- a/crates/ty_python_semantic/resources/mdtest/override.md
+++ b/crates/ty_python_semantic/resources/mdtest/override.md
@@ -551,10 +551,8 @@ class MyNamedTupleChild(MyNamedTupleParent):
class MyTypedDict(TypedDict):
x: int
+ # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
@override
- # TODO: it's invalid to define a method on a `TypedDict` class,
- # so we should emit a diagnostic here.
- # It shouldn't be an `invalid-explicit-override` diagnostic, however.
def copy(self) -> Self: ...
class Grandparent(Any): ...
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla…_(bef70731cae5b8af).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla…_(bef70731cae5b8af).snap
new file mode 100644
index 0000000000..ecee54b703
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla…_(bef70731cae5b8af).snap
@@ -0,0 +1,103 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+
+---
+mdtest name: typed_dict.md - `TypedDict` - Only annotated declarations are allowed in the class body
+mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+ 1 | from typing import TypedDict
+ 2 |
+ 3 | class Foo(TypedDict):
+ 4 | """docstring"""
+ 5 |
+ 6 | annotated_item: int
+ 7 | """attribute docstring"""
+ 8 |
+ 9 | pass
+10 |
+11 | # As a non-standard but common extension, we interpret `...` as equivalent to `pass`.
+12 | ...
+13 |
+14 | class Bar(TypedDict):
+15 | a: int
+16 | # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
+17 | 42
+18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
+19 | b: str = "hello"
+20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
+21 | def bar(self): ...
+22 | class Baz(Bar):
+23 | # error: [invalid-typed-dict-statement]
+24 | def baz(self):
+25 | pass
+```
+
+# Diagnostics
+
+```
+error[invalid-typed-dict-statement]: invalid statement in TypedDict class body
+ --> src/mdtest_snippet.py:17:5
+ |
+15 | a: int
+16 | # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
+17 | 42
+ | ^^
+18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
+19 | b: str = "hello"
+ |
+info: Only annotated declarations (`: `) are allowed.
+info: rule `invalid-typed-dict-statement` is enabled by default
+
+```
+
+```
+error[invalid-typed-dict-statement]: TypedDict item cannot have a value
+ --> src/mdtest_snippet.py:19:14
+ |
+17 | 42
+18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
+19 | b: str = "hello"
+ | ^^^^^^^
+20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
+21 | def bar(self): ...
+ |
+info: rule `invalid-typed-dict-statement` is enabled by default
+
+```
+
+```
+error[invalid-typed-dict-statement]: TypedDict class cannot have methods
+ --> src/mdtest_snippet.py:21:5
+ |
+19 | b: str = "hello"
+20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
+21 | def bar(self): ...
+ | ^^^^^^^^^^^^^^^^^^
+22 | class Baz(Bar):
+23 | # error: [invalid-typed-dict-statement]
+ |
+info: rule `invalid-typed-dict-statement` is enabled by default
+
+```
+
+```
+error[invalid-typed-dict-statement]: TypedDict class cannot have methods
+ --> src/mdtest_snippet.py:24:5
+ |
+22 | class Baz(Bar):
+23 | # error: [invalid-typed-dict-statement]
+24 | / def baz(self):
+25 | | pass
+ | |____________^
+ |
+info: rule `invalid-typed-dict-statement` 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 7eddc51a62..a6dbce3426 100644
--- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md
+++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
@@ -2266,6 +2266,47 @@ def match_with_dict(u: Foo | Bar | dict):
reveal_type(u) # revealed: Foo | (dict[Unknown, Unknown] & ~)
```
+## Only annotated declarations are allowed in the class body
+
+
+
+`TypedDict` class bodies are very restricted in what kinds of statements they can contain. Besides
+annotated items, the only allowed statements are docstrings and `pass`. Annotated items are are also
+not allowed to have a value.
+
+```py
+from typing import TypedDict
+
+class Foo(TypedDict):
+ """docstring"""
+
+ annotated_item: int
+ """attribute docstring"""
+
+ pass
+
+ # As a non-standard but common extension, we interpret `...` as equivalent to `pass`.
+ ...
+
+class Bar(TypedDict):
+ a: int
+ # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
+ 42
+ # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
+ b: str = "hello"
+ # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
+ def bar(self): ...
+```
+
+These rules are also enforced for `TypedDict` classes that don't directly inherit from `TypedDict`:
+
+```py
+class Baz(Bar):
+ # error: [invalid-typed-dict-statement]
+ def baz(self):
+ pass
+```
+
[closed]: https://peps.python.org/pep-0728/#disallowing-extra-items-explicitly
[subtyping section]: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 0f1a23aae5..a924e03d4a 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -120,6 +120,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&REDUNDANT_CAST);
registry.register_lint(&UNRESOLVED_GLOBAL);
registry.register_lint(&MISSING_TYPED_DICT_KEY);
+ registry.register_lint(&INVALID_TYPED_DICT_STATEMENT);
registry.register_lint(&INVALID_METHOD_OVERRIDE);
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
@@ -2167,6 +2168,31 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Detects statements other than annotated declarations in `TypedDict` class bodies.
+ ///
+ /// ## Why is this bad?
+ /// `TypedDict` class bodies aren't allowed to contain any other types of statements. For
+ /// example, method definitions and field values aren't allowed. None of these will be
+ /// available on "instances of the `TypedDict`" at runtime (as `dict` is the runtime class of
+ /// all "`TypedDict` instances").
+ ///
+ /// ## Example
+ /// ```python
+ /// from typing import TypedDict
+ ///
+ /// class Foo(TypedDict):
+ /// def bar(self): # error: [invalid-typed-dict-statement]
+ /// pass
+ /// ```
+ pub(crate) static INVALID_TYPED_DICT_STATEMENT = {
+ summary: "detects invalid statements in `TypedDict` class bodies",
+ status: LintStatus::stable("0.0.9"),
+ default_level: Level::Error,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Detects method overrides that violate the [Liskov Substitution Principle] ("LSP").
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 420286ca31..8b7b28d2cf 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -63,11 +63,11 @@ use crate::types::diagnostic::{
INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
- INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NOT_SUBSCRIPTABLE,
- POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
- SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
- UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
- USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
+ INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
+ NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
+ POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL,
+ UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
+ UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
@@ -1054,6 +1054,67 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(protocol) = class.into_protocol_class(self.db()) {
protocol.validate_members(&self.context);
}
+
+ // (9) If it's a `TypedDict` class, check that it doesn't include any invalid
+ // statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax
+ //
+ // The body of the class definition defines the items of the `TypedDict` type. It
+ // may also contain a docstring or pass statements (primarily to allow the creation
+ // of an empty `TypedDict`). No other statements are allowed, and type checkers
+ // should report an error if any are present.
+ if class.is_typed_dict(self.db()) {
+ for stmt in &class_node.body {
+ match stmt {
+ // Annotated assignments are allowed (that's the whole point), but they're
+ // not allowed to have a value.
+ ast::Stmt::AnnAssign(ann_assign) => {
+ if let Some(value) = &ann_assign.value {
+ if let Some(builder) = self
+ .context
+ .report_lint(&INVALID_TYPED_DICT_STATEMENT, &**value)
+ {
+ builder.into_diagnostic(format_args!(
+ "TypedDict item cannot have a value"
+ ));
+ }
+ }
+ continue;
+ }
+ // Pass statements are allowed.
+ ast::Stmt::Pass(_) => continue,
+ ast::Stmt::Expr(expr) => {
+ // Docstrings are allowed.
+ if matches!(*expr.value, ast::Expr::StringLiteral(_)) {
+ continue;
+ }
+ // As a non-standard but common extension, we also interpret `...` as
+ // equivalent to `pass`.
+ if matches!(*expr.value, ast::Expr::EllipsisLiteral(_)) {
+ continue;
+ }
+ }
+ // Everything else is forbidden.
+ _ => {}
+ }
+ if let Some(builder) = self
+ .context
+ .report_lint(&INVALID_TYPED_DICT_STATEMENT, stmt)
+ {
+ if matches!(stmt, ast::Stmt::FunctionDef(_)) {
+ builder.into_diagnostic(format_args!(
+ "TypedDict class cannot have methods"
+ ));
+ } else {
+ let mut diagnostic = builder.into_diagnostic(format_args!(
+ "invalid statement in TypedDict class body"
+ ));
+ diagnostic.info(
+ "Only annotated declarations (`: `) are allowed.",
+ );
+ }
+ }
+ }
+ }
}
}
diff --git a/ty.schema.json b/ty.schema.json
index 50e0dee989..9b65d7e24e 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -856,6 +856,16 @@
}
]
},
+ "invalid-typed-dict-statement": {
+ "title": "detects invalid statements in `TypedDict` class bodies",
+ "description": "## What it does\nDetects statements other than annotated declarations in `TypedDict` class bodies.\n\n## Why is this bad?\n`TypedDict` class bodies aren't allowed to contain any other types of statements. For\nexample, method definitions and field values aren't allowed. None of these will be\navailable on \"instances of the `TypedDict`\" at runtime (as `dict` is the runtime class of\nall \"`TypedDict` instances\").\n\n## Example\n```python\nfrom typing import TypedDict\n\nclass Foo(TypedDict):\n def bar(self): # error: [invalid-typed-dict-statement]\n pass\n```",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"missing-argument": {
"title": "detects missing required arguments in a call",
"description": "## What it does\nChecks for missing required arguments in a call.\n\n## Why is this bad?\nFailing to provide a required argument will raise a `TypeError` at runtime.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc() # TypeError: func() missing 1 required positional argument: 'x'\n```",