From 12d143422fdea791809f73e9ea8c394ee69a2586 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Mon, 2 Dec 2024 15:28:22 +0000 Subject: [PATCH 01/10] [`ruff`] Unnecessary cast to `int` (`RUF046`) --- .../resources/test/fixtures/ruff/RUF046.py | 50 ++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/ruff/mod.rs | 1 + .../ruff_linter/src/rules/ruff/rules/mod.rs | 2 + .../ruff/rules/unnecessary_cast_to_int.rs | 143 ++++++ ...uff__tests__preview__RUF046_RUF046.py.snap | 470 ++++++++++++++++++ crates/ruff_python_ast/src/int.rs | 2 +- ruff.schema.json | 1 + 9 files changed, 672 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py new file mode 100644 index 0000000000000..e520158e43fe7 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py @@ -0,0 +1,50 @@ +import math + + +### Safely fixable + +# Arguments are not checked +int(id()) +int(len([])) +int(ord(foo)) +int(hash(foo, bar)) +int(int('')) + +int(math.comb()) +int(math.factorial()) +int(math.gcd()) +int(math.lcm()) +int(math.isqrt()) +int(math.perm()) + + +### Unsafe + +int(math.ceil()) +int(math.floor()) +int(math.trunc()) + + +### `round()` + +## Errors +int(round(0)) +int(round(0, 0)) +int(round(0, None)) + +int(round(0.1)) +int(round(0.1, 0)) +int(round(0.1, None)) + +# Argument type is not checked +foo = type("Foo", (), {"__round__": lambda self: 4.2})() + +int(round(foo)) +int(round(foo, 0)) +int(round(foo, None)) + +## No errors +int(round(0, 3.14)) +int(round(0, non_literal)) +int(round(0, 0), base) +int(round(0, 0, extra=keyword)) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index ae3dfdc31b859..d582aaef64746 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1093,6 +1093,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::Airflow3Removal) { airflow::rules::removed_in_3(checker, expr); } + if checker.enabled(Rule::UnnecessaryCastToInt) { + ruff::rules::unnecessary_cast_to_int(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index d35bf82bbde2e..5b89fe84fdbda 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -983,6 +983,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern), (Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument), (Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral), + (Ruff, "046") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryCastToInt), (Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing), (Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable), (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 14d6f3e121a35..69a3ea3053b80 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -413,6 +413,7 @@ mod tests { #[test_case(Rule::UnrawRePattern, Path::new("RUF039_concat.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_0.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))] + #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 69f92b8da2bab..ac0a17445cc49 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -30,6 +30,7 @@ pub(crate) use sort_dunder_slots::*; pub(crate) use static_key_dict_comprehension::*; #[cfg(any(feature = "test-rules", test))] pub(crate) use test_rules::*; +pub(crate) use unnecessary_cast_to_int::*; pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_key_check::*; pub(crate) use unnecessary_nested_literal::*; @@ -78,6 +79,7 @@ mod static_key_dict_comprehension; mod suppression_comment_visitor; #[cfg(any(feature = "test-rules", test))] pub(crate) mod test_rules; +mod unnecessary_cast_to_int; mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_key_check; mod unnecessary_nested_literal; diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs new file mode 100644 index 0000000000000..ae2a3016e3ac8 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -0,0 +1,143 @@ +use crate::checkers::ast::Checker; +use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::{Arguments, Expr, ExprCall, ExprNumberLiteral, Number}; +use ruff_python_semantic::SemanticModel; +use ruff_text_size::TextRange; + +/// ## What it does +/// Checks for `int` conversions of values that are already integers. +/// +/// ## Why is this bad? +/// Such a conversion is unnecessary. +/// +/// ## Known problems +/// This rule is prone to false positives due to type inference limitations. +/// +/// ## Example +/// +/// ```python +/// int(len([])) +/// int(round(foo, 0)) +/// ``` +/// +/// Use instead: +/// +/// ```python +/// len([]) +/// round(foo) +/// ``` +#[derive(ViolationMetadata)] +pub(crate) struct UnnecessaryCastToInt; + +impl AlwaysFixableViolation for UnnecessaryCastToInt { + #[derive_message_formats] + fn message(&self) -> String { + "Value being casted is already an integer".to_string() + } + + fn fix_title(&self) -> String { + "Remove `int()` wrapper call".to_string() + } +} + +/// RUF046 +pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { + let semantic = checker.semantic(); + + let Some(Expr::Call(inner_call)) = single_argument_to_int_call(semantic, call) else { + return; + }; + + let (func, arguments) = (&inner_call.func, &inner_call.arguments); + let (outer_range, inner_range) = (call.range, inner_call.range); + + let Some(qualified_name) = checker.semantic().resolve_qualified_name(func) else { + return; + }; + + let (edit, applicability) = match qualified_name.segments() { + // Always returns a strict instance of `int` + ["" | "builtins", "len" | "id" | "hash" | "ord" | "int"] + | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] => ( + handle_other(checker, outer_range, inner_range), + Applicability::Safe, + ), + + // Depends on `ndigits` and `number.__round__` + ["" | "builtins", "round"] => match handle_round(checker, outer_range, arguments) { + None => return, + Some(edit) => (edit, Applicability::Unsafe), + }, + + // Depends on `__ceil__`/`__floor__`/`__trunc__` + ["math", "ceil" | "floor" | "trunc"] => ( + handle_other(checker, outer_range, inner_range), + Applicability::Unsafe, + ), + + _ => return, + }; + + let diagnostic = Diagnostic::new(UnnecessaryCastToInt {}, call.range); + let fix = Fix::applicable_edit(edit, applicability); + + checker.diagnostics.push(diagnostic.with_fix(fix)); +} + +fn single_argument_to_int_call<'a>( + semantic: &SemanticModel, + call: &'a ExprCall, +) -> Option<&'a Expr> { + let ExprCall { + func, arguments, .. + } = call; + + if !semantic.match_builtin_expr(func, "int") { + return None; + } + + if !arguments.keywords.is_empty() { + return None; + } + + let [argument] = &*arguments.args else { + return None; + }; + + Some(argument) +} + +fn handle_round(checker: &Checker, outer_range: TextRange, arguments: &Arguments) -> Option { + if arguments.len() > 2 { + return None; + } + + let number = arguments.find_argument("number", 0)?; + let ndigits = arguments.find_argument("ndigits", 1); + + let number_expr = checker.locator().slice(number); + let new_content = match ndigits { + Some(Expr::NumberLiteral(ExprNumberLiteral { value, .. })) if is_literal_zero(value) => { + format!("round({})", number_expr) + } + Some(Expr::NoneLiteral(_)) | None => format!("round({})", number_expr), + _ => return None, + }; + + Some(Edit::range_replacement(new_content, outer_range)) +} + +fn is_literal_zero(value: &Number) -> bool { + let Number::Int(int) = value else { + return false; + }; + + matches!(int.as_u8(), Some(0)) +} + +fn handle_other(checker: &Checker, outer_range: TextRange, inner_range: TextRange) -> Edit { + let inner_expr = checker.locator().slice(inner_range); + + Edit::range_replacement(inner_expr.to_string(), outer_range) +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap new file mode 100644 index 0000000000000..93e69cfef504d --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -0,0 +1,470 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF046.py:7:1: RUF046 [*] Value being casted is already an integer + | +6 | # Arguments are not checked +7 | int(id()) + | ^^^^^^^^^ RUF046 +8 | int(len([])) +9 | int(ord(foo)) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +4 4 | ### Safely fixable +5 5 | +6 6 | # Arguments are not checked +7 |-int(id()) + 7 |+id() +8 8 | int(len([])) +9 9 | int(ord(foo)) +10 10 | int(hash(foo, bar)) + +RUF046.py:8:1: RUF046 [*] Value being casted is already an integer + | + 6 | # Arguments are not checked + 7 | int(id()) + 8 | int(len([])) + | ^^^^^^^^^^^^ RUF046 + 9 | int(ord(foo)) +10 | int(hash(foo, bar)) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +5 5 | +6 6 | # Arguments are not checked +7 7 | int(id()) +8 |-int(len([])) + 8 |+len([]) +9 9 | int(ord(foo)) +10 10 | int(hash(foo, bar)) +11 11 | int(int('')) + +RUF046.py:9:1: RUF046 [*] Value being casted is already an integer + | + 7 | int(id()) + 8 | int(len([])) + 9 | int(ord(foo)) + | ^^^^^^^^^^^^^ RUF046 +10 | int(hash(foo, bar)) +11 | int(int('')) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +6 6 | # Arguments are not checked +7 7 | int(id()) +8 8 | int(len([])) +9 |-int(ord(foo)) + 9 |+ord(foo) +10 10 | int(hash(foo, bar)) +11 11 | int(int('')) +12 12 | + +RUF046.py:10:1: RUF046 [*] Value being casted is already an integer + | + 8 | int(len([])) + 9 | int(ord(foo)) +10 | int(hash(foo, bar)) + | ^^^^^^^^^^^^^^^^^^^ RUF046 +11 | int(int('')) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +7 7 | int(id()) +8 8 | int(len([])) +9 9 | int(ord(foo)) +10 |-int(hash(foo, bar)) + 10 |+hash(foo, bar) +11 11 | int(int('')) +12 12 | +13 13 | int(math.comb()) + +RUF046.py:11:1: RUF046 [*] Value being casted is already an integer + | + 9 | int(ord(foo)) +10 | int(hash(foo, bar)) +11 | int(int('')) + | ^^^^^^^^^^^^ RUF046 +12 | +13 | int(math.comb()) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +8 8 | int(len([])) +9 9 | int(ord(foo)) +10 10 | int(hash(foo, bar)) +11 |-int(int('')) + 11 |+int('') +12 12 | +13 13 | int(math.comb()) +14 14 | int(math.factorial()) + +RUF046.py:13:1: RUF046 [*] Value being casted is already an integer + | +11 | int(int('')) +12 | +13 | int(math.comb()) + | ^^^^^^^^^^^^^^^^ RUF046 +14 | int(math.factorial()) +15 | int(math.gcd()) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +10 10 | int(hash(foo, bar)) +11 11 | int(int('')) +12 12 | +13 |-int(math.comb()) + 13 |+math.comb() +14 14 | int(math.factorial()) +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) + +RUF046.py:14:1: RUF046 [*] Value being casted is already an integer + | +13 | int(math.comb()) +14 | int(math.factorial()) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +15 | int(math.gcd()) +16 | int(math.lcm()) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +11 11 | int(int('')) +12 12 | +13 13 | int(math.comb()) +14 |-int(math.factorial()) + 14 |+math.factorial() +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) +17 17 | int(math.isqrt()) + +RUF046.py:15:1: RUF046 [*] Value being casted is already an integer + | +13 | int(math.comb()) +14 | int(math.factorial()) +15 | int(math.gcd()) + | ^^^^^^^^^^^^^^^ RUF046 +16 | int(math.lcm()) +17 | int(math.isqrt()) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +12 12 | +13 13 | int(math.comb()) +14 14 | int(math.factorial()) +15 |-int(math.gcd()) + 15 |+math.gcd() +16 16 | int(math.lcm()) +17 17 | int(math.isqrt()) +18 18 | int(math.perm()) + +RUF046.py:16:1: RUF046 [*] Value being casted is already an integer + | +14 | int(math.factorial()) +15 | int(math.gcd()) +16 | int(math.lcm()) + | ^^^^^^^^^^^^^^^ RUF046 +17 | int(math.isqrt()) +18 | int(math.perm()) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +13 13 | int(math.comb()) +14 14 | int(math.factorial()) +15 15 | int(math.gcd()) +16 |-int(math.lcm()) + 16 |+math.lcm() +17 17 | int(math.isqrt()) +18 18 | int(math.perm()) +19 19 | + +RUF046.py:17:1: RUF046 [*] Value being casted is already an integer + | +15 | int(math.gcd()) +16 | int(math.lcm()) +17 | int(math.isqrt()) + | ^^^^^^^^^^^^^^^^^ RUF046 +18 | int(math.perm()) + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +14 14 | int(math.factorial()) +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) +17 |-int(math.isqrt()) + 17 |+math.isqrt() +18 18 | int(math.perm()) +19 19 | +20 20 | + +RUF046.py:18:1: RUF046 [*] Value being casted is already an integer + | +16 | int(math.lcm()) +17 | int(math.isqrt()) +18 | int(math.perm()) + | ^^^^^^^^^^^^^^^^ RUF046 + | + = help: Remove `int()` wrapper call + +ℹ Safe fix +15 15 | int(math.gcd()) +16 16 | int(math.lcm()) +17 17 | int(math.isqrt()) +18 |-int(math.perm()) + 18 |+math.perm() +19 19 | +20 20 | +21 21 | ### Unsafe + +RUF046.py:23:1: RUF046 [*] Value being casted is already an integer + | +21 | ### Unsafe +22 | +23 | int(math.ceil()) + | ^^^^^^^^^^^^^^^^ RUF046 +24 | int(math.floor()) +25 | int(math.trunc()) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +20 20 | +21 21 | ### Unsafe +22 22 | +23 |-int(math.ceil()) + 23 |+math.ceil() +24 24 | int(math.floor()) +25 25 | int(math.trunc()) +26 26 | + +RUF046.py:24:1: RUF046 [*] Value being casted is already an integer + | +23 | int(math.ceil()) +24 | int(math.floor()) + | ^^^^^^^^^^^^^^^^^ RUF046 +25 | int(math.trunc()) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +21 21 | ### Unsafe +22 22 | +23 23 | int(math.ceil()) +24 |-int(math.floor()) + 24 |+math.floor() +25 25 | int(math.trunc()) +26 26 | +27 27 | + +RUF046.py:25:1: RUF046 [*] Value being casted is already an integer + | +23 | int(math.ceil()) +24 | int(math.floor()) +25 | int(math.trunc()) + | ^^^^^^^^^^^^^^^^^ RUF046 + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +22 22 | +23 23 | int(math.ceil()) +24 24 | int(math.floor()) +25 |-int(math.trunc()) + 25 |+math.trunc() +26 26 | +27 27 | +28 28 | ### `round()` + +RUF046.py:31:1: RUF046 [*] Value being casted is already an integer + | +30 | ## Errors +31 | int(round(0)) + | ^^^^^^^^^^^^^ RUF046 +32 | int(round(0, 0)) +33 | int(round(0, None)) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +28 28 | ### `round()` +29 29 | +30 30 | ## Errors +31 |-int(round(0)) + 31 |+round(0) +32 32 | int(round(0, 0)) +33 33 | int(round(0, None)) +34 34 | + +RUF046.py:32:1: RUF046 [*] Value being casted is already an integer + | +30 | ## Errors +31 | int(round(0)) +32 | int(round(0, 0)) + | ^^^^^^^^^^^^^^^^ RUF046 +33 | int(round(0, None)) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +29 29 | +30 30 | ## Errors +31 31 | int(round(0)) +32 |-int(round(0, 0)) + 32 |+round(0) +33 33 | int(round(0, None)) +34 34 | +35 35 | int(round(0.1)) + +RUF046.py:33:1: RUF046 [*] Value being casted is already an integer + | +31 | int(round(0)) +32 | int(round(0, 0)) +33 | int(round(0, None)) + | ^^^^^^^^^^^^^^^^^^^ RUF046 +34 | +35 | int(round(0.1)) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +30 30 | ## Errors +31 31 | int(round(0)) +32 32 | int(round(0, 0)) +33 |-int(round(0, None)) + 33 |+round(0) +34 34 | +35 35 | int(round(0.1)) +36 36 | int(round(0.1, 0)) + +RUF046.py:35:1: RUF046 [*] Value being casted is already an integer + | +33 | int(round(0, None)) +34 | +35 | int(round(0.1)) + | ^^^^^^^^^^^^^^^ RUF046 +36 | int(round(0.1, 0)) +37 | int(round(0.1, None)) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +32 32 | int(round(0, 0)) +33 33 | int(round(0, None)) +34 34 | +35 |-int(round(0.1)) + 35 |+round(0.1) +36 36 | int(round(0.1, 0)) +37 37 | int(round(0.1, None)) +38 38 | + +RUF046.py:36:1: RUF046 [*] Value being casted is already an integer + | +35 | int(round(0.1)) +36 | int(round(0.1, 0)) + | ^^^^^^^^^^^^^^^^^^ RUF046 +37 | int(round(0.1, None)) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +33 33 | int(round(0, None)) +34 34 | +35 35 | int(round(0.1)) +36 |-int(round(0.1, 0)) + 36 |+round(0.1) +37 37 | int(round(0.1, None)) +38 38 | +39 39 | # Argument type is not checked + +RUF046.py:37:1: RUF046 [*] Value being casted is already an integer + | +35 | int(round(0.1)) +36 | int(round(0.1, 0)) +37 | int(round(0.1, None)) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +38 | +39 | # Argument type is not checked + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +34 34 | +35 35 | int(round(0.1)) +36 36 | int(round(0.1, 0)) +37 |-int(round(0.1, None)) + 37 |+round(0.1) +38 38 | +39 39 | # Argument type is not checked +40 40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() + +RUF046.py:42:1: RUF046 [*] Value being casted is already an integer + | +40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +41 | +42 | int(round(foo)) + | ^^^^^^^^^^^^^^^ RUF046 +43 | int(round(foo, 0)) +44 | int(round(foo, None)) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +39 39 | # Argument type is not checked +40 40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +41 41 | +42 |-int(round(foo)) + 42 |+round(foo) +43 43 | int(round(foo, 0)) +44 44 | int(round(foo, None)) +45 45 | + +RUF046.py:43:1: RUF046 [*] Value being casted is already an integer + | +42 | int(round(foo)) +43 | int(round(foo, 0)) + | ^^^^^^^^^^^^^^^^^^ RUF046 +44 | int(round(foo, None)) + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +40 40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +41 41 | +42 42 | int(round(foo)) +43 |-int(round(foo, 0)) + 43 |+round(foo) +44 44 | int(round(foo, None)) +45 45 | +46 46 | ## No errors + +RUF046.py:44:1: RUF046 [*] Value being casted is already an integer + | +42 | int(round(foo)) +43 | int(round(foo, 0)) +44 | int(round(foo, None)) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +45 | +46 | ## No errors + | + = help: Remove `int()` wrapper call + +ℹ Unsafe fix +41 41 | +42 42 | int(round(foo)) +43 43 | int(round(foo, 0)) +44 |-int(round(foo, None)) + 44 |+round(foo) +45 45 | +46 46 | ## No errors +47 47 | int(round(0, 3.14)) diff --git a/crates/ruff_python_ast/src/int.rs b/crates/ruff_python_ast/src/int.rs index bbcf1b0b2a349..4d918f5574354 100644 --- a/crates/ruff_python_ast/src/int.rs +++ b/crates/ruff_python_ast/src/int.rs @@ -96,7 +96,7 @@ impl Int { } } - /// Return the [`Int`] as an u64, if it can be represented as that data type. + /// Return the [`Int`] as an usize, if it can be represented as that data type. pub fn as_usize(&self) -> Option { match &self.0 { Number::Small(small) => usize::try_from(*small).ok(), diff --git a/ruff.schema.json b/ruff.schema.json index 72ff32f77bace..4ababbdf9eb30 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3843,6 +3843,7 @@ "RUF04", "RUF040", "RUF041", + "RUF046", "RUF048", "RUF05", "RUF052", From 77d2f3ef6ee16a4eb0ed68d5550d36ff7a112bc8 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Mon, 2 Dec 2024 15:33:40 +0000 Subject: [PATCH 02/10] Clippy --- .../src/rules/ruff/rules/unnecessary_cast_to_int.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index ae2a3016e3ac8..124cf46a0b81c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -119,9 +119,9 @@ fn handle_round(checker: &Checker, outer_range: TextRange, arguments: &Arguments let number_expr = checker.locator().slice(number); let new_content = match ndigits { Some(Expr::NumberLiteral(ExprNumberLiteral { value, .. })) if is_literal_zero(value) => { - format!("round({})", number_expr) + format!("round({number_expr})") } - Some(Expr::NoneLiteral(_)) | None => format!("round({})", number_expr), + Some(Expr::NoneLiteral(_)) | None => format!("round({number_expr})"), _ => return None, }; From 98a0ece45da6228821af83c25305a254077b769a Mon Sep 17 00:00:00 2001 From: InSync Date: Tue, 3 Dec 2024 01:16:13 +0700 Subject: [PATCH 03/10] Add a note regarding type inference limitations per review --- .../src/rules/ruff/rules/unnecessary_cast_to_int.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 124cf46a0b81c..f5ac2b376cc18 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -14,6 +14,11 @@ use ruff_text_size::TextRange; /// ## Known problems /// This rule is prone to false positives due to type inference limitations. /// +/// It assumes that `round`, `math.ceil`, `math.floor`, `math.trunc` +/// always return `int`, which might not be the case for objects +/// with the corresponding dunder methods overridden. +/// In such cases, the fix is marked as unsafe. +/// /// ## Example /// /// ```python From 23a35c29930915346f2ad64611f22b436c69a74f Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Tue, 3 Dec 2024 13:56:51 +0000 Subject: [PATCH 04/10] Per review --- .../resources/test/fixtures/ruff/RUF046.py | 2 +- .../ruff/rules/unnecessary_cast_to_int.rs | 85 ++++++----- ...uff__tests__preview__RUF046_RUF046.py.snap | 140 +++++++----------- 3 files changed, 102 insertions(+), 125 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py index e520158e43fe7..e558cf05148c3 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py @@ -33,7 +33,6 @@ int(round(0, None)) int(round(0.1)) -int(round(0.1, 0)) int(round(0.1, None)) # Argument type is not checked @@ -48,3 +47,4 @@ int(round(0, non_literal)) int(round(0, 0), base) int(round(0, 0, extra=keyword)) +int(round(0.1, 0)) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index f5ac2b376cc18..5c414ded30975 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -1,7 +1,8 @@ use crate::checkers::ast::Checker; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{Arguments, Expr, ExprCall, ExprNumberLiteral, Number}; +use ruff_python_ast::{Arguments, Expr, ExprCall, ExprName, ExprNumberLiteral, Number}; +use ruff_python_semantic::analyze::typing; use ruff_python_semantic::SemanticModel; use ruff_text_size::TextRange; @@ -12,18 +13,15 @@ use ruff_text_size::TextRange; /// Such a conversion is unnecessary. /// /// ## Known problems -/// This rule is prone to false positives due to type inference limitations. -/// -/// It assumes that `round`, `math.ceil`, `math.floor`, `math.trunc` -/// always return `int`, which might not be the case for objects -/// with the corresponding dunder methods overridden. -/// In such cases, the fix is marked as unsafe. +/// When values incorrectly override the `__round__`, `__ceil__`, `__floor__`, +/// or `__trunc__` operators such that they don't return an integer, +/// this rule may produce false positives. /// /// ## Example /// /// ```python /// int(len([])) -/// int(round(foo, 0)) +/// int(round(foo, None)) /// ``` /// /// Use instead: @@ -61,31 +59,28 @@ pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { return; }; - let (edit, applicability) = match qualified_name.segments() { + let edit = match qualified_name.segments() { // Always returns a strict instance of `int` ["" | "builtins", "len" | "id" | "hash" | "ord" | "int"] - | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] => ( - handle_other(checker, outer_range, inner_range), - Applicability::Safe, - ), + | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] + // Depends on `__ceil__`/`__floor__`/`__trunc__` + | ["math", "ceil" | "floor" | "trunc"] => { + replace_with_inner(checker, outer_range, inner_range) + } // Depends on `ndigits` and `number.__round__` - ["" | "builtins", "round"] => match handle_round(checker, outer_range, arguments) { - None => return, - Some(edit) => (edit, Applicability::Unsafe), - }, - - // Depends on `__ceil__`/`__floor__`/`__trunc__` - ["math", "ceil" | "floor" | "trunc"] => ( - handle_other(checker, outer_range, inner_range), - Applicability::Unsafe, - ), + ["" | "builtins", "round"] => { + match replace_with_shortened_round_call(checker, outer_range, arguments) { + None => return, + Some(edit) => edit, + } + } _ => return, }; - let diagnostic = Diagnostic::new(UnnecessaryCastToInt {}, call.range); - let fix = Fix::applicable_edit(edit, applicability); + let diagnostic = Diagnostic::new(UnnecessaryCastToInt, call.range); + let fix = Fix::safe_edit(edit); checker.diagnostics.push(diagnostic.with_fix(fix)); } @@ -113,7 +108,14 @@ fn single_argument_to_int_call<'a>( Some(argument) } -fn handle_round(checker: &Checker, outer_range: TextRange, arguments: &Arguments) -> Option { +/// Returns an [`Edit`] when the call is of any of the forms: +/// * `round(integer)`, `round(integer, 0)`, `round(integer, None)` +/// * `round(whatever)`, `round(integer, None)` +fn replace_with_shortened_round_call( + checker: &Checker, + outer_range: TextRange, + arguments: &Arguments, +) -> Option { if arguments.len() > 2 { return None; } @@ -121,18 +123,33 @@ fn handle_round(checker: &Checker, outer_range: TextRange, arguments: &Arguments let number = arguments.find_argument("number", 0)?; let ndigits = arguments.find_argument("ndigits", 1); - let number_expr = checker.locator().slice(number); - let new_content = match ndigits { - Some(Expr::NumberLiteral(ExprNumberLiteral { value, .. })) if is_literal_zero(value) => { - format!("round({number_expr})") - } - Some(Expr::NoneLiteral(_)) | None => format!("round({number_expr})"), + let number_is_int = match number { + Expr::Name(name) => is_int(checker.semantic(), name), + Expr::NumberLiteral(ExprNumberLiteral { value, .. }) => matches!(value, Number::Int(..)), + _ => false, + }; + + match ndigits { + Some(Expr::NumberLiteral(ExprNumberLiteral { value, .. })) + if is_literal_zero(value) && number_is_int => {} + Some(Expr::NoneLiteral(_)) | None => {} _ => return None, }; + let number_expr = checker.locator().slice(number); + let new_content = format!("round({number_expr})"); + Some(Edit::range_replacement(new_content, outer_range)) } +fn is_int(semantic: &SemanticModel, name: &ExprName) -> bool { + let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { + return false; + }; + + typing::is_int(binding, semantic) +} + fn is_literal_zero(value: &Number) -> bool { let Number::Int(int) = value else { return false; @@ -141,7 +158,7 @@ fn is_literal_zero(value: &Number) -> bool { matches!(int.as_u8(), Some(0)) } -fn handle_other(checker: &Checker, outer_range: TextRange, inner_range: TextRange) -> Edit { +fn replace_with_inner(checker: &Checker, outer_range: TextRange, inner_range: TextRange) -> Edit { let inner_expr = checker.locator().slice(inner_range); Edit::range_replacement(inner_expr.to_string(), outer_range) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap index 93e69cfef504d..b37948c545c08 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -238,7 +238,7 @@ RUF046.py:23:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 20 20 | 21 21 | ### Unsafe 22 22 | @@ -257,7 +257,7 @@ RUF046.py:24:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 21 21 | ### Unsafe 22 22 | 23 23 | int(math.ceil()) @@ -276,7 +276,7 @@ RUF046.py:25:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 22 22 | 23 23 | int(math.ceil()) 24 24 | int(math.floor()) @@ -296,7 +296,7 @@ RUF046.py:31:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 28 28 | ### `round()` 29 29 | 30 30 | ## Errors @@ -316,7 +316,7 @@ RUF046.py:32:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 29 29 | 30 30 | ## Errors 31 31 | int(round(0)) @@ -337,7 +337,7 @@ RUF046.py:33:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 30 30 | ## Errors 31 31 | int(round(0)) 32 32 | int(round(0, 0)) @@ -345,7 +345,7 @@ RUF046.py:33:1: RUF046 [*] Value being casted is already an integer 33 |+round(0) 34 34 | 35 35 | int(round(0.1)) -36 36 | int(round(0.1, 0)) +36 36 | int(round(0.1, None)) RUF046.py:35:1: RUF046 [*] Value being casted is already an integer | @@ -353,118 +353,78 @@ RUF046.py:35:1: RUF046 [*] Value being casted is already an integer 34 | 35 | int(round(0.1)) | ^^^^^^^^^^^^^^^ RUF046 -36 | int(round(0.1, 0)) -37 | int(round(0.1, None)) +36 | int(round(0.1, None)) | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 32 32 | int(round(0, 0)) 33 33 | int(round(0, None)) 34 34 | 35 |-int(round(0.1)) 35 |+round(0.1) -36 36 | int(round(0.1, 0)) -37 37 | int(round(0.1, None)) -38 38 | +36 36 | int(round(0.1, None)) +37 37 | +38 38 | # Argument type is not checked RUF046.py:36:1: RUF046 [*] Value being casted is already an integer | 35 | int(round(0.1)) -36 | int(round(0.1, 0)) - | ^^^^^^^^^^^^^^^^^^ RUF046 -37 | int(round(0.1, None)) +36 | int(round(0.1, None)) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +37 | +38 | # Argument type is not checked | = help: Remove `int()` wrapper call -ℹ Unsafe fix +ℹ Safe fix 33 33 | int(round(0, None)) 34 34 | 35 35 | int(round(0.1)) -36 |-int(round(0.1, 0)) +36 |-int(round(0.1, None)) 36 |+round(0.1) -37 37 | int(round(0.1, None)) -38 38 | -39 39 | # Argument type is not checked +37 37 | +38 38 | # Argument type is not checked +39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -RUF046.py:37:1: RUF046 [*] Value being casted is already an integer - | -35 | int(round(0.1)) -36 | int(round(0.1, 0)) -37 | int(round(0.1, None)) - | ^^^^^^^^^^^^^^^^^^^^^ RUF046 -38 | -39 | # Argument type is not checked +RUF046.py:41:1: RUF046 [*] Value being casted is already an integer | - = help: Remove `int()` wrapper call - -ℹ Unsafe fix -34 34 | -35 35 | int(round(0.1)) -36 36 | int(round(0.1, 0)) -37 |-int(round(0.1, None)) - 37 |+round(0.1) -38 38 | -39 39 | # Argument type is not checked -40 40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() - -RUF046.py:42:1: RUF046 [*] Value being casted is already an integer - | -40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -41 | -42 | int(round(foo)) +39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +40 | +41 | int(round(foo)) | ^^^^^^^^^^^^^^^ RUF046 -43 | int(round(foo, 0)) -44 | int(round(foo, None)) +42 | int(round(foo, 0)) +43 | int(round(foo, None)) | = help: Remove `int()` wrapper call -ℹ Unsafe fix -39 39 | # Argument type is not checked -40 40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -41 41 | -42 |-int(round(foo)) - 42 |+round(foo) -43 43 | int(round(foo, 0)) -44 44 | int(round(foo, None)) -45 45 | +ℹ Safe fix +38 38 | # Argument type is not checked +39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +40 40 | +41 |-int(round(foo)) + 41 |+round(foo) +42 42 | int(round(foo, 0)) +43 43 | int(round(foo, None)) +44 44 | RUF046.py:43:1: RUF046 [*] Value being casted is already an integer | -42 | int(round(foo)) -43 | int(round(foo, 0)) - | ^^^^^^^^^^^^^^^^^^ RUF046 -44 | int(round(foo, None)) - | - = help: Remove `int()` wrapper call - -ℹ Unsafe fix -40 40 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -41 41 | -42 42 | int(round(foo)) -43 |-int(round(foo, 0)) - 43 |+round(foo) -44 44 | int(round(foo, None)) -45 45 | -46 46 | ## No errors - -RUF046.py:44:1: RUF046 [*] Value being casted is already an integer - | -42 | int(round(foo)) -43 | int(round(foo, 0)) -44 | int(round(foo, None)) +41 | int(round(foo)) +42 | int(round(foo, 0)) +43 | int(round(foo, None)) | ^^^^^^^^^^^^^^^^^^^^^ RUF046 -45 | -46 | ## No errors +44 | +45 | ## No errors | = help: Remove `int()` wrapper call -ℹ Unsafe fix -41 41 | -42 42 | int(round(foo)) -43 43 | int(round(foo, 0)) -44 |-int(round(foo, None)) - 44 |+round(foo) -45 45 | -46 46 | ## No errors -47 47 | int(round(0, 3.14)) +ℹ Safe fix +40 40 | +41 41 | int(round(foo)) +42 42 | int(round(foo, 0)) +43 |-int(round(foo, None)) + 43 |+round(foo) +44 44 | +45 45 | ## No errors +46 46 | int(round(0, 3.14)) From 4e98b97ede729ff0b60aedca93a1f508d1dee8fb Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Tue, 3 Dec 2024 15:04:19 +0000 Subject: [PATCH 05/10] Clippy --- .../src/rules/ruff/rules/unnecessary_cast_to_int.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 5c414ded30975..4b0f8d2057e49 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -60,15 +60,11 @@ pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { }; let edit = match qualified_name.segments() { - // Always returns a strict instance of `int` ["" | "builtins", "len" | "id" | "hash" | "ord" | "int"] - | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] - // Depends on `__ceil__`/`__floor__`/`__trunc__` - | ["math", "ceil" | "floor" | "trunc"] => { + | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm" | "ceil" | "floor" | "trunc"] => { replace_with_inner(checker, outer_range, inner_range) } - // Depends on `ndigits` and `number.__round__` ["" | "builtins", "round"] => { match replace_with_shortened_round_call(checker, outer_range, arguments) { None => return, From cd3e3eba1656d14eb78e06289dac278c14b528c8 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Wed, 4 Dec 2024 16:14:39 +0000 Subject: [PATCH 06/10] Revert --- .../ruff/rules/unnecessary_cast_to_int.rs | 32 +++++++++++++------ ...uff__tests__preview__RUF046_RUF046.py.snap | 20 ++++++------ 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 4b0f8d2057e49..53b1dc206d180 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -1,5 +1,5 @@ use crate::checkers::ast::Checker; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{Arguments, Expr, ExprCall, ExprName, ExprNumberLiteral, Number}; use ruff_python_semantic::analyze::typing; @@ -13,9 +13,12 @@ use ruff_text_size::TextRange; /// Such a conversion is unnecessary. /// /// ## Known problems -/// When values incorrectly override the `__round__`, `__ceil__`, `__floor__`, -/// or `__trunc__` operators such that they don't return an integer, -/// this rule may produce false positives. +/// This rule is prone to false positives due to type inference limitations. +/// +/// It assumes that `round`, `math.ceil`, `math.floor`, `math.trunc` +/// always return `int`, which might not be the case for objects +/// with the corresponding dunder methods overridden. +/// In such cases, the fix is marked as unsafe. /// /// ## Example /// @@ -59,24 +62,33 @@ pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { return; }; - let edit = match qualified_name.segments() { + let (edit, applicability) = match qualified_name.segments() { + // Always returns a strict instance of `int` ["" | "builtins", "len" | "id" | "hash" | "ord" | "int"] - | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm" | "ceil" | "floor" | "trunc"] => { - replace_with_inner(checker, outer_range, inner_range) - } + | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] => ( + replace_with_inner(checker, outer_range, inner_range), + Applicability::Safe, + ), + // Depends on `ndigits` and `number.__round__` ["" | "builtins", "round"] => { match replace_with_shortened_round_call(checker, outer_range, arguments) { None => return, - Some(edit) => edit, + Some(edit) => (edit, Applicability::Unsafe), } } + // Depends on `__ceil__`/`__floor__`/`__trunc__` + ["math", "ceil" | "floor" | "trunc"] => ( + replace_with_inner(checker, outer_range, inner_range), + Applicability::Unsafe, + ), + _ => return, }; let diagnostic = Diagnostic::new(UnnecessaryCastToInt, call.range); - let fix = Fix::safe_edit(edit); + let fix = Fix::applicable_edit(edit, applicability); checker.diagnostics.push(diagnostic.with_fix(fix)); } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap index b37948c545c08..97c65c1884751 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -238,7 +238,7 @@ RUF046.py:23:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 20 20 | 21 21 | ### Unsafe 22 22 | @@ -257,7 +257,7 @@ RUF046.py:24:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 21 21 | ### Unsafe 22 22 | 23 23 | int(math.ceil()) @@ -276,7 +276,7 @@ RUF046.py:25:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 22 22 | 23 23 | int(math.ceil()) 24 24 | int(math.floor()) @@ -296,7 +296,7 @@ RUF046.py:31:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 28 28 | ### `round()` 29 29 | 30 30 | ## Errors @@ -316,7 +316,7 @@ RUF046.py:32:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 29 29 | 30 30 | ## Errors 31 31 | int(round(0)) @@ -337,7 +337,7 @@ RUF046.py:33:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 30 30 | ## Errors 31 31 | int(round(0)) 32 32 | int(round(0, 0)) @@ -357,7 +357,7 @@ RUF046.py:35:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 32 32 | int(round(0, 0)) 33 33 | int(round(0, None)) 34 34 | @@ -377,7 +377,7 @@ RUF046.py:36:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 33 33 | int(round(0, None)) 34 34 | 35 35 | int(round(0.1)) @@ -398,7 +398,7 @@ RUF046.py:41:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 38 38 | # Argument type is not checked 39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() 40 40 | @@ -419,7 +419,7 @@ RUF046.py:43:1: RUF046 [*] Value being casted is already an integer | = help: Remove `int()` wrapper call -ℹ Safe fix +ℹ Unsafe fix 40 40 | 41 41 | int(round(foo)) 42 42 | int(round(foo, 0)) From 7c8709b54727b2f3f4a8843585ec6c103ea53e5d Mon Sep 17 00:00:00 2001 From: InSync Date: Thu, 5 Dec 2024 06:19:39 +0700 Subject: [PATCH 07/10] Fix a typo, change message --- .../ruff/rules/unnecessary_cast_to_int.rs | 4 +- ...uff__tests__preview__RUF046_RUF046.py.snap | 42 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 53b1dc206d180..1cc4773feb05c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -39,7 +39,7 @@ pub(crate) struct UnnecessaryCastToInt; impl AlwaysFixableViolation for UnnecessaryCastToInt { #[derive_message_formats] fn message(&self) -> String { - "Value being casted is already an integer".to_string() + "Value being converted is already an integer".to_string() } fn fix_title(&self) -> String { @@ -118,7 +118,7 @@ fn single_argument_to_int_call<'a>( /// Returns an [`Edit`] when the call is of any of the forms: /// * `round(integer)`, `round(integer, 0)`, `round(integer, None)` -/// * `round(whatever)`, `round(integer, None)` +/// * `round(whatever)`, `round(whatever, None)` fn replace_with_shortened_round_call( checker: &Checker, outer_range: TextRange, diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap index 97c65c1884751..7f2783ea6c373 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs snapshot_kind: text --- -RUF046.py:7:1: RUF046 [*] Value being casted is already an integer +RUF046.py:7:1: RUF046 [*] Value being converted is already an integer | 6 | # Arguments are not checked 7 | int(id()) @@ -22,7 +22,7 @@ RUF046.py:7:1: RUF046 [*] Value being casted is already an integer 9 9 | int(ord(foo)) 10 10 | int(hash(foo, bar)) -RUF046.py:8:1: RUF046 [*] Value being casted is already an integer +RUF046.py:8:1: RUF046 [*] Value being converted is already an integer | 6 | # Arguments are not checked 7 | int(id()) @@ -43,7 +43,7 @@ RUF046.py:8:1: RUF046 [*] Value being casted is already an integer 10 10 | int(hash(foo, bar)) 11 11 | int(int('')) -RUF046.py:9:1: RUF046 [*] Value being casted is already an integer +RUF046.py:9:1: RUF046 [*] Value being converted is already an integer | 7 | int(id()) 8 | int(len([])) @@ -64,7 +64,7 @@ RUF046.py:9:1: RUF046 [*] Value being casted is already an integer 11 11 | int(int('')) 12 12 | -RUF046.py:10:1: RUF046 [*] Value being casted is already an integer +RUF046.py:10:1: RUF046 [*] Value being converted is already an integer | 8 | int(len([])) 9 | int(ord(foo)) @@ -84,7 +84,7 @@ RUF046.py:10:1: RUF046 [*] Value being casted is already an integer 12 12 | 13 13 | int(math.comb()) -RUF046.py:11:1: RUF046 [*] Value being casted is already an integer +RUF046.py:11:1: RUF046 [*] Value being converted is already an integer | 9 | int(ord(foo)) 10 | int(hash(foo, bar)) @@ -105,7 +105,7 @@ RUF046.py:11:1: RUF046 [*] Value being casted is already an integer 13 13 | int(math.comb()) 14 14 | int(math.factorial()) -RUF046.py:13:1: RUF046 [*] Value being casted is already an integer +RUF046.py:13:1: RUF046 [*] Value being converted is already an integer | 11 | int(int('')) 12 | @@ -126,7 +126,7 @@ RUF046.py:13:1: RUF046 [*] Value being casted is already an integer 15 15 | int(math.gcd()) 16 16 | int(math.lcm()) -RUF046.py:14:1: RUF046 [*] Value being casted is already an integer +RUF046.py:14:1: RUF046 [*] Value being converted is already an integer | 13 | int(math.comb()) 14 | int(math.factorial()) @@ -146,7 +146,7 @@ RUF046.py:14:1: RUF046 [*] Value being casted is already an integer 16 16 | int(math.lcm()) 17 17 | int(math.isqrt()) -RUF046.py:15:1: RUF046 [*] Value being casted is already an integer +RUF046.py:15:1: RUF046 [*] Value being converted is already an integer | 13 | int(math.comb()) 14 | int(math.factorial()) @@ -167,7 +167,7 @@ RUF046.py:15:1: RUF046 [*] Value being casted is already an integer 17 17 | int(math.isqrt()) 18 18 | int(math.perm()) -RUF046.py:16:1: RUF046 [*] Value being casted is already an integer +RUF046.py:16:1: RUF046 [*] Value being converted is already an integer | 14 | int(math.factorial()) 15 | int(math.gcd()) @@ -188,7 +188,7 @@ RUF046.py:16:1: RUF046 [*] Value being casted is already an integer 18 18 | int(math.perm()) 19 19 | -RUF046.py:17:1: RUF046 [*] Value being casted is already an integer +RUF046.py:17:1: RUF046 [*] Value being converted is already an integer | 15 | int(math.gcd()) 16 | int(math.lcm()) @@ -208,7 +208,7 @@ RUF046.py:17:1: RUF046 [*] Value being casted is already an integer 19 19 | 20 20 | -RUF046.py:18:1: RUF046 [*] Value being casted is already an integer +RUF046.py:18:1: RUF046 [*] Value being converted is already an integer | 16 | int(math.lcm()) 17 | int(math.isqrt()) @@ -227,7 +227,7 @@ RUF046.py:18:1: RUF046 [*] Value being casted is already an integer 20 20 | 21 21 | ### Unsafe -RUF046.py:23:1: RUF046 [*] Value being casted is already an integer +RUF046.py:23:1: RUF046 [*] Value being converted is already an integer | 21 | ### Unsafe 22 | @@ -248,7 +248,7 @@ RUF046.py:23:1: RUF046 [*] Value being casted is already an integer 25 25 | int(math.trunc()) 26 26 | -RUF046.py:24:1: RUF046 [*] Value being casted is already an integer +RUF046.py:24:1: RUF046 [*] Value being converted is already an integer | 23 | int(math.ceil()) 24 | int(math.floor()) @@ -267,7 +267,7 @@ RUF046.py:24:1: RUF046 [*] Value being casted is already an integer 26 26 | 27 27 | -RUF046.py:25:1: RUF046 [*] Value being casted is already an integer +RUF046.py:25:1: RUF046 [*] Value being converted is already an integer | 23 | int(math.ceil()) 24 | int(math.floor()) @@ -286,7 +286,7 @@ RUF046.py:25:1: RUF046 [*] Value being casted is already an integer 27 27 | 28 28 | ### `round()` -RUF046.py:31:1: RUF046 [*] Value being casted is already an integer +RUF046.py:31:1: RUF046 [*] Value being converted is already an integer | 30 | ## Errors 31 | int(round(0)) @@ -306,7 +306,7 @@ RUF046.py:31:1: RUF046 [*] Value being casted is already an integer 33 33 | int(round(0, None)) 34 34 | -RUF046.py:32:1: RUF046 [*] Value being casted is already an integer +RUF046.py:32:1: RUF046 [*] Value being converted is already an integer | 30 | ## Errors 31 | int(round(0)) @@ -326,7 +326,7 @@ RUF046.py:32:1: RUF046 [*] Value being casted is already an integer 34 34 | 35 35 | int(round(0.1)) -RUF046.py:33:1: RUF046 [*] Value being casted is already an integer +RUF046.py:33:1: RUF046 [*] Value being converted is already an integer | 31 | int(round(0)) 32 | int(round(0, 0)) @@ -347,7 +347,7 @@ RUF046.py:33:1: RUF046 [*] Value being casted is already an integer 35 35 | int(round(0.1)) 36 36 | int(round(0.1, None)) -RUF046.py:35:1: RUF046 [*] Value being casted is already an integer +RUF046.py:35:1: RUF046 [*] Value being converted is already an integer | 33 | int(round(0, None)) 34 | @@ -367,7 +367,7 @@ RUF046.py:35:1: RUF046 [*] Value being casted is already an integer 37 37 | 38 38 | # Argument type is not checked -RUF046.py:36:1: RUF046 [*] Value being casted is already an integer +RUF046.py:36:1: RUF046 [*] Value being converted is already an integer | 35 | int(round(0.1)) 36 | int(round(0.1, None)) @@ -387,7 +387,7 @@ RUF046.py:36:1: RUF046 [*] Value being casted is already an integer 38 38 | # Argument type is not checked 39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -RUF046.py:41:1: RUF046 [*] Value being casted is already an integer +RUF046.py:41:1: RUF046 [*] Value being converted is already an integer | 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() 40 | @@ -408,7 +408,7 @@ RUF046.py:41:1: RUF046 [*] Value being casted is already an integer 43 43 | int(round(foo, None)) 44 44 | -RUF046.py:43:1: RUF046 [*] Value being casted is already an integer +RUF046.py:43:1: RUF046 [*] Value being converted is already an integer | 41 | int(round(foo)) 42 | int(round(foo, 0)) From e7c177142ec20f53b265af5ea95f8e93a7e64a7c Mon Sep 17 00:00:00 2001 From: InSync Date: Thu, 5 Dec 2024 06:52:03 +0700 Subject: [PATCH 08/10] Change back --- .../ruff/rules/unnecessary_cast_to_int.rs | 2 +- ...uff__tests__preview__RUF046_RUF046.py.snap | 42 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 1cc4773feb05c..947fae60b7436 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -39,7 +39,7 @@ pub(crate) struct UnnecessaryCastToInt; impl AlwaysFixableViolation for UnnecessaryCastToInt { #[derive_message_formats] fn message(&self) -> String { - "Value being converted is already an integer".to_string() + "Value being casted is already an integer".to_string() } fn fix_title(&self) -> String { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap index 7f2783ea6c373..97c65c1884751 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs snapshot_kind: text --- -RUF046.py:7:1: RUF046 [*] Value being converted is already an integer +RUF046.py:7:1: RUF046 [*] Value being casted is already an integer | 6 | # Arguments are not checked 7 | int(id()) @@ -22,7 +22,7 @@ RUF046.py:7:1: RUF046 [*] Value being converted is already an integer 9 9 | int(ord(foo)) 10 10 | int(hash(foo, bar)) -RUF046.py:8:1: RUF046 [*] Value being converted is already an integer +RUF046.py:8:1: RUF046 [*] Value being casted is already an integer | 6 | # Arguments are not checked 7 | int(id()) @@ -43,7 +43,7 @@ RUF046.py:8:1: RUF046 [*] Value being converted is already an integer 10 10 | int(hash(foo, bar)) 11 11 | int(int('')) -RUF046.py:9:1: RUF046 [*] Value being converted is already an integer +RUF046.py:9:1: RUF046 [*] Value being casted is already an integer | 7 | int(id()) 8 | int(len([])) @@ -64,7 +64,7 @@ RUF046.py:9:1: RUF046 [*] Value being converted is already an integer 11 11 | int(int('')) 12 12 | -RUF046.py:10:1: RUF046 [*] Value being converted is already an integer +RUF046.py:10:1: RUF046 [*] Value being casted is already an integer | 8 | int(len([])) 9 | int(ord(foo)) @@ -84,7 +84,7 @@ RUF046.py:10:1: RUF046 [*] Value being converted is already an integer 12 12 | 13 13 | int(math.comb()) -RUF046.py:11:1: RUF046 [*] Value being converted is already an integer +RUF046.py:11:1: RUF046 [*] Value being casted is already an integer | 9 | int(ord(foo)) 10 | int(hash(foo, bar)) @@ -105,7 +105,7 @@ RUF046.py:11:1: RUF046 [*] Value being converted is already an integer 13 13 | int(math.comb()) 14 14 | int(math.factorial()) -RUF046.py:13:1: RUF046 [*] Value being converted is already an integer +RUF046.py:13:1: RUF046 [*] Value being casted is already an integer | 11 | int(int('')) 12 | @@ -126,7 +126,7 @@ RUF046.py:13:1: RUF046 [*] Value being converted is already an integer 15 15 | int(math.gcd()) 16 16 | int(math.lcm()) -RUF046.py:14:1: RUF046 [*] Value being converted is already an integer +RUF046.py:14:1: RUF046 [*] Value being casted is already an integer | 13 | int(math.comb()) 14 | int(math.factorial()) @@ -146,7 +146,7 @@ RUF046.py:14:1: RUF046 [*] Value being converted is already an integer 16 16 | int(math.lcm()) 17 17 | int(math.isqrt()) -RUF046.py:15:1: RUF046 [*] Value being converted is already an integer +RUF046.py:15:1: RUF046 [*] Value being casted is already an integer | 13 | int(math.comb()) 14 | int(math.factorial()) @@ -167,7 +167,7 @@ RUF046.py:15:1: RUF046 [*] Value being converted is already an integer 17 17 | int(math.isqrt()) 18 18 | int(math.perm()) -RUF046.py:16:1: RUF046 [*] Value being converted is already an integer +RUF046.py:16:1: RUF046 [*] Value being casted is already an integer | 14 | int(math.factorial()) 15 | int(math.gcd()) @@ -188,7 +188,7 @@ RUF046.py:16:1: RUF046 [*] Value being converted is already an integer 18 18 | int(math.perm()) 19 19 | -RUF046.py:17:1: RUF046 [*] Value being converted is already an integer +RUF046.py:17:1: RUF046 [*] Value being casted is already an integer | 15 | int(math.gcd()) 16 | int(math.lcm()) @@ -208,7 +208,7 @@ RUF046.py:17:1: RUF046 [*] Value being converted is already an integer 19 19 | 20 20 | -RUF046.py:18:1: RUF046 [*] Value being converted is already an integer +RUF046.py:18:1: RUF046 [*] Value being casted is already an integer | 16 | int(math.lcm()) 17 | int(math.isqrt()) @@ -227,7 +227,7 @@ RUF046.py:18:1: RUF046 [*] Value being converted is already an integer 20 20 | 21 21 | ### Unsafe -RUF046.py:23:1: RUF046 [*] Value being converted is already an integer +RUF046.py:23:1: RUF046 [*] Value being casted is already an integer | 21 | ### Unsafe 22 | @@ -248,7 +248,7 @@ RUF046.py:23:1: RUF046 [*] Value being converted is already an integer 25 25 | int(math.trunc()) 26 26 | -RUF046.py:24:1: RUF046 [*] Value being converted is already an integer +RUF046.py:24:1: RUF046 [*] Value being casted is already an integer | 23 | int(math.ceil()) 24 | int(math.floor()) @@ -267,7 +267,7 @@ RUF046.py:24:1: RUF046 [*] Value being converted is already an integer 26 26 | 27 27 | -RUF046.py:25:1: RUF046 [*] Value being converted is already an integer +RUF046.py:25:1: RUF046 [*] Value being casted is already an integer | 23 | int(math.ceil()) 24 | int(math.floor()) @@ -286,7 +286,7 @@ RUF046.py:25:1: RUF046 [*] Value being converted is already an integer 27 27 | 28 28 | ### `round()` -RUF046.py:31:1: RUF046 [*] Value being converted is already an integer +RUF046.py:31:1: RUF046 [*] Value being casted is already an integer | 30 | ## Errors 31 | int(round(0)) @@ -306,7 +306,7 @@ RUF046.py:31:1: RUF046 [*] Value being converted is already an integer 33 33 | int(round(0, None)) 34 34 | -RUF046.py:32:1: RUF046 [*] Value being converted is already an integer +RUF046.py:32:1: RUF046 [*] Value being casted is already an integer | 30 | ## Errors 31 | int(round(0)) @@ -326,7 +326,7 @@ RUF046.py:32:1: RUF046 [*] Value being converted is already an integer 34 34 | 35 35 | int(round(0.1)) -RUF046.py:33:1: RUF046 [*] Value being converted is already an integer +RUF046.py:33:1: RUF046 [*] Value being casted is already an integer | 31 | int(round(0)) 32 | int(round(0, 0)) @@ -347,7 +347,7 @@ RUF046.py:33:1: RUF046 [*] Value being converted is already an integer 35 35 | int(round(0.1)) 36 36 | int(round(0.1, None)) -RUF046.py:35:1: RUF046 [*] Value being converted is already an integer +RUF046.py:35:1: RUF046 [*] Value being casted is already an integer | 33 | int(round(0, None)) 34 | @@ -367,7 +367,7 @@ RUF046.py:35:1: RUF046 [*] Value being converted is already an integer 37 37 | 38 38 | # Argument type is not checked -RUF046.py:36:1: RUF046 [*] Value being converted is already an integer +RUF046.py:36:1: RUF046 [*] Value being casted is already an integer | 35 | int(round(0.1)) 36 | int(round(0.1, None)) @@ -387,7 +387,7 @@ RUF046.py:36:1: RUF046 [*] Value being converted is already an integer 38 38 | # Argument type is not checked 39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -RUF046.py:41:1: RUF046 [*] Value being converted is already an integer +RUF046.py:41:1: RUF046 [*] Value being casted is already an integer | 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() 40 | @@ -408,7 +408,7 @@ RUF046.py:41:1: RUF046 [*] Value being converted is already an integer 43 43 | int(round(foo, None)) 44 44 | -RUF046.py:43:1: RUF046 [*] Value being converted is already an integer +RUF046.py:43:1: RUF046 [*] Value being casted is already an integer | 41 | int(round(foo)) 42 | int(round(foo, 0)) From ca42abf4cd55d14461e71ae02a1a44d0dc616141 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 5 Dec 2024 08:12:17 +0100 Subject: [PATCH 09/10] Documentation --- .../rules/ruff/rules/unnecessary_cast_to_int.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 947fae60b7436..b2a73b21f469f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -13,12 +13,9 @@ use ruff_text_size::TextRange; /// Such a conversion is unnecessary. /// /// ## Known problems -/// This rule is prone to false positives due to type inference limitations. -/// -/// It assumes that `round`, `math.ceil`, `math.floor`, `math.trunc` -/// always return `int`, which might not be the case for objects -/// with the corresponding dunder methods overridden. -/// In such cases, the fix is marked as unsafe. +/// This rule may produce false positives for `round`, `math.ceil`, `math.floor`, +/// and `math.trunc` calls when values override the `__round__`, `__ceil__`, `__floor__`, +/// or `__trunc__` operators such that they don't return an integer. /// /// ## Example /// @@ -33,6 +30,12 @@ use ruff_text_size::TextRange; /// len([]) /// round(foo) /// ``` +/// +/// ## Fix safety +/// The fix for `round`, `math.ceil`, `math.floor`, and `math.truncate` is unsafe +/// because removing the `int` conversion can change the semantics for values +/// overriding the `__round__`, `__ceil__`, `__floor__`, or `__trunc__` dunder methods +/// such that they don't return an integer. #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryCastToInt; @@ -43,7 +46,7 @@ impl AlwaysFixableViolation for UnnecessaryCastToInt { } fn fix_title(&self) -> String { - "Remove `int()` wrapper call".to_string() + "Remove unnecessary conversion to `int`".to_string() } } From 52f8971756eda6f1850135d2f4e04938590fdcc2 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 5 Dec 2024 08:27:20 +0100 Subject: [PATCH 10/10] Few smaller nits --- .../ruff/rules/unnecessary_cast_to_int.rs | 43 ++++++++++------- ...uff__tests__preview__RUF046_RUF046.py.snap | 48 +++++++++---------- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index b2a73b21f469f..ddbe26a24ae47 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -65,35 +65,33 @@ pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { return; }; - let (edit, applicability) = match qualified_name.segments() { + let fix = match qualified_name.segments() { // Always returns a strict instance of `int` ["" | "builtins", "len" | "id" | "hash" | "ord" | "int"] - | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] => ( - replace_with_inner(checker, outer_range, inner_range), - Applicability::Safe, - ), + | ["math", "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm"] => { + Fix::safe_edit(replace_with_inner(checker, outer_range, inner_range)) + } // Depends on `ndigits` and `number.__round__` ["" | "builtins", "round"] => { - match replace_with_shortened_round_call(checker, outer_range, arguments) { - None => return, - Some(edit) => (edit, Applicability::Unsafe), + if let Some(fix) = replace_with_shortened_round_call(checker, outer_range, arguments) { + fix + } else { + return; } } // Depends on `__ceil__`/`__floor__`/`__trunc__` - ["math", "ceil" | "floor" | "trunc"] => ( - replace_with_inner(checker, outer_range, inner_range), - Applicability::Unsafe, - ), + ["math", "ceil" | "floor" | "trunc"] => { + Fix::unsafe_edit(replace_with_inner(checker, outer_range, inner_range)) + } _ => return, }; - let diagnostic = Diagnostic::new(UnnecessaryCastToInt, call.range); - let fix = Fix::applicable_edit(edit, applicability); - - checker.diagnostics.push(diagnostic.with_fix(fix)); + checker + .diagnostics + .push(Diagnostic::new(UnnecessaryCastToInt, call.range).with_fix(fix)); } fn single_argument_to_int_call<'a>( @@ -126,7 +124,7 @@ fn replace_with_shortened_round_call( checker: &Checker, outer_range: TextRange, arguments: &Arguments, -) -> Option { +) -> Option { if arguments.len() > 2 { return None; } @@ -150,7 +148,16 @@ fn replace_with_shortened_round_call( let number_expr = checker.locator().slice(number); let new_content = format!("round({number_expr})"); - Some(Edit::range_replacement(new_content, outer_range)) + let applicability = if number_is_int { + Applicability::Safe + } else { + Applicability::Unsafe + }; + + Some(Fix::applicable_edit( + Edit::range_replacement(new_content, outer_range), + applicability, + )) } fn is_int(semantic: &SemanticModel, name: &ExprName) -> bool { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap index 97c65c1884751..22e139816bcf8 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -10,7 +10,7 @@ RUF046.py:7:1: RUF046 [*] Value being casted is already an integer 8 | int(len([])) 9 | int(ord(foo)) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 4 4 | ### Safely fixable @@ -31,7 +31,7 @@ RUF046.py:8:1: RUF046 [*] Value being casted is already an integer 9 | int(ord(foo)) 10 | int(hash(foo, bar)) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 5 5 | @@ -52,7 +52,7 @@ RUF046.py:9:1: RUF046 [*] Value being casted is already an integer 10 | int(hash(foo, bar)) 11 | int(int('')) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 6 6 | # Arguments are not checked @@ -72,7 +72,7 @@ RUF046.py:10:1: RUF046 [*] Value being casted is already an integer | ^^^^^^^^^^^^^^^^^^^ RUF046 11 | int(int('')) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 7 7 | int(id()) @@ -93,7 +93,7 @@ RUF046.py:11:1: RUF046 [*] Value being casted is already an integer 12 | 13 | int(math.comb()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 8 8 | int(len([])) @@ -114,7 +114,7 @@ RUF046.py:13:1: RUF046 [*] Value being casted is already an integer 14 | int(math.factorial()) 15 | int(math.gcd()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 10 10 | int(hash(foo, bar)) @@ -134,7 +134,7 @@ RUF046.py:14:1: RUF046 [*] Value being casted is already an integer 15 | int(math.gcd()) 16 | int(math.lcm()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 11 11 | int(int('')) @@ -155,7 +155,7 @@ RUF046.py:15:1: RUF046 [*] Value being casted is already an integer 16 | int(math.lcm()) 17 | int(math.isqrt()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 12 12 | @@ -176,7 +176,7 @@ RUF046.py:16:1: RUF046 [*] Value being casted is already an integer 17 | int(math.isqrt()) 18 | int(math.perm()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 13 13 | int(math.comb()) @@ -196,7 +196,7 @@ RUF046.py:17:1: RUF046 [*] Value being casted is already an integer | ^^^^^^^^^^^^^^^^^ RUF046 18 | int(math.perm()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 14 14 | int(math.factorial()) @@ -215,7 +215,7 @@ RUF046.py:18:1: RUF046 [*] Value being casted is already an integer 18 | int(math.perm()) | ^^^^^^^^^^^^^^^^ RUF046 | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Safe fix 15 15 | int(math.gcd()) @@ -236,7 +236,7 @@ RUF046.py:23:1: RUF046 [*] Value being casted is already an integer 24 | int(math.floor()) 25 | int(math.trunc()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Unsafe fix 20 20 | @@ -255,7 +255,7 @@ RUF046.py:24:1: RUF046 [*] Value being casted is already an integer | ^^^^^^^^^^^^^^^^^ RUF046 25 | int(math.trunc()) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Unsafe fix 21 21 | ### Unsafe @@ -274,7 +274,7 @@ RUF046.py:25:1: RUF046 [*] Value being casted is already an integer 25 | int(math.trunc()) | ^^^^^^^^^^^^^^^^^ RUF046 | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Unsafe fix 22 22 | @@ -294,9 +294,9 @@ RUF046.py:31:1: RUF046 [*] Value being casted is already an integer 32 | int(round(0, 0)) 33 | int(round(0, None)) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` -ℹ Unsafe fix +ℹ Safe fix 28 28 | ### `round()` 29 29 | 30 30 | ## Errors @@ -314,9 +314,9 @@ RUF046.py:32:1: RUF046 [*] Value being casted is already an integer | ^^^^^^^^^^^^^^^^ RUF046 33 | int(round(0, None)) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` -ℹ Unsafe fix +ℹ Safe fix 29 29 | 30 30 | ## Errors 31 31 | int(round(0)) @@ -335,9 +335,9 @@ RUF046.py:33:1: RUF046 [*] Value being casted is already an integer 34 | 35 | int(round(0.1)) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` -ℹ Unsafe fix +ℹ Safe fix 30 30 | ## Errors 31 31 | int(round(0)) 32 32 | int(round(0, 0)) @@ -355,7 +355,7 @@ RUF046.py:35:1: RUF046 [*] Value being casted is already an integer | ^^^^^^^^^^^^^^^ RUF046 36 | int(round(0.1, None)) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Unsafe fix 32 32 | int(round(0, 0)) @@ -375,7 +375,7 @@ RUF046.py:36:1: RUF046 [*] Value being casted is already an integer 37 | 38 | # Argument type is not checked | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Unsafe fix 33 33 | int(round(0, None)) @@ -396,7 +396,7 @@ RUF046.py:41:1: RUF046 [*] Value being casted is already an integer 42 | int(round(foo, 0)) 43 | int(round(foo, None)) | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Unsafe fix 38 38 | # Argument type is not checked @@ -417,7 +417,7 @@ RUF046.py:43:1: RUF046 [*] Value being casted is already an integer 44 | 45 | ## No errors | - = help: Remove `int()` wrapper call + = help: Remove unnecessary conversion to `int` ℹ Unsafe fix 40 40 |