Skip to content

Commit

Permalink
[ruff] Unnecessary rounding (RUF057) (#14828)
Browse files Browse the repository at this point in the history
Co-authored-by: Micha Reiser <micha@reiser.io>
  • Loading branch information
InSyncWithFoo and MichaReiser authored Jan 2, 2025
1 parent f8c9665 commit 89ea037
Show file tree
Hide file tree
Showing 9 changed files with 663 additions and 70 deletions.
59 changes: 59 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF057.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
inferred_int = 1
inferred_float = 1.



round(42) # Error (safe)
round(42, None) # Error (safe)
round(42, 2) # Error (safe)
round(42, inferred_int) # Error (safe)
round(42, 3 + 4) # Error (safe)
round(42, foo) # Error (unsafe)


round(42.) # No error
round(42., None) # No error
round(42., 2) # No error
round(42., inferred_int) # No error
round(42., 3 + 4) # No error
round(42., foo) # No error


round(4 + 2) # Error (safe)
round(4 + 2, None) # Error (safe)
round(4 + 2, 2) # Error (safe)
round(4 + 2, inferred_int) # Error (safe)
round(4 + 2, 3 + 4) # Error (safe)
round(4 + 2, foo) # Error (unsafe)


round(4. + 2.) # No error
round(4. + 2., None) # No error
round(4. + 2., 2) # No error
round(4. + 2., inferred_int) # No error
round(4. + 2., 3 + 4) # No error
round(4. + 2., foo) # No error


round(inferred_int) # Error (unsafe)
round(inferred_int, None) # Error (unsafe)
round(inferred_int, 2) # Error (unsafe)
round(inferred_int, inferred_int) # Error (unsafe)
round(inferred_int, 3 + 4) # Error (unsafe)
round(inferred_int, foo) # No error


round(inferred_float) # No error
round(inferred_float, None) # No error
round(inferred_float, 2) # No error
round(inferred_float, inferred_int) # No error
round(inferred_float, 3 + 4) # No error
round(inferred_float, foo) # No error


round(lorem) # No error
round(lorem, None) # No error
round(lorem, 2) # No error
round(lorem, inferred_int) # No error
round(lorem, 3 + 4) # No error
round(lorem, foo) # No error
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 @@ -1114,6 +1114,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::FalsyDictGetFallback) {
ruff::rules::falsy_dict_get_fallback(checker, expr);
}
if checker.enabled(Rule::UnnecessaryRound) {
ruff::rules::unnecessary_round(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 @@ -992,6 +992,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),

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 @@ -419,6 +419,7 @@ mod tests {
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))]
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))]
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
#[test_case(Rule::UnnecessaryRound, Path::new("RUF057.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 @@ -38,6 +38,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::*;
pub(crate) use unraw_re_pattern::*;
pub(crate) use unsafe_markup_use::*;
pub(crate) use unused_async::*;
Expand Down Expand Up @@ -90,6 +91,7 @@ mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;
mod unnecessary_nested_literal;
mod unnecessary_regular_expression;
mod unnecessary_round;
mod unraw_re_pattern;
mod unsafe_markup_use;
mod unused_async;
Expand Down
121 changes: 51 additions & 70 deletions crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::unnecessary_round::{
rounded_and_ndigits, InferredType, NdigitsValue, RoundedValue,
};
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_ast::{Arguments, Expr, ExprCall};
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;

Expand Down Expand Up @@ -76,12 +78,13 @@ pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) {

let fix = unwrap_int_expression(checker, call, argument, applicability);
let diagnostic = Diagnostic::new(UnnecessaryCastToInt, call.range);

checker.diagnostics.push(diagnostic.with_fix(fix));
}

/// Creates a fix that replaces `int(expression)` with `expression`.
fn unwrap_int_expression(
checker: &mut Checker,
checker: &Checker,
call: &ExprCall,
argument: &Expr,
applicability: Applicability,
Expand Down Expand Up @@ -148,78 +151,56 @@ fn single_argument_to_int_call<'a>(
Some(argument)
}

