From 18ffb330267b0d92e71861bbfbc3fcc1c9add806 Mon Sep 17 00:00:00 2001 From: InSyncWithFoo Date: Sun, 1 Dec 2024 05:18:41 +0000 Subject: [PATCH] [`ruff`] Unnecessary `round()` cast (`RUF046`) --- .../resources/test/fixtures/ruff/RUF046.py | 22 +++ .../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_round_cast.rs | 120 ++++++++++++ ...uff__tests__preview__RUF046_RUF046.py.snap | 181 ++++++++++++++++++ ruff.schema.json | 1 + 8 files changed, 331 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/unnecessary_round_cast.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 00000000000000..dc1d1a7c71f1c9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py @@ -0,0 +1,22 @@ +### 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 304e142ba9ff00..a14bbb124845d9 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1084,6 +1084,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::UnnecessaryRegularExpression) { ruff::rules::unnecessary_regular_expression(checker, call); } + if checker.enabled(Rule::UnnecessaryRoundCast) { + ruff::rules::unnecessary_round_cast(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 dc28b554894463..bdf87e17b497b4 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::UnnecessaryRoundCast), (Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing), (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 870cd5f3eb53a6..dd7504ed9d3914 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -410,6 +410,7 @@ mod tests { #[test_case(Rule::UnrawRePattern, Path::new("RUF039.py"))] #[test_case(Rule::UnrawRePattern, Path::new("RUF039_concat.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055.py"))] + #[test_case(Rule::UnnecessaryRoundCast, 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 a53994e3c928e2..20276883959c4f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -34,6 +34,7 @@ pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_key_check::*; pub(crate) use unnecessary_nested_literal::*; pub(crate) use unnecessary_regular_expression::*; +pub(crate) use unnecessary_round_cast::*; pub(crate) use unraw_re_pattern::*; pub(crate) use unsafe_markup_use::*; pub(crate) use unused_async::*; @@ -81,6 +82,7 @@ mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_key_check; mod unnecessary_nested_literal; mod unnecessary_regular_expression; +mod unnecessary_round_cast; mod unraw_re_pattern; mod unsafe_markup_use; mod unused_async; diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round_cast.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round_cast.rs new file mode 100644 index 00000000000000..28abbce36990eb --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round_cast.rs @@ -0,0 +1,120 @@ +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::{Expr, ExprCall, ExprNumberLiteral, Number}; +use ruff_python_semantic::SemanticModel; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `int` conversions of `round()` calls +/// with the second argument being either omitted, `0` or `None`. +/// +/// ## Why is this bad? +/// Such a `round()` call already returns an integer, +/// so calling `int()` is unnecessary. +/// Additionally, the second argument can be omitted if it is `0` or `None`. +/// +/// ## Known problems +/// This rule is prone to false positives due to type inference limitations. +/// +/// ## Example +/// +/// ```python +/// int(round(foo, 0)) +/// ``` +/// +/// Use instead: +/// +/// ```python +/// round(foo) +/// ``` +#[derive(ViolationMetadata)] +pub(crate) struct UnnecessaryRoundCast; + +impl AlwaysFixableViolation for UnnecessaryRoundCast { + #[derive_message_formats] + fn message(&self) -> String { + "The result of `round()` is already an integer".to_string() + } + + fn fix_title(&self) -> String { + "Replace with `round(...)`".to_string() + } +} + +/// RUF046 +pub(crate) fn unnecessary_round_cast(checker: &mut Checker, call: &ExprCall) { + let semantic = checker.semantic(); + + let Some(argument) = single_argument_to_int_call(semantic, call) else { + return; + }; + + let Some(argument) = first_argument_to_round_to_int_call(semantic, argument) else { + return; + }; + + let new = format!("round({})", checker.locator().slice(argument)); + let edit = Edit::range_replacement(new, call.range); + let fix = Fix::safe_edit(edit); + + let diagnostic = Diagnostic::new(UnnecessaryRoundCast, call.range); + + 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 first_argument_to_round_to_int_call<'a>( + semantic: &SemanticModel, + expr: &'a Expr, +) -> Option<&'a Expr> { + let Expr::Call(ExprCall { + func, arguments, .. + }) = expr + else { + return None; + }; + + if !semantic.match_builtin_expr(func, "round") { + return None; + } + + if arguments.len() != 1 && arguments.len() != 2 { + return None; + } + + let number = arguments.find_argument("number", 0)?; + let Some(ndigits) = arguments.find_argument("ndigits", 1) else { + return Some(number); + }; + + match ndigits { + Expr::NumberLiteral(ExprNumberLiteral { value, .. }) => { + matches!(value, Number::Int(..)).then_some(number) + } + Expr::NoneLiteral(_) => Some(number), + _ => None, + } +} 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 00000000000000..7c25a0d051b207 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -0,0 +1,181 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF046.py:2:1: RUF046 [*] The result of `round()` is already an integer + | +1 | ### Errors +2 | int(round(0)) + | ^^^^^^^^^^^^^ RUF046 +3 | int(round(0, 0)) +4 | int(round(0, None)) + | + = help: Replace with `round(...)` + +ℹ Safe fix +1 1 | ### Errors +2 |-int(round(0)) + 2 |+round(0) +3 3 | int(round(0, 0)) +4 4 | int(round(0, None)) +5 5 | + +RUF046.py:3:1: RUF046 [*] The result of `round()` is already an integer + | +1 | ### Errors +2 | int(round(0)) +3 | int(round(0, 0)) + | ^^^^^^^^^^^^^^^^ RUF046 +4 | int(round(0, None)) + | + = help: Replace with `round(...)` + +ℹ Safe fix +1 1 | ### Errors +2 2 | int(round(0)) +3 |-int(round(0, 0)) + 3 |+round(0) +4 4 | int(round(0, None)) +5 5 | +6 6 | int(round(0.1)) + +RUF046.py:4:1: RUF046 [*] The result of `round()` is already an integer + | +2 | int(round(0)) +3 | int(round(0, 0)) +4 | int(round(0, None)) + | ^^^^^^^^^^^^^^^^^^^ RUF046 +5 | +6 | int(round(0.1)) + | + = help: Replace with `round(...)` + +ℹ Safe fix +1 1 | ### Errors +2 2 | int(round(0)) +3 3 | int(round(0, 0)) +4 |-int(round(0, None)) + 4 |+round(0) +5 5 | +6 6 | int(round(0.1)) +7 7 | int(round(0.1, 0)) + +RUF046.py:6:1: RUF046 [*] The result of `round()` is already an integer + | +4 | int(round(0, None)) +5 | +6 | int(round(0.1)) + | ^^^^^^^^^^^^^^^ RUF046 +7 | int(round(0.1, 0)) +8 | int(round(0.1, None)) + | + = help: Replace with `round(...)` + +ℹ Safe fix +3 3 | int(round(0, 0)) +4 4 | int(round(0, None)) +5 5 | +6 |-int(round(0.1)) + 6 |+round(0.1) +7 7 | int(round(0.1, 0)) +8 8 | int(round(0.1, None)) +9 9 | + +RUF046.py:7:1: RUF046 [*] The result of `round()` is already an integer + | +6 | int(round(0.1)) +7 | int(round(0.1, 0)) + | ^^^^^^^^^^^^^^^^^^ RUF046 +8 | int(round(0.1, None)) + | + = help: Replace with `round(...)` + +ℹ Safe fix +4 4 | int(round(0, None)) +5 5 | +6 6 | int(round(0.1)) +7 |-int(round(0.1, 0)) + 7 |+round(0.1) +8 8 | int(round(0.1, None)) +9 9 | +10 10 | # Argument type is not checked + +RUF046.py:8:1: RUF046 [*] The result of `round()` is already an integer + | + 6 | int(round(0.1)) + 7 | int(round(0.1, 0)) + 8 | int(round(0.1, None)) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 + 9 | +10 | # Argument type is not checked + | + = help: Replace with `round(...)` + +ℹ Safe fix +5 5 | +6 6 | int(round(0.1)) +7 7 | int(round(0.1, 0)) +8 |-int(round(0.1, None)) + 8 |+round(0.1) +9 9 | +10 10 | # Argument type is not checked +11 11 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() + +RUF046.py:13:1: RUF046 [*] The result of `round()` is already an integer + | +11 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +12 | +13 | int(round(foo)) + | ^^^^^^^^^^^^^^^ RUF046 +14 | int(round(foo, 0)) +15 | int(round(foo, None)) + | + = help: Replace with `round(...)` + +ℹ Safe fix +10 10 | # Argument type is not checked +11 11 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +12 12 | +13 |-int(round(foo)) + 13 |+round(foo) +14 14 | int(round(foo, 0)) +15 15 | int(round(foo, None)) +16 16 | + +RUF046.py:14:1: RUF046 [*] The result of `round()` is already an integer + | +13 | int(round(foo)) +14 | int(round(foo, 0)) + | ^^^^^^^^^^^^^^^^^^ RUF046 +15 | int(round(foo, None)) + | + = help: Replace with `round(...)` + +ℹ Safe fix +11 11 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() +12 12 | +13 13 | int(round(foo)) +14 |-int(round(foo, 0)) + 14 |+round(foo) +15 15 | int(round(foo, None)) +16 16 | +17 17 | + +RUF046.py:15:1: RUF046 [*] The result of `round()` is already an integer + | +13 | int(round(foo)) +14 | int(round(foo, 0)) +15 | int(round(foo, None)) + | ^^^^^^^^^^^^^^^^^^^^^ RUF046 + | + = help: Replace with `round(...)` + +ℹ Safe fix +12 12 | +13 13 | int(round(foo)) +14 14 | int(round(foo, 0)) +15 |-int(round(foo, None)) + 15 |+round(foo) +16 16 | +17 17 | +18 18 | ### No errors diff --git a/ruff.schema.json b/ruff.schema.json index 40cdf7e96d4a9c..965f90402ff730 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3842,6 +3842,7 @@ "RUF04", "RUF040", "RUF041", + "RUF046", "RUF048", "RUF05", "RUF055",