Skip to content

Commit 9039f50

Browse files
committed
float_cmp: Ignore comparisons to detect if an operation changes the value.
1 parent f52e4bb commit 9039f50

File tree

12 files changed

+258
-19
lines changed

12 files changed

+258
-19
lines changed

clippy_config/src/conf.rs

+12
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,18 @@ define_Conf! {
616616
/// }
617617
/// ```
618618
(float_cmp_ignore_constant_comparisons: bool = true),
619+
/// Lint: FLOAT_CMP
620+
///
621+
/// Whether to ignore comparisons which check if an operation changes the value of it's operand.
622+
///
623+
/// #### Example
624+
/// ```no_run
625+
/// fn f(x: f64) -> bool {
626+
/// // Will warn if the config is `false`
627+
/// x == x + 1.0
628+
/// }
629+
/// ```
630+
(float_cmp_ignore_change_detection: bool = true),
619631
}
620632

621633
/// Search for the configuration file.

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
582582
allow_comparison_to_zero,
583583
float_cmp_ignore_named_constants,
584584
float_cmp_ignore_constant_comparisons,
585+
float_cmp_ignore_change_detection,
585586

586587
blacklisted_names: _,
587588
cyclomatic_complexity_threshold: _,
@@ -986,6 +987,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
986987
allow_comparison_to_zero,
987988
float_cmp_ignore_named_constants,
988989
float_cmp_ignore_constant_comparisons,
990+
float_cmp_ignore_change_detection,
989991
))
990992
});
991993
store.register_late_pass(|_| Box::<std_instead_of_core::StdReexports>::default());

clippy_lints/src/operators/float_cmp.rs

+95-15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use clippy_utils::consts::{constant, Constant};
22
use clippy_utils::diagnostics::span_lint_and_then;
33
use clippy_utils::sugg::Sugg;
4-
use clippy_utils::visitors::is_const_evaluatable;
5-
use clippy_utils::{get_item_name, is_expr_named_const, peel_hir_expr_while};
4+
use clippy_utils::visitors::{for_each_expr, is_const_evaluatable};
5+
use clippy_utils::{get_item_name, is_expr_named_const, peel_hir_expr_while, SpanlessEq};
6+
use core::ops::ControlFlow;
67
use rustc_errors::Applicability;
7-
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, UnOp};
8+
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, UnOp, Unsafety};
89
use rustc_lint::LateContext;
9-
use rustc_middle::ty;
10+
use rustc_middle::ty::{self, Ty, TypeFlags, TypeVisitableExt};
1011

1112
use super::{FloatCmpConfig, FLOAT_CMP};
1213

@@ -24,33 +25,40 @@ pub(crate) fn check<'tcx>(
2425
};
2526

