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, is_diag_trait_item, is_trait_method, path_res, path_to_local_id, peel_blocks, peel_blocks_with_stmt,
11 use itertools::Itertools;
12 use rustc_errors::Applicability;
13 use rustc_errors::Diagnostic;
15 def::Res, Arm, BinOpKind, Block, Expr, ExprKind, Guard, HirId, PatKind, PathSegment, PrimTy, QPath, StmtKind,
17 use rustc_lint::{LateContext, LateLintPass};
18 use rustc_middle::ty::Ty;
19 use rustc_session::{declare_tool_lint, impl_lint_pass};
20 use rustc_span::{symbol::sym, Span};
23 declare_clippy_lint! {
25 /// Identifies good opportunities for a clamp function from std or core, and suggests using it.
27 /// ### Why is this bad?
28 /// clamp is much shorter, easier to read, and doesn't use any control flow.
30 /// ### Known issue(s)
31 /// If the clamped variable is NaN this suggestion will cause the code to propagate NaN
32 /// rather than returning either `max` or `min`.
34 /// `clamp` functions will panic if `max < min`, `max.is_nan()`, or `min.is_nan()`.
35 /// Some may consider panicking in these situations to be desirable, but it also may
36 /// introduce panicking where there wasn't any before.
38 /// See also [the discussion in the
39 /// PR](https://github.com/rust-lang/rust-clippy/pull/9484#issuecomment-1278922613).
43 /// # let (input, min, max) = (0, -2, 1);
46 /// } else if input < min {
55 /// # let (input, min, max) = (0, -2, 1);
56 /// input.max(min).min(max)
61 /// # let (input, min, max) = (0, -2, 1);
63 /// x if x > max => max,
64 /// x if x < min => min,
71 /// # let (input, min, max) = (0, -2, 1);
72 /// let mut x = input;
73 /// if x < min { x = min; }
74 /// if x > max { x = max; }
78 /// # let (input, min, max) = (0, -2, 1);
79 /// input.clamp(min, max)
82 #[clippy::version = "1.66.0"]
85 "using a clamp pattern instead of the clamp function"
87 impl_lint_pass!(ManualClamp => [MANUAL_CLAMP]);
89 pub struct ManualClamp {
94 pub fn new(msrv: Msrv) -> Self {
100 struct ClampSuggestion<'tcx> {
101 params: InputMinMax<'tcx>,
103 make_assignment: Option<&'tcx Expr<'tcx>>,
104 hir_with_ignore_attr: Option<HirId>,
108 struct InputMinMax<'tcx> {
109 input: &'tcx Expr<'tcx>,
110 min: &'tcx Expr<'tcx>,
111 max: &'tcx Expr<'tcx>,
115 impl<'tcx> LateLintPass<'tcx> for ManualClamp {
116 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
117 if !self.msrv.meets(msrvs::CLAMP) {
120 if !expr.span.from_expansion() {
121 let suggestion = is_if_elseif_else_pattern(cx, expr)
122 .or_else(|| is_max_min_pattern(cx, expr))
123 .or_else(|| is_call_max_min_pattern(cx, expr))
124 .or_else(|| is_match_pattern(cx, expr))
125 .or_else(|| is_if_elseif_pattern(cx, expr));
126 if let Some(suggestion) = suggestion {
127 emit_suggestion(cx, &suggestion);
132 fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) {
133 if !self.msrv.meets(msrvs::CLAMP) {
136 for suggestion in is_two_if_pattern(cx, block) {
137 emit_suggestion(cx, &suggestion);
140 extract_msrv_attr!(LateContext);
143 fn emit_suggestion<'tcx>(cx: &LateContext<'tcx>, suggestion: &ClampSuggestion<'tcx>) {
144 let ClampSuggestion {
145 params: InputMinMax {
153 hir_with_ignore_attr,
155 let input = Sugg::hir(cx, input, "..").maybe_par();
156 let min = Sugg::hir(cx, min, "..");
157 let max = Sugg::hir(cx, max, "..");
158 let semicolon = if make_assignment.is_some() { ";" } else { "" };
159 let assignment = if let Some(assignment) = make_assignment {
160 let assignment = Sugg::hir(cx, assignment, "..");
161 format!("{assignment} = ")
165 let suggestion = format!("{assignment}{input}.clamp({min}, {max}){semicolon}");
166 let msg = "clamp-like pattern without using clamp function";
167 let lint_builder = |d: &mut Diagnostic| {
168 d.span_suggestion(*span, "replace with clamp", suggestion, Applicability::MaybeIncorrect);
170 d.note("clamp will panic if max < min, min.is_nan(), or max.is_nan()")
171 .note("clamp returns NaN if the input is NaN");
173 d.note("clamp will panic if max < min");
176 if let Some(hir_id) = hir_with_ignore_attr {
177 span_lint_hir_and_then(cx, MANUAL_CLAMP, *hir_id, *span, msg, lint_builder);
179 span_lint_and_then(cx, MANUAL_CLAMP, *span, msg, lint_builder);
183 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
184 enum TypeClampability {
189 impl TypeClampability {
190 fn is_clampable<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<TypeClampability> {
191 if ty.is_floating_point() {
192 Some(TypeClampability::Float)
195 .get_diagnostic_item(sym::Ord)
196 .map_or(false, |id| implements_trait(cx, ty, id, &[]))
198 Some(TypeClampability::Ord)
204 fn is_float(self) -> bool {
205 matches!(self, TypeClampability::Float)
209 /// Targets patterns like
212 /// # let (input, min, max) = (0, -3, 12);
216 /// } else if input > max {
223 fn is_if_elseif_else_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> {
227 r#else: Some(else_if),
232 r#else: Some(else_body),
233 }) = If::hir(peel_blocks(else_if))
235 let params = is_clamp_meta_pattern(
237 &BinaryOp::new(peel_blocks(cond))?,
238 &BinaryOp::new(peel_blocks(else_if_cond))?,
240 peel_blocks(else_if_then),
243 // Contents of the else should be the resolved input.
244 if !eq_expr_value(cx, params.input, peel_blocks(else_body)) {
247 Some(ClampSuggestion {
250 make_assignment: None,
251 hir_with_ignore_attr: None,
258 /// Targets patterns like
261 /// # let (input, min_value, max_value) = (0, -3, 12);
263 /// input.max(min_value).min(max_value)
266 fn is_max_min_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> {
267 if let ExprKind::MethodCall(seg_second, receiver, [arg_second], _) = &expr.kind
268 && (cx.typeck_results().expr_ty_adjusted(receiver).is_floating_point() || is_trait_method(cx, expr, sym::Ord))
269 && let ExprKind::MethodCall(seg_first, input, [arg_first], _) = &receiver.kind
270 && (cx.typeck_results().expr_ty_adjusted(input).is_floating_point() || is_trait_method(cx, receiver, sym::Ord))
272 let is_float = cx.typeck_results().expr_ty_adjusted(input).is_floating_point();
273 let (min, max) = match (seg_first.ident.as_str(), seg_second.ident.as_str()) {
274 ("min", "max") => (arg_second, arg_first),
275 ("max", "min") => (arg_first, arg_second),
278 Some(ClampSuggestion {
279 params: InputMinMax { input, min, max, is_float },
281 make_assignment: None,
282 hir_with_ignore_attr: None,
289 /// Targets patterns like
292 /// # let (input, min_value, max_value) = (0, -3, 12);
293 /// # use std::cmp::{max, min};
294 /// min(max(input, min_value), max_value)
297 fn is_call_max_min_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> {
298 fn segment<'tcx>(cx: &LateContext<'_>, func: &Expr<'tcx>) -> Option<FunctionType<'tcx>> {
300 ExprKind::Path(QPath::Resolved(None, path)) => {
301 let id = path.res.opt_def_id()?;
302 match cx.tcx.get_diagnostic_name(id) {
303 Some(sym::cmp_min) => Some(FunctionType::CmpMin),
304 Some(sym::cmp_max) => Some(FunctionType::CmpMax),
305 _ if is_diag_trait_item(cx, id, sym::Ord) => {
306 Some(FunctionType::OrdOrFloat(path.segments.last().expect("infallible")))
311 ExprKind::Path(QPath::TypeRelative(ty, seg)) => {
312 matches!(path_res(cx, ty), Res::PrimTy(PrimTy::Float(_))).then(|| FunctionType::OrdOrFloat(seg))
318 enum FunctionType<'tcx> {
321 OrdOrFloat(&'tcx PathSegment<'tcx>),
325 cx: &LateContext<'tcx>,
326 outer_fn: &'tcx Expr<'tcx>,
327 inner_call: &'tcx Expr<'tcx>,
328 outer_arg: &'tcx Expr<'tcx>,
330 ) -> Option<ClampSuggestion<'tcx>> {
331 if let ExprKind::Call(inner_fn, [first, second]) = &inner_call.kind
332 && let Some(inner_seg) = segment(cx, inner_fn)
333 && let Some(outer_seg) = segment(cx, outer_fn)
335 let (input, inner_arg) = match (is_const_evaluatable(cx, first), is_const_evaluatable(cx, second)) {
336 (true, false) => (second, first),
337 (false, true) => (first, second),
340 let is_float = cx.typeck_results().expr_ty_adjusted(input).is_floating_point();
341 let (min, max) = match (inner_seg, outer_seg) {
342 (FunctionType::CmpMin, FunctionType::CmpMax) => (outer_arg, inner_arg),
343 (FunctionType::CmpMax, FunctionType::CmpMin) => (inner_arg, outer_arg),
344 (FunctionType::OrdOrFloat(first_segment), FunctionType::OrdOrFloat(second_segment)) => {
345 match (first_segment.ident.as_str(), second_segment.ident.as_str()) {
346 ("min", "max") => (outer_arg, inner_arg),
347 ("max", "min") => (inner_arg, outer_arg),
353 Some(ClampSuggestion {
354 params: InputMinMax { input, min, max, is_float },
356 make_assignment: None,
357 hir_with_ignore_attr: None,
364 if let ExprKind::Call(outer_fn, [first, second]) = &expr.kind {
365 check(cx, outer_fn, first, second, expr.span).or_else(|| check(cx, outer_fn, second, first, expr.span))
371 /// Targets patterns like
374 /// # let (input, min, max) = (0, -3, 12);
377 /// input if input > max => max,
378 /// input if input < min => min,
383 fn is_match_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> {
384 if let ExprKind::Match(value, [first_arm, second_arm, last_arm], rustc_hir::MatchSource::Normal) = &expr.kind {
385 // Find possible min/max branches
386 let minmax_values = |a: &'tcx Arm<'tcx>| {
387 if let PatKind::Binding(_, var_hir_id, _, None) = &a.pat.kind
388 && let Some(Guard::If(e)) = a.guard {
389 Some((e, var_hir_id, a.body))
394 let (first, first_hir_id, first_expr) = minmax_values(first_arm)?;
395 let (second, second_hir_id, second_expr) = minmax_values(second_arm)?;
396 let first = BinaryOp::new(first)?;
397 let second = BinaryOp::new(second)?;
398 if let PatKind::Binding(_, binding, _, None) = &last_arm.pat.kind
399 && path_to_local_id(peel_blocks_with_stmt(last_arm.body), *binding)
400 && last_arm.guard.is_none()
406 if let Some(params) = is_clamp_meta_pattern(
412 Some((*first_hir_id, *second_hir_id)),
414 return Some(ClampSuggestion {
415 params: InputMinMax {
419 is_float: params.is_float,
422 make_assignment: None,
423 hir_with_ignore_attr: None,
430 /// Targets patterns like
433 /// # let (input, min, max) = (0, -3, 12);
435 /// let mut x = input;
436 /// if x < min { x = min; }
437 /// if x > max { x = max; }
439 fn is_two_if_pattern<'tcx>(cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) -> Vec<ClampSuggestion<'tcx>> {
440 block_stmt_with_last(block)
442 .filter_map(|(maybe_set_first, maybe_set_second)| {
443 if let StmtKind::Expr(first_expr) = *maybe_set_first
444 && let StmtKind::Expr(second_expr) = *maybe_set_second
445 && let Some(If { cond: first_cond, then: first_then, r#else: None }) = If::hir(first_expr)
446 && let Some(If { cond: second_cond, then: second_then, r#else: None }) = If::hir(second_expr)
447 && let ExprKind::Assign(
448 maybe_input_first_path,
451 ) = peel_blocks_with_stmt(first_then).kind
452 && let ExprKind::Assign(
453 maybe_input_second_path,
454 maybe_min_max_second,
456 ) = peel_blocks_with_stmt(second_then).kind
457 && eq_expr_value(cx, maybe_input_first_path, maybe_input_second_path)
458 && let Some(first_bin) = BinaryOp::new(first_cond)
459 && let Some(second_bin) = BinaryOp::new(second_cond)
460 && let Some(input_min_max) = is_clamp_meta_pattern(
465 maybe_min_max_second,
469 Some(ClampSuggestion {
470 params: InputMinMax {
471 input: maybe_input_first_path,
472 min: input_min_max.min,
473 max: input_min_max.max,
474 is_float: input_min_max.is_float,
476 span: first_expr.span.to(second_expr.span),
477 make_assignment: Some(maybe_input_first_path),
478 hir_with_ignore_attr: Some(first_expr.hir_id()),
487 /// Targets patterns like
490 /// # let (mut input, min, max) = (0, -3, 12);
494 /// } else if input > max {
498 fn is_if_elseif_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> {
502 r#else: Some(else_if),
508 }) = If::hir(peel_blocks(else_if))
509 && let ExprKind::Assign(
510 maybe_input_first_path,
513 ) = peel_blocks_with_stmt(then).kind
514 && let ExprKind::Assign(
515 maybe_input_second_path,
516 maybe_min_max_second,
518 ) = peel_blocks_with_stmt(else_if_then).kind
520 let params = is_clamp_meta_pattern(
522 &BinaryOp::new(peel_blocks(cond))?,
523 &BinaryOp::new(peel_blocks(else_if_cond))?,
524 peel_blocks(maybe_min_max_first),
525 peel_blocks(maybe_min_max_second),
528 if !eq_expr_value(cx, maybe_input_first_path, maybe_input_second_path) {
531 Some(ClampSuggestion {
534 make_assignment: Some(maybe_input_first_path),
535 hir_with_ignore_attr: None,
542 /// `ExprKind::Binary` but more narrowly typed
543 #[derive(Debug, Clone, Copy)]
544 struct BinaryOp<'tcx> {
546 left: &'tcx Expr<'tcx>,
547 right: &'tcx Expr<'tcx>,
550 impl<'tcx> BinaryOp<'tcx> {
551 fn new(e: &'tcx Expr<'tcx>) -> Option<BinaryOp<'tcx>> {
553 ExprKind::Binary(op, left, right) => Some(BinaryOp {
562 fn flip(&self) -> Self {
565 BinOpKind::Le => BinOpKind::Ge,
566 BinOpKind::Lt => BinOpKind::Gt,
567 BinOpKind::Ge => BinOpKind::Le,
568 BinOpKind::Gt => BinOpKind::Lt,
577 /// The clamp meta pattern is a pattern shared between many (but not all) patterns.
578 /// In summary, this pattern consists of two if statements that meet many criteria,
579 /// - binary operators that are one of [`>`, `<`, `>=`, `<=`].
580 /// - Both binary statements must have a shared argument
581 /// - Which can appear on the left or right side of either statement
582 /// - The binary operators must define a finite range for the shared argument. To put this in
583 /// the terms of Rust `std` library, the following ranges are acceptable
585 /// - `RangeInclusive`
586 /// And all other range types are not accepted. For the purposes of `clamp` it's irrelevant
587 /// whether the range is inclusive or not, the output is the same.
588 /// - The result of each if statement must be equal to the argument unique to that if statement. The
589 /// result can not be the shared argument in either case.
590 fn is_clamp_meta_pattern<'tcx>(
591 cx: &LateContext<'tcx>,
592 first_bin: &BinaryOp<'tcx>,
593 second_bin: &BinaryOp<'tcx>,
594 first_expr: &'tcx Expr<'tcx>,
595 second_expr: &'tcx Expr<'tcx>,
596 // This parameters is exclusively for the match pattern.
597 // It exists because the variable bindings used in that pattern
598 // refer to the variable bound in the match arm, not the variable
599 // bound outside of it. Fortunately due to context we know this has to
600 // be the input variable, not the min or max.
601 input_hir_ids: Option<(HirId, HirId)>,
602 ) -> Option<InputMinMax<'tcx>> {
604 cx: &LateContext<'tcx>,
605 first_bin: &BinaryOp<'tcx>,
606 second_bin: &BinaryOp<'tcx>,
607 first_expr: &'tcx Expr<'tcx>,
608 second_expr: &'tcx Expr<'tcx>,
609 input_hir_ids: Option<(HirId, HirId)>,
611 ) -> Option<InputMinMax<'tcx>> {
612 match (&first_bin.op, &second_bin.op) {
613 (BinOpKind::Ge | BinOpKind::Gt, BinOpKind::Le | BinOpKind::Lt) => {
614 let (min, max) = (second_expr, first_expr);
615 let refers_to_input = match input_hir_ids {
616 Some((first_hir_id, second_hir_id)) => {
617 path_to_local_id(peel_blocks(first_bin.left), first_hir_id)
618 && path_to_local_id(peel_blocks(second_bin.left), second_hir_id)
620 None => eq_expr_value(cx, first_bin.left, second_bin.left),
623 && eq_expr_value(cx, first_bin.right, first_expr)
624 && eq_expr_value(cx, second_bin.right, second_expr))
625 .then_some(InputMinMax {
626 input: first_bin.left,
635 // First filter out any expressions with side effects
644 let clampability = TypeClampability::is_clampable(cx, cx.typeck_results().expr_ty(first_expr))?;
645 let is_float = clampability.is_float();
646 if exprs.iter().any(|e| peel_blocks(e).can_have_side_effects()) {
649 if !(is_ord_op(first_bin.op) && is_ord_op(second_bin.op)) {
653 (*first_bin, *second_bin),
654 (first_bin.flip(), second_bin.flip()),
655 (first_bin.flip(), *second_bin),
656 (*first_bin, second_bin.flip()),
659 cases.into_iter().find_map(|(first, second)| {
660 check(cx, &first, &second, first_expr, second_expr, input_hir_ids, is_float).or_else(|| {
667 input_hir_ids.map(|(l, r)| (r, l)),
674 fn block_stmt_with_last<'tcx>(block: &'tcx Block<'tcx>) -> impl Iterator<Item = MaybeBorrowedStmtKind<'tcx>> {
678 .map(|s| MaybeBorrowedStmtKind::Borrowed(&s.kind))
683 .map(|e| MaybeBorrowedStmtKind::Owned(StmtKind::Expr(e))),
687 fn is_ord_op(op: BinOpKind) -> bool {
688 matches!(op, BinOpKind::Ge | BinOpKind::Gt | BinOpKind::Le | BinOpKind::Lt)
691 /// Really similar to Cow, but doesn't have a `Clone` requirement.
693 enum MaybeBorrowedStmtKind<'a> {
694 Borrowed(&'a StmtKind<'a>),
698 impl<'a> Clone for MaybeBorrowedStmtKind<'a> {
699 fn clone(&self) -> Self {
701 Self::Borrowed(t) => Self::Borrowed(t),
702 Self::Owned(StmtKind::Expr(e)) => Self::Owned(StmtKind::Expr(e)),
703 Self::Owned(_) => unreachable!("Owned should only ever contain a StmtKind::Expr."),
708 impl<'a> Deref for MaybeBorrowedStmtKind<'a> {
709 type Target = StmtKind<'a>;
711 fn deref(&self) -> &Self::Target {
713 Self::Borrowed(t) => t,