diff options
| author | Philipp Krones <hello@philkrones.com> | 2023-07-31 23:53:53 +0200 |
|---|---|---|
| committer | Philipp Krones <hello@philkrones.com> | 2023-07-31 23:53:53 +0200 |
| commit | b0e64a9c09773e5ac908d89bff3db95d35491308 (patch) | |
| tree | 81571ddba0b66bcc5c6d658fc3adcc7032b1810b /clippy_lints/src | |
| parent | f54263af58f5ae062d382c40c28ed8d9fa9c6935 (diff) | |
| download | rust-b0e64a9c09773e5ac908d89bff3db95d35491308.tar.gz rust-b0e64a9c09773e5ac908d89bff3db95d35491308.zip | |
Merge commit '5436dba826191964ac1d0dab534b7eb6d4c878f6' into clippyup
Diffstat (limited to 'clippy_lints/src')
52 files changed, 2923 insertions, 1353 deletions
diff --git a/clippy_lints/src/absolute_paths.rs b/clippy_lints/src/absolute_paths.rs new file mode 100644 index 00000000000..04417c4c460 --- /dev/null +++ b/clippy_lints/src/absolute_paths.rs @@ -0,0 +1,100 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::source::snippet_opt; +use rustc_data_structures::fx::FxHashSet; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::def_id::{DefId, CRATE_DEF_INDEX}; +use rustc_hir::{HirId, ItemKind, Node, Path}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::kw; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of items through absolute paths, like `std::env::current_dir`. + /// + /// ### Why is this bad? + /// Many codebases have their own style when it comes to importing, but one that is seldom used + /// is using absolute paths *everywhere*. This is generally considered unidiomatic, and you + /// should add a `use` statement. + /// + /// The default maximum segments (2) is pretty strict, you may want to increase this in + /// `clippy.toml`. + /// + /// Note: One exception to this is code from macro expansion - this does not lint such cases, as + /// using absolute paths is the proper way of referencing items in one. + /// + /// ### Example + /// ```rust + /// let x = std::f64::consts::PI; + /// ``` + /// Use any of the below instead, or anything else: + /// ```rust + /// use std::f64; + /// use std::f64::consts; + /// use std::f64::consts::PI; + /// let x = f64::consts::PI; + /// let x = consts::PI; + /// let x = PI; + /// use std::f64::consts as f64_consts; + /// let x = f64_consts::PI; + /// ``` + #[clippy::version = "1.73.0"] + pub ABSOLUTE_PATHS, + restriction, + "checks for usage of an item without a `use` statement" +} +impl_lint_pass!(AbsolutePaths => [ABSOLUTE_PATHS]); + +pub struct AbsolutePaths { + pub absolute_paths_max_segments: u64, + pub absolute_paths_allowed_crates: FxHashSet<String>, +} + +impl LateLintPass<'_> for AbsolutePaths { + // We should only lint `QPath::Resolved`s, but since `Path` is only used in `Resolved` and `UsePath` + // we don't need to use a visitor or anything as we can just check if the `Node` for `hir_id` isn't + // a `Use` + #[expect(clippy::cast_possible_truncation)] + fn check_path(&mut self, cx: &LateContext<'_>, path: &Path<'_>, hir_id: HirId) { + let Self { + absolute_paths_max_segments, + absolute_paths_allowed_crates, + } = self; + + if !path.span.from_expansion() + && let Some(node) = cx.tcx.hir().find(hir_id) + && !matches!(node, Node::Item(item) if matches!(item.kind, ItemKind::Use(_, _))) + && let [first, rest @ ..] = path.segments + // Handle `::std` + && let (segment, len) = if first.ident.name == kw::PathRoot { + // Indexing is fine as `PathRoot` must be followed by another segment. `len() - 1` + // is fine here for the same reason + (&rest[0], path.segments.len() - 1) + } else { + (first, path.segments.len()) + } + && len > *absolute_paths_max_segments as usize + && let Some(segment_snippet) = snippet_opt(cx, segment.ident.span) + && segment_snippet == segment.ident.as_str() + { + let is_abs_external = + matches!(segment.res, Res::Def(DefKind::Mod, DefId { index, .. }) if index == CRATE_DEF_INDEX); + let is_abs_crate = segment.ident.name == kw::Crate; + + if is_abs_external && absolute_paths_allowed_crates.contains(segment.ident.name.as_str()) + || is_abs_crate && absolute_paths_allowed_crates.contains("crate") + { + return; + } + + if is_abs_external || is_abs_crate { + span_lint( + cx, + ABSOLUTE_PATHS, + path.span, + "consider bringing this path into scope with the `use` keyword", + ); + } + } + } +} diff --git a/clippy_lints/src/arc_with_non_send_sync.rs b/clippy_lints/src/arc_with_non_send_sync.rs index 7adcd9ad055..35a04b5e44a 100644 --- a/clippy_lints/src/arc_with_non_send_sync.rs +++ b/clippy_lints/src/arc_with_non_send_sync.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_then; -use clippy_utils::last_path_segment; use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use clippy_utils::{is_from_proc_macro, last_path_segment}; use rustc_hir::{Expr, ExprKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty; @@ -38,10 +38,11 @@ declare_clippy_lint! { } declare_lint_pass!(ArcWithNonSendSync => [ARC_WITH_NON_SEND_SYNC]); -impl LateLintPass<'_> for ArcWithNonSendSync { - fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { - let ty = cx.typeck_results().expr_ty(expr); - if is_type_diagnostic_item(cx, ty, sym::Arc) +impl<'tcx> LateLintPass<'tcx> for ArcWithNonSendSync { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if !expr.span.from_expansion() + && let ty = cx.typeck_results().expr_ty(expr) + && is_type_diagnostic_item(cx, ty, sym::Arc) && let ExprKind::Call(func, [arg]) = expr.kind && let ExprKind::Path(func_path) = func.kind && last_path_segment(&func_path).ident.name == sym::new @@ -54,6 +55,7 @@ impl LateLintPass<'_> for ArcWithNonSendSync { && let Some(sync) = cx.tcx.lang_items().sync_trait() && let [is_send, is_sync] = [send, sync].map(|id| implements_trait(cx, arg_ty, id, &[])) && !(is_send && is_sync) + && !is_from_proc_macro(cx, expr) { span_lint_and_then( cx, diff --git a/clippy_lints/src/casts/mod.rs b/clippy_lints/src/casts/mod.rs index 0ac6ef6496a..d34de305f5d 100644 --- a/clippy_lints/src/casts/mod.rs +++ b/clippy_lints/src/casts/mod.rs @@ -181,6 +181,14 @@ declare_clippy_lint! { /// ### Why is this bad? /// It's just unnecessary. /// + /// ### Known problems + /// When the expression on the left is a function call, the lint considers the return type to be + /// a type alias if it's aliased through a `use` statement + /// (like `use std::io::Result as IoResult`). It will not lint such cases. + /// + /// This check is also rather primitive. It will only work on primitive types without any + /// intermediate references, raw pointers and trait objects may or may not work. + /// /// ### Example /// ```rust /// let _ = 2i32 as i32; diff --git a/clippy_lints/src/casts/unnecessary_cast.rs b/clippy_lints/src/casts/unnecessary_cast.rs index ae56f38d9ad..86057bb74ee 100644 --- a/clippy_lints/src/casts/unnecessary_cast.rs +++ b/clippy_lints/src/casts/unnecessary_cast.rs @@ -85,11 +85,6 @@ pub(super) fn check<'tcx>( } } - // skip cast of fn call that returns type alias - if let ExprKind::Cast(inner, ..) = expr.kind && is_cast_from_ty_alias(cx, inner, cast_from) { - return false; - } - // skip cast to non-primitive type if_chain! { if let ExprKind::Cast(_, cast_to) = expr.kind; @@ -101,6 +96,11 @@ pub(super) fn check<'tcx>( } } + // skip cast of fn call that returns type alias + if let ExprKind::Cast(inner, ..) = expr.kind && is_cast_from_ty_alias(cx, inner, cast_from) { + return false; + } + if let Some(lit) = get_numeric_literal(cast_expr) { let literal_str = &cast_str; @@ -254,14 +254,12 @@ fn is_cast_from_ty_alias<'tcx>(cx: &LateContext<'tcx>, expr: impl Visitable<'tcx // function's declaration snippet is exactly equal to the `Ty`. That way, we can // see whether it's a type alias. // - // Will this work for more complex types? Probably not! + // FIXME: This won't work if the type is given an alias through `use`, should we + // consider this a type alias as well? if !snippet .split("->") - .skip(0) - .map(|s| { - s.trim() == cast_from.to_string() - || s.split("where").any(|ty| ty.trim() == cast_from.to_string()) - }) + .skip(1) + .map(|s| snippet_eq_ty(s, cast_from) || s.split("where").any(|ty| snippet_eq_ty(ty, cast_from))) .any(|a| a) { return ControlFlow::Break(()); @@ -288,3 +286,7 @@ fn is_cast_from_ty_alias<'tcx>(cx: &LateContext<'tcx>, expr: impl Visitable<'tcx }) .is_some() } + +fn snippet_eq_ty(snippet: &str, ty: Ty<'_>) -> bool { + snippet.trim() == ty.to_string() || snippet.trim().contains(&format!("::{ty}")) +} diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 498d657b31f..d4e0d286334 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -37,6 +37,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::utils::internal_lints::produce_ice::PRODUCE_ICE_INFO, #[cfg(feature = "internal")] crate::utils::internal_lints::unnecessary_def_path::UNNECESSARY_DEF_PATH_INFO, + crate::absolute_paths::ABSOLUTE_PATHS_INFO, crate::allow_attributes::ALLOW_ATTRIBUTES_INFO, crate::almost_complete_range::ALMOST_COMPLETE_RANGE_INFO, crate::approx_const::APPROX_CONSTANT_INFO, @@ -155,6 +156,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::enum_variants::MODULE_INCEPTION_INFO, crate::enum_variants::MODULE_NAME_REPETITIONS_INFO, crate::equatable_if_let::EQUATABLE_IF_LET_INFO, + crate::error_impl_error::ERROR_IMPL_ERROR_INFO, crate::escape::BOXED_LOCAL_INFO, crate::eta_reduction::REDUNDANT_CLOSURE_INFO, crate::eta_reduction::REDUNDANT_CLOSURE_FOR_METHOD_CALLS_INFO, @@ -183,6 +185,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING_INFO, crate::formatting::SUSPICIOUS_ELSE_FORMATTING_INFO, crate::formatting::SUSPICIOUS_UNARY_OP_FORMATTING_INFO, + crate::four_forward_slashes::FOUR_FORWARD_SLASHES_INFO, crate::from_over_into::FROM_OVER_INTO_INFO, crate::from_raw_with_void_ptr::FROM_RAW_WITH_VOID_PTR_INFO, crate::from_str_radix_10::FROM_STR_RADIX_10_INFO, @@ -305,6 +308,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::matches::MATCH_WILDCARD_FOR_SINGLE_VARIANTS_INFO, crate::matches::MATCH_WILD_ERR_ARM_INFO, crate::matches::NEEDLESS_MATCH_INFO, + crate::matches::REDUNDANT_GUARDS_INFO, crate::matches::REDUNDANT_PATTERN_MATCHING_INFO, crate::matches::REST_PAT_IN_FULLY_BOUND_STRUCTS_INFO, crate::matches::SIGNIFICANT_DROP_IN_SCRUTINEE_INFO, @@ -333,11 +337,13 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::EXPECT_USED_INFO, crate::methods::EXTEND_WITH_DRAIN_INFO, crate::methods::FILETYPE_IS_FILE_INFO, + crate::methods::FILTER_MAP_BOOL_THEN_INFO, crate::methods::FILTER_MAP_IDENTITY_INFO, crate::methods::FILTER_MAP_NEXT_INFO, crate::methods::FILTER_NEXT_INFO, crate::methods::FLAT_MAP_IDENTITY_INFO, crate::methods::FLAT_MAP_OPTION_INFO, + crate::methods::FORMAT_COLLECT_INFO, crate::methods::FROM_ITER_INSTEAD_OF_COLLECT_INFO, crate::methods::GET_FIRST_INFO, crate::methods::GET_LAST_WITH_LEN_INFO, @@ -358,6 +364,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::ITER_ON_SINGLE_ITEMS_INFO, crate::methods::ITER_OVEREAGER_CLONED_INFO, crate::methods::ITER_SKIP_NEXT_INFO, + crate::methods::ITER_SKIP_ZERO_INFO, crate::methods::ITER_WITH_DRAIN_INFO, crate::methods::MANUAL_FILTER_MAP_INFO, crate::methods::MANUAL_FIND_MAP_INFO, @@ -391,6 +398,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::OR_THEN_UNWRAP_INFO, crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO, crate::methods::RANGE_ZIP_WITH_LEN_INFO, + crate::methods::READONLY_WRITE_LOCK_INFO, crate::methods::READ_LINE_WITHOUT_TRIM_INFO, crate::methods::REPEAT_ONCE_INFO, crate::methods::RESULT_MAP_OR_INTO_OPTION_INFO, @@ -403,6 +411,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::SKIP_WHILE_NEXT_INFO, crate::methods::STABLE_SORT_PRIMITIVE_INFO, crate::methods::STRING_EXTEND_CHARS_INFO, + crate::methods::STRING_LIT_CHARS_ANY_INFO, crate::methods::SUSPICIOUS_COMMAND_ARG_SPACE_INFO, crate::methods::SUSPICIOUS_MAP_INFO, crate::methods::SUSPICIOUS_SPLITN_INFO, @@ -418,7 +427,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::UNNECESSARY_LITERAL_UNWRAP_INFO, crate::methods::UNNECESSARY_SORT_BY_INFO, crate::methods::UNNECESSARY_TO_OWNED_INFO, - crate::methods::UNWRAP_OR_ELSE_DEFAULT_INFO, + crate::methods::UNWRAP_OR_DEFAULT_INFO, crate::methods::UNWRAP_USED_INFO, crate::methods::USELESS_ASREF_INFO, crate::methods::VEC_RESIZE_TO_ZERO_INFO, @@ -555,6 +564,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::redundant_closure_call::REDUNDANT_CLOSURE_CALL_INFO, crate::redundant_else::REDUNDANT_ELSE_INFO, crate::redundant_field_names::REDUNDANT_FIELD_NAMES_INFO, + crate::redundant_locals::REDUNDANT_LOCALS_INFO, crate::redundant_pub_crate::REDUNDANT_PUB_CRATE_INFO, crate::redundant_slicing::DEREF_BY_SLICING_INFO, crate::redundant_slicing::REDUNDANT_SLICING_INFO, @@ -568,6 +578,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::return_self_not_must_use::RETURN_SELF_NOT_MUST_USE_INFO, crate::returns::LET_AND_RETURN_INFO, crate::returns::NEEDLESS_RETURN_INFO, + crate::returns::NEEDLESS_RETURN_WITH_QUESTION_MARK_INFO, crate::same_name_method::SAME_NAME_METHOD_INFO, crate::self_named_constructors::SELF_NAMED_CONSTRUCTORS_INFO, crate::semicolon_block::SEMICOLON_INSIDE_BLOCK_INFO, diff --git a/clippy_lints/src/dereference.rs b/clippy_lints/src/dereference.rs index 8a9d978a106..a5ec979ccd9 100644 --- a/clippy_lints/src/dereference.rs +++ b/clippy_lints/src/dereference.rs @@ -3,21 +3,23 @@ use clippy_utils::mir::{enclosing_mir, expr_local, local_assignments, used_exact use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::{snippet_with_applicability, snippet_with_context}; use clippy_utils::sugg::has_enclosing_paren; -use clippy_utils::ty::{adt_and_variant_of_res, expr_sig, is_copy, peel_mid_ty_refs, ty_sig}; +use clippy_utils::ty::{is_copy, peel_mid_ty_refs}; use clippy_utils::{ - fn_def_id, get_parent_expr, get_parent_expr_for_hir, is_lint_allowed, path_to_local, walk_to_expr_usage, + expr_use_ctxt, get_parent_expr, get_parent_node, is_lint_allowed, path_to_local, DefinedTy, ExprUseNode, }; +use hir::def::DefKind; +use hir::MatchSource; use rustc_ast::util::parser::{PREC_POSTFIX, PREC_PREFIX}; use rustc_data_structures::fx::FxIndexMap; use rustc_data_structures::graph::iterate::{CycleDetector, TriColorDepthFirstSearch}; use rustc_errors::Applicability; +use rustc_hir::def::Res; use rustc_hir::def_id::{DefId, LocalDefId}; use rustc_hir::intravisit::{walk_ty, Visitor}; use rustc_hir::{ - self as hir, BindingAnnotation, Body, BodyId, BorrowKind, Closure, Expr, ExprKind, FnRetTy, GenericArg, HirId, - ImplItem, ImplItemKind, Item, ItemKind, Local, MatchSource, Mutability, Node, Pat, PatKind, Path, QPath, TraitItem, - TraitItemKind, TyKind, UnOp, + self as hir, BindingAnnotation, Body, BodyId, BorrowKind, Expr, ExprKind, HirId, Mutability, Node, Pat, PatKind, + Path, QPath, TyKind, UnOp, }; use rustc_index::bit_set::BitSet; use rustc_infer::infer::TyCtxtInferExt; @@ -25,8 +27,8 @@ use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::mir::{Rvalue, StatementKind}; use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow, AutoBorrowMutability}; use rustc_middle::ty::{ - self, Binder, BoundVariableKind, ClauseKind, EarlyBinder, FnSig, GenericArgKind, List, ParamEnv, ParamTy, - ProjectionPredicate, Ty, TyCtxt, TypeVisitableExt, TypeckResults, + self, ClauseKind, EarlyBinder, FnSig, GenericArg, GenericArgKind, List, ParamEnv, ParamTy, ProjectionPredicate, Ty, + TyCtxt, TypeVisitableExt, TypeckResults, }; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::symbol::sym; @@ -163,7 +165,7 @@ impl_lint_pass!(Dereferencing<'_> => [ #[derive(Default)] pub struct Dereferencing<'tcx> { - state: Option<(State, StateData)>, + state: Option<(State, StateData<'tcx>)>, // While parsing a `deref` method call in ufcs form, the path to the function is itself an // expression. This is to store the id of that expression so it can be skipped when @@ -203,29 +205,28 @@ impl<'tcx> Dereferencing<'tcx> { } #[derive(Debug)] -struct StateData { +struct StateData<'tcx> { /// Span of the top level expression span: Span, hir_id: HirId, - position: Position, + adjusted_ty: Ty<'tcx>, } -#[derive(Debug)] struct DerefedBorrow { count: usize, msg: &'static str, - snip_expr: Option<HirId>, + stability: TyCoercionStability, + for_field_access: Option<Symbol>, } -#[derive(Debug)] enum State { // Any number of deref method calls. DerefMethod { // The number of calls in a sequence which changed the referenced type ty_changed_count: usize, - is_final_ufcs: bool, + is_ufcs: bool, /// The required mutability - target_mut: Mutability, + mutbl: Mutability, }, DerefedBorrow(DerefedBorrow), ExplicitDeref { @@ -244,7 +245,7 @@ enum State { // A reference operation considered by this lint pass enum RefOp { - Method(Mutability), + Method { mutbl: Mutability, is_ufcs: bool }, Deref, AddrOf(Mutability), } @@ -294,48 +295,115 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { match (self.state.take(), kind) { (None, kind) => { let expr_ty = typeck.expr_ty(expr); - let (position, adjustments) = walk_parents(cx, &mut self.possible_borrowers, expr, &self.msrv); - match kind { - RefOp::Deref => { + let use_cx = expr_use_ctxt(cx, expr); + let adjusted_ty = match &use_cx { + Some(use_cx) => match use_cx.adjustments { + [.., a] => a.target, + _ => expr_ty, + }, + _ => typeck.expr_ty_adjusted(expr), + }; + + match (use_cx, kind) { + (Some(use_cx), RefOp::Deref) => { let sub_ty = typeck.expr_ty(sub_expr); - if let Position::FieldAccess { - name, - of_union: false, - } = position - && !ty_contains_field(sub_ty, name) + if let ExprUseNode::FieldAccess(name) = use_cx.node + && adjusted_ty.ty_adt_def().map_or(true, |adt| !adt.is_union()) + && !ty_contains_field(sub_ty, name.name) { self.state = Some(( - State::ExplicitDerefField { name }, - StateData { span: expr.span, hir_id: expr.hir_id, position }, + State::ExplicitDerefField { name: name.name }, + StateData { + span: expr.span, + hir_id: expr.hir_id, + adjusted_ty, + }, )); - } else if position.is_deref_stable() && sub_ty.is_ref() { + } else if sub_ty.is_ref() + // Linting method receivers would require verifying that name lookup + // would resolve the same way. This is complicated by trait methods. + && !use_cx.node.is_recv() + && let Some(ty) = use_cx.node.defined_ty(cx) + && TyCoercionStability::for_defined_ty(cx, ty, use_cx.node.is_return()).is_deref_stable() + { self.state = Some(( State::ExplicitDeref { mutability: None }, - StateData { span: expr.span, hir_id: expr.hir_id, position }, + StateData { + span: expr.span, + hir_id: expr.hir_id, + adjusted_ty, + }, )); } }, - RefOp::Method(target_mut) + (_, RefOp::Method { mutbl, is_ufcs }) if !is_lint_allowed(cx, EXPLICIT_DEREF_METHODS, expr.hir_id) - && position.lint_explicit_deref() => + // Allow explicit deref in method chains. e.g. `foo.deref().bar()` + && (is_ufcs || !in_postfix_position(cx, expr)) => { let ty_changed_count = usize::from(!deref_method_same_type(expr_ty, typeck.expr_ty(sub_expr))); self.state = Some(( State::DerefMethod { ty_changed_count, - is_final_ufcs: matches!(expr.kind, ExprKind::Call(..)), - target_mut, + is_ufcs, + mutbl, }, StateData { span: expr.span, hir_id: expr.hir_id, - position, + adjusted_ty, }, )); }, - RefOp::AddrOf(mutability) => { + (Some(use_cx), RefOp::AddrOf(mutability)) => { + let defined_ty = use_cx.node.defined_ty(cx); + + // Check needless_borrow for generic arguments. + if !use_cx.is_ty_unified + && let Some(DefinedTy::Mir(ty)) = defined_ty + && let ty::Param(ty) = *ty.value.skip_binder().kind() + && let Some((hir_id, fn_id, i)) = match use_cx.node { + ExprUseNode::MethodArg(_, _, 0) => None, + ExprUseNode::MethodArg(hir_id, None, i) => { + typeck.type_dependent_def_id(hir_id).map(|id| (hir_id, id, i)) + }, + ExprUseNode::FnArg(&Expr { kind: ExprKind::Path(ref p), hir_id, .. }, i) + if !path_has_args(p) => match typeck.qpath_res(p, hir_id) { + Res::Def(DefKind::Fn | DefKind::Ctor(..) | DefKind::AssocFn, id) => { + Some((hir_id, id, i)) + }, + _ => None, + }, + _ => None, + } && let count = needless_borrow_generic_arg_count( + cx, + &mut self.possible_borrowers, + fn_id, + typeck.node_args(hir_id), + i, + ty, + expr, + &self.msrv, + ) && count != 0 + { + self.state = Some(( + State::DerefedBorrow(DerefedBorrow { + count: count - 1, + msg: "the borrowed expression implements the required traits", + stability: TyCoercionStability::None, + for_field_access: None, + }), + StateData { + span: expr.span, + hir_id: expr.hir_id, + adjusted_ty: use_cx.adjustments.last().map_or(expr_ty, |a| a.target), + }, + )); + return; + } + // Find the number of times the borrow is auto-derefed. - let mut iter = adjustments.iter(); + let mut iter = use_cx.adjustments.iter(); let mut deref_count = 0usize; let next_adjust = loop { match iter.next() { @@ -352,6 +420,58 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { }; }; + let stability = defined_ty.map_or(TyCoercionStability::None, |ty| { + TyCoercionStability::for_defined_ty(cx, ty, use_cx.node.is_return()) + }); + let can_auto_borrow = match use_cx.node { + ExprUseNode::Callee => true, + ExprUseNode::FieldAccess(_) => adjusted_ty.ty_adt_def().map_or(true, |adt| !adt.is_union()), + ExprUseNode::MethodArg(hir_id, _, 0) if !use_cx.moved_before_use => { + // Check for calls to trait methods where the trait is implemented + // on a reference. + // Two cases need to be handled: + // * `self` methods on `&T` will never have auto-borrow + // * `&self` methods on `&T` can have auto-borrow, but `&self` methods on `T` will take + // priority. + if let Some(fn_id) = typeck.type_dependent_def_id(hir_id) + && let Some(trait_id) = cx.tcx.trait_of_item(fn_id) + && let arg_ty + = cx.tcx.erase_regions(use_cx.adjustments.last().map_or(expr_ty, |a| a.target)) + && let ty::Ref(_, sub_ty, _) = *arg_ty.kind() + && let args = cx + .typeck_results() + .node_args_opt(hir_id).map(|args| &args[1..]).unwrap_or_default() + && let impl_ty = if cx.tcx.fn_sig(fn_id) + .instantiate_identity() + .skip_binder() + .inputs()[0].is_ref() + { + // Trait methods taking `&self` + sub_ty + } else { + // Trait methods taking `self` + arg_ty + } && impl_ty.is_ref() + && cx.tcx.infer_ctxt().build() + .type_implements_trait( + trait_id, + [impl_ty.into()].into_iter().chain(args.iter().copied()), + cx.param_env, + ) + .must_apply_modulo_regions() + { + false + } else { + true + } + }, + _ => false, + }; + + let deref_msg = + "this expression creates a reference which is immediately dereferenced by the compiler"; + let borrow_msg = "this expression borrows a value the compiler would automatically borrow"; + // Determine the required number of references before any can be removed. In all cases the // reference made by the current expression will be removed. After that there are four cases to // handle. @@ -374,26 +494,18 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { // }; // } // ``` - let deref_msg = - "this expression creates a reference which is immediately dereferenced by the compiler"; - let borrow_msg = "this expression borrows a value the compiler would automatically borrow"; - let impl_msg = "the borrowed expression implements the required traits"; - - let (required_refs, msg, snip_expr) = if position.can_auto_borrow() { - (1, if deref_count == 1 { borrow_msg } else { deref_msg }, None) - } else if let Position::ImplArg(hir_id) = position { - (0, impl_msg, Some(hir_id)) - } else if let Some(&Adjust::Borrow(AutoBorrow::Ref(_, mutability))) = - next_adjust.map(|a| &a.kind) + let (required_refs, msg) = if can_auto_borrow { + (1, if deref_count == 1 { borrow_msg } else { deref_msg }) + } else if let Some(&Adjustment { + kind: Adjust::Borrow(AutoBorrow::Ref(_, mutability)), + .. + }) = next_adjust + && matches!(mutability, AutoBorrowMutability::Mut { .. }) + && !stability.is_reborrow_stable() { - if matches!(mutability, AutoBorrowMutability::Mut { .. }) && !position.is_reborrow_stable() - { - (3, deref_msg, None) - } else { - (2, deref_msg, None) - } + (3, deref_msg) } else { - (2, deref_msg, None) + (2, deref_msg) }; if deref_count >= required_refs { @@ -403,15 +515,19 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { // can't be removed without breaking the code. See earlier comment. count: deref_count - required_refs, msg, - snip_expr, + stability, + for_field_access: match use_cx.node { + ExprUseNode::FieldAccess(name) => Some(name.name), + _ => None, + }, }), StateData { span: expr.span, hir_id: expr.hir_id, - position, + adjusted_ty: use_cx.adjustments.last().map_or(expr_ty, |a| a.target), }, )); - } else if position.is_deref_stable() + } else if stability.is_deref_stable() // Auto-deref doesn't combine with other adjustments && next_adjust.map_or(true, |a| matches!(a.kind, Adjust::Deref(_) | Adjust::Borrow(_))) && iter.all(|a| matches!(a.kind, Adjust::Deref(_) | Adjust::Borrow(_))) @@ -421,24 +537,24 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { StateData { span: expr.span, hir_id: expr.hir_id, - position, + adjusted_ty: use_cx.adjustments.last().map_or(expr_ty, |a| a.target), }, )); } }, - RefOp::Method(..) => (), + (None, _) | (_, RefOp::Method { .. }) => (), } }, ( Some(( State::DerefMethod { - target_mut, + mutbl, ty_changed_count, .. }, data, )), - RefOp::Method(_), + RefOp::Method { is_ufcs, .. }, ) => { self.state = Some(( State::DerefMethod { @@ -447,8 +563,8 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { } else { ty_changed_count + 1 }, - is_final_ufcs: matches!(expr.kind, ExprKind::Call(..)), - target_mut, + is_ufcs, + mutbl, }, data, )); @@ -463,33 +579,44 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { )); }, (Some((State::DerefedBorrow(state), data)), RefOp::AddrOf(mutability)) => { - let position = data.position; + let adjusted_ty = data.adjusted_ty; + let stability = state.stability; report(cx, expr, State::DerefedBorrow(state), data); - if position.is_deref_stable() { + if stability.is_deref_stable() { self.state = Some(( State::Borrow { mutability }, StateData { span: expr.span, hir_id: expr.hir_id, - position, + adjusted_ty, }, )); } }, (Some((State::DerefedBorrow(state), data)), RefOp::Deref) => { - let position = data.position; + let adjusted_ty = data.adjusted_ty; + let stability = state.stability; + let for_field_access = state.for_field_access; report(cx, expr, State::DerefedBorrow(state), data); - if let Position::FieldAccess{name, ..} = position + if let Some(name) = for_field_access && !ty_contains_field(typeck.expr_ty(sub_expr), name) { self.state = Some(( State::ExplicitDerefField { name }, - StateData { span: expr.span, hir_id: expr.hir_id, position }, + StateData { + span: expr.span, + hir_id: expr.hir_id, + adjusted_ty, + }, )); - } else if position.is_deref_stable() { + } else if stability.is_deref_stable() { self.state = Some(( State::ExplicitDeref { mutability: None }, - StateData { span: expr.span, hir_id: expr.hir_id, position }, + StateData { + span: expr.span, + hir_id: expr.hir_id, + adjusted_ty, + }, )); } }, @@ -611,8 +738,8 @@ fn try_parse_ref_op<'tcx>( typeck: &'tcx TypeckResults<'_>, expr: &'tcx Expr<'_>, ) -> Option<(RefOp, &'tcx Expr<'tcx>)> { - let (def_id, arg) = match expr.kind { - ExprKind::MethodCall(_, arg, [], _) => (typeck.type_dependent_def_id(expr.hir_id)?, arg), + let (is_ufcs, def_id, arg) = match expr.kind { + ExprKind::MethodCall(_, arg, [], _) => (false, typeck.type_dependent_def_id(expr.hir_id)?, arg), ExprKind::Call( Expr { kind: ExprKind::Path(path), @@ -620,7 +747,7 @@ fn try_parse_ref_op<'tcx>( .. }, [arg], - ) => (typeck.qpath_res(path, *hir_id).opt_def_id()?, arg), + ) => (true, typeck.qpath_res(path, *hir_id).opt_def_id()?, arg), ExprKind::Unary(UnOp::Deref, sub_expr) if !typeck.expr_ty(sub_expr).is_unsafe_ptr() => { return Some((RefOp::Deref, sub_expr)); }, @@ -628,9 +755,21 @@ fn try_parse_ref_op<'tcx>( _ => return None, }; if tcx.is_diagnostic_item(sym::deref_method, def_id) { - Some((RefOp::Method(Mutability::Not), arg)) + Some(( + RefOp::Method { + mutbl: Mutability::Not, + is_ufcs, + }, + arg, + )) } else if tcx.trait_of_item(def_id)? == tcx.lang_items().deref_mut_trait()? { - Some((RefOp::Method(Mutability::Mut), arg)) + Some(( + RefOp::Method { + mutbl: Mutability::Mut, + is_ufcs, + }, + arg, + )) } else { None } @@ -649,420 +788,164 @@ fn deref_method_same_type<'tcx>(result_ty: Ty<'tcx>, arg_ty: Ty<'tcx>) -> bool { } } -/// The position of an expression relative to it's parent. -#[derive(Clone, Copy, Debug)] -enum Position { - MethodReceiver, - /// The method is defined on a reference type. e.g. `impl Foo for &T` - MethodReceiverRefImpl, - Callee, - ImplArg(HirId), - FieldAccess { - name: Symbol, - of_union: bool, - }, // union fields cannot be auto borrowed - Postfix, - Deref, - /// Any other location which will trigger auto-deref to a specific time. - /// Contains the precedence of the parent expression and whether the target type is sized. - DerefStable(i8, bool), - /// Any other location which will trigger auto-reborrowing. - /// Contains the precedence of the parent expression. - ReborrowStable(i8), - /// Contains the precedence of the parent expression. - Other(i8), -} -impl Position { - fn is_deref_stable(self) -> bool { - matches!(self, Self::DerefStable(..)) +fn path_has_args(p: &QPath<'_>) -> bool { + match *p { + QPath::Resolved(_, Path { segments: [.., s], .. }) | QPath::TypeRelative(_, s) => s.args.is_some(), + _ => false, } +} - fn is_reborrow_stable(self) -> bool { - matches!(self, Self::DerefStable(..) | Self::ReborrowStable(_)) +fn in_postfix_position<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) -> bool { + if let Some(parent) = get_parent_expr(cx, e) + && parent.span.ctxt() == e.span.ctxt() + { + match parent.kind { + ExprKind::Call(child, _) | ExprKind::MethodCall(_, child, _, _) | ExprKind::Index(child, _) + if child.hir_id == e.hir_id => true, + ExprKind::Field(_, _) | ExprKind::Match(_, _, MatchSource::TryDesugar | MatchSource::AwaitDesugar) => true, + _ => false, + } + } else { + false } +} - fn can_auto_borrow(self) -> bool { - matches!( - self, - Self::MethodReceiver | Self::FieldAccess { of_union: false, .. } | Self::Callee - ) +#[derive(Clone, Copy)] +enum TyCoercionStability { + Deref, + Reborrow, + None, +} +impl TyCoercionStability { + fn is_deref_stable(self) -> bool { + matches!(self, Self::Deref) } - fn lint_explicit_deref(self) -> bool { - matches!(self, Self::Other(_) | Self::DerefStable(..) | Self::ReborrowStable(_)) + fn is_reborrow_stable(self) -> bool { + matches!(self, Self::Deref | Self::Reborrow) } - fn precedence(self) -> i8 { - match self { - Self::MethodReceiver - | Self::MethodReceiverRefImpl - | Self::Callee - | Self::FieldAccess { .. } - | Self::Postfix => PREC_POSTFIX, - Self::ImplArg(_) | Self::Deref => PREC_PREFIX, - Self::DerefStable(p, _) | Self::ReborrowStable(p) | Self::Other(p) => p, + fn for_defined_ty<'tcx>(cx: &LateContext<'tcx>, ty: DefinedTy<'tcx>, for_return: bool) -> Self { + match ty { + DefinedTy::Hir(ty) => Self::for_hir_ty(ty), + DefinedTy::Mir(ty) => Self::for_mir_ty( + cx.tcx, + ty.param_env, + cx.tcx.erase_late_bound_regions(ty.value), + for_return, + ), } } -} - -/// Walks up the parent expressions attempting to determine both how stable the auto-deref result -/// is, and which adjustments will be applied to it. Note this will not consider auto-borrow -/// locations as those follow different rules. -#[expect(clippy::too_many_lines)] -fn walk_parents<'tcx>( - cx: &LateContext<'tcx>, - possible_borrowers: &mut Vec<(LocalDefId, PossibleBorrowerMap<'tcx, 'tcx>)>, - e: &'tcx Expr<'_>, - msrv: &Msrv, -) -> (Position, &'tcx [Adjustment<'tcx>]) { - let mut adjustments = [].as_slice(); - let mut precedence = 0i8; - let ctxt = e.span.ctxt(); - let position = walk_to_expr_usage(cx, e, &mut |parent, child_id| { - // LocalTableInContext returns the wrong lifetime, so go use `expr_adjustments` instead. - if adjustments.is_empty() && let Node::Expr(e) = cx.tcx.hir().get(child_id) { - adjustments = cx.typeck_results().expr_adjustments(e); - } - match parent { - Node::Local(Local { ty: Some(ty), span, .. }) if span.ctxt() == ctxt => { - Some(binding_ty_auto_deref_stability(cx, ty, precedence, List::empty())) - }, - Node::Item(&Item { - kind: ItemKind::Static(..) | ItemKind::Const(..), - owner_id, - span, - .. - }) - | Node::TraitItem(&TraitItem { - kind: TraitItemKind::Const(..), - owner_id, - span, - .. - }) - | Node::ImplItem(&ImplItem { - kind: ImplItemKind::Const(..), - owner_id, - span, - .. - }) if span.ctxt() == ctxt => { - let ty = cx.tcx.type_of(owner_id.def_id).instantiate_identity(); - Some(ty_auto_deref_stability(cx.tcx, cx.param_env, ty, precedence).position_for_result(cx)) - }, - Node::Item(&Item { - kind: ItemKind::Fn(..), - owner_id, - span, - .. - }) - | Node::TraitItem(&TraitItem { - kind: TraitItemKind::Fn(..), - owner_id, - span, - .. - }) - | Node::ImplItem(&ImplItem { - kind: ImplItemKind::Fn(..), - owner_id, - span, - .. - }) if span.ctxt() == ctxt => { - let output = cx - .tcx - .erase_late_bound_regions(cx.tcx.fn_sig(owner_id).instantiate_identity().output()); - Some(ty_auto_deref_stability(cx.tcx, cx.param_env, output, precedence).position_for_result(cx)) - }, - - Node::ExprField(field) if field.span.ctxt() == ctxt => match get_parent_expr_for_hir(cx, field.hir_id) { - Some(Expr { - hir_id, - kind: ExprKind::Struct(path, ..), - .. - }) => adt_and_variant_of_res(cx, cx.qpath_res(path, *hir_id)) - .and_then(|(adt, variant)| { - variant - .fields - .iter() - .find(|f| f.name == field.ident.name) - .map(|f| (adt, f)) - }) - .map(|(adt, field_def)| { - ty_auto_deref_stability( - cx.tcx, - // Use the param_env of the target type. - cx.tcx.param_env(adt.did()), - cx.tcx.type_of(field_def.did).instantiate_identity(), - precedence, - ) - .position_for_arg() - }), - _ => None, - }, + // Checks the stability of type coercions when assigned to a binding with the given explicit type. + // + // e.g. + // let x = Box::new(Box::new(0u32)); + // let y1: &Box<_> = x.deref(); + // let y2: &Box<_> = &x; + // + // Here `y1` and `y2` would resolve to different types, so the type `&Box<_>` is not stable when + // switching to auto-dereferencing. + fn for_hir_ty<'tcx>(ty: &'tcx hir::Ty<'tcx>) -> Self { + let TyKind::Ref(_, ty) = &ty.kind else { + return Self::None; + }; + let mut ty = ty; - Node::Expr(parent) if parent.span.ctxt() == ctxt => match parent.kind { - ExprKind::Ret(_) => { - let owner_id = cx.tcx.hir().body_owner_def_id(cx.enclosing_body.unwrap()); - Some( - if let Node::Expr( - closure_expr @ Expr { - kind: ExprKind::Closure(closure), - .. - }, - ) = cx.tcx.hir().get_by_def_id(owner_id) - { - closure_result_position(cx, closure, cx.typeck_results().expr_ty(closure_expr), precedence) - } else { - let output = cx - .tcx - .erase_late_bound_regions(cx.tcx.fn_sig(owner_id).instantiate_identity().output()); - ty_auto_deref_stability(cx.tcx, cx.param_env, output, precedence).position_for_result(cx) - }, - ) - }, - ExprKind::Closure(closure) => Some(closure_result_position( - cx, - closure, - cx.typeck_results().expr_ty(parent), - precedence, - )), - ExprKind::Call(func, _) if func.hir_id == child_id => { - (child_id == e.hir_id).then_some(Position::Callee) + loop { + break match ty.ty.kind { + TyKind::Ref(_, ref ref_ty) => { + ty = ref_ty; + continue; }, - ExprKind::Call(func, args) => args - .iter() - .position(|arg| arg.hir_id == child_id) - .zip(expr_sig(cx, func)) - .and_then(|(i, sig)| { - sig.input_with_hir(i).map(|(hir_ty, ty)| { - match hir_ty { - // Type inference for closures can depend on how they're called. Only go by the explicit - // types here. - Some(hir_ty) => { - binding_ty_auto_deref_stability(cx, hir_ty, precedence, ty.bound_vars()) - }, - None => { - // `e.hir_id == child_id` for https://github.com/rust-lang/rust-clippy/issues/9739 - // `!call_is_qualified(func)` for https://github.com/rust-lang/rust-clippy/issues/9782 - if e.hir_id == child_id - && !call_is_qualified(func) - && let ty::Param(param_ty) = ty.skip_binder().kind() - { - needless_borrow_impl_arg_position( - cx, - possible_borrowers, - parent, - i, - *param_ty, - e, - precedence, - msrv, - ) - } else { - ty_auto_deref_stability( - cx.tcx, - // Use the param_env of the target function. - sig.predicates_id().map_or(ParamEnv::empty(), |id| cx.tcx.param_env(id)), - cx.tcx.erase_late_bound_regions(ty), - precedence - ).position_for_arg() - } - }, - } + TyKind::Path( + QPath::TypeRelative(_, path) + | QPath::Resolved( + _, + Path { + segments: [.., path], .. + }, + ), + ) => { + if let Some(args) = path.args + && args.args.iter().any(|arg| match arg { + hir::GenericArg::Infer(_) => true, + hir::GenericArg::Type(ty) => ty_contains_infer(ty), + _ => false, }) - }), - ExprKind::MethodCall(method, receiver, args, _) => { - let fn_id = cx.typeck_results().type_dependent_def_id(parent.hir_id).unwrap(); - if receiver.hir_id == child_id { - // Check for calls to trait methods where the trait is implemented on a reference. - // Two cases need to be handled: - // * `self` methods on `&T` will never have auto-borrow - // * `&self` methods on `&T` can have auto-borrow, but `&self` methods on `T` will take - // priority. - if e.hir_id != child_id { - return Some(Position::ReborrowStable(precedence)) - } else if let Some(trait_id) = cx.tcx.trait_of_item(fn_id) - && let arg_ty = cx.tcx.erase_regions(cx.typeck_results().expr_ty_adjusted(e)) - && let ty::Ref(_, sub_ty, _) = *arg_ty.kind() - && let subs = cx - .typeck_results() - .node_args_opt(parent.hir_id).map(|subs| &subs[1..]).unwrap_or_default() - && let impl_ty = if cx.tcx.fn_sig(fn_id) - .instantiate_identity() - .skip_binder() - .inputs()[0].is_ref() - { - // Trait methods taking `&self` - sub_ty - } else { - // Trait methods taking `self` - arg_ty - } && impl_ty.is_ref() - && let infcx = cx.tcx.infer_ctxt().build() - && infcx - .type_implements_trait( - trait_id, - [impl_ty.into()].into_iter().chain(subs.iter().copied()), - cx.param_env, - ) - .must_apply_modulo_regions() - { - return Some(Position::MethodReceiverRefImpl) - } - return Some(Position::MethodReceiver); + { + Self::Reborrow + } else { + Self::Deref } - args.iter().position(|arg| arg.hir_id == child_id).map(|i| { - let ty = cx.tcx.fn_sig(fn_id).instantiate_identity().input(i + 1); - // `e.hir_id == child_id` for https://github.com/rust-lang/rust-clippy/issues/9739 - // `method.args.is_none()` for https://github.com/rust-lang/rust-clippy/issues/9782 - if e.hir_id == child_id - && method.args.is_none() - && let ty::Param(param_ty) = ty.skip_binder().kind() - { - needless_borrow_impl_arg_position( - cx, - possible_borrowers, - parent, - i + 1, - *param_ty, - e, - precedence, - msrv, - ) - } else { - ty_auto_deref_stability( - cx.tcx, - // Use the param_env of the target function. - cx.tcx.param_env(fn_id), - cx.tcx.erase_late_bound_regions(ty), - precedence, - ) - .position_for_arg() - } - }) - }, - ExprKind::Field(child, name) if child.hir_id == e.hir_id => Some(Position::FieldAccess { - name: name.name, - of_union: is_union(cx.typeck_results(), child), - }), - ExprKind::Unary(UnOp::Deref, child) if child.hir_id == e.hir_id => Some(Position::Deref), - ExprKind::Match(child, _, MatchSource::TryDesugar | MatchSource::AwaitDesugar) - | ExprKind::Index(child, _) - if child.hir_id == e.hir_id => - { - Some(Position::Postfix) - }, - _ if child_id == e.hir_id => { - precedence = parent.precedence().order(); - None }, - _ => None, - }, - _ => None, + TyKind::Slice(_) + | TyKind::Array(..) + | TyKind::Ptr(_) + | TyKind::BareFn(_) + | TyKind::Never + | TyKind::Tup(_) + | TyKind::Path(_) => Self::Deref, + TyKind::OpaqueDef(..) + | TyKind::Infer + | TyKind::Typeof(..) + | TyKind::TraitObject(..) + | TyKind::Err(_) => Self::Reborrow, + }; } - }) - .unwrap_or(Position::Other(precedence)); - (position, adjustments) -} - -fn is_union<'tcx>(typeck: &'tcx TypeckResults<'_>, path_expr: &'tcx Expr<'_>) -> bool { - typeck - .expr_ty_adjusted(path_expr) - .ty_adt_def() - .map_or(false, rustc_middle::ty::AdtDef::is_union) -} - -fn closure_result_position<'tcx>( - cx: &LateContext<'tcx>, - closure: &'tcx Closure<'_>, - ty: Ty<'tcx>, - precedence: i8, -) -> Position { - match closure.fn_decl.output { - FnRetTy::Return(hir_ty) => { - if let Some(sig) = ty_sig(cx, ty) - && let Some(output) = sig.output() - { - binding_ty_auto_deref_stability(cx, hir_ty, precedence, output.bound_vars()) - } else { - Position::Other(precedence) - } - }, - FnRetTy::DefaultReturn(_) => Position::Other(precedence), } -} -// Checks the stability of auto-deref when assigned to a binding with the given explicit type. -// -// e.g. -// let x = Box::new(Box::new(0u32)); -// let y1: &Box<_> = x.deref(); -// let y2: &Box<_> = &x; -// -// Here `y1` and `y2` would resolve to different types, so the type `&Box<_>` is not stable when -// switching to auto-dereferencing. -fn binding_ty_auto_deref_stability<'tcx>( - cx: &LateContext<'tcx>, - ty: &'tcx hir::Ty<'_>, - precedence: i8, - binder_args: &'tcx List<BoundVariableKind>, -) -> Position { - let TyKind::Ref(_, ty) = &ty.kind else { - return Position::Other(precedence); - }; - let mut ty = ty; + fn for_mir_ty<'tcx>(tcx: TyCtxt<'tcx>, param_env: ParamEnv<'tcx>, ty: Ty<'tcx>, for_return: bool) -> Self { + let ty::Ref(_, mut ty, _) = *ty.kind() else { + return Self::None; + }; - loop { - break match ty.ty.kind { - TyKind::Ref(_, ref ref_ty) => { - ty = ref_ty; - continue; - }, - TyKind::Path( - QPath::TypeRelative(_, path) - | QPath::Resolved( - _, - Path { - segments: [.., path], .. - }, - ), - ) => { - if let Some(args) = path.args - && args.args.iter().any(|arg| match arg { - GenericArg::Infer(_) => true, - GenericArg::Type(ty) => ty_contains_infer(ty), - _ => false, - }) + ty = tcx.try_normalize_erasing_regions(param_env, ty).unwrap_or(ty); + loop { + break match *ty.kind() { + ty::Ref(_, ref_ty, _) => { + ty = ref_ty; + continue; + }, + ty::Param(_) if for_return => Self::Deref, + ty::Alias(ty::Weak | ty::Inherent, _) => unreachable!("should have been normalized away above"), + ty::Alias(ty::Projection, _) if !for_return && ty.has_non_region_param() => Self::Reborrow, + ty::Infer(_) + | ty::Error(_) + | ty::Bound(..) + | ty::Alias(ty::Opaque, ..) + | ty::Placeholder(_) + | ty::Dynamic(..) + | ty::Param(_) => Self::Reborrow, + ty::Adt(_, args) + if ty.has_placeholders() + || ty.has_opaque_types() + || (!for_return && args.has_non_region_param()) => { - Position::ReborrowStable(precedence) - } else { - Position::DerefStable( - precedence, - cx.tcx - .erase_late_bound_regions(Binder::bind_with_vars( - cx.typeck_results().node_type(ty.ty.hir_id), - binder_args, - )) - .is_sized(cx.tcx, cx.param_env.without_caller_bounds()), - ) - } - }, - TyKind::Slice(_) => Position::DerefStable(precedence, false), - TyKind::Array(..) | TyKind::Ptr(_) | TyKind::BareFn(_) => Position::DerefStable(precedence, true), - TyKind::Never - | TyKind::Tup(_) - | TyKind::Path(_) => Position::DerefStable( - precedence, - cx.tcx - .erase_late_bound_regions(Binder::bind_with_vars( - cx.typeck_results().node_type(ty.ty.hir_id), - binder_args, - )) - .is_sized(cx.tcx, cx.param_env.without_caller_bounds()), - ), - TyKind::OpaqueDef(..) | TyKind::Infer | TyKind::Typeof(..) | TyKind::TraitObject(..) | TyKind::Err(_) => { - Position::ReborrowStable(precedence) - }, - }; + Self::Reborrow + }, + ty::Bool + | ty::Char + | ty::Int(_) + | ty::Uint(_) + | ty::Array(..) + | ty::Float(_) + | ty::RawPtr(..) + | ty::FnPtr(_) + | ty::Str + | ty::Slice(..) + | ty::Adt(..) + | ty::Foreign(_) + | ty::FnDef(..) + | ty::Generator(..) + | ty::GeneratorWitness(..) + | ty::GeneratorWitnessMIR(..) + | ty::Closure(..) + | ty::Never + | ty::Tuple(_) + | ty::Alias(ty::Projection, _) => Self::Deref, + }; + } } } @@ -1084,10 +967,10 @@ fn ty_contains_infer(ty: &hir::Ty<'_>) -> bool { } } - fn visit_generic_arg(&mut self, arg: &GenericArg<'_>) { - if self.0 || matches!(arg, GenericArg::Infer(_)) { + fn visit_generic_arg(&mut self, arg: &hir::GenericArg<'_>) { + if self.0 || matches!(arg, hir::GenericArg::Infer(_)) { self.0 = true; - } else if let GenericArg::Type(ty) = arg { + } else if let hir::GenericArg::Type(ty) = arg { self.visit_ty(ty); } } @@ -1097,51 +980,29 @@ fn ty_contains_infer(ty: &hir::Ty<'_>) -> bool { v.0 } -fn call_is_qualified(expr: &Expr<'_>) -> bool { - if let ExprKind::Path(path) = &expr.kind { - match path { - QPath::Resolved(_, path) => path.segments.last().map_or(false, |segment| segment.args.is_some()), - QPath::TypeRelative(_, segment) => segment.args.is_some(), - QPath::LangItem(..) => false, - } - } else { - false - } -} - -// Checks whether: -// * child is an expression of the form `&e` in an argument position requiring an `impl Trait` -// * `e`'s type implements `Trait` and is copyable -// If the conditions are met, returns `Some(Position::ImplArg(..))`; otherwise, returns `None`. -// The "is copyable" condition is to avoid the case where removing the `&` means `e` would have to -// be moved, but it cannot be. -#[expect(clippy::too_many_arguments, clippy::too_many_lines)] -fn needless_borrow_impl_arg_position<'tcx>( +/// Checks for the number of borrow expressions which can be removed from the given expression +/// where the expression is used as an argument to a function expecting a generic type. +/// +/// The following constraints will be checked: +/// * The borrowed expression meets all the generic type's constraints. +/// * The generic type appears only once in the functions signature. +/// * The borrowed value will not be moved if it is used later in the function. +#[expect(clippy::too_many_arguments)] +fn needless_borrow_generic_arg_count<'tcx>( cx: &LateContext<'tcx>, possible_borrowers: &mut Vec<(LocalDefId, PossibleBorrowerMap<'tcx, 'tcx>)>, - parent: &Expr<'tcx>, + fn_id: DefId, + callee_args: &'tcx List<GenericArg<'tcx>>, arg_index: usize, param_ty: ParamTy, mut expr: &Expr<'tcx>, - precedence: i8, msrv: &Msrv, -) -> Position { +) -> usize { let destruct_trait_def_id = cx.tcx.lang_items().destruct_trait(); let sized_trait_def_id = cx.tcx.lang_items().sized_trait(); - let Some(callee_def_id) = fn_def_id(cx, parent) else { - return Position::Other(precedence); - }; - let fn_sig = cx.tcx.fn_sig(callee_def_id).instantiate_identity().skip_binder(); - let args_with_expr_ty = cx - .typeck_results() - .node_args(if let ExprKind::Call(callee, _) = parent.kind { - callee.hir_id - } else { - parent.hir_id - }); - - let predicates = cx.tcx.param_env(callee_def_id).caller_bounds(); + let fn_sig = cx.tcx.fn_sig(fn_id).instantiate_identity().skip_binder(); + let predicates = cx.tcx.param_env(fn_id).caller_bounds(); let projection_predicates = predicates .iter() .filter_map(|predicate| { @@ -1176,7 +1037,7 @@ fn needless_borrow_impl_arg_position<'tcx>( || cx.tcx.is_diagnostic_item(sym::Any, trait_def_id) }) { - return Position::Other(precedence); + return 0; } // See: @@ -1184,14 +1045,14 @@ fn needless_borrow_impl_arg_position<'tcx>( // - https://github.com/rust-lang/rust-clippy/pull/9674#issuecomment-1292225232 if projection_predicates .iter() - .any(|projection_predicate| is_mixed_projection_predicate(cx, callee_def_id, projection_predicate)) + .any(|projection_predicate| is_mixed_projection_predicate(cx, fn_id, projection_predicate)) { - return Position::Other(precedence); + return 0; } // `args_with_referent_ty` can be constructed outside of `check_referent` because the same // elements are modified each time `check_referent` is called. - let mut args_with_referent_ty = args_with_expr_ty.to_vec(); + let mut args_with_referent_ty = callee_args.to_vec(); let mut check_reference_and_referent = |reference, referent| { let referent_ty = cx.typeck_results().expr_ty(referent); @@ -1238,20 +1099,15 @@ fn needless_borrow_impl_arg_position<'tcx>( }) }; - let mut needless_borrow = false; + let mut count = 0; while let ExprKind::AddrOf(_, _, referent) = expr.kind { if !check_reference_and_referent(expr, referent) { break; } expr = referent; - needless_borrow = true; - } - - if needless_borrow { - Position::ImplArg(expr.hir_id) - } else { - Position::Other(precedence) + count += 1; } + count } fn has_ref_mut_self_method(cx: &LateContext<'_>, trait_def_id: DefId) -> bool { @@ -1392,93 +1248,6 @@ fn replace_types<'tcx>( true } -struct TyPosition<'tcx> { - position: Position, - ty: Option<Ty<'tcx>>, -} -impl From<Position> for TyPosition<'_> { - fn from(position: Position) -> Self { - Self { position, ty: None } - } -} -impl<'tcx> TyPosition<'tcx> { - fn new_deref_stable_for_result(precedence: i8, ty: Ty<'tcx>) -> Self { - Self { - position: Position::ReborrowStable(precedence), - ty: Some(ty), - } - } - fn position_for_result(self, cx: &LateContext<'tcx>) -> Position { - match (self.position, self.ty) { - (Position::ReborrowStable(precedence), Some(ty)) => { - Position::DerefStable(precedence, ty.is_sized(cx.tcx, cx.param_env)) - }, - (position, _) => position, - } - } - fn position_for_arg(self) -> Position { - self.position - } -} - -// Checks whether a type is stable when switching to auto dereferencing, -fn ty_auto_deref_stability<'tcx>( - tcx: TyCtxt<'tcx>, - param_env: ParamEnv<'tcx>, - ty: Ty<'tcx>, - precedence: i8, -) -> TyPosition<'tcx> { - let ty::Ref(_, mut ty, _) = *ty.kind() else { - return Position::Other(precedence).into(); - }; - - ty = tcx.try_normalize_erasing_regions(param_env, ty).unwrap_or(ty); - - loop { - break match *ty.kind() { - ty::Ref(_, ref_ty, _) => { - ty = ref_ty; - continue; - }, - ty::Param(_) => TyPosition::new_deref_stable_for_result(precedence, ty), - ty::Alias(ty::Weak, _) => unreachable!("should have been normalized away above"), - ty::Alias(ty::Inherent, _) => unreachable!("inherent projection should have been normalized away above"), - ty::Alias(ty::Projection, _) if ty.has_non_region_param() => { - TyPosition::new_deref_stable_for_result(precedence, ty) - }, - ty::Infer(_) - | ty::Error(_) - | ty::Bound(..) - | ty::Alias(ty::Opaque, ..) - | ty::Placeholder(_) - | ty::Dynamic(..) => Position::ReborrowStable(precedence).into(), - ty::Adt(..) if ty.has_placeholders() || ty.has_opaque_types() => { - Position::ReborrowStable(precedence).into() - }, - ty::Adt(_, args) if args.has_non_region_param() => TyPosition::new_deref_stable_for_result(precedence, ty), - ty::Bool - | ty::Char - | ty::Int(_) - | ty::Uint(_) - | ty::Array(..) - | ty::Float(_) - | ty::RawPtr(..) - | ty::FnPtr(_) => Position::DerefStable(precedence, true).into(), - ty::Str | ty::Slice(..) => Position::DerefStable(precedence, false).into(), - ty::Adt(..) - | ty::Foreign(_) - | ty::FnDef(..) - | ty::Generator(..) - | ty::GeneratorWitness(..) - | ty::GeneratorWitnessMIR(..) - | ty::Closure(..) - | ty::Never - | ty::Tuple(_) - | ty::Alias(ty::Projection, _) => Position::DerefStable(precedence, ty.is_sized(tcx, param_env)).into(), - }; - } -} - fn ty_contains_field(ty: Ty<'_>, name: Symbol) -> bool { if let ty::Adt(adt, _) = *ty.kind() { adt.is_struct() && adt.all_fields().any(|f| f.name == name) @@ -1488,12 +1257,12 @@ fn ty_contains_field(ty: Ty<'_>, name: Symbol) -> bool { } #[expect(clippy::needless_pass_by_value, clippy::too_many_lines)] -fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data: StateData) { +fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data: StateData<'tcx>) { match state { State::DerefMethod { ty_changed_count, - is_final_ufcs, - target_mut, + is_ufcs, + mutbl, } => { let mut app = Applicability::MachineApplicable; let (expr_str, _expr_is_macro_call) = snippet_with_context(cx, expr.span, data.span.ctxt(), "..", &mut app); @@ -1508,12 +1277,12 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data }; let addr_of_str = if ty_changed_count < ref_count { // Check if a reborrow from &mut T -> &T is required. - if target_mut == Mutability::Not && matches!(ty.kind(), ty::Ref(_, _, Mutability::Mut)) { + if mutbl == Mutability::Not && matches!(ty.kind(), ty::Ref(_, _, Mutability::Mut)) { "&*" } else { "" } - } else if target_mut == Mutability::Mut { + } else if mutbl == Mutability::Mut { "&mut " } else { "&" @@ -1530,7 +1299,7 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data */ // Fix #10850, do not lint if it's `Foo::deref` instead of `foo.deref()`. - if is_final_ufcs { + if is_ufcs { return; } @@ -1538,7 +1307,7 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data cx, EXPLICIT_DEREF_METHODS, data.span, - match target_mut { + match mutbl { Mutability::Not => "explicit `deref` method call", Mutability::Mut => "explicit `deref_mut` method call", }, @@ -1549,13 +1318,19 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data }, State::DerefedBorrow(state) => { let mut app = Applicability::MachineApplicable; - let snip_expr = state.snip_expr.map_or(expr, |hir_id| cx.tcx.hir().expect_expr(hir_id)); - let (snip, snip_is_macro) = snippet_with_context(cx, snip_expr.span, data.span.ctxt(), "..", &mut app); + let (snip, snip_is_macro) = snippet_with_context(cx, expr.span, data.span.ctxt(), "..", &mut app); span_lint_hir_and_then(cx, NEEDLESS_BORROW, data.hir_id, data.span, state.msg, |diag| { - let calls_field = matches!(expr.kind, ExprKind::Field(..)) && matches!(data.position, Position::Callee); + let (precedence, calls_field) = match get_parent_node(cx.tcx, data.hir_id) { + Some(Node::Expr(e)) => match e.kind { + ExprKind::Call(callee, _) if callee.hir_id != data.hir_id => (0, false), + ExprKind::Call(..) => (PREC_POSTFIX, matches!(expr.kind, ExprKind::Field(..))), + _ => (e.precedence().order(), false), + }, + _ => (0, false), + }; let sugg = if !snip_is_macro + && (calls_field || expr.precedence().order() < precedence) && !has_enclosing_paren(&snip) - && (expr.precedence().order() < data.position.precedence() || calls_field) { format!("({snip})") } else { @@ -1572,7 +1347,8 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data | ExprKind::If(..) | ExprKind::Loop(..) | ExprKind::Match(..) - ) && matches!(data.position, Position::DerefStable(_, true)) + ) && let ty::Ref(_, ty, _) = data.adjusted_ty.kind() + && ty.is_sized(cx.tcx, cx.param_env) { // Rustc bug: auto deref doesn't work on block expression when targeting sized types. return; @@ -1585,9 +1361,9 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data Mutability::Not => "&", Mutability::Mut => "&mut ", }; - (prefix, 0) + (prefix, PREC_PREFIX) } else { - ("", data.position.precedence()) + ("", 0) }; span_lint_hir_and_then( cx, @@ -1616,7 +1392,7 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data | ExprKind::If(..) | ExprKind::Loop(..) | ExprKind::Match(..) - ) && matches!(data.position, Position::DerefStable(_, true)) + ) && data.adjusted_ty.is_sized(cx.tcx, cx.param_env) { // Rustc bug: auto deref doesn't work on block expression when targeting sized types. return; diff --git a/clippy_lints/src/derive.rs b/clippy_lints/src/derive.rs index 91bc30ab577..e3f2026cfe9 100644 --- a/clippy_lints/src/derive.rs +++ b/clippy_lints/src/derive.rs @@ -1,4 +1,6 @@ -use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_note, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::diagnostics::{ + span_lint_and_help, span_lint_and_note, span_lint_and_sugg, span_lint_and_then, +}; use clippy_utils::ty::{implements_trait, implements_trait_with_env, is_copy}; use clippy_utils::{is_lint_allowed, match_def_path, paths}; use if_chain::if_chain; @@ -6,15 +8,15 @@ use rustc_errors::Applicability; use rustc_hir::def_id::DefId; use rustc_hir::intravisit::{walk_expr, walk_fn, walk_item, FnKind, Visitor}; use rustc_hir::{ - self as hir, BlockCheckMode, BodyId, Expr, ExprKind, FnDecl, Impl, Item, ItemKind, UnsafeSource, - Unsafety, + self as hir, BlockCheckMode, BodyId, Expr, ExprKind, FnDecl, Impl, Item, ItemKind, + UnsafeSource, Unsafety, }; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::hir::nested_filter; use rustc_middle::traits::Reveal; use rustc_middle::ty::{ - self, BoundConstness, ClauseKind, GenericArgKind, GenericParamDefKind, ImplPolarity, ParamEnv, ToPredicate, - TraitPredicate, Ty, TyCtxt, + self, BoundConstness, ClauseKind, GenericArgKind, GenericParamDefKind, ImplPolarity, ParamEnv, + ToPredicate, TraitPredicate, Ty, TyCtxt, }; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::def_id::LocalDefId; @@ -205,13 +207,10 @@ declare_lint_pass!(Derive => [ impl<'tcx> LateLintPass<'tcx> for Derive { fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { - if let ItemKind::Impl(Impl { - of_trait: Some(ref trait_ref), - .. - }) = item.kind - { + if let ItemKind::Impl(Impl { of_trait: Some(ref trait_ref), .. }) = item.kind { let ty = cx.tcx.type_of(item.owner_id).instantiate_identity(); - let is_automatically_derived = cx.tcx.has_attr(item.owner_id, sym::automatically_derived); + let is_automatically_derived = + cx.tcx.has_attr(item.owner_id, sym::automatically_derived); check_hash_peq(cx, item.span, trait_ref, ty, is_automatically_derived); check_ord_partial_ord(cx, item.span, trait_ref, ty, is_automatically_derived); @@ -328,7 +327,12 @@ fn check_ord_partial_ord<'tcx>( } /// Implementation of the `EXPL_IMPL_CLONE_ON_COPY` lint. -fn check_copy_clone<'tcx>(cx: &LateContext<'tcx>, item: &Item<'_>, trait_ref: &hir::TraitRef<'_>, ty: Ty<'tcx>) { +fn check_copy_clone<'tcx>( + cx: &LateContext<'tcx>, + item: &Item<'_>, + trait_ref: &hir::TraitRef<'_>, + ty: Ty<'tcx>, +) { let clone_id = match cx.tcx.lang_items().clone_trait() { Some(id) if trait_ref.trait_def_id() == Some(id) => id, _ => return, @@ -427,7 +431,14 @@ struct UnsafeVisitor<'a, 'tcx> { impl<'tcx> Visitor<'tcx> for UnsafeVisitor<'_, 'tcx> { type NestedFilter = nested_filter::All; - fn visit_fn(&mut self, kind: FnKind<'tcx>, decl: &'tcx FnDecl<'_>, body_id: BodyId, _: Span, id: LocalDefId) { + fn visit_fn( + &mut self, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body_id: BodyId, + _: Span, + id: LocalDefId, + ) { if self.has_unsafe { return; } @@ -463,7 +474,12 @@ impl<'tcx> Visitor<'tcx> for UnsafeVisitor<'_, 'tcx> { } /// Implementation of the `DERIVE_PARTIAL_EQ_WITHOUT_EQ` lint. -fn check_partial_eq_without_eq<'tcx>(cx: &LateContext<'tcx>, span: Span, trait_ref: &hir::TraitRef<'_>, ty: Ty<'tcx>) { +fn check_partial_eq_without_eq<'tcx>( + cx: &LateContext<'tcx>, + span: Span, + trait_ref: &hir::TraitRef<'_>, + ty: Ty<'tcx>, +) { if_chain! { if let ty::Adt(adt, args) = ty.kind(); if cx.tcx.visibility(adt.did()).is_public(); @@ -471,12 +487,12 @@ fn check_partial_eq_without_eq<'tcx>(cx: &LateContext<'tcx>, span: Span, trait_r if let Some(def_id) = trait_ref.trait_def_id(); if cx.tcx.is_diagnostic_item(sym::PartialEq, def_id); let param_env = param_env_for_derived_eq(cx.tcx, adt.did(), eq_trait_def_id); - if !implements_trait_with_env(cx.tcx, param_env, ty, eq_trait_def_id, []); + if !implements_trait_with_env(cx.tcx, param_env, ty, eq_trait_def_id, &[]); // If all of our fields implement `Eq`, we can implement `Eq` too if adt .all_fields() .map(|f| f.ty(cx.tcx, args)) - .all(|ty| implements_trait_with_env(cx.tcx, param_env, ty, eq_trait_def_id, [])); + .all(|ty| implements_trait_with_env(cx.tcx, param_env, ty, eq_trait_def_id, &[])); then { span_lint_and_sugg( cx, diff --git a/clippy_lints/src/error_impl_error.rs b/clippy_lints/src/error_impl_error.rs new file mode 100644 index 00000000000..379af9b2234 --- /dev/null +++ b/clippy_lints/src/error_impl_error.rs @@ -0,0 +1,87 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_hir_and_then}; +use clippy_utils::path_res; +use clippy_utils::ty::implements_trait; +use rustc_hir::def_id::{DefId, LocalDefId}; +use rustc_hir::{Item, ItemKind}; +use rustc_hir_analysis::hir_ty_to_ty; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::Visibility; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for types named `Error` that implement `Error`. + /// + /// ### Why is this bad? + /// It can become confusing when a codebase has 20 types all named `Error`, requiring either + /// aliasing them in the `use` statement or qualifying them like `my_module::Error`. This + /// hinders comprehension, as it requires you to memorize every variation of importing `Error` + /// used across a codebase. + /// + /// ### Example + /// ```rust,ignore + /// #[derive(Debug)] + /// pub enum Error { ... } + /// + /// impl std::fmt::Display for Error { ... } + /// + /// impl std::error::Error for Error { ... } + /// ``` + #[clippy::version = "1.72.0"] + pub ERROR_IMPL_ERROR, + restriction, + "exported types named `Error` that implement `Error`" +} +declare_lint_pass!(ErrorImplError => [ERROR_IMPL_ERROR]); + +impl<'tcx> LateLintPass<'tcx> for ErrorImplError { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + let Some(error_def_id) = cx.tcx.get_diagnostic_item(sym::Error) else { + return; + }; + + match item.kind { + ItemKind::TyAlias(ty, _) if implements_trait(cx, hir_ty_to_ty(cx.tcx, ty), error_def_id, &[]) + && item.ident.name == sym::Error + && is_visible_outside_module(cx, item.owner_id.def_id) => + { + span_lint( + cx, + ERROR_IMPL_ERROR, + item.ident.span, + "exported type alias named `Error` that implements `Error`", + ); + }, + ItemKind::Impl(imp) if let Some(trait_def_id) = imp.of_trait.and_then(|t| t.trait_def_id()) + && error_def_id == trait_def_id + && let Some(def_id) = path_res(cx, imp.self_ty).opt_def_id().and_then(DefId::as_local) + && let hir_id = cx.tcx.hir().local_def_id_to_hir_id(def_id) + && let Some(ident) = cx.tcx.opt_item_ident(def_id.to_def_id()) + && ident.name == sym::Error + && is_visible_outside_module(cx, def_id) => + { + span_lint_hir_and_then( + cx, + ERROR_IMPL_ERROR, + hir_id, + ident.span, + "exported type named `Error` that implements `Error`", + |diag| { + diag.span_note(item.span, "`Error` was implemented here"); + } + ); + } + _ => {}, + } + } +} + +/// Do not lint private `Error`s, i.e., ones without any `pub` (minus `pub(self)` of course) and +/// which aren't reexported +fn is_visible_outside_module(cx: &LateContext<'_>, def_id: LocalDefId) -> bool { + !matches!( + cx.tcx.visibility(def_id), + Visibility::Restricted(mod_def_id) if cx.tcx.parent_module_from_def_id(def_id).to_def_id() == mod_def_id + ) +} diff --git a/clippy_lints/src/eta_reduction.rs b/clippy_lints/src/eta_reduction.rs index 22e10accd35..8d6fb8438b6 100644 --- a/clippy_lints/src/eta_reduction.rs +++ b/clippy_lints/src/eta_reduction.rs @@ -1,19 +1,22 @@ use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::higher::VecArgs; use clippy_utils::source::snippet_opt; -use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; -use clippy_utils::usage::local_used_after_expr; +use clippy_utils::ty::type_diagnostic_name; +use clippy_utils::usage::{local_used_after_expr, local_used_in}; use clippy_utils::{higher, is_adjusted, path_to_local, path_to_local_id}; -use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir::def_id::DefId; -use rustc_hir::{Closure, Expr, ExprKind, Param, PatKind, Unsafety}; +use rustc_hir::{BindingAnnotation, Expr, ExprKind, FnRetTy, Param, PatKind, QPath, TyKind, Unsafety}; +use rustc_infer::infer::TyCtxtInferExt; use rustc_lint::{LateContext, LateLintPass}; -use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow}; -use rustc_middle::ty::binding::BindingMode; -use rustc_middle::ty::{self, EarlyBinder, GenericArgsRef, Ty, TypeVisitableExt}; +use rustc_middle::ty::{ + self, Binder, BoundConstness, ClosureArgs, ClosureKind, EarlyBinder, FnSig, GenericArg, GenericArgKind, + GenericArgsRef, ImplPolarity, List, Region, RegionKind, Ty, TypeVisitableExt, TypeckResults, +}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::symbol::sym; +use rustc_target::spec::abi::Abi; +use rustc_trait_selection::traits::error_reporting::InferCtxtExt as _; declare_clippy_lint! { /// ### What it does @@ -72,14 +75,18 @@ declare_clippy_lint! { declare_lint_pass!(EtaReduction => [REDUNDANT_CLOSURE, REDUNDANT_CLOSURE_FOR_METHOD_CALLS]); impl<'tcx> LateLintPass<'tcx> for EtaReduction { + #[allow(clippy::too_many_lines)] fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - if expr.span.from_expansion() { + let body = if let ExprKind::Closure(c) = expr.kind + && c.fn_decl.inputs.iter().all(|ty| matches!(ty.kind, TyKind::Infer)) + && matches!(c.fn_decl.output, FnRetTy::DefaultReturn(_)) + && !expr.span.from_expansion() + { + cx.tcx.hir().body(c.body) + } else { return; - } - let body = match expr.kind { - ExprKind::Closure(&Closure { body, .. }) => cx.tcx.hir().body(body), - _ => return, }; + if body.value.span.from_expansion() { if body.params.is_empty() { if let Some(VecArgs::Vec(&[])) = higher::VecArgs::hir(cx, body.value) { @@ -99,140 +106,209 @@ impl<'tcx> LateLintPass<'tcx> for EtaReduction { return; } - let closure_ty = cx.typeck_results().expr_ty(expr); + let typeck = cx.typeck_results(); + let closure = if let ty::Closure(_, closure_subs) = typeck.expr_ty(expr).kind() { + closure_subs.as_closure() + } else { + return; + }; - if_chain!( - if !is_adjusted(cx, body.value); - if let ExprKind::Call(callee, args) = body.value.kind; - if let ExprKind::Path(_) = callee.kind; - if check_inputs(cx, body.params, None, args); - let callee_ty = cx.typeck_results().expr_ty_adjusted(callee); - let call_ty = cx.typeck_results().type_dependent_def_id(body.value.hir_id) - .map_or(callee_ty, |id| cx.tcx.type_of(id).instantiate_identity()); - if check_sig(cx, closure_ty, call_ty); - let args = cx.typeck_results().node_args(callee.hir_id); - // This fixes some false positives that I don't entirely understand - if args.is_empty() || !cx.typeck_results().expr_ty(expr).has_late_bound_regions(); - // A type param function ref like `T::f` is not 'static, however - // it is if cast like `T::f as fn()`. This seems like a rustc bug. - if !args.types().any(|t| matches!(t.kind(), ty::Param(_))); - let callee_ty_unadjusted = cx.typeck_results().expr_ty(callee).peel_refs(); - if !is_type_diagnostic_item(cx, callee_ty_unadjusted, sym::Arc); - if !is_type_diagnostic_item(cx, callee_ty_unadjusted, sym::Rc); - if let ty::Closure(_, args) = *closure_ty.kind(); - // Don't lint if this is an inclusive range expression. - // They desugar to a call to `RangeInclusiveNew` which would have odd suggestions. (#10684) - if !matches!(higher::Range::hir(body.value), Some(higher::Range { - start: Some(_), - end: Some(_), - limits: rustc_ast::RangeLimits::Closed - })); - then { - span_lint_and_then(cx, REDUNDANT_CLOSURE, expr.span, "redundant closure", |diag| { - if let Some(mut snippet) = snippet_opt(cx, callee.span) { - if let Some(fn_mut_id) = cx.tcx.lang_items().fn_mut_trait() - && let args = cx.tcx.erase_late_bound_regions(args.as_closure().sig()).inputs() - && implements_trait( - cx, - callee_ty.peel_refs(), - fn_mut_id, - &args.iter().copied().map(Into::into).collect::<Vec<_>>(), - ) - && path_to_local(callee).map_or(false, |l| local_used_after_expr(cx, l, expr)) - { - // Mutable closure is used after current expr; we cannot consume it. - snippet = format!("&mut {snippet}"); - } + if is_adjusted(cx, body.value) { + return; + } - diag.span_suggestion( - expr.span, - "replace the closure with the function itself", - snippet, - Applicability::MachineApplicable, - ); - } - }); - } - ); + match body.value.kind { + ExprKind::Call(callee, args) + if matches!(callee.kind, ExprKind::Path(QPath::Resolved(..) | QPath::TypeRelative(..))) => + { + let callee_ty = typeck.expr_ty(callee).peel_refs(); + if matches!( + type_diagnostic_name(cx, callee_ty), + Some(sym::Arc | sym::Rc) + ) || !check_inputs(typeck, body.params, None, args) { + return; + } + let callee_ty_adjusted = typeck.expr_adjustments(callee).last().map_or( + callee_ty, + |a| a.target.peel_refs(), + ); - if_chain!( - if !is_adjusted(cx, body.value); - if let ExprKind::MethodCall(path, receiver, args, _) = body.value.kind; - if check_inputs(cx, body.params, Some(receiver), args); - let method_def_id = cx.typeck_results().type_dependent_def_id(body.value.hir_id).unwrap(); - let args = cx.typeck_results().node_args(body.value.hir_id); - let call_ty = cx.tcx.type_of(method_def_id).instantiate(cx.tcx, args); - if check_sig(cx, closure_ty, call_ty); - then { - span_lint_and_then(cx, REDUNDANT_CLOSURE_FOR_METHOD_CALLS, expr.span, "redundant closure", |diag| { - let name = get_ufcs_type_name(cx, method_def_id, args); - diag.span_suggestion( + let sig = match callee_ty_adjusted.kind() { + ty::FnDef(def, _) => cx.tcx.fn_sig(def).skip_binder().skip_binder(), + ty::FnPtr(sig) => sig.skip_binder(), + ty::Closure(_, subs) => cx + .tcx + .signature_unclosure(subs.as_closure().sig(), Unsafety::Normal) + .skip_binder(), + _ => { + if typeck.type_dependent_def_id(body.value.hir_id).is_some() + && let subs = typeck.node_args(body.value.hir_id) + && let output = typeck.expr_ty(body.value) + && let ty::Tuple(tys) = *subs.type_at(1).kind() + { + cx.tcx.mk_fn_sig(tys, output, false, Unsafety::Normal, Abi::Rust) + } else { + return; + } + }, + }; + if check_sig(cx, closure, sig) + && let generic_args = typeck.node_args(callee.hir_id) + // Given some trait fn `fn f() -> ()` and some type `T: Trait`, `T::f` is not + // `'static` unless `T: 'static`. The cast `T::f as fn()` will, however, result + // in a type which is `'static`. + // For now ignore all callee types which reference a type parameter. + && !generic_args.types().any(|t| matches!(t.kind(), ty::Param(_))) + { + span_lint_and_then( + cx, + REDUNDANT_CLOSURE, expr.span, - "replace the closure with the method itself", - format!("{name}::{}", path.ident.name), - Applicability::MachineApplicable, + "redundant closure", + |diag| { + if let Some(mut snippet) = snippet_opt(cx, callee.span) { + if let Ok((ClosureKind::FnMut, _)) + = cx.tcx.infer_ctxt().build().type_implements_fn_trait( + cx.param_env, + Binder::bind_with_vars(callee_ty_adjusted, List::empty()), + BoundConstness::NotConst, + ImplPolarity::Positive, + ) && path_to_local(callee) + .map_or( + false, + |l| local_used_in(cx, l, args) || local_used_after_expr(cx, l, expr), + ) + { + // Mutable closure is used after current expr; we cannot consume it. + snippet = format!("&mut {snippet}"); + } + diag.span_suggestion( + expr.span, + "replace the closure with the function itself", + snippet, + Applicability::MachineApplicable, + ); + } + } ); - }) - } - ); + } + }, + ExprKind::MethodCall(path, self_, args, _) if check_inputs(typeck, body.params, Some(self_), args) => { + if let Some(method_def_id) = typeck.type_dependent_def_id(body.value.hir_id) + && check_sig(cx, closure, cx.tcx.fn_sig(method_def_id).skip_binder().skip_binder()) + { + span_lint_and_then( + cx, + REDUNDANT_CLOSURE_FOR_METHOD_CALLS, + expr.span, + "redundant closure", + |diag| { + let args = typeck.node_args(body.value.hir_id); + let name = get_ufcs_type_name(cx, method_def_id, args); + diag.span_suggestion( + expr.span, + "replace the closure with the method itself", + format!("{}::{}", name, path.ident.name), + Applicability::MachineApplicable, + ); + }, + ); + } + }, + _ => (), + } } } fn check_inputs( - cx: &LateContext<'_>, + typeck: &TypeckResults<'_>, params: &[Param<'_>], - receiver: Option<&Expr<'_>>, - call_args: &[Expr<'_>], + self_arg: Option<&Expr<'_>>, + args: &[Expr<'_>], ) -> bool { - if receiver.map_or(params.len() != call_args.len(), |_| params.len() != call_args.len() + 1) { - return false; + params.len() == self_arg.map_or(0, |_| 1) + args.len() + && params.iter().zip(self_arg.into_iter().chain(args)).all(|(p, arg)| { + matches!( + p.pat.kind,PatKind::Binding(BindingAnnotation::NONE, id, _, None) + if path_to_local_id(arg, id) + ) + // Only allow adjustments which change regions (i.e. re-borrowing). + && typeck + .expr_adjustments(arg) + .last() + .map_or(true, |a| a.target == typeck.expr_ty(arg)) + }) +} + +fn check_sig<'tcx>(cx: &LateContext<'tcx>, closure: ClosureArgs<'tcx>, call_sig: FnSig<'_>) -> bool { + call_sig.unsafety == Unsafety::Normal + && !has_late_bound_to_non_late_bound_regions( + cx.tcx + .signature_unclosure(closure.sig(), Unsafety::Normal) + .skip_binder(), + call_sig, + ) +} + +/// This walks through both signatures and checks for any time a late-bound region is expected by an +/// `impl Fn` type, but the target signature does not have a late-bound region in the same position. +/// +/// This is needed because rustc is unable to late bind early-bound regions in a function signature. +fn has_late_bound_to_non_late_bound_regions(from_sig: FnSig<'_>, to_sig: FnSig<'_>) -> bool { + fn check_region(from_region: Region<'_>, to_region: Region<'_>) -> bool { + matches!(from_region.kind(), RegionKind::ReLateBound(..)) + && !matches!(to_region.kind(), RegionKind::ReLateBound(..)) } - let binding_modes = cx.typeck_results().pat_binding_modes(); - let check_inputs = |param: &Param<'_>, arg| { - match param.pat.kind { - PatKind::Binding(_, id, ..) if path_to_local_id(arg, id) => {}, - _ => return false, - } - // checks that parameters are not bound as `ref` or `ref mut` - if let Some(BindingMode::BindByReference(_)) = binding_modes.get(param.pat.hir_id) { - return false; - } - match *cx.typeck_results().expr_adjustments(arg) { - [] => true, - [ - Adjustment { - kind: Adjust::Deref(None), - .. + fn check_subs(from_subs: &[GenericArg<'_>], to_subs: &[GenericArg<'_>]) -> bool { + if from_subs.len() != to_subs.len() { + return true; + } + for (from_arg, to_arg) in to_subs.iter().zip(from_subs) { + match (from_arg.unpack(), to_arg.unpack()) { + (GenericArgKind::Lifetime(from_region), GenericArgKind::Lifetime(to_region)) => { + if check_region(from_region, to_region) { + return true; + } }, - Adjustment { - kind: Adjust::Borrow(AutoBorrow::Ref(_, mu2)), - .. + (GenericArgKind::Type(from_ty), GenericArgKind::Type(to_ty)) => { + if check_ty(from_ty, to_ty) { + return true; + } }, - ] => { - // re-borrow with the same mutability is allowed - let ty = cx.typeck_results().expr_ty(arg); - matches!(*ty.kind(), ty::Ref(.., mu1) if mu1 == mu2.into()) - }, - _ => false, + (GenericArgKind::Const(_), GenericArgKind::Const(_)) => (), + _ => return true, + } } - }; - std::iter::zip(params, receiver.into_iter().chain(call_args.iter())).all(|(param, arg)| check_inputs(param, arg)) -} - -fn check_sig<'tcx>(cx: &LateContext<'tcx>, closure_ty: Ty<'tcx>, call_ty: Ty<'tcx>) -> bool { - let call_sig = call_ty.fn_sig(cx.tcx); - if call_sig.unsafety() == Unsafety::Unsafe { - return false; + false } - if !closure_ty.has_late_bound_regions() { - return true; + + fn check_ty(from_ty: Ty<'_>, to_ty: Ty<'_>) -> bool { + match (from_ty.kind(), to_ty.kind()) { + (&ty::Adt(_, from_subs), &ty::Adt(_, to_subs)) => check_subs(from_subs, to_subs), + (&ty::Array(from_ty, _), &ty::Array(to_ty, _)) | (&ty::Slice(from_ty), &ty::Slice(to_ty)) => { + check_ty(from_ty, to_ty) + }, + (&ty::Ref(from_region, from_ty, _), &ty::Ref(to_region, to_ty, _)) => { + check_region(from_region, to_region) || check_ty(from_ty, to_ty) + }, + (&ty::Tuple(from_tys), &ty::Tuple(to_tys)) => { + from_tys.len() != to_tys.len() + || from_tys + .iter() + .zip(to_tys) + .any(|(from_ty, to_ty)| check_ty(from_ty, to_ty)) + }, + _ => from_ty.has_late_bound_regions(), + } } - let ty::Closure(_, args) = closure_ty.kind() else { - return false; - }; - let closure_sig = cx.tcx.signature_unclosure(args.as_closure().sig(), Unsafety::Normal); - cx.tcx.erase_late_bound_regions(closure_sig) == cx.tcx.erase_late_bound_regions(call_sig) + + assert!(from_sig.inputs_and_output.len() == to_sig.inputs_and_output.len()); + from_sig + .inputs_and_output + .iter() + .zip(to_sig.inputs_and_output) + .any(|(from_ty, to_ty)| check_ty(from_ty, to_ty)) } fn get_ufcs_type_name<'tcx>(cx: &LateContext<'tcx>, method_def_id: DefId, args: GenericArgsRef<'tcx>) -> String { @@ -241,7 +317,7 @@ fn get_ufcs_type_name<'tcx>(cx: &LateContext<'tcx>, method_def_id: DefId, args: match assoc_item.container { ty::TraitContainer => cx.tcx.def_path_str(def_id), ty::ImplContainer => { - let ty = cx.tcx.type_of(def_id).skip_binder(); + let ty = cx.tcx.type_of(def_id).instantiate_identity(); match ty.kind() { ty::Adt(adt, _) => cx.tcx.def_path_str(adt.did()), ty::Array(..) diff --git a/clippy_lints/src/four_forward_slashes.rs b/clippy_lints/src/four_forward_slashes.rs new file mode 100644 index 00000000000..419c7734344 --- /dev/null +++ b/clippy_lints/src/four_forward_slashes.rs @@ -0,0 +1,99 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use rustc_errors::Applicability; +use rustc_hir::Item; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for outer doc comments written with 4 forward slashes (`////`). + /// + /// ### Why is this bad? + /// This is (probably) a typo, and results in it not being a doc comment; just a regular + /// comment. + /// + /// ### Example + /// ```rust + /// //// My amazing data structure + /// pub struct Foo { + /// // ... + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// /// My amazing data structure + /// pub struct Foo { + /// // ... + /// } + /// ``` + #[clippy::version = "1.72.0"] + pub FOUR_FORWARD_SLASHES, + suspicious, + "comments with 4 forward slashes (`////`) likely intended to be doc comments (`///`)" +} +declare_lint_pass!(FourForwardSlashes => [FOUR_FORWARD_SLASHES]); + +impl<'tcx> LateLintPass<'tcx> for FourForwardSlashes { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if item.span.from_expansion() { + return; + } + let sm = cx.sess().source_map(); + let mut span = cx + .tcx + .hir() + .attrs(item.hir_id()) + .iter() + .fold(item.span.shrink_to_lo(), |span, attr| span.to(attr.span)); + let (Some(file), _, _, end_line, _) = sm.span_to_location_info(span) else { + return; + }; + let mut bad_comments = vec![]; + for line in (0..end_line.saturating_sub(1)).rev() { + let Some(contents) = file.get_line(line).map(|c| c.trim().to_owned()) else { + return; + }; + // Keep searching until we find the next item + if !contents.is_empty() && !contents.starts_with("//") && !contents.starts_with("#[") { + break; + } + + if contents.starts_with("////") && !matches!(contents.chars().nth(4), Some('/' | '!')) { + let bounds = file.line_bounds(line); + let line_span = Span::with_root_ctxt(bounds.start, bounds.end); + span = line_span.to(span); + bad_comments.push((line_span, contents)); + } + } + + if !bad_comments.is_empty() { + span_lint_and_then( + cx, + FOUR_FORWARD_SLASHES, + span, + "this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't", + |diag| { + let msg = if bad_comments.len() == 1 { + "make this a doc comment by removing one `/`" + } else { + "turn these into doc comments by removing one `/`" + }; + + diag.multipart_suggestion( + msg, + bad_comments + .into_iter() + // It's a little unfortunate but the span includes the `\n` yet the contents + // do not, so we must add it back. If some codebase uses `\r\n` instead they + // will need normalization but it should be fine + .map(|(span, c)| (span, c.replacen("////", "///", 1) + "\n")) + .collect(), + Applicability::MachineApplicable, + ); + }, + ); + } + } +} diff --git a/clippy_lints/src/incorrect_impls.rs b/clippy_lints/src/incorrect_impls.rs index e6c42ebb832..3c59b839a39 100644 --- a/clippy_lints/src/incorrect_impls.rs +++ b/clippy_lints/src/incorrect_impls.rs @@ -1,8 +1,9 @@ use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::paths::ORD_CMP; use clippy_utils::ty::implements_trait; -use clippy_utils::{get_parent_node, is_res_lang_ctor, last_path_segment, path_res}; +use clippy_utils::{get_parent_node, is_res_lang_ctor, last_path_segment, match_def_path, path_res, std_or_core}; use rustc_errors::Applicability; -use rustc_hir::def::Res; +use rustc_hir::def_id::LocalDefId; use rustc_hir::{Expr, ExprKind, ImplItem, ImplItemKind, ItemKind, LangItem, Node, UnOp}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::EarlyBinder; @@ -59,6 +60,10 @@ declare_clippy_lint! { /// wrapping the result of `cmp` in `Some` for `partial_cmp`. Not doing this may silently /// introduce an error upon refactoring. /// + /// ### Known issues + /// Code that calls the `.into()` method instead will be flagged as incorrect, despite `.into()` + /// wrapping it in `Some`. + /// /// ### Limitations /// Will not lint if `Self` and `Rhs` do not have the same type. /// @@ -190,6 +195,11 @@ impl LateLintPass<'_> for IncorrectImpls { &[], ) { + // If the `cmp` call likely needs to be fully qualified in the suggestion + // (like `std::cmp::Ord::cmp`). It's unfortunate we must put this here but we can't + // access `cmp_expr` in the suggestion without major changes, as we lint in `else`. + let mut needs_fully_qualified = false; + if block.stmts.is_empty() && let Some(expr) = block.expr && let ExprKind::Call( @@ -201,9 +211,8 @@ impl LateLintPass<'_> for IncorrectImpls { [cmp_expr], ) = expr.kind && is_res_lang_ctor(cx, cx.qpath_res(some_path, *some_hir_id), LangItem::OptionSome) - && let ExprKind::MethodCall(cmp_path, _, [other_expr], ..) = cmp_expr.kind - && cmp_path.ident.name == sym::cmp - && let Res::Local(..) = path_res(cx, other_expr) + // Fix #11178, allow `Self::cmp(self, ..)` too + && self_cmp_call(cx, cmp_expr, impl_item.owner_id.def_id, &mut needs_fully_qualified) {} else { // If `Self` and `Rhs` are not the same type, bail. This makes creating a valid // suggestion tons more complex. @@ -220,14 +229,29 @@ impl LateLintPass<'_> for IncorrectImpls { let [_, other] = body.params else { return; }; + let Some(std_or_core) = std_or_core(cx) else { + return; + }; - let suggs = if let Some(other_ident) = other.pat.simple_ident() { - vec![(block.span, format!("{{ Some(self.cmp({})) }}", other_ident.name))] - } else { - vec![ + let suggs = match (other.pat.simple_ident(), needs_fully_qualified) { + (Some(other_ident), true) => vec![( + block.span, + format!("{{ Some({std_or_core}::cmp::Ord::cmp(self, {})) }}", other_ident.name), + )], + (Some(other_ident), false) => { + vec![(block.span, format!("{{ Some(self.cmp({})) }}", other_ident.name))] + }, + (None, true) => vec![ + ( + block.span, + format!("{{ Some({std_or_core}::cmp::Ord::cmp(self, other)) }}"), + ), + (other.pat.span, "other".to_owned()), + ], + (None, false) => vec![ (block.span, "{ Some(self.cmp(other)) }".to_owned()), (other.pat.span, "other".to_owned()), - ] + ], }; diag.multipart_suggestion( @@ -241,3 +265,31 @@ impl LateLintPass<'_> for IncorrectImpls { } } } + +/// Returns whether this is any of `self.cmp(..)`, `Self::cmp(self, ..)` or `Ord::cmp(self, ..)`. +fn self_cmp_call<'tcx>( + cx: &LateContext<'tcx>, + cmp_expr: &'tcx Expr<'tcx>, + def_id: LocalDefId, + needs_fully_qualified: &mut bool, +) -> bool { + match cmp_expr.kind { + ExprKind::Call(path, [_self, _other]) => path_res(cx, path) + .opt_def_id() + .is_some_and(|def_id| match_def_path(cx, def_id, &ORD_CMP)), + ExprKind::MethodCall(_, _, [_other], ..) => { + // We can set this to true here no matter what as if it's a `MethodCall` and goes to the + // `else` branch, it must be a method named `cmp` that isn't `Ord::cmp` + *needs_fully_qualified = true; + + // It's a bit annoying but `typeck_results` only gives us the CURRENT body, which we + // have none, not of any `LocalDefId` we want, so we must call the query itself to avoid + // an immediate ICE + cx.tcx + .typeck(def_id) + .type_dependent_def_id(cmp_expr.hir_id) + .is_some_and(|def_id| match_def_path(cx, def_id, &ORD_CMP)) + }, + _ => false, + } +} diff --git a/clippy_lints/src/inherent_to_string.rs b/clippy_lints/src/inherent_to_string.rs index d43e5cc9b2c..bc4ec33b733 100644 --- a/clippy_lints/src/inherent_to_string.rs +++ b/clippy_lints/src/inherent_to_string.rs @@ -1,11 +1,11 @@ use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::ty::{implements_trait, is_type_lang_item}; use clippy_utils::{return_ty, trait_ref_of_method}; -use if_chain::if_chain; -use rustc_hir::{GenericParamKind, ImplItem, ImplItemKind, LangItem}; +use rustc_hir::{GenericParamKind, ImplItem, ImplItemKind, LangItem, Unsafety}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::sym; +use rustc_target::spec::abi::Abi; declare_clippy_lint! { /// ### What it does @@ -95,24 +95,23 @@ impl<'tcx> LateLintPass<'tcx> for InherentToString { return; } - if_chain! { - // Check if item is a method, called to_string and has a parameter 'self' - if let ImplItemKind::Fn(ref signature, _) = impl_item.kind; - if impl_item.ident.name == sym::to_string; - let decl = &signature.decl; - if decl.implicit_self.has_implicit_self(); - if decl.inputs.len() == 1; - if impl_item.generics.params.iter().all(|p| matches!(p.kind, GenericParamKind::Lifetime { .. })); - + // Check if item is a method called `to_string` and has a parameter 'self' + if let ImplItemKind::Fn(ref signature, _) = impl_item.kind + // #11201 + && let header = signature.header + && header.unsafety == Unsafety::Normal + && header.abi == Abi::Rust + && impl_item.ident.name == sym::to_string + && let decl = signature.decl + && decl.implicit_self.has_implicit_self() + && decl.inputs.len() == 1 + && impl_item.generics.params.iter().all(|p| matches!(p.kind, GenericParamKind::Lifetime { .. })) // Check if return type is String - if is_type_lang_item(cx, return_ty(cx, impl_item.owner_id), LangItem::String); - + && is_type_lang_item(cx, return_ty(cx, impl_item.owner_id), LangItem::String) // Filters instances of to_string which are required by a trait - if trait_ref_of_method(cx, impl_item.owner_id.def_id).is_none(); - - then { - show_lint(cx, impl_item); - } + && trait_ref_of_method(cx, impl_item.owner_id.def_id).is_none() + { + show_lint(cx, impl_item); } } } diff --git a/clippy_lints/src/len_zero.rs b/clippy_lints/src/len_zero.rs index 50c433d3161..deba232bdd2 100644 --- a/clippy_lints/src/len_zero.rs +++ b/clippy_lints/src/len_zero.rs @@ -7,11 +7,10 @@ use rustc_ast::ast::LitKind; use rustc_errors::Applicability; use rustc_hir::def::Res; use rustc_hir::def_id::{DefId, DefIdSet}; -use rustc_hir::lang_items::LangItem; use rustc_hir::{ AssocItemKind, BinOpKind, Expr, ExprKind, FnRetTy, GenericArg, GenericBound, ImplItem, ImplItemKind, - ImplicitSelfKind, Item, ItemKind, Mutability, Node, PathSegment, PrimTy, QPath, TraitItemRef, TyKind, - TypeBindingKind, + ImplicitSelfKind, Item, ItemKind, LangItem, Mutability, Node, PatKind, PathSegment, PrimTy, QPath, TraitItemRef, + TyKind, TypeBindingKind, }; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::{self, AssocKind, FnSig, Ty}; @@ -171,6 +170,31 @@ impl<'tcx> LateLintPass<'tcx> for LenZero { return; } + if let ExprKind::Let(lt) = expr.kind + && has_is_empty(cx, lt.init) + && match lt.pat.kind { + PatKind::Slice([], None, []) => true, + PatKind::Lit(lit) if is_empty_string(lit) => true, + _ => false, + } + { + let mut applicability = Applicability::MachineApplicable; + + let lit1 = peel_ref_operators(cx, lt.init); + let lit_str = + Sugg::hir_with_context(cx, lit1, lt.span.ctxt(), "_", &mut applicability).maybe_par(); + + span_lint_and_sugg( + cx, + COMPARISON_TO_EMPTY, + lt.span, + "comparison to empty slice using `if let`", + "using `is_empty` is clearer and more explicit", + format!("{lit_str}.is_empty()"), + applicability, + ); + } + if let ExprKind::Binary(Spanned { node: cmp, .. }, left, right) = expr.kind { // expr.span might contains parenthesis, see issue #10529 let actual_span = left.span.with_hi(right.span.hi()); diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 5e62cfd70ec..9d6096ccb2a 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -65,6 +65,7 @@ mod declared_lints; mod renamed_lints; // begin lints modules, do not remove this comment, it’s used in `update_lints` +mod absolute_paths; mod allow_attributes; mod almost_complete_range; mod approx_const; @@ -120,6 +121,7 @@ mod entry; mod enum_clike; mod enum_variants; mod equatable_if_let; +mod error_impl_error; mod escape; mod eta_reduction; mod excessive_bools; @@ -136,6 +138,7 @@ mod format_args; mod format_impl; mod format_push_string; mod formatting; +mod four_forward_slashes; mod from_over_into; mod from_raw_with_void_ptr; mod from_str_radix_10; @@ -272,6 +275,7 @@ mod redundant_clone; mod redundant_closure_call; mod redundant_else; mod redundant_field_names; +mod redundant_locals; mod redundant_pub_crate; mod redundant_slicing; mod redundant_static_lifetimes; @@ -909,7 +913,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(move |_| Box::new(if_then_some_else_none::IfThenSomeElseNone::new(msrv()))); store.register_late_pass(|_| Box::new(bool_assert_comparison::BoolAssertComparison)); store.register_early_pass(move || Box::new(module_style::ModStyle)); - store.register_late_pass(|_| Box::new(unused_async::UnusedAsync)); + store.register_late_pass(|_| Box::<unused_async::UnusedAsync>::default()); let disallowed_types = conf.disallowed_types.clone(); store.register_late_pass(move |_| Box::new(disallowed_types::DisallowedTypes::new(disallowed_types.clone()))); let import_renames = conf.enforced_import_renames.clone(); @@ -1078,6 +1082,17 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_early_pass(|| Box::new(visibility::Visibility)); store.register_late_pass(move |_| Box::new(tuple_array_conversions::TupleArrayConversions { msrv: msrv() })); store.register_late_pass(|_| Box::new(manual_float_methods::ManualFloatMethods)); + store.register_late_pass(|_| Box::new(four_forward_slashes::FourForwardSlashes)); + store.register_late_pass(|_| Box::new(error_impl_error::ErrorImplError)); + let absolute_paths_max_segments = conf.absolute_paths_max_segments; + let absolute_paths_allowed_crates = conf.absolute_paths_allowed_crates.clone(); + store.register_late_pass(move |_| { + Box::new(absolute_paths::AbsolutePaths { + absolute_paths_max_segments, + absolute_paths_allowed_crates: absolute_paths_allowed_crates.clone(), + }) + }); + store.register_late_pass(|_| Box::new(redundant_locals::RedundantLocals)); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/clippy_lints/src/lifetimes.rs b/clippy_lints/src/lifetimes.rs index 852f6736585..0004a150d51 100644 --- a/clippy_lints/src/lifetimes.rs +++ b/clippy_lints/src/lifetimes.rs @@ -15,6 +15,7 @@ use rustc_hir::{ PredicateOrigin, TraitFn, TraitItem, TraitItemKind, Ty, TyKind, WherePredicate, }; use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::hir::map::Map; use rustc_middle::hir::nested_filter as middle_nested_filter; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_lint_pass, declare_tool_lint}; @@ -620,7 +621,7 @@ impl<'cx, 'tcx, F> Visitor<'tcx> for LifetimeChecker<'cx, 'tcx, F> where F: NestedFilter<'tcx>, { - type Map = rustc_middle::hir::map::Map<'tcx>; + type Map = Map<'tcx>; type NestedFilter = F; // for lifetimes as parameters of generics diff --git a/clippy_lints/src/loops/explicit_iter_loop.rs b/clippy_lints/src/loops/explicit_iter_loop.rs index a84a0a6eeb8..7b8c88235a9 100644 --- a/clippy_lints/src/loops/explicit_iter_loop.rs +++ b/clippy_lints/src/loops/explicit_iter_loop.rs @@ -109,7 +109,7 @@ fn is_ref_iterable<'tcx>( && let sig = cx.tcx.liberate_late_bound_regions(fn_id, cx.tcx.fn_sig(fn_id).skip_binder()) && let &[req_self_ty, req_res_ty] = &**sig.inputs_and_output && let param_env = cx.tcx.param_env(fn_id) - && implements_trait_with_env(cx.tcx, param_env, req_self_ty, trait_id, []) + && implements_trait_with_env(cx.tcx, param_env, req_self_ty, trait_id, &[]) && let Some(into_iter_ty) = make_normalized_projection_with_regions(cx.tcx, param_env, trait_id, sym!(IntoIter), [req_self_ty]) && let req_res_ty = normalize_with_regions(cx.tcx, param_env, req_res_ty) diff --git a/clippy_lints/src/loops/single_element_loop.rs b/clippy_lints/src/loops/single_element_loop.rs index 744fd61bd13..dfb800ccf71 100644 --- a/clippy_lints/src/loops/single_element_loop.rs +++ b/clippy_lints/src/loops/single_element_loop.rs @@ -9,6 +9,7 @@ use rustc_errors::Applicability; use rustc_hir::{is_range_literal, BorrowKind, Expr, ExprKind, Pat}; use rustc_lint::LateContext; use rustc_span::edition::Edition; +use rustc_span::sym; pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, @@ -51,7 +52,7 @@ pub(super) fn check<'tcx>( }, [], _, - ) if method.ident.name.as_str() == "iter_mut" => (arg, "&mut "), + ) if method.ident.name == sym::iter_mut => (arg, "&mut "), ExprKind::MethodCall( method, Expr { diff --git a/clippy_lints/src/loops/utils.rs b/clippy_lints/src/loops/utils.rs index 28ee24309cc..6edca2d55f6 100644 --- a/clippy_lints/src/loops/utils.rs +++ b/clippy_lints/src/loops/utils.rs @@ -76,7 +76,7 @@ impl<'a, 'tcx> Visitor<'tcx> for IncrementVisitor<'a, 'tcx> { ExprKind::Assign(lhs, _, _) if lhs.hir_id == expr.hir_id => { *state = IncrementVisitorVarState::DontWarn; }, - ExprKind::AddrOf(BorrowKind::Ref, mutability, _) if mutability == Mutability::Mut => { + ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _) => { *state = IncrementVisitorVarState::DontWarn; }, _ => (), @@ -226,7 +226,7 @@ impl<'a, 'tcx> Visitor<'tcx> for InitializeVisitor<'a, 'tcx> { InitializeVisitorState::DontWarn } }, - ExprKind::AddrOf(BorrowKind::Ref, mutability, _) if mutability == Mutability::Mut => { + ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _) => { self.state = InitializeVisitorState::DontWarn; }, _ => (), diff --git a/clippy_lints/src/matches/mod.rs b/clippy_lints/src/matches/mod.rs index d1061171e4d..6d16d188754 100644 --- a/clippy_lints/src/matches/mod.rs +++ b/clippy_lints/src/matches/mod.rs @@ -16,6 +16,7 @@ mod match_wild_enum; mod match_wild_err_arm; mod needless_match; mod overlapping_arms; +mod redundant_guards; mod redundant_pattern_match; mod rest_pat_in_fully_bound_struct; mod significant_drop_in_scrutinee; @@ -936,6 +937,36 @@ declare_clippy_lint! { "reimplementation of `filter`" } +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary guards in match expressions. + /// + /// ### Why is this bad? + /// It's more complex and much less readable. Making it part of the pattern can improve + /// exhaustiveness checking as well. + /// + /// ### Example + /// ```rust,ignore + /// match x { + /// Some(x) if matches!(x, Some(1)) => .., + /// Some(x) if x == Some(2) => .., + /// _ => todo!(), + /// } + /// ``` + /// Use instead: + /// ```rust,ignore + /// match x { + /// Some(Some(1)) => .., + /// Some(Some(2)) => .., + /// _ => todo!(), + /// } + /// ``` + #[clippy::version = "1.72.0"] + pub REDUNDANT_GUARDS, + complexity, + "checks for unnecessary guards in match expressions" +} + #[derive(Default)] pub struct Matches { msrv: Msrv, @@ -978,6 +1009,7 @@ impl_lint_pass!(Matches => [ TRY_ERR, MANUAL_MAP, MANUAL_FILTER, + REDUNDANT_GUARDS, ]); impl<'tcx> LateLintPass<'tcx> for Matches { @@ -1025,6 +1057,7 @@ impl<'tcx> LateLintPass<'tcx> for Matches { needless_match::check_match(cx, ex, arms, expr); match_on_vec_items::check(cx, ex); match_str_case_mismatch::check(cx, ex, arms); + redundant_guards::check(cx, arms); if !in_constant(cx, expr.hir_id) { manual_unwrap_or::check(cx, expr, ex, arms); diff --git a/clippy_lints/src/matches/redundant_guards.rs b/clippy_lints/src/matches/redundant_guards.rs new file mode 100644 index 00000000000..6383326aa38 --- /dev/null +++ b/clippy_lints/src/matches/redundant_guards.rs @@ -0,0 +1,190 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::path_to_local; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::visitors::{for_each_expr, is_local_used}; +use rustc_errors::Applicability; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{Arm, BinOpKind, Expr, ExprKind, Guard, MatchSource, Node, Pat, PatKind}; +use rustc_lint::LateContext; +use rustc_span::Span; +use std::ops::ControlFlow; + +use super::REDUNDANT_GUARDS; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'tcx>]) { + for outer_arm in arms { + let Some(guard) = outer_arm.guard else { + continue; + }; + + // `Some(x) if matches!(x, y)` + if let Guard::If(if_expr) = guard + && let ExprKind::Match( + scrutinee, + [ + arm, + Arm { + pat: Pat { + kind: PatKind::Wild, + .. + }, + .. + }, + ], + MatchSource::Normal, + ) = if_expr.kind + { + emit_redundant_guards( + cx, + outer_arm, + if_expr.span, + scrutinee, + arm.pat.span, + arm.guard, + ); + } + // `Some(x) if let Some(2) = x` + else if let Guard::IfLet(let_expr) = guard { + emit_redundant_guards( + cx, + outer_arm, + let_expr.span, + let_expr.init, + let_expr.pat.span, + None, + ); + } + // `Some(x) if x == Some(2)` + else if let Guard::If(if_expr) = guard + && let ExprKind::Binary(bin_op, local, pat) = if_expr.kind + && matches!(bin_op.node, BinOpKind::Eq) + && expr_can_be_pat(cx, pat) + // Ensure they have the same type. If they don't, we'd need deref coercion which isn't + // possible (currently) in a pattern. In some cases, you can use something like + // `as_deref` or similar but in general, we shouldn't lint this as it'd create an + // extraordinary amount of FPs. + // + // This isn't necessary in the other two checks, as they must be a pattern already. + && cx.typeck_results().expr_ty(local) == cx.typeck_results().expr_ty(pat) + { + emit_redundant_guards( + cx, + outer_arm, + if_expr.span, + local, + pat.span, + None, + ); + } + } +} + +fn get_pat_binding<'tcx>(cx: &LateContext<'tcx>, guard_expr: &Expr<'_>, outer_arm: &Arm<'tcx>) -> Option<(Span, bool)> { + if let Some(local) = path_to_local(guard_expr) && !is_local_used(cx, outer_arm.body, local) { + let mut span = None; + let mut multiple_bindings = false; + // `each_binding` gives the `HirId` of the `Pat` itself, not the binding + outer_arm.pat.walk(|pat| { + if let PatKind::Binding(_, hir_id, _, _) = pat.kind + && hir_id == local + && span.replace(pat.span).is_some() + { + multiple_bindings = true; + return false; + } + + true + }); + + // Ignore bindings from or patterns, like `First(x) | Second(x, _) | Third(x, _, _)` + if !multiple_bindings { + return span.map(|span| { + ( + span, + !matches!(cx.tcx.hir().get_parent(local), Node::PatField(_)), + ) + }); + } + } + + None +} + +fn emit_redundant_guards<'tcx>( + cx: &LateContext<'tcx>, + outer_arm: &Arm<'tcx>, + guard_span: Span, + local: &Expr<'_>, + pat_span: Span, + inner_guard: Option<Guard<'_>>, +) { + let mut app = Applicability::MaybeIncorrect; + let Some((pat_binding, can_use_shorthand)) = get_pat_binding(cx, local, outer_arm) else { + return; + }; + + span_lint_and_then( + cx, + REDUNDANT_GUARDS, + guard_span.source_callsite(), + "redundant guard", + |diag| { + let binding_replacement = snippet_with_applicability(cx, pat_span, "<binding_repl>", &mut app); + diag.multipart_suggestion_verbose( + "try", + vec![ + if can_use_shorthand { + (pat_binding, binding_replacement.into_owned()) + } else { + (pat_binding.shrink_to_hi(), format!(": {binding_replacement}")) + }, + ( + guard_span.source_callsite().with_lo(outer_arm.pat.span.hi()), + inner_guard.map_or_else(String::new, |guard| { + let (prefix, span) = match guard { + Guard::If(e) => ("if", e.span), + Guard::IfLet(l) => ("if let", l.span), + }; + + format!( + " {prefix} {}", + snippet_with_applicability(cx, span, "<guard>", &mut app), + ) + }), + ), + ], + app, + ); + }, + ); +} + +/// Checks if the given `Expr` can also be represented as a `Pat`. +fn expr_can_be_pat(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + for_each_expr(expr, |expr| { + if match expr.kind { + ExprKind::ConstBlock(..) => cx.tcx.features().inline_const_pat, + ExprKind::Call(c, ..) if let ExprKind::Path(qpath) = c.kind => { + // Allow ctors + matches!(cx.qpath_res(&qpath, c.hir_id), Res::Def(DefKind::Ctor(..), ..)) + }, + ExprKind::Path(qpath) => { + matches!( + cx.qpath_res(&qpath, expr.hir_id), + Res::Def(DefKind::Struct | DefKind::Enum | DefKind::Ctor(..), ..), + ) + }, + ExprKind::AddrOf(..) + | ExprKind::Array(..) + | ExprKind::Tup(..) + | ExprKind::Struct(..) + | ExprKind::Lit(..) => true, + _ => false, + } { + return ControlFlow::Continue(()); + } + + ControlFlow::Break(()) + }) + .is_none() +} diff --git a/clippy_lints/src/matches/redundant_pattern_match.rs b/clippy_lints/src/matches/redundant_pattern_match.rs index ad38f1394b4..9a7c00823b6 100644 --- a/clippy_lints/src/matches/redundant_pattern_match.rs +++ b/clippy_lints/src/matches/redundant_pattern_match.rs @@ -3,17 +3,19 @@ use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::source::{snippet, walk_span_to_context}; use clippy_utils::sugg::Sugg; use clippy_utils::ty::{is_type_diagnostic_item, needs_ordered_drop}; -use clippy_utils::visitors::any_temporaries_need_ordered_drop; +use clippy_utils::visitors::{any_temporaries_need_ordered_drop, for_each_expr}; use clippy_utils::{higher, is_expn_of, is_trait_method}; use if_chain::if_chain; use rustc_ast::ast::LitKind; use rustc_errors::Applicability; use rustc_hir::def::{DefKind, Res}; use rustc_hir::LangItem::{self, OptionNone, OptionSome, PollPending, PollReady, ResultErr, ResultOk}; -use rustc_hir::{Arm, Expr, ExprKind, Node, Pat, PatKind, QPath, UnOp}; +use rustc_hir::{Arm, Expr, ExprKind, Guard, Node, Pat, PatKind, QPath, UnOp}; use rustc_lint::LateContext; use rustc_middle::ty::{self, GenericArgKind, Ty}; use rustc_span::{sym, Symbol}; +use std::fmt::Write; +use std::ops::ControlFlow; pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { if let Some(higher::WhileLet { let_pat, let_expr, .. }) = higher::WhileLet::hir(expr) { @@ -201,30 +203,58 @@ pub(super) fn check_match<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, op if arms.len() == 2 { let node_pair = (&arms[0].pat.kind, &arms[1].pat.kind); - if let Some(good_method) = found_good_method(cx, arms, node_pair) { + if let Some((good_method, maybe_guard)) = found_good_method(cx, arms, node_pair) { let span = is_expn_of(expr.span, "matches").unwrap_or(expr.span.to(op.span)); let result_expr = match &op.kind { ExprKind::AddrOf(_, _, borrowed) => borrowed, _ => op, }; + let mut sugg = format!("{}.{good_method}", snippet(cx, result_expr.span, "_")); + + if let Some(guard) = maybe_guard { + let Guard::If(guard) = *guard else { return }; // `...is_none() && let ...` is a syntax error + + // wow, the HIR for match guards in `PAT if let PAT = expr && expr => ...` is annoying! + // `guard` here is `Guard::If` with the let expression somewhere deep in the tree of exprs, + // counter to the intuition that it should be `Guard::IfLet`, so we need another check + // to see that there aren't any let chains anywhere in the guard, as that would break + // if we suggest `t.is_none() && (let X = y && z)` for: + // `match t { None if let X = y && z => true, _ => false }` + let has_nested_let_chain = for_each_expr(guard, |expr| { + if matches!(expr.kind, ExprKind::Let(..)) { + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }) + .is_some(); + + if has_nested_let_chain { + return; + } + + let guard = Sugg::hir(cx, guard, ".."); + let _ = write!(sugg, " && {}", guard.maybe_par()); + } + span_lint_and_sugg( cx, REDUNDANT_PATTERN_MATCHING, span, &format!("redundant pattern matching, consider using `{good_method}`"), "try", - format!("{}.{good_method}", snippet(cx, result_expr.span, "_")), + sugg, Applicability::MachineApplicable, ); } } } -fn found_good_method<'a>( +fn found_good_method<'tcx>( cx: &LateContext<'_>, - arms: &[Arm<'_>], + arms: &'tcx [Arm<'tcx>], node: (&PatKind<'_>, &PatKind<'_>), -) -> Option<&'a str> { +) -> Option<(&'static str, Option<&'tcx Guard<'tcx>>)> { match node { ( PatKind::TupleStruct(ref path_left, patterns_left, _), @@ -310,7 +340,11 @@ fn get_ident(path: &QPath<'_>) -> Option<rustc_span::symbol::Ident> { } } -fn get_good_method<'a>(cx: &LateContext<'_>, arms: &[Arm<'_>], path_left: &QPath<'_>) -> Option<&'a str> { +fn get_good_method<'tcx>( + cx: &LateContext<'_>, + arms: &'tcx [Arm<'tcx>], + path_left: &QPath<'_>, +) -> Option<(&'static str, Option<&'tcx Guard<'tcx>>)> { if let Some(name) = get_ident(path_left) { return match name.as_str() { "Ok" => { @@ -376,16 +410,16 @@ fn is_pat_variant(cx: &LateContext<'_>, pat: &Pat<'_>, path: &QPath<'_>, expecte } #[expect(clippy::too_many_arguments)] -fn find_good_method_for_match<'a>( +fn find_good_method_for_match<'a, 'tcx>( cx: &LateContext<'_>, - arms: &[Arm<'_>], + arms: &'tcx [Arm<'tcx>], path_left: &QPath<'_>, path_right: &QPath<'_>, expected_item_left: Item, expected_item_right: Item, should_be_left: &'a str, should_be_right: &'a str, -) -> Option<&'a str> { +) -> Option<(&'a str, Option<&'tcx Guard<'tcx>>)> { let first_pat = arms[0].pat; let second_pat = arms[1].pat; @@ -403,22 +437,22 @@ fn find_good_method_for_match<'a>( match body_node_pair { (ExprKind::Lit(lit_left), ExprKind::Lit(lit_right)) => match (&lit_left.node, &lit_right.node) { - (LitKind::Bool(true), LitKind::Bool(false)) => Some(should_be_left), - (LitKind::Bool(false), LitKind::Bool(true)) => Some(should_be_right), + (LitKind::Bool(true), LitKind::Bool(false)) => Some((should_be_left, arms[0].guard.as_ref())), + (LitKind::Bool(false), LitKind::Bool(true)) => Some((should_be_right, arms[1].guard.as_ref())), _ => None, }, _ => None, } } -fn find_good_method_for_matches_macro<'a>( +fn find_good_method_for_matches_macro<'a, 'tcx>( cx: &LateContext<'_>, - arms: &[Arm<'_>], + arms: &'tcx [Arm<'tcx>], path_left: &QPath<'_>, expected_item_left: Item, should_be_left: &'a str, should_be_right: &'a str, -) -> Option<&'a str> { +) -> Option<(&'a str, Option<&'tcx Guard<'tcx>>)> { let first_pat = arms[0].pat; let body_node_pair = if is_pat_variant(cx, first_pat, path_left, expected_item_left) { @@ -429,8 +463,8 @@ fn find_good_method_for_matches_macro<'a>( match body_node_pair { (ExprKind::Lit(lit_left), ExprKind::Lit(lit_right)) => match (&lit_left.node, &lit_right.node) { - (LitKind::Bool(true), LitKind::Bool(false)) => Some(should_be_left), - (LitKind::Bool(false), LitKind::Bool(true)) => Some(should_be_right), + (LitKind::Bool(true), LitKind::Bool(false)) => Some((should_be_left, arms[0].guard.as_ref())), + (LitKind::Bool(false), LitKind::Bool(true)) => Some((should_be_right, arms[1].guard.as_ref())), _ => None, }, _ => None, diff --git a/clippy_lints/src/methods/filter_map.rs b/clippy_lints/src/methods/filter_map.rs index 597a423b537..c9eaa185acc 100644 --- a/clippy_lints/src/methods/filter_map.rs +++ b/clippy_lints/src/methods/filter_map.rs @@ -1,7 +1,9 @@ -use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::macros::{is_panic, root_macro_call}; use clippy_utils::source::{indent_of, reindent_multiline, snippet}; use clippy_utils::ty::is_type_diagnostic_item; -use clippy_utils::{is_trait_method, path_to_local_id, peel_blocks, SpanlessEq}; +use clippy_utils::{higher, is_trait_method, path_to_local_id, peel_blocks, SpanlessEq}; +use hir::{Body, HirId, MatchSource, Pat}; use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir as hir; @@ -10,7 +12,7 @@ use rustc_hir::{Closure, Expr, ExprKind, PatKind, PathSegment, QPath, UnOp}; use rustc_lint::LateContext; use rustc_middle::ty::adjustment::Adjust; use rustc_span::source_map::Span; -use rustc_span::symbol::{sym, Symbol}; +use rustc_span::symbol::{sym, Ident, Symbol}; use std::borrow::Cow; use super::{MANUAL_FILTER_MAP, MANUAL_FIND_MAP, OPTION_FILTER_MAP}; @@ -48,6 +50,214 @@ fn is_option_filter_map(cx: &LateContext<'_>, filter_arg: &hir::Expr<'_>, map_ar is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_some)) } +#[derive(Debug, Copy, Clone)] +enum OffendingFilterExpr<'tcx> { + /// `.filter(|opt| opt.is_some())` + IsSome { + /// The receiver expression + receiver: &'tcx Expr<'tcx>, + /// If `Some`, then this contains the span of an expression that possibly contains side + /// effects: `.filter(|opt| side_effect(opt).is_some())` + /// ^^^^^^^^^^^^^^^^ + /// + /// We will use this later for warning the user that the suggested fix may change + /// the behavior. + side_effect_expr_span: Option<Span>, + }, + /// `.filter(|res| res.is_ok())` + IsOk { + /// The receiver expression + receiver: &'tcx Expr<'tcx>, + /// See `IsSome` + side_effect_expr_span: Option<Span>, + }, + /// `.filter(|enum| matches!(enum, Enum::A(_)))` + Matches { + /// The DefId of the variant being matched + variant_def_id: hir::def_id::DefId, + }, +} + +#[derive(Debug)] +enum CalledMethod { + OptionIsSome, + ResultIsOk, +} + +/// The result of checking a `map` call, returned by `OffendingFilterExpr::check_map_call` +#[derive(Debug)] +enum CheckResult<'tcx> { + Method { + map_arg: &'tcx Expr<'tcx>, + /// The method that was called inside of `filter` + method: CalledMethod, + /// See `OffendingFilterExpr::IsSome` + side_effect_expr_span: Option<Span>, + }, + PatternMatching { + /// The span of the variant being matched + /// if let Some(s) = enum + /// ^^^^^^^ + variant_span: Span, + /// if let Some(s) = enum + /// ^ + variant_ident: Ident, + }, +} + +impl<'tcx> OffendingFilterExpr<'tcx> { + pub fn check_map_call( + &mut self, + cx: &LateContext<'tcx>, + map_body: &'tcx Body<'tcx>, + map_param_id: HirId, + filter_param_id: HirId, + is_filter_param_ref: bool, + ) -> Option<CheckResult<'tcx>> { + match *self { + OffendingFilterExpr::IsSome { + receiver, + side_effect_expr_span, + } + | OffendingFilterExpr::IsOk { + receiver, + side_effect_expr_span, + } => { + // check if closure ends with expect() or unwrap() + if let ExprKind::MethodCall(seg, map_arg, ..) = map_body.value.kind + && matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or) + // .map(|y| f(y).copied().unwrap()) + // ~~~~ + && let map_arg_peeled = match map_arg.kind { + ExprKind::MethodCall(method, original_arg, [], _) if acceptable_methods(method) => { + original_arg + }, + _ => map_arg, + } + // .map(|y| y[.acceptable_method()].unwrap()) + && let simple_equal = (path_to_local_id(receiver, filter_param_id) + && path_to_local_id(map_arg_peeled, map_param_id)) + && let eq_fallback = (|a: &Expr<'_>, b: &Expr<'_>| { + // in `filter(|x| ..)`, replace `*x` with `x` + let a_path = if_chain! { + if !is_filter_param_ref; + if let ExprKind::Unary(UnOp::Deref, expr_path) = a.kind; + then { expr_path } else { a } + }; + // let the filter closure arg and the map closure arg be equal + path_to_local_id(a_path, filter_param_id) + && path_to_local_id(b, map_param_id) + && cx.typeck_results().expr_ty_adjusted(a) == cx.typeck_results().expr_ty_adjusted(b) + }) + && (simple_equal + || SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(receiver, map_arg_peeled)) + { + Some(CheckResult::Method { + map_arg, + side_effect_expr_span, + method: match self { + OffendingFilterExpr::IsSome { .. } => CalledMethod::OptionIsSome, + OffendingFilterExpr::IsOk { .. } => CalledMethod::ResultIsOk, + OffendingFilterExpr::Matches { .. } => unreachable!("only IsSome and IsOk can get here"), + } + }) + } else { + None + } + }, + OffendingFilterExpr::Matches { variant_def_id } => { + let expr_uses_local = |pat: &Pat<'_>, expr: &Expr<'_>| { + if let PatKind::TupleStruct(QPath::Resolved(_, path), [subpat], _) = pat.kind + && let PatKind::Binding(_, local_id, ident, _) = subpat.kind + && path_to_local_id(expr.peel_blocks(), local_id) + && let Some(local_variant_def_id) = path.res.opt_def_id() + && local_variant_def_id == variant_def_id + { + Some((ident, pat.span)) + } else { + None + } + }; + + // look for: + // `if let Variant (v) = enum { v } else { unreachable!() }` + // ^^^^^^^ ^ ^^^^ ^^^^^^^^^^^^^^^^^^ + // variant_span variant_ident scrutinee else_ (blocks peeled later) + // OR + // `match enum { Variant (v) => v, _ => unreachable!() }` + // ^^^^ ^^^^^^^ ^ ^^^^^^^^^^^^^^ + // scrutinee variant_span variant_ident else_ + let (scrutinee, else_, variant_ident, variant_span) = + match higher::IfLetOrMatch::parse(cx, map_body.value) { + // For `if let` we want to check that the variant matching arm references the local created by its pattern + Some(higher::IfLetOrMatch::IfLet(sc, pat, then, Some(else_))) + if let Some((ident, span)) = expr_uses_local(pat, then) => + { + (sc, else_, ident, span) + }, + // For `match` we want to check that the "else" arm is the wildcard (`_`) pattern + // and that the variant matching arm references the local created by its pattern + Some(higher::IfLetOrMatch::Match(sc, [arm, wild_arm], MatchSource::Normal)) + if let PatKind::Wild = wild_arm.pat.kind + && let Some((ident, span)) = expr_uses_local(arm.pat, arm.body.peel_blocks()) => + { + (sc, wild_arm.body, ident, span) + }, + _ => return None, + }; + + if path_to_local_id(scrutinee, map_param_id) + // else branch should be a `panic!` or `unreachable!` macro call + && let Some(mac) = root_macro_call(else_.peel_blocks().span) + && (is_panic(cx, mac.def_id) || cx.tcx.opt_item_name(mac.def_id) == Some(sym::unreachable)) + { + Some(CheckResult::PatternMatching { variant_span, variant_ident }) + } else { + None + } + }, + } + } + + fn hir(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, filter_param_id: HirId) -> Option<Self> { + if let ExprKind::MethodCall(path, receiver, [], _) = expr.kind + && let Some(recv_ty) = cx.typeck_results().expr_ty(receiver).peel_refs().ty_adt_def() + { + // we still want to lint if the expression possibly contains side effects, + // *but* it can't be machine-applicable then, because that can change the behavior of the program: + // .filter(|x| effect(x).is_some()).map(|x| effect(x).unwrap()) + // vs. + // .filter_map(|x| effect(x)) + // + // the latter only calls `effect` once + let side_effect_expr_span = receiver.can_have_side_effects().then_some(receiver.span); + + if cx.tcx.is_diagnostic_item(sym::Option, recv_ty.did()) + && path.ident.name == sym!(is_some) + { + Some(Self::IsSome { receiver, side_effect_expr_span }) + } else if cx.tcx.is_diagnostic_item(sym::Result, recv_ty.did()) + && path.ident.name == sym!(is_ok) + { + Some(Self::IsOk { receiver, side_effect_expr_span }) + } else { + None + } + } else if let Some(macro_call) = root_macro_call(expr.span) + && cx.tcx.get_diagnostic_name(macro_call.def_id) == Some(sym::matches_macro) + // we know for a fact that the wildcard pattern is the second arm + && let ExprKind::Match(scrutinee, [arm, _], _) = expr.kind + && path_to_local_id(scrutinee, filter_param_id) + && let PatKind::TupleStruct(QPath::Resolved(_, path), ..) = arm.pat.kind + && let Some(variant_def_id) = path.res.opt_def_id() + { + Some(OffendingFilterExpr::Matches { variant_def_id }) + } else { + None + } + } +} + /// is `filter(|x| x.is_some()).map(|x| x.unwrap())` fn is_filter_some_map_unwrap( cx: &LateContext<'_>, @@ -102,55 +312,18 @@ pub(super) fn check( } else { (filter_param.pat, false) }; - // closure ends with is_some() or is_ok() + if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind; - if let ExprKind::MethodCall(path, filter_arg, [], _) = filter_body.value.kind; - if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).peel_refs().ty_adt_def(); - if let Some(is_result) = if cx.tcx.is_diagnostic_item(sym::Option, opt_ty.did()) { - Some(false) - } else if cx.tcx.is_diagnostic_item(sym::Result, opt_ty.did()) { - Some(true) - } else { - None - }; - if path.ident.name.as_str() == if is_result { "is_ok" } else { "is_some" }; + if let Some(mut offending_expr) = OffendingFilterExpr::hir(cx, filter_body.value, filter_param_id); - // ...map(|x| ...unwrap()) if let ExprKind::Closure(&Closure { body: map_body_id, .. }) = map_arg.kind; let map_body = cx.tcx.hir().body(map_body_id); if let [map_param] = map_body.params; if let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind; - // closure ends with expect() or unwrap() - if let ExprKind::MethodCall(seg, map_arg, ..) = map_body.value.kind; - if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or); - - // .filter(..).map(|y| f(y).copied().unwrap()) - // ~~~~ - let map_arg_peeled = match map_arg.kind { - ExprKind::MethodCall(method, original_arg, [], _) if acceptable_methods(method) => { - original_arg - }, - _ => map_arg, - }; - // .filter(|x| x.is_some()).map(|y| y[.acceptable_method()].unwrap()) - let simple_equal = path_to_local_id(filter_arg, filter_param_id) - && path_to_local_id(map_arg_peeled, map_param_id); + if let Some(check_result) = + offending_expr.check_map_call(cx, map_body, map_param_id, filter_param_id, is_filter_param_ref); - let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| { - // in `filter(|x| ..)`, replace `*x` with `x` - let a_path = if_chain! { - if !is_filter_param_ref; - if let ExprKind::Unary(UnOp::Deref, expr_path) = a.kind; - then { expr_path } else { a } - }; - // let the filter closure arg and the map closure arg be equal - path_to_local_id(a_path, filter_param_id) - && path_to_local_id(b, map_param_id) - && cx.typeck_results().expr_ty_adjusted(a) == cx.typeck_results().expr_ty_adjusted(b) - }; - - if simple_equal || SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg_peeled); then { let span = filter_span.with_hi(expr.span.hi()); let (filter_name, lint) = if is_find { @@ -159,22 +332,53 @@ pub(super) fn check( ("filter", MANUAL_FILTER_MAP) }; let msg = format!("`{filter_name}(..).map(..)` can be simplified as `{filter_name}_map(..)`"); - let (to_opt, deref) = if is_result { - (".ok()", String::new()) - } else { - let derefs = cx.typeck_results() - .expr_adjustments(map_arg) - .iter() - .filter(|adj| matches!(adj.kind, Adjust::Deref(_))) - .count(); - ("", "*".repeat(derefs)) + let (sugg, note_and_span, applicability) = match check_result { + CheckResult::Method { map_arg, method, side_effect_expr_span } => { + let (to_opt, deref) = match method { + CalledMethod::ResultIsOk => (".ok()", String::new()), + CalledMethod::OptionIsSome => { + let derefs = cx.typeck_results() + .expr_adjustments(map_arg) + .iter() + .filter(|adj| matches!(adj.kind, Adjust::Deref(_))) + .count(); + + ("", "*".repeat(derefs)) + } + }; + + let sugg = format!( + "{filter_name}_map(|{map_param_ident}| {deref}{}{to_opt})", + snippet(cx, map_arg.span, ".."), + ); + let (note_and_span, applicability) = if let Some(span) = side_effect_expr_span { + let note = "the suggestion might change the behavior of the program when merging `filter` and `map`, \ + because this expression potentially contains side effects and will only execute once"; + + (Some((note, span)), Applicability::MaybeIncorrect) + } else { + (None, Applicability::MachineApplicable) + }; + + (sugg, note_and_span, applicability) + } + CheckResult::PatternMatching { variant_span, variant_ident } => { + let pat = snippet(cx, variant_span, "<pattern>"); + + (format!("{filter_name}_map(|{map_param_ident}| match {map_param_ident} {{ \ + {pat} => Some({variant_ident}), \ + _ => None \ + }})"), None, Applicability::MachineApplicable) + } }; - let sugg = format!( - "{filter_name}_map(|{map_param_ident}| {deref}{}{to_opt})", - snippet(cx, map_arg.span, ".."), - ); - span_lint_and_sugg(cx, lint, span, &msg, "try", sugg, Applicability::MachineApplicable); + span_lint_and_then(cx, lint, span, &msg, |diag| { + diag.span_suggestion(span, "try", sugg, applicability); + + if let Some((note, span)) = note_and_span { + diag.span_note(span, note); + } + }); } } } diff --git a/clippy_lints/src/methods/filter_map_bool_then.rs b/clippy_lints/src/methods/filter_map_bool_then.rs new file mode 100644 index 00000000000..4aee22a4afc --- /dev/null +++ b/clippy_lints/src/methods/filter_map_bool_then.rs @@ -0,0 +1,45 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::paths::BOOL_THEN; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::is_copy; +use clippy_utils::{is_from_proc_macro, is_trait_method, match_def_path, peel_blocks}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_span::{sym, Span}; + +use super::FILTER_MAP_BOOL_THEN; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, arg: &Expr<'_>, call_span: Span) { + if !in_external_macro(cx.sess(), expr.span) + && is_trait_method(cx, expr, sym::Iterator) + && let ExprKind::Closure(closure) = arg.kind + && let body = cx.tcx.hir().body(closure.body) + && let value = peel_blocks(body.value) + // Indexing should be fine as `filter_map` always has 1 input, we unfortunately need both + // `inputs` and `params` here as we need both the type and the span + && let param_ty = closure.fn_decl.inputs[0] + && let param = body.params[0] + && is_copy(cx, cx.typeck_results().node_type(param_ty.hir_id).peel_refs()) + && let ExprKind::MethodCall(_, recv, [then_arg], _) = value.kind + && let ExprKind::Closure(then_closure) = then_arg.kind + && let then_body = peel_blocks(cx.tcx.hir().body(then_closure.body).value) + && let Some(def_id) = cx.typeck_results().type_dependent_def_id(value.hir_id) + && match_def_path(cx, def_id, &BOOL_THEN) + && !is_from_proc_macro(cx, expr) + && let Some(param_snippet) = snippet_opt(cx, param.span) + && let Some(filter) = snippet_opt(cx, recv.span) + && let Some(map) = snippet_opt(cx, then_body.span) + { + span_lint_and_sugg( + cx, + FILTER_MAP_BOOL_THEN, + call_span, + "usage of `bool::then` in `filter_map`", + "use `filter` then `map` instead", + format!("filter(|&{param_snippet}| {filter}).map(|{param_snippet}| {map})"), + Applicability::MachineApplicable, + ); + } +} diff --git a/clippy_lints/src/methods/format_collect.rs b/clippy_lints/src/methods/format_collect.rs new file mode 100644 index 00000000000..1f8863f8521 --- /dev/null +++ b/clippy_lints/src/methods/format_collect.rs @@ -0,0 +1,33 @@ +use super::FORMAT_COLLECT; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::macros::{is_format_macro, root_macro_call_first_node}; +use clippy_utils::ty::is_type_lang_item; +use rustc_hir::{Expr, ExprKind, LangItem}; +use rustc_lint::LateContext; +use rustc_span::Span; + +/// Same as `peel_blocks` but only actually considers blocks that are not from an expansion. +/// This is needed because always calling `peel_blocks` would otherwise remove parts of the +/// `format!` macro, which would cause `root_macro_call_first_node` to return `None`. +fn peel_non_expn_blocks<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { + match expr.kind { + ExprKind::Block(block, _) if !expr.span.from_expansion() => peel_non_expn_blocks(block.expr?), + _ => Some(expr), + } +} + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, map_arg: &Expr<'_>, map_span: Span) { + if is_type_lang_item(cx, cx.typeck_results().expr_ty(expr), LangItem::String) + && let ExprKind::Closure(closure) = map_arg.kind + && let body = cx.tcx.hir().body(closure.body) + && let Some(value) = peel_non_expn_blocks(body.value) + && let Some(mac) = root_macro_call_first_node(cx, value) + && is_format_macro(cx, mac.def_id) + { + span_lint_and_then(cx, FORMAT_COLLECT, expr.span, "use of `format!` to build up a string from an iterator", |diag| { + diag.span_help(map_span, "call `fold` instead") + .span_help(value.span.source_callsite(), "... and use the `write!` macro here") + .note("this can be written more efficiently by appending to a `String` directly"); + }); + } +} diff --git a/clippy_lints/src/methods/iter_skip_zero.rs b/clippy_lints/src/methods/iter_skip_zero.rs new file mode 100644 index 00000000000..6b696b42a69 --- /dev/null +++ b/clippy_lints/src/methods/iter_skip_zero.rs @@ -0,0 +1,34 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{is_from_proc_macro, is_trait_method}; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::ITER_SKIP_ZERO; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, arg_expr: &Expr<'_>) { + if !expr.span.from_expansion() + && is_trait_method(cx, expr, sym::Iterator) + && let Some(arg) = constant(cx, cx.typeck_results(), arg_expr).and_then(|constant| { + if let Constant::Int(arg) = constant { + Some(arg) + } else { + None + } + }) + && arg == 0 + && !is_from_proc_macro(cx, expr) + { + span_lint_and_then(cx, ITER_SKIP_ZERO, arg_expr.span, "usage of `.skip(0)`", |diag| { + diag.span_suggestion( + arg_expr.span, + "if you meant to skip the first element, use", + "1", + Applicability::MaybeIncorrect, + ) + .note("this call to `skip` does nothing and is useless; remove it"); + }); + } +} diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs index 123ab520e48..dd694ce7393 100644 --- a/clippy_lints/src/methods/mod.rs +++ b/clippy_lints/src/methods/mod.rs @@ -21,11 +21,13 @@ mod expect_used; mod extend_with_drain; mod filetype_is_file; mod filter_map; +mod filter_map_bool_then; mod filter_map_identity; mod filter_map_next; mod filter_next; mod flat_map_identity; mod flat_map_option; +mod format_collect; mod from_iter_instead_of_collect; mod get_first; mod get_last_with_len; @@ -44,6 +46,7 @@ mod iter_nth_zero; mod iter_on_single_or_empty_collections; mod iter_overeager_cloned; mod iter_skip_next; +mod iter_skip_zero; mod iter_with_drain; mod iterator_step_by_zero; mod manual_next_back; @@ -73,6 +76,7 @@ mod or_then_unwrap; mod path_buf_push_overwrite; mod range_zip_with_len; mod read_line_without_trim; +mod readonly_write_lock; mod repeat_once; mod search_is_some; mod seek_from_current; @@ -85,6 +89,7 @@ mod skip_while_next; mod stable_sort_primitive; mod str_splitn; mod string_extend_chars; +mod string_lit_chars_any; mod suspicious_command_arg_space; mod suspicious_map; mod suspicious_splitn; @@ -100,7 +105,6 @@ mod unnecessary_lazy_eval; mod unnecessary_literal_unwrap; mod unnecessary_sort_by; mod unnecessary_to_owned; -mod unwrap_or_else_default; mod unwrap_used; mod useless_asref; mod utils; @@ -114,7 +118,7 @@ use clippy_utils::consts::{constant, Constant}; use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::ty::{contains_ty_adt_constructor_opaque, implements_trait, is_copy, is_type_diagnostic_item}; -use clippy_utils::{contains_return, is_bool, is_trait_method, iter_input_pats, return_ty}; +use clippy_utils::{contains_return, is_bool, is_trait_method, iter_input_pats, peel_blocks, return_ty}; use if_chain::if_chain; use rustc_hir as hir; use rustc_hir::{Expr, ExprKind, Node, Stmt, StmtKind, TraitItem, TraitItemKind}; @@ -473,29 +477,40 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does - /// Checks for usage of `_.unwrap_or_else(Default::default)` on `Option` and - /// `Result` values. + /// Checks for usages of the following functions with an argument that constructs a default value + /// (e.g., `Default::default` or `String::new`): + /// - `unwrap_or` + /// - `unwrap_or_else` + /// - `or_insert` + /// - `or_insert_with` /// /// ### Why is this bad? - /// Readability, these can be written as `_.unwrap_or_default`, which is - /// simpler and more concise. + /// Readability. Using `unwrap_or_default` in place of `unwrap_or`/`unwrap_or_else`, or `or_default` + /// in place of `or_insert`/`or_insert_with`, is simpler and more concise. + /// + /// ### Known problems + /// In some cases, the argument of `unwrap_or`, etc. is needed for type inference. The lint uses a + /// heuristic to try to identify such cases. However, the heuristic can produce false negatives. /// /// ### Examples /// ```rust /// # let x = Some(1); - /// x.unwrap_or_else(Default::default); - /// x.unwrap_or_else(u32::default); + /// # let mut map = std::collections::HashMap::<u64, String>::new(); + /// x.unwrap_or(Default::default()); + /// map.entry(42).or_insert_with(String::new); /// ``` /// /// Use instead: /// ```rust /// # let x = Some(1); + /// # let mut map = std::collections::HashMap::<u64, String>::new(); /// x.unwrap_or_default(); + /// map.entry(42).or_default(); /// ``` #[clippy::version = "1.56.0"] - pub UNWRAP_OR_ELSE_DEFAULT, + pub UNWRAP_OR_DEFAULT, style, - "using `.unwrap_or_else(Default::default)`, which is more succinctly expressed as `.unwrap_or_default()`" + "using `.unwrap_or`, etc. with an argument that constructs a default value" } declare_clippy_lint! { @@ -3378,6 +3393,152 @@ declare_clippy_lint! { "calling `Stdin::read_line`, then trying to parse it without first trimming" } +declare_clippy_lint! { + /// ### What it does + /// Checks for `<string_lit>.chars().any(|i| i == c)`. + /// + /// ### Why is this bad? + /// It's significantly slower than using a pattern instead, like + /// `matches!(c, '\\' | '.' | '+')`. + /// + /// Despite this being faster, this is not `perf` as this is pretty common, and is a rather nice + /// way to check if a `char` is any in a set. In any case, this `restriction` lint is available + /// for situations where that additional performance is absolutely necessary. + /// + /// ### Example + /// ```rust + /// # let c = 'c'; + /// "\\.+*?()|[]{}^$#&-~".chars().any(|x| x == c); + /// ``` + /// Use instead: + /// ```rust + /// # let c = 'c'; + /// matches!(c, '\\' | '.' | '+' | '*' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$' | '#' | '&' | '-' | '~'); + /// ``` + #[clippy::version = "1.72.0"] + pub STRING_LIT_CHARS_ANY, + restriction, + "checks for `<string_lit>.chars().any(|i| i == c)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.map(|_| format!(..)).collect::<String>()`. + /// + /// ### Why is this bad? + /// This allocates a new string for every element in the iterator. + /// This can be done more efficiently by creating the `String` once and appending to it in `Iterator::fold`, + /// using either the `write!` macro which supports exactly the same syntax as the `format!` macro, + /// or concatenating with `+` in case the iterator yields `&str`/`String`. + /// + /// Note also that `write!`-ing into a `String` can never fail, despite the return type of `write!` being `std::fmt::Result`, + /// so it can be safely ignored or unwrapped. + /// + /// ### Example + /// ```rust + /// fn hex_encode(bytes: &[u8]) -> String { + /// bytes.iter().map(|b| format!("{b:02X}")).collect() + /// } + /// ``` + /// Use instead: + /// ```rust + /// use std::fmt::Write; + /// fn hex_encode(bytes: &[u8]) -> String { + /// bytes.iter().fold(String::new(), |mut output, b| { + /// let _ = write!(output, "{b:02X}"); + /// output + /// }) + /// } + /// ``` + #[clippy::version = "1.72.0"] + pub FORMAT_COLLECT, + perf, + "`format!`ing every element in a collection, then collecting the strings into a new `String`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.skip(0)` on iterators. + /// + /// ### Why is this bad? + /// This was likely intended to be `.skip(1)` to skip the first element, as `.skip(0)` does + /// nothing. If not, the call should be removed. + /// + /// ### Example + /// ```rust + /// let v = vec![1, 2, 3]; + /// let x = v.iter().skip(0).collect::<Vec<_>>(); + /// let y = v.iter().collect::<Vec<_>>(); + /// assert_eq!(x, y); + /// ``` + #[clippy::version = "1.72.0"] + pub ITER_SKIP_ZERO, + correctness, + "disallows `.skip(0)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `bool::then` in `Iterator::filter_map`. + /// + /// ### Why is this bad? + /// This can be written with `filter` then `map` instead, which would reduce nesting and + /// separates the filtering from the transformation phase. This comes with no cost to + /// performance and is just cleaner. + /// + /// ### Limitations + /// Does not lint `bool::then_some`, as it eagerly evaluates its arguments rather than lazily. + /// This can create differing behavior, so better safe than sorry. + /// + /// ### Example + /// ```rust + /// # fn really_expensive_fn(i: i32) -> i32 { i } + /// # let v = vec![]; + /// _ = v.into_iter().filter_map(|i| (i % 2 == 0).then(|| really_expensive_fn(i))); + /// ``` + /// Use instead: + /// ```rust + /// # fn really_expensive_fn(i: i32) -> i32 { i } + /// # let v = vec![]; + /// _ = v.into_iter().filter(|i| i % 2 == 0).map(|i| really_expensive_fn(i)); + /// ``` + #[clippy::version = "1.72.0"] + pub FILTER_MAP_BOOL_THEN, + style, + "checks for usage of `bool::then` in `Iterator::filter_map`" +} + +declare_clippy_lint! { + /// ### What it does + /// Looks for calls to `RwLock::write` where the lock is only used for reading. + /// + /// ### Why is this bad? + /// The write portion of `RwLock` is exclusive, meaning that no other thread + /// can access the lock while this writer is active. + /// + /// ### Example + /// ```rust + /// use std::sync::RwLock; + /// fn assert_is_zero(lock: &RwLock<i32>) { + /// let num = lock.write().unwrap(); + /// assert_eq!(*num, 0); + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// use std::sync::RwLock; + /// fn assert_is_zero(lock: &RwLock<i32>) { + /// let num = lock.read().unwrap(); + /// assert_eq!(*num, 0); + /// } + /// ``` + #[clippy::version = "1.73.0"] + pub READONLY_WRITE_LOCK, + nursery, + "acquiring a write lock when a read lock would work" +} + pub struct Methods { avoid_breaking_exported_api: bool, msrv: Msrv, @@ -3408,7 +3569,7 @@ impl_lint_pass!(Methods => [ SHOULD_IMPLEMENT_TRAIT, WRONG_SELF_CONVENTION, OK_EXPECT, - UNWRAP_OR_ELSE_DEFAULT, + UNWRAP_OR_DEFAULT, MAP_UNWRAP_OR, RESULT_MAP_OR_INTO_OPTION, OPTION_MAP_OR_NONE, @@ -3512,6 +3673,11 @@ impl_lint_pass!(Methods => [ UNNECESSARY_LITERAL_UNWRAP, DRAIN_COLLECT, MANUAL_TRY_FOLD, + FORMAT_COLLECT, + STRING_LIT_CHARS_ANY, + ITER_SKIP_ZERO, + FILTER_MAP_BOOL_THEN, + READONLY_WRITE_LOCK ]); /// Extracts a method call name, args, and `Span` of the method name. @@ -3666,8 +3832,7 @@ impl<'tcx> LateLintPass<'tcx> for Methods { then { let first_arg_span = first_arg_ty.span; let first_arg_ty = hir_ty_to_ty(cx.tcx, first_arg_ty); - let self_ty = TraitRef::identity(cx.tcx, item.owner_id.to_def_id()) - .self_ty(); + let self_ty = TraitRef::identity(cx.tcx, item.owner_id.to_def_id()).self_ty(); wrong_self_convention::check( cx, item.ident.name.as_str(), @@ -3684,8 +3849,7 @@ impl<'tcx> LateLintPass<'tcx> for Methods { if item.ident.name == sym::new; if let TraitItemKind::Fn(_, _) = item.kind; let ret_ty = return_ty(cx, item.owner_id); - let self_ty = TraitRef::identity(cx.tcx, item.owner_id.to_def_id()) - .self_ty(); + let self_ty = TraitRef::identity(cx.tcx, item.owner_id.to_def_id()).self_ty(); if !ret_ty.contains(self_ty); then { @@ -3733,8 +3897,9 @@ impl Methods { Some((name @ ("cloned" | "copied"), recv2, [], _, _)) => { iter_cloned_collect::check(cx, name, expr, recv2); }, - Some(("map", m_recv, [m_arg], _, _)) => { + Some(("map", m_recv, [m_arg], m_ident_span, _)) => { map_collect_result_unit::check(cx, expr, m_recv, m_arg); + format_collect::check(cx, expr, m_arg, m_ident_span); }, Some(("take", take_self_arg, [take_arg], _, _)) => { if self.msrv.meets(msrvs::STR_REPEAT) { @@ -3790,6 +3955,7 @@ impl Methods { }, ("filter_map", [arg]) => { unnecessary_filter_map::check(cx, expr, arg, name); + filter_map_bool_then::check(cx, expr, arg, call_span); filter_map_identity::check(cx, expr, arg, span); }, ("find_map", [arg]) => { @@ -3833,7 +3999,16 @@ impl Methods { unnecessary_join::check(cx, expr, recv, join_arg, span); } }, - ("last", []) | ("skip", [_]) => { + ("skip", [arg]) => { + iter_skip_zero::check(cx, expr, arg); + + if let Some((name2, recv2, args2, _span2, _)) = method_call(recv) { + if let ("cloned", []) = (name2, args2) { + iter_overeager_cloned::check(cx, expr, recv, recv2, false, false); + } + } + } + ("last", []) => { if let Some((name2, recv2, args2, _span2, _)) = method_call(recv) { if let ("cloned", []) = (name2, args2) { iter_overeager_cloned::check(cx, expr, recv, recv2, false, false); @@ -3885,6 +4060,13 @@ impl Methods { } } }, + ("any", [arg]) if let ExprKind::Closure(arg) = arg.kind + && let body = cx.tcx.hir().body(arg.body) + && let [param] = body.params + && let Some(("chars", recv, _, _, _)) = method_call(recv) => + { + string_lit_chars_any::check(cx, expr, recv, param, peel_blocks(body.value), &self.msrv); + } ("nth", [n_arg]) => match method_call(recv) { Some(("bytes", recv2, [], _, _)) => bytes_nth::check(cx, expr, recv2, n_arg), Some(("cloned", recv2, [], _, _)) => iter_overeager_cloned::check(cx, expr, recv, recv2, false, false), @@ -4027,7 +4209,6 @@ impl Methods { Some(("map", recv, [map_arg], _, _)) if map_unwrap_or::check(cx, expr, recv, map_arg, u_arg, &self.msrv) => {}, _ => { - unwrap_or_else_default::check(cx, expr, recv, u_arg); unnecessary_lazy_eval::check(cx, expr, recv, u_arg, "unwrap_or"); }, } @@ -4040,6 +4221,9 @@ impl Methods { range_zip_with_len::check(cx, expr, iter_recv, arg); } }, + ("write", []) => { + readonly_write_lock::check(cx, expr, recv); + } _ => {}, } } diff --git a/clippy_lints/src/methods/or_fun_call.rs b/clippy_lints/src/methods/or_fun_call.rs index 9165c1248f0..8b2f57160af 100644 --- a/clippy_lints/src/methods/or_fun_call.rs +++ b/clippy_lints/src/methods/or_fun_call.rs @@ -1,16 +1,17 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::eager_or_lazy::switch_to_lazy_eval; use clippy_utils::source::snippet_with_context; -use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; -use clippy_utils::{contains_return, is_trait_item, last_path_segment}; +use clippy_utils::ty::{expr_type_is_certain, implements_trait, is_type_diagnostic_item}; +use clippy_utils::{contains_return, is_default_equivalent, is_default_equivalent_call, last_path_segment}; use if_chain::if_chain; use rustc_errors::Applicability; -use rustc_hir as hir; use rustc_lint::LateContext; +use rustc_middle::ty; use rustc_span::source_map::Span; -use rustc_span::symbol::{kw, sym, Symbol}; +use rustc_span::symbol::{self, sym, Symbol}; +use {rustc_ast as ast, rustc_hir as hir}; -use super::OR_FUN_CALL; +use super::{OR_FUN_CALL, UNWRAP_OR_DEFAULT}; /// Checks for the `OR_FUN_CALL` lint. #[allow(clippy::too_many_lines)] @@ -24,53 +25,72 @@ pub(super) fn check<'tcx>( ) { /// Checks for `unwrap_or(T::new())`, `unwrap_or(T::default())`, /// `or_insert(T::new())` or `or_insert(T::default())`. + /// Similarly checks for `unwrap_or_else(T::new)`, `unwrap_or_else(T::default)`, + /// `or_insert_with(T::new)` or `or_insert_with(T::default)`. #[allow(clippy::too_many_arguments)] fn check_unwrap_or_default( cx: &LateContext<'_>, name: &str, + receiver: &hir::Expr<'_>, fun: &hir::Expr<'_>, - arg: &hir::Expr<'_>, - or_has_args: bool, + call_expr: Option<&hir::Expr<'_>>, span: Span, method_span: Span, ) -> bool { - let is_default_default = || is_trait_item(cx, fun, sym::Default); + if !expr_type_is_certain(cx, receiver) { + return false; + } - let implements_default = |arg, default_trait_id| { - let arg_ty = cx.typeck_results().expr_ty(arg); - implements_trait(cx, arg_ty, default_trait_id, &[]) + let is_new = |fun: &hir::Expr<'_>| { + if let hir::ExprKind::Path(ref qpath) = fun.kind { + let path = last_path_segment(qpath).ident.name; + matches!(path, sym::new) + } else { + false + } }; - if_chain! { - if !or_has_args; - if let Some(sugg) = match name { - "unwrap_or" => Some("unwrap_or_default"), - "or_insert" => Some("or_default"), - _ => None, - }; - if let hir::ExprKind::Path(ref qpath) = fun.kind; - if let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default); - let path = last_path_segment(qpath).ident.name; - // needs to target Default::default in particular or be *::new and have a Default impl - // available - if (matches!(path, kw::Default) && is_default_default()) - || (matches!(path, sym::new) && implements_default(arg, default_trait_id)); - - then { - span_lint_and_sugg( - cx, - OR_FUN_CALL, - method_span.with_hi(span.hi()), - &format!("use of `{name}` followed by a call to `{path}`"), - "try", - format!("{sugg}()"), - Applicability::MachineApplicable, - ); - - true + let output_type_implements_default = |fun| { + let fun_ty = cx.typeck_results().expr_ty(fun); + if let ty::FnDef(def_id, args) = fun_ty.kind() { + let output_ty = cx.tcx.fn_sig(def_id).instantiate(cx.tcx, args).skip_binder().output(); + cx.tcx + .get_diagnostic_item(sym::Default) + .map_or(false, |default_trait_id| { + implements_trait(cx, output_ty, default_trait_id, &[]) + }) } else { false } + }; + + let sugg = match (name, call_expr.is_some()) { + ("unwrap_or", true) | ("unwrap_or_else", false) => "unwrap_or_default", + ("or_insert", true) | ("or_insert_with", false) => "or_default", + _ => return false, + }; + + // needs to target Default::default in particular or be *::new and have a Default impl + // available + if (is_new(fun) && output_type_implements_default(fun)) + || match call_expr { + Some(call_expr) => is_default_equivalent(cx, call_expr), + None => is_default_equivalent_call(cx, fun) || closure_body_returns_empty_to_string(cx, fun), + } + { + span_lint_and_sugg( + cx, + UNWRAP_OR_DEFAULT, + method_span.with_hi(span.hi()), + &format!("use of `{name}` to construct default value"), + "try", + format!("{sugg}()"), + Applicability::MachineApplicable, + ); + + true + } else { + false } } @@ -168,11 +188,16 @@ pub(super) fn check<'tcx>( match inner_arg.kind { hir::ExprKind::Call(fun, or_args) => { let or_has_args = !or_args.is_empty(); - if !check_unwrap_or_default(cx, name, fun, arg, or_has_args, expr.span, method_span) { + if or_has_args + || !check_unwrap_or_default(cx, name, receiver, fun, Some(inner_arg), expr.span, method_span) + { let fun_span = if or_has_args { None } else { Some(fun.span) }; check_general_case(cx, name, method_span, receiver, arg, None, expr.span, fun_span); } }, + hir::ExprKind::Path(..) | hir::ExprKind::Closure(..) => { + check_unwrap_or_default(cx, name, receiver, inner_arg, None, expr.span, method_span); + }, hir::ExprKind::Index(..) | hir::ExprKind::MethodCall(..) => { check_general_case(cx, name, method_span, receiver, arg, None, expr.span, None); }, @@ -189,3 +214,22 @@ pub(super) fn check<'tcx>( } } } + +fn closure_body_returns_empty_to_string(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> bool { + if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = e.kind { + let body = cx.tcx.hir().body(body); + + if body.params.is_empty() + && let hir::Expr{ kind, .. } = &body.value + && let hir::ExprKind::MethodCall(hir::PathSegment {ident, ..}, self_arg, _, _) = kind + && ident.name == sym::to_string + && let hir::Expr{ kind, .. } = self_arg + && let hir::ExprKind::Lit(lit) = kind + && let ast::LitKind::Str(symbol::kw::Empty, _) = lit.node + { + return true; + } + } + + false +} diff --git a/clippy_lints/src/methods/read_line_without_trim.rs b/clippy_lints/src/methods/read_line_without_trim.rs index 8add0656101..81f9e2a77fc 100644 --- a/clippy_lints/src/methods/read_line_without_trim.rs +++ b/clippy_lints/src/methods/read_line_without_trim.rs @@ -35,8 +35,8 @@ pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr< && segment.ident.name == sym!(parse) && let parse_result_ty = cx.typeck_results().expr_ty(parent) && is_type_diagnostic_item(cx, parse_result_ty, sym::Result) - && let ty::Adt(_, substs) = parse_result_ty.kind() - && let Some(ok_ty) = substs[0].as_type() + && let ty::Adt(_, args) = parse_result_ty.kind() + && let Some(ok_ty) = args[0].as_type() && parse_fails_on_trailing_newline(ok_ty) { let local_snippet = snippet(cx, expr.span, "<expr>"); diff --git a/clippy_lints/src/methods/readonly_write_lock.rs b/clippy_lints/src/methods/readonly_write_lock.rs new file mode 100644 index 00000000000..e3ec921da0c --- /dev/null +++ b/clippy_lints/src/methods/readonly_write_lock.rs @@ -0,0 +1,52 @@ +use super::READONLY_WRITE_LOCK; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::mir::{enclosing_mir, visit_local_usage}; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, Node}; +use rustc_lint::LateContext; +use rustc_middle::mir::{Location, START_BLOCK}; +use rustc_span::sym; + +fn is_unwrap_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + if let ExprKind::MethodCall(path, receiver, ..) = expr.kind + && path.ident.name == sym::unwrap + { + is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(receiver).peel_refs(), sym::Result) + } else { + false + } +} + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, receiver: &Expr<'_>) { + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(receiver).peel_refs(), sym::RwLock) + && let Node::Expr(unwrap_call_expr) = cx.tcx.hir().get_parent(expr.hir_id) + && is_unwrap_call(cx, unwrap_call_expr) + && let parent = cx.tcx.hir().get_parent(unwrap_call_expr.hir_id) + && let Node::Local(local) = parent + && let Some(mir) = enclosing_mir(cx.tcx, expr.hir_id) + && let Some((local, _)) = mir.local_decls.iter_enumerated().find(|(_, decl)| { + local.span.contains(decl.source_info.span) + }) + && let Some(usages) = visit_local_usage(&[local], mir, Location { + block: START_BLOCK, + statement_index: 0, + }) + && let [usage] = usages.as_slice() + { + let writer_never_mutated = usage.local_consume_or_mutate_locs.is_empty(); + + if writer_never_mutated { + span_lint_and_sugg( + cx, + READONLY_WRITE_LOCK, + expr.span, + "this write lock is used only for reading", + "consider using a read lock instead", + format!("{}.read()", snippet(cx, receiver.span, "<receiver>")), + Applicability::MaybeIncorrect // write lock might be intentional for enforcing exclusiveness + ); + } + } +} diff --git a/clippy_lints/src/methods/string_lit_chars_any.rs b/clippy_lints/src/methods/string_lit_chars_any.rs new file mode 100644 index 00000000000..70da6ad58bd --- /dev/null +++ b/clippy_lints/src/methods/string_lit_chars_any.rs @@ -0,0 +1,58 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::msrvs::{Msrv, MATCHES_MACRO}; +use clippy_utils::source::snippet_opt; +use clippy_utils::{is_from_proc_macro, is_trait_method, path_to_local}; +use itertools::Itertools; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, Param, PatKind}; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::STRING_LIT_CHARS_ANY; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, + recv: &Expr<'_>, + param: &'tcx Param<'tcx>, + body: &Expr<'_>, + msrv: &Msrv, +) { + if msrv.meets(MATCHES_MACRO) + && is_trait_method(cx, expr, sym::Iterator) + && let PatKind::Binding(_, arg, _, _) = param.pat.kind + && let ExprKind::Lit(lit_kind) = recv.kind + && let LitKind::Str(val, _) = lit_kind.node + && let ExprKind::Binary(kind, lhs, rhs) = body.kind + && let BinOpKind::Eq = kind.node + && let Some(lhs_path) = path_to_local(lhs) + && let Some(rhs_path) = path_to_local(rhs) + && let scrutinee = match (lhs_path == arg, rhs_path == arg) { + (true, false) => rhs, + (false, true) => lhs, + _ => return, + } + && !is_from_proc_macro(cx, expr) + && let Some(scrutinee_snip) = snippet_opt(cx, scrutinee.span) + { + // Normalize the char using `map` so `join` doesn't use `Display`, if we don't then + // something like `r"\"` will become `'\'`, which is of course invalid + let pat_snip = val.as_str().chars().map(|c| format!("{c:?}")).join(" | "); + + span_lint_and_then( + cx, + STRING_LIT_CHARS_ANY, + expr.span, + "usage of `.chars().any(...)` to check if a char matches any from a string literal", + |diag| { + diag.span_suggestion_verbose( + expr.span, + "use `matches!(...)` instead", + format!("matches!({scrutinee_snip}, {pat_snip})"), + Applicability::MachineApplicable, + ); + } + ); + } +} diff --git a/clippy_lints/src/methods/type_id_on_box.rs b/clippy_lints/src/methods/type_id_on_box.rs index 35137c97101..3404bdfe79b 100644 --- a/clippy_lints/src/methods/type_id_on_box.rs +++ b/clippy_lints/src/methods/type_id_on_box.rs @@ -24,9 +24,9 @@ pub(super) fn check(cx: &LateContext<'_>, receiver: &Expr<'_>, call_span: Span) if let Some(Adjustment { target: recv_ty, .. }) = recv_adjusts.last() && let ty::Ref(_, ty, _) = recv_ty.kind() - && let ty::Adt(adt, substs) = ty.kind() + && let ty::Adt(adt, args) = ty.kind() && adt.is_box() - && is_dyn_any(cx, substs.type_at(0)) + && is_dyn_any(cx, args.type_at(0)) { span_lint_and_then( cx, diff --git a/clippy_lints/src/methods/unnecessary_filter_map.rs b/clippy_lints/src/methods/unnecessary_filter_map.rs index cc64a2e7948..fabf3fa0c0c 100644 --- a/clippy_lints/src/methods/unnecessary_filter_map.rs +++ b/clippy_lints/src/methods/unnecessary_filter_map.rs @@ -77,6 +77,16 @@ fn check_expression<'tcx>(cx: &LateContext<'tcx>, arg_id: hir::HirId, expr: &'tc } (true, true) }, + hir::ExprKind::MethodCall(segment, recv, [arg], _) => { + if segment.ident.name == sym!(then_some) + && cx.typeck_results().expr_ty(recv).is_bool() + && path_to_local_id(arg, arg_id) + { + (false, true) + } else { + (true, true) + } + }, hir::ExprKind::Block(block, _) => block .expr .as_ref() diff --git a/clippy_lints/src/methods/unnecessary_literal_unwrap.rs b/clippy_lints/src/methods/unnecessary_literal_unwrap.rs index 111bcaaecec..937aac8d25e 100644 --- a/clippy_lints/src/methods/unnecessary_literal_unwrap.rs +++ b/clippy_lints/src/methods/unnecessary_literal_unwrap.rs @@ -3,6 +3,8 @@ use clippy_utils::{is_res_lang_ctor, last_path_segment, path_res, MaybePath}; use rustc_errors::Applicability; use rustc_hir as hir; use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_middle::ty::print::with_forced_trimmed_paths; use super::UNNECESSARY_LITERAL_UNWRAP; @@ -22,6 +24,7 @@ fn get_ty_from_args<'a>(args: Option<&'a [hir::GenericArg<'a>]>, index: usize) - } } +#[expect(clippy::too_many_lines)] pub(super) fn check( cx: &LateContext<'_>, expr: &hir::Expr<'_>, @@ -84,6 +87,34 @@ pub(super) fn check( } Some(suggs) }, + ("None", "unwrap_or_default", _) => { + let ty = cx.typeck_results().expr_ty(expr); + let default_ty_string = if let ty::Adt(def, ..) = ty.kind() { + with_forced_trimmed_paths!(format!("{}", cx.tcx.def_path_str(def.did()))) + } else { + "Default".to_string() + }; + Some(vec![(expr.span, format!("{default_ty_string}::default()"))]) + }, + ("None", "unwrap_or", _) => Some(vec![ + (expr.span.with_hi(args[0].span.lo()), String::new()), + (expr.span.with_lo(args[0].span.hi()), String::new()), + ]), + ("None", "unwrap_or_else", _) => match args[0].kind { + hir::ExprKind::Closure(hir::Closure { + fn_decl: + hir::FnDecl { + output: hir::FnRetTy::DefaultReturn(span) | hir::FnRetTy::Return(hir::Ty { span, .. }), + .. + }, + .. + }) => Some(vec![ + (expr.span.with_hi(span.hi()), String::new()), + (expr.span.with_lo(args[0].span.hi()), String::new()), + ]), + _ => None, + }, + _ if call_args.is_empty() => None, (_, _, Some(_)) => None, ("Ok", "unwrap_err", None) | ("Err", "unwrap", None) => Some(vec![ ( diff --git a/clippy_lints/src/methods/unwrap_or_else_default.rs b/clippy_lints/src/methods/unwrap_or_else_default.rs deleted file mode 100644 index 474a33b67e1..00000000000 --- a/clippy_lints/src/methods/unwrap_or_else_default.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Lint for `some_result_or_option.unwrap_or_else(Default::default)` - -use super::UNWRAP_OR_ELSE_DEFAULT; -use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::is_default_equivalent_call; -use clippy_utils::source::snippet_with_applicability; -use clippy_utils::ty::is_type_diagnostic_item; -use rustc_ast::ast::LitKind; -use rustc_errors::Applicability; -use rustc_hir as hir; -use rustc_lint::LateContext; -use rustc_span::{sym, symbol}; - -pub(super) fn check<'tcx>( - cx: &LateContext<'tcx>, - expr: &'tcx hir::Expr<'_>, - recv: &'tcx hir::Expr<'_>, - u_arg: &'tcx hir::Expr<'_>, -) { - // something.unwrap_or_else(Default::default) - // ^^^^^^^^^- recv ^^^^^^^^^^^^^^^^- u_arg - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- expr - let recv_ty = cx.typeck_results().expr_ty(recv); - let is_option = is_type_diagnostic_item(cx, recv_ty, sym::Option); - let is_result = is_type_diagnostic_item(cx, recv_ty, sym::Result); - - if_chain! { - if is_option || is_result; - if closure_body_returns_empty_to_string(cx, u_arg) || is_default_equivalent_call(cx, u_arg); - then { - let mut applicability = Applicability::MachineApplicable; - - span_lint_and_sugg( - cx, - UNWRAP_OR_ELSE_DEFAULT, - expr.span, - "use of `.unwrap_or_else(..)` to construct default value", - "try", - format!( - "{}.unwrap_or_default()", - snippet_with_applicability(cx, recv.span, "..", &mut applicability) - ), - applicability, - ); - } - } -} - -fn closure_body_returns_empty_to_string(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> bool { - if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = e.kind { - let body = cx.tcx.hir().body(body); - - if body.params.is_empty() - && let hir::Expr{ kind, .. } = &body.value - && let hir::ExprKind::MethodCall(hir::PathSegment {ident, ..}, self_arg, _, _) = kind - && ident == &symbol::Ident::from_str("to_string") - && let hir::Expr{ kind, .. } = self_arg - && let hir::ExprKind::Lit(lit) = kind - && let LitKind::Str(symbol::kw::Empty, _) = lit.node - { - return true; - } - } - - false -} diff --git a/clippy_lints/src/min_ident_chars.rs b/clippy_lints/src/min_ident_chars.rs index 2a60f2faca0..c79a1a7b9d4 100644 --- a/clippy_lints/src/min_ident_chars.rs +++ b/clippy_lints/src/min_ident_chars.rs @@ -129,6 +129,14 @@ impl Visitor<'_> for IdentVisitor<'_, '_> { return; } + // `struct Array<T, const N: usize>([T; N])` + // ^ + if let Node::GenericParam(generic_param) = node + && let GenericParamKind::Const { .. } = generic_param.kind + { + return; + } + if is_from_proc_macro(cx, &ident) { return; } diff --git a/clippy_lints/src/needless_pass_by_ref_mut.rs b/clippy_lints/src/needless_pass_by_ref_mut.rs index d323a16c2a7..c634de960d1 100644 --- a/clippy_lints/src/needless_pass_by_ref_mut.rs +++ b/clippy_lints/src/needless_pass_by_ref_mut.rs @@ -1,16 +1,18 @@ use super::needless_pass_by_value::requires_exact_signature; -use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::diagnostics::span_lint_hir_and_then; use clippy_utils::source::snippet; -use clippy_utils::{is_from_proc_macro, is_self}; -use if_chain::if_chain; +use clippy_utils::{get_parent_node, inherits_cfg, is_from_proc_macro, is_self}; +use rustc_data_structures::fx::{FxHashSet, FxIndexMap}; use rustc_errors::Applicability; -use rustc_hir::intravisit::FnKind; -use rustc_hir::{Body, FnDecl, HirId, HirIdMap, HirIdSet, Impl, ItemKind, Mutability, Node, PatKind}; +use rustc_hir::intravisit::{walk_qpath, FnKind, Visitor}; +use rustc_hir::{Body, ExprKind, FnDecl, HirId, HirIdMap, HirIdSet, Impl, ItemKind, Mutability, Node, PatKind, QPath}; use rustc_hir_typeck::expr_use_visitor as euv; use rustc_infer::infer::TyCtxtInferExt; use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::map::associated_body; +use rustc_middle::hir::nested_filter::OnlyBodies; use rustc_middle::mir::FakeReadCause; -use rustc_middle::ty::{self, Ty}; +use rustc_middle::ty::{self, Ty, UpvarId, UpvarPath}; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::def_id::LocalDefId; use rustc_span::symbol::kw; @@ -46,20 +48,24 @@ declare_clippy_lint! { "using a `&mut` argument when it's not mutated" } -#[derive(Copy, Clone)] -pub struct NeedlessPassByRefMut { +#[derive(Clone)] +pub struct NeedlessPassByRefMut<'tcx> { avoid_breaking_exported_api: bool, + used_fn_def_ids: FxHashSet<LocalDefId>, + fn_def_ids_to_maybe_unused_mut: FxIndexMap<LocalDefId, Vec<rustc_hir::Ty<'tcx>>>, } -impl NeedlessPassByRefMut { +impl NeedlessPassByRefMut<'_> { pub fn new(avoid_breaking_exported_api: bool) -> Self { Self { avoid_breaking_exported_api, + used_fn_def_ids: FxHashSet::default(), + fn_def_ids_to_maybe_unused_mut: FxIndexMap::default(), } } } -impl_lint_pass!(NeedlessPassByRefMut => [NEEDLESS_PASS_BY_REF_MUT]); +impl_lint_pass!(NeedlessPassByRefMut<'_> => [NEEDLESS_PASS_BY_REF_MUT]); fn should_skip<'tcx>( cx: &LateContext<'tcx>, @@ -87,12 +93,12 @@ fn should_skip<'tcx>( is_from_proc_macro(cx, &input) } -impl<'tcx> LateLintPass<'tcx> for NeedlessPassByRefMut { +impl<'tcx> LateLintPass<'tcx> for NeedlessPassByRefMut<'tcx> { fn check_fn( &mut self, cx: &LateContext<'tcx>, kind: FnKind<'tcx>, - decl: &'tcx FnDecl<'_>, + decl: &'tcx FnDecl<'tcx>, body: &'tcx Body<'_>, span: Span, fn_def_id: LocalDefId, @@ -102,17 +108,17 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByRefMut { } let hir_id = cx.tcx.hir().local_def_id_to_hir_id(fn_def_id); - - match kind { + let is_async = match kind { FnKind::ItemFn(.., header) => { let attrs = cx.tcx.hir().attrs(hir_id); if header.abi != Abi::Rust || requires_exact_signature(attrs) { return; } + header.is_async() }, - FnKind::Method(..) => (), + FnKind::Method(.., sig) => sig.header.is_async(), FnKind::Closure => return, - } + }; // Exclude non-inherent impls if let Some(Node::Item(item)) = cx.tcx.hir().find_parent(hir_id) { @@ -128,63 +134,89 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByRefMut { let fn_sig = cx.tcx.liberate_late_bound_regions(fn_def_id.to_def_id(), fn_sig); // If there are no `&mut` argument, no need to go any further. - if !decl + let mut it = decl .inputs .iter() .zip(fn_sig.inputs()) .zip(body.params) - .any(|((&input, &ty), arg)| !should_skip(cx, input, ty, arg)) - { + .filter(|((&input, &ty), arg)| !should_skip(cx, input, ty, arg)) + .peekable(); + if it.peek().is_none() { return; } - // Collect variables mutably used and spans which will need dereferencings from the // function body. let MutablyUsedVariablesCtxt { mutably_used_vars, .. } = { let mut ctx = MutablyUsedVariablesCtxt::default(); let infcx = cx.tcx.infer_ctxt().build(); euv::ExprUseVisitor::new(&mut ctx, &infcx, fn_def_id, cx.param_env, cx.typeck_results()).consume_body(body); + if is_async { + let closures = ctx.async_closures.clone(); + let hir = cx.tcx.hir(); + for closure in closures { + ctx.prev_bind = None; + ctx.prev_move_to_closure.clear(); + if let Some(body) = hir + .find_by_def_id(closure) + .and_then(associated_body) + .map(|(_, body_id)| hir.body(body_id)) + { + euv::ExprUseVisitor::new(&mut ctx, &infcx, closure, cx.param_env, cx.typeck_results()) + .consume_body(body); + } + } + } ctx }; - - let mut it = decl - .inputs - .iter() - .zip(fn_sig.inputs()) - .zip(body.params) - .filter(|((&input, &ty), arg)| !should_skip(cx, input, ty, arg)) - .peekable(); - if it.peek().is_none() { - return; - } - let show_semver_warning = self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(fn_def_id); for ((&input, &_), arg) in it { // Only take `&mut` arguments. - if_chain! { - if let PatKind::Binding(_, canonical_id, ..) = arg.pat.kind; - if !mutably_used_vars.contains(&canonical_id); - if let rustc_hir::TyKind::Ref(_, inner_ty) = input.kind; - then { - // If the argument is never used mutably, we emit the warning. - let sp = input.span; - span_lint_and_then( + if let PatKind::Binding(_, canonical_id, ..) = arg.pat.kind + && !mutably_used_vars.contains(&canonical_id) + { + self.fn_def_ids_to_maybe_unused_mut.entry(fn_def_id).or_default().push(input); + } + } + } + + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + cx.tcx.hir().visit_all_item_likes_in_crate(&mut FnNeedsMutVisitor { + cx, + used_fn_def_ids: &mut self.used_fn_def_ids, + }); + + for (fn_def_id, unused) in self + .fn_def_ids_to_maybe_unused_mut + .iter() + .filter(|(def_id, _)| !self.used_fn_def_ids.contains(def_id)) + { + let show_semver_warning = + self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(*fn_def_id); + + let mut is_cfged = None; + for input in unused { + // If the argument is never used mutably, we emit the warning. + let sp = input.span; + if let rustc_hir::TyKind::Ref(_, inner_ty) = input.kind { + let is_cfged = is_cfged.get_or_insert_with(|| inherits_cfg(cx.tcx, *fn_def_id)); + span_lint_hir_and_then( cx, NEEDLESS_PASS_BY_REF_MUT, + cx.tcx.hir().local_def_id_to_hir_id(*fn_def_id), sp, "this argument is a mutable reference, but not used mutably", |diag| { diag.span_suggestion( sp, "consider changing to".to_string(), - format!( - "&{}", - snippet(cx, cx.tcx.hir().span(inner_ty.ty.hir_id), "_"), - ), + format!("&{}", snippet(cx, cx.tcx.hir().span(inner_ty.ty.hir_id), "_"),), Applicability::Unspecified, ); if show_semver_warning { diag.warn("changing this function will impact semver compatibility"); } + if *is_cfged { + diag.note("this is cfg-gated and may require further changes"); + } }, ); } @@ -197,7 +229,9 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByRefMut { struct MutablyUsedVariablesCtxt { mutably_used_vars: HirIdSet, prev_bind: Option<HirId>, + prev_move_to_closure: HirIdSet, aliases: HirIdMap<HirId>, + async_closures: FxHashSet<LocalDefId>, } impl MutablyUsedVariablesCtxt { @@ -213,16 +247,27 @@ impl MutablyUsedVariablesCtxt { impl<'tcx> euv::Delegate<'tcx> for MutablyUsedVariablesCtxt { fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, _id: HirId) { if let euv::Place { - base: euv::PlaceBase::Local(vid), + base: + euv::PlaceBase::Local(vid) + | euv::PlaceBase::Upvar(UpvarId { + var_path: UpvarPath { hir_id: vid }, + .. + }), base_ty, .. } = &cmt.place { if let Some(bind_id) = self.prev_bind.take() { - self.aliases.insert(bind_id, *vid); - } else if matches!(base_ty.ref_mutability(), Some(Mutability::Mut)) { + if bind_id != *vid { + self.aliases.insert(bind_id, *vid); + } + } else if !self.prev_move_to_closure.contains(vid) + && matches!(base_ty.ref_mutability(), Some(Mutability::Mut)) + { self.add_mutably_used_var(*vid); } + self.prev_bind = None; + self.prev_move_to_closure.remove(vid); } } @@ -265,9 +310,73 @@ impl<'tcx> euv::Delegate<'tcx> for MutablyUsedVariablesCtxt { self.prev_bind = None; } - fn fake_read(&mut self, _: &rustc_hir_typeck::expr_use_visitor::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {} + fn fake_read( + &mut self, + cmt: &rustc_hir_typeck::expr_use_visitor::PlaceWithHirId<'tcx>, + cause: FakeReadCause, + _id: HirId, + ) { + if let euv::Place { + base: + euv::PlaceBase::Upvar(UpvarId { + var_path: UpvarPath { hir_id: vid }, + .. + }), + .. + } = &cmt.place + { + if let FakeReadCause::ForLet(Some(inner)) = cause { + // Seems like we are inside an async function. We need to store the closure `DefId` + // to go through it afterwards. + self.async_closures.insert(inner); + self.aliases.insert(cmt.hir_id, *vid); + self.prev_move_to_closure.insert(*vid); + } + } + } fn bind(&mut self, _cmt: &euv::PlaceWithHirId<'tcx>, id: HirId) { self.prev_bind = Some(id); } } + +/// A final pass to check for paths referencing this function that require the argument to be +/// `&mut`, basically if the function is ever used as a `fn`-like argument. +struct FnNeedsMutVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + used_fn_def_ids: &'a mut FxHashSet<LocalDefId>, +} + +impl<'tcx> Visitor<'tcx> for FnNeedsMutVisitor<'_, 'tcx> { + type NestedFilter = OnlyBodies; + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_qpath(&mut self, qpath: &'tcx QPath<'tcx>, hir_id: HirId, _: Span) { + walk_qpath(self, qpath, hir_id); + + let Self { cx, used_fn_def_ids } = self; + + // #11182; do not lint if mutability is required elsewhere + if let Node::Expr(expr) = cx.tcx.hir().get(hir_id) + && let Some(parent) = get_parent_node(cx.tcx, expr.hir_id) + && let ty::FnDef(def_id, _) = cx.tcx.typeck(cx.tcx.hir().enclosing_body_owner(hir_id)).expr_ty(expr).kind() + && let Some(def_id) = def_id.as_local() + { + if let Node::Expr(e) = parent + && let ExprKind::Call(call, _) = e.kind + && call.hir_id == expr.hir_id + { + return; + } + + // We don't need to check each argument individually as you cannot coerce a function + // taking `&mut` -> `&`, for some reason, so if we've gotten this far we know it's + // passed as a `fn`-like argument (or is unified) and should ignore every "unused" + // argument entirely + used_fn_def_ids.insert(def_id); + } + } +} diff --git a/clippy_lints/src/needless_pass_by_value.rs b/clippy_lints/src/needless_pass_by_value.rs index 5e26601537f..5ee26966fa7 100644 --- a/clippy_lints/src/needless_pass_by_value.rs +++ b/clippy_lints/src/needless_pass_by_value.rs @@ -2,7 +2,7 @@ use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; use clippy_utils::ptr::get_spans; use clippy_utils::source::{snippet, snippet_opt}; use clippy_utils::ty::{ - implements_trait, implements_trait_with_env, is_copy, is_type_diagnostic_item, is_type_lang_item, + implements_trait, implements_trait_with_env_from_iter, is_copy, is_type_diagnostic_item, is_type_lang_item, }; use clippy_utils::{get_trait_def_id, is_self, paths}; use if_chain::if_chain; @@ -182,7 +182,13 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { if !ty.is_mutable_ptr(); if !is_copy(cx, ty); if ty.is_sized(cx.tcx, cx.param_env); - if !allowed_traits.iter().any(|&t| implements_trait_with_env(cx.tcx, cx.param_env, ty, t, [None])); + if !allowed_traits.iter().any(|&t| implements_trait_with_env_from_iter( + cx.tcx, + cx.param_env, + ty, + t, + [Option::<ty::GenericArg<'tcx>>::None], + )); if !implements_borrow_trait; if !all_borrowable_trait; diff --git a/clippy_lints/src/non_copy_const.rs b/clippy_lints/src/non_copy_const.rs index 8bb2fa92585..a70692d8ff8 100644 --- a/clippy_lints/src/non_copy_const.rs +++ b/clippy_lints/src/non_copy_const.rs @@ -154,7 +154,7 @@ fn is_value_unfrozen_raw<'tcx>( ty::Adt(def, ..) if def.is_union() => false, ty::Array(ty, _) => val.unwrap_branch().iter().any(|field| inner(cx, *field, ty)), ty::Adt(def, _) if def.is_union() => false, - ty::Adt(def, substs) if def.is_enum() => { + ty::Adt(def, args) if def.is_enum() => { let (&variant_index, fields) = val.unwrap_branch().split_first().unwrap(); let variant_index = VariantIdx::from_u32(variant_index.unwrap_leaf().try_to_u32().ok().unwrap()); fields @@ -164,19 +164,14 @@ fn is_value_unfrozen_raw<'tcx>( def.variants()[variant_index] .fields .iter() - .map(|field| field.ty(cx.tcx, substs)), + .map(|field| field.ty(cx.tcx, args)), ) .any(|(field, ty)| inner(cx, field, ty)) }, - ty::Adt(def, substs) => val + ty::Adt(def, args) => val .unwrap_branch() .iter() - .zip( - def.non_enum_variant() - .fields - .iter() - .map(|field| field.ty(cx.tcx, substs)), - ) + .zip(def.non_enum_variant().fields.iter().map(|field| field.ty(cx.tcx, args))) .any(|(field, ty)| inner(cx, *field, ty)), ty::Tuple(tys) => val .unwrap_branch() diff --git a/clippy_lints/src/operators/arithmetic_side_effects.rs b/clippy_lints/src/operators/arithmetic_side_effects.rs index 35dd8fabe6e..f9108145cdb 100644 --- a/clippy_lints/src/operators/arithmetic_side_effects.rs +++ b/clippy_lints/src/operators/arithmetic_side_effects.rs @@ -1,7 +1,7 @@ use super::ARITHMETIC_SIDE_EFFECTS; use clippy_utils::consts::{constant, constant_simple, Constant}; use clippy_utils::diagnostics::span_lint; -use clippy_utils::{is_from_proc_macro, is_lint_allowed, peel_hir_expr_refs, peel_hir_expr_unary}; +use clippy_utils::{expr_or_init, is_from_proc_macro, is_lint_allowed, peel_hir_expr_refs, peel_hir_expr_unary}; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::Ty; @@ -138,8 +138,10 @@ impl ArithmeticSideEffects { ) { return; }; - let (actual_lhs, lhs_ref_counter) = peel_hir_expr_refs(lhs); - let (actual_rhs, rhs_ref_counter) = peel_hir_expr_refs(rhs); + let (mut actual_lhs, lhs_ref_counter) = peel_hir_expr_refs(lhs); + let (mut actual_rhs, rhs_ref_counter) = peel_hir_expr_refs(rhs); + actual_lhs = expr_or_init(cx, actual_lhs); + actual_rhs = expr_or_init(cx, actual_rhs); let lhs_ty = cx.typeck_results().expr_ty(actual_lhs).peel_refs(); let rhs_ty = cx.typeck_results().expr_ty(actual_rhs).peel_refs(); if self.has_allowed_binary(lhs_ty, rhs_ty) { diff --git a/clippy_lints/src/option_env_unwrap.rs b/clippy_lints/src/option_env_unwrap.rs index 377bddeaa5f..9c7f7e1cd7f 100644 --- a/clippy_lints/src/option_env_unwrap.rs +++ b/clippy_lints/src/option_env_unwrap.rs @@ -1,10 +1,9 @@ use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::is_direct_expn_of; -use if_chain::if_chain; use rustc_ast::ast::{Expr, ExprKind, MethodCall}; use rustc_lint::{EarlyContext, EarlyLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; -use rustc_span::sym; +use rustc_span::{sym, Span}; declare_clippy_lint! { /// ### What it does @@ -36,21 +35,27 @@ declare_lint_pass!(OptionEnvUnwrap => [OPTION_ENV_UNWRAP]); impl EarlyLintPass for OptionEnvUnwrap { fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { - if_chain! { - if let ExprKind::MethodCall(box MethodCall { seg, receiver, .. }) = &expr.kind; - if matches!(seg.ident.name, sym::expect | sym::unwrap); - if let ExprKind::Call(caller, _) = &receiver.kind; - if is_direct_expn_of(caller.span, "option_env").is_some(); - then { - span_lint_and_help( - cx, - OPTION_ENV_UNWRAP, - expr.span, - "this will panic at run-time if the environment variable doesn't exist at compile-time", - None, - "consider using the `env!` macro instead" - ); - } + fn lint(cx: &EarlyContext<'_>, span: Span) { + span_lint_and_help( + cx, + OPTION_ENV_UNWRAP, + span, + "this will panic at run-time if the environment variable doesn't exist at compile-time", + None, + "consider using the `env!` macro instead", + ); } + + if let ExprKind::MethodCall(box MethodCall { seg, receiver, .. }) = &expr.kind && + matches!(seg.ident.name, sym::expect | sym::unwrap) { + if let ExprKind::Call(caller, _) = &receiver.kind && + // If it exists, it will be ::core::option::Option::Some("<env var>").unwrap() (A method call in the HIR) + is_direct_expn_of(caller.span, "option_env").is_some() { + lint(cx, expr.span); + } else if let ExprKind::Path(_, caller) = &receiver.kind && // If it doesn't exist, it will be ::core::option::Option::None::<&'static str>.unwrap() (A path in the HIR) + is_direct_expn_of(caller.span, "option_env").is_some() { + lint(cx, expr.span); + } + } } } diff --git a/clippy_lints/src/ptr.rs b/clippy_lints/src/ptr.rs index 264c38df11f..42299d8d42f 100644 --- a/clippy_lints/src/ptr.rs +++ b/clippy_lints/src/ptr.rs @@ -26,6 +26,7 @@ use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::source_map::Span; use rustc_span::sym; use rustc_span::symbol::Symbol; +use rustc_target::spec::abi::Abi; use rustc_trait_selection::infer::InferCtxtExt as _; use rustc_trait_selection::traits::query::evaluate_obligation::InferCtxtExt as _; use std::{fmt, iter}; @@ -163,6 +164,12 @@ impl<'tcx> LateLintPass<'tcx> for Ptr { } check_mut_from_ref(cx, sig, None); + + if !matches!(sig.header.abi, Abi::Rust) { + // Ignore `extern` functions with non-Rust calling conventions + return; + } + for arg in check_fn_args( cx, cx.tcx @@ -222,6 +229,12 @@ impl<'tcx> LateLintPass<'tcx> for Ptr { }; check_mut_from_ref(cx, sig, Some(body)); + + if !matches!(sig.header.abi, Abi::Rust) { + // Ignore `extern` functions with non-Rust calling conventions + return; + } + let decl = sig.decl; let sig = cx.tcx.fn_sig(item_id).instantiate_identity().skip_binder(); let lint_args: Vec<_> = check_fn_args(cx, sig.inputs(), decl.inputs, &decl.output, body.params) diff --git a/clippy_lints/src/redundant_locals.rs b/clippy_lints/src/redundant_locals.rs new file mode 100644 index 00000000000..140ae837a17 --- /dev/null +++ b/clippy_lints/src/redundant_locals.rs @@ -0,0 +1,103 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_from_proc_macro; +use clippy_utils::ty::needs_ordered_drop; +use rustc_hir::def::Res; +use rustc_hir::{BindingAnnotation, ByRef, Expr, ExprKind, HirId, Local, Node, Pat, PatKind, QPath}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::Ident; + +declare_clippy_lint! { + /// ### What it does + /// Checks for redundant redefinitions of local bindings. + /// + /// ### Why is this bad? + /// Redundant redefinitions of local bindings do not change behavior and are likely to be unintended. + /// + /// Note that although these bindings do not affect your code's meaning, they _may_ affect `rustc`'s stack allocation. + /// + /// ### Example + /// ```rust + /// let a = 0; + /// let a = a; + /// + /// fn foo(b: i32) { + /// let b = b; + /// } + /// ``` + /// Use instead: + /// ```rust + /// let a = 0; + /// // no redefinition with the same name + /// + /// fn foo(b: i32) { + /// // no redefinition with the same name + /// } + /// ``` + #[clippy::version = "1.72.0"] + pub REDUNDANT_LOCALS, + correctness, + "redundant redefinition of a local binding" +} +declare_lint_pass!(RedundantLocals => [REDUNDANT_LOCALS]); + +impl<'tcx> LateLintPass<'tcx> for RedundantLocals { + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { + if_chain! { + // the pattern is a single by-value binding + if let PatKind::Binding(BindingAnnotation(ByRef::No, mutability), _, ident, None) = local.pat.kind; + // the binding is not type-ascribed + if local.ty.is_none(); + // the expression is a resolved path + if let Some(expr) = local.init; + if let ExprKind::Path(qpath @ QPath::Resolved(None, path)) = expr.kind; + // the path is a single segment equal to the local's name + if let [last_segment] = path.segments; + if last_segment.ident == ident; + // resolve the path to its defining binding pattern + if let Res::Local(binding_id) = cx.qpath_res(&qpath, expr.hir_id); + if let Node::Pat(binding_pat) = cx.tcx.hir().get(binding_id); + // the previous binding has the same mutability + if find_binding(binding_pat, ident).unwrap().1 == mutability; + // the local does not affect the code's drop behavior + if !affects_drop_behavior(cx, binding_id, local.hir_id, expr); + // the local is user-controlled + if !in_external_macro(cx.sess(), local.span); + if !is_from_proc_macro(cx, expr); + then { + span_lint_and_help( + cx, + REDUNDANT_LOCALS, + vec![binding_pat.span, local.span], + "redundant redefinition of a binding", + None, + &format!("remove the redefinition of `{ident}`"), + ); + } + } + } +} + +/// Find the annotation of a binding introduced by a pattern, or `None` if it's not introduced. +fn find_binding(pat: &Pat<'_>, name: Ident) -> Option<BindingAnnotation> { + let mut ret = None; + + pat.each_binding_or_first(&mut |annotation, _, _, ident| { + if ident == name { + ret = Some(annotation); + } + }); + + ret +} + +/// Check if a rebinding of a local affects the code's drop behavior. +fn affects_drop_behavior<'tcx>(cx: &LateContext<'tcx>, bind: HirId, rebind: HirId, rebind_expr: &Expr<'tcx>) -> bool { + let hir = cx.tcx.hir(); + + // the rebinding is in a different scope than the original binding + // and the type of the binding cares about drop order + hir.get_enclosing_scope(bind) != hir.get_enclosing_scope(rebind) + && needs_ordered_drop(cx, cx.typeck_results().expr_ty(rebind_expr)) +} diff --git a/clippy_lints/src/redundant_static_lifetimes.rs b/clippy_lints/src/redundant_static_lifetimes.rs index 038dfe8e480..ed42a422b4b 100644 --- a/clippy_lints/src/redundant_static_lifetimes.rs +++ b/clippy_lints/src/redundant_static_lifetimes.rs @@ -5,6 +5,7 @@ use rustc_ast::ast::{ConstItem, Item, ItemKind, StaticItem, Ty, TyKind}; use rustc_errors::Applicability; use rustc_lint::{EarlyContext, EarlyLintPass}; use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::kw; declare_clippy_lint! { /// ### What it does @@ -64,7 +65,7 @@ impl RedundantStaticLifetimes { if let Some(lifetime) = *optional_lifetime { match borrow_type.ty.kind { TyKind::Path(..) | TyKind::Slice(..) | TyKind::Array(..) | TyKind::Tup(..) => { - if lifetime.ident.name == rustc_span::symbol::kw::StaticLifetime { + if lifetime.ident.name == kw::StaticLifetime { let snip = snippet(cx, borrow_type.ty.span, "<type>"); let sugg = format!("&{}{snip}", borrow_type.mutbl.prefix_str()); span_lint_and_then( diff --git a/clippy_lints/src/renamed_lints.rs b/clippy_lints/src/renamed_lints.rs index e532dd61a82..49bdc679604 100644 --- a/clippy_lints/src/renamed_lints.rs +++ b/clippy_lints/src/renamed_lints.rs @@ -30,6 +30,7 @@ pub static RENAMED_LINTS: &[(&str, &str)] = &[ ("clippy::single_char_push_str", "clippy::single_char_add_str"), ("clippy::stutter", "clippy::module_name_repetitions"), ("clippy::to_string_in_display", "clippy::recursive_format_impl"), + ("clippy::unwrap_or_else_default", "clippy::unwrap_or_default"), ("clippy::zero_width_space", "clippy::invisible_characters"), ("clippy::cast_ref_to_mut", "invalid_reference_casting"), ("clippy::clone_double_ref", "suspicious_double_ref_op"), diff --git a/clippy_lints/src/returns.rs b/clippy_lints/src/returns.rs index 2a494ff1fa6..351bacf5691 100644 --- a/clippy_lints/src/returns.rs +++ b/clippy_lints/src/returns.rs @@ -1,12 +1,14 @@ -use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then}; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then, span_lint_hir_and_then}; use clippy_utils::source::{snippet_opt, snippet_with_context}; use clippy_utils::visitors::{for_each_expr_with_closures, Descend}; -use clippy_utils::{fn_def_id, path_to_local_id, span_find_starting_semi}; +use clippy_utils::{fn_def_id, is_from_proc_macro, path_to_local_id, span_find_starting_semi}; use core::ops::ControlFlow; use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir::intravisit::FnKind; -use rustc_hir::{Block, Body, Expr, ExprKind, FnDecl, LangItem, MatchSource, PatKind, QPath, StmtKind}; +use rustc_hir::{ + Block, Body, Expr, ExprKind, FnDecl, ItemKind, LangItem, MatchSource, OwnerNode, PatKind, QPath, Stmt, StmtKind, +}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; use rustc_middle::ty::{self, GenericArgKind, Ty}; @@ -76,6 +78,46 @@ declare_clippy_lint! { "using a return statement like `return expr;` where an expression would suffice" } +declare_clippy_lint! { + /// ### What it does + /// Checks for return statements on `Err` paired with the `?` operator. + /// + /// ### Why is this bad? + /// The `return` is unnecessary. + /// + /// ### Example + /// ```rust,ignore + /// fn foo(x: usize) -> Result<(), Box<dyn Error>> { + /// if x == 0 { + /// return Err(...)?; + /// } + /// Ok(()) + /// } + /// ``` + /// simplify to + /// ```rust,ignore + /// fn foo(x: usize) -> Result<(), Box<dyn Error>> { + /// if x == 0 { + /// Err(...)?; + /// } + /// Ok(()) + /// } + /// ``` + /// if paired with `try_err`, use instead: + /// ```rust,ignore + /// fn foo(x: usize) -> Result<(), Box<dyn Error>> { + /// if x == 0 { + /// return Err(...); + /// } + /// Ok(()) + /// } + /// ``` + #[clippy::version = "1.73.0"] + pub NEEDLESS_RETURN_WITH_QUESTION_MARK, + style, + "using a return statement like `return Err(expr)?;` where removing it would suffice" +} + #[derive(PartialEq, Eq)] enum RetReplacement<'tcx> { Empty, @@ -115,9 +157,35 @@ impl<'tcx> ToString for RetReplacement<'tcx> { } } -declare_lint_pass!(Return => [LET_AND_RETURN, NEEDLESS_RETURN]); +declare_lint_pass!(Return => [LET_AND_RETURN, NEEDLESS_RETURN, NEEDLESS_RETURN_WITH_QUESTION_MARK]); impl<'tcx> LateLintPass<'tcx> for Return { + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + if !in_external_macro(cx.sess(), stmt.span) + && let StmtKind::Semi(expr) = stmt.kind + && let ExprKind::Ret(Some(ret)) = expr.kind + && let ExprKind::Match(.., MatchSource::TryDesugar) = ret.kind + // Ensure this is not the final stmt, otherwise removing it would cause a compile error + && let OwnerNode::Item(item) = cx.tcx.hir().owner(cx.tcx.hir().get_parent_item(expr.hir_id)) + && let ItemKind::Fn(_, _, body) = item.kind + && let block = cx.tcx.hir().body(body).value + && let ExprKind::Block(block, _) = block.kind + && let [.., final_stmt] = block.stmts + && final_stmt.hir_id != stmt.hir_id + && !is_from_proc_macro(cx, expr) + { + span_lint_and_sugg( + cx, + NEEDLESS_RETURN_WITH_QUESTION_MARK, + expr.span.until(ret.span), + "unneeded `return` statement with `?` operator", + "remove it", + String::new(), + Applicability::MachineApplicable, + ); + } + } + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'_>) { // we need both a let-binding stmt and an expr if_chain! { @@ -173,6 +241,10 @@ impl<'tcx> LateLintPass<'tcx> for Return { sp: Span, _: LocalDefId, ) { + if sp.from_expansion() { + return; + } + match kind { FnKind::Closure => { // when returning without value in closure, replace this `return` diff --git a/clippy_lints/src/semicolon_if_nothing_returned.rs b/clippy_lints/src/semicolon_if_nothing_returned.rs index 355f907e257..c9547cd95dc 100644 --- a/clippy_lints/src/semicolon_if_nothing_returned.rs +++ b/clippy_lints/src/semicolon_if_nothing_returned.rs @@ -43,7 +43,7 @@ impl<'tcx> LateLintPass<'tcx> for SemicolonIfNothingReturned { if let Some(expr) = block.expr; let t_expr = cx.typeck_results().expr_ty(expr); if t_expr.is_unit(); - let mut app = Applicability::MaybeIncorrect; + let mut app = Applicability::MachineApplicable; if let snippet = snippet_with_context(cx, expr.span, block.span.ctxt(), "}", &mut app).0; if !snippet.ends_with('}') && !snippet.ends_with(';'); if cx.sess().source_map().is_multiline(block.span); diff --git a/clippy_lints/src/significant_drop_tightening.rs b/clippy_lints/src/significant_drop_tightening.rs index 3b282dbfa04..4b248c9c790 100644 --- a/clippy_lints/src/significant_drop_tightening.rs +++ b/clippy_lints/src/significant_drop_tightening.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::source::{indent_of, snippet}; -use clippy_utils::{expr_or_init, get_attr, path_to_local}; +use clippy_utils::{expr_or_init, get_attr, path_to_local, peel_hir_expr_unary}; use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap}; use rustc_errors::Applicability; use rustc_hir::def::{DefKind, Res}; @@ -235,7 +235,7 @@ impl<'ap, 'lc, 'others, 'stmt, 'tcx> StmtsChecker<'ap, 'lc, 'others, 'stmt, 'tcx fn manage_has_expensive_expr_after_last_attr(&mut self) { let has_expensive_stmt = match self.ap.curr_stmt.kind { - hir::StmtKind::Expr(expr) if !is_expensive_expr(expr) => false, + hir::StmtKind::Expr(expr) if is_inexpensive_expr(expr) => false, hir::StmtKind::Local(local) if let Some(expr) = local.init && let hir::ExprKind::Path(_) = expr.kind => false, _ => true @@ -330,13 +330,13 @@ impl<'ap, 'lc, 'others, 'stmt, 'tcx> Visitor<'tcx> for StmtsChecker<'ap, 'lc, 'o apa.last_method_span = span; } }, - hir::StmtKind::Semi(expr) => { - if has_drop(expr, &apa.first_bind_ident, self.cx) { + hir::StmtKind::Semi(semi_expr) => { + if has_drop(semi_expr, &apa.first_bind_ident, self.cx) { apa.has_expensive_expr_after_last_attr = false; apa.last_stmt_span = DUMMY_SP; return; } - if let hir::ExprKind::MethodCall(_, _, _, span) = expr.kind { + if let hir::ExprKind::MethodCall(_, _, _, span) = semi_expr.kind { apa.last_method_span = span; } }, @@ -434,16 +434,31 @@ fn has_drop(expr: &hir::Expr<'_>, first_bind_ident: &Ident, lcx: &LateContext<'_ && let Res::Def(DefKind::Fn, did) = fun_path.res && lcx.tcx.is_diagnostic_item(sym::mem_drop, did) && let [first_arg, ..] = args - && let hir::ExprKind::Path(hir::QPath::Resolved(_, arg_path)) = &first_arg.kind - && let [first_arg_ps, .. ] = arg_path.segments { - &first_arg_ps.ident == first_bind_ident - } - else { - false + let has_ident = |local_expr: &hir::Expr<'_>| { + if let hir::ExprKind::Path(hir::QPath::Resolved(_, arg_path)) = &local_expr.kind + && let [first_arg_ps, .. ] = arg_path.segments + && &first_arg_ps.ident == first_bind_ident + { + true + } + else { + false + } + }; + if has_ident(first_arg) { + return true; + } + if let hir::ExprKind::Tup(value) = &first_arg.kind && value.iter().any(has_ident) { + return true; + } } + false } -fn is_expensive_expr(expr: &hir::Expr<'_>) -> bool { - !matches!(expr.kind, hir::ExprKind::Path(_)) +fn is_inexpensive_expr(expr: &hir::Expr<'_>) -> bool { + let actual = peel_hir_expr_unary(expr).0; + let is_path = matches!(actual.kind, hir::ExprKind::Path(_)); + let is_lit = matches!(actual.kind, hir::ExprKind::Lit(_)); + is_path || is_lit } diff --git a/clippy_lints/src/slow_vector_initialization.rs b/clippy_lints/src/slow_vector_initialization.rs index 858135c8d46..54a33eb2986 100644 --- a/clippy_lints/src/slow_vector_initialization.rs +++ b/clippy_lints/src/slow_vector_initialization.rs @@ -1,13 +1,13 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::sugg::Sugg; -use clippy_utils::ty::is_type_diagnostic_item; use clippy_utils::{ - get_enclosing_block, is_integer_literal, is_path_diagnostic_item, path_to_local, path_to_local_id, SpanlessEq, + get_enclosing_block, is_expr_path_def_path, is_integer_literal, is_path_diagnostic_item, path_to_local, + path_to_local_id, paths, SpanlessEq, }; use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir::intravisit::{walk_block, walk_expr, walk_stmt, Visitor}; -use rustc_hir::{BindingAnnotation, Block, Expr, ExprKind, HirId, PatKind, QPath, Stmt, StmtKind}; +use rustc_hir::{BindingAnnotation, Block, Expr, ExprKind, HirId, PatKind, Stmt, StmtKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::symbol::sym; @@ -60,7 +60,24 @@ struct VecAllocation<'tcx> { /// Reference to the expression used as argument on `with_capacity` call. This is used /// to only match slow zero-filling idioms of the same length than vector initialization. - len_expr: &'tcx Expr<'tcx>, + size_expr: InitializedSize<'tcx>, +} + +/// Initializer for the creation of the vector. +/// +/// When `Vec::with_capacity(size)` is found, the `size` expression will be in +/// `InitializedSize::Initialized`. +/// +/// Otherwise, for `Vec::new()` calls, there is no allocation initializer yet, so +/// `InitializedSize::Uninitialized` is used. +/// Later, when a call to `.resize(size, 0)` or similar is found, it's set +/// to `InitializedSize::Initialized(size)`. +/// +/// Since it will be set to `InitializedSize::Initialized(size)` when a slow initialization is +/// found, it is always safe to "unwrap" it at lint time. +enum InitializedSize<'tcx> { + Initialized(&'tcx Expr<'tcx>), + Uninitialized, } /// Type of slow initialization @@ -77,18 +94,14 @@ impl<'tcx> LateLintPass<'tcx> for SlowVectorInit { // Matches initialization on reassignments. For example: `vec = Vec::with_capacity(100)` if_chain! { if let ExprKind::Assign(left, right, _) = expr.kind; - - // Extract variable if let Some(local_id) = path_to_local(left); - - // Extract len argument - if let Some(len_arg) = Self::is_vec_with_capacity(cx, right); + if let Some(size_expr) = Self::as_vec_initializer(cx, right); then { let vi = VecAllocation { local_id, allocation_expr: right, - len_expr: len_arg, + size_expr, }; Self::search_initialization(cx, vi, expr.hir_id); @@ -98,17 +111,18 @@ impl<'tcx> LateLintPass<'tcx> for SlowVectorInit { fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { // Matches statements which initializes vectors. For example: `let mut vec = Vec::with_capacity(10)` + // or `Vec::new()` if_chain! { if let StmtKind::Local(local) = stmt.kind; if let PatKind::Binding(BindingAnnotation::MUT, local_id, _, None) = local.pat.kind; if let Some(init) = local.init; - if let Some(len_arg) = Self::is_vec_with_capacity(cx, init); + if let Some(size_expr) = Self::as_vec_initializer(cx, init); then { let vi = VecAllocation { local_id, allocation_expr: init, - len_expr: len_arg, + size_expr, }; Self::search_initialization(cx, vi, stmt.hir_id); @@ -118,19 +132,20 @@ impl<'tcx> LateLintPass<'tcx> for SlowVectorInit { } impl SlowVectorInit { - /// Checks if the given expression is `Vec::with_capacity(..)`. It will return the expression - /// of the first argument of `with_capacity` call if it matches or `None` if it does not. - fn is_vec_with_capacity<'tcx>(cx: &LateContext<'_>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { - if_chain! { - if let ExprKind::Call(func, [arg]) = expr.kind; - if let ExprKind::Path(QPath::TypeRelative(ty, name)) = func.kind; - if name.ident.as_str() == "with_capacity"; - if is_type_diagnostic_item(cx, cx.typeck_results().node_type(ty.hir_id), sym::Vec); - then { - Some(arg) - } else { - None - } + /// Looks for `Vec::with_capacity(size)` or `Vec::new()` calls and returns the initialized size, + /// if any. More specifically, it returns: + /// - `Some(InitializedSize::Initialized(size))` for `Vec::with_capacity(size)` + /// - `Some(InitializedSize::Uninitialized)` for `Vec::new()` + /// - `None` for other, unrelated kinds of expressions + fn as_vec_initializer<'tcx>(cx: &LateContext<'_>, expr: &'tcx Expr<'tcx>) -> Option<InitializedSize<'tcx>> { + if let ExprKind::Call(func, [len_expr]) = expr.kind + && is_expr_path_def_path(cx, func, &paths::VEC_WITH_CAPACITY) + { + Some(InitializedSize::Initialized(len_expr)) + } else if matches!(expr.kind, ExprKind::Call(func, _) if is_expr_path_def_path(cx, func, &paths::VEC_NEW)) { + Some(InitializedSize::Uninitialized) + } else { + None } } @@ -169,12 +184,19 @@ impl SlowVectorInit { } fn emit_lint(cx: &LateContext<'_>, slow_fill: &Expr<'_>, vec_alloc: &VecAllocation<'_>, msg: &str) { - let len_expr = Sugg::hir(cx, vec_alloc.len_expr, "len"); + let len_expr = Sugg::hir( + cx, + match vec_alloc.size_expr { + InitializedSize::Initialized(expr) => expr, + InitializedSize::Uninitialized => unreachable!("size expression must be set by this point"), + }, + "len", + ); span_lint_and_then(cx, SLOW_VECTOR_INITIALIZATION, slow_fill.span, msg, |diag| { diag.span_suggestion( vec_alloc.allocation_expr.span, - "consider replace allocation with", + "consider replacing this with", format!("vec![0; {len_expr}]"), Applicability::Unspecified, ); @@ -214,36 +236,45 @@ impl<'a, 'tcx> VectorInitializationVisitor<'a, 'tcx> { } /// Checks if the given expression is resizing a vector with 0 - fn search_slow_resize_filling(&mut self, expr: &'tcx Expr<'_>) { + fn search_slow_resize_filling(&mut self, expr: &'tcx Expr<'tcx>) { if self.initialization_found && let ExprKind::MethodCall(path, self_arg, [len_arg, fill_arg], _) = expr.kind && path_to_local_id(self_arg, self.vec_alloc.local_id) && path.ident.name == sym!(resize) // Check that is filled with 0 - && is_integer_literal(fill_arg, 0) { - // Check that len expression is equals to `with_capacity` expression - if SpanlessEq::new(self.cx).eq_expr(len_arg, self.vec_alloc.len_expr) { - self.slow_expression = Some(InitializationType::Resize(expr)); - } else if let ExprKind::MethodCall(path, ..) = len_arg.kind && path.ident.as_str() == "capacity" { - self.slow_expression = Some(InitializationType::Resize(expr)); - } + && is_integer_literal(fill_arg, 0) + { + let is_matching_resize = if let InitializedSize::Initialized(size_expr) = self.vec_alloc.size_expr { + // If we have a size expression, check that it is equal to what's passed to `resize` + SpanlessEq::new(self.cx).eq_expr(len_arg, size_expr) + || matches!(len_arg.kind, ExprKind::MethodCall(path, ..) if path.ident.as_str() == "capacity") + } else { + self.vec_alloc.size_expr = InitializedSize::Initialized(len_arg); + true + }; + + if is_matching_resize { + self.slow_expression = Some(InitializationType::Resize(expr)); } + } } /// Returns `true` if give expression is `repeat(0).take(...)` - fn is_repeat_take(&self, expr: &Expr<'_>) -> bool { + fn is_repeat_take(&mut self, expr: &'tcx Expr<'tcx>) -> bool { if_chain! { if let ExprKind::MethodCall(take_path, recv, [len_arg, ..], _) = expr.kind; if take_path.ident.name == sym!(take); // Check that take is applied to `repeat(0)` if self.is_repeat_zero(recv); then { - // Check that len expression is equals to `with_capacity` expression - if SpanlessEq::new(self.cx).eq_expr(len_arg, self.vec_alloc.len_expr) { - return true; - } else if let ExprKind::MethodCall(path, ..) = len_arg.kind && path.ident.as_str() == "capacity" { - return true; + if let InitializedSize::Initialized(size_expr) = self.vec_alloc.size_expr { + // Check that len expression is equals to `with_capacity` expression + return SpanlessEq::new(self.cx).eq_expr(len_arg, size_expr) + || matches!(len_arg.kind, ExprKind::MethodCall(path, ..) if path.ident.as_str() == "capacity") } + + self.vec_alloc.size_expr = InitializedSize::Initialized(len_arg); + return true; } } diff --git a/clippy_lints/src/tuple_array_conversions.rs b/clippy_lints/src/tuple_array_conversions.rs index 2e00ed042a8..7eec6982092 100644 --- a/clippy_lints/src/tuple_array_conversions.rs +++ b/clippy_lints/src/tuple_array_conversions.rs @@ -1,21 +1,29 @@ use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::msrvs::{self, Msrv}; +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, HirId, Node, Pat}; +use rustc_hir::{Expr, ExprKind, Node, PatKind}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; -use rustc_middle::ty; +use rustc_middle::ty::{self, Ty}; use rustc_session::{declare_tool_lint, 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 may convey intent more clearly. + /// 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 @@ -29,7 +37,7 @@ declare_clippy_lint! { /// ``` #[clippy::version = "1.72.0"] pub TUPLE_ARRAY_CONVERSIONS, - pedantic, + nursery, "checks for tuple<=>array conversions that are not done with `.into()`" } impl_lint_pass!(TupleArrayConversions => [TUPLE_ARRAY_CONVERSIONS]); @@ -41,130 +49,152 @@ pub struct TupleArrayConversions { 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) { - 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), - _ => {}, - } + 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); } -#[expect( - clippy::blocks_in_if_conditions, - reason = "not a FP, but this is much easier to understand" -)] fn check_array<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, elements: &'tcx [Expr<'tcx>]) { - if should_lint( - cx, - elements, - // This is cursed. - Some, - |(first_id, local)| { - if let Node::Pat(pat) = local - && let parent = parent_pat(cx, pat) - && parent.hir_id == first_id - { - return matches!( - cx.typeck_results().pat_ty(parent).peel_refs().kind(), - ty::Tuple(len) if len.len() == elements.len() - ); - } - - false - }, - ) || should_lint( - cx, - elements, - |(i, expr)| { - if let ExprKind::Field(path, field) = expr.kind && field.as_str() == i.to_string() { - return Some((i, path)); - }; - - None - }, - |(first_id, local)| { - if let Node::Pat(pat) = local - && let parent = parent_pat(cx, pat) - && parent.hir_id == first_id - { - return matches!( - cx.typeck_results().pat_ty(parent).peel_refs().kind(), - ty::Tuple(len) if len.len() == elements.len() - ); - } + 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", + ); + } +} - false - }, - ) { - emit_lint(cx, expr, ToType::Array); +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", + ); } } -#[expect( - clippy::blocks_in_if_conditions, - reason = "not a FP, but this is much easier to understand" -)] +/// 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 check_tuple<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, elements: &'tcx [Expr<'tcx>]) { - if should_lint(cx, elements, Some, |(first_id, local)| { - if let Node::Pat(pat) = local - && let parent = parent_pat(cx, pat) - && parent.hir_id == first_id - { - return matches!( - cx.typeck_results().pat_ty(parent).peel_refs().kind(), - ty::Array(_, len) if len.eval_target_usize(cx.tcx, cx.param_env) as usize == elements.len() - ); +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, } - - false - }) || should_lint( - cx, - elements, - |(i, expr)| { - if let ExprKind::Index(path, index) = expr.kind - && let ExprKind::Lit(lit) = index.kind - && let LitKind::Int(val, _) = lit.node - && val as usize == i - { - return Some((i, path)); + .map(|hir_id| cx.typeck_results().node_type(hir_id)) else { + return false; }; - - None - }, - |(first_id, local)| { - if let Node::Pat(pat) = local - && let parent = parent_pat(cx, pat) - && parent.hir_id == first_id - { - return matches!( - cx.typeck_results().pat_ty(parent).peel_refs().kind(), - ty::Array(_, len) if len.eval_target_usize(cx.tcx, cx.param_env) as usize == elements.len() - ); + 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)) => { + len.eval_target_usize(cx.tcx, cx.param_env) as usize == elements.len() + && final_tys.iter().chain(once(ty)).all_equal() + }, + _ => false, } - - false - }, - ) { - emit_lint(cx, expr, ToType::Tuple); - } -} - -/// Walks up the `Pat` until it's reached the final containing `Pat`. -fn parent_pat<'tcx>(cx: &LateContext<'tcx>, start: &'tcx Pat<'tcx>) -> &'tcx Pat<'tcx> { - let mut end = start; - for (_, node) in cx.tcx.hir().parent_iter(start.hir_id) { - if let Node::Pat(pat) = node { - end = pat; - } else { - break; - } - } - end + }) } #[derive(Clone, Copy)] @@ -173,61 +203,11 @@ enum ToType { Tuple, } -impl ToType { - fn msg(self) -> &'static str { - match self { - ToType::Array => "it looks like you're trying to convert a tuple to an array", - ToType::Tuple => "it looks like you're trying to convert an array to a tuple", - } - } - - fn help(self) -> &'static str { +impl PartialEq<PatKind<'_>> for ToType { + fn eq(&self, other: &PatKind<'_>) -> bool { match self { - ToType::Array => "use `.into()` instead, or `<[T; N]>::from` if type annotations are needed", - ToType::Tuple => "use `.into()` instead, or `<(T0, T1, ..., Tn)>::from` if type annotations are needed", + ToType::Array => matches!(other, PatKind::Tuple(_, _)), + ToType::Tuple => matches!(other, PatKind::Slice(_, _, _)), } } } - -fn emit_lint<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, to_type: ToType) -> bool { - if !is_from_proc_macro(cx, expr) { - span_lint_and_help( - cx, - TUPLE_ARRAY_CONVERSIONS, - expr.span, - to_type.msg(), - None, - to_type.help(), - ); - - return true; - } - - false -} - -fn should_lint<'tcx>( - cx: &LateContext<'tcx>, - elements: &'tcx [Expr<'tcx>], - map: impl FnMut((usize, &'tcx Expr<'tcx>)) -> Option<(usize, &Expr<'_>)>, - predicate: impl FnMut((HirId, &Node<'tcx>)) -> bool, -) -> bool { - if let Some(elements) = elements - .iter() - .enumerate() - .map(map) - .collect::<Option<Vec<_>>>() - && let Some(locals) = elements - .iter() - .map(|(_, element)| path_to_local(element).and_then(|local| cx.tcx.hir().find(local))) - .collect::<Option<Vec<_>>>() - && let [first, rest @ ..] = &*locals - && let Node::Pat(first_pat) = first - && let parent = parent_pat(cx, first_pat).hir_id - && rest.iter().chain(once(first)).map(|i| (parent, i)).all(predicate) - { - return true; - } - - false -} diff --git a/clippy_lints/src/unused_async.rs b/clippy_lints/src/unused_async.rs index 5e42cf7e4f3..bc7c3897a6e 100644 --- a/clippy_lints/src/unused_async.rs +++ b/clippy_lints/src/unused_async.rs @@ -1,11 +1,12 @@ -use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::diagnostics::span_lint_hir_and_then; use clippy_utils::is_def_id_trait_method; +use rustc_hir::def::DefKind; use rustc_hir::intravisit::{walk_body, walk_expr, walk_fn, FnKind, Visitor}; -use rustc_hir::{Body, Expr, ExprKind, FnDecl, YieldSource}; +use rustc_hir::{Body, Expr, ExprKind, FnDecl, Node, YieldSource}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::hir::nested_filter; -use rustc_session::{declare_lint_pass, declare_tool_lint}; -use rustc_span::def_id::LocalDefId; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::def_id::{LocalDefId, LocalDefIdSet}; use rustc_span::Span; declare_clippy_lint! { @@ -38,7 +39,24 @@ declare_clippy_lint! { "finds async functions with no await statements" } -declare_lint_pass!(UnusedAsync => [UNUSED_ASYNC]); +#[derive(Default)] +pub struct UnusedAsync { + /// Keeps track of async functions used as values (i.e. path expressions to async functions that + /// are not immediately called) + async_fns_as_value: LocalDefIdSet, + /// Functions with unused `async`, linted post-crate after we've found all uses of local async + /// functions + unused_async_fns: Vec<UnusedAsyncFn>, +} + +#[derive(Copy, Clone)] +struct UnusedAsyncFn { + def_id: LocalDefId, + fn_span: Span, + await_in_async_block: Option<Span>, +} + +impl_lint_pass!(UnusedAsync => [UNUSED_ASYNC]); struct AsyncFnVisitor<'a, 'tcx> { cx: &'a LateContext<'tcx>, @@ -101,24 +119,70 @@ impl<'tcx> LateLintPass<'tcx> for UnusedAsync { }; walk_fn(&mut visitor, fn_kind, fn_decl, body.id(), def_id); if !visitor.found_await { - span_lint_and_then( - cx, - UNUSED_ASYNC, - span, - "unused `async` for function with no await statements", - |diag| { - diag.help("consider removing the `async` from this function"); - - if let Some(span) = visitor.await_in_async_block { - diag.span_note( - span, - "`await` used in an async block, which does not require \ - the enclosing function to be `async`", - ); - } - }, - ); + // Don't lint just yet, but store the necessary information for later. + // The actual linting happens in `check_crate_post`, once we've found all + // uses of local async functions that do require asyncness to pass typeck + self.unused_async_fns.push(UnusedAsyncFn { + await_in_async_block: visitor.await_in_async_block, + fn_span: span, + def_id, + }); } } } + + fn check_path(&mut self, cx: &LateContext<'tcx>, path: &rustc_hir::Path<'tcx>, hir_id: rustc_hir::HirId) { + fn is_node_func_call(node: Node<'_>, expected_receiver: Span) -> bool { + matches!( + node, + Node::Expr(Expr { + kind: ExprKind::Call(Expr { span, .. }, _) | ExprKind::MethodCall(_, Expr { span, .. }, ..), + .. + }) if *span == expected_receiver + ) + } + + // Find paths to local async functions that aren't immediately called. + // E.g. `async fn f() {}; let x = f;` + // Depending on how `x` is used, f's asyncness might be required despite not having any `await` + // statements, so don't lint at all if there are any such paths. + if let Some(def_id) = path.res.opt_def_id() + && let Some(local_def_id) = def_id.as_local() + && let Some(DefKind::Fn) = cx.tcx.opt_def_kind(def_id) + && cx.tcx.asyncness(def_id).is_async() + && !is_node_func_call(cx.tcx.hir().get_parent(hir_id), path.span) + { + self.async_fns_as_value.insert(local_def_id); + } + } + + // After collecting all unused `async` and problematic paths to such functions, + // lint those unused ones that didn't have any path expressions to them. + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + let iter = self + .unused_async_fns + .iter() + .filter(|UnusedAsyncFn { def_id, .. }| (!self.async_fns_as_value.contains(def_id))); + + for fun in iter { + span_lint_hir_and_then( + cx, + UNUSED_ASYNC, + cx.tcx.local_def_id_to_hir_id(fun.def_id), + fun.fn_span, + "unused `async` for function with no await statements", + |diag| { + diag.help("consider removing the `async` from this function"); + + if let Some(span) = fun.await_in_async_block { + diag.span_note( + span, + "`await` used in an async block, which does not require \ + the enclosing function to be `async`", + ); + } + }, + ); + } + } } diff --git a/clippy_lints/src/utils/conf.rs b/clippy_lints/src/utils/conf.rs index 76654bfe536..58ae0656db7 100644 --- a/clippy_lints/src/utils/conf.rs +++ b/clippy_lints/src/utils/conf.rs @@ -551,6 +551,16 @@ define_Conf! { /// /// Whether to allow `r#""#` when `r""` can be used (allow_one_hash_in_raw_strings: bool = false), + /// Lint: ABSOLUTE_PATHS. + /// + /// The maximum number of segments a path can have before being linted, anything above this will + /// be linted. + (absolute_paths_max_segments: u64 = 2), + /// Lint: ABSOLUTE_PATHS. + /// + /// Which crates to allow absolute paths from + (absolute_paths_allowed_crates: rustc_data_structures::fx::FxHashSet<String> = + rustc_data_structures::fx::FxHashSet::default()), } /// Search for the configuration file. diff --git a/clippy_lints/src/utils/mod.rs b/clippy_lints/src/utils/mod.rs index fb08256931f..4fef8c0717d 100644 --- a/clippy_lints/src/utils/mod.rs +++ b/clippy_lints/src/utils/mod.rs @@ -18,7 +18,7 @@ const BOOK_CONFIGS_PATH: &str = "https://doc.rust-lang.org/clippy/lint_configura // ================================================================== // Configuration // ================================================================== -#[derive(Debug, Clone, Default)] //~ ERROR no such field +#[derive(Debug, Clone, Default)] pub struct ClippyConfiguration { pub name: String, #[allow(dead_code)] |