2627
if matches!(op, BinOpKind::Eq | BinOpKind::Ne)
27-
&& let left = peel_hir_expr_while(left, peel_expr)
28-
&& let right = peel_hir_expr_while(right, peel_expr)
29-
&& is_float(cx, left)
28+
&& let left_red = peel_hir_expr_while(left, peel_expr)
29+
&& let right_red = peel_hir_expr_while(right, peel_expr)
30+
&& is_float(cx, left_red)
3031
// Don't lint literal comparisons
31-
&& !(matches!(left.kind, ExprKind::Lit(_)) && matches!(right.kind, ExprKind::Lit(_)))
32+
&& !(matches!(left_red.kind, ExprKind::Lit(_)) && matches!(right_red.kind, ExprKind::Lit(_)))
3233
// Allow comparing the results of signum()
33-
&& !(is_signum(cx, left) && is_signum(cx, right))
34+
&& !(is_signum(cx, left_red) && is_signum(cx, right_red))
3435
{
35-
let left_c = constant(cx, cx.typeck_results(), left);
36+
let left_c = constant(cx, cx.typeck_results(), left_red);
3637
let is_left_const = left_c.is_some();
3738
if left_c.is_some_and(|c| is_allowed(&c)) {
3839
return;
3940
}
40-
let right_c = constant(cx, cx.typeck_results(), right);
41+
let right_c = constant(cx, cx.typeck_results(), right_red);
4142
let is_right_const = right_c.is_some();
4243
if right_c.is_some_and(|c| is_allowed(&c)) {
4344
return;
4445
}
4546

4647
if config.ignore_constant_comparisons
47-
&& (is_left_const || is_const_evaluatable(cx, left))
48-
&& (is_right_const || is_const_evaluatable(cx, right))
48+
&& (is_left_const || is_const_evaluatable(cx, left_red))
49+
&& (is_right_const || is_const_evaluatable(cx, right_red))
4950
{
5051
return;
5152
}
5253

53-
if config.ignore_named_constants && (is_expr_named_const(cx, left) || is_expr_named_const(cx, right)) {
54+
if config.ignore_named_constants && (is_expr_named_const(cx, left_red) || is_expr_named_const(cx, right_red)) {
55+
return;
56+
}
57+
58+
if config.ignore_change_detection
59+
&& ((is_pure_expr(cx, left_red) && contains_expr(cx, right, left))
60+
|| (is_pure_expr(cx, right_red) && contains_expr(cx, left, right)))
61+
{
5462
return;
5563
}
5664

@@ -60,7 +68,7 @@ pub(crate) fn check<'tcx>(
6068
return;
6169
}
6270
}
63-
let is_comparing_arrays = is_array(cx, left) || is_array(cx, right);
71+
let is_comparing_arrays = is_array(cx, left_red) || is_array(cx, right_red);
6472
let msg = if is_comparing_arrays {
6573
"strict comparison of `f32` or `f64` arrays"
6674
} else {
@@ -105,6 +113,78 @@ fn is_allowed(val: &Constant<'_>) -> bool {
105113
}
106114
}
107115

