diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 013c8ca134..34e4227f11 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -217,7 +217,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -329,7 +329,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -359,7 +359,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -385,7 +385,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 @@ -474,7 +474,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -563,7 +563,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 @@ -599,7 +599,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -650,7 +650,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -679,7 +679,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -723,7 +723,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -756,7 +756,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -795,7 +795,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -830,7 +830,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -864,7 +864,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -950,7 +950,7 @@ and `__ne__` methods accept `object` as their second argument. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -982,7 +982,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1012,7 +1012,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 @@ -1062,7 +1062,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1088,7 +1088,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1119,7 +1119,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 @@ -1153,7 +1153,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 @@ -1202,7 +1202,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1227,7 +1227,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1285,7 +1285,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1312,7 +1312,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1342,7 +1342,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1372,7 +1372,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 @@ -1406,7 +1406,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1440,7 +1440,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1475,7 +1475,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1500,7 +1500,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 @@ -1533,7 +1533,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1562,7 +1562,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1586,7 +1586,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1612,7 +1612,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 @@ -1639,7 +1639,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1697,7 +1697,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1727,7 +1727,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 @@ -1756,7 +1756,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1783,7 +1783,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1811,7 +1811,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1857,7 +1857,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1884,7 +1884,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 @@ -1912,7 +1912,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1937,7 +1937,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1962,7 +1962,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1999,7 +1999,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2027,7 +2027,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2052,7 +2052,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2093,7 +2093,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2181,7 +2181,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2209,7 +2209,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 @@ -2241,7 +2241,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2273,7 +2273,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2300,7 +2300,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2324,7 +2324,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2382,7 +2382,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2421,7 +2421,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2484,7 +2484,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2508,7 +2508,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index fd74eca365..24a1c3e2c2 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -523,3 +523,63 @@ class Baz(NamedTuple): class Spam(Baz): def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override] ``` + +## Staticmethods and classmethods + +Methods decorated with `@staticmethod` or `@classmethod` are checked in much the same way as other +methods. + + + +```pyi +class Parent: + def instance_method(self, x: int) -> int: ... + @classmethod + def class_method(cls, x: int) -> int: ... + @staticmethod + def static_method(x: int) -> int: ... + +class BadChild1(Parent): + @staticmethod + def instance_method(self, x: int) -> int: ... # error: [invalid-method-override] + # TODO: we should emit `invalid-method-override` here. + # Although the method has the same signature as `Parent.class_method` + # when accessed on instances, it does not have the same signature as + # `Parent.class_method` when accessed on the class object itself + def class_method(cls, x: int) -> int: ... + def static_method(x: int) -> int: ... # error: [invalid-method-override] + +class BadChild2(Parent): + # TODO: we should emit `invalid-method-override` here. + # Although the method has the same signature as `Parent.class_method` + # when accessed on instances, it does not have the same signature as + # `Parent.class_method` when accessed on the class object itself. + # + # Note that whereas `BadChild1.class_method` is reported as a Liskov violation by + # mypy, pyright and pyrefly, pyright is the only one of those three to report a + # Liskov violation on this method as of 2025-11-23. + @classmethod + def instance_method(self, x: int) -> int: ... + @staticmethod + def class_method(cls, x: int) -> int: ... # error: [invalid-method-override] + @classmethod + def static_method(x: int) -> int: ... # error: [invalid-method-override] + +class BadChild3(Parent): + @classmethod + def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override] + @staticmethod + def static_method(x: bool) -> object: ... # error: [invalid-method-override] + +class GoodChild1(Parent): + @classmethod + def class_method(cls, x: int) -> int: ... + @staticmethod + def static_method(x: int) -> int: ... + +class GoodChild2(Parent): + @classmethod + def class_method(cls, x: object) -> bool: ... + @staticmethod + def static_method(x: object) -> bool: ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut…_-_Staticmethods_and_cl…_(49e28aae6fdd1291).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut…_-_Staticmethods_and_cl…_(49e28aae6fdd1291).snap new file mode 100644 index 0000000000..f7ca2c257f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut…_-_Staticmethods_and_cl…_(49e28aae6fdd1291).snap @@ -0,0 +1,220 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: liskov.md - The Liskov Substitution Principle - Staticmethods and classmethods +mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md +--- + +# Python source files + +## mdtest_snippet.pyi + +``` + 1 | class Parent: + 2 | def instance_method(self, x: int) -> int: ... + 3 | @classmethod + 4 | def class_method(cls, x: int) -> int: ... + 5 | @staticmethod + 6 | def static_method(x: int) -> int: ... + 7 | + 8 | class BadChild1(Parent): + 9 | @staticmethod +10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override] +11 | # TODO: we should emit `invalid-method-override` here. +12 | # Although the method has the same signature as `Parent.class_method` +13 | # when accessed on instances, it does not have the same signature as +14 | # `Parent.class_method` when accessed on the class object itself +15 | def class_method(cls, x: int) -> int: ... +16 | def static_method(x: int) -> int: ... # error: [invalid-method-override] +17 | +18 | class BadChild2(Parent): +19 | # TODO: we should emit `invalid-method-override` here. +20 | # Although the method has the same signature as `Parent.class_method` +21 | # when accessed on instances, it does not have the same signature as +22 | # `Parent.class_method` when accessed on the class object itself. +23 | # +24 | # Note that whereas `BadChild1.class_method` is reported as a Liskov violation by +25 | # mypy, pyright and pyrefly, pyright is the only one of those three to report a +26 | # Liskov violation on this method as of 2025-11-23. +27 | @classmethod +28 | def instance_method(self, x: int) -> int: ... +29 | @staticmethod +30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override] +31 | @classmethod +32 | def static_method(x: int) -> int: ... # error: [invalid-method-override] +33 | +34 | class BadChild3(Parent): +35 | @classmethod +36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override] +37 | @staticmethod +38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override] +39 | +40 | class GoodChild1(Parent): +41 | @classmethod +42 | def class_method(cls, x: int) -> int: ... +43 | @staticmethod +44 | def static_method(x: int) -> int: ... +45 | +46 | class GoodChild2(Parent): +47 | @classmethod +48 | def class_method(cls, x: object) -> bool: ... +49 | @staticmethod +50 | def static_method(x: object) -> bool: ... +``` + +# Diagnostics + +``` +error[invalid-method-override]: Invalid override of method `instance_method` + --> src/mdtest_snippet.pyi:10:9 + | + 8 | class BadChild1(Parent): + 9 | @staticmethod +10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.instance_method` +11 | # TODO: we should emit `invalid-method-override` here. +12 | # Although the method has the same signature as `Parent.class_method` + | + ::: src/mdtest_snippet.pyi:2:9 + | + 1 | class Parent: + 2 | def instance_method(self, x: int) -> int: ... + | ------------------------------------ `Parent.instance_method` defined here + 3 | @classmethod + 4 | def class_method(cls, x: int) -> int: ... + | +info: `BadChild1.instance_method` is a staticmethod but `Parent.instance_method` is an instance method +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + +``` +error[invalid-method-override]: Invalid override of method `static_method` + --> src/mdtest_snippet.pyi:16:9 + | +14 | # `Parent.class_method` when accessed on the class object itself +15 | def class_method(cls, x: int) -> int: ... +16 | def static_method(x: int) -> int: ... # error: [invalid-method-override] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method` +17 | +18 | class BadChild2(Parent): + | + ::: src/mdtest_snippet.pyi:6:9 + | + 4 | def class_method(cls, x: int) -> int: ... + 5 | @staticmethod + 6 | def static_method(x: int) -> int: ... + | ---------------------------- `Parent.static_method` defined here + 7 | + 8 | class BadChild1(Parent): + | +info: `BadChild1.static_method` is an instance method but `Parent.static_method` is a staticmethod +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + +``` +error[invalid-method-override]: Invalid override of method `class_method` + --> src/mdtest_snippet.pyi:30:9 + | +28 | def instance_method(self, x: int) -> int: ... +29 | @staticmethod +30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method` +31 | @classmethod +32 | def static_method(x: int) -> int: ... # error: [invalid-method-override] + | + ::: src/mdtest_snippet.pyi:4:9 + | + 2 | def instance_method(self, x: int) -> int: ... + 3 | @classmethod + 4 | def class_method(cls, x: int) -> int: ... + | -------------------------------- `Parent.class_method` defined here + 5 | @staticmethod + 6 | def static_method(x: int) -> int: ... + | +info: `BadChild2.class_method` is a staticmethod but `Parent.class_method` is a classmethod +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + +``` +error[invalid-method-override]: Invalid override of method `static_method` + --> src/mdtest_snippet.pyi:32:9 + | +30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override] +31 | @classmethod +32 | def static_method(x: int) -> int: ... # error: [invalid-method-override] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method` +33 | +34 | class BadChild3(Parent): + | + ::: src/mdtest_snippet.pyi:6:9 + | + 4 | def class_method(cls, x: int) -> int: ... + 5 | @staticmethod + 6 | def static_method(x: int) -> int: ... + | ---------------------------- `Parent.static_method` defined here + 7 | + 8 | class BadChild1(Parent): + | +info: `BadChild2.static_method` is a classmethod but `Parent.static_method` is a staticmethod +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + +``` +error[invalid-method-override]: Invalid override of method `class_method` + --> src/mdtest_snippet.pyi:36:9 + | +34 | class BadChild3(Parent): +35 | @classmethod +36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method` +37 | @staticmethod +38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override] + | + ::: src/mdtest_snippet.pyi:4:9 + | + 2 | def instance_method(self, x: int) -> int: ... + 3 | @classmethod + 4 | def class_method(cls, x: int) -> int: ... + | -------------------------------- `Parent.class_method` defined here + 5 | @staticmethod + 6 | def static_method(x: int) -> int: ... + | +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + +``` +error[invalid-method-override]: Invalid override of method `static_method` + --> src/mdtest_snippet.pyi:38:9 + | +36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override] +37 | @staticmethod +38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method` +39 | +40 | class GoodChild1(Parent): + | + ::: src/mdtest_snippet.pyi:6:9 + | + 4 | def class_method(cls, x: int) -> int: ... + 5 | @staticmethod + 6 | def static_method(x: int) -> int: ... + | ---------------------------- `Parent.static_method` defined here + 7 | + 8 | class BadChild1(Parent): + | +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cbe2f14e0e..1f5dfc71b8 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1107,13 +1107,6 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_bound_method(self) -> Option> { - match self { - Type::BoundMethod(bound_method) => Some(bound_method), - _ => None, - } - } - #[track_caller] pub(crate) const fn expect_class_literal(self) -> ClassLiteral<'db> { self.as_class_literal() diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 1543dcd88f..151d329d47 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1264,6 +1264,14 @@ impl MethodDecorator { (false, false) => Ok(Self::None), } } + + pub(crate) const fn description(self) -> &'static str { + match self { + MethodDecorator::None => "an instance method", + MethodDecorator::ClassMethod => "a classmethod", + MethodDecorator::StaticMethod => "a staticmethod", + } + } } /// Kind-specific metadata for different types of fields diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index ceecbce7f4..050a55fb93 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -8,13 +8,13 @@ use super::{ use crate::diagnostic::did_you_mean; use crate::diagnostic::format_enumeration; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; +use crate::place::Place; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::place::{PlaceTable, ScopedPlaceId}; use crate::semantic_index::{global_scope, place_table, use_def_map}; use crate::suppression::FileSuppressionId; -use crate::types::KnownInstanceType; use crate::types::call::CallError; -use crate::types::class::{DisjointBase, DisjointBaseKind, Field}; +use crate::types::class::{DisjointBase, DisjointBaseKind, Field, MethodDecorator}; use crate::types::function::{FunctionType, KnownFunction}; use crate::types::liskov::{MethodKind, SynthesizedMethodKind}; use crate::types::string_annotation::{ @@ -27,6 +27,7 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, infer_isolated_expression, protocol_class::ProtocolClass, }; +use crate::types::{KnownInstanceType, MemberLookupPolicy}; use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint}; use itertools::Itertools; use ruff_db::{ @@ -3519,6 +3520,27 @@ pub(super) fn report_invalid_method_override<'db>( "Definition is incompatible with `{overridden_method}`" )); + let class_member = |cls: ClassType<'db>| { + cls.class_member(db, member, MemberLookupPolicy::default()) + .place + }; + + if let Place::Defined(Type::FunctionLiteral(subclass_function), _, _) = class_member(subclass) + && let Place::Defined(Type::FunctionLiteral(superclass_function), _, _) = + class_member(superclass) + && let Ok(superclass_function_kind) = + MethodDecorator::try_from_fn_type(db, superclass_function) + && let Ok(subclass_function_kind) = MethodDecorator::try_from_fn_type(db, subclass_function) + && superclass_function_kind != subclass_function_kind + { + diagnostic.info(format_args!( + "`{class_name}.{member}` is {subclass_function_kind} \ + but `{overridden_method}` is {superclass_function_kind}", + superclass_function_kind = superclass_function_kind.description(), + subclass_function_kind = subclass_function_kind.description(), + )); + } + diagnostic.info("This violates the Liskov Substitution Principle"); if !subclass_definition_kind.is_function_def() @@ -3545,9 +3567,11 @@ pub(super) fn report_invalid_method_override<'db>( .full_range(db, &parsed_module(db, superclass_scope.file(db)).load(db)), ); - let superclass_function_span = superclass_type - .as_bound_method() - .and_then(|method| signature_span(method.function(db))); + let superclass_function_span = match superclass_type { + Type::FunctionLiteral(function) => signature_span(function), + Type::BoundMethod(method) => signature_span(method.function(db)), + _ => None, + }; let superclass_definition_kind = definition.kind(db); diff --git a/crates/ty_python_semantic/src/types/liskov.rs b/crates/ty_python_semantic/src/types/liskov.rs index ec0f1765d7..2547e391d3 100644 --- a/crates/ty_python_semantic/src/types/liskov.rs +++ b/crates/ty_python_semantic/src/types/liskov.rs @@ -49,11 +49,6 @@ fn check_class_declaration<'db>( return; }; - // TODO: classmethods and staticmethods - if function.is_classmethod(db) || function.is_staticmethod(db) { - return; - } - // Constructor methods are not checked for Liskov compliance if matches!( &*member.name,