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```",