116+
// This is a best effort guess and may have false positives and negatives.
117+
fn is_pure_expr<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> bool {
118+
match e.kind {
119+
ExprKind::Path(_) | ExprKind::Lit(_) => true,
120+
ExprKind::Field(e, _) | ExprKind::Cast(e, _) | ExprKind::Repeat(e, _) => is_pure_expr(cx, e),
121+
ExprKind::Tup(args) => args.iter().all(|arg| is_pure_expr(cx, arg)),
122+
ExprKind::Struct(_, fields, base) => {
123+
base.map_or(true, |base| is_pure_expr(cx, base)) && fields.iter().all(|f| is_pure_expr(cx, f.expr))
124+
},
125+
126+
// Since rust doesn't actually have the concept of a pure function we
127+
// have to guess whether it's likely pure from the signature of the
128+
// function.
129+
ExprKind::Unary(_, e) => is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(e)) && is_pure_expr(cx, e),
130+
ExprKind::Binary(_, x, y) | ExprKind::Index(x, y, _) => {
131+
is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(x))
132+
&& is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(y))
133+
&& is_pure_expr(cx, x)
134+
&& is_pure_expr(cx, y)
135+
},
136+
ExprKind::MethodCall(_, recv, args, _) => {
137+
is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(recv))
138+
&& is_pure_expr(cx, recv)
139+
&& cx.typeck_results().type_dependent_def_id(e.hir_id).is_some_and(|did| {
140+
matches!(
141+
cx.tcx.fn_sig(did).skip_binder().skip_binder().unsafety,
142+
Unsafety::Normal
143+
)
144+
})
145+
&& args
146+
.iter()
147+
.all(|arg| is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(arg)) && is_pure_expr(cx, arg))
148+
},
149+
ExprKind::Call(f, args @ [_, ..]) => {
150+
is_pure_expr(cx, f)
151+
&& is_safe_fn(cx, f)
152+
&& args
153+
.iter()
154+
.all(|arg| is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(arg)) && is_pure_expr(cx, arg))
155+
},
156+
157+
_ => false,
158+
}
159+
}
160+
161+
fn is_safe_fn<'tcx>(cx: &LateContext<'tcx>, e: &Expr<'tcx>) -> bool {
162+
let sig = match *cx.typeck_results().expr_ty(e).kind() {
163+
ty::FnDef(did, _) => cx.tcx.fn_sig(did).skip_binder(),
164+
ty::FnPtr(sig) => sig,
165+
_ => return true,
166+
};
167+
matches!(sig.skip_binder().unsafety, Unsafety::Normal)
168+
}
169+
170+
fn is_pure_arg_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
171+
!ty.is_mutable_ptr()
172+
&& ty.is_copy_modulo_regions(cx.tcx, cx.param_env)
173+
&& (ty.peel_refs().is_freeze(cx.tcx, cx.param_env)
174+
|| !ty.has_type_flags(TypeFlags::HAS_FREE_REGIONS | TypeFlags::HAS_RE_ERASED | TypeFlags::HAS_RE_BOUND))
175+
}
176+
177+
fn contains_expr<'tcx>(cx: &LateContext<'tcx>, corpus: &'tcx Expr<'tcx>, e: &'tcx Expr<'tcx>) -> bool {
178+
for_each_expr(corpus, |corpus| {
179+
if SpanlessEq::new(cx).eq_expr(corpus, e) {
180+
ControlFlow::Break(())
181+
} else {
182+
ControlFlow::Continue(())
183+
}
184+
})
185+
.is_some()
186+
}
187+
108188
// Return true if `expr` is the result of `signum()` invoked on a float value.
109189
fn is_signum(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
110190
if let ExprKind::MethodCall(method_name, self_arg, ..) = expr.kind

clippy_lints/src/operators/mod.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -735,9 +735,10 @@ declare_clippy_lint! {
735735

736736
#[expect(clippy::struct_field_names)]
737737
#[derive(Clone, Copy)]
738-
pub struct FloatCmpConfig {
739-
pub ignore_named_constants: bool,
740-
pub ignore_constant_comparisons: bool,
738+
struct FloatCmpConfig {
739+
ignore_named_constants: bool,
740+
ignore_constant_comparisons: bool,
741+
ignore_change_detection: bool,
741742
}
742743

743744
pub struct Operators {
@@ -779,6 +780,7 @@ impl Operators {
779780
modulo_arithmetic_allow_comparison_to_zero: bool,
780781
ignore_named_constants: bool,
781782
ignore_constant_comparisons: bool,
783+
ignore_change_detection: bool,
782784
) -> Self {
783785
Self {
784786
arithmetic_context: numeric_arithmetic::Context::default(),
@@ -787,6 +789,7 @@ impl Operators {
787789
float_cmp_config: FloatCmpConfig {
788790
ignore_named_constants,
789791
ignore_constant_comparisons,
792+
ignore_change_detection,
790793
},
791794
}
792795
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
float-cmp-ignore-change-detection = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//@no-rustfix
2+
3+
#![deny(clippy::float_cmp)]
4+
5+
fn main() {
6+
{
7+
const C: f64 = 1.0;
8+
fn f(x: f64) {
9+
let _ = x == C;
10+
}
11+
}
12+
{
13+
const fn f(x: f64) -> f64 {
14+
todo!()
15+
}
16+
let _ = f(1.0) == f(2.0);
17+
}
18+
{
19+
let _ = 1.0f32 == 2.0f32;
20+
let _ = -1.0f32 == -2.0f32;
21+
let _ = 1.0f64 == 2.0f64;
22+
}
23+
{
24+
fn f(x: f32) {
25+
let _ = x + 1.0 == x;
26+
let _ = x == x + 1.0;
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
error: strict comparison of `f32` or `f64`
2+
--> $DIR/test.rs:25:21
3+
|
4+
LL | let _ = x + 1.0 == x;
5+
| ^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(x + 1.0 - x).abs() < error_margin`
6+
|
7+
note: the lint level is defined here
8+
--> $DIR/test.rs:3:9
9+
|
10+
LL | #![deny(clippy::float_cmp)]
11+
| ^^^^^^^^^^^^^^^^^
12+
13+
error: strict comparison of `f32` or `f64`
14+
--> $DIR/test.rs:26:21
15+
|
16+
LL | let _ = x == x + 1.0;
17+
| ^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(x - (x + 1.0)).abs() < error_margin`
18+
19+
error: aborting due to 2 previous errors
20+

tests/ui-toml/float_cmp_constant_comparisons/test.rs

+6
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ fn main() {
2020
let _ = -1.0f32 == -2.0f32;
2121
let _ = 1.0f64 == 2.0f64;
2222
}
23+
{
24+
fn f(x: f32) {
25+
let _ = x + 1.0 == x;
26+
let _ = x == x + 1.0;
27+
}
28+
}
2329
}

tests/ui-toml/float_cmp_named_constants/test.rs

+6
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ fn main() {
2020
let _ = -1.0f32 == -2.0f32;
2121
let _ = 1.0f64 == 2.0f64;
2222
}
23+
{
24+
fn f(x: f32) {
25+
let _ = x + 1.0 == x;
26+
let _ = x == x + 1.0;
27+
}
28+
}
2329
}

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
3838
enum-variant-name-threshold
3939
enum-variant-size-threshold
4040
excessive-nesting-threshold
41+
float-cmp-ignore-change-detection
4142
float-cmp-ignore-constant-comparisons
4243
float-cmp-ignore-named-constants
4344
future-size-threshold
@@ -119,6 +120,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
119120
enum-variant-name-threshold
120121
enum-variant-size-threshold
121122
excessive-nesting-threshold
123+
float-cmp-ignore-change-detection
122124
float-cmp-ignore-constant-comparisons
123125
float-cmp-ignore-named-constants
124126
future-size-threshold

tests/ui/float_cmp.rs

+60
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,64 @@ fn main() {
270270
let _ = x == y;
271271
}
272272
}
273+
274+
// modified operands
275+
{
276+
fn f1(x: f32) -> f32 {
277+
x + 1.0
278+
}
279+
280+
fn f2(x: f32, y: f32) -> f32 {
281+
x + y
282+
}
283+
284+
fn _f(x: f32, y: f32) {
285+
let _ = x == x + 1.0;
286+
let _ = x + 1.0 == x;
287+
let _ = -x == -x + 1.0;
288+
let _ = -x + 1.0 == -x;
289+
let _ = x == f1(x);
290+
let _ = f1(x) == x;
291+
let _ = x == f2(x, y);
292+
let _ = f2(x, y) == x;
293+
let _ = f1(f1(x)) == f1(x);
294+
let _ = f1(x) == f1(f1(x));
295+
296+
let z = (x, y);
297+
let _ = z.0 == z.0 + 1.0;
298+
let _ = z.0 + 1.0 == z.0;
299+
}
300+
301+
fn _f2(x: &f32) {
302+
let _ = *x + 1.0 == *x;
303+
let _ = *x == *x + 1.0;
304+
let _ = *x == f1(*x);
305+
let _ = f1(*x) == *x;
306+
}
307+
}
308+
{
309+
fn _f(mut x: impl Iterator<Item = f32>) {
310+
let _ = x.next().unwrap() == x.next().unwrap() + 1.0;
311+
//~^ ERROR: strict comparison of `f32` or `f64`
312+
}
313+
}
314+
{
315+
use core::cell::RefCell;
316+
317+
struct S(RefCell<f32>);
318+
impl S {
319+
fn f(&self) -> f32 {
320+
let x = *self.0.borrow();
321+
*self.0.borrow_mut() *= 2.0;
322+
x
323+
}
324+
}
325+
326+
fn _f(x: S) {
327+
let _ = x.f() + 1.0 == x.f();
328+
//~^ ERROR: strict comparison of `f32` or `f64`
329+
let _ = x.f() == x.f() + 1.0;
330+
//~^ ERROR: strict comparison of `f32` or `f64`
331+
}
332+
}
273333
}

0 commit comments

Comments
 (0)