[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:
Dylan
2024-09-07 08:25:49 -05:00
committed by GitHub
parent c3bcd5c842
commit a7c936878d
3 changed files with 171 additions and 68 deletions

View File

@@ -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():

View File

@@ -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))
}

View File

@@ -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 |