[red-knot] Fix more [redundant-cast] false positives (#17170)

Fixes #17164. Simply checking whether one type is gradually equivalent
to another is too simplistic here: `Any` is gradually equivalent to
`Todo`, but we should permit users to cast from `Todo` or `Unknown` to
`Any` without complaining about it. This changes our logic so that we
only complain about redundant casts if:
- the two types are exactly equal (when normalized) OR they are
equivalent (we'll still complain about `Any -> Any` casts, and about
`Any | str | int` -> `str | int | Any` casts, since their normalized
forms are exactly equal, even though the type is not fully static -- and
therefore does not participate in equivalence relations)
- AND the casted type does not contain `Todo`
This commit is contained in:
Alex Waygood 2025-04-03 15:00:00 +01:00 committed by GitHub
parent 3f00010a7a
commit ca0cce3f9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 27 additions and 4 deletions

View file

@ -49,3 +49,22 @@ def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]):
a = cast(Callable[[list[bytes]], None], x)
b = cast(tuple[list[bytes]], y)
```
A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are
understood as gradually equivalent types by red-knot, they are understood as different types by
human readers of red-knot's output. For `Unknown` in particular, we may consider it differently in
the context of some opt-in diagnostics, as it indicates that the gradual type has come about due to
an invalid annotation, missing annotation or missing type argument somewhere.
```py
from knot_extensions import Unknown
def f(x: Any, y: Unknown, z: Any | str | int):
a = cast(dict[str, Any], x)
reveal_type(a) # revealed: @Todo(generics)
b = cast(Any, y)
reveal_type(b) # revealed: Any
c = cast(str | int | Any, z) # error: [redundant-cast]
```

View file

@ -4049,16 +4049,20 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
KnownFunction::Cast => {
if let [Some(casted_ty), Some(source_ty)] = overload.parameter_types() {
if source_ty.is_gradual_equivalent_to(self.context.db(), *casted_ty)
&& !source_ty.contains_todo(self.context.db())
if let [Some(casted_type), Some(source_type)] =
overload.parameter_types()
{
let db = self.db();
if (source_type.is_equivalent_to(db, *casted_type)
|| source_type.normalized(db) == casted_type.normalized(db))
&& !source_type.contains_todo(db)
{
self.context.report_lint(
&REDUNDANT_CAST,
call_expression,
format_args!(
"Value is already of type `{}`",
casted_ty.display(self.context.db()),
casted_type.display(db),
),
);
}