1 use clippy_utils
::diagnostics
::{span_lint_and_then, span_lint_hir_and_then}
;
2 use clippy_utils
::higher
::If
;
3 use clippy_utils
::msrvs
::{self, Msrv}
;
4 use clippy_utils
::sugg
::Sugg
;
5 use clippy_utils
::ty
::implements_trait
;
6 use clippy_utils
::visitors
::is_const_evaluatable
;
7 use clippy_utils
::MaybePath
;
9 eq_expr_value
, in_constant
, is_diag_trait_item
, is_trait_method
, path_res
, path_to_local_id
, peel_blocks
,
10 peel_blocks_with_stmt
,
12 use itertools
::Itertools
;
13 use rustc_errors
::Applicability
;
14 use rustc_errors
::Diagnostic
;
16 def
::Res
, Arm
, BinOpKind
, Block
, Expr
, ExprKind
, Guard
, HirId
, PatKind
, PathSegment
, PrimTy
, QPath
, StmtKind
,
18 use rustc_lint
::{LateContext, LateLintPass}
;
19 use rustc_middle
::ty
::Ty
;
20 use rustc_session
::{declare_tool_lint, impl_lint_pass}
;
21 use rustc_span
::{symbol::sym, Span}
;
24 declare_clippy_lint
! {
26 /// Identifies good opportunities for a clamp function from std or core, and suggests using it.
28 /// ### Why is this bad?
29 /// clamp is much shorter, easier to read, and doesn't use any control flow.
31 /// ### Known issue(s)
32 /// If the clamped variable is NaN this suggestion will cause the code to propagate NaN
33 /// rather than returning either `max` or `min`.
35 /// `clamp` functions will panic if `max < min`, `max.is_nan()`, or `min.is_nan()`.
36 /// Some may consider panicking in these situations to be desirable, but it also may
37 /// introduce panicking where there wasn't any before.
39 /// See also [the discussion in the
40 /// PR](https://github.com/rust-lang/rust-clippy/pull/9484#issuecomment-1278922613).
44 /// # let (input, min, max) = (0, -2, 1);
47 /// } else if input < min {
56 /// # let (input, min, max) = (0, -2, 1);
57 /// input.max(min).min(max)
62 /// # let (input, min, max) = (0, -2, 1);
64 /// x if x > max => max,
65 /// x if x < min => min,
72 /// # let (input, min, max) = (0, -2, 1);
73 /// let mut x = input;
74 /// if x < min { x = min; }
75 /// if x > max { x = max; }
79 /// # let (input, min, max) = (0, -2, 1);
80 /// input.clamp(min, max)
83 #[clippy::version = "1.66.0"]
86 "using a clamp pattern instead of the clamp function"
88 impl_lint_pass
!(ManualClamp
=> [MANUAL_CLAMP
]);
90 pub struct ManualClamp
{
95 pub fn new(msrv
: Msrv
) -> Self {
101 struct ClampSuggestion
<'tcx
> {
102 params
: InputMinMax
<'tcx
>,
104 make_assignment
: Option
<&'tcx Expr
<'tcx
>>,
105 hir_with_ignore_attr
: Option
<HirId
>,
109 struct InputMinMax
<'tcx
> {
110 input
: &'tcx Expr
<'tcx
>,
111 min
: &'tcx Expr
<'tcx
>,
112 max
: &'tcx Expr
<'tcx
>,
116 impl<'tcx
> LateLintPass
<'tcx
> for ManualClamp
{
117 fn check_expr(&mut self, cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'tcx
>) {
118 if !self.msrv
.meets(msrvs
::CLAMP
) {
121 if !expr
.span
.from_expansion() && !in_constant(cx
, expr
.hir_id
) {
122 let suggestion
= is_if_elseif_else_pattern(cx
, expr
)
123 .or_else(|| is_max_min_pattern(cx
, expr
))
124 .or_else(|| is_call_max_min_pattern(cx
, expr
))
125 .or_else(|| is_match_pattern(cx
, expr
))
126 .or_else(|| is_if_elseif_pattern(cx
, expr
));
127 if let Some(suggestion
) = suggestion
{
128 emit_suggestion(cx
, &suggestion
);
133 fn check_block(&mut self, cx
: &LateContext
<'tcx
>, block
: &'tcx Block
<'tcx
>) {
134 if !self.msrv
.meets(msrvs
::CLAMP
) || in_constant(cx
, block
.hir_id
) {
137 for suggestion
in is_two_if_pattern(cx
, block
) {
138 emit_suggestion(cx
, &suggestion
);
141 extract_msrv_attr
!(LateContext
);
144 fn emit_suggestion
<'tcx
>(cx
: &LateContext
<'tcx
>, suggestion
: &ClampSuggestion
<'tcx
>) {
145 let ClampSuggestion
{
146 params
: InputMinMax
{
154 hir_with_ignore_attr
,
156 let input
= Sugg
::hir(cx
, input
, "..").maybe_par();
157 let min
= Sugg
::hir(cx
, min
, "..");
158 let max
= Sugg
::hir(cx
, max
, "..");
159 let semicolon
= if make_assignment
.is_some() { ";" }
else { "" }
;
160 let assignment
= if let Some(assignment
) = make_assignment
{
161 let assignment
= Sugg
::hir(cx
, assignment
, "..");
162 format
!("{assignment} = ")
166 let suggestion
= format
!("{assignment}{input}.clamp({min}, {max}){semicolon}");
167 let msg
= "clamp-like pattern without using clamp function";
168 let lint_builder
= |d
: &mut Diagnostic
| {
169 d
.span_suggestion(*span
, "replace with clamp", suggestion
, Applicability
::MaybeIncorrect
);
171 d
.note("clamp will panic if max < min, min.is_nan(), or max.is_nan()")
172 .note("clamp returns NaN if the input is NaN");
174 d
.note("clamp will panic if max < min");
177 if let Some(hir_id
) = hir_with_ignore_attr
{
178 span_lint_hir_and_then(cx
, MANUAL_CLAMP
, *hir_id
, *span
, msg
, lint_builder
);
180 span_lint_and_then(cx
, MANUAL_CLAMP
, *span
, msg
, lint_builder
);
184 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
185 enum TypeClampability
{
190 impl TypeClampability
{
191 fn is_clampable
<'tcx
>(cx
: &LateContext
<'tcx
>, ty
: Ty
<'tcx
>) -> Option
<TypeClampability
> {
192 if ty
.is_floating_point() {
193 Some(TypeClampability
::Float
)
196 .get_diagnostic_item(sym
::Ord
)
197 .map_or(false, |id
| implements_trait(cx
, ty
, id
, &[]))
199 Some(TypeClampability
::Ord
)
205 fn is_float(self) -> bool
{
206 matches
!(self, TypeClampability
::Float
)
210 /// Targets patterns like
213 /// # let (input, min, max) = (0, -3, 12);
217 /// } else if input > max {
224 fn is_if_elseif_else_pattern
<'tcx
>(cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'tcx
>) -> Option
<ClampSuggestion
<'tcx
>> {
228 r
#else: Some(else_if),
233 r
#else: Some(else_body),
234 }) = If
::hir(peel_blocks(else_if
))
236 let params
= is_clamp_meta_pattern(
238 &BinaryOp
::new(peel_blocks(cond
))?
,
239 &BinaryOp
::new(peel_blocks(else_if_cond
))?
,
241 peel_blocks(else_if_then
),
244 // Contents of the else should be the resolved input.
245 if !eq_expr_value(cx
, params
.input
, peel_blocks(else_body
)) {
248 Some(ClampSuggestion
{
251 make_assignment
: None
,
252 hir_with_ignore_attr
: None
,
259 /// Targets patterns like
262 /// # let (input, min_value, max_value) = (0, -3, 12);
264 /// input.max(min_value).min(max_value)
267 fn is_max_min_pattern
<'tcx
>(cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'tcx
>) -> Option
<ClampSuggestion
<'tcx
>> {
268 if let ExprKind
::MethodCall(seg_second
, receiver
, [arg_second
], _
) = &expr
.kind
269 && (cx
.typeck_results().expr_ty_adjusted(receiver
).is_floating_point() || is_trait_method(cx
, expr
, sym
::Ord
))
270 && let ExprKind
::MethodCall(seg_first
, input
, [arg_first
], _
) = &receiver
.kind
271 && (cx
.typeck_results().expr_ty_adjusted(input
).is_floating_point() || is_trait_method(cx
, receiver
, sym
::Ord
))
273 let is_float
= cx
.typeck_results().expr_ty_adjusted(input
).is_floating_point();
274 let (min
, max
) = match (seg_first
.ident
.as_str(), seg_second
.ident
.as_str()) {
275 ("min", "max") => (arg_second
, arg_first
),
276 ("max", "min") => (arg_first
, arg_second
),
279 Some(ClampSuggestion
{
280 params
: InputMinMax { input, min, max, is_float }
,
282 make_assignment
: None
,
283 hir_with_ignore_attr
: None
,
290 /// Targets patterns like
293 /// # let (input, min_value, max_value) = (0, -3, 12);
294 /// # use std::cmp::{max, min};
295 /// min(max(input, min_value), max_value)
298 fn is_call_max_min_pattern
<'tcx
>(cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'tcx
>) -> Option
<ClampSuggestion
<'tcx
>> {
299 fn segment
<'tcx
>(cx
: &LateContext
<'_
>, func
: &Expr
<'tcx
>) -> Option
<FunctionType
<'tcx
>> {
301 ExprKind
::Path(QPath
::Resolved(None
, path
)) => {
302 let id
= path
.res
.opt_def_id()?
;
303 match cx
.tcx
.get_diagnostic_name(id
) {
304 Some(sym
::cmp_min
) => Some(FunctionType
::CmpMin
),
305 Some(sym
::cmp_max
) => Some(FunctionType
::CmpMax
),
306 _
if is_diag_trait_item(cx
, id
, sym
::Ord
) => {
307 Some(FunctionType
::OrdOrFloat(path
.segments
.last().expect("infallible")))
312 ExprKind
::Path(QPath
::TypeRelative(ty
, seg
)) => {
313 matches
!(path_res(cx
, ty
), Res
::PrimTy(PrimTy
::Float(_
))).then(|| FunctionType
::OrdOrFloat(seg
))
319 enum FunctionType
<'tcx
> {
322 OrdOrFloat(&'tcx PathSegment
<'tcx
>),
326 cx
: &LateContext
<'tcx
>,
327 outer_fn
: &'tcx Expr
<'tcx
>,
328 inner_call
: &'tcx Expr
<'tcx
>,
329 outer_arg
: &'tcx Expr
<'tcx
>,
331 ) -> Option
<ClampSuggestion
<'tcx
>> {
332 if let ExprKind
::Call(inner_fn
, [first
, second
]) = &inner_call
.kind
333 && let Some(inner_seg
) = segment(cx
, inner_fn
)
334 && let Some(outer_seg
) = segment(cx
, outer_fn
)
336 let (input
, inner_arg
) = match (is_const_evaluatable(cx
, first
), is_const_evaluatable(cx
, second
)) {
337 (true, false) => (second
, first
),
338 (false, true) => (first
, second
),
341 let is_float
= cx
.typeck_results().expr_ty_adjusted(input
).is_floating_point();
342 let (min
, max
) = match (inner_seg
, outer_seg
) {
343 (FunctionType
::CmpMin
, FunctionType
::CmpMax
) => (outer_arg
, inner_arg
),
344 (FunctionType
::CmpMax
, FunctionType
::CmpMin
) => (inner_arg
, outer_arg
),
345 (FunctionType
::OrdOrFloat(first_segment
), FunctionType
::OrdOrFloat(second_segment
)) => {
346 match (first_segment
.ident
.as_str(), second_segment
.ident
.as_str()) {
347 ("min", "max") => (outer_arg
, inner_arg
),
348 ("max", "min") => (inner_arg
, outer_arg
),
354 Some(ClampSuggestion
{
355 params
: InputMinMax { input, min, max, is_float }
,
357 make_assignment
: None
,
358 hir_with_ignore_attr
: None
,
365 if let ExprKind
::Call(outer_fn
, [first
, second
]) = &expr
.kind
{
366 check(cx
, outer_fn
, first
, second
, expr
.span
).or_else(|| check(cx
, outer_fn
, second
, first
, expr
.span
))
372 /// Targets patterns like
375 /// # let (input, min, max) = (0, -3, 12);
378 /// input if input > max => max,
379 /// input if input < min => min,
384 fn is_match_pattern
<'tcx
>(cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'tcx
>) -> Option
<ClampSuggestion
<'tcx
>> {
385 if let ExprKind
::Match(value
, [first_arm
, second_arm
, last_arm
], rustc_hir
::MatchSource
::Normal
) = &expr
.kind
{
386 // Find possible min/max branches
387 let minmax_values
= |a
: &'tcx Arm
<'tcx
>| {
388 if let PatKind
::Binding(_
, var_hir_id
, _
, None
) = &a
.pat
.kind
389 && let Some(Guard
::If(e
)) = a
.guard
{
390 Some((e
, var_hir_id
, a
.body
))
395 let (first
, first_hir_id
, first_expr
) = minmax_values(first_arm
)?
;
396 let (second
, second_hir_id
, second_expr
) = minmax_values(second_arm
)?
;
397 let first
= BinaryOp
::new(first
)?
;
398 let second
= BinaryOp
::new(second
)?
;
399 if let PatKind
::Binding(_
, binding
, _
, None
) = &last_arm
.pat
.kind
400 && path_to_local_id(peel_blocks_with_stmt(last_arm
.body
), *binding
)
401 && last_arm
.guard
.is_none()
407 if let Some(params
) = is_clamp_meta_pattern(
413 Some((*first_hir_id
, *second_hir_id
)),
415 return Some(ClampSuggestion
{
416 params
: InputMinMax
{
420 is_float
: params
.is_float
,
423 make_assignment
: None
,
424 hir_with_ignore_attr
: None
,
431 /// Targets patterns like
434 /// # let (input, min, max) = (0, -3, 12);
436 /// let mut x = input;
437 /// if x < min { x = min; }
438 /// if x > max { x = max; }
440 fn is_two_if_pattern
<'tcx
>(cx
: &LateContext
<'tcx
>, block
: &'tcx Block
<'tcx
>) -> Vec
<ClampSuggestion
<'tcx
>> {
441 block_stmt_with_last(block
)
443 .filter_map(|(maybe_set_first
, maybe_set_second
)| {
444 if let StmtKind
::Expr(first_expr
) = *maybe_set_first
445 && let StmtKind
::Expr(second_expr
) = *maybe_set_second
446 && let Some(If { cond: first_cond, then: first_then, r#else: None }
) = If
::hir(first_expr
)
447 && let Some(If { cond: second_cond, then: second_then, r#else: None }
) = If
::hir(second_expr
)
448 && let ExprKind
::Assign(
449 maybe_input_first_path
,
452 ) = peel_blocks_with_stmt(first_then
).kind
453 && let ExprKind
::Assign(
454 maybe_input_second_path
,
455 maybe_min_max_second
,
457 ) = peel_blocks_with_stmt(second_then
).kind
458 && eq_expr_value(cx
, maybe_input_first_path
, maybe_input_second_path
)
459 && let Some(first_bin
) = BinaryOp
::new(first_cond
)
460 && let Some(second_bin
) = BinaryOp
::new(second_cond
)
461 && let Some(input_min_max
) = is_clamp_meta_pattern(
466 maybe_min_max_second
,
470 Some(ClampSuggestion
{
471 params
: InputMinMax
{
472 input
: maybe_input_first_path
,
473 min
: input_min_max
.min
,
474 max
: input_min_max
.max
,
475 is_float
: input_min_max
.is_float
,
477 span
: first_expr
.span
.to(second_expr
.span
),
478 make_assignment
: Some(maybe_input_first_path
),
479 hir_with_ignore_attr
: Some(first_expr
.hir_id()),
488 /// Targets patterns like
491 /// # let (mut input, min, max) = (0, -3, 12);
495 /// } else if input > max {
499 fn is_if_elseif_pattern
<'tcx
>(cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'tcx
>) -> Option
<ClampSuggestion
<'tcx
>> {
503 r
#else: Some(else_if),
509 }) = If
::hir(peel_blocks(else_if
))
510 && let ExprKind
::Assign(
511 maybe_input_first_path
,
514 ) = peel_blocks_with_stmt(then
).kind
515 && let ExprKind
::Assign(
516 maybe_input_second_path
,
517 maybe_min_max_second
,
519 ) = peel_blocks_with_stmt(else_if_then
).kind
521 let params
= is_clamp_meta_pattern(
523 &BinaryOp
::new(peel_blocks(cond
))?
,
524 &BinaryOp
::new(peel_blocks(else_if_cond
))?
,
525 peel_blocks(maybe_min_max_first
),
526 peel_blocks(maybe_min_max_second
),
529 if !eq_expr_value(cx
, maybe_input_first_path
, maybe_input_second_path
) {
532 Some(ClampSuggestion
{
535 make_assignment
: Some(maybe_input_first_path
),
536 hir_with_ignore_attr
: None
,
543 /// `ExprKind::Binary` but more narrowly typed
544 #[derive(Debug, Clone, Copy)]
545 struct BinaryOp
<'tcx
> {
547 left
: &'tcx Expr
<'tcx
>,
548 right
: &'tcx Expr
<'tcx
>,
551 impl<'tcx
> BinaryOp
<'tcx
> {
552 fn new(e
: &'tcx Expr
<'tcx
>) -> Option
<BinaryOp
<'tcx
>> {
554 ExprKind
::Binary(op
, left
, right
) => Some(BinaryOp
{
563 fn flip(&self) -> Self {
566 BinOpKind
::Le
=> BinOpKind
::Ge
,
567 BinOpKind
::Lt
=> BinOpKind
::Gt
,
568 BinOpKind
::Ge
=> BinOpKind
::Le
,
569 BinOpKind
::Gt
=> BinOpKind
::Lt
,
578 /// The clamp meta pattern is a pattern shared between many (but not all) patterns.
579 /// In summary, this pattern consists of two if statements that meet many criteria,
580 /// - binary operators that are one of [`>`, `<`, `>=`, `<=`].
581 /// - Both binary statements must have a shared argument
582 /// - Which can appear on the left or right side of either statement
583 /// - The binary operators must define a finite range for the shared argument. To put this in
584 /// the terms of Rust `std` library, the following ranges are acceptable
586 /// - `RangeInclusive`
587 /// And all other range types are not accepted. For the purposes of `clamp` it's irrelevant
588 /// whether the range is inclusive or not, the output is the same.
589 /// - The result of each if statement must be equal to the argument unique to that if statement. The
590 /// result can not be the shared argument in either case.
591 fn is_clamp_meta_pattern
<'tcx
>(
592 cx
: &LateContext
<'tcx
>,
593 first_bin
: &BinaryOp
<'tcx
>,
594 second_bin
: &BinaryOp
<'tcx
>,
595 first_expr
: &'tcx Expr
<'tcx
>,
596 second_expr
: &'tcx Expr
<'tcx
>,
597 // This parameters is exclusively for the match pattern.
598 // It exists because the variable bindings used in that pattern
599 // refer to the variable bound in the match arm, not the variable
600 // bound outside of it. Fortunately due to context we know this has to
601 // be the input variable, not the min or max.
602 input_hir_ids
: Option
<(HirId
, HirId
)>,
603 ) -> Option
<InputMinMax
<'tcx
>> {
605 cx
: &LateContext
<'tcx
>,
606 first_bin
: &BinaryOp
<'tcx
>,
607 second_bin
: &BinaryOp
<'tcx
>,
608 first_expr
: &'tcx Expr
<'tcx
>,
609 second_expr
: &'tcx Expr
<'tcx
>,
610 input_hir_ids
: Option
<(HirId
, HirId
)>,
612 ) -> Option
<InputMinMax
<'tcx
>> {
613 match (&first_bin
.op
, &second_bin
.op
) {
614 (BinOpKind
::Ge
| BinOpKind
::Gt
, BinOpKind
::Le
| BinOpKind
::Lt
) => {
615 let (min
, max
) = (second_expr
, first_expr
);
616 let refers_to_input
= match input_hir_ids
{
617 Some((first_hir_id
, second_hir_id
)) => {
618 path_to_local_id(peel_blocks(first_bin
.left
), first_hir_id
)
619 && path_to_local_id(peel_blocks(second_bin
.left
), second_hir_id
)
621 None
=> eq_expr_value(cx
, first_bin
.left
, second_bin
.left
),
624 && eq_expr_value(cx
, first_bin
.right
, first_expr
)
625 && eq_expr_value(cx
, second_bin
.right
, second_expr
))
626 .then_some(InputMinMax
{
627 input
: first_bin
.left
,
636 // First filter out any expressions with side effects
645 let clampability
= TypeClampability
::is_clampable(cx
, cx
.typeck_results().expr_ty(first_expr
))?
;
646 let is_float
= clampability
.is_float();
647 if exprs
.iter().any(|e
| peel_blocks(e
).can_have_side_effects()) {
650 if !(is_ord_op(first_bin
.op
) && is_ord_op(second_bin
.op
)) {
654 (*first_bin
, *second_bin
),
655 (first_bin
.flip(), second_bin
.flip()),
656 (first_bin
.flip(), *second_bin
),
657 (*first_bin
, second_bin
.flip()),
660 cases
.into_iter().find_map(|(first
, second
)| {
661 check(cx
, &first
, &second
, first_expr
, second_expr
, input_hir_ids
, is_float
).or_else(|| {
668 input_hir_ids
.map(|(l
, r
)| (r
, l
)),
675 fn block_stmt_with_last
<'tcx
>(block
: &'tcx Block
<'tcx
>) -> impl Iterator
<Item
= MaybeBorrowedStmtKind
<'tcx
>> {
679 .map(|s
| MaybeBorrowedStmtKind
::Borrowed(&s
.kind
))
684 .map(|e
| MaybeBorrowedStmtKind
::Owned(StmtKind
::Expr(e
))),
688 fn is_ord_op(op
: BinOpKind
) -> bool
{
689 matches
!(op
, BinOpKind
::Ge
| BinOpKind
::Gt
| BinOpKind
::Le
| BinOpKind
::Lt
)
692 /// Really similar to Cow, but doesn't have a `Clone` requirement.
694 enum MaybeBorrowedStmtKind
<'a
> {
695 Borrowed(&'a StmtKind
<'a
>),
699 impl<'a
> Clone
for MaybeBorrowedStmtKind
<'a
> {
700 fn clone(&self) -> Self {
702 Self::Borrowed(t
) => Self::Borrowed(t
),
703 Self::Owned(StmtKind
::Expr(e
)) => Self::Owned(StmtKind
::Expr(e
)),
704 Self::Owned(_
) => unreachable
!("Owned should only ever contain a StmtKind::Expr."),
709 impl<'a
> Deref
for MaybeBorrowedStmtKind
<'a
> {
710 type Target
= StmtKind
<'a
>;
712 fn deref(&self) -> &Self::Target
{
714 Self::Borrowed(t
) => t
,