/// The type of the first argument to `round()`
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Rounded {
InferredInt,
InferredFloat,
LiteralInt,
LiteralFloat,
Other,
}

/// The type of the second argument to `round()`
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Ndigits {
NotGiven,
LiteralInt,
LiteralNone,
Other,
}

/// Determines the [`Applicability`] for a `round(..)` call.
///
/// The Applicability depends on the `ndigits` and the number argument.
fn round_applicability(checker: &Checker, arguments: &Arguments) -> Option<Applicability> {
if arguments.len() > 2 {
return None;
}

let number = arguments.find_argument_value("number", 0)?;
let ndigits = arguments.find_argument_value("ndigits", 1);

let number_kind = match number {
Expr::Name(name) => {
let semantic = checker.semantic();

match semantic.only_binding(name).map(|id| semantic.binding(id)) {
Some(binding) if typing::is_int(binding, semantic) => Rounded::InferredInt,
Some(binding) if typing::is_float(binding, semantic) => Rounded::InferredFloat,
_ => Rounded::Other,
}
}

Expr::NumberLiteral(ExprNumberLiteral { value, .. }) => match value {
Number::Int(..) => Rounded::LiteralInt,
Number::Float(..) => Rounded::LiteralFloat,
Number::Complex { .. } => Rounded::Other,
},

_ => Rounded::Other,
};

let ndigits_kind = match ndigits {
None => Ndigits::NotGiven,
Some(Expr::NoneLiteral(_)) => Ndigits::LiteralNone,

Some(Expr::NumberLiteral(ExprNumberLiteral {
value: Number::Int(..),
..
})) => Ndigits::LiteralInt,

_ => Ndigits::Other,
};

match (number_kind, ndigits_kind) {
(Rounded::LiteralInt, Ndigits::LiteralInt)
| (Rounded::LiteralInt | Rounded::LiteralFloat, Ndigits::NotGiven | Ndigits::LiteralNone) => {
Some(Applicability::Safe)
}
let (_rounded, rounded_value, ndigits_value) = rounded_and_ndigits(checker, arguments)?;

match (rounded_value, ndigits_value) {
// ```python
// int(round(2, 0))
// int(round(2))
// int(round(2, None))
// ```
(
RoundedValue::Int(InferredType::Equivalent),
NdigitsValue::Int(InferredType::Equivalent)
| NdigitsValue::NotGiven
| NdigitsValue::LiteralNone,
) => Some(Applicability::Safe),

// ```python
// int(round(2.0))
// int(round(2.0, None))
// ```
(
RoundedValue::Float(InferredType::Equivalent),
NdigitsValue::NotGiven | NdigitsValue::LiteralNone,
) => Some(Applicability::Safe),

// ```python
// a: int = 2 # or True
// int(round(a, 1))
// int(round(a))
// int(round(a, None))
// ```
(
RoundedValue::Int(InferredType::AssignableTo),
NdigitsValue::Int(InferredType::Equivalent)
| NdigitsValue::NotGiven
| NdigitsValue::LiteralNone,
) => Some(Applicability::Unsafe),

(Rounded::InferredInt, Ndigits::LiteralInt)
| (
Rounded::InferredInt | Rounded::InferredFloat | Rounded::Other,
Ndigits::NotGiven | Ndigits::LiteralNone,
// ```python
// int(round(2.0))
// int(round(2.0, None))
// int(round(x))
// int(round(x, None))
// ```
(
RoundedValue::Float(InferredType::AssignableTo) | RoundedValue::Other,
NdigitsValue::NotGiven | NdigitsValue::LiteralNone,
) => Some(Applicability::Unsafe),

_ => None,
Expand Down
Loading

0 comments on commit 89ea037

Please sign in to comment.