[ty] Implement typing.final for methods (#21646)
Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
@@ -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`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```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
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`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.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`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
|
||||
<https://discuss.python.org/t/imported-final-variable/82429>, 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.)
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```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
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```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]
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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<KnownClass> {
|
||||
let (class_literal, _) = self.class_literal(db);
|
||||
class_literal.known(db)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Span> {
|
||||
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<Span> {
|
||||
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<OverloadLiteral<'db>>) {
|
||||
let self_overload = self.last_definition(db);
|
||||
let mut current = self_overload;
|
||||
let mut overloads = vec![];
|
||||
) -> (&'db [OverloadLiteral<'db>], Option<OverloadLiteral<'db>>) {
|
||||
#[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<OverloadLiteral<'db>>) {
|
||||
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<Item = OverloadLiteral<'db>> + '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<OverloadLiteral<'db>>) {
|
||||
) -> (&'db [OverloadLiteral<'db>], Option<OverloadLiteral<'db>>) {
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<smallvec::SmallVec<[FunctionType<'db>; 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user