diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md index 81712cff8b..96a01d90d9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md @@ -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] +``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index b0766921fe..ecb17aef0f 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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), ), ); }