`explicit-any` is reported for `@override` methods that must match a base-class signature annotated with `Any`
- Dominant language
- Rust
- Stars
- 7k
- Forks
- 516
- PR merge metrics
- No merged PRs in 30d
Description
# Describe the Bug
When a method is decorated with `@typing.override`, its signature is dictated by the base class. If the base class annotates a parameter or the return as `Any`, the override has to repeat that `Any` — narrowing it would change the signature, and removing it is not possible. `explicit-any` currently fires on those annotations anyway, so the diagnostic points at code that cannot be changed.
## Repro
`repro.py`:
```python
import json
from typing import Any
from typing import override
class CustomEncoder(json.JSONEncoder):
@override
def default(self, o: Any) -> Any:
return super().default(o)
```
`pyrefly.toml`:
```toml
[errors]
explicit-any = "error"
```
Actual (pyrefly 1.2.0):
```
ERROR Explicit `Any` is not allowed [explicit-any]
--> repro.py:8:26
ERROR Explicit `Any` is not allowed [explicit-any]
--> repro.py:8:34
INFO 2 errors
```
Expected: no errors. Both annotations are fixed by typeshed's `json.JSONEncoder.default`, which is declared as `def default(self, o: Any) -> Any`.
## Why this seems worth special-casing
Ruff reached the same conclusion for its equivalent lint, ANN401. It exempts `@override` methods, implemented in [astral-sh/ruff#4409](https://github.com/astral-sh/ruff/pull/4409) closing [astral-sh/ruff#4408](https://github.com/astral-sh/ruff/issues/4408). The reasoning given there:
> Where that discussion landed was that it is reasonable to suppress `ARG002` for overridden methods, since you cannot control the method signature. I think it follows that `ANN401` should be suppressed as well, since if we cannot control the method signature we also cannot control the types, and it is impossible to narrow or remove `Any` in such cases.
That earlier `ARG002` discussion is [astral-sh/ruff#3952](https://github.com/astral-sh/ruff/issues/3952).
The practical effect is that in a codebase running `preset = "all"`, `explicit-any` is actionable for first-party annotations but produces unsuppressable noise wherever the codebase subclasses a framework. In ours, every `explicit-any` site in `src/` that we could not remove was an `@override` of a Starlette/syrupy method.
This is a different mechanism from [#4088](https://github.com/facebook/pyrefly/issues/4088) (third-party type aliases that expand to `Any`), but the same underlying theme: `explicit-any` firing on `Any` that the first-party code did not choose.
## Workaround
Per-line suppression on each affected parameter and return:
```python
@override
def default(self, o: Any) -> Any: # pyrefly: ignore[explicit-any]
...
```
One suppression covers every annotation on its line, so the cost is bounded, but it has to be repeated at every override of every framework method. For `*args: Any, **kwargs: Any` overrides it is worse than it looks: a formatter (we use `ruff format`) splits the parameters onto separate lines once the suppression comment makes the line long enough, which strands the annotations that no longer share a line with the comment and forces one suppression per parameter.
Contributor guide
Assessment
This issue has not been assessed yet.