Skip to content

Commit

Permalink
[ruff] Unnecessary round() cast (RUF046)
Browse files Browse the repository at this point in the history
  • Loading branch information
InSyncWithFoo committed Dec 1, 2024
1 parent 84748be commit 18ffb33
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 0 deletions.
22 changes: 22 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py
Original file line number Diff line number Diff line change
@@ -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))
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&[
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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__{}_{}",
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/ruff/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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;
Expand Down
120 changes: 120 additions & 0 deletions crates/ruff_linter/src/rules/ruff/rules/unnecessary_round_cast.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 18ffb33

Please sign in to comment.