| use clippy_config::Conf; | |
| use clippy_utils::diagnostics::span_lint_and_help; | |
| use clippy_utils::msrvs::{self, Msrv}; | |
| use clippy_utils::res::MaybeResPath; | |
| use clippy_utils::visitors::local_used_once; | |
| use clippy_utils::{get_enclosing_block, is_from_proc_macro}; | |
| use itertools::Itertools; | |
| use rustc_ast::LitKind; | |
| use rustc_hir::{Expr, ExprKind, Node, PatKind}; | |
| use rustc_lint::{LateContext, LateLintPass, LintContext}; | |
| use rustc_middle::ty::{self, Ty}; | |
| use rustc_session::impl_lint_pass; | |
| use std::iter::once; | |
| 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]); | |
| pub struct TupleArrayConversions { | |
| msrv: Msrv, | |
| } | |
| impl TupleArrayConversions { | |
| pub fn new(conf: &'static Conf) -> Self { | |
| Self { msrv: conf.msrv } | |
| } | |
| } | |
| impl LateLintPass<'_> for TupleArrayConversions { | |
| fn check_expr<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { | |
| if expr.span.in_external_macro(cx.sess().source_map()) || !self.msrv.meets(cx, 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), | |
| _ => {}, | |
| } | |
| } | |
| } | |
| fn check_array<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, elements: &'tcx [Expr<'tcx>]) { | |
| let Some(ty) = cx.typeck_results().expr_ty(expr).builtin_index() 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], 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, 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>], | |
| elements: &[Expr<'_>], | |
| locals: &[&Expr<'_>], | |
| kind: ToType, | |
| ) -> bool { | |
| let Some(locals) = locals.iter().map(|e| e.res_local_id()).collect::<Option<Vec<_>>>() else { | |
| return false; | |
| }; | |
| let local_parents = locals.iter().map(|l| cx.tcx.parent_hir_node(*l)).collect::<Vec<_>>(); | |
| local_parents | |
| .iter() | |
| .map(|node| match node { | |
| Node::Pat(pat) => kind.eq(&pat.kind).then_some(pat.hir_id), | |
| Node::LetStmt(l) => Some(l.hir_id), | |
| _ => None, | |
| }) | |
| .all_equal() | |
| && locals.iter().zip(local_parents.iter()).all(|(&l, &parent)| { | |
| if let Node::LetStmt(_) = parent { | |
| return true; | |
| } | |
| let Some(b) = get_enclosing_block(cx, l) else { | |
| return true; | |
| }; | |
| local_used_once(cx, b, l).is_some() | |
| }) | |
| && local_parents.first().is_some_and(|node| { | |
| let Some(ty) = match node { | |
| Node::Pat(pat) | |
| if let PatKind::Tuple(pats, _) | PatKind::Slice(pats, None, []) = &pat.kind | |
| && pats.iter().zip(locals.iter()).all(|(p, l)| { | |
| if let PatKind::Binding(_, id, _, _) = p.kind { | |
| id == *l | |
| } else { | |
| true | |
| } | |
| }) => | |
| { | |
| Some(pat.hir_id) | |
| }, | |
| Node::LetStmt(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_to_target_usize(cx.tcx) 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(_, _, _)), | |
| } | |
| } | |
| } |