mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 13:34:40 +00:00
[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:
parent
3ad123bc23
commit
53cfaaebc4
5 changed files with 68 additions and 3 deletions
|
@ -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())
|
||||
```
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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```",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue