[red-knot] Add redundant-cast error (#17100)

## Summary

Following up from earlier discussion on Discord, this PR adds logic to
flag casts as redundant when the inferred type of the expression is the
same as the target type. It should follow the semantics from
[mypy](https://github.com/python/mypy/pull/1705).

Example:

```python
def f() -> int:
    return 10

# error: [redundant-cast] "Value is already of type `int`"
cast(int, f())
```
This commit is contained in:
Trevor Manz 2025-03-31 17:37:25 -07:00 committed by GitHub
parent 3ad123bc23
commit 53cfaaebc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 68 additions and 3 deletions

View file

@ -5,7 +5,7 @@
The (inferred) type of the value and the given type do not need to have any correlation.
```py
from typing import Literal, cast
from typing import Literal, cast, Any
reveal_type(True) # revealed: Literal[True]
reveal_type(cast(str, True)) # revealed: str
@ -25,4 +25,16 @@ reveal_type(cast(1, True)) # revealed: Unknown
cast(str)
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
cast(str, b"ar", "foo")
def function_returning_int() -> int:
return 10
# error: [redundant-cast] "Value is already of type `int`"
cast(int, function_returning_int())
def function_returning_any() -> Any:
return "blah"
# error: [redundant-cast] "Value is already of type `Any`"
cast(Any, function_returning_any())
```

View file

@ -320,6 +320,11 @@ impl<'db> Type<'db> {
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
}
pub fn contains_todo(&self, db: &'db dyn Db) -> bool {
self.is_todo()
|| matches!(self, Type::Union(union) if union.elements(db).iter().any(Type::is_todo))
}
pub const fn class_literal(class: Class<'db>) -> Self {
Self::ClassLiteral(ClassLiteralType { class })
}

View file

@ -67,6 +67,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&STATIC_ASSERT_ERROR);
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
registry.register_lint(&REDUNDANT_CAST);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@ -878,6 +879,27 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects redundant `cast` calls where the value already has the target type.
///
/// ## Why is this bad?
/// These casts have no effect and can be removed.
///
/// ## Example
/// ```python
/// def f() -> int:
/// return 10
///
/// cast(int, f()) # Redundant
/// ```
pub(crate) static REDUNDANT_CAST = {
summary: "detects redundant `cast` calls",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
pub(crate) id: DiagnosticId,

View file

@ -93,8 +93,8 @@ use super::diagnostic::{
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_type_checking_constant,
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
report_unresolved_reference, INVALID_METACLASS, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS,
TYPE_ASSERTION_FAILURE,
report_unresolved_reference, INVALID_METACLASS, REDUNDANT_CAST, STATIC_ASSERT_ERROR,
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
};
use super::slots::check_class_slots;
use super::string_annotation::{
@ -4017,6 +4017,22 @@ 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())
{
self.context.report_lint(
&REDUNDANT_CAST,
call_expression,
format_args!(
"Value is already of type `{}`",
casted_ty.display(self.context.db()),
),
);
}
}
}
_ => {}
}
}

View file

@ -610,6 +610,16 @@
}
]
},
"redundant-cast": {
"title": "detects redundant `cast` calls",
"description": "## What it does\nDetects redundant `cast` calls where the value already has the target type.\n\n## Why is this bad?\nThese casts have no effect and can be removed.\n\n## Example\n```python\ndef f() -> int:\n return 10\n\ncast(int, f()) # Redundant\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"static-assert-error": {
"title": "Failed static assertion",
"description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Examples\n```python\nfrom knot_extensions import static_assert\n\nstatic_assert(1 + 1 == 3) # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness\n```",