[ruff] Handle unary operators in decimal-from-float-literal (RUF032) (#13275)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
@@ -50,6 +50,13 @@ a = 10.0
|
||||
|
||||
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():
|
||||
|
||||
@@ -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 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_arg_float_literal(arg: &ast::Expr) -> bool {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Sign {
|
||||
Positive,
|
||||
Negative,
|
||||
}
|
||||
|
||||
impl Sign {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Positive => "",
|
||||
Self::Negative => "-",
|
||||
}
|
||||
}
|
||||
|
||||
const fn flip(self) -> Self {
|
||||
match self {
|
||||
Self::Negative => Self::Positive,
|
||||
Self::Positive => Self::Negative,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<Float> {
|
||||
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,
|
||||
..
|
||||
}) => true,
|
||||
ast::Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_arg_float_literal(operand),
|
||||
_ => false,
|
||||
}) => extract_float_literal(operand, sign),
|
||||
ast::Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
operand,
|
||||
op: ast::UnaryOp::USub,
|
||||
..
|
||||
}) => 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))
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
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:85:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
RUF032.py:92:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
|
|
||||
83 | val = decimal.Decimal("0.0")
|
||||
84 |
|
||||
85 | val = decimal.Decimal(10.0) # Should error
|
||||
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 |
|
||||
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:89:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
RUF032.py:96:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
|
|
||||
87 | val = decimal.Decimal("10.0")
|
||||
88 |
|
||||
89 | val = decimal.Decimal(-10.0) # Should error
|
||||
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 |
|
||||
|
||||
Reference in New Issue
Block a user