From 8bcfc198b83051db0c8a3c7b8f0c8a8c339947c7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 28 Nov 2025 15:18:02 +0000 Subject: [PATCH] [ty] Implement `typing.final` for methods (#21646) Co-authored-by: Micha Reiser --- crates/ty/docs/rules.md | 177 +++--- .../resources/mdtest/final.md | 455 +++++++++++++- ..._possibly-undefined…_(fc7b496fd1986deb).snap | 320 ++++++++++ ...annot_override_a_me…_(338615109711a91b).snap | 545 +++++++++++++++++ ...iagnostic_edge_case…_(2389d52c5ecfa2bd).snap | 59 ++ ...nly_the_first_`@fin…_(9863b583f4c651c5).snap | 101 ++++ ...verloaded_methods_d…_(861757f48340ed92).snap | 565 ++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 5 + .../src/types/diagnostic.rs | 151 ++++- .../ty_python_semantic/src/types/function.rs | 103 +++- .../src/types/infer/builder.rs | 4 +- crates/ty_python_semantic/src/types/liskov.rs | 270 +++++++-- .../e2e__commands__debug_command.snap | 1 + ty.schema.json | 10 + 14 files changed, 2607 insertions(+), 159 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_A_possibly-undefined…_(fc7b496fd1986deb).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Diagnostic_edge_case…_(2389d52c5ecfa2bd).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Only_the_first_`@fin…_(9863b583f4c651c5).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Overloaded_methods_d…_(861757f48340ed92).snap diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 16c4c225dc..6923aeef6f 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 · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,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 @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,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 @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -793,7 +793,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -826,7 +826,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -934,7 +934,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1041,7 +1041,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 @@ -1073,7 +1073,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 @@ -1103,7 +1103,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 @@ -1153,7 +1153,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1179,7 +1179,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1210,7 +1210,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 @@ -1244,7 +1244,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 @@ -1293,7 +1293,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1318,7 +1318,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1376,7 +1376,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1403,7 +1403,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 @@ -1450,7 +1450,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1480,7 +1480,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1510,7 +1510,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 @@ -1544,7 +1544,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1578,7 +1578,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1613,7 +1613,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 @@ -1638,7 +1638,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 @@ -1671,7 +1671,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1700,7 +1700,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1724,7 +1724,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 @@ -1744,13 +1744,46 @@ for i in 34: # TypeError: 'int' object is not iterable pass ``` +## `override-of-final-method` + + +Default level: error · +Added in 0.0.1-alpha.29 · +Related issues · +View source + + + +**What it does** + +Checks for methods on subclasses that override superclass methods decorated with `@final`. + +**Why is this bad?** + +Decorating a method with `@final` declares to the type checker that it should not be +overridden on any subclass. + +**Example** + + +```python +from typing import final + +class A: + @final + def foo(self): ... + +class B(A): + def foo(self): ... # Error raised here +``` + ## `parameter-already-assigned` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1777,7 +1810,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1835,7 +1868,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1865,7 +1898,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 @@ -1894,7 +1927,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1921,7 +1954,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1949,7 +1982,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1995,7 +2028,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2022,7 +2055,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 @@ -2050,7 +2083,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 @@ -2075,7 +2108,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2100,7 +2133,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 @@ -2137,7 +2170,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2165,7 +2198,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 @@ -2190,7 +2223,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 @@ -2231,7 +2264,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2319,7 +2352,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2347,7 +2380,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 @@ -2379,7 +2412,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 @@ -2411,7 +2444,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 @@ -2438,7 +2471,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2462,7 +2495,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 @@ -2520,7 +2553,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2559,7 +2592,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2622,7 +2655,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2646,7 +2679,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/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index e37cf41c8c..1ccb647e4d 100644 --- a/crates/ty_python_semantic/resources/mdtest/final.md +++ b/crates/ty_python_semantic/resources/mdtest/final.md @@ -1,6 +1,6 @@ # Tests for the `@typing(_extensions).final` decorator -## Cannot subclass +## Cannot subclass a class decorated with `@final` Don't do this: @@ -29,3 +29,456 @@ class H( G, ): ... ``` + +## Cannot override a method decorated with `@final` + + + +```pyi +from typing_extensions import final, Callable, TypeVar + +def lossy_decorator(fn: Callable) -> Callable: ... + +class Parent: + @final + def foo(self): ... + + @final + @property + def my_property1(self) -> int: ... + + @property + @final + def my_property2(self) -> int: ... + + @final + @classmethod + def class_method1(cls) -> int: ... + + @classmethod + @final + def class_method2(cls) -> int: ... + + @final + @staticmethod + def static_method1() -> int: ... + + @staticmethod + @final + def static_method2() -> int: ... + + @lossy_decorator + @final + def decorated_1(self): ... + + @final + @lossy_decorator + def decorated_2(self): ... + +class Child(Parent): + # explicitly test the concise diagnostic message, + # which is different to the verbose diagnostic summary message: + # + # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`" + def foo(self): ... + @property + def my_property1(self) -> int: ... # error: [override-of-final-method] + + @property + def my_property2(self) -> int: ... # error: [override-of-final-method] + + @classmethod + def class_method1(cls) -> int: ... # error: [override-of-final-method] + + @staticmethod + def static_method1() -> int: ... # error: [override-of-final-method] + + @classmethod + def class_method2(cls) -> int: ... # error: [override-of-final-method] + + @staticmethod + def static_method2() -> int: ... # error: [override-of-final-method] + + def decorated_1(self): ... # TODO: should emit [override-of-final-method] + + @lossy_decorator + def decorated_2(self): ... # TODO: should emit [override-of-final-method] + +class OtherChild(Parent): ... + +class Grandchild(OtherChild): + @staticmethod + # TODO: we should emit a Liskov violation here too + # error: [override-of-final-method] + def foo(): ... + @property + # TODO: we should emit a Liskov violation here too + # error: [override-of-final-method] + def my_property1(self) -> str: ... + # TODO: we should emit a Liskov violation here too + # error: [override-of-final-method] + class_method1 = None + +# Diagnostic edge case: `final` is very far away from the method definition in the source code: + +T = TypeVar("T") + +def identity(x: T) -> T: ... + +class Foo: + @final + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + @identity + def bar(self): ... + +class Baz(Foo): + def bar(self): ... # error: [override-of-final-method] +``` + +## Diagnostic edge case: superclass with `@final` method has the same name as the subclass + + + +`module1.py`: + +```py +from typing import final + +class Foo: + @final + def f(self): ... +``` + +`module2.py`: + +```py +import module1 + +class Foo(module1.Foo): + def f(self): ... # error: [override-of-final-method] +``` + +## Overloaded methods decorated with `@final` + +In a stub file, `@final` should be applied to the first overload. In a runtime file, `@final` should +only be applied to the implementation function. + + + +`stub.pyi`: + +```pyi +from typing import final, overload + +class Good: + @overload + @final + def bar(self, x: str) -> str: ... + @overload + def bar(self, x: int) -> int: ... + + @final + @overload + def baz(self, x: str) -> str: ... + @overload + def baz(self, x: int) -> int: ... + +class ChildOfGood(Good): + @overload + def bar(self, x: str) -> str: ... + @overload + def bar(self, x: int) -> int: ... # error: [override-of-final-method] + + @overload + def baz(self, x: str) -> str: ... + @overload + def baz(self, x: int) -> int: ... # error: [override-of-final-method] + +class Bad: + @overload + def bar(self, x: str) -> str: ... + @overload + @final + # error: [invalid-overload] + def bar(self, x: int) -> int: ... + + @overload + def baz(self, x: str) -> str: ... + @final + @overload + # error: [invalid-overload] + def baz(self, x: int) -> int: ... + +class ChildOfBad(Bad): + @overload + def bar(self, x: str) -> str: ... + @overload + def bar(self, x: int) -> int: ... # error: [override-of-final-method] + + @overload + def baz(self, x: str) -> str: ... + @overload + def baz(self, x: int) -> int: ... # error: [override-of-final-method] +``` + +`main.py`: + +```py +from typing import overload, final + +class Good: + @overload + def f(self, x: str) -> str: ... + @overload + def f(self, x: int) -> int: ... + @final + def f(self, x: int | str) -> int | str: + return x + +class ChildOfGood(Good): + @overload + def f(self, x: str) -> str: ... + @overload + def f(self, x: int) -> int: ... + # error: [override-of-final-method] + def f(self, x: int | str) -> int | str: + return x + +class Bad: + @overload + @final + def f(self, x: str) -> str: ... + @overload + def f(self, x: int) -> int: ... + # error: [invalid-overload] + def f(self, x: int | str) -> int | str: + return x + + @final + @overload + def g(self, x: str) -> str: ... + @overload + def g(self, x: int) -> int: ... + # error: [invalid-overload] + def g(self, x: int | str) -> int | str: + return x + + @overload + def h(self, x: str) -> str: ... + @overload + @final + def h(self, x: int) -> int: ... + # error: [invalid-overload] + def h(self, x: int | str) -> int | str: + return x + + @overload + def i(self, x: str) -> str: ... + @final + @overload + def i(self, x: int) -> int: ... + # error: [invalid-overload] + def i(self, x: int | str) -> int | str: + return x + +class ChildOfBad(Bad): + # TODO: these should all cause us to emit Liskov violations as well + f = None # error: [override-of-final-method] + g = None # error: [override-of-final-method] + h = None # error: [override-of-final-method] + i = None # error: [override-of-final-method] +``` + +## Edge case: the function is decorated with `@final` but originally defined elsewhere + +As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not. For mypy and +pyright to emit a diagnostic, the superclass definition decorated with `@final` must be a literal +function definition: an assignment definition where the right-hand side of the assignment is a +`@final-decorated` function is not sufficient for them to consider the superclass definition as +being `@final`. + +For now, we choose to follow mypy's and pyright's behaviour here, in order to maximise compatibility +with other type checkers. We may decide to change this in the future, however, as it would simplify +our implementation. Mypy's and pyright's behaviour here is also arguably inconsistent with their +treatment of other type qualifiers such as `Final`. As discussed in +, both type checkers view the `Final` +type qualifier as travelling *across* scopes. + +```py +from typing import final + +class A: + @final + def method(self) -> None: ... + +class B: + method = A.method + +class C(B): + def method(self) -> None: ... # no diagnostic here (see prose discussion above) +``` + +## Constructor methods are also checked + +```py +from typing import final + +class A: + @final + def __init__(self) -> None: ... + +class B(A): + def __init__(self) -> None: ... # error: [override-of-final-method] +``` + +## Only the first `@final` violation is reported + +(Don't do this.) + + + +```py +from typing import final + +class A: + @final + def f(self): ... + +class B(A): + @final + def f(self): ... # error: [override-of-final-method] + +class C(B): + @final + # we only emit one error here, not two + def f(self): ... # error: [override-of-final-method] +``` + +## For when you just really want to drive the point home + +```py +from typing import final, Final + +@final +@final +@final +@final +@final +@final +class A: + @final + @final + @final + @final + @final + def method(self): ... + +@final +@final +@final +@final +@final +class B: + method: Final = A.method + +class C(A): # error: [subclass-of-final-class] + def method(self): ... # error: [override-of-final-method] + +class D(B): # error: [subclass-of-final-class] + # TODO: we should emit a diagnostic here + def method(self): ... +``` + +## An `@final` method is overridden by an implicit instance attribute + +```py +from typing import final, Any + +class Parent: + @final + def method(self) -> None: ... + +class Child(Parent): + def __init__(self) -> None: + self.method: Any = 42 # TODO: we should emit `[override-of-final-method]` here +``` + +## A possibly-undefined `@final` method is overridden + + + +```py +from typing import final + +def coinflip() -> bool: + return False + +class A: + if coinflip(): + @final + def method1(self) -> None: ... + else: + def method1(self) -> None: ... + + if coinflip(): + def method2(self) -> None: ... + else: + @final + def method2(self) -> None: ... + + if coinflip(): + @final + def method3(self) -> None: ... + else: + @final + def method3(self) -> None: ... + + if coinflip(): + def method4(self) -> None: ... + elif coinflip(): + @final + def method4(self) -> None: ... + else: + def method4(self) -> None: ... + +class B(A): + def method1(self) -> None: ... # error: [override-of-final-method] + def method2(self) -> None: ... # error: [override-of-final-method] + def method3(self) -> None: ... # error: [override-of-final-method] + def method4(self) -> None: ... # error: [override-of-final-method] + +# Possible overrides of possibly `@final` methods... +class C(A): + if coinflip(): + # TODO: the autofix here introduces invalid syntax because there are now no + # statements inside the `if:` branch + # (but it might still be a useful autofix in an IDE context?) + def method1(self) -> None: ... # error: [override-of-final-method] + else: + pass + + if coinflip(): + def method2(self) -> None: ... # TODO: should emit [override-of-final-method] + else: + def method2(self) -> None: ... # TODO: should emit [override-of-final-method] + + if coinflip(): + def method3(self) -> None: ... # error: [override-of-final-method] + def method4(self) -> None: ... # error: [override-of-final-method] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_A_possibly-undefined…_(fc7b496fd1986deb).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_A_possibly-undefined…_(fc7b496fd1986deb).snap new file mode 100644 index 0000000000..41b39fef34 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_A_possibly-undefined…_(fc7b496fd1986deb).snap @@ -0,0 +1,320 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: final.md - Tests for the `@typing(_extensions).final` decorator - A possibly-undefined `@final` method is overridden +mdtest path: crates/ty_python_semantic/resources/mdtest/final.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import final + 2 | + 3 | def coinflip() -> bool: + 4 | return False + 5 | + 6 | class A: + 7 | if coinflip(): + 8 | @final + 9 | def method1(self) -> None: ... +10 | else: +11 | def method1(self) -> None: ... +12 | +13 | if coinflip(): +14 | def method2(self) -> None: ... +15 | else: +16 | @final +17 | def method2(self) -> None: ... +18 | +19 | if coinflip(): +20 | @final +21 | def method3(self) -> None: ... +22 | else: +23 | @final +24 | def method3(self) -> None: ... +25 | +26 | if coinflip(): +27 | def method4(self) -> None: ... +28 | elif coinflip(): +29 | @final +30 | def method4(self) -> None: ... +31 | else: +32 | def method4(self) -> None: ... +33 | +34 | class B(A): +35 | def method1(self) -> None: ... # error: [override-of-final-method] +36 | def method2(self) -> None: ... # error: [override-of-final-method] +37 | def method3(self) -> None: ... # error: [override-of-final-method] +38 | def method4(self) -> None: ... # error: [override-of-final-method] +39 | +40 | # Possible overrides of possibly `@final` methods... +41 | class C(A): +42 | if coinflip(): +43 | # TODO: the autofix here introduces invalid syntax because there are now no +44 | # statements inside the `if:` branch +45 | # (but it might still be a useful autofix in an IDE context?) +46 | def method1(self) -> None: ... # error: [override-of-final-method] +47 | else: +48 | pass +49 | +50 | if coinflip(): +51 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method] +52 | else: +53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method] +54 | +55 | if coinflip(): +56 | def method3(self) -> None: ... # error: [override-of-final-method] +57 | def method4(self) -> None: ... # error: [override-of-final-method] +``` + +# Diagnostics + +``` +error[override-of-final-method]: Cannot override `A.method1` + --> src/mdtest_snippet.py:35:9 + | +34 | class B(A): +35 | def method1(self) -> None: ... # error: [override-of-final-method] + | ^^^^^^^ Overrides a definition from superclass `A` +36 | def method2(self) -> None: ... # error: [override-of-final-method] +37 | def method3(self) -> None: ... # error: [override-of-final-method] + | +info: `A.method1` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:8:9 + | + 6 | class A: + 7 | if coinflip(): + 8 | @final + | ------ + 9 | def method1(self) -> None: ... + | ------- `A.method1` defined here +10 | else: +11 | def method1(self) -> None: ... + | +help: Remove the override of `method1` +info: rule `override-of-final-method` is enabled by default +32 | def method4(self) -> None: ... +33 | +34 | class B(A): + - def method1(self) -> None: ... # error: [override-of-final-method] +35 + # error: [override-of-final-method] +36 | def method2(self) -> None: ... # error: [override-of-final-method] +37 | def method3(self) -> None: ... # error: [override-of-final-method] +38 | def method4(self) -> None: ... # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `A.method2` + --> src/mdtest_snippet.py:36:9 + | +34 | class B(A): +35 | def method1(self) -> None: ... # error: [override-of-final-method] +36 | def method2(self) -> None: ... # error: [override-of-final-method] + | ^^^^^^^ Overrides a definition from superclass `A` +37 | def method3(self) -> None: ... # error: [override-of-final-method] +38 | def method4(self) -> None: ... # error: [override-of-final-method] + | +info: `A.method2` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:16:9 + | +14 | def method2(self) -> None: ... +15 | else: +16 | @final + | ------ +17 | def method2(self) -> None: ... + | ------- `A.method2` defined here +18 | +19 | if coinflip(): + | +help: Remove the override of `method2` +info: rule `override-of-final-method` is enabled by default +33 | +34 | class B(A): +35 | def method1(self) -> None: ... # error: [override-of-final-method] + - def method2(self) -> None: ... # error: [override-of-final-method] +36 + # error: [override-of-final-method] +37 | def method3(self) -> None: ... # error: [override-of-final-method] +38 | def method4(self) -> None: ... # error: [override-of-final-method] +39 | +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `A.method3` + --> src/mdtest_snippet.py:37:9 + | +35 | def method1(self) -> None: ... # error: [override-of-final-method] +36 | def method2(self) -> None: ... # error: [override-of-final-method] +37 | def method3(self) -> None: ... # error: [override-of-final-method] + | ^^^^^^^ Overrides a definition from superclass `A` +38 | def method4(self) -> None: ... # error: [override-of-final-method] + | +info: `A.method3` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:20:9 + | +19 | if coinflip(): +20 | @final + | ------ +21 | def method3(self) -> None: ... + | ------- `A.method3` defined here +22 | else: +23 | @final + | +help: Remove the override of `method3` +info: rule `override-of-final-method` is enabled by default +34 | class B(A): +35 | def method1(self) -> None: ... # error: [override-of-final-method] +36 | def method2(self) -> None: ... # error: [override-of-final-method] + - def method3(self) -> None: ... # error: [override-of-final-method] +37 + # error: [override-of-final-method] +38 | def method4(self) -> None: ... # error: [override-of-final-method] +39 | +40 | # Possible overrides of possibly `@final` methods... +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `A.method4` + --> src/mdtest_snippet.py:38:9 + | +36 | def method2(self) -> None: ... # error: [override-of-final-method] +37 | def method3(self) -> None: ... # error: [override-of-final-method] +38 | def method4(self) -> None: ... # error: [override-of-final-method] + | ^^^^^^^ Overrides a definition from superclass `A` +39 | +40 | # Possible overrides of possibly `@final` methods... + | +info: `A.method4` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:29:9 + | +27 | def method4(self) -> None: ... +28 | elif coinflip(): +29 | @final + | ------ +30 | def method4(self) -> None: ... + | ------- `A.method4` defined here +31 | else: +32 | def method4(self) -> None: ... + | +help: Remove the override of `method4` +info: rule `override-of-final-method` is enabled by default +35 | def method1(self) -> None: ... # error: [override-of-final-method] +36 | def method2(self) -> None: ... # error: [override-of-final-method] +37 | def method3(self) -> None: ... # error: [override-of-final-method] + - def method4(self) -> None: ... # error: [override-of-final-method] +38 + # error: [override-of-final-method] +39 | +40 | # Possible overrides of possibly `@final` methods... +41 | class C(A): +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `A.method1` + --> src/mdtest_snippet.py:46:13 + | +44 | # statements inside the `if:` branch +45 | # (but it might still be a useful autofix in an IDE context?) +46 | def method1(self) -> None: ... # error: [override-of-final-method] + | ^^^^^^^ Overrides a definition from superclass `A` +47 | else: +48 | pass + | +info: `A.method1` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:8:9 + | + 6 | class A: + 7 | if coinflip(): + 8 | @final + | ------ + 9 | def method1(self) -> None: ... + | ------- `A.method1` defined here +10 | else: +11 | def method1(self) -> None: ... + | +help: Remove the override of `method1` +info: rule `override-of-final-method` is enabled by default +43 | # TODO: the autofix here introduces invalid syntax because there are now no +44 | # statements inside the `if:` branch +45 | # (but it might still be a useful autofix in an IDE context?) + - def method1(self) -> None: ... # error: [override-of-final-method] +46 + # error: [override-of-final-method] +47 | else: +48 | pass +49 | +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `A.method3` + --> src/mdtest_snippet.py:56:13 + | +55 | if coinflip(): +56 | def method3(self) -> None: ... # error: [override-of-final-method] + | ^^^^^^^ Overrides a definition from superclass `A` +57 | def method4(self) -> None: ... # error: [override-of-final-method] + | +info: `A.method3` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:20:9 + | +19 | if coinflip(): +20 | @final + | ------ +21 | def method3(self) -> None: ... + | ------- `A.method3` defined here +22 | else: +23 | @final + | +help: Remove the override of `method3` +info: rule `override-of-final-method` is enabled by default +53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method] +54 | +55 | if coinflip(): + - def method3(self) -> None: ... # error: [override-of-final-method] +56 + # error: [override-of-final-method] +57 | def method4(self) -> None: ... # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `A.method4` + --> src/mdtest_snippet.py:57:13 + | +55 | if coinflip(): +56 | def method3(self) -> None: ... # error: [override-of-final-method] +57 | def method4(self) -> None: ... # error: [override-of-final-method] + | ^^^^^^^ Overrides a definition from superclass `A` + | +info: `A.method4` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:29:9 + | +27 | def method4(self) -> None: ... +28 | elif coinflip(): +29 | @final + | ------ +30 | def method4(self) -> None: ... + | ------- `A.method4` defined here +31 | else: +32 | def method4(self) -> None: ... + | +help: Remove the override of `method4` +info: rule `override-of-final-method` is enabled by default +54 | +55 | if coinflip(): +56 | def method3(self) -> None: ... # error: [override-of-final-method] + - def method4(self) -> None: ... # error: [override-of-final-method] +57 + # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap new file mode 100644 index 0000000000..ad51738ec5 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap @@ -0,0 +1,545 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: final.md - Tests for the `@typing(_extensions).final` decorator - Cannot override a method decorated with `@final` +mdtest path: crates/ty_python_semantic/resources/mdtest/final.md +--- + +# Python source files + +## mdtest_snippet.pyi + +``` + 1 | from typing_extensions import final, Callable, TypeVar + 2 | + 3 | def lossy_decorator(fn: Callable) -> Callable: ... + 4 | + 5 | class Parent: + 6 | @final + 7 | def foo(self): ... + 8 | + 9 | @final + 10 | @property + 11 | def my_property1(self) -> int: ... + 12 | + 13 | @property + 14 | @final + 15 | def my_property2(self) -> int: ... + 16 | + 17 | @final + 18 | @classmethod + 19 | def class_method1(cls) -> int: ... + 20 | + 21 | @classmethod + 22 | @final + 23 | def class_method2(cls) -> int: ... + 24 | + 25 | @final + 26 | @staticmethod + 27 | def static_method1() -> int: ... + 28 | + 29 | @staticmethod + 30 | @final + 31 | def static_method2() -> int: ... + 32 | + 33 | @lossy_decorator + 34 | @final + 35 | def decorated_1(self): ... + 36 | + 37 | @final + 38 | @lossy_decorator + 39 | def decorated_2(self): ... + 40 | + 41 | class Child(Parent): + 42 | # explicitly test the concise diagnostic message, + 43 | # which is different to the verbose diagnostic summary message: + 44 | # + 45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`" + 46 | def foo(self): ... + 47 | @property + 48 | def my_property1(self) -> int: ... # error: [override-of-final-method] + 49 | + 50 | @property + 51 | def my_property2(self) -> int: ... # error: [override-of-final-method] + 52 | + 53 | @classmethod + 54 | def class_method1(cls) -> int: ... # error: [override-of-final-method] + 55 | + 56 | @staticmethod + 57 | def static_method1() -> int: ... # error: [override-of-final-method] + 58 | + 59 | @classmethod + 60 | def class_method2(cls) -> int: ... # error: [override-of-final-method] + 61 | + 62 | @staticmethod + 63 | def static_method2() -> int: ... # error: [override-of-final-method] + 64 | + 65 | def decorated_1(self): ... # TODO: should emit [override-of-final-method] + 66 | + 67 | @lossy_decorator + 68 | def decorated_2(self): ... # TODO: should emit [override-of-final-method] + 69 | + 70 | class OtherChild(Parent): ... + 71 | + 72 | class Grandchild(OtherChild): + 73 | @staticmethod + 74 | # TODO: we should emit a Liskov violation here too + 75 | # error: [override-of-final-method] + 76 | def foo(): ... + 77 | @property + 78 | # TODO: we should emit a Liskov violation here too + 79 | # error: [override-of-final-method] + 80 | def my_property1(self) -> str: ... + 81 | # TODO: we should emit a Liskov violation here too + 82 | # error: [override-of-final-method] + 83 | class_method1 = None + 84 | + 85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code: + 86 | + 87 | T = TypeVar("T") + 88 | + 89 | def identity(x: T) -> T: ... + 90 | + 91 | class Foo: + 92 | @final + 93 | @identity + 94 | @identity + 95 | @identity + 96 | @identity + 97 | @identity + 98 | @identity + 99 | @identity +100 | @identity +101 | @identity +102 | @identity +103 | @identity +104 | @identity +105 | @identity +106 | @identity +107 | @identity +108 | @identity +109 | @identity +110 | @identity +111 | def bar(self): ... +112 | +113 | class Baz(Foo): +114 | def bar(self): ... # error: [override-of-final-method] +``` + +# Diagnostics + +``` +error[override-of-final-method]: Cannot override `Parent.foo` + --> src/mdtest_snippet.pyi:46:9 + | +44 | # +45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`" +46 | def foo(self): ... + | ^^^ Overrides a definition from superclass `Parent` +47 | @property +48 | def my_property1(self) -> int: ... # error: [override-of-final-method] + | +info: `Parent.foo` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:6:5 + | +5 | class Parent: +6 | @final + | ------ +7 | def foo(self): ... + | --- `Parent.foo` defined here +8 | +9 | @final + | +help: Remove the override of `foo` +info: rule `override-of-final-method` is enabled by default +43 | # which is different to the verbose diagnostic summary message: +44 | # +45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`" + - def foo(self): ... +46 + +47 | @property +48 | def my_property1(self) -> int: ... # error: [override-of-final-method] +49 | +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.my_property1` + --> src/mdtest_snippet.pyi:48:9 + | +46 | def foo(self): ... +47 | @property +48 | def my_property1(self) -> int: ... # error: [override-of-final-method] + | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +49 | +50 | @property + | +info: `Parent.my_property1` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:9:5 + | + 7 | def foo(self): ... + 8 | + 9 | @final + | ------ +10 | @property +11 | def my_property1(self) -> int: ... + | ------------ `Parent.my_property1` defined here +12 | +13 | @property + | +help: Remove the override of `my_property1` +info: rule `override-of-final-method` is enabled by default +44 | # +45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`" +46 | def foo(self): ... + - @property + - def my_property1(self) -> int: ... # error: [override-of-final-method] +47 + # error: [override-of-final-method] +48 | +49 | @property +50 | def my_property2(self) -> int: ... # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.my_property2` + --> src/mdtest_snippet.pyi:51:9 + | +50 | @property +51 | def my_property2(self) -> int: ... # error: [override-of-final-method] + | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +52 | +53 | @classmethod + | +info: `Parent.my_property2` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:14:5 + | +13 | @property +14 | @final + | ------ +15 | def my_property2(self) -> int: ... + | ------------ `Parent.my_property2` defined here +16 | +17 | @final + | +help: Remove the override of `my_property2` +info: rule `override-of-final-method` is enabled by default +47 | @property +48 | def my_property1(self) -> int: ... # error: [override-of-final-method] +49 | + - @property + - def my_property2(self) -> int: ... # error: [override-of-final-method] +50 + # error: [override-of-final-method] +51 | +52 | @classmethod +53 | def class_method1(cls) -> int: ... # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.class_method1` + --> src/mdtest_snippet.pyi:54:9 + | +53 | @classmethod +54 | def class_method1(cls) -> int: ... # error: [override-of-final-method] + | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +55 | +56 | @staticmethod + | +info: `Parent.class_method1` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:17:5 + | +15 | def my_property2(self) -> int: ... +16 | +17 | @final + | ------ +18 | @classmethod +19 | def class_method1(cls) -> int: ... + | ------------- `Parent.class_method1` defined here +20 | +21 | @classmethod + | +help: Remove the override of `class_method1` +info: rule `override-of-final-method` is enabled by default +50 | @property +51 | def my_property2(self) -> int: ... # error: [override-of-final-method] +52 | + - @classmethod + - def class_method1(cls) -> int: ... # error: [override-of-final-method] +53 + # error: [override-of-final-method] +54 | +55 | @staticmethod +56 | def static_method1() -> int: ... # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.static_method1` + --> src/mdtest_snippet.pyi:57:9 + | +56 | @staticmethod +57 | def static_method1() -> int: ... # error: [override-of-final-method] + | ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +58 | +59 | @classmethod + | +info: `Parent.static_method1` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:25:5 + | +23 | def class_method2(cls) -> int: ... +24 | +25 | @final + | ------ +26 | @staticmethod +27 | def static_method1() -> int: ... + | -------------- `Parent.static_method1` defined here +28 | +29 | @staticmethod + | +help: Remove the override of `static_method1` +info: rule `override-of-final-method` is enabled by default +53 | @classmethod +54 | def class_method1(cls) -> int: ... # error: [override-of-final-method] +55 | + - @staticmethod + - def static_method1() -> int: ... # error: [override-of-final-method] +56 + # error: [override-of-final-method] +57 | +58 | @classmethod +59 | def class_method2(cls) -> int: ... # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.class_method2` + --> src/mdtest_snippet.pyi:60:9 + | +59 | @classmethod +60 | def class_method2(cls) -> int: ... # error: [override-of-final-method] + | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +61 | +62 | @staticmethod + | +info: `Parent.class_method2` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:22:5 + | +21 | @classmethod +22 | @final + | ------ +23 | def class_method2(cls) -> int: ... + | ------------- `Parent.class_method2` defined here +24 | +25 | @final + | +help: Remove the override of `class_method2` +info: rule `override-of-final-method` is enabled by default +56 | @staticmethod +57 | def static_method1() -> int: ... # error: [override-of-final-method] +58 | + - @classmethod + - def class_method2(cls) -> int: ... # error: [override-of-final-method] +59 + # error: [override-of-final-method] +60 | +61 | @staticmethod +62 | def static_method2() -> int: ... # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.static_method2` + --> src/mdtest_snippet.pyi:63:9 + | +62 | @staticmethod +63 | def static_method2() -> int: ... # error: [override-of-final-method] + | ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +64 | +65 | def decorated_1(self): ... # TODO: should emit [override-of-final-method] + | +info: `Parent.static_method2` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:30:5 + | +29 | @staticmethod +30 | @final + | ------ +31 | def static_method2() -> int: ... + | -------------- `Parent.static_method2` defined here +32 | +33 | @lossy_decorator + | +help: Remove the override of `static_method2` +info: rule `override-of-final-method` is enabled by default +59 | @classmethod +60 | def class_method2(cls) -> int: ... # error: [override-of-final-method] +61 | + - @staticmethod + - def static_method2() -> int: ... # error: [override-of-final-method] +62 + # error: [override-of-final-method] +63 | +64 | def decorated_1(self): ... # TODO: should emit [override-of-final-method] +65 | +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.foo` + --> src/mdtest_snippet.pyi:76:9 + | +74 | # TODO: we should emit a Liskov violation here too +75 | # error: [override-of-final-method] +76 | def foo(): ... + | ^^^ Overrides a definition from superclass `Parent` +77 | @property +78 | # TODO: we should emit a Liskov violation here too + | +info: `Parent.foo` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:6:5 + | +5 | class Parent: +6 | @final + | ------ +7 | def foo(self): ... + | --- `Parent.foo` defined here +8 | +9 | @final + | +help: Remove the override of `foo` +info: rule `override-of-final-method` is enabled by default +70 | class OtherChild(Parent): ... +71 | +72 | class Grandchild(OtherChild): + - @staticmethod + - # TODO: we should emit a Liskov violation here too + - # error: [override-of-final-method] + - def foo(): ... +73 + +74 | @property +75 | # TODO: we should emit a Liskov violation here too +76 | # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.my_property1` + --> src/mdtest_snippet.pyi:80:9 + | +78 | # TODO: we should emit a Liskov violation here too +79 | # error: [override-of-final-method] +80 | def my_property1(self) -> str: ... + | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +81 | # TODO: we should emit a Liskov violation here too +82 | # error: [override-of-final-method] + | +info: `Parent.my_property1` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:9:5 + | + 7 | def foo(self): ... + 8 | + 9 | @final + | ------ +10 | @property +11 | def my_property1(self) -> int: ... + | ------------ `Parent.my_property1` defined here +12 | +13 | @property + | +help: Remove the override of `my_property1` +info: rule `override-of-final-method` is enabled by default +74 | # TODO: we should emit a Liskov violation here too +75 | # error: [override-of-final-method] +76 | def foo(): ... + - @property + - # TODO: we should emit a Liskov violation here too + - # error: [override-of-final-method] + - def my_property1(self) -> str: ... +77 + +78 | # TODO: we should emit a Liskov violation here too +79 | # error: [override-of-final-method] +80 | class_method1 = None +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Parent.class_method1` + --> src/mdtest_snippet.pyi:83:5 + | +81 | # TODO: we should emit a Liskov violation here too +82 | # error: [override-of-final-method] +83 | class_method1 = None + | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` +84 | +85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code: + | +info: `Parent.class_method1` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:17:5 + | +15 | def my_property2(self) -> int: ... +16 | +17 | @final + | ------ +18 | @classmethod +19 | def class_method1(cls) -> int: ... + | ------------- `Parent.class_method1` defined here +20 | +21 | @classmethod + | +help: Remove the override of `class_method1` +info: rule `override-of-final-method` is enabled by default +80 | def my_property1(self) -> str: ... +81 | # TODO: we should emit a Liskov violation here too +82 | # error: [override-of-final-method] + - class_method1 = None +83 + +84 | +85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code: +86 | +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Foo.bar` + --> src/mdtest_snippet.pyi:114:9 + | +113 | class Baz(Foo): +114 | def bar(self): ... # error: [override-of-final-method] + | ^^^ Overrides a definition from superclass `Foo` + | +info: `Foo.bar` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.pyi:92:5 + | + 91 | class Foo: + 92 | @final + | ------ + 93 | @identity + 94 | @identity + | + ::: src/mdtest_snippet.pyi:111:9 + | +109 | @identity +110 | @identity +111 | def bar(self): ... + | --- `Foo.bar` defined here +112 | +113 | class Baz(Foo): + | +help: Remove the override of `bar` +info: rule `override-of-final-method` is enabled by default +111 | def bar(self): ... +112 | +113 | class Baz(Foo): + - def bar(self): ... # error: [override-of-final-method] +114 + # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Diagnostic_edge_case…_(2389d52c5ecfa2bd).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Diagnostic_edge_case…_(2389d52c5ecfa2bd).snap new file mode 100644 index 0000000000..5e7f11ca02 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Diagnostic_edge_case…_(2389d52c5ecfa2bd).snap @@ -0,0 +1,59 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: final.md - Tests for the `@typing(_extensions).final` decorator - Diagnostic edge case: superclass with `@final` method has the same name as the subclass +mdtest path: crates/ty_python_semantic/resources/mdtest/final.md +--- + +# Python source files + +## module1.py + +``` +1 | from typing import final +2 | +3 | class Foo: +4 | @final +5 | def f(self): ... +``` + +## module2.py + +``` +1 | import module1 +2 | +3 | class Foo(module1.Foo): +4 | def f(self): ... # error: [override-of-final-method] +``` + +# Diagnostics + +``` +error[override-of-final-method]: Cannot override `module1.Foo.f` + --> src/module2.py:4:9 + | +3 | class Foo(module1.Foo): +4 | def f(self): ... # error: [override-of-final-method] + | ^ Overrides a definition from superclass `module1.Foo` + | +info: `module1.Foo.f` is decorated with `@final`, forbidding overrides + --> src/module1.py:4:5 + | +3 | class Foo: +4 | @final + | ------ +5 | def f(self): ... + | - `module1.Foo.f` defined here + | +help: Remove the override of `f` +info: rule `override-of-final-method` is enabled by default +1 | import module1 +2 | +3 | class Foo(module1.Foo): + - def f(self): ... # error: [override-of-final-method] +4 + # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Only_the_first_`@fin…_(9863b583f4c651c5).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Only_the_first_`@fin…_(9863b583f4c651c5).snap new file mode 100644 index 0000000000..342b7ec8b6 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Only_the_first_`@fin…_(9863b583f4c651c5).snap @@ -0,0 +1,101 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: final.md - Tests for the `@typing(_extensions).final` decorator - Only the first `@final` violation is reported +mdtest path: crates/ty_python_semantic/resources/mdtest/final.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import final + 2 | + 3 | class A: + 4 | @final + 5 | def f(self): ... + 6 | + 7 | class B(A): + 8 | @final + 9 | def f(self): ... # error: [override-of-final-method] +10 | +11 | class C(B): +12 | @final +13 | # we only emit one error here, not two +14 | def f(self): ... # error: [override-of-final-method] +``` + +# Diagnostics + +``` +error[override-of-final-method]: Cannot override `A.f` + --> src/mdtest_snippet.py:9:9 + | + 7 | class B(A): + 8 | @final + 9 | def f(self): ... # error: [override-of-final-method] + | ^ Overrides a definition from superclass `A` +10 | +11 | class C(B): + | +info: `A.f` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:4:5 + | +3 | class A: +4 | @final + | ------ +5 | def f(self): ... + | - `A.f` defined here +6 | +7 | class B(A): + | +help: Remove the override of `f` +info: rule `override-of-final-method` is enabled by default +5 | def f(self): ... +6 | +7 | class B(A): + - @final + - def f(self): ... # error: [override-of-final-method] +8 + # error: [override-of-final-method] +9 | +10 | class C(B): +11 | @final +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `B.f` + --> src/mdtest_snippet.py:14:9 + | +12 | @final +13 | # we only emit one error here, not two +14 | def f(self): ... # error: [override-of-final-method] + | ^ Overrides a definition from superclass `B` + | +info: `B.f` is decorated with `@final`, forbidding overrides + --> src/mdtest_snippet.py:8:5 + | + 7 | class B(A): + 8 | @final + | ------ + 9 | def f(self): ... # error: [override-of-final-method] + | - `B.f` defined here +10 | +11 | class C(B): + | +help: Remove the override of `f` +info: rule `override-of-final-method` is enabled by default +9 | def f(self): ... # error: [override-of-final-method] +10 | +11 | class C(B): + - @final + - # we only emit one error here, not two + - def f(self): ... # error: [override-of-final-method] +12 + # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Overloaded_methods_d…_(861757f48340ed92).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Overloaded_methods_d…_(861757f48340ed92).snap new file mode 100644 index 0000000000..38ec75b003 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Overloaded_methods_d…_(861757f48340ed92).snap @@ -0,0 +1,565 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: final.md - Tests for the `@typing(_extensions).final` decorator - Overloaded methods decorated with `@final` +mdtest path: crates/ty_python_semantic/resources/mdtest/final.md +--- + +# Python source files + +## stub.pyi + +``` + 1 | from typing import final, overload + 2 | + 3 | class Good: + 4 | @overload + 5 | @final + 6 | def bar(self, x: str) -> str: ... + 7 | @overload + 8 | def bar(self, x: int) -> int: ... + 9 | +10 | @final +11 | @overload +12 | def baz(self, x: str) -> str: ... +13 | @overload +14 | def baz(self, x: int) -> int: ... +15 | +16 | class ChildOfGood(Good): +17 | @overload +18 | def bar(self, x: str) -> str: ... +19 | @overload +20 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] +21 | +22 | @overload +23 | def baz(self, x: str) -> str: ... +24 | @overload +25 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] +26 | +27 | class Bad: +28 | @overload +29 | def bar(self, x: str) -> str: ... +30 | @overload +31 | @final +32 | # error: [invalid-overload] +33 | def bar(self, x: int) -> int: ... +34 | +35 | @overload +36 | def baz(self, x: str) -> str: ... +37 | @final +38 | @overload +39 | # error: [invalid-overload] +40 | def baz(self, x: int) -> int: ... +41 | +42 | class ChildOfBad(Bad): +43 | @overload +44 | def bar(self, x: str) -> str: ... +45 | @overload +46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] +47 | +48 | @overload +49 | def baz(self, x: str) -> str: ... +50 | @overload +51 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] +``` + +## main.py + +``` + 1 | from typing import overload, final + 2 | + 3 | class Good: + 4 | @overload + 5 | def f(self, x: str) -> str: ... + 6 | @overload + 7 | def f(self, x: int) -> int: ... + 8 | @final + 9 | def f(self, x: int | str) -> int | str: +10 | return x +11 | +12 | class ChildOfGood(Good): +13 | @overload +14 | def f(self, x: str) -> str: ... +15 | @overload +16 | def f(self, x: int) -> int: ... +17 | # error: [override-of-final-method] +18 | def f(self, x: int | str) -> int | str: +19 | return x +20 | +21 | class Bad: +22 | @overload +23 | @final +24 | def f(self, x: str) -> str: ... +25 | @overload +26 | def f(self, x: int) -> int: ... +27 | # error: [invalid-overload] +28 | def f(self, x: int | str) -> int | str: +29 | return x +30 | +31 | @final +32 | @overload +33 | def g(self, x: str) -> str: ... +34 | @overload +35 | def g(self, x: int) -> int: ... +36 | # error: [invalid-overload] +37 | def g(self, x: int | str) -> int | str: +38 | return x +39 | +40 | @overload +41 | def h(self, x: str) -> str: ... +42 | @overload +43 | @final +44 | def h(self, x: int) -> int: ... +45 | # error: [invalid-overload] +46 | def h(self, x: int | str) -> int | str: +47 | return x +48 | +49 | @overload +50 | def i(self, x: str) -> str: ... +51 | @final +52 | @overload +53 | def i(self, x: int) -> int: ... +54 | # error: [invalid-overload] +55 | def i(self, x: int | str) -> int | str: +56 | return x +57 | +58 | class ChildOfBad(Bad): +59 | # TODO: these should all cause us to emit Liskov violations as well +60 | f = None # error: [override-of-final-method] +61 | g = None # error: [override-of-final-method] +62 | h = None # error: [override-of-final-method] +63 | i = None # error: [override-of-final-method] +``` + +# Diagnostics + +``` +error[override-of-final-method]: Cannot override `Good.bar` + --> src/stub.pyi:20:9 + | +18 | def bar(self, x: str) -> str: ... +19 | @overload +20 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] + | ^^^ Overrides a definition from superclass `Good` +21 | +22 | @overload + | +info: `Good.bar` is decorated with `@final`, forbidding overrides + --> src/stub.pyi:5:5 + | +3 | class Good: +4 | @overload +5 | @final + | ------ +6 | def bar(self, x: str) -> str: ... + | --- `Good.bar` defined here +7 | @overload +8 | def bar(self, x: int) -> int: ... + | +help: Remove all overloads for `bar` +info: rule `override-of-final-method` is enabled by default +14 | def baz(self, x: int) -> int: ... +15 | +16 | class ChildOfGood(Good): + - @overload + - def bar(self, x: str) -> str: ... + - @overload + - def bar(self, x: int) -> int: ... # error: [override-of-final-method] +17 + +18 + # error: [override-of-final-method] +19 | +20 | @overload +21 | def baz(self, x: str) -> str: ... +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Good.baz` + --> src/stub.pyi:25:9 + | +23 | def baz(self, x: str) -> str: ... +24 | @overload +25 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] + | ^^^ Overrides a definition from superclass `Good` +26 | +27 | class Bad: + | +info: `Good.baz` is decorated with `@final`, forbidding overrides + --> src/stub.pyi:10:5 + | + 8 | def bar(self, x: int) -> int: ... + 9 | +10 | @final + | ------ +11 | @overload +12 | def baz(self, x: str) -> str: ... + | --- `Good.baz` defined here +13 | @overload +14 | def baz(self, x: int) -> int: ... + | +help: Remove all overloads for `baz` +info: rule `override-of-final-method` is enabled by default +19 | @overload +20 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] +21 | + - @overload + - def baz(self, x: str) -> str: ... + - @overload + - def baz(self, x: int) -> int: ... # error: [override-of-final-method] +22 + +23 + # error: [override-of-final-method] +24 | +25 | class Bad: +26 | @overload +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the first overload + --> src/stub.pyi:29:9 + | +27 | class Bad: +28 | @overload +29 | def bar(self, x: str) -> str: ... + | --- First overload defined here +30 | @overload +31 | @final +32 | # error: [invalid-overload] +33 | def bar(self, x: int) -> int: ... + | ^^^ +34 | +35 | @overload + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the first overload + --> src/stub.pyi:36:9 + | +35 | @overload +36 | def baz(self, x: str) -> str: ... + | --- First overload defined here +37 | @final +38 | @overload +39 | # error: [invalid-overload] +40 | def baz(self, x: int) -> int: ... + | ^^^ +41 | +42 | class ChildOfBad(Bad): + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[override-of-final-method]: Cannot override `Bad.bar` + --> src/stub.pyi:46:9 + | +44 | def bar(self, x: str) -> str: ... +45 | @overload +46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] + | ^^^ Overrides a definition from superclass `Bad` +47 | +48 | @overload + | +info: `Bad.bar` is decorated with `@final`, forbidding overrides + --> src/stub.pyi:29:9 + | +27 | class Bad: +28 | @overload +29 | def bar(self, x: str) -> str: ... + | --- `Bad.bar` defined here +30 | @overload +31 | @final + | +help: Remove all overloads for `bar` +info: rule `override-of-final-method` is enabled by default +40 | def baz(self, x: int) -> int: ... +41 | +42 | class ChildOfBad(Bad): + - @overload + - def bar(self, x: str) -> str: ... + - @overload + - def bar(self, x: int) -> int: ... # error: [override-of-final-method] +43 | +44 + # error: [override-of-final-method] +45 + +46 | @overload +47 | def baz(self, x: str) -> str: ... +48 | @overload +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Bad.baz` + --> src/stub.pyi:51:9 + | +49 | def baz(self, x: str) -> str: ... +50 | @overload +51 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] + | ^^^ Overrides a definition from superclass `Bad` + | +info: `Bad.baz` is decorated with `@final`, forbidding overrides + --> src/stub.pyi:36:9 + | +35 | @overload +36 | def baz(self, x: str) -> str: ... + | --- `Bad.baz` defined here +37 | @final +38 | @overload + | +help: Remove all overloads for `baz` +info: rule `override-of-final-method` is enabled by default +45 | @overload +46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] +47 | + - @overload + - def baz(self, x: str) -> str: ... + - @overload + - def baz(self, x: int) -> int: ... # error: [override-of-final-method] +48 + +49 + # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Good.f` + --> src/main.py:18:9 + | +16 | def f(self, x: int) -> int: ... +17 | # error: [override-of-final-method] +18 | def f(self, x: int | str) -> int | str: + | ^ Overrides a definition from superclass `Good` +19 | return x + | +info: `Good.f` is decorated with `@final`, forbidding overrides + --> src/main.py:8:5 + | + 6 | @overload + 7 | def f(self, x: int) -> int: ... + 8 | @final + | ------ + 9 | def f(self, x: int | str) -> int | str: + | - `Good.f` defined here +10 | return x + | +help: Remove all overloads and the implementation for `f` +info: rule `override-of-final-method` is enabled by default +10 | return x +11 | +12 | class ChildOfGood(Good): + - @overload + - def f(self, x: str) -> str: ... + - @overload + - def f(self, x: int) -> int: ... +13 + +14 + +15 | # error: [override-of-final-method] + - def f(self, x: int | str) -> int | str: + - return x +16 + +17 | +18 | class Bad: +19 | @overload +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the overload implementation + --> src/main.py:28:9 + | +26 | def f(self, x: int) -> int: ... +27 | # error: [invalid-overload] +28 | def f(self, x: int | str) -> int | str: + | - + | | + | Implementation defined here +29 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the overload implementation + --> src/main.py:37:9 + | +35 | def g(self, x: int) -> int: ... +36 | # error: [invalid-overload] +37 | def g(self, x: int | str) -> int | str: + | - + | | + | Implementation defined here +38 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the overload implementation + --> src/main.py:46:9 + | +44 | def h(self, x: int) -> int: ... +45 | # error: [invalid-overload] +46 | def h(self, x: int | str) -> int | str: + | - + | | + | Implementation defined here +47 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the overload implementation + --> src/main.py:55:9 + | +53 | def i(self, x: int) -> int: ... +54 | # error: [invalid-overload] +55 | def i(self, x: int | str) -> int | str: + | - + | | + | Implementation defined here +56 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[override-of-final-method]: Cannot override `Bad.f` + --> src/main.py:60:5 + | +58 | class ChildOfBad(Bad): +59 | # TODO: these should all cause us to emit Liskov violations as well +60 | f = None # error: [override-of-final-method] + | ^ Overrides a definition from superclass `Bad` +61 | g = None # error: [override-of-final-method] +62 | h = None # error: [override-of-final-method] + | +info: `Bad.f` is decorated with `@final`, forbidding overrides + --> src/main.py:28:9 + | +26 | def f(self, x: int) -> int: ... +27 | # error: [invalid-overload] +28 | def f(self, x: int | str) -> int | str: + | - `Bad.f` defined here +29 | return x + | +help: Remove the override of `f` +info: rule `override-of-final-method` is enabled by default +57 | +58 | class ChildOfBad(Bad): +59 | # TODO: these should all cause us to emit Liskov violations as well + - f = None # error: [override-of-final-method] +60 + # error: [override-of-final-method] +61 | g = None # error: [override-of-final-method] +62 | h = None # error: [override-of-final-method] +63 | i = None # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Bad.g` + --> src/main.py:61:5 + | +59 | # TODO: these should all cause us to emit Liskov violations as well +60 | f = None # error: [override-of-final-method] +61 | g = None # error: [override-of-final-method] + | ^ Overrides a definition from superclass `Bad` +62 | h = None # error: [override-of-final-method] +63 | i = None # error: [override-of-final-method] + | +info: `Bad.g` is decorated with `@final`, forbidding overrides + --> src/main.py:37:9 + | +35 | def g(self, x: int) -> int: ... +36 | # error: [invalid-overload] +37 | def g(self, x: int | str) -> int | str: + | - `Bad.g` defined here +38 | return x + | +help: Remove the override of `g` +info: rule `override-of-final-method` is enabled by default +58 | class ChildOfBad(Bad): +59 | # TODO: these should all cause us to emit Liskov violations as well +60 | f = None # error: [override-of-final-method] + - g = None # error: [override-of-final-method] +61 + # error: [override-of-final-method] +62 | h = None # error: [override-of-final-method] +63 | i = None # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Bad.h` + --> src/main.py:62:5 + | +60 | f = None # error: [override-of-final-method] +61 | g = None # error: [override-of-final-method] +62 | h = None # error: [override-of-final-method] + | ^ Overrides a definition from superclass `Bad` +63 | i = None # error: [override-of-final-method] + | +info: `Bad.h` is decorated with `@final`, forbidding overrides + --> src/main.py:46:9 + | +44 | def h(self, x: int) -> int: ... +45 | # error: [invalid-overload] +46 | def h(self, x: int | str) -> int | str: + | - `Bad.h` defined here +47 | return x + | +help: Remove the override of `h` +info: rule `override-of-final-method` is enabled by default +59 | # TODO: these should all cause us to emit Liskov violations as well +60 | f = None # error: [override-of-final-method] +61 | g = None # error: [override-of-final-method] + - h = None # error: [override-of-final-method] +62 + # error: [override-of-final-method] +63 | i = None # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` + +``` +error[override-of-final-method]: Cannot override `Bad.i` + --> src/main.py:63:5 + | +61 | g = None # error: [override-of-final-method] +62 | h = None # error: [override-of-final-method] +63 | i = None # error: [override-of-final-method] + | ^ Overrides a definition from superclass `Bad` + | +info: `Bad.i` is decorated with `@final`, forbidding overrides + --> src/main.py:55:9 + | +53 | def i(self, x: int) -> int: ... +54 | # error: [invalid-overload] +55 | def i(self, x: int | str) -> int | str: + | - `Bad.i` defined here +56 | return x + | +help: Remove the override of `i` +info: rule `override-of-final-method` is enabled by default +60 | f = None # error: [override-of-final-method] +61 | g = None # error: [override-of-final-method] +62 | h = None # error: [override-of-final-method] + - i = None # error: [override-of-final-method] +63 + # error: [override-of-final-method] +note: This is an unsafe fix and may change runtime behavior + +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 26a7a2c42c..1f613fa568 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -497,6 +497,11 @@ impl<'db> ClassType<'db> { class_literal.name(db) } + pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { + let (class_literal, _) = self.class_literal(db); + class_literal.qualified_name(db) + } + pub(crate) fn known(self, db: &'db dyn Db) -> Option { let (class_literal, _) = self.class_literal(db); class_literal.known(db) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index ae33d378a2..e3bc4d755d 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -17,7 +17,7 @@ use crate::types::call::CallError; use crate::types::class::{ CodeGeneratorKind, DisjointBase, DisjointBaseKind, Field, MethodDecorator, }; -use crate::types::function::{FunctionType, KnownFunction}; +use crate::types::function::{FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral}; use crate::types::liskov::MethodKind; use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION, @@ -101,6 +101,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&POSSIBLY_MISSING_IMPORT); registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE); registry.register_lint(&SUBCLASS_OF_FINAL_CLASS); + registry.register_lint(&OVERRIDE_OF_FINAL_METHOD); registry.register_lint(&TYPE_ASSERTION_FAILURE); registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS); registry.register_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS); @@ -1614,6 +1615,33 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for methods on subclasses that override superclass methods decorated with `@final`. + /// + /// ## Why is this bad? + /// Decorating a method with `@final` declares to the type checker that it should not be + /// overridden on any subclass. + /// + /// ## Example + /// + /// ```python + /// from typing import final + /// + /// class A: + /// @final + /// def foo(self): ... + /// + /// class B(A): + /// def foo(self): ... # Error raised here + /// ``` + pub(crate) static OVERRIDE_OF_FINAL_METHOD = { + summary: "detects subclasses of final classes", + status: LintStatus::stable("0.0.1-alpha.29"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for methods that are decorated with `@override` but do not override any method in a superclass. @@ -3625,7 +3653,7 @@ pub(super) fn report_invalid_method_override<'db>( let overridden_method = if class_name == superclass_name { format!( "{superclass}.{member}", - superclass = superclass.class_literal(db).0.qualified_name(db), + superclass = superclass.qualified_name(db), ) } else { format!("{superclass_name}.{member}") @@ -3768,6 +3796,125 @@ pub(super) fn report_invalid_method_override<'db>( } } +pub(super) fn report_overridden_final_method<'db>( + context: &InferContext<'db, '_>, + member: &str, + subclass_definition: Definition<'db>, + subclass_type: Type<'db>, + superclass: ClassType<'db>, + subclass: ClassType<'db>, + superclass_method_defs: &[FunctionType<'db>], +) { + let db = context.db(); + + let Some(builder) = context.report_lint( + &OVERRIDE_OF_FINAL_METHOD, + subclass_definition.focus_range(db, context.module()), + ) else { + return; + }; + + let superclass_name = if superclass.name(db) == subclass.name(db) { + superclass.qualified_name(db).to_string() + } else { + superclass.name(db).to_string() + }; + + let mut diagnostic = + builder.into_diagnostic(format_args!("Cannot override `{superclass_name}.{member}`")); + diagnostic.set_primary_message(format_args!( + "Overrides a definition from superclass `{superclass_name}`" + )); + diagnostic.set_concise_message(format_args!( + "Cannot override final member `{member}` from superclass `{superclass_name}`" + )); + + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "`{superclass_name}.{member}` is decorated with `@final`, forbidding overrides" + ), + ); + + let first_final_superclass_definition = superclass_method_defs + .iter() + .find(|function| function.has_known_decorator(db, FunctionDecorators::FINAL)) + .expect( + "At least one function definition in the superclass should be decorated with `@final`", + ); + + let superclass_function_literal = if first_final_superclass_definition.file(db).is_stub(db) { + first_final_superclass_definition.first_overload_or_implementation(db) + } else { + first_final_superclass_definition + .literal(db) + .last_definition(db) + }; + + sub.annotate( + Annotation::secondary(Span::from(superclass_function_literal.focus_range( + db, + &parsed_module(db, first_final_superclass_definition.file(db)).load(db), + ))) + .message(format_args!("`{superclass_name}.{member}` defined here")), + ); + + if let Some(decorator_span) = + superclass_function_literal.find_known_decorator_span(db, KnownFunction::Final) + { + sub.annotate(Annotation::secondary(decorator_span)); + } + + diagnostic.sub(sub); + + let underlying_function = match subclass_type { + Type::FunctionLiteral(function) => Some(function), + Type::BoundMethod(method) => Some(method.function(db)), + _ => None, + }; + + if let Some(function) = underlying_function { + let overload_deletion = |overload: &OverloadLiteral<'db>| { + Edit::range_deletion(overload.node(db, context.file(), context.module()).range()) + }; + + match function.overloads_and_implementation(db) { + ([first_overload, rest @ ..], None) => { + diagnostic.help(format_args!("Remove all overloads for `{member}`")); + diagnostic.set_fix(Fix::unsafe_edits( + overload_deletion(first_overload), + rest.iter().map(overload_deletion), + )); + } + ([first_overload, rest @ ..], Some(implementation)) => { + diagnostic.help(format_args!( + "Remove all overloads and the implementation for `{member}`" + )); + diagnostic.set_fix(Fix::unsafe_edits( + overload_deletion(first_overload), + rest.iter().chain([&implementation]).map(overload_deletion), + )); + } + ([], Some(implementation)) => { + diagnostic.help(format_args!("Remove the override of `{member}`")); + diagnostic.set_fix(Fix::unsafe_edit(overload_deletion(&implementation))); + } + ([], None) => { + // Should be impossible to get here: how would we even infer a function as a function + // if it has 0 overloads and no implementation? + unreachable!( + "A function should always have an implementation and/or >=1 overloads" + ); + } + } + } else { + diagnostic.help(format_args!("Remove the override of `{member}`")); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion( + subclass_definition.full_range(db, context.module()).range(), + ))); + } +} + /// 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/function.rs b/crates/ty_python_semantic/src/types/function.rs index 262868b7b5..7c07a86af1 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -83,7 +83,7 @@ use crate::types::{ ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, - UnionBuilder, binding_type, walk_signature, + UnionBuilder, binding_type, definition_expression_type, walk_signature, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -278,7 +278,7 @@ impl<'db> OverloadLiteral<'db> { || is_implicit_classmethod(self.name(db)) } - pub(super) fn node<'ast>( + pub(crate) fn node<'ast>( self, db: &dyn Db, file: File, @@ -294,6 +294,41 @@ impl<'db> OverloadLiteral<'db> { self.body_scope(db).node(db).expect_function().node(module) } + /// Iterate through the decorators on this function, returning the span of the first one + /// that matches the given predicate. + pub(super) fn find_decorator_span( + self, + db: &'db dyn Db, + predicate: impl Fn(Type<'db>) -> bool, + ) -> Option { + let definition = self.definition(db); + let file = definition.file(db); + self.node(db, file, &parsed_module(db, file).load(db)) + .decorator_list + .iter() + .find(|decorator| { + predicate(definition_expression_type( + db, + definition, + &decorator.expression, + )) + }) + .map(|decorator| Span::from(file).with_range(decorator.range)) + } + + /// Iterate through the decorators on this function, returning the span of the first one + /// that matches the given [`KnownFunction`]. + pub(super) fn find_known_decorator_span( + self, + db: &'db dyn Db, + needle: KnownFunction, + ) -> Option { + self.find_decorator_span(db, |ty| { + ty.as_function_literal() + .is_some_and(|f| f.is_known(db, needle)) + }) + } + /// Returns the [`FileRange`] of the function's name. pub(crate) fn focus_range(self, db: &dyn Db, module: &ParsedModuleRef) -> FileRange { FileRange::new( @@ -584,32 +619,44 @@ impl<'db> FunctionLiteral<'db> { self.last_definition(db).spans(db) } - #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size, cycle_initial=overloads_and_implementation_cycle_initial)] fn overloads_and_implementation( self, db: &'db dyn Db, - ) -> (Box<[OverloadLiteral<'db>]>, Option>) { - let self_overload = self.last_definition(db); - let mut current = self_overload; - let mut overloads = vec![]; + ) -> (&'db [OverloadLiteral<'db>], Option>) { + #[salsa::tracked( + returns(ref), + heap_size=ruff_memory_usage::heap_size, + cycle_initial=overloads_and_implementation_cycle_initial + )] + fn overloads_and_implementation_inner<'db>( + db: &'db dyn Db, + function: FunctionLiteral<'db>, + ) -> (Box<[OverloadLiteral<'db>]>, Option>) { + let self_overload = function.last_definition(db); + let mut current = self_overload; + let mut overloads = vec![]; - while let Some(previous) = current.previous_overload(db) { - let overload = previous.last_definition(db); - overloads.push(overload); - current = overload; + while let Some(previous) = current.previous_overload(db) { + let overload = previous.last_definition(db); + overloads.push(overload); + current = overload; + } + + // Overloads are inserted in reverse order, from bottom to top. + overloads.reverse(); + + let implementation = if self_overload.is_overload(db) { + overloads.push(self_overload); + None + } else { + Some(self_overload) + }; + + (overloads.into_boxed_slice(), implementation) } - // Overloads are inserted in reverse order, from bottom to top. - overloads.reverse(); - - let implementation = if self_overload.is_overload(db) { - overloads.push(self_overload); - None - } else { - Some(self_overload) - }; - - (overloads.into_boxed_slice(), implementation) + let (overloads, implementation) = overloads_and_implementation_inner(db, self); + (overloads.as_ref(), *implementation) } fn iter_overloads_and_implementation( @@ -617,7 +664,7 @@ impl<'db> FunctionLiteral<'db> { db: &'db dyn Db, ) -> impl Iterator> + 'db { let (implementation, overloads) = self.overloads_and_implementation(db); - overloads.iter().chain(implementation).copied() + overloads.into_iter().chain(implementation.iter().copied()) } /// Typed externally-visible signature for this function. @@ -773,7 +820,7 @@ impl<'db> FunctionType<'db> { } /// Returns the AST node for this function. - pub(crate) fn node<'ast>( + pub(super) fn node<'ast>( self, db: &dyn Db, file: File, @@ -892,7 +939,7 @@ impl<'db> FunctionType<'db> { pub(crate) fn overloads_and_implementation( self, db: &'db dyn Db, - ) -> &'db (Box<[OverloadLiteral<'db>]>, Option>) { + ) -> (&'db [OverloadLiteral<'db>], Option>) { self.literal(db).overloads_and_implementation(db) } @@ -905,6 +952,12 @@ impl<'db> FunctionType<'db> { self.literal(db).iter_overloads_and_implementation(db) } + pub(crate) fn first_overload_or_implementation(self, db: &'db dyn Db) -> OverloadLiteral<'db> { + self.iter_overloads_and_implementation(db) + .next() + .expect("A function must have at least one overload/implementation") + } + /// Typed externally-visible signature for this function. /// /// This is the signature as seen by external callers, possibly modified by decorators and/or diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index af98132015..71d4dfec17 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1044,7 +1044,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Check that the overloaded function has at least two overloads - if let [single_overload] = overloads.as_ref() { + if let [single_overload] = overloads { let function_node = function.node(self.db(), self.file(), self.module()); if let Some(builder) = self .context @@ -1164,7 +1164,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (FunctionDecorators::OVERRIDE, "override"), ] { if let Some(implementation) = implementation { - for overload in overloads.as_ref() { + for overload in overloads { if !overload.has_known_decorator(self.db(), decorator) { continue; } diff --git a/crates/ty_python_semantic/src/types/liskov.rs b/crates/ty_python_semantic/src/types/liskov.rs index 75b9f11a01..79b79889fc 100644 --- a/crates/ty_python_semantic/src/types/liskov.rs +++ b/crates/ty_python_semantic/src/types/liskov.rs @@ -2,26 +2,32 @@ //! //! [Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle +use bitflags::bitflags; use ruff_db::diagnostic::Annotation; use rustc_hash::FxHashSet; use crate::{ + Db, + lint::LintId, place::Place, - semantic_index::place_table, + semantic_index::{place_table, scope::ScopeId, symbol::ScopedSymbolId, use_def_map}, types::{ ClassBase, ClassLiteral, ClassType, KnownClass, Type, class::CodeGeneratorKind, context::InferContext, - definition_expression_type, - diagnostic::{INVALID_EXPLICIT_OVERRIDE, report_invalid_method_override}, - function::{FunctionDecorators, KnownFunction}, + diagnostic::{ + INVALID_EXPLICIT_OVERRIDE, INVALID_METHOD_OVERRIDE, OVERRIDE_OF_FINAL_METHOD, + report_invalid_method_override, report_overridden_final_method, + }, + function::{FunctionDecorators, FunctionType, KnownFunction}, ide_support::{MemberWithDefinition, all_declarations_and_bindings}, }, }; pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLiteral<'db>) { let db = context.db(); - if class.is_known(db, KnownClass::Object) { + let configuration = OverrideRulesConfig::from(context); + if configuration.no_rules_enabled() { return; } @@ -30,56 +36,100 @@ pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLite all_declarations_and_bindings(db, class.body_scope(db)).collect(); for member in own_class_members { - check_class_declaration(context, class_specialized, &member); + check_class_declaration(context, configuration, class_specialized, &member); } } fn check_class_declaration<'db>( context: &InferContext<'db, '_>, + configuration: OverrideRulesConfig, class: ClassType<'db>, member: &MemberWithDefinition<'db>, ) { + /// Salsa-tracked query to check whether any of the definitions of a symbol + /// in a superclass scope are function definitions. + /// + /// We need to know this for compatibility with pyright and mypy, neither of which emit an error + /// on `C.f` here: + /// + /// ```python + /// from typing import final + /// + /// class A: + /// @final + /// def f(self) -> None: ... + /// + /// class B: + /// f = A.f + /// + /// class C(B): + /// def f(self) -> None: ... # no error here + /// ``` + /// + /// This is a Salsa-tracked query because it has to look at the AST node for the definition, + /// which might be in a different Python module. If this weren't a tracked query, we could + /// introduce cross-module dependencies and over-invalidation. + #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] + fn is_function_definition<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + symbol: ScopedSymbolId, + ) -> bool { + use_def_map(db, scope) + .end_of_scope_symbol_bindings(symbol) + .filter_map(|binding| binding.binding.definition()) + .any(|definition| definition.kind(db).is_function_def()) + } + + fn extract_underlying_functions<'db>( + db: &'db dyn Db, + ty: Type<'db>, + ) -> Option; 1]>> { + match ty { + Type::FunctionLiteral(function) => Some(smallvec::smallvec_inline![function]), + Type::BoundMethod(method) => Some(smallvec::smallvec_inline![method.function(db)]), + Type::PropertyInstance(property) => { + extract_underlying_functions(db, property.getter(db)?) + } + Type::Union(union) => { + let mut functions = smallvec::smallvec![]; + for member in union.elements(db) { + if let Some(mut member_functions) = extract_underlying_functions(db, *member) { + functions.append(&mut member_functions); + } + } + if functions.is_empty() { + None + } else { + Some(functions) + } + } + _ => None, + } + } + let db = context.db(); let MemberWithDefinition { member, definition } = member; - // TODO: Check Liskov on non-methods too - let Type::FunctionLiteral(function) = member.ty else { - return; - }; - let Some(definition) = definition else { return; }; - // Constructor methods are not checked for Liskov compliance - if matches!( - &*member.name, - "__init__" | "__new__" | "__post_init__" | "__init_subclass__" - ) { - return; - } - - let (literal, specialization) = class.class_literal(db); - let class_kind = CodeGeneratorKind::from_class(db, literal, specialization); - - // Synthesized `__replace__` methods on dataclasses are not checked - if &member.name == "__replace__" - && matches!(class_kind, Some(CodeGeneratorKind::DataclassLike(_))) - { - return; - } - let Place::Defined(type_on_subclass_instance, _, _) = Type::instance(db, class).member(db, &member.name).place else { return; }; + let (literal, specialization) = class.class_literal(db); + let class_kind = CodeGeneratorKind::from_class(db, literal, specialization); + let mut subclass_overrides_superclass_declaration = false; let mut has_dynamic_superclass = false; let mut has_typeddict_in_mro = false; let mut liskov_diagnostic_emitted = false; + let mut overridden_final_method = None; for class_base in class.iter_mro(db).skip(1) { let superclass = match class_base { @@ -96,11 +146,15 @@ fn check_class_declaration<'db>( }; let (superclass_literal, superclass_specialization) = superclass.class_literal(db); - let superclass_symbol_table = place_table(db, superclass_literal.body_scope(db)); + let superclass_scope = superclass_literal.body_scope(db); + let superclass_symbol_table = place_table(db, superclass_scope); + let superclass_symbol_id = superclass_symbol_table.symbol_id(&member.name); + let mut method_kind = MethodKind::default(); // If the member is not defined on the class itself, skip it - if let Some(superclass_symbol) = superclass_symbol_table.symbol_by_name(&member.name) { + if let Some(id) = superclass_symbol_id { + let superclass_symbol = superclass_symbol_table.symbol(id); if !(superclass_symbol.is_bound() || superclass_symbol.is_declared()) { continue; } @@ -119,12 +173,6 @@ fn check_class_declaration<'db>( subclass_overrides_superclass_declaration = true; - // Only one Liskov diagnostic should be emitted per each invalid override, - // even if it overrides multiple superclasses incorrectly! - if liskov_diagnostic_emitted { - continue; - } - let Place::Defined(superclass_type, _, _) = Type::instance(db, superclass) .member(db, &member.name) .place @@ -133,14 +181,74 @@ fn check_class_declaration<'db>( break; }; - let Some(superclass_type_as_callable) = superclass_type - .try_upcast_to_callable(db) - .map(|callables| callables.into_type(db)) - else { + if configuration.check_final_method_overridden() { + overridden_final_method = overridden_final_method.or_else(|| { + let superclass_symbol_id = superclass_symbol_id?; + + // TODO: `@final` should be more like a type qualifier: + // we should also recognise `@final`-decorated methods that don't end up + // as being function- or property-types (because they're wrapped by other + // decorators that transform the type into something else). + let underlying_functions = extract_underlying_functions( + db, + superclass + .own_class_member(db, None, &member.name) + .ignore_possibly_undefined()?, + )?; + + if underlying_functions + .iter() + .any(|function| function.has_known_decorator(db, FunctionDecorators::FINAL)) + && is_function_definition(db, superclass_scope, superclass_symbol_id) + { + Some((superclass, underlying_functions)) + } else { + None + } + }); + } + + // ********************************************************** + // Everything below this point in the loop + // is about Liskov Substitution Principle checks + // ********************************************************** + + // Only one Liskov diagnostic should be emitted per each invalid override, + // even if it overrides multiple superclasses incorrectly! + if liskov_diagnostic_emitted { + continue; + } + + if !configuration.check_method_liskov_violations() { + continue; + } + + // TODO: Check Liskov on non-methods too + let Type::FunctionLiteral(subclass_function) = member.ty else { continue; }; - if type_on_subclass_instance.is_assignable_to(db, superclass_type_as_callable) { + // Constructor methods are not checked for Liskov compliance + if matches!( + &*member.name, + "__init__" | "__new__" | "__post_init__" | "__init_subclass__" + ) { + continue; + } + + // Synthesized `__replace__` methods on dataclasses are not checked + if &member.name == "__replace__" + && matches!(class_kind, Some(CodeGeneratorKind::DataclassLike(_))) + { + continue; + } + + let Some(superclass_type_as_callable) = superclass_type.try_upcast_to_callable(db) else { + continue; + }; + + if type_on_subclass_instance.is_assignable_to(db, superclass_type_as_callable.into_type(db)) + { continue; } @@ -149,7 +257,7 @@ fn check_class_declaration<'db>( &member.name, class, *definition, - function, + subclass_function, superclass, superclass_type, method_kind, @@ -187,13 +295,11 @@ fn check_class_declaration<'db>( && function.has_known_decorator(db, FunctionDecorators::OVERRIDE) { let function_literal = if context.in_stub() { - function - .iter_overloads_and_implementation(db) - .next() - .expect("There should always be at least one overload or implementation") + function.first_overload_or_implementation(db) } else { function.literal(db).last_definition(db) }; + if let Some(builder) = context.report_lint( &INVALID_EXPLICIT_OVERRIDE, function_literal.focus_range(db, context.module()), @@ -202,17 +308,10 @@ fn check_class_declaration<'db>( "Method `{}` is decorated with `@override` but does not override anything", member.name )); - if let Some(decorator) = function_literal - .node(db, context.file(), context.module()) - .decorator_list - .iter() - .find(|decorator| { - definition_expression_type(db, *definition, &decorator.expression) - .as_function_literal() - .is_some_and(|function| function.is_known(db, KnownFunction::Override)) - }) + if let Some(decorator_span) = + function_literal.find_known_decorator_span(db, KnownFunction::Override) { - diagnostic.annotate(Annotation::secondary(context.span(decorator))); + diagnostic.annotate(Annotation::secondary(decorator_span)); } diagnostic.info(format_args!( "No `{member}` definitions were found on any superclasses of `{class}`", @@ -221,6 +320,18 @@ fn check_class_declaration<'db>( )); } } + + if let Some((superclass, superclass_method)) = overridden_final_method { + report_overridden_final_method( + context, + &member.name, + *definition, + type_on_subclass_instance, + superclass, + class, + &superclass_method, + ); + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -229,3 +340,48 @@ pub(super) enum MethodKind<'db> { #[default] NotSynthesized, } + +bitflags! { + /// Bitflags representing which override-related rules have been enabled. + #[derive(Default, Debug, Copy, Clone)] + struct OverrideRulesConfig: u8 { + const LISKOV_METHODS = 1 << 0; + const EXPLICIT_OVERRIDE = 1 << 1; + const FINAL_METHOD_OVERRIDDEN = 1 << 2; + } +} + +impl From<&InferContext<'_, '_>> for OverrideRulesConfig { + fn from(value: &InferContext<'_, '_>) -> Self { + let db = value.db(); + let rule_selection = db.rule_selection(value.file()); + + let mut config = OverrideRulesConfig::empty(); + + if rule_selection.is_enabled(LintId::of(&INVALID_METHOD_OVERRIDE)) { + config |= OverrideRulesConfig::LISKOV_METHODS; + } + if rule_selection.is_enabled(LintId::of(&INVALID_EXPLICIT_OVERRIDE)) { + config |= OverrideRulesConfig::EXPLICIT_OVERRIDE; + } + if rule_selection.is_enabled(LintId::of(&OVERRIDE_OF_FINAL_METHOD)) { + config |= OverrideRulesConfig::FINAL_METHOD_OVERRIDDEN; + } + + config + } +} + +impl OverrideRulesConfig { + const fn no_rules_enabled(self) -> bool { + self.is_empty() + } + + const fn check_method_liskov_violations(self) -> bool { + self.contains(OverrideRulesConfig::LISKOV_METHODS) + } + + const fn check_final_method_overridden(self) -> bool { + self.contains(OverrideRulesConfig::FINAL_METHOD_OVERRIDDEN) + } +} diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 0676f15fd7..5505fbd5f2 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -83,6 +83,7 @@ Settings: Settings { "no-matching-overload": Error (Default), "non-subscriptable": Error (Default), "not-iterable": Error (Default), + "override-of-final-method": Error (Default), "parameter-already-assigned": Error (Default), "positional-only-parameter-as-kwarg": Error (Default), "possibly-missing-attribute": Warning (Default), diff --git a/ty.schema.json b/ty.schema.json index 919b0e8dfc..8a19ce44cf 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -863,6 +863,16 @@ } ] }, + "override-of-final-method": { + "title": "detects subclasses of final classes", + "description": "## What it does\nChecks for methods on subclasses that override superclass methods decorated with `@final`.\n\n## Why is this bad?\nDecorating a method with `@final` declares to the type checker that it should not be\noverridden on any subclass.\n\n## Example\n\n```python\nfrom typing import final\n\nclass A:\n @final\n def foo(self): ...\n\nclass B(A):\n def foo(self): ... # Error raised here\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "parameter-already-assigned": { "title": "detects multiple arguments for the same parameter", "description": "## What it does\nChecks for calls which provide more than one argument for a single parameter.\n\n## Why is this bad?\nProviding multiple values for a single parameter will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\ndef f(x: int) -> int: ...\n\nf(1, x=2) # Error raised here\n```",