Skip to content

Commit 3a97f5a

Browse files
committed
Auto merge of #11948 - Jarcho:float_cmp, r=xFrednet
`float_cmp` changes fixes #2834 fixes #6816 --- changelog: This: * Deprecated `float_cmp_const` in favor of a config option on [`float_cmp`]. [#11948](#11948) * [`float_cmp`]: Don't lint literal and self comparisons. [#11948](#11948) * [`float_cmp`] [#11948](#11948) * Add the [`float-cmp-ignore-named-constants`] configuration to ignore comparisons to named constants. * Add the [`float-cmp-ignore-change-detection`] configuration to ignore const-evaluatabled values. * Add the [`float-cmp-ignore-constant-comparisons`] configuration to ignore comparisons to the modification of an operand (e.g. `x == f(x)`).
2 parents 0ee9f44 + c3c15d0 commit 3a97f5a

22 files changed

+1348
-418
lines changed

clippy_config/src/conf.rs

+41
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,47 @@ define_Conf! {
645645
///
646646
/// Whether to also emit warnings for unsafe blocks with metavariable expansions in **private** macros.
647647
(warn_unsafe_macro_metavars_in_private_macros: bool = false),
648+
/// Lint: FLOAT_CMP
649+
///
650+
/// Whether to ignore comparisons to a named constnat
651+
///
652+
/// #### Example
653+
/// ```no_run
654+
/// const VALUE: f64 = 1.0;
655+
/// fn is_value(x: f64) -> bool {
656+
/// // Will warn if the config is `false`
657+
/// x == VALUE
658+
/// }
659+
/// ```
660+
(float_cmp_ignore_named_constants: bool = true),
661+
/// Lint: FLOAT_CMP
662+
///
663+
/// Whether to ignore comparisons which have a constant result.
664+
///
665+
/// #### Example
666+
/// ```no_run
667+
/// const fn f(x: f64) -> f64 {
668+
/// todo!()
669+
/// }
670+
///
671+
/// // Will warn if the config is `false`
672+
/// if f(1.0) == f(2.0) {
673+
/// // ...
674+
/// }
675+
/// ```
676+
(float_cmp_ignore_constant_comparisons: bool = true),
677+
/// Lint: FLOAT_CMP
678+
///
679+
/// Whether to ignore comparisons which check if an operation changes the value of it's operand.
680+
///
681+
/// #### Example
682+
/// ```no_run
683+
/// fn f(x: f64) -> bool {
684+
/// // Will warn if the config is `false`
685+
/// x == x + 1.0
686+
/// }
687+
/// ```
688+
(float_cmp_ignore_change_detection: bool = true),
648689
}
649690

650691
/// Search for the configuration file.

clippy_lints/src/declared_lints.rs

-1
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,6 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
570570
crate::operators::ERASING_OP_INFO,
571571
crate::operators::FLOAT_ARITHMETIC_INFO,
572572
crate::operators::FLOAT_CMP_INFO,
573-
crate::operators::FLOAT_CMP_CONST_INFO,
574573
crate::operators::FLOAT_EQUALITY_WITHOUT_ABS_INFO,
575574
crate::operators::IDENTITY_OP_INFO,
576575
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,

clippy_lints/src/deprecated_lints.rs

+11
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,14 @@ declare_deprecated_lint! {
241241
pub MISMATCHED_TARGET_OS,
242242
"this lint has been replaced by `unexpected_cfgs`"
243243
}
244+
245+
declare_deprecated_lint! {
246+
/// ### What it does
247+
/// Nothing. This lint has been deprecated.
248+
///
249+
/// ### Deprecation reason
250+
/// `float_cmp` handles this via config options
251+
#[clippy::version = "1.76.0"]
252+
pub FLOAT_CMP_CONST,
253+
"`float_cmp` handles this via config options"
254+
}

clippy_lints/src/lib.deprecated.rs

+4
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,8 @@
7575
"clippy::mismatched_target_os",
7676
"this lint has been replaced by `unexpected_cfgs`",
7777
);
78+
store.register_removed(
79+
"clippy::float_cmp_const",
80+
"`float_cmp` handles this via config options",
81+
);
7882
}

clippy_lints/src/lib.rs

