diff options
Diffstat (limited to 'src/tools/clippy/clippy_lints/src/manual_clamp.rs')
-rw-r--r-- | src/tools/clippy/clippy_lints/src/manual_clamp.rs | 717 |
1 files changed, 717 insertions, 0 deletions
diff --git a/src/tools/clippy/clippy_lints/src/manual_clamp.rs b/src/tools/clippy/clippy_lints/src/manual_clamp.rs new file mode 100644 index 000000000..02dc8755d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_clamp.rs @@ -0,0 +1,717 @@ +use itertools::Itertools; +use rustc_errors::Diagnostic; +use rustc_hir::{ + def::Res, Arm, BinOpKind, Block, Expr, ExprKind, Guard, HirId, PatKind, PathSegment, PrimTy, QPath, StmtKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::Ty; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{symbol::sym, Span}; +use std::ops::Deref; + +use clippy_utils::{ + diagnostics::{span_lint_and_then, span_lint_hir_and_then}, + eq_expr_value, + higher::If, + is_diag_trait_item, is_trait_method, meets_msrv, msrvs, path_res, path_to_local_id, peel_blocks, + peel_blocks_with_stmt, + sugg::Sugg, + ty::implements_trait, + visitors::is_const_evaluatable, + MaybePath, +}; +use rustc_errors::Applicability; + +declare_clippy_lint! { + /// ### What it does + /// Identifies good opportunities for a clamp function from std or core, and suggests using it. + /// + /// ### Why is this bad? + /// clamp is much shorter, easier to read, and doesn't use any control flow. + /// + /// ### Known issue(s) + /// If the clamped variable is NaN this suggestion will cause the code to propagate NaN + /// rather than returning either `max` or `min`. + /// + /// `clamp` functions will panic if `max < min`, `max.is_nan()`, or `min.is_nan()`. + /// Some may consider panicking in these situations to be desirable, but it also may + /// introduce panicking where there wasn't any before. + /// + /// ### Examples + /// ```rust + /// # let (input, min, max) = (0, -2, 1); + /// if input > max { + /// max + /// } else if input < min { + /// min + /// } else { + /// input + /// } + /// # ; + /// ``` + /// + /// ```rust + /// # let (input, min, max) = (0, -2, 1); + /// input.max(min).min(max) + /// # ; + /// ``` + /// + /// ```rust + /// # let (input, min, max) = (0, -2, 1); + /// match input { + /// x if x > max => max, + /// x if x < min => min, + /// x => x, + /// } + /// # ; + /// ``` + /// + /// ```rust + /// # let (input, min, max) = (0, -2, 1); + /// let mut x = input; + /// if x < min { x = min; } + /// if x > max { x = max; } + /// ``` + /// Use instead: + /// ```rust + /// # let (input, min, max) = (0, -2, 1); + /// input.clamp(min, max) + /// # ; + /// ``` + #[clippy::version = "1.66.0"] + pub MANUAL_CLAMP, + complexity, + "using a clamp pattern instead of the clamp function" +} +impl_lint_pass!(ManualClamp => [MANUAL_CLAMP]); + +pub struct ManualClamp { + msrv: Option<RustcVersion>, +} + +impl ManualClamp { + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +#[derive(Debug)] +struct ClampSuggestion<'tcx> { + params: InputMinMax<'tcx>, + span: Span, + make_assignment: Option<&'tcx Expr<'tcx>>, + hir_with_ignore_attr: Option<HirId>, +} + +#[derive(Debug)] +struct InputMinMax<'tcx> { + input: &'tcx Expr<'tcx>, + min: &'tcx Expr<'tcx>, + max: &'tcx Expr<'tcx>, + is_float: bool, +} + +impl<'tcx> LateLintPass<'tcx> for ManualClamp { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if !meets_msrv(self.msrv, msrvs::CLAMP) { + return; + } + if !expr.span.from_expansion() { + let suggestion = is_if_elseif_else_pattern(cx, expr) + .or_else(|| is_max_min_pattern(cx, expr)) + .or_else(|| is_call_max_min_pattern(cx, expr)) + .or_else(|| is_match_pattern(cx, expr)) + .or_else(|| is_if_elseif_pattern(cx, expr)); + if let Some(suggestion) = suggestion { + emit_suggestion(cx, &suggestion); + } + } + } + + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) { + if !meets_msrv(self.msrv, msrvs::CLAMP) { + return; + } + for suggestion in is_two_if_pattern(cx, block) { + emit_suggestion(cx, &suggestion); + } + } + extract_msrv_attr!(LateContext); +} + +fn emit_suggestion<'tcx>(cx: &LateContext<'tcx>, suggestion: &ClampSuggestion<'tcx>) { + let ClampSuggestion { + params: InputMinMax { + input, + min, + max, + is_float, + }, + span, + make_assignment, + hir_with_ignore_attr, + } = suggestion; + let input = Sugg::hir(cx, input, "..").maybe_par(); + let min = Sugg::hir(cx, min, ".."); + let max = Sugg::hir(cx, max, ".."); + let semicolon = if make_assignment.is_some() { ";" } else { "" }; + let assignment = if let Some(assignment) = make_assignment { + let assignment = Sugg::hir(cx, assignment, ".."); + format!("{assignment} = ") + } else { + String::new() + }; + let suggestion = format!("{assignment}{input}.clamp({min}, {max}){semicolon}"); + let msg = "clamp-like pattern without using clamp function"; + let lint_builder = |d: &mut Diagnostic| { + d.span_suggestion(*span, "replace with clamp", suggestion, Applicability::MaybeIncorrect); + if *is_float { + d.note("clamp will panic if max < min, min.is_nan(), or max.is_nan()") + .note("clamp returns NaN if the input is NaN"); + } else { + d.note("clamp will panic if max < min"); + } + }; + if let Some(hir_id) = hir_with_ignore_attr { + span_lint_hir_and_then(cx, MANUAL_CLAMP, *hir_id, *span, msg, lint_builder); + } else { + span_lint_and_then(cx, MANUAL_CLAMP, *span, msg, lint_builder); + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum TypeClampability { + Float, + Ord, +} + +impl TypeClampability { + fn is_clampable<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<TypeClampability> { + if ty.is_floating_point() { + Some(TypeClampability::Float) + } else if cx + .tcx + .get_diagnostic_item(sym::Ord) + .map_or(false, |id| implements_trait(cx, ty, id, &[])) + { + Some(TypeClampability::Ord) + } else { + None + } + } + + fn is_float(self) -> bool { + matches!(self, TypeClampability::Float) + } +} + +/// Targets patterns like +/// +/// ``` +/// # let (input, min, max) = (0, -3, 12); +/// +/// if input < min { +/// min +/// } else if input > max { +/// max +/// } else { +/// input +/// } +/// # ; +/// ``` +fn is_if_elseif_else_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> { + if let Some(If { + cond, + then, + r#else: Some(else_if), + }) = If::hir(expr) + && let Some(If { + cond: else_if_cond, + then: else_if_then, + r#else: Some(else_body), + }) = If::hir(peel_blocks(else_if)) + { + let params = is_clamp_meta_pattern( + cx, + &BinaryOp::new(peel_blocks(cond))?, + &BinaryOp::new(peel_blocks(else_if_cond))?, + peel_blocks(then), + peel_blocks(else_if_then), + None, + )?; + // Contents of the else should be the resolved input. + if !eq_expr_value(cx, params.input, peel_blocks(else_body)) { + return None; + } + Some(ClampSuggestion { + params, + span: expr.span, + make_assignment: None, + hir_with_ignore_attr: None, + }) + } else { + None + } +} + +/// Targets patterns like +/// +/// ``` +/// # let (input, min_value, max_value) = (0, -3, 12); +/// +/// input.max(min_value).min(max_value) +/// # ; +/// ``` +fn is_max_min_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> { + if let ExprKind::MethodCall(seg_second, receiver, [arg_second], _) = &expr.kind + && (cx.typeck_results().expr_ty_adjusted(receiver).is_floating_point() || is_trait_method(cx, expr, sym::Ord)) + && let ExprKind::MethodCall(seg_first, input, [arg_first], _) = &receiver.kind + && (cx.typeck_results().expr_ty_adjusted(input).is_floating_point() || is_trait_method(cx, receiver, sym::Ord)) + { + let is_float = cx.typeck_results().expr_ty_adjusted(input).is_floating_point(); + let (min, max) = match (seg_first.ident.as_str(), seg_second.ident.as_str()) { + ("min", "max") => (arg_second, arg_first), + ("max", "min") => (arg_first, arg_second), + _ => return None, + }; + Some(ClampSuggestion { + params: InputMinMax { input, min, max, is_float }, + span: expr.span, + make_assignment: None, + hir_with_ignore_attr: None, + }) + } else { + None + } +} + +/// Targets patterns like +/// +/// ``` +/// # let (input, min_value, max_value) = (0, -3, 12); +/// # use std::cmp::{max, min}; +/// min(max(input, min_value), max_value) +/// # ; +/// ``` +fn is_call_max_min_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> { + fn segment<'tcx>(cx: &LateContext<'_>, func: &Expr<'tcx>) -> Option<FunctionType<'tcx>> { + match func.kind { + ExprKind::Path(QPath::Resolved(None, path)) => { + let id = path.res.opt_def_id()?; + match cx.tcx.get_diagnostic_name(id) { + Some(sym::cmp_min) => Some(FunctionType::CmpMin), + Some(sym::cmp_max) => Some(FunctionType::CmpMax), + _ if is_diag_trait_item(cx, id, sym::Ord) => { + Some(FunctionType::OrdOrFloat(path.segments.last().expect("infallible"))) + }, + _ => None, + } + }, + ExprKind::Path(QPath::TypeRelative(ty, seg)) => { + matches!(path_res(cx, ty), Res::PrimTy(PrimTy::Float(_))).then(|| FunctionType::OrdOrFloat(seg)) + }, + _ => None, + } + } + + enum FunctionType<'tcx> { + CmpMin, + CmpMax, + OrdOrFloat(&'tcx PathSegment<'tcx>), + } + + fn check<'tcx>( + cx: &LateContext<'tcx>, + outer_fn: &'tcx Expr<'tcx>, + inner_call: &'tcx Expr<'tcx>, + outer_arg: &'tcx Expr<'tcx>, + span: Span, + ) -> Option<ClampSuggestion<'tcx>> { + if let ExprKind::Call(inner_fn, [first, second]) = &inner_call.kind + && let Some(inner_seg) = segment(cx, inner_fn) + && let Some(outer_seg) = segment(cx, outer_fn) + { + let (input, inner_arg) = match (is_const_evaluatable(cx, first), is_const_evaluatable(cx, second)) { + (true, false) => (second, first), + (false, true) => (first, second), + _ => return None, + }; + let is_float = cx.typeck_results().expr_ty_adjusted(input).is_floating_point(); + let (min, max) = match (inner_seg, outer_seg) { + (FunctionType::CmpMin, FunctionType::CmpMax) => (outer_arg, inner_arg), + (FunctionType::CmpMax, FunctionType::CmpMin) => (inner_arg, outer_arg), + (FunctionType::OrdOrFloat(first_segment), FunctionType::OrdOrFloat(second_segment)) => { + match (first_segment.ident.as_str(), second_segment.ident.as_str()) { + ("min", "max") => (outer_arg, inner_arg), + ("max", "min") => (inner_arg, outer_arg), + _ => return None, + } + } + _ => return None, + }; + Some(ClampSuggestion { + params: InputMinMax { input, min, max, is_float }, + span, + make_assignment: None, + hir_with_ignore_attr: None, + }) + } else { + None + } + } + + if let ExprKind::Call(outer_fn, [first, second]) = &expr.kind { + check(cx, outer_fn, first, second, expr.span).or_else(|| check(cx, outer_fn, second, first, expr.span)) + } else { + None + } +} + +/// Targets patterns like +/// +/// ``` +/// # let (input, min, max) = (0, -3, 12); +/// +/// match input { +/// input if input > max => max, +/// input if input < min => min, +/// input => input, +/// } +/// # ; +/// ``` +fn is_match_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> { + if let ExprKind::Match(value, [first_arm, second_arm, last_arm], rustc_hir::MatchSource::Normal) = &expr.kind { + // Find possible min/max branches + let minmax_values = |a: &'tcx Arm<'tcx>| { + if let PatKind::Binding(_, var_hir_id, _, None) = &a.pat.kind + && let Some(Guard::If(e)) = a.guard { + Some((e, var_hir_id, a.body)) + } else { + None + } + }; + let (first, first_hir_id, first_expr) = minmax_values(first_arm)?; + let (second, second_hir_id, second_expr) = minmax_values(second_arm)?; + let first = BinaryOp::new(first)?; + let second = BinaryOp::new(second)?; + if let PatKind::Binding(_, binding, _, None) = &last_arm.pat.kind + && path_to_local_id(peel_blocks_with_stmt(last_arm.body), *binding) + && last_arm.guard.is_none() + { + // Proceed as normal + } else { + return None; + } + if let Some(params) = is_clamp_meta_pattern( + cx, + &first, + &second, + first_expr, + second_expr, + Some((*first_hir_id, *second_hir_id)), + ) { + return Some(ClampSuggestion { + params: InputMinMax { + input: value, + min: params.min, + max: params.max, + is_float: params.is_float, + }, + span: expr.span, + make_assignment: None, + hir_with_ignore_attr: None, + }); + } + } + None +} + +/// Targets patterns like +/// +/// ``` +/// # let (input, min, max) = (0, -3, 12); +/// +/// let mut x = input; +/// if x < min { x = min; } +/// if x > max { x = max; } +/// ``` +fn is_two_if_pattern<'tcx>(cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) -> Vec<ClampSuggestion<'tcx>> { + block_stmt_with_last(block) + .tuple_windows() + .filter_map(|(maybe_set_first, maybe_set_second)| { + if let StmtKind::Expr(first_expr) = *maybe_set_first + && let StmtKind::Expr(second_expr) = *maybe_set_second + && let Some(If { cond: first_cond, then: first_then, r#else: None }) = If::hir(first_expr) + && let Some(If { cond: second_cond, then: second_then, r#else: None }) = If::hir(second_expr) + && let ExprKind::Assign( + maybe_input_first_path, + maybe_min_max_first, + _ + ) = peel_blocks_with_stmt(first_then).kind + && let ExprKind::Assign( + maybe_input_second_path, + maybe_min_max_second, + _ + ) = peel_blocks_with_stmt(second_then).kind + && eq_expr_value(cx, maybe_input_first_path, maybe_input_second_path) + && let Some(first_bin) = BinaryOp::new(first_cond) + && let Some(second_bin) = BinaryOp::new(second_cond) + && let Some(input_min_max) = is_clamp_meta_pattern( + cx, + &first_bin, + &second_bin, + maybe_min_max_first, + maybe_min_max_second, + None + ) + { + Some(ClampSuggestion { + params: InputMinMax { + input: maybe_input_first_path, + min: input_min_max.min, + max: input_min_max.max, + is_float: input_min_max.is_float, + }, + span: first_expr.span.to(second_expr.span), + make_assignment: Some(maybe_input_first_path), + hir_with_ignore_attr: Some(first_expr.hir_id()), + }) + } else { + None + } + }) + .collect() +} + +/// Targets patterns like +/// +/// ``` +/// # let (mut input, min, max) = (0, -3, 12); +/// +/// if input < min { +/// input = min; +/// } else if input > max { +/// input = max; +/// } +/// ``` +fn is_if_elseif_pattern<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<ClampSuggestion<'tcx>> { + if let Some(If { + cond, + then, + r#else: Some(else_if), + }) = If::hir(expr) + && let Some(If { + cond: else_if_cond, + then: else_if_then, + r#else: None, + }) = If::hir(peel_blocks(else_if)) + && let ExprKind::Assign( + maybe_input_first_path, + maybe_min_max_first, + _ + ) = peel_blocks_with_stmt(then).kind + && let ExprKind::Assign( + maybe_input_second_path, + maybe_min_max_second, + _ + ) = peel_blocks_with_stmt(else_if_then).kind + { + let params = is_clamp_meta_pattern( + cx, + &BinaryOp::new(peel_blocks(cond))?, + &BinaryOp::new(peel_blocks(else_if_cond))?, + peel_blocks(maybe_min_max_first), + peel_blocks(maybe_min_max_second), + None, + )?; + if !eq_expr_value(cx, maybe_input_first_path, maybe_input_second_path) { + return None; + } + Some(ClampSuggestion { + params, + span: expr.span, + make_assignment: Some(maybe_input_first_path), + hir_with_ignore_attr: None, + }) + } else { + None + } +} + +/// `ExprKind::Binary` but more narrowly typed +#[derive(Debug, Clone, Copy)] +struct BinaryOp<'tcx> { + op: BinOpKind, + left: &'tcx Expr<'tcx>, + right: &'tcx Expr<'tcx>, +} + +impl<'tcx> BinaryOp<'tcx> { + fn new(e: &'tcx Expr<'tcx>) -> Option<BinaryOp<'tcx>> { + match &e.kind { + ExprKind::Binary(op, left, right) => Some(BinaryOp { + op: op.node, + left, + right, + }), + _ => None, + } + } + + fn flip(&self) -> Self { + Self { + op: match self.op { + BinOpKind::Le => BinOpKind::Ge, + BinOpKind::Lt => BinOpKind::Gt, + BinOpKind::Ge => BinOpKind::Le, + BinOpKind::Gt => BinOpKind::Lt, + other => other, + }, + left: self.right, + right: self.left, + } + } +} + +/// The clamp meta pattern is a pattern shared between many (but not all) patterns. +/// In summary, this pattern consists of two if statements that meet many criteria, +/// - binary operators that are one of [`>`, `<`, `>=`, `<=`]. +/// - Both binary statements must have a shared argument +/// - Which can appear on the left or right side of either statement +/// - The binary operators must define a finite range for the shared argument. To put this in +/// the terms of Rust `std` library, the following ranges are acceptable +/// - `Range` +/// - `RangeInclusive` +/// And all other range types are not accepted. For the purposes of `clamp` it's irrelevant +/// whether the range is inclusive or not, the output is the same. +/// - The result of each if statement must be equal to the argument unique to that if statement. The +/// result can not be the shared argument in either case. +fn is_clamp_meta_pattern<'tcx>( + cx: &LateContext<'tcx>, + first_bin: &BinaryOp<'tcx>, + second_bin: &BinaryOp<'tcx>, + first_expr: &'tcx Expr<'tcx>, + second_expr: &'tcx Expr<'tcx>, + // This parameters is exclusively for the match pattern. + // It exists because the variable bindings used in that pattern + // refer to the variable bound in the match arm, not the variable + // bound outside of it. Fortunately due to context we know this has to + // be the input variable, not the min or max. + input_hir_ids: Option<(HirId, HirId)>, +) -> Option<InputMinMax<'tcx>> { + fn check<'tcx>( + cx: &LateContext<'tcx>, + first_bin: &BinaryOp<'tcx>, + second_bin: &BinaryOp<'tcx>, + first_expr: &'tcx Expr<'tcx>, + second_expr: &'tcx Expr<'tcx>, + input_hir_ids: Option<(HirId, HirId)>, + is_float: bool, + ) -> Option<InputMinMax<'tcx>> { + match (&first_bin.op, &second_bin.op) { + (BinOpKind::Ge | BinOpKind::Gt, BinOpKind::Le | BinOpKind::Lt) => { + let (min, max) = (second_expr, first_expr); + let refers_to_input = match input_hir_ids { + Some((first_hir_id, second_hir_id)) => { + path_to_local_id(peel_blocks(first_bin.left), first_hir_id) + && path_to_local_id(peel_blocks(second_bin.left), second_hir_id) + }, + None => eq_expr_value(cx, first_bin.left, second_bin.left), + }; + (refers_to_input + && eq_expr_value(cx, first_bin.right, first_expr) + && eq_expr_value(cx, second_bin.right, second_expr)) + .then_some(InputMinMax { + input: first_bin.left, + min, + max, + is_float, + }) + }, + _ => None, + } + } + // First filter out any expressions with side effects + let exprs = [ + first_bin.left, + first_bin.right, + second_bin.left, + second_bin.right, + first_expr, + second_expr, + ]; + let clampability = TypeClampability::is_clampable(cx, cx.typeck_results().expr_ty(first_expr))?; + let is_float = clampability.is_float(); + if exprs.iter().any(|e| peel_blocks(e).can_have_side_effects()) { + return None; + } + if !(is_ord_op(first_bin.op) && is_ord_op(second_bin.op)) { + return None; + } + let cases = [ + (*first_bin, *second_bin), + (first_bin.flip(), second_bin.flip()), + (first_bin.flip(), *second_bin), + (*first_bin, second_bin.flip()), + ]; + + cases.into_iter().find_map(|(first, second)| { + check(cx, &first, &second, first_expr, second_expr, input_hir_ids, is_float).or_else(|| { + check( + cx, + &second, + &first, + second_expr, + first_expr, + input_hir_ids.map(|(l, r)| (r, l)), + is_float, + ) + }) + }) +} + +fn block_stmt_with_last<'tcx>(block: &'tcx Block<'tcx>) -> impl Iterator<Item = MaybeBorrowedStmtKind<'tcx>> { + block + .stmts + .iter() + .map(|s| MaybeBorrowedStmtKind::Borrowed(&s.kind)) + .chain( + block + .expr + .as_ref() + .map(|e| MaybeBorrowedStmtKind::Owned(StmtKind::Expr(e))), + ) +} + +fn is_ord_op(op: BinOpKind) -> bool { + matches!(op, BinOpKind::Ge | BinOpKind::Gt | BinOpKind::Le | BinOpKind::Lt) +} + +/// Really similar to Cow, but doesn't have a `Clone` requirement. +#[derive(Debug)] +enum MaybeBorrowedStmtKind<'a> { + Borrowed(&'a StmtKind<'a>), + Owned(StmtKind<'a>), +} + +impl<'a> Clone for MaybeBorrowedStmtKind<'a> { + fn clone(&self) -> Self { + match self { + Self::Borrowed(t) => Self::Borrowed(t), + Self::Owned(StmtKind::Expr(e)) => Self::Owned(StmtKind::Expr(e)), + Self::Owned(_) => unreachable!("Owned should only ever contain a StmtKind::Expr."), + } + } +} + +impl<'a> Deref for MaybeBorrowedStmtKind<'a> { + type Target = StmtKind<'a>; + + fn deref(&self) -> &Self::Target { + match self { + Self::Borrowed(t) => t, + Self::Owned(t) => t, + } + } +} |