Skip to content

Commit 57dd653

Browse files
committed
fix
1 parent e196c2a commit 57dd653

File tree

7 files changed

+121
-0
lines changed

7 files changed

+121
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
a = 1.0
2+
b = 2.0
3+
x = 99
4+
5+
if a is None: ...
6+
if a == 1: ...
7+
if a != b: ...
8+
if a > 1.0: ...
9+
10+
assert x == 0.0
11+
print(3.14 != a)
12+
if x == 0.3: ...
13+
if x == 0.42: ...
14+
15+
def foo(a, b):
16+
return a == b - 0.1
17+
18+
print(x == 3.0 == 3.0)

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
16321632
if checker.is_rule_enabled(Rule::YodaConditions) {
16331633
flake8_simplify::rules::yoda_conditions(checker, expr, left, ops, comparators);
16341634
}
1635+
if checker.is_rule_enabled(Rule::FloatComparison) {
1636+
ruff::rules::float_comparison(checker, compare);
1637+
}
16351638
if checker.is_rule_enabled(Rule::PandasNuniqueConstantSeriesCheck) {
16361639
pandas_vet::rules::nunique_constant_series_check(
16371640
checker,

crates/ruff_linter/src/codes.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
10591059
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
10601060
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
10611061

1062+
(Ruff, "067") => rules::ruff::rules::FloatComparison,
1063+
10621064
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
10631065
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,
10641066
(Ruff, "102") => rules::ruff::rules::InvalidRuleCode,

crates/ruff_linter/src/rules/ruff/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ mod tests {
565565
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_3.py"))]
566566
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
567567
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
568+
#[test_case(Rule::FloatComparison, Path::new("RUF067.py"))]
568569
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
569570
let snapshot = format!(
570571
"preview__{}_{}",
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use itertools::Itertools;
2+
use ruff_macros::{ViolationMetadata, derive_message_formats};
3+
use ruff_python_ast::{self as ast, CmpOp, Expr};
4+
use ruff_text_size::{Ranged, TextRange};
5+
6+
use crate::checkers::ast::Checker;
7+
use crate::{FixAvailability, Violation};
8+
9+
/// ## What it does
10+
/// Checks for comparisons between floating-point values using `==` or `!=`.
11+
///
12+
/// ## Why is this bad?
13+
/// Directly comparing floats can produce unreliable results due to the
14+
/// inherent imprecision of floating-point arithmetic.
15+
///
16+
/// ## When to use `math.isclose()` vs `numpy.isclose()`
17+
///
18+
/// **Use `math.isclose()` for scalar values:**
19+
/// - Comparing individual float numbers
20+
/// - Working with regular Python variables (not arrays)
21+
/// - When you need a single `True`/`False` result
22+
///
23+
/// **Use `numpy.isclose()` for array-like objects:**
24+
/// - Comparing `pandas` Series, `numpy` arrays, or other vectorized objects
25+
/// - When you need element-wise comparison of arrays
26+
/// - Working in data science contexts with vectorized operations
27+
///
28+
/// ## Example
29+
/// ```python
30+
/// assert 0.1 + 0.2 == 0.3 # AssertionError
31+
/// ```
32+
/// Use instead:
33+
/// ```python
34+
/// import math
35+
///
36+
/// # Scalar comparison
37+
/// assert math.isclose(0.1 + 0.2, 0.3, abs_tol=1e-9)
38+
/// ```
39+
/// ## References
40+
/// - [Python documentation: `math.isclose`](https://docs.python.org/3/library/math.html#math.isclose)
41+
/// - [NumPy documentation: `numpy.isclose`](https://numpy.org/doc/stable/reference/generated/numpy.isclose.html#numpy-isclose)
42+
#[derive(ViolationMetadata)]
43+
#[violation_metadata(preview_since = "0.14.3")]
44+
pub(crate) struct FloatComparison {
45+
pub left: String,
46+
pub right: String,
47+
pub operand: String,
48+
}
49+
50+
impl Violation for FloatComparison {
51+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
52+
53+
#[derive_message_formats]
54+
fn message(&self) -> String {
55+
format!(
56+
"Comparison `{} {} {}` should be replaced by `math.isclose()` or `numpy.isclose()`",
57+
self.left, self.operand, self.right,
58+
)
59+
}
60+
}
61+
62+
/// RUF067
63+
pub(crate) fn float_comparison(checker: &Checker, compare: &ast::ExprCompare) {
64+
let locator = checker.locator();
65+
66+
for (left, right, operand) in std::iter::once(&*compare.left)
67+
.chain(&compare.comparators)
68+
.tuple_windows()
69+
.zip(&compare.ops)
70+
.filter(|(_, op)| matches!(op, CmpOp::Eq | CmpOp::NotEq))
71+
.filter(|((left, right), _)| has_float(left) || has_float(right))
72+
.map(|((left, right), op)| (left, right, op))
73+
{
74+
checker.report_diagnostic(
75+
FloatComparison {
76+
left: locator.slice(left.range()).to_string(),
77+
right: locator.slice(right.range()).to_string(),
78+
operand: operand.to_string(),
79+
},
80+
TextRange::new(left.start(), right.end()),
81+
);
82+
}
83+
}
84+
85+
fn has_float(expr: &Expr) -> bool {
86+
match expr {
87+
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
88+
matches!(value, ast::Number::Float(_))
89+
}
90+
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => has_float(left) || has_float(right),
91+
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => has_float(operand),
92+
_ => false,
93+
}
94+
}

crates/ruff_linter/src/rules/ruff/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub(crate) use decimal_from_float_literal::*;
1010
pub(crate) use default_factory_kwarg::*;
1111
pub(crate) use explicit_f_string_type_conversion::*;
1212
pub(crate) use falsy_dict_get_fallback::*;
13+
pub(crate) use float_comparison::*;
1314
pub(crate) use function_call_in_dataclass_default::*;
1415
pub(crate) use if_key_in_dict_del::*;
1516
pub(crate) use implicit_classvar_in_dataclass::*;
@@ -74,6 +75,7 @@ mod decimal_from_float_literal;
7475
mod default_factory_kwarg;
7576
mod explicit_f_string_type_conversion;
7677
mod falsy_dict_get_fallback;
78+
mod float_comparison;
7779
mod function_call_in_dataclass_default;
7880
mod if_key_in_dict_del;
7981
mod implicit_classvar_in_dataclass;

ruff.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)