diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index ae4f3d5009..eb6d62eb85 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
@@ -1681,13 +1681,59 @@ class C: ...
- [Typing spec: The meaning of annotations](https://typing.python.org/en/latest/spec/annotations.html#the-meaning-of-annotations)
- [Typing spec: String annotations](https://typing.python.org/en/latest/spec/annotations.html#string-annotations)
+## `invalid-total-ordering`
+
+
+Default level: error ·
+Added in 0.0.10 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for classes decorated with `@functools.total_ordering` that don't
+define any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).
+
+**Why is this bad?**
+
+The `@total_ordering` decorator requires the class to define at least one
+ordering method. If none is defined, Python raises a `ValueError` at runtime.
+
+**Example**
+
+
+```python
+from functools import total_ordering
+
+@total_ordering
+class MyClass: # Error: no ordering method defined
+ def __eq__(self, other: object) -> bool:
+ return True
+```
+
+Use instead:
+
+```python
+from functools import total_ordering
+
+@total_ordering
+class MyClass:
+ def __eq__(self, other: object) -> bool:
+ return True
+
+ def __lt__(self, other: "MyClass") -> bool:
+ return True
+```
+
## `invalid-type-alias-type`
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1714,7 +1760,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 +1807,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 +1837,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1821,7 +1867,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 +1901,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1889,7 +1935,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1924,7 +1970,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.9 ·
Related issues ·
-View source
+View source
@@ -1955,7 +2001,7 @@ class Foo(TypedDict):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1980,7 +2026,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
@@ -2013,7 +2059,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2042,7 +2088,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2068,7 +2114,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
@@ -2092,7 +2138,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
@@ -2125,7 +2171,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2152,7 +2198,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2179,7 +2225,7 @@ f(x=1) # Error raised here
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2207,7 +2253,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
@@ -2239,7 +2285,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
@@ -2276,7 +2322,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
@@ -2340,7 +2386,7 @@ def test(): -> "int":
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2367,7 +2413,7 @@ cast(int, f()) # Redundant
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2397,7 +2443,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
@@ -2426,7 +2472,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -2460,7 +2506,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2487,7 +2533,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2515,7 +2561,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2561,7 +2607,7 @@ class A:
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2585,7 +2631,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
@@ -2612,7 +2658,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
@@ -2640,7 +2686,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
@@ -2698,7 +2744,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2723,7 +2769,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2748,7 +2794,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
@@ -2787,7 +2833,7 @@ class D(C): ... # error: [unsupported-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2824,7 +2870,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2883,7 +2929,7 @@ a = 20 / 2
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2946,7 +2992,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/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
index 94b7d45da3..e9e661c3a0 100644
--- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
+++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
@@ -583,7 +583,7 @@ from module import NotFrozenBase
@final
@dataclass(frozen=True)
-@total_ordering
+@total_ordering # error: [invalid-total-ordering]
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
y: str
```
diff --git a/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md b/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md
index 2739b31d61..48e6b7ee40 100644
--- a/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md
+++ b/crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md
@@ -194,12 +194,12 @@ reveal_type(p1 >= p2) # revealed: bool
## Missing ordering method
If a class has `@total_ordering` but doesn't define any ordering method (itself or in a superclass),
-the decorator would fail at runtime. We don't synthesize methods in this case:
+a diagnostic is emitted at the decorator site:
```py
from functools import total_ordering
-@total_ordering
+@total_ordering # error: [invalid-total-ordering]
class NoOrdering:
def __eq__(self, other: object) -> bool:
return True
@@ -207,7 +207,7 @@ class NoOrdering:
n1 = NoOrdering()
n2 = NoOrdering()
-# These should error because no ordering method is defined.
+# Comparison operators also error because no methods were synthesized.
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap
index fc2dc77a44..d21a71ca8d 100644
--- a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_Other_dataclass_para…_-_frozen__non-frozen_in…_(9af2ab07b8e829e).snap
@@ -61,7 +61,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.
6 |
7 | @final
8 | @dataclass(frozen=True)
- 9 | @total_ordering
+ 9 | @total_ordering # error: [invalid-total-ordering]
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
11 | y: str
```
@@ -126,6 +126,22 @@ info: rule `invalid-frozen-dataclass-subclass` is enabled by default
```
+```
+error[invalid-total-ordering]: Class decorated with `@total_ordering` must define at least one ordering method
+ --> src/main.py:9:1
+ |
+ 7 | @final
+ 8 | @dataclass(frozen=True)
+ 9 | @total_ordering # error: [invalid-total-ordering]
+ | ^^^^^^^^^^^^^^^ `FrozenChild` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`
+10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
+11 | y: str
+ |
+info: The decorator will raise `ValueError` at runtime
+info: rule `invalid-total-ordering` is enabled by default
+
+```
+
```
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
--> src/main.py:8:1
@@ -133,7 +149,7 @@ error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from n
7 | @final
8 | @dataclass(frozen=True)
| ----------------------- `FrozenChild` dataclass parameters
- 9 | @total_ordering
+ 9 | @total_ordering # error: [invalid-total-ordering]
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
| ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not
11 | y: str
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index a924e03d4a..f60cf81142 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -125,6 +125,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
registry.register_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS);
+ registry.register_lint(&INVALID_TOTAL_ORDERING);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -2329,6 +2330,46 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for classes decorated with `@functools.total_ordering` that don't
+ /// define any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).
+ ///
+ /// ## Why is this bad?
+ /// The `@total_ordering` decorator requires the class to define at least one
+ /// ordering method. If none is defined, Python raises a `ValueError` at runtime.
+ ///
+ /// ## Example
+ ///
+ /// ```python
+ /// from functools import total_ordering
+ ///
+ /// @total_ordering
+ /// class MyClass: # Error: no ordering method defined
+ /// def __eq__(self, other: object) -> bool:
+ /// return True
+ /// ```
+ ///
+ /// Use instead:
+ ///
+ /// ```python
+ /// from functools import total_ordering
+ ///
+ /// @total_ordering
+ /// class MyClass:
+ /// def __eq__(self, other: object) -> bool:
+ /// return True
+ ///
+ /// def __lt__(self, other: "MyClass") -> bool:
+ /// return True
+ /// ```
+ pub(crate) static INVALID_TOTAL_ORDERING = {
+ summary: "detects `@total_ordering` classes without an ordering method",
+ status: LintStatus::stable("0.0.10"),
+ default_level: Level::Error,
+ }
+}
+
/// A collection of type check diagnostics.
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
pub struct TypeCheckDiagnostics {
@@ -4618,6 +4659,27 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
}
}
+pub(super) fn report_invalid_total_ordering(
+ context: &InferContext<'_, '_>,
+ class: ClassLiteral<'_>,
+ decorator: &ast::Decorator,
+) {
+ let db = context.db();
+
+ let Some(builder) = context.report_lint(&INVALID_TOTAL_ORDERING, decorator) else {
+ return;
+ };
+
+ let mut diagnostic = builder.into_diagnostic(
+ "Class decorated with `@total_ordering` must define at least one ordering method",
+ );
+ diagnostic.set_primary_message(format_args!(
+ "`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`",
+ class.name(db)
+ ));
+ diagnostic.info("The decorator will raise `ValueError` at runtime");
+}
+
/// This function receives an unresolved `from foo import bar` import,
/// where `foo` can be resolved to a module but that module does not
/// have a `bar` member or submodule.
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 7a057380e9..e018784643 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -77,7 +77,7 @@ use crate::types::diagnostic::{
report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_exception_tuple_caught,
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
- report_invalid_or_unsupported_base, report_invalid_return_type,
+ report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_total_ordering,
report_invalid_type_checking_constant, report_invalid_type_param_order,
report_named_tuple_field_with_leading_underscore,
report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable,
@@ -852,7 +852,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (5) Check that the class's metaclass can be determined without error.
+ // (5) Check that @total_ordering has a valid ordering method in the MRO
+ if class.total_ordering(self.db()) {
+ let has_ordering_method = class
+ .iter_mro(self.db(), None)
+ .filter_map(super::super::class_base::ClassBase::into_class)
+ .filter(|base_class| {
+ !base_class
+ .class_literal(self.db())
+ .0
+ .is_known(self.db(), KnownClass::Object)
+ })
+ .any(|base_class| {
+ base_class
+ .class_literal(self.db())
+ .0
+ .has_own_ordering_method(self.db())
+ });
+
+ if !has_ordering_method {
+ // Find the @total_ordering decorator to report the diagnostic at its location
+ if let Some(decorator) = class_node.decorator_list.iter().find(|decorator| {
+ self.expression_type(&decorator.expression)
+ .as_function_literal()
+ .is_some_and(|function| {
+ function.is_known(self.db(), KnownFunction::TotalOrdering)
+ })
+ }) {
+ report_invalid_total_ordering(&self.context, class, decorator);
+ }
+ }
+ }
+
+ // (6) Check that the class's metaclass can be determined without error.
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
match metaclass_error.reason() {
MetaclassErrorKind::Cycle => {
diff --git a/ty.schema.json b/ty.schema.json
index 9b65d7e24e..0edef9201b 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -786,6 +786,16 @@
}
]
},
+ "invalid-total-ordering": {
+ "title": "detects `@total_ordering` classes without an ordering method",
+ "description": "## What it does\nChecks for classes decorated with `@functools.total_ordering` that don't\ndefine any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).\n\n## Why is this bad?\nThe `@total_ordering` decorator requires the class to define at least one\nordering method. If none is defined, Python raises a `ValueError` at runtime.\n\n## Example\n\n```python\nfrom functools import total_ordering\n\n@total_ordering\nclass MyClass: # Error: no ordering method defined\n def __eq__(self, other: object) -> bool:\n return True\n```\n\nUse instead:\n\n```python\nfrom functools import total_ordering\n\n@total_ordering\nclass MyClass:\n def __eq__(self, other: object) -> bool:\n return True\n\n def __lt__(self, other: \"MyClass\") -> bool:\n return True\n```",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"invalid-type-alias-type": {
"title": "detects invalid TypeAliasType definitions",
"description": "## What it does\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n```python\nfrom typing import TypeAliasType\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\nNewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal\n```",