mirror of https://github.com/astral-sh/ruff
[ruff] Implement check for Decimal called with a float literal (RUF032) (#12909)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
65de8f2c9b
commit
f4c8c7eb70
|
|
@ -0,0 +1,120 @@
|
||||||
|
import decimal
|
||||||
|
|
||||||
|
# Tests with fully qualified import
|
||||||
|
decimal.Decimal(0)
|
||||||
|
|
||||||
|
decimal.Decimal(0.0) # Should error
|
||||||
|
|
||||||
|
decimal.Decimal("0.0")
|
||||||
|
|
||||||
|
decimal.Decimal(10)
|
||||||
|
|
||||||
|
decimal.Decimal(10.0) # Should error
|
||||||
|
|
||||||
|
decimal.Decimal("10.0")
|
||||||
|
|
||||||
|
decimal.Decimal(-10)
|
||||||
|
|
||||||
|
decimal.Decimal(-10.0) # Should error
|
||||||
|
|
||||||
|
decimal.Decimal("-10.0")
|
||||||
|
|
||||||
|
a = 10.0
|
||||||
|
|
||||||
|
decimal.Decimal(a)
|
||||||
|
|
||||||
|
|
||||||
|
# Tests with relative import
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
|
val = Decimal(0)
|
||||||
|
|
||||||
|
val = Decimal(0.0) # Should error
|
||||||
|
|
||||||
|
val = Decimal("0.0")
|
||||||
|
|
||||||
|
val = Decimal(10)
|
||||||
|
|
||||||
|
val = Decimal(10.0) # Should error
|
||||||
|
|
||||||
|
val = Decimal("10.0")
|
||||||
|
|
||||||
|
val = Decimal(-10)
|
||||||
|
|
||||||
|
val = Decimal(-10.0) # Should error
|
||||||
|
|
||||||
|
val = Decimal("-10.0")
|
||||||
|
|
||||||
|
a = 10.0
|
||||||
|
|
||||||
|
val = Decimal(a)
|
||||||
|
|
||||||
|
|
||||||
|
# Tests with shadowed name
|
||||||
|
class Decimal():
|
||||||
|
value: float | int | str
|
||||||
|
|
||||||
|
def __init__(self, value: float | int | str) -> None:
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
|
val = Decimal(0.0)
|
||||||
|
|
||||||
|
val = Decimal("0.0")
|
||||||
|
|
||||||
|
val = Decimal(10.0)
|
||||||
|
|
||||||
|
val = Decimal("10.0")
|
||||||
|
|
||||||
|
val = Decimal(-10.0)
|
||||||
|
|
||||||
|
val = Decimal("-10.0")
|
||||||
|
|
||||||
|
a = 10.0
|
||||||
|
|
||||||
|
val = Decimal(a)
|
||||||
|
|
||||||
|
|
||||||
|
# Retest with fully qualified import
|
||||||
|
|
||||||
|
val = decimal.Decimal(0.0) # Should error
|
||||||
|
|
||||||
|
val = decimal.Decimal("0.0")
|
||||||
|
|
||||||
|
val = decimal.Decimal(10.0) # Should error
|
||||||
|
|
||||||
|
val = decimal.Decimal("10.0")
|
||||||
|
|
||||||
|
val = decimal.Decimal(-10.0) # Should error
|
||||||
|
|
||||||
|
val = decimal.Decimal("-10.0")
|
||||||
|
|
||||||
|
a = 10.0
|
||||||
|
|
||||||
|
val = decimal.Decimal(a)
|
||||||
|
|
||||||
|
|
||||||
|
class decimal():
|
||||||
|
class Decimal():
|
||||||
|
value: float | int | str
|
||||||
|
|
||||||
|
def __init__(self, value: float | int | str) -> None:
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
|
val = decimal.Decimal(0.0)
|
||||||
|
|
||||||
|
val = decimal.Decimal("0.0")
|
||||||
|
|
||||||
|
val = decimal.Decimal(10.0)
|
||||||
|
|
||||||
|
val = decimal.Decimal("10.0")
|
||||||
|
|
||||||
|
val = decimal.Decimal(-10.0)
|
||||||
|
|
||||||
|
val = decimal.Decimal("-10.0")
|
||||||
|
|
||||||
|
a = 10.0
|
||||||
|
|
||||||
|
val = decimal.Decimal(a)
|
||||||
|
|
@ -1011,6 +1011,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
|
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
|
||||||
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr);
|
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr);
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::DecimalFromFloatLiteral) {
|
||||||
|
ruff::rules::decimal_from_float_literal_syntax(checker, call);
|
||||||
|
}
|
||||||
if checker.enabled(Rule::IntOnSlicedStr) {
|
if checker.enabled(Rule::IntOnSlicedStr) {
|
||||||
refurb::rules::int_on_sliced_str(checker, call);
|
refurb::rules::int_on_sliced_str(checker, call);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -959,6 +959,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync),
|
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync),
|
||||||
(Ruff, "030") => (RuleGroup::Preview, rules::ruff::rules::AssertWithPrintMessage),
|
(Ruff, "030") => (RuleGroup::Preview, rules::ruff::rules::AssertWithPrintMessage),
|
||||||
(Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript),
|
(Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript),
|
||||||
|
(Ruff, "032") => (RuleGroup::Preview, rules::ruff::rules::DecimalFromFloatLiteral),
|
||||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ mod tests {
|
||||||
#[test_case(Rule::UnusedAsync, Path::new("RUF029.py"))]
|
#[test_case(Rule::UnusedAsync, Path::new("RUF029.py"))]
|
||||||
#[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))]
|
#[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))]
|
||||||
#[test_case(Rule::IncorrectlyParenthesizedTupleInSubscript, Path::new("RUF031.py"))]
|
#[test_case(Rule::IncorrectlyParenthesizedTupleInSubscript, Path::new("RUF031.py"))]
|
||||||
|
#[test_case(Rule::DecimalFromFloatLiteral, Path::new("RUF032.py"))]
|
||||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
|
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
|
||||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_ast::{self as ast};
|
||||||
|
use ruff_python_codegen::Stylist;
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for `Decimal` calls passing a float literal.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// Float literals have limited precision that can lead to unexpected results.
|
||||||
|
/// The `Decimal` class is designed to handle numbers with fixed-point precision,
|
||||||
|
/// so a string literal should be used instead.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// num = Decimal(1.2345)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// num = Decimal("1.2345")
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Fix Safety
|
||||||
|
/// This rule's fix is marked as unsafe because it changes the underlying value
|
||||||
|
/// of the `Decimal` instance that is constructed. This can lead to unexpected
|
||||||
|
/// behavior if your program relies on the previous value (whether deliberately or not).
|
||||||
|
#[violation]
|
||||||
|
pub struct DecimalFromFloatLiteral;
|
||||||
|
|
||||||
|
impl AlwaysFixableViolation for DecimalFromFloatLiteral {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
format!("`Decimal()` called with float literal argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_title(&self) -> String {
|
||||||
|
"Use a string literal instead".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RUF032: `Decimal()` called with float literal argument
|
||||||
|
pub(crate) fn decimal_from_float_literal_syntax(checker: &mut Checker, call: &ast::ExprCall) {
|
||||||
|
let Some(arg) = call.arguments.args.first() else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_arg_float_literal(arg: &ast::Expr) -> bool {
|
||||||
|
match arg {
|
||||||
|
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||||
|
value: ast::Number::Float(_),
|
||||||
|
..
|
||||||
|
}) => true,
|
||||||
|
ast::Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_arg_float_literal(operand),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ pub(crate) use assert_with_print_message::*;
|
||||||
pub(crate) use assignment_in_assert::*;
|
pub(crate) use assignment_in_assert::*;
|
||||||
pub(crate) use asyncio_dangling_task::*;
|
pub(crate) use asyncio_dangling_task::*;
|
||||||
pub(crate) use collection_literal_concatenation::*;
|
pub(crate) use collection_literal_concatenation::*;
|
||||||
|
pub(crate) use decimal_from_float_literal::*;
|
||||||
pub(crate) use default_factory_kwarg::*;
|
pub(crate) use default_factory_kwarg::*;
|
||||||
pub(crate) use explicit_f_string_type_conversion::*;
|
pub(crate) use explicit_f_string_type_conversion::*;
|
||||||
pub(crate) use function_call_in_dataclass_default::*;
|
pub(crate) use function_call_in_dataclass_default::*;
|
||||||
|
|
@ -36,6 +37,7 @@ mod assignment_in_assert;
|
||||||
mod asyncio_dangling_task;
|
mod asyncio_dangling_task;
|
||||||
mod collection_literal_concatenation;
|
mod collection_literal_concatenation;
|
||||||
mod confusables;
|
mod confusables;
|
||||||
|
mod decimal_from_float_literal;
|
||||||
mod default_factory_kwarg;
|
mod default_factory_kwarg;
|
||||||
mod explicit_f_string_type_conversion;
|
mod explicit_f_string_type_conversion;
|
||||||
mod function_call_in_dataclass_default;
|
mod function_call_in_dataclass_default;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||||
|
---
|
||||||
|
RUF032.py:6:17: RUF032 [*] `Decimal()` called with float literal argument
|
||||||
|
|
|
||||||
|
4 | decimal.Decimal(0)
|
||||||
|
5 |
|
||||||
|
6 | decimal.Decimal(0.0) # Should error
|
||||||
|
| ^^^ RUF032
|
||||||
|
7 |
|
||||||
|
8 | decimal.Decimal("0.0")
|
||||||
|
|
|
||||||
|
= help: Use a string literal instead
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
3 3 | # Tests with fully qualified import
|
||||||
|
4 4 | decimal.Decimal(0)
|
||||||
|
5 5 |
|
||||||
|
6 |-decimal.Decimal(0.0) # Should error
|
||||||
|
6 |+decimal.Decimal("0.0") # Should error
|
||||||
|
7 7 |
|
||||||
|
8 8 | decimal.Decimal("0.0")
|
||||||
|
9 9 |
|
||||||
|
|
||||||
|
RUF032.py:12:17: RUF032 [*] `Decimal()` called with float literal argument
|
||||||
|
|
|
||||||
|
10 | decimal.Decimal(10)
|
||||||
|
11 |
|
||||||
|
12 | decimal.Decimal(10.0) # Should error
|
||||||
|
| ^^^^ RUF032
|
||||||
|
13 |
|
||||||
|
14 | decimal.Decimal("10.0")
|
||||||
|
|
|
||||||
|
= help: Use a string literal instead
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
9 9 |
|
||||||
|
10 10 | decimal.Decimal(10)
|
||||||
|
11 11 |
|
||||||
|
12 |-decimal.Decimal(10.0) # Should error
|
||||||
|
12 |+decimal.Decimal("10.0") # Should error
|
||||||
|
13 13 |
|
||||||
|
14 14 | decimal.Decimal("10.0")
|
||||||
|
15 15 |
|
||||||
|
|
||||||
|
RUF032.py:18:17: RUF032 [*] `Decimal()` called with float literal argument
|
||||||
|
|
|
||||||
|
16 | decimal.Decimal(-10)
|
||||||
|
17 |
|
||||||
|
18 | decimal.Decimal(-10.0) # Should error
|
||||||
|
| ^^^^^ RUF032
|
||||||
|
19 |
|
||||||
|
20 | decimal.Decimal("-10.0")
|
||||||
|
|
|
||||||
|
= help: Use a string literal instead
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
15 15 |
|
||||||
|
16 16 | decimal.Decimal(-10)
|
||||||
|
17 17 |
|
||||||
|
18 |-decimal.Decimal(-10.0) # Should error
|
||||||
|
18 |+decimal.Decimal("-10.0") # Should error
|
||||||
|
19 19 |
|
||||||
|
20 20 | decimal.Decimal("-10.0")
|
||||||
|
21 21 |
|
||||||
|
|
||||||
|
RUF032.py:33:15: RUF032 [*] `Decimal()` called with float literal argument
|
||||||
|
|
|
||||||
|
31 | val = Decimal(0)
|
||||||
|
32 |
|
||||||
|
33 | val = Decimal(0.0) # Should error
|
||||||
|
| ^^^ RUF032
|
||||||
|
34 |
|
||||||
|
35 | val = Decimal("0.0")
|
||||||
|
|
|
||||||
|
= help: Use a string literal instead
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
30 30 |
|
||||||
|
31 31 | val = Decimal(0)
|
||||||
|
32 32 |
|
||||||
|
33 |-val = Decimal(0.0) # Should error
|
||||||
|
33 |+val = Decimal("0.0") # Should error
|
||||||
|
34 34 |
|
||||||
|
35 35 | val = Decimal("0.0")
|
||||||
|
36 36 |
|
||||||
|
|
||||||
|
RUF032.py:39:15: RUF032 [*] `Decimal()` called with float literal argument
|
||||||
|
|
|
||||||
|
37 | val = Decimal(10)
|
||||||
|
38 |
|
||||||
|
39 | val = Decimal(10.0) # Should error
|
||||||
|
| ^^^^ RUF032
|
||||||
|
40 |
|
||||||
|
41 | val = Decimal("10.0")
|
||||||
|
|
|
||||||
|
= help: Use a string literal instead
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
36 36 |
|
||||||
|
37 37 | val = Decimal(10)
|
||||||
|
38 38 |
|
||||||
|
39 |-val = Decimal(10.0) # Should error
|
||||||
|
39 |+val = Decimal("10.0") # Should error
|
||||||
|
40 40 |
|
||||||
|
41 41 | val = Decimal("10.0")
|
||||||
|
42 42 |
|
||||||
|
|
||||||
|
RUF032.py:45:15: RUF032 [*] `Decimal()` called with float literal argument
|
||||||
|
|
|
||||||
|
43 | val = Decimal(-10)
|
||||||
|
44 |
|
||||||
|
45 | val = Decimal(-10.0) # Should error
|
||||||
|
| ^^^^^ RUF032
|
||||||
|
46 |
|
||||||
|
47 | val = Decimal("-10.0")
|
||||||
|
|
|
||||||
|
= help: Use a string literal instead
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
42 42 |
|
||||||
|
43 43 | val = Decimal(-10)
|
||||||
|
44 44 |
|
||||||
|
45 |-val = Decimal(-10.0) # Should error
|
||||||
|
45 |+val = Decimal("-10.0") # Should error
|
||||||
|
46 46 |
|
||||||
|
47 47 | val = Decimal("-10.0")
|
||||||
|
48 48 |
|
||||||
|
|
||||||
|
RUF032.py:81:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||||
|
|
|
||||||
|
79 | # Retest with fully qualified import
|
||||||
|
80 |
|
||||||
|
81 | val = decimal.Decimal(0.0) # Should error
|
||||||
|
| ^^^ RUF032
|
||||||
|
82 |
|
||||||
|
83 | 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
|
||||||
|
| ^^^^ RUF032
|
||||||
|
86 |
|
||||||
|
87 | 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
|
||||||
|
| ^^^^^ RUF032
|
||||||
|
90 |
|
||||||
|
91 | 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 |
|
||||||
|
|
@ -3738,6 +3738,7 @@
|
||||||
"RUF03",
|
"RUF03",
|
||||||
"RUF030",
|
"RUF030",
|
||||||
"RUF031",
|
"RUF031",
|
||||||
|
"RUF032",
|
||||||
"RUF1",
|
"RUF1",
|
||||||
"RUF10",
|
"RUF10",
|
||||||
"RUF100",
|
"RUF100",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue