[ty] Check method definitions on subclasses for Liskov violations (#21436)

This commit is contained in:
Alex Waygood
2025-11-23 18:08:15 +00:00
committed by GitHub
parent aec225d825
commit e642874cf1
26 changed files with 2192 additions and 187 deletions

View File

@@ -1904,6 +1904,7 @@ we only consider the attribute assignment to be valid if the assigned attribute
from typing import Literal
class Date:
# error: [invalid-method-override]
def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None:
pass

View File

@@ -501,8 +501,8 @@ class A[T]:
return a
class B[T](A[T]):
def f(self, b: T) -> T:
return super().f(b)
def f(self, a: T) -> T:
return super().f(a)
```
## Invalid Usages

View File

@@ -24,10 +24,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> EqReturnType:
def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: A) -> NeReturnType:
def __ne__(self, other: A) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: A) -> LtReturnType:
@@ -66,10 +66,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> EqReturnType:
def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: B) -> NeReturnType:
def __ne__(self, other: B) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: B) -> LtReturnType:
@@ -111,10 +111,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> EqReturnType:
def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: B) -> NeReturnType:
def __ne__(self, other: B) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: B) -> LtReturnType:
@@ -132,12 +132,10 @@ class A:
class Unrelated: ...
class B:
# To override builtins.object.__eq__ and builtins.object.__ne__
# TODO these should emit an invalid override diagnostic
def __eq__(self, other: Unrelated) -> B:
def __eq__(self, other: Unrelated) -> B: # error: [invalid-method-override]
return B()
def __ne__(self, other: Unrelated) -> B:
def __ne__(self, other: Unrelated) -> B: # error: [invalid-method-override]
return B()
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
@@ -180,10 +178,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> A:
def __eq__(self, other: A) -> A: # error: [invalid-method-override]
return A()
def __ne__(self, other: A) -> A:
def __ne__(self, other: A) -> A: # error: [invalid-method-override]
return A()
def __lt__(self, other: A) -> A:
@@ -199,22 +197,22 @@ class A:
return A()
class B(A):
def __eq__(self, other: A) -> EqReturnType:
def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: A) -> NeReturnType:
def __ne__(self, other: A) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: A) -> LtReturnType:
def __lt__(self, other: A) -> LtReturnType: # error: [invalid-method-override]
return LtReturnType()
def __le__(self, other: A) -> LeReturnType:
def __le__(self, other: A) -> LeReturnType: # error: [invalid-method-override]
return LeReturnType()
def __gt__(self, other: A) -> GtReturnType:
def __gt__(self, other: A) -> GtReturnType: # error: [invalid-method-override]
return GtReturnType()
def __ge__(self, other: A) -> GeReturnType:
def __ge__(self, other: A) -> GeReturnType: # error: [invalid-method-override]
return GeReturnType()
reveal_type(A() == B()) # revealed: EqReturnType
@@ -243,10 +241,10 @@ class A:
return A()
class B(A):
def __lt__(self, other: int) -> B:
def __lt__(self, other: int) -> B: # error: [invalid-method-override]
return B()
def __gt__(self, other: int) -> B:
def __gt__(self, other: int) -> B: # error: [invalid-method-override]
return B()
reveal_type(A() < B()) # revealed: A
@@ -291,11 +289,10 @@ Please refer to the [docs](https://docs.python.org/3/reference/datamodel.html#ob
from __future__ import annotations
class A:
# TODO both these overrides should emit invalid-override diagnostic
def __eq__(self, other: int) -> A:
def __eq__(self, other: int) -> A: # error: [invalid-method-override]
return A()
def __ne__(self, other: int) -> A:
def __ne__(self, other: int) -> A: # error: [invalid-method-override]
return A()
reveal_type(A() == A()) # revealed: bool

View File

@@ -155,10 +155,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, o: object) -> EqReturnType:
def __eq__(self, o: object) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, o: object) -> NeReturnType:
def __ne__(self, o: object) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, o: A) -> LtReturnType:
@@ -386,6 +386,7 @@ class NotBoolable:
__bool__: None = None
class A:
# error: [invalid-method-override]
def __eq__(self, other) -> NotBoolable:
return NotBoolable()

View File

@@ -103,8 +103,7 @@ class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]):
reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42]
class FixedLengthSubclassWithDunderLenOverridden(tuple[int]):
# TODO: we should complain about this as a Liskov violation (incompatible override)
def __len__(self) -> Literal[42]:
def __len__(self) -> Literal[42]: # error: [invalid-method-override]
return 42
reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42]

View File

@@ -0,0 +1,525 @@
# The Liskov Substitution Principle
The Liskov Substitution Principle provides the basis for many of the assumptions a type checker
generally makes about types in Python:
> Subtype Requirement: Let `ϕ(x)` be a property provable about objects `x` of type `T`. Then
> `ϕ(y)` should be true for objects `y` of type `S` where `S` is a subtype of `T`.
In order for a type checker's assumptions to be sound, it is crucial for the type checker to enforce
the Liskov Substitution Principle on code that it checks. In practice, this usually manifests as
several checks for a type checker to perform when it checks a subclass `B` of a class `A`:
1. Read-only attributes should only ever be overridden covariantly: if a property `A.p` resolves to
`int` when accessed, accessing `B.p` should either resolve to `int` or a subtype of `int`.
1. Method return types should only ever be overridden covariantly: if a method `A.f` returns `int`
when called, calling `B.f` should also resolve to `int or a subtype of`int\`.
1. Method parameters should only ever be overridden contravariantly: if a method `A.f` can be called
with an argument of type `bool`, then the method `B.f` must also be callable with type `bool`
(though it is permitted for the override to also accept other types)
1. Mutable attributes should only ever be overridden invariantly: if a mutable attribute `A.attr`
resolves to type `str`, it can only be overridden on a subclass with exactly the same type.
## Method return types
<!-- snapshot-diagnostics -->
```pyi
class Super:
def method(self) -> int: ...
class Sub1(Super):
def method(self) -> int: ... # fine
class Sub2(Super):
def method(self) -> bool: ... # fine: `bool` is a subtype of `int`
class Sub3(Super):
def method(self) -> object: ... # error: [invalid-method-override]
class Sub4(Super):
def method(self) -> str: ... # error: [invalid-method-override]
```
## Method parameters
<!-- snapshot-diagnostics -->
```pyi
class Super:
def method(self, x: int, /): ...
class Sub1(Super):
def method(self, x: int, /): ... # fine
class Sub2(Super):
def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
class Sub4(Super):
def method(self, x: int | str, /): ... # fine
class Sub5(Super):
def method(self, x: int): ... # fine: `x` can still be passed positionally
class Sub6(Super):
# fine: `method()` can still be called with just a single argument
def method(self, x: int, *args): ...
class Sub7(Super):
def method(self, x: int, **kwargs): ... # fine
class Sub8(Super):
def method(self, x: int, *args, **kwargs): ... # fine
class Sub9(Super):
def method(self, x: int, extra_positional_arg=42, /): ... # fine
class Sub10(Super):
def method(self, x: int, extra_pos_or_kw_arg=42): ... # fine
class Sub11(Super):
def method(self, x: int, *, extra_kw_only_arg=42): ... # fine
class Sub12(Super):
# Some calls permitted by the superclass are now no longer allowed
# (the method can no longer be passed any arguments!)
def method(self, /): ... # error: [invalid-method-override]
class Sub13(Super):
# Some calls permitted by the superclass are now no longer allowed
# (the method can no longer be passed exactly one argument!)
def method(self, x, y, /): ... # error: [invalid-method-override]
class Sub14(Super):
# Some calls permitted by the superclass are now no longer allowed
# (x can no longer be passed positionally!)
def method(self, /, *, x): ... # error: [invalid-method-override]
class Sub15(Super):
# Some calls permitted by the superclass are now no longer allowed
# (x can no longer be passed any integer -- it now requires a bool!)
def method(self, x: bool, /): ... # error: [invalid-method-override]
class Super2:
def method2(self, x): ...
class Sub16(Super2):
def method2(self, x, /): ... # error: [invalid-method-override]
class Sub17(Super2):
def method2(self, *, x): ... # error: [invalid-method-override]
class Super3:
def method3(self, *, x): ...
class Sub18(Super3):
def method3(self, x): ... # fine: `x` can still be used as a keyword argument
class Sub19(Super3):
def method3(self, x, /): ... # error: [invalid-method-override]
class Super4:
def method(self, *args: int, **kwargs: str): ...
class Sub20(Super4):
def method(self, *args: object, **kwargs: object): ... # fine
class Sub21(Super4):
def method(self, *args): ... # error: [invalid-method-override]
class Sub22(Super4):
def method(self, **kwargs): ... # error: [invalid-method-override]
class Sub23(Super4):
def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
```
## The entire class hierarchy is checked
If a child class's method definition is Liskov-compatible with the method definition on its parent
class, Liskov compatibility must also nonetheless be checked with respect to the method definition
on its grandparent class. This is because type checkers will treat the child class as a subtype of
the grandparent class just as much as they treat it as a subtype of the parent class, so
substitutability with respect to the grandparent class is just as important:
<!-- snapshot-diagnostics -->
`stub.pyi`:
```pyi
from typing import Any
class Grandparent:
def method(self, x: int) -> None: ...
class Parent(Grandparent):
def method(self, x: str) -> None: ... # error: [invalid-method-override]
class Child(Parent):
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
def method(self, x: str) -> None: ... # error: [invalid-method-override]
class OtherChild(Parent):
# compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
def method(self, x: int) -> None: ... # error: [invalid-method-override]
class GradualParent(Grandparent):
def method(self, x: Any) -> None: ...
class ThirdChild(GradualParent):
# `GradualParent.method` is compatible with the signature of `Grandparent.method`,
# and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
# but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
def method(self, x: str) -> None: ... # error: [invalid-method-override]
```
`other_stub.pyi`:
```pyi
class A:
def get(self, default): ...
class B(A):
def get(self, default, /): ... # error: [invalid-method-override]
get = 56
class C(B):
# `get` appears in the symbol table of `C`,
# but that doesn't confuse our diagnostic...
foo = get
class D(C):
# compatible with `C.get` and `B.get`, but not with `A.get`
def get(self, my_default): ... # error: [invalid-method-override]
```
## Non-generic methods on generic classes work as expected
```toml
[environment]
python-version = "3.12"
```
```pyi
class A[T]:
def method(self, x: T) -> None: ...
class B[T](A[T]):
def method(self, x: T) -> None: ... # fine
class C(A[int]):
def method(self, x: int) -> None: ... # fine
class D[T](A[T]):
def method(self, x: object) -> None: ... # fine
class E(A[int]):
def method(self, x: object) -> None: ... # fine
class F[T](A[T]):
# TODO: we should emit `invalid-method-override` on this:
# `str` is not necessarily a supertype of `T`!
def method(self, x: str) -> None: ...
class G(A[int]):
def method(self, x: bool) -> None: ... # error: [invalid-method-override]
```
## Generic methods on non-generic classes work as expected
```toml
[environment]
python-version = "3.12"
```
```pyi
from typing import Never, Self
class A:
def method[T](self, x: T) -> T: ...
class B(A):
def method[T](self, x: T) -> T: ... # fine
class C(A):
def method(self, x: object) -> Never: ... # fine
class D(A):
# TODO: we should emit [invalid-method-override] here:
# `A.method` accepts an argument of any type,
# but `D.method` only accepts `int`s
def method(self, x: int) -> int: ...
class A2:
def method(self, x: int) -> int: ...
class B2(A2):
# fine: although `B2.method()` will not always return an `int`,
# an instance of `B2` can be substituted wherever an instance of `A2` is expected,
# and it *will* always return an `int` if it is passed an `int`
# (which is all that will be allowed if an instance of `A2` is expected)
def method[T](self, x: T) -> T: ...
class C2(A2):
def method[T: int](self, x: T) -> T: ...
class D2(A2):
# The type variable is bound to a type disjoint from `int`,
# so the method will not accept integers, and therefore this is an invalid override
def method[T: str](self, x: T) -> T: ... # error: [invalid-method-override]
class A3:
def method(self) -> Self: ...
class B3(A3):
def method(self) -> Self: ... # fine
class C3(A3):
# TODO: should this be allowed?
# Mypy/pyright/pyrefly all allow it,
# but conceptually it seems similar to `B4.method` below,
# which mypy/pyrefly agree is a Liskov violation
# (pyright disagrees as of 20/11/2025: https://github.com/microsoft/pyright/issues/11128)
# when called on a subclass, `C3.method()` will not return an
# instance of that subclass
def method(self) -> C3: ...
class D3(A3):
def method(self: Self) -> Self: ... # fine
class E3(A3):
def method(self: E3) -> Self: ... # fine
class F3(A3):
def method(self: A3) -> Self: ... # fine
class G3(A3):
def method(self: object) -> Self: ... # fine
class H3(A3):
# TODO: we should emit `invalid-method-override` here
# (`A3.method()` can be called on any instance of `A3`,
# but `H3.method()` can only be called on objects that are
# instances of `str`)
def method(self: str) -> Self: ...
class I3(A3):
# TODO: we should emit `invalid-method-override` here
# (`I3.method()` cannot be called with any inhabited type!)
def method(self: Never) -> Self: ...
class A4:
def method[T: int](self, x: T) -> T: ...
class B4(A4):
# TODO: we should emit `invalid-method-override` here.
# `A4.method` promises that if it is passed a `bool`, it will return a `bool`,
# but this is not necessarily true for `B4.method`: if passed a `bool`,
# it could return a non-`bool` `int`!
def method(self, x: int) -> int: ...
```
## Generic methods on generic classes work as expected
```toml
[environment]
python-version = "3.12"
```
```pyi
from typing import Never
class A[T]:
def method[S](self, x: T, y: S) -> S: ...
class B[T](A[T]):
def method[S](self, x: T, y: S) -> S: ... # fine
class C(A[int]):
def method[S](self, x: int, y: S) -> S: ... # fine
class D[T](A[T]):
def method[S](self, x: object, y: S) -> S: ... # fine
class E(A[int]):
def method[S](self, x: object, y: S) -> S: ... # fine
class F(A[int]):
def method(self, x: object, y: object) -> Never: ... # fine
class A2[T]:
def method(self, x: T, y: int) -> int: ...
class B2[T](A2[T]):
def method[S](self, x: T, y: S) -> S: ... # fine
```
## Fully qualified names are used in diagnostics where appropriate
<!-- snapshot-diagnostics -->
`a.pyi`:
```pyi
class A:
def foo(self, x): ...
```
`b.pyi`:
```pyi
import a
class A(a.A):
def foo(self, y): ... # error: [invalid-method-override]
```
## Excluded methods
Certain special constructor methods are excluded from Liskov checks. None of the following classes
cause us to emit any errors, therefore:
```toml
# This is so that the dataclasses machinery will generate `__replace__` methods for us
# (the synthesized `__replace__` methods should not be reported as invalid overrides!)
[environment]
python-version = "3.13"
```
```pyi
from dataclasses import dataclass
from typing_extensions import Self
class Grandparent: ...
class Parent(Grandparent):
def __new__(cls, x: int) -> Self: ...
def __init__(self, x: int) -> None: ...
class Child(Parent):
def __new__(cls, x: str, y: str) -> Self: ...
def __init__(self, x: str, y: str) -> Self: ...
@dataclass(init=False)
class DataSuper:
x: int
def __post_init__(self, x: int) -> None:
self.x = x
@dataclass(init=False)
class DataSub(DataSuper):
y: str
def __post_init__(self, x: int, y: str) -> None:
self.y = y
super().__post_init__(x)
```
## Edge case: function defined in another module and then assigned in a class body
<!-- snapshot-diagnostics -->
`foo.pyi`:
```pyi
def x(self, y: str): ...
```
`bar.pyi`:
```pyi
import foo
class A:
def x(self, y: int): ...
class B(A):
x = foo.x # error: [invalid-method-override]
class C:
x = foo.x
class D(C):
def x(self, y: int): ... # error: [invalid-method-override]
```
## Bad override of `__eq__`
<!-- snapshot-diagnostics -->
```py
class Bad:
x: int
def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
return self.x == other.x
```
## Synthesized methods
`NamedTuple` classes and dataclasses both have methods generated at runtime that do not have
source-code definitions. There are several scenarios to consider here:
1. A synthesized method on a superclass is overridden by a "normal" (not synthesized) method on a
subclass
1. A "normal" method on a superclass is overridden by a synthesized method on a subclass
1. A synthesized method on a superclass is overridden by a synthesized method on a subclass
<!-- snapshot-diagnostics -->
```pyi
from dataclasses import dataclass
from typing import NamedTuple
@dataclass(order=True)
class Foo:
x: int
class Bar(Foo):
def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
# TODO: specifying `order=True` on the subclass means that a `__lt__` method is
# generated that is incompatible with the generated `__lt__` method on the superclass.
# We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
# be `invalid-method-override` since we'd emit it on the class definition rather than
# on any method definition. Note also that no other type checker complains about this
# as of 2025-11-21.
@dataclass(order=True)
class Bar2(Foo):
y: str
# TODO: Although this class does not override any methods of `Foo`, the design of the
# `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
# Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
# expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
# and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
# be compared with instances of subclasses of `Foo`).
#
# Many users would probably like their type checkers to alert them to cases where instances
# of subclasses cannot be substituted for instances of superclasses, as this violates many
# assumptions a type checker will make and makes it likely that a type checker will fail to
# catch type errors elsewhere in the user's code. We could therefore consider treating all
# `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
# this probably shouldn't be reported with the same error code as Liskov violations, since
# the error does not stem from any method signatures written by the user. The example is
# only included here for completeness.
#
# Note that no other type checker catches this error as of 2025-11-21.
class Bar3(Foo): ...
class Eggs:
def __lt__(self, other: Eggs) -> bool: ...
# TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
# We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
# diagnostic here but pyright and pyrefly do not.
@dataclass(order=True)
class Ham(Eggs):
x: int
class Baz(NamedTuple):
x: int
class Spam(Baz):
def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
```

View File

@@ -3069,18 +3069,15 @@ from typing import Protocol
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to, is_disjoint_from
class HasRepr(Protocol):
# TODO: we should emit a diagnostic here complaining about a Liskov violation
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasRepr`)
# error: [invalid-method-override]
def __repr__(self) -> object: ...
class HasReprRecursive(Protocol):
# TODO: we should emit a diagnostic here complaining about a Liskov violation
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursive`)
# error: [invalid-method-override]
def __repr__(self) -> "HasReprRecursive": ...
class HasReprRecursiveAndFoo(Protocol):
# TODO: we should emit a diagnostic here complaining about a Liskov violation
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursiveAndFoo`)
# error: [invalid-method-override]
def __repr__(self) -> "HasReprRecursiveAndFoo": ...
foo: int

