Skip to content

Commit 7d57fee

Browse files
committed
moar tests
1 parent 7f7169a commit 7d57fee

File tree

3 files changed

+148
-78
lines changed

3 files changed

+148
-78
lines changed

crates/ty_python_semantic/resources/mdtest/liskov.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,17 @@ class Bad:
455455
return self.x == other.x
456456
```
457457

458-
## Bad override of a synthesized method
458+
## Synthesized methods
459+
460+
`NamedTuple` classes and dataclasses both have methods generated at runtime that do not have
461+
source-code definitions. There are several scenarios to consider here:
462+
463+
1. A synthesized method on a superclass is overridden by a "normal" (not synthesized) method on a
464+
subclass
465+
1. A "normal" method on a superclass is overridden by a synthesized method on a subclass
466+
1. A synthesized method on a superclass is overridden by a synthesized method on a subclass
467+
1. No methods are overridden, but the Liskov Substitution Principle is arguably violated anyway
468+
because a generated method on a superclass is incompatible with subclassing(!)
459469

460470
<!-- snapshot-diagnostics -->
461471

@@ -470,6 +480,36 @@ class Foo:
470480
class Bar(Foo):
471481
def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
472482

483+
# TODO: specifying `order=True` on the subclass means that a `__lt__` method is
484+
# generated that is incompatible with the generated `__lt__` method on the superclass.
485+
# We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
486+
# be `invalid-method-override` since we'd emit it on the class definition rather than
487+
# on any method definition. Note also that no other type checker complains about this
488+
# as of 2025-11-21.
489+
@dataclass(order=True)
490+
class Bar2(Foo):
491+
y: str
492+
493+
# TODO: Although this class does not override any methods of `Foo`, it nonetheless
494+
# arguably violates the Liskov Substitution Principle. Instances of `Bar3` cannot be
495+
# substituted wherever an instance of `Foo` is expected, because the generated
496+
# `__lt__` method on `Foo` raises an error unless the r.h.s. and `l.h.s.` have exactly
497+
# the same `__class__` (it does not permit instances of `Foo` to be compared with
498+
# instances of subclasses of `Foo`). We could therefore consider treating all
499+
# `order=True` dataclasses as implicitly `@final` in order to enforce Liskov soundness.
500+
# Note that no other type checker catches this error as of 2025-11-21.
501+
class Bar3(Foo): ...
502+
503+
class Eggs:
504+
def __lt__(self, other: Eggs) -> bool: ...
505+
506+
# TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
507+
# We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
508+
# diagnostic here but pyright and pyrefly do not.
509+
@dataclass(order=True)
510+
class Ham(Eggs):
511+
x: int
512+
473513
class Baz(NamedTuple):
474514
x: int
475515

crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut…_-_Bad_override_of_a_sy…_(b723f1988e2e7bf2).snap

Lines changed: 0 additions & 77 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: liskov.md - The Liskov Substitution Principle - Synthesized methods
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.pyi
13+
14+
```
15+
1 | from dataclasses import dataclass
16+
2 | from typing import NamedTuple
17+
3 |
18+
4 | @dataclass(order=True)
19+
5 | class Foo:
20+
6 | x: int
21+
7 |
22+
8 | class Bar(Foo):
23+
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
24+
10 |
25+
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
26+
12 | # generated that is incompatible with the generated `__lt__` method on the superclass.
27+
13 | # We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
28+
14 | # be `invalid-method-override` since we'd emit it on the class definition rather than
29+
15 | # on any method definition. Note also that no other type checker complains about this
30+
16 | # as of 2025-11-21.
31+
17 | @dataclass(order=True)
32+
18 | class Bar2(Foo):
33+
19 | y: str
34+
20 |
35+
21 | # TODO: Although this class does not override any methods of `Foo`, it nonetheless
36+
22 | # arguably violates the Liskov Substitution Principle. Instances of `Bar3` cannot be
37+
23 | # substituted wherever an instance of `Foo` is expected, because the generated
38+
24 | # `__lt__` method on `Foo` raises an error unless the r.h.s. and `l.h.s.` have exactly
39+
25 | # the same `__class__` (it does not permit instances of `Foo` to be compared with
40+
26 | # instances of subclasses of `Foo`). We could therefore consider treating all
41+
27 | # `order=True` dataclasses as implicitly `@final` in order to enforce Liskov soundness.
42+
28 | # Note that no other type checker catches this error as of 2025-11-21.
43+
29 | class Bar3(Foo): ...
44+
30 |
45+
31 | class Eggs:
46+
32 | def __lt__(self, other: Eggs) -> bool: ...
47+
33 |
48+
34 | # TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
49+
35 | # We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
50+
36 | # diagnostic here but pyright and pyrefly do not.
51+
37 | @dataclass(order=True)
52+
38 | class Ham(Eggs):
53+
39 | x: int
54+
40 |
55+
41 | class Baz(NamedTuple):
56+
42 | x: int
57+
43 |
58+
44 | class Spam(Baz):
59+
45 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
60+
```
61+
62+
# Diagnostics
63+
64+
```
65+
error[invalid-method-override]: Invalid override of method `__lt__`
66+
--> src/mdtest_snippet.pyi:9:9
67+
|
68+
8 | class Bar(Foo):
69+
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
70+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Foo.__lt__`
71+
10 |
72+
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
73+
|
74+
info: This violates the Liskov Substitution Principle
75+
info: `Foo.__lt__` is a generated method created because `Foo` is a dataclass
76+
--> src/mdtest_snippet.pyi:5:7
77+
|
78+
4 | @dataclass(order=True)
79+
5 | class Foo:
80+
| ^^^ Definition of `Foo`
81+
6 | x: int
82+
|
83+
info: rule `invalid-method-override` is enabled by default
84+
85+
```
86+
87+
```
88+
error[invalid-method-override]: Invalid override of method `_asdict`
89+
--> src/mdtest_snippet.pyi:45:9
90+
|
91+
44 | class Spam(Baz):
92+
45 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
93+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Baz._asdict`
94+
|
95+
info: This violates the Liskov Substitution Principle
96+
info: `Baz._asdict` is a generated method created because `Baz` inherits from `typing.NamedTuple`
97+
--> src/mdtest_snippet.pyi:41:7
98+
|
99+
39 | x: int
100+
40 |
101+
41 | class Baz(NamedTuple):
102+
| ^^^^^^^^^^^^^^^ Definition of `Baz`
103+
42 | x: int
104+
|
105+
info: rule `invalid-method-override` is enabled by default
106+
107+
```

0 commit comments

Comments
 (0)