Skip to content

Commit 9282e61

Browse files
Disallow @disjoint_base on TypedDicts and Protocols (#24671)
1 parent e9986d8 commit 9282e61

4 files changed

Lines changed: 83 additions & 29 deletions

File tree

crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ decorator introduced by this PEP provides a generalised way for type checkers to
183183
classes.
184184

185185
```py
186-
from typing_extensions import disjoint_base
186+
from typing_extensions import Protocol, TypedDict, disjoint_base
187187

188188
# fmt: off
189189

@@ -213,12 +213,16 @@ class G: ...
213213

214214
@disjoint_base
215215
class H: ...
216-
216+
@disjoint_base # error: [invalid-typed-dict-header] "`@disjoint_base` cannot be used with `TypedDict` class `Movie`"
217+
class Movie(TypedDict):
218+
name: str
219+
@disjoint_base # error: [invalid-protocol] "`@disjoint_base` cannot be used with protocol class `SupportsClose`"
220+
class SupportsClose(Protocol):
221+
def close(self) -> None: ...
217222
class I( # error: [instance-layout-conflict]
218223
G,
219224
H
220225
): ...
221-
222226
# fmt: on
223227
```
224228

crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf…_-_Tests_for_ty's_`inst…_-_Builtins_with_implic…_(4c3d127986a58f11).snap

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict
1313
## mdtest_snippet.py
1414

1515
```
16-
1 | from typing_extensions import disjoint_base
16+
1 | from typing_extensions import Protocol, TypedDict, disjoint_base
1717
2 |
1818
3 | # fmt: off
1919
4 |
@@ -43,15 +43,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict
4343
28 |
4444
29 | @disjoint_base
4545
30 | class H: ...
46-
31 |
47-
32 | class I( # error: [instance-layout-conflict]
48-
33 | G,
49-
34 | H
50-
35 | ): ...
51-
36 |
52-
37 | # fmt: on
53-
38 | # error: [invalid-generic-class]
54-
39 | class Foo(range, str): ... # error: [subclass-of-final-class]
46+
31 | @disjoint_base # error: [invalid-typed-dict-header] "`@disjoint_base` cannot be used with `TypedDict` class `Movie`"
47+
32 | class Movie(TypedDict):
48+
33 | name: str
49+
34 | @disjoint_base # error: [invalid-protocol] "`@disjoint_base` cannot be used with protocol class `SupportsClose`"
50+
35 | class SupportsClose(Protocol):
51+
36 | def close(self) -> None: ...
52+
37 | class I( # error: [instance-layout-conflict]
53+
38 | G,
54+
39 | H
55+
40 | ): ...
56+
41 | # fmt: on
57+
42 | # error: [invalid-generic-class]
58+
43 | class Foo(range, str): ... # error: [subclass-of-final-class]
5559
```
5660

5761
# Diagnostics
@@ -144,33 +148,53 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp
144148
145149
```
146150

151+
```
152+
error[invalid-typed-dict-header]: `@disjoint_base` cannot be used with `TypedDict` class `Movie`
153+
--> src/mdtest_snippet.py:31:1
154+
|
155+
31 | @disjoint_base # error: [invalid-typed-dict-header] "`@disjoint_base` cannot be used with `TypedDict` class `Movie`"
156+
| ^^^^^^^^^^^^^^
157+
|
158+
159+
```
160+
161+
```
162+
error[invalid-protocol]: `@disjoint_base` cannot be used with protocol class `SupportsClose`
163+
--> src/mdtest_snippet.py:34:1
164+
|
165+
34 | @disjoint_base # error: [invalid-protocol] "`@disjoint_base` cannot be used with protocol class `SupportsClose`"
166+
| ^^^^^^^^^^^^^^
167+
|
168+
169+
```
170+
147171
```
148172
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
149-
--> src/mdtest_snippet.py:32:7
173+
--> src/mdtest_snippet.py:37:7
150174
|
151-
32 | class I( # error: [instance-layout-conflict]
175+
37 | class I( # error: [instance-layout-conflict]
152176
| _______^
153-
33 | | G,
154-
34 | | H
155-
35 | | ): ...
177+
38 | | G,
178+
39 | | H
179+
40 | | ): ...
156180
| |_^ Bases `G` and `H` cannot be combined in multiple inheritance
157181
|
158182
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
159-
--> src/mdtest_snippet.py:33:5
183+
--> src/mdtest_snippet.py:38:5
160184
|
161-
33 | G,
185+
38 | G,
162186
| - `G` instances have a distinct memory layout because of the way `G` is implemented in a C extension
163-
34 | H
187+
39 | H
164188
| - `H` instances have a distinct memory layout because of the way `H` is implemented in a C extension
165189
|
166190
167191
```
168192

169193
```
170194
error[invalid-generic-class]: Inconsistent type arguments for `Sequence` among class bases
171-
--> src/mdtest_snippet.py:39:7
195+
--> src/mdtest_snippet.py:43:7
172196
|
173-
39 | class Foo(range, str): ... # error: [subclass-of-final-class]
197+
43 | class Foo(range, str): ... # error: [subclass-of-final-class]
174198
| ^^^^-----^^---^
175199
| | |
176200
| | Later class base inherits from `Sequence[str]`
@@ -181,9 +205,9 @@ error[invalid-generic-class]: Inconsistent type arguments for `Sequence` among c
181205

182206
```
183207
error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `range`
184-
--> src/mdtest_snippet.py:39:11
208+
--> src/mdtest_snippet.py:43:11
185209
|
186-
39 | class Foo(range, str): ... # error: [subclass-of-final-class]
210+
43 | class Foo(range, str): ... # error: [subclass-of-final-class]
187211
| ^^^^^
188212
|
189213

crates/ty_python_semantic/src/types/class/static_literal.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ impl<'db> StaticClassLiteral<'db> {
490490
if self
491491
.known_function_decorators(db)
492492
.contains(&KnownFunction::DisjointBase)
493+
&& !self.is_typed_dict(db)
494+
&& !self.is_protocol(db)
493495
{
494496
Some(DisjointBase::due_to_decorator(self))
495497
} else if SlotsKind::from(db, self) == SlotsKind::NotEmpty {

crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ use crate::{
5353
use ty_python_core::{SemanticIndex, definition::DefinitionKind, scope::ScopeId};
5454

5555
/// Iterate over all static class definitions (created using `class` statements) to check that
56-
/// the definition will not cause an exception to be raised at runtime. This needs to be done
57-
/// after most other types in the scope have been inferred, due to the fact that base classes
58-
/// can be deferred. If it looks like a class definition is invalid in some way, issue a
59-
/// diagnostic.
56+
/// the definition is semantically valid and will not cause an exception to be raised at runtime.
57+
/// This needs to be done after most other types in the scope have been inferred, due to the fact
58+
/// that base classes can be deferred. If it looks like a class definition is invalid in some way,
59+
/// issue a diagnostic.
6060
///
6161
/// Note: Dynamic classes created via `type()` calls are checked separately during type
6262
/// inference of the call expression.
@@ -142,6 +142,30 @@ pub(crate) fn check_static_class_definitions<'db>(
142142

143143
let is_protocol = class.is_protocol(db);
144144

145+
if let Some(disjoint_base_decorator) = class_node.decorator_list.iter().find(|decorator| {
146+
file_expression_type(&decorator.expression)
147+
.as_function_literal()
148+
.is_some_and(|function| function.is_known(db, KnownFunction::DisjointBase))
149+
}) {
150+
if class_kind == Some(CodeGeneratorKind::TypedDict) {
151+
if let Some(builder) =
152+
context.report_lint(&INVALID_TYPED_DICT_HEADER, disjoint_base_decorator)
153+
{
154+
builder.into_diagnostic(format_args!(
155+
"`@disjoint_base` cannot be used with `TypedDict` class `{}`",
156+
class.name(db),
157+
));
158+
}
159+
} else if is_protocol
160+
&& let Some(builder) = context.report_lint(&INVALID_PROTOCOL, disjoint_base_decorator)
161+
{
162+
builder.into_diagnostic(format_args!(
163+
"`@disjoint_base` cannot be used with protocol class `{}`",
164+
class.name(db),
165+
));
166+
}
167+
}
168+
145169
// Check for invalid `@dataclass` applications.
146170
if class.dataclass_params(db).is_some() {
147171
if class.has_named_tuple_class_in_mro(db) {

0 commit comments

Comments
 (0)