View File

@@ -0,0 +1,52 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Bad override of `__eq__`
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.py
```
1 | class Bad:
2 | x: int
3 | def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
4 | return self.x == other.x
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `__eq__`
--> src/mdtest_snippet.py:3:9
|
1 | class Bad:
2 | x: int
3 | def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__`
4 | return self.x == other.x
|
::: stdlib/builtins.pyi:142:9
|
140 | def __setattr__(self, name: str, value: Any, /) -> None: ...
141 | def __delattr__(self, name: str, /) -> None: ...
142 | def __eq__(self, value: object, /) -> bool: ...
| -------------------------------------- `object.__eq__` defined here
143 | def __ne__(self, value: object, /) -> bool: ...
144 | def __str__(self) -> str: ... # noqa: Y029
|
info: This violates the Liskov Substitution Principle
help: It is recommended for `__eq__` to work with arbitrary objects, for example:
help
help: def __eq__(self, other: object) -> bool:
help: if not isinstance(other, Bad):
help: return False
help: return <logic to compare two `Bad` instances>
help
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,82 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Edge case: function defined in another module and then assigned in a class body
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## foo.pyi
```
1 | def x(self, y: str): ...
```
## bar.pyi
```
1 | import foo
2 |
3 | class A:
4 | def x(self, y: int): ...
5 |
6 | class B(A):
7 | x = foo.x # error: [invalid-method-override]
8 |
9 | class C:
10 | x = foo.x
11 |
12 | class D(C):
13 | def x(self, y: int): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `x`
--> src/bar.pyi:4:9
|
3 | class A:
4 | def x(self, y: int): ...
| --------------- `A.x` defined here
5 |
6 | class B(A):
7 | x = foo.x # error: [invalid-method-override]
| ^^^^^^^^^ Definition is incompatible with `A.x`
8 |
9 | class C:
|
::: src/foo.pyi:1:5
|
1 | def x(self, y: str): ...
| --------------- Signature of `B.x`
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `x`
--> src/bar.pyi:10:5
|
9 | class C:
10 | x = foo.x
| --------- `C.x` defined here
11 |
12 | class D(C):
13 | def x(self, y: int): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^ Definition is incompatible with `C.x`
|
::: src/foo.pyi:1:5
|
1 | def x(self, y: str): ...
| --------------- Signature of `C.x`
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,47 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Fully qualified names are used in diagnostics where appropriate
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## a.pyi
```
1 | class A:
2 | def foo(self, x): ...
```
## b.pyi
```
1 | import a
2 |
3 | class A(a.A):
4 | def foo(self, y): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `foo`
--> src/b.pyi:4:9
|
3 | class A(a.A):
4 | def foo(self, y): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^ Definition is incompatible with `a.A.foo`
|
::: src/a.pyi:2:9
|
1 | class A:
2 | def foo(self, x): ...
| ------------ `a.A.foo` defined here
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,331 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Method parameters
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | class Super:
2 | def method(self, x: int, /): ...
3 |
4 | class Sub1(Super):
5 | def method(self, x: int, /): ... # fine
6 |
7 | class Sub2(Super):
8 | def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
9 |
10 | class Sub4(Super):
11 | def method(self, x: int | str, /): ... # fine
12 |
13 | class Sub5(Super):
14 | def method(self, x: int): ... # fine: `x` can still be passed positionally
15 |
16 | class Sub6(Super):
17 | # fine: `method()` can still be called with just a single argument
18 | def method(self, x: int, *args): ...
19 |
20 | class Sub7(Super):
21 | def method(self, x: int, **kwargs): ... # fine
22 |
23 | class Sub8(Super):
24 | def method(self, x: int, *args, **kwargs): ... # fine
25 |
26 | class Sub9(Super):
27 | def method(self, x: int, extra_positional_arg=42, /): ... # fine
28 |
29 | class Sub10(Super):
30 | def method(self, x: int, extra_pos_or_kw_arg=42): ... # fine
31 |
32 | class Sub11(Super):
33 | def method(self, x: int, *, extra_kw_only_arg=42): ... # fine
34 |
35 | class Sub12(Super):
36 | # Some calls permitted by the superclass are now no longer allowed
37 | # (the method can no longer be passed any arguments!)
38 | def method(self, /): ... # error: [invalid-method-override]
39 |
40 | class Sub13(Super):
41 | # Some calls permitted by the superclass are now no longer allowed
42 | # (the method can no longer be passed exactly one argument!)
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
44 |
45 | class Sub14(Super):
46 | # Some calls permitted by the superclass are now no longer allowed
47 | # (x can no longer be passed positionally!)
48 | def method(self, /, *, x): ... # error: [invalid-method-override]
49 |
50 | class Sub15(Super):
51 | # Some calls permitted by the superclass are now no longer allowed
52 | # (x can no longer be passed any integer -- it now requires a bool!)
53 | def method(self, x: bool, /): ... # error: [invalid-method-override]
54 |
55 | class Super2:
56 | def method2(self, x): ...
57 |
58 | class Sub16(Super2):
59 | def method2(self, x, /): ... # error: [invalid-method-override]
60 |
61 | class Sub17(Super2):
62 | def method2(self, *, x): ... # error: [invalid-method-override]
63 |
64 | class Super3:
65 | def method3(self, *, x): ...
66 |
67 | class Sub18(Super3):
68 | def method3(self, x): ... # fine: `x` can still be used as a keyword argument
69 |
70 | class Sub19(Super3):
71 | def method3(self, x, /): ... # error: [invalid-method-override]
72 |
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
75 |
76 | class Sub20(Super4):
77 | def method(self, *args: object, **kwargs: object): ... # fine
78 |
79 | class Sub21(Super4):
80 | def method(self, *args): ... # error: [invalid-method-override]
81 |
82 | class Sub22(Super4):
83 | def method(self, **kwargs): ... # error: [invalid-method-override]
84 |
85 | class Sub23(Super4):
86 | def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:38:9
|
36 | # Some calls permitted by the superclass are now no longer allowed
37 | # (the method can no longer be passed any arguments!)
38 | def method(self, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
39 |
40 | class Sub13(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:43:9
|
41 | # Some calls permitted by the superclass are now no longer allowed
42 | # (the method can no longer be passed exactly one argument!)
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
44 |
45 | class Sub14(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:48:9
|
46 | # Some calls permitted by the superclass are now no longer allowed
47 | # (x can no longer be passed positionally!)
48 | def method(self, /, *, x): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
49 |
50 | class Sub15(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:53:9
|
51 | # Some calls permitted by the superclass are now no longer allowed
52 | # (x can no longer be passed any integer -- it now requires a bool!)
53 | def method(self, x: bool, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
54 |
55 | class Super2:
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method2`
--> src/mdtest_snippet.pyi:56:9
|
55 | class Super2:
56 | def method2(self, x): ...
| ---------------- `Super2.method2` defined here
57 |
58 | class Sub16(Super2):
59 | def method2(self, x, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2`
60 |
61 | class Sub17(Super2):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method2`
--> src/mdtest_snippet.pyi:62:9
|
61 | class Sub17(Super2):
62 | def method2(self, *, x): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2`
63 |
64 | class Super3:
|
::: src/mdtest_snippet.pyi:56:9
|
55 | class Super2:
56 | def method2(self, x): ...
| ---------------- `Super2.method2` defined here
57 |
58 | class Sub16(Super2):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method3`
--> src/mdtest_snippet.pyi:71:9
|
70 | class Sub19(Super3):
71 | def method3(self, x, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super3.method3`
72 |
73 | class Super4:
|
::: src/mdtest_snippet.pyi:65:9
|
64 | class Super3:
65 | def method3(self, *, x): ...
| ------------------- `Super3.method3` defined here
66 |
67 | class Sub18(Super3):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:80:9
|
79 | class Sub21(Super4):
80 | def method(self, *args): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
81 |
82 | class Sub22(Super4):
|
::: src/mdtest_snippet.pyi:74:9
|
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
| --------------------------------------- `Super4.method` defined here
75 |
76 | class Sub20(Super4):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:83:9
|
82 | class Sub22(Super4):
83 | def method(self, **kwargs): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
84 |
85 | class Sub23(Super4):
|
::: src/mdtest_snippet.pyi:74:9
|
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
| --------------------------------------- `Super4.method` defined here
75 |
76 | class Sub20(Super4):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:86:9
|
85 | class Sub23(Super4):
86 | def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
|
::: src/mdtest_snippet.pyi:74:9
|
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
| --------------------------------------- `Super4.method` defined here
75 |
76 | class Sub20(Super4):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,75 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Method return types
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | class Super:
2 | def method(self) -> int: ...
3 |
4 | class Sub1(Super):
5 | def method(self) -> int: ... # fine
6 |
7 | class Sub2(Super):
8 | def method(self) -> bool: ... # fine: `bool` is a subtype of `int`
9 |
10 | class Sub3(Super):
11 | def method(self) -> object: ... # error: [invalid-method-override]
12 |
13 | class Sub4(Super):
14 | def method(self) -> str: ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:11:9
|
10 | class Sub3(Super):
11 | def method(self) -> object: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
12 |
13 | class Sub4(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self) -> int: ...
| ------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:14:9
|
13 | class Sub4(Super):
14 | def method(self) -> str: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self) -> int: ...
| ------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,116 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Synthesized methods
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | from dataclasses import dataclass
2 | from typing import NamedTuple
3 |
4 | @dataclass(order=True)
5 | class Foo:
6 | x: int
7 |
8 | class Bar(Foo):
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
10 |
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
12 | # generated that is incompatible with the generated `__lt__` method on the superclass.
13 | # We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
14 | # be `invalid-method-override` since we'd emit it on the class definition rather than
15 | # on any method definition. Note also that no other type checker complains about this
16 | # as of 2025-11-21.
17 | @dataclass(order=True)
18 | class Bar2(Foo):
19 | y: str
20 |
21 | # TODO: Although this class does not override any methods of `Foo`, the design of the
22 | # `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
23 | # Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
24 | # expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
25 | # and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
26 | # be compared with instances of subclasses of `Foo`).
27 | #
28 | # Many users would probably like their type checkers to alert them to cases where instances
29 | # of subclasses cannot be substituted for instances of superclasses, as this violates many
30 | # assumptions a type checker will make and makes it likely that a type checker will fail to
31 | # catch type errors elsewhere in the user's code. We could therefore consider treating all
32 | # `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
33 | # this probably shouldn't be reported with the same error code as Liskov violations, since
34 | # the error does not stem from any method signatures written by the user. The example is
35 | # only included here for completeness.
36 | #
37 | # Note that no other type checker catches this error as of 2025-11-21.
38 | class Bar3(Foo): ...
39 |
40 | class Eggs:
41 | def __lt__(self, other: Eggs) -> bool: ...
42 |
43 | # TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
44 | # We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
45 | # diagnostic here but pyright and pyrefly do not.
46 | @dataclass(order=True)
47 | class Ham(Eggs):
48 | x: int
49 |
50 | class Baz(NamedTuple):
51 | x: int
52 |
53 | class Spam(Baz):
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `__lt__`
--> src/mdtest_snippet.pyi:9:9
|
8 | class Bar(Foo):
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Foo.__lt__`
10 |
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
|
info: This violates the Liskov Substitution Principle
info: `Foo.__lt__` is a generated method created because `Foo` is a dataclass
--> src/mdtest_snippet.pyi:5:7
|
4 | @dataclass(order=True)
5 | class Foo:
| ^^^ Definition of `Foo`
6 | x: int
|
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `_asdict`
--> src/mdtest_snippet.pyi:54:9
|
53 | class Spam(Baz):
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Baz._asdict`
|
info: This violates the Liskov Substitution Principle
info: `Baz._asdict` is a generated method created because `Baz` inherits from `typing.NamedTuple`
--> src/mdtest_snippet.pyi:50:7
|
48 | x: int
49 |
50 | class Baz(NamedTuple):
| ^^^^^^^^^^^^^^^ Definition of `Baz`
51 | x: int
|
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,192 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - The entire class hierarchy is checked
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## stub.pyi
```
1 | from typing import Any
2 |
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
5 |
6 | class Parent(Grandparent):
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
8 |
9 | class Child(Parent):
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
12 |
13 | class OtherChild(Parent):
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
16 |
17 | class GradualParent(Grandparent):
18 | def method(self, x: Any) -> None: ...
19 |
20 | class ThirdChild(GradualParent):
21 | # `GradualParent.method` is compatible with the signature of `Grandparent.method`,
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
```
## other_stub.pyi
```
1 | class A:
2 | def get(self, default): ...
3 |
4 | class B(A):
5 | def get(self, default, /): ... # error: [invalid-method-override]
6 |
7 | get = 56
8 |
9 | class C(B):
10 | # `get` appears in the symbol table of `C`,
11 | # but that doesn't confuse our diagnostic...
12 | foo = get
13 |
14 | class D(C):
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
16 | def get(self, my_default): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:4:9
|
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
| ---------------------------- `Grandparent.method` defined here
5 |
6 | class Parent(Grandparent):
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
8 |
9 | class Child(Parent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:11:9
|
9 | class Child(Parent):
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
12 |
13 | class OtherChild(Parent):
|
::: src/stub.pyi:4:9
|
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
| ---------------------------- `Grandparent.method` defined here
5 |
6 | class Parent(Grandparent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:15:9
|
13 | class OtherChild(Parent):
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method`
16 |
17 | class GradualParent(Grandparent):
|
::: src/stub.pyi:7:9
|
6 | class Parent(Grandparent):
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ---------------------------- `Parent.method` defined here
8 |
9 | class Child(Parent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:24:9
|
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
|
::: src/stub.pyi:4:9
|
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
| ---------------------------- `Grandparent.method` defined here
5 |
6 | class Parent(Grandparent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `get`
--> src/other_stub.pyi:2:9
|
1 | class A:
2 | def get(self, default): ...
| ------------------ `A.get` defined here
3 |
4 | class B(A):
5 | def get(self, default, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get`
6 |
7 | get = 56
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `get`
--> src/other_stub.pyi:16:9
|
14 | class D(C):
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
16 | def get(self, my_default): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get`
|
::: src/other_stub.pyi:2:9
|
1 | class A:
2 | def get(self, default): ...
| ------------------ `A.get` defined here
3 |
4 | class B(A):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -12,27 +12,59 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
## mdtest_snippet.py
```
1 | class NotBoolable:
2 | __bool__: None = None
3 |
4 | class A:
5 | def __eq__(self, other) -> NotBoolable:
6 | return NotBoolable()
7 |
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
1 | class NotBoolable:
2 | __bool__: None = None
3 |
4 | class A:
5 | # error: [invalid-method-override]
6 | def __eq__(self, other) -> NotBoolable:
7 | return NotBoolable()
8 |
9 | # error: [unsupported-bool-conversion]
10 | (A(),) == (A(),)
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `__eq__`
--> src/mdtest_snippet.py:6:9
|
4 | class A:
5 | # error: [invalid-method-override]
6 | def __eq__(self, other) -> NotBoolable:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__`
7 | return NotBoolable()
|
::: stdlib/builtins.pyi:142:9
|
140 | def __setattr__(self, name: str, value: Any, /) -> None: ...
141 | def __delattr__(self, name: str, /) -> None: ...
142 | def __eq__(self, value: object, /) -> bool: ...
| -------------------------------------- `object.__eq__` defined here
143 | def __ne__(self, value: object, /) -> bool: ...
144 | def __str__(self) -> str: ... # noqa: Y029
|
info: This violates the Liskov Substitution Principle
help: It is recommended for `__eq__` to work with arbitrary objects, for example:
help
help: def __eq__(self, other: object) -> bool:
help: if not isinstance(other, A):
help: return False
help: return <logic to compare two `A` instances>
help
info: rule `invalid-method-override` is enabled by default
```
```
error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable`
--> src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
| ^^^^^^^^^^^^^^^^
|
--> src/mdtest_snippet.py:10:1
|
9 | # error: [unsupported-bool-conversion]
10 | (A(),) == (A(),)
| ^^^^^^^^^^^^^^^^
|
info: `__bool__` on `NotBoolable` must be callable
info: rule `unsupported-bool-conversion` is enabled by default