+6
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,9 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
608608
allow_comparison_to_zero,
609609
ref allowed_prefixes,
610610
ref allow_renamed_params_for,
611+
float_cmp_ignore_named_constants,
612+
float_cmp_ignore_constant_comparisons,
613+
float_cmp_ignore_change_detection,
611614

612615
blacklisted_names: _,
613616
cyclomatic_complexity_threshold: _,
@@ -1031,6 +1034,9 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
10311034
Box::new(operators::Operators::new(
10321035
verbose_bit_mask_threshold,
10331036
allow_comparison_to_zero,
1037+
float_cmp_ignore_named_constants,
1038+
float_cmp_ignore_constant_comparisons,
1039+
float_cmp_ignore_change_detection,
10341040
))
10351041
});
10361042
store.register_late_pass(|_| Box::<std_instead_of_core::StdReexports>::default());

clippy_lints/src/operators/float_cmp.rs

+144-53
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,87 @@
1-
use clippy_utils::consts::{constant_with_source, Constant};
1+
use clippy_utils::consts::{constant, Constant};
22
use clippy_utils::diagnostics::span_lint_and_then;
3-
use clippy_utils::get_item_name;
43
use clippy_utils::sugg::Sugg;
4+
use clippy_utils::visitors::{for_each_expr_without_closures, is_const_evaluatable};
5+
use clippy_utils::{get_item_name, is_expr_named_const, path_res, peel_hir_expr_while, SpanlessEq};
6+
use core::ops::ControlFlow;
57
use rustc_errors::Applicability;
6-
use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp};
8+
use rustc_hir::def::Res;
9+
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, Safety, UnOp};
710
use rustc_lint::LateContext;
8-
use rustc_middle::ty;
11+
use rustc_middle::ty::{self, Ty, TypeFlags, TypeVisitableExt};
912

10-
use super::{FLOAT_CMP, FLOAT_CMP_CONST};
13+
use super::{FloatCmpConfig, FLOAT_CMP};
1114

