Emit diagnostic on unsound `super()` call to abstract method with trivial body
Mypy, pyright and pyrefly all detect the unsoundness here. We should too:
from abc import abstractmethod
class F:
@abstractmethod
def method(self) -> int: ...
class G(F):
def method(self) -> int:
# mypy: error: Call to abstract method "method" of "F" with trivial body via super() is unsafe [safe-super]
return super().method()
This is similar to, but distinct from, https://github.com/astral-sh/ty/issues/1877.
We should ensure that abstract properties are also covered, e.g.
from abc import abstractmethod
class F:
@property
@abstractmethod
def prop(self) -> int: ...
class G(F):
@property
def prop(self) -> int:
# mypy: error: Call to abstract method "method" of "F" with trivial body via super() is unsafe [safe-super]
return super().prop
Note that the "with trivial body" part of this is important. For example, this is fine, because the abstract method has a default implementation:
from abc import abstractmethod
class F:
@abstractmethod
def method(self) -> int:
return 42
class G(F):
def method(self) -> int:
return super().method()
As a result of the above being fine, however, this means that we must also reject super() calls to abstract methods even if the super() call occurs in the body of an overriding method that is also abstract. To see why, consider this example below:
from abc import abstractmethod
class F:
@abstractmethod
def method(self) -> int: ...
class G(F):
@abstractmethod
def method(self) -> int:
return super().method()
class H(G):
def method(self) -> int:
return super().method()
H.method() says it will return int, but it actually returns None. But H.method doesn't break the rule outlined above: it calls super() on G.method, which is an abstract method that has a default implementation. Therefore the only way to prevent this unsoundness is to forbid the super() call in G.method, despite the fact that G.method is also an abstract method.
Mypy and pyrefly also emit a diagnostic on G.method in this snippet, where F.method is not explicitly abstract, but is a protocol method with a trivial body. The same soundness issues occur with this, so it makes sense that we should also emit a diagnostic here:
from typing import Protocol
class F(Protocol):
def method(self) -> int: ...
class G(F):
def method(self) -> int:
return super().method()
Pyright does not emit a diagnostic on this variation.