diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF032.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF032.py index 4b2146aace2bf..59afec033c23d 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF032.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF032.py @@ -50,6 +50,13 @@ val = Decimal(a) +# See https://github.com/astral-sh/ruff/issues/13258 +val = Decimal(~4.0) # Skip + +val = Decimal(++4.0) # Suggest `Decimal("4.0")` + +val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")` + # Tests with shadowed name class Decimal(): diff --git a/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs b/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs index 3f7120d92359a..b02b731a3e49c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs @@ -1,7 +1,10 @@ +use std::fmt; + use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::{self as ast}; +use ruff_python_ast as ast; use ruff_python_codegen::Stylist; +use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -49,37 +52,90 @@ pub(crate) fn decimal_from_float_literal_syntax(checker: &mut Checker, call: &as return; }; - if !is_arg_float_literal(arg) { - return; + if let Some(float) = extract_float_literal(arg, Sign::Positive) { + if checker + .semantic() + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["decimal", "Decimal"]) + }) + { + let diagnostic = Diagnostic::new(DecimalFromFloatLiteral, arg.range()).with_fix( + fix_float_literal(arg.range(), float, checker.locator(), checker.stylist()), + ); + checker.diagnostics.push(diagnostic); + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Sign { + Positive, + Negative, +} + +impl Sign { + const fn as_str(self) -> &'static str { + match self { + Self::Positive => "", + Self::Negative => "-", + } } - if checker - .semantic() - .resolve_qualified_name(call.func.as_ref()) - .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["decimal", "Decimal"])) - { - let diagnostic = - Diagnostic::new(DecimalFromFloatLiteral, arg.range()).with_fix(fix_float_literal( - arg.range(), - &checker.generator().expr(arg), - checker.stylist(), - )); - checker.diagnostics.push(diagnostic); + const fn flip(self) -> Self { + match self { + Self::Negative => Self::Positive, + Self::Positive => Self::Negative, + } } } -fn is_arg_float_literal(arg: &ast::Expr) -> bool { +impl fmt::Display for Sign { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Debug, Copy, Clone)] +struct Float { + /// The range of the float excluding the sign. + /// E.g. for `+--+-+-4.3`, this will be the range of `4.3` + value_range: TextRange, + /// The resolved sign of the float (either `-` or `+`) + sign: Sign, +} + +fn extract_float_literal(arg: &ast::Expr, sign: Sign) -> Option { match arg { - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Float(_), + ast::Expr::NumberLiteral(number_literal_expr) if number_literal_expr.value.is_float() => { + Some(Float { + value_range: arg.range(), + sign, + }) + } + ast::Expr::UnaryOp(ast::ExprUnaryOp { + operand, + op: ast::UnaryOp::UAdd, + .. + }) => extract_float_literal(operand, sign), + ast::Expr::UnaryOp(ast::ExprUnaryOp { + operand, + op: ast::UnaryOp::USub, .. - }) => true, - ast::Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_arg_float_literal(operand), - _ => false, + }) => extract_float_literal(operand, sign.flip()), + _ => None, } } -fn fix_float_literal(range: TextRange, float_literal: &str, stylist: &Stylist) -> Fix { - let content = format!("{quote}{float_literal}{quote}", quote = stylist.quote()); - Fix::unsafe_edit(Edit::range_replacement(content, range)) +fn fix_float_literal( + original_range: TextRange, + float: Float, + locator: &Locator, + stylist: &Stylist, +) -> Fix { + let quote = stylist.quote(); + let Float { value_range, sign } = float; + let float_value = locator.slice(value_range); + let content = format!("{quote}{sign}{float_value}{quote}"); + Fix::unsafe_edit(Edit::range_replacement(content, original_range)) } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap index c21499b3f490f..f170aea8042d3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap @@ -127,65 +127,105 @@ RUF032.py:45:15: RUF032 [*] `Decimal()` called with float literal argument 47 47 | val = Decimal("-10.0") 48 48 | -RUF032.py:81:23: RUF032 [*] `Decimal()` called with float literal argument +RUF032.py:56:15: RUF032 [*] `Decimal()` called with float literal argument | -79 | # Retest with fully qualified import -80 | -81 | val = decimal.Decimal(0.0) # Should error +54 | val = Decimal(~4.0) # Skip +55 | +56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")` + | ^^^^^ RUF032 +57 | +58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")` + | + = help: Use a string literal instead + +ℹ Unsafe fix +53 53 | # See https://github.com/astral-sh/ruff/issues/13258 +54 54 | val = Decimal(~4.0) # Skip +55 55 | +56 |-val = Decimal(++4.0) # Suggest `Decimal("4.0")` + 56 |+val = Decimal("4.0") # Suggest `Decimal("4.0")` +57 57 | +58 58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")` +59 59 | + +RUF032.py:58:15: RUF032 [*] `Decimal()` called with float literal argument + | +56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")` +57 | +58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")` + | ^^^^^^^^^^^ RUF032 + | + = help: Use a string literal instead + +ℹ Unsafe fix +55 55 | +56 56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")` +57 57 | +58 |-val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")` + 58 |+val = Decimal("-4.0") # Suggest `Decimal("-4.0")` +59 59 | +60 60 | +61 61 | # Tests with shadowed name + +RUF032.py:88:23: RUF032 [*] `Decimal()` called with float literal argument + | +86 | # Retest with fully qualified import +87 | +88 | val = decimal.Decimal(0.0) # Should error | ^^^ RUF032 -82 | -83 | val = decimal.Decimal("0.0") +89 | +90 | val = decimal.Decimal("0.0") | = help: Use a string literal instead ℹ Unsafe fix -78 78 | -79 79 | # Retest with fully qualified import -80 80 | -81 |-val = decimal.Decimal(0.0) # Should error - 81 |+val = decimal.Decimal("0.0") # Should error -82 82 | -83 83 | val = decimal.Decimal("0.0") -84 84 | - -RUF032.py:85:23: RUF032 [*] `Decimal()` called with float literal argument - | -83 | val = decimal.Decimal("0.0") -84 | -85 | val = decimal.Decimal(10.0) # Should error +85 85 | +86 86 | # Retest with fully qualified import +87 87 | +88 |-val = decimal.Decimal(0.0) # Should error + 88 |+val = decimal.Decimal("0.0") # Should error +89 89 | +90 90 | val = decimal.Decimal("0.0") +91 91 | + +RUF032.py:92:23: RUF032 [*] `Decimal()` called with float literal argument + | +90 | val = decimal.Decimal("0.0") +91 | +92 | val = decimal.Decimal(10.0) # Should error | ^^^^ RUF032 -86 | -87 | val = decimal.Decimal("10.0") +93 | +94 | val = decimal.Decimal("10.0") | = help: Use a string literal instead ℹ Unsafe fix -82 82 | -83 83 | val = decimal.Decimal("0.0") -84 84 | -85 |-val = decimal.Decimal(10.0) # Should error - 85 |+val = decimal.Decimal("10.0") # Should error -86 86 | -87 87 | val = decimal.Decimal("10.0") -88 88 | - -RUF032.py:89:23: RUF032 [*] `Decimal()` called with float literal argument - | -87 | val = decimal.Decimal("10.0") -88 | -89 | val = decimal.Decimal(-10.0) # Should error +89 89 | +90 90 | val = decimal.Decimal("0.0") +91 91 | +92 |-val = decimal.Decimal(10.0) # Should error + 92 |+val = decimal.Decimal("10.0") # Should error +93 93 | +94 94 | val = decimal.Decimal("10.0") +95 95 | + +RUF032.py:96:23: RUF032 [*] `Decimal()` called with float literal argument + | +94 | val = decimal.Decimal("10.0") +95 | +96 | val = decimal.Decimal(-10.0) # Should error | ^^^^^ RUF032 -90 | -91 | val = decimal.Decimal("-10.0") +97 | +98 | val = decimal.Decimal("-10.0") | = help: Use a string literal instead ℹ Unsafe fix -86 86 | -87 87 | val = decimal.Decimal("10.0") -88 88 | -89 |-val = decimal.Decimal(-10.0) # Should error - 89 |+val = decimal.Decimal("-10.0") # Should error -90 90 | -91 91 | val = decimal.Decimal("-10.0") -92 92 | +93 93 | +94 94 | val = decimal.Decimal("10.0") +95 95 | +96 |-val = decimal.Decimal(-10.0) # Should error + 96 |+val = decimal.Decimal("-10.0") # Should error +97 97 | +98 98 | val = decimal.Decimal("-10.0") +99 99 |