rust/clippy_lints/src/manual_clamp.rs
2024-09-25 13:52:12 -04:00

764 lines
25 KiB
Rust

use clippy_config::Conf;
use clippy_config::msrvs::{self, Msrv};
use clippy_utils::consts::{ConstEvalCtxt, Constant};
use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then};
use clippy_utils::higher::If;
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::implements_trait;
use clippy_utils::visitors::is_const_evaluatable;
use clippy_utils::{
MaybePath, eq_expr_value, is_diag_trait_item, is_in_const_context, is_trait_method, path_res, path_to_local_id,
peel_blocks, peel_blocks_with_stmt,
};
use itertools::Itertools;
use rustc_errors::{Applicability, Diag};
use rustc_hir::def::Res;
use rustc_hir::{Arm, BinOpKind, Block, Expr, ExprKind, HirId, PatKind, PathSegment, PrimTy, QPath, StmtKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::Ty;
use rustc_session::impl_lint_pass;
use rustc_span::Span;
use rustc_span::symbol::sym;
use std::cmp::Ordering;
use std::ops::Deref;
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.
///
/// ### Limitations
///
/// This lint will only trigger if max and min are known at compile time, and max is
/// greater than min.
///
/// ### 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.
///
/// See also [the discussion in the
/// PR](https://github.com/rust-lang/rust-clippy/pull/9484#issuecomment-1278922613).
///
/// ### Examples
/// ```no_run
/// # let (input, min, max) = (0, -2, 1);
/// if input > max {
/// max
/// } else if input < min {
/// min
/// } else {
/// input
/// }
/// # ;
/// ```
///
/// ```no_run
/// # let (input, min, max) = (0, -2, 1);
/// input.max(min).min(max)
/// # ;
/// ```
///
/// ```no_run
/// # let (input, min, max) = (0, -2, 1);
/// match input {
/// x if x > max => max,
/// x if x < min => min,
/// x => x,
/// }
/// # ;
/// ```
///
/// ```no_run
/// # let (input, min, max) = (0, -2, 1);
/// let mut x = input;
/// if x < min { x = min; }
/// if x > max { x = max; }
/// ```
/// Use instead:
/// ```no_run
/// # 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: Msrv,
}
impl ManualClamp {
pub fn new(conf: &'static Conf) -> Self {
Self {
msrv: conf.msrv.clone(),
}
}
}
#[derive(Debug)]
struct ClampSuggestion<'tcx> {
params: InputMinMax<'tcx>,
span: Span,
make_assignment: Option<&'tcx Expr<'tcx>>,
hir_with_ignore_attr: Option<HirId>,
}
impl<'tcx> ClampSuggestion<'tcx> {
/// This function will return true if and only if you can demonstrate at compile time that min
/// is less than max.
fn min_less_than_max(&self, cx: &LateContext<'tcx>) -> bool {
let max_type = cx.typeck_results().expr_ty(self.params.max);
let min_type = cx.typeck_results().expr_ty(self.params.min);
if max_type != min_type {
return false;
}
let ecx = ConstEvalCtxt::new(cx);
if let Some(max) = ecx.eval(self.params.max)
&& let Some(min) = ecx.eval(self.params.min)
&& let Some(ord) = Constant::partial_cmp(cx.tcx, max_type, &min, &max)
{
ord != Ordering::Greater
} else {
false
}
}
}
#[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 !self.msrv.meets(msrvs::CLAMP) {
return;
}
if !expr.span.from_expansion() && !is_in_const_context(cx) {
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 {
maybe_emit_suggestion(cx, &suggestion);
}
}
}
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) {
if !self.msrv.meets(msrvs::CLAMP) || is_in_const_context(cx) {
return;
}
for suggestion in is_two_if_pattern(cx, block) {
maybe_emit_suggestion(cx, &suggestion);
}
}
extract_msrv_attr!(LateContext);
}
fn maybe_emit_suggestion<'tcx>(cx: &LateContext<'tcx>, suggestion: &ClampSuggestion<'tcx>) {
if !suggestion.min_less_than_max(cx) {
return;
}
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 Diag<'_, ()>| {
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
///
/// ```no_run
/// # 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
///
/// ```no_run
/// # 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
///
/// ```no_run
/// # 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
///
/// ```no_run
/// # 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(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
///
/// ```no_run
/// # 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
///
/// ```no_run
/// # 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 Clone for MaybeBorrowedStmtKind<'_> {
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,
}
}
}