214 lines
7.8 KiB
Rust
214 lines
7.8 KiB
Rust
use clippy_config::msrvs::{self, Msrv};
|
|
use clippy_utils::diagnostics::span_lint_and_help;
|
|
use clippy_utils::visitors::for_each_local_use_after_expr;
|
|
use clippy_utils::{is_from_proc_macro, path_to_local};
|
|
use itertools::Itertools;
|
|
use rustc_ast::LitKind;
|
|
use rustc_hir::{Expr, ExprKind, Node, PatKind};
|
|
use rustc_lint::{LateContext, LateLintPass, LintContext};
|
|
use rustc_middle::lint::in_external_macro;
|
|
use rustc_middle::ty::{self, Ty};
|
|
use rustc_session::impl_lint_pass;
|
|
use std::iter::once;
|
|
use std::ops::ControlFlow;
|
|
|
|
declare_clippy_lint! {
|
|
/// ### What it does
|
|
/// Checks for tuple<=>array conversions that are not done with `.into()`.
|
|
///
|
|
/// ### Why is this bad?
|
|
/// It may be unnecessary complexity. `.into()` works for converting tuples<=> arrays of up to
|
|
/// 12 elements and conveys the intent more clearly, while also leaving less room for hard to
|
|
/// spot bugs!
|
|
///
|
|
/// ### Known issues
|
|
/// The suggested code may hide potential asymmetry in some cases. See
|
|
/// [#11085](https://github.com/rust-lang/rust-clippy/issues/11085) for more info.
|
|
///
|
|
/// ### Example
|
|
/// ```rust,ignore
|
|
/// let t1 = &[(1, 2), (3, 4)];
|
|
/// let v1: Vec<[u32; 2]> = t1.iter().map(|&(a, b)| [a, b]).collect();
|
|
/// ```
|
|
/// Use instead:
|
|
/// ```rust,ignore
|
|
/// let t1 = &[(1, 2), (3, 4)];
|
|
/// let v1: Vec<[u32; 2]> = t1.iter().map(|&t| t.into()).collect();
|
|
/// ```
|
|
#[clippy::version = "1.72.0"]
|
|
pub TUPLE_ARRAY_CONVERSIONS,
|
|
nursery,
|
|
"checks for tuple<=>array conversions that are not done with `.into()`"
|
|
}
|
|
impl_lint_pass!(TupleArrayConversions => [TUPLE_ARRAY_CONVERSIONS]);
|
|
|
|
#[derive(Clone)]
|
|
pub struct TupleArrayConversions {
|
|
pub msrv: Msrv,
|
|
}
|
|
|
|
impl LateLintPass<'_> for TupleArrayConversions {
|
|
fn check_expr<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
|
|
if in_external_macro(cx.sess(), expr.span) || !self.msrv.meets(msrvs::TUPLE_ARRAY_CONVERSIONS) {
|
|
return;
|
|
}
|
|
|
|
match expr.kind {
|
|
ExprKind::Array(elements) if (1..=12).contains(&elements.len()) => check_array(cx, expr, elements),
|
|
ExprKind::Tup(elements) if (1..=12).contains(&elements.len()) => check_tuple(cx, expr, elements),
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
extract_msrv_attr!(LateContext);
|
|
}
|
|
|
|
fn check_array<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, elements: &'tcx [Expr<'tcx>]) {
|
|
let (ty::Array(ty, _) | ty::Slice(ty)) = cx.typeck_results().expr_ty(expr).kind() else {
|
|
unreachable!("`expr` must be an array or slice due to `ExprKind::Array`");
|
|
};
|
|
|
|
if let [first, ..] = elements
|
|
&& let Some(locals) = (match first.kind {
|
|
ExprKind::Field(_, _) => elements
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, f)| -> Option<&'tcx Expr<'tcx>> {
|
|
let ExprKind::Field(lhs, ident) = f.kind else {
|
|
return None;
|
|
};
|
|
(ident.name.as_str() == i.to_string()).then_some(lhs)
|
|
})
|
|
.collect::<Option<Vec<_>>>(),
|
|
ExprKind::Path(_) => Some(elements.iter().collect()),
|
|
_ => None,
|
|
})
|
|
&& all_bindings_are_for_conv(cx, &[*ty], expr, elements, &locals, ToType::Array)
|
|
&& !is_from_proc_macro(cx, expr)
|
|
{
|
|
span_lint_and_help(
|
|
cx,
|
|
TUPLE_ARRAY_CONVERSIONS,
|
|
expr.span,
|
|
"it looks like you're trying to convert a tuple to an array",
|
|
None,
|
|
"use `.into()` instead, or `<[T; N]>::from` if type annotations are needed",
|
|
);
|
|
}
|
|
}
|
|
|
|
fn check_tuple<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, elements: &'tcx [Expr<'tcx>]) {
|
|
if let ty::Tuple(tys) = cx.typeck_results().expr_ty(expr).kind()
|
|
&& let [first, ..] = elements
|
|
// Fix #11100
|
|
&& tys.iter().all_equal()
|
|
&& let Some(locals) = (match first.kind {
|
|
ExprKind::Index(..) => elements
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, i_expr)| -> Option<&'tcx Expr<'tcx>> {
|
|
if let ExprKind::Index(lhs, index, _) = i_expr.kind
|
|
&& let ExprKind::Lit(lit) = index.kind
|
|
&& let LitKind::Int(val, _) = lit.node
|
|
{
|
|
return (val == i as u128).then_some(lhs);
|
|
};
|
|
|
|
None
|
|
})
|
|
.collect::<Option<Vec<_>>>(),
|
|
ExprKind::Path(_) => Some(elements.iter().collect()),
|
|
_ => None,
|
|
})
|
|
&& all_bindings_are_for_conv(cx, tys, expr, elements, &locals, ToType::Tuple)
|
|
&& !is_from_proc_macro(cx, expr)
|
|
{
|
|
span_lint_and_help(
|
|
cx,
|
|
TUPLE_ARRAY_CONVERSIONS,
|
|
expr.span,
|
|
"it looks like you're trying to convert an array to a tuple",
|
|
None,
|
|
"use `.into()` instead, or `<(T0, T1, ..., Tn)>::from` if type annotations are needed",
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Checks that every binding in `elements` comes from the same parent `Pat` with the kind if there
|
|
/// is a parent `Pat`. Returns false in any of the following cases:
|
|
/// * `kind` does not match `pat.kind`
|
|
/// * one or more elements in `elements` is not a binding
|
|
/// * one or more bindings does not have the same parent `Pat`
|
|
/// * one or more bindings are used after `expr`
|
|
/// * the bindings do not all have the same type
|
|
#[expect(clippy::cast_possible_truncation)]
|
|
fn all_bindings_are_for_conv<'tcx>(
|
|
cx: &LateContext<'tcx>,
|
|
final_tys: &[Ty<'tcx>],
|
|
expr: &Expr<'_>,
|
|
elements: &[Expr<'_>],
|
|
locals: &[&Expr<'_>],
|
|
kind: ToType,
|
|
) -> bool {
|
|
let Some(locals) = locals.iter().map(|e| path_to_local(e)).collect::<Option<Vec<_>>>() else {
|
|
return false;
|
|
};
|
|
let Some(local_parents) = locals
|
|
.iter()
|
|
.map(|&l| cx.tcx.hir().find_parent(l))
|
|
.collect::<Option<Vec<_>>>()
|
|
else {
|
|
return false;
|
|
};
|
|
|
|
local_parents
|
|
.iter()
|
|
.map(|node| match node {
|
|
Node::Pat(pat) => kind.eq(&pat.kind).then_some(pat.hir_id),
|
|
Node::Local(l) => Some(l.hir_id),
|
|
_ => None,
|
|
})
|
|
.all_equal()
|
|
// Fix #11124, very convenient utils function! ❤️
|
|
&& locals
|
|
.iter()
|
|
.all(|&l| for_each_local_use_after_expr(cx, l, expr.hir_id, |_| ControlFlow::Break::<()>(())).is_continue())
|
|
&& local_parents.first().is_some_and(|node| {
|
|
let Some(ty) = match node {
|
|
Node::Pat(pat) => Some(pat.hir_id),
|
|
Node::Local(l) => Some(l.hir_id),
|
|
_ => None,
|
|
}
|
|
.map(|hir_id| cx.typeck_results().node_type(hir_id)) else {
|
|
return false;
|
|
};
|
|
match (kind, ty.kind()) {
|
|
// Ensure the final type and the original type have the same length, and that there
|
|
// is no implicit `&mut`<=>`&` anywhere (#11100). Bit ugly, I know, but it works.
|
|
(ToType::Array, ty::Tuple(tys)) => {
|
|
tys.len() == elements.len() && tys.iter().chain(final_tys.iter().copied()).all_equal()
|
|
},
|
|
(ToType::Tuple, ty::Array(ty, len)) => {
|
|
let Some(len) = len.try_eval_target_usize(cx.tcx, cx.param_env) else { return false };
|
|
len as usize == elements.len() && final_tys.iter().chain(once(ty)).all_equal()
|
|
},
|
|
_ => false,
|
|
}
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum ToType {
|
|
Array,
|
|
Tuple,
|
|
}
|
|
|
|
impl PartialEq<PatKind<'_>> for ToType {
|
|
fn eq(&self, other: &PatKind<'_>) -> bool {
|
|
match self {
|
|
ToType::Array => matches!(other, PatKind::Tuple(_, _)),
|
|
ToType::Tuple => matches!(other, PatKind::Slice(_, _, _)),
|
|
}
|
|
}
|
|
}
|