1215
pub(crate) fn check<'tcx>(
1316
cx: &LateContext<'tcx>,
17+
config: FloatCmpConfig,
1418
expr: &'tcx Expr<'_>,
1519
op: BinOpKind,
1620
left: &'tcx Expr<'_>,
1721
right: &'tcx Expr<'_>,
1822
) {
19-
if (op == BinOpKind::Eq || op == BinOpKind::Ne) && is_float(cx, left) {
20-
let left_is_local = match constant_with_source(cx, cx.typeck_results(), left) {
21-
Some((c, s)) if !is_allowed(&c) => s.is_local(),
22-
Some(_) => return,
23-
None => true,
24-
};
25-
let right_is_local = match constant_with_source(cx, cx.typeck_results(), right) {
26-
Some((c, s)) if !is_allowed(&c) => s.is_local(),
27-
Some(_) => return,
28-
None => true,
29-
};
23+
let peel_expr = |e: &'tcx Expr<'tcx>| match e.kind {
24+
ExprKind::Cast(e, _) | ExprKind::AddrOf(BorrowKind::Ref, _, e) | ExprKind::Unary(UnOp::Neg, e) => Some(e),
25+
_ => None,
26+
};
3027

28+
if matches!(op, BinOpKind::Eq | BinOpKind::Ne)
29+
&& let left_reduced = peel_hir_expr_while(left, peel_expr)
30+
&& let right_reduced = peel_hir_expr_while(right, peel_expr)
31+
&& is_float(cx, left_reduced)
32+
// Don't lint literal comparisons
33+
&& !(matches!(left_reduced.kind, ExprKind::Lit(_)) && matches!(right_reduced.kind, ExprKind::Lit(_)))
3134
// Allow comparing the results of signum()
32-
if is_signum(cx, left) && is_signum(cx, right) {
35+
&& !(is_signum(cx, left_reduced) && is_signum(cx, right_reduced))
36+
&& match (path_res(cx, left_reduced), path_res(cx, right_reduced)) {
37+
(Res::Err, _) | (_, Res::Err) => true,
38+
(left, right) => left != right,
39+
}
40+
{
41+
let left_c = constant(cx, cx.typeck_results(), left_reduced);
42+
let is_left_const = left_c.is_some();
43+
if left_c.is_some_and(|c| is_allowed(&c)) {
44+
return;
45+
}
46+
let right_c = constant(cx, cx.typeck_results(), right_reduced);
47+
let is_right_const = right_c.is_some();
48+
if right_c.is_some_and(|c| is_allowed(&c)) {
49+
return;
50+
}
51+
52+
if config.ignore_constant_comparisons
53+
&& (is_left_const || is_const_evaluatable(cx, left_reduced))
54+
&& (is_right_const || is_const_evaluatable(cx, right_reduced))
55+
{
56+
return;
57+
}
58+
59+
if config.ignore_named_constants
60+
&& (is_expr_named_const(cx, left_reduced) || is_expr_named_const(cx, right_reduced))
61+
{
62+
return;
63+
}
64+
65+
if config.ignore_change_detection
66+
&& ((is_pure_expr(cx, left_reduced) && contains_expr(cx, right, left))
67+
|| (is_pure_expr(cx, right_reduced) && contains_expr(cx, left, right)))
68+
{
3369
return;
3470
}
3571

3672
if let Some(name) = get_item_name(cx, expr) {
3773
let name = name.as_str();
38-
if name == "eq" || name == "ne" || name == "is_nan" || name.starts_with("eq_") || name.ends_with("_eq") {
74+
if name == "eq" || name == "ne" || name.starts_with("eq_") || name.ends_with("_eq") {
3975
return;
4076
}
4177
}
42-
let is_comparing_arrays = is_array(cx, left) || is_array(cx, right);
43-
let (lint, msg) = get_lint_and_message(left_is_local && right_is_local, is_comparing_arrays);
44-
span_lint_and_then(cx, lint, expr.span, msg, |diag| {
78+
let is_comparing_arrays = is_array(cx, left_reduced) || is_array(cx, right_reduced);
79+
let msg = if is_comparing_arrays {
80+
"strict comparison of `f32` or `f64` arrays"
81+
} else {
82+
"strict comparison of `f32` or `f64`"
83+
};
84+
span_lint_and_then(cx, FLOAT_CMP, expr.span, msg, |diag| {
4585
let lhs = Sugg::hir(cx, left, "..");
4686
let rhs = Sugg::hir(cx, right, "..");
4787

@@ -61,54 +101,105 @@ pub(crate) fn check<'tcx>(
61101
}
62102
}
63103

64-
fn get_lint_and_message(is_local: bool, is_comparing_arrays: bool) -> (&'static rustc_lint::Lint, &'static str) {
65-
if is_local {
66-
(
67-
FLOAT_CMP,
68-
if is_comparing_arrays {
69-
"strict comparison of `f32` or `f64` arrays"
70-
} else {
71-
"strict comparison of `f32` or `f64`"
72-
},
73-
)
74-
} else {
75-
(
76-
FLOAT_CMP_CONST,
77-
if is_comparing_arrays {
78-
"strict comparison of `f32` or `f64` constant arrays"
79-
} else {
80-
"strict comparison of `f32` or `f64` constant"
81-
},
82-
)
83-
}
84-
}
85-
86104
fn is_allowed(val: &Constant<'_>) -> bool {
87105
match val {
88106
// FIXME(f16_f128): add when equality check is available on all platforms
107+
Constant::Ref(val) => is_allowed(val),
89108
&Constant::F32(f) => f == 0.0 || f.is_infinite(),
90109
&Constant::F64(f) => f == 0.0 || f.is_infinite(),
91-
Constant::Vec(vec) => vec.iter().all(|f| match f {
92-
Constant::F32(f) => *f == 0.0 || (*f).is_infinite(),
93-
Constant::F64(f) => *f == 0.0 || (*f).is_infinite(),
110+
Constant::Vec(vec) => vec.iter().all(|f| match *f {
111+
Constant::F32(f) => f == 0.0 || f.is_infinite(),
112+
Constant::F64(f) => f == 0.0 || f.is_infinite(),
94113
_ => false,
95114
}),
115+
Constant::Repeat(val, _) => match **val {
116+
Constant::F32(f) => f == 0.0 || f.is_infinite(),
117+
Constant::F64(f) => f == 0.0 || f.is_infinite(),
118+
_ => false,
119+
},
96120
_ => false,
97121
}
98122
}
99123

100-
// Return true if `expr` is the result of `signum()` invoked on a float value.
101-
fn is_signum(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
102-
// The negation of a signum is still a signum
103-
if let ExprKind::Unary(UnOp::Neg, child_expr) = expr.kind {
104-
return is_signum(cx, child_expr);
124+
// This is a best effort guess and may have false positives and negatives.
125+
fn is_pure_expr<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> bool {
126+
match e.kind {
127+
ExprKind::Path(_) | ExprKind::Lit(_) => true,
128+
ExprKind::Field(e, _) | ExprKind::Cast(e, _) | ExprKind::Repeat(e, _) => is_pure_expr(cx, e),
129+
ExprKind::Tup(args) => args.iter().all(|arg| is_pure_expr(cx, arg)),
130+
ExprKind::Struct(_, fields, base) => {
131+
base.map_or(true, |base| is_pure_expr(cx, base)) && fields.iter().all(|f| is_pure_expr(cx, f.expr))
132+
},
133+
134+
// Since rust doesn't actually have the concept of a pure function we
135+
// have to guess whether it's likely pure from the signature of the
136+
// function.
137+
ExprKind::Unary(_, e) => is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(e)) && is_pure_expr(cx, e),
138+
ExprKind::Binary(_, x, y) | ExprKind::Index(x, y, _) => {
139+
is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(x))
140+
&& is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(y))
141+
&& is_pure_expr(cx, x)
142+
&& is_pure_expr(cx, y)
143+
},
144+
ExprKind::MethodCall(_, recv, args, _) => {
145+
is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(recv))
146+
&& is_pure_expr(cx, recv)
147+
&& cx
148+
.typeck_results()
149+
.type_dependent_def_id(e.hir_id)
150+
.is_some_and(|did| matches!(cx.tcx.fn_sig(did).skip_binder().skip_binder().safety, Safety::Safe))
151+
&& args
152+
.iter()
153+
.all(|arg| is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(arg)) && is_pure_expr(cx, arg))
154+
},
155+
ExprKind::Call(f, args @ [_, ..]) => {
156+
is_pure_expr(cx, f)
157+
&& is_pure_fn_ty(cx, cx.typeck_results().expr_ty_adjusted(f))
158+
&& args
159+
.iter()
160+
.all(|arg| is_pure_arg_ty(cx, cx.typeck_results().expr_ty_adjusted(arg)) && is_pure_expr(cx, arg))
161+
},
162+
163+
_ => false,
105164
}
165+
}
166+
167+
fn is_pure_fn_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
168+
let sig = match *ty.peel_refs().kind() {
169+
ty::FnDef(did, _) => cx.tcx.fn_sig(did).skip_binder(),
170+
ty::FnPtr(sig) => sig,
171+
ty::Closure(_, args) => {
172+
return args.as_closure().upvar_tys().iter().all(|ty| is_pure_arg_ty(cx, ty));
173+
},
174+
_ => return false,
175+
};
176+
matches!(sig.skip_binder().safety, Safety::Safe)
177+
}
106178

179+
fn is_pure_arg_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
180+
!ty.is_mutable_ptr()
181+
&& ty.is_copy_modulo_regions(cx.tcx, cx.param_env)
182+
&& (ty.peel_refs().is_freeze(cx.tcx, cx.param_env)
183+
|| !ty.has_type_flags(TypeFlags::HAS_FREE_REGIONS | TypeFlags::HAS_RE_ERASED | TypeFlags::HAS_RE_BOUND))
184+
}
185+
186+
fn contains_expr<'tcx>(cx: &LateContext<'tcx>, corpus: &'tcx Expr<'tcx>, e: &'tcx Expr<'tcx>) -> bool {
187+
for_each_expr_without_closures(corpus, |corpus| {
188+
if SpanlessEq::new(cx).eq_expr(corpus, e) {
189+
ControlFlow::Break(())
190+
} else {
191+
ControlFlow::Continue(())
192+
}
193+
})
194+
.is_some()
195+
}
196+
197+
// Return true if `expr` is the result of `signum()` invoked on a float value.
198+
fn is_signum(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
107199
if let ExprKind::MethodCall(method_name, self_arg, ..) = expr.kind
108200
&& sym!(signum) == method_name.ident.name
109-
// Check that the receiver of the signum() is a float (expressions[0] is the receiver of
110-
// the method call)
111201
{
202+
// Check that the receiver of the signum() is a float
112203
return is_float(cx, self_arg);
113204
}
114205
false

0 commit comments

Comments
 (0)