diff options
| author | Philipp Krones <hello@philkrones.com> | 2023-07-02 14:35:19 +0200 |
|---|---|---|
| committer | Philipp Krones <hello@philkrones.com> | 2023-07-02 14:59:02 +0200 |
| commit | cb3ecf7b792fdc4b00e61935b9e40ca836752492 (patch) | |
| tree | a183611f56d9139413f6ab1c78c0619512d3c751 /clippy_lints | |
| parent | bb33e0343fe37815f6180a861619a9fca6771ce9 (diff) | |
Merge commit '37f4c1725d3fd7e9c3ffd8783246bc5589debc53' into clippyup
Diffstat (limited to 'clippy_lints')
106 files changed, 5394 insertions, 1181 deletions
diff --git a/clippy_lints/Cargo.toml b/clippy_lints/Cargo.toml index be9261a4704..c23054443bb 100644 --- a/clippy_lints/Cargo.toml +++ b/clippy_lints/Cargo.toml @@ -22,6 +22,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", optional = true } tempfile = { version = "3.3.0", optional = true } toml = "0.7.3" +regex = { version = "1.5", optional = true } unicode-normalization = "0.1" unicode-script = { version = "0.5", default-features = false } semver = "1.0" @@ -31,7 +32,7 @@ url = "2.2" [features] deny-warnings = ["clippy_utils/deny-warnings"] # build clippy with internal lints enabled, off by default -internal = ["clippy_utils/internal", "serde_json", "tempfile"] +internal = ["clippy_utils/internal", "serde_json", "tempfile", "regex"] [package.metadata.rust-analyzer] # This crate uses #[feature(rustc_private)] diff --git a/clippy_lints/src/allow_attributes.rs b/clippy_lints/src/allow_attributes.rs index 554efdc58e1..eb21184713e 100644 --- a/clippy_lints/src/allow_attributes.rs +++ b/clippy_lints/src/allow_attributes.rs @@ -1,5 +1,5 @@ -use ast::AttrStyle; -use clippy_utils::diagnostics::span_lint_and_sugg; +use ast::{AttrStyle, Attribute}; +use clippy_utils::{diagnostics::span_lint_and_sugg, is_from_proc_macro}; use rustc_ast as ast; use rustc_errors::Applicability; use rustc_lint::{LateContext, LateLintPass, LintContext}; @@ -50,13 +50,14 @@ declare_lint_pass!(AllowAttribute => [ALLOW_ATTRIBUTES]); impl LateLintPass<'_> for AllowAttribute { // Separate each crate's features. - fn check_attribute(&mut self, cx: &LateContext<'_>, attr: &ast::Attribute) { + fn check_attribute<'cx>(&mut self, cx: &LateContext<'cx>, attr: &'cx Attribute) { if_chain! { if !in_external_macro(cx.sess(), attr.span); if cx.tcx.features().lint_reasons; if let AttrStyle::Outer = attr.style; if let Some(ident) = attr.ident(); if ident.name == rustc_span::symbol::sym::allow; + if !is_from_proc_macro(cx, &attr); then { span_lint_and_sugg( cx, diff --git a/clippy_lints/src/arc_with_non_send_sync.rs b/clippy_lints/src/arc_with_non_send_sync.rs new file mode 100644 index 00000000000..a1e44668e1a --- /dev/null +++ b/clippy_lints/src/arc_with_non_send_sync.rs @@ -0,0 +1,72 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::last_path_segment; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use if_chain::if_chain; + +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_lint::LateLintPass; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does. + /// This lint warns when you use `Arc` with a type that does not implement `Send` or `Sync`. + /// + /// ### Why is this bad? + /// Wrapping a type in Arc doesn't add thread safety to the underlying data, so data races + /// could occur when touching the underlying data. + /// + /// ### Example + /// ```rust + /// # use std::cell::RefCell; + /// # use std::sync::Arc; + /// + /// fn main() { + /// // This is safe, as `i32` implements `Send` and `Sync`. + /// let a = Arc::new(42); + /// + /// // This is not safe, as `RefCell` does not implement `Sync`. + /// let b = Arc::new(RefCell::new(42)); + /// } + /// ``` + #[clippy::version = "1.72.0"] + pub ARC_WITH_NON_SEND_SYNC, + correctness, + "using `Arc` with a type that does not implement `Send` or `Sync`" +} +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_chain! { + if is_type_diagnostic_item(cx, ty, sym::Arc); + if let ExprKind::Call(func, [arg]) = expr.kind; + if let ExprKind::Path(func_path) = func.kind; + if last_path_segment(&func_path).ident.name == sym::new; + if let arg_ty = cx.typeck_results().expr_ty(arg); + if !matches!(arg_ty.kind(), ty::Param(_)); + if !cx.tcx + .lang_items() + .sync_trait() + .map_or(false, |id| implements_trait(cx, arg_ty, id, &[])) || + !cx.tcx + .get_diagnostic_item(sym::Send) + .map_or(false, |id| implements_trait(cx, arg_ty, id, &[])); + + then { + span_lint_and_help( + cx, + ARC_WITH_NON_SEND_SYNC, + expr.span, + "usage of `Arc<T>` where `T` is not `Send` or `Sync`", + None, + "consider using `Rc<T>` instead or wrapping `T` in a std::sync type like \ + `Mutex<T>`", + ); + } + } + } +} diff --git a/clippy_lints/src/as_conversions.rs b/clippy_lints/src/as_conversions.rs index c7a76e5f907..b9dda49ca41 100644 --- a/clippy_lints/src/as_conversions.rs +++ b/clippy_lints/src/as_conversions.rs @@ -1,6 +1,7 @@ use clippy_utils::diagnostics::span_lint_and_help; -use rustc_ast::ast::{Expr, ExprKind}; -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use clippy_utils::is_from_proc_macro; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_lint_pass, declare_tool_lint}; @@ -45,9 +46,9 @@ declare_clippy_lint! { declare_lint_pass!(AsConversions => [AS_CONVERSIONS]); -impl EarlyLintPass for AsConversions { - fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { - if in_external_macro(cx.sess(), expr.span) { +impl<'tcx> LateLintPass<'tcx> for AsConversions { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + if in_external_macro(cx.sess(), expr.span) || is_from_proc_macro(cx, expr) { return; } diff --git a/clippy_lints/src/attrs.rs b/clippy_lints/src/attrs.rs index 897495ba108..2ba78f99569 100644 --- a/clippy_lints/src/attrs.rs +++ b/clippy_lints/src/attrs.rs @@ -1,9 +1,12 @@ //! checks for attributes -use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then}; use clippy_utils::macros::{is_panic, macro_backtrace}; use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::{first_line_of_span, is_present_in_source, snippet_opt, without_block_comments}; +use clippy_utils::{ + diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then}, + is_from_proc_macro, +}; use if_chain::if_chain; use rustc_ast::{AttrKind, AttrStyle, Attribute, LitKind, MetaItemKind, MetaItemLit, NestedMetaItem}; use rustc_errors::Applicability; @@ -362,6 +365,32 @@ declare_clippy_lint! { "ensure that all `cfg(any())` and `cfg(all())` have more than one condition" } +declare_clippy_lint! { + /// ### What it does + /// Checks for `#[cfg(features = "...")]` and suggests to replace it with + /// `#[cfg(feature = "...")]`. + /// + /// ### Why is this bad? + /// Misspelling `feature` as `features` can be sometimes hard to spot. It + /// may cause conditional compilation not work quitely. + /// + /// ### Example + /// ```rust + /// #[cfg(features = "some-feature")] + /// fn conditional() { } + /// ``` + /// + /// Use instead: + /// ```rust + /// #[cfg(feature = "some-feature")] + /// fn conditional() { } + /// ``` + #[clippy::version = "1.69.0"] + pub MAYBE_MISUSED_CFG, + suspicious, + "prevent from misusing the wrong attr name" +} + declare_lint_pass!(Attributes => [ ALLOW_ATTRIBUTES_WITHOUT_REASON, INLINE_ALWAYS, @@ -540,7 +569,7 @@ fn check_clippy_lint_names(cx: &LateContext<'_>, name: Symbol, items: &[NestedMe } } -fn check_lint_reason(cx: &LateContext<'_>, name: Symbol, items: &[NestedMetaItem], attr: &'_ Attribute) { +fn check_lint_reason<'cx>(cx: &LateContext<'cx>, name: Symbol, items: &[NestedMetaItem], attr: &'cx Attribute) { // Check for the feature if !cx.tcx.features().lint_reasons { return; @@ -555,7 +584,7 @@ fn check_lint_reason(cx: &LateContext<'_>, name: Symbol, items: &[NestedMetaItem } // Check if the attribute is in an external macro and therefore out of the developer's control - if in_external_macro(cx.sess(), attr.span) { + if in_external_macro(cx.sess(), attr.span) || is_from_proc_macro(cx, &attr) { return; } @@ -676,6 +705,7 @@ impl_lint_pass!(EarlyAttributes => [ EMPTY_LINE_AFTER_OUTER_ATTR, EMPTY_LINE_AFTER_DOC_COMMENTS, NON_MINIMAL_CFG, + MAYBE_MISUSED_CFG, ]); impl EarlyLintPass for EarlyAttributes { @@ -687,6 +717,7 @@ impl EarlyLintPass for EarlyAttributes { check_deprecated_cfg_attr(cx, attr, &self.msrv); check_mismatched_target_os(cx, attr); check_minimal_cfg_condition(cx, attr); + check_misused_cfg(cx, attr); } extract_msrv_attr!(EarlyContext); @@ -777,7 +808,7 @@ fn check_deprecated_cfg_attr(cx: &EarlyContext<'_>, attr: &Attribute, msrv: &Msr } fn check_nested_cfg(cx: &EarlyContext<'_>, items: &[NestedMetaItem]) { - for item in items.iter() { + for item in items { if let NestedMetaItem::MetaItem(meta) = item { if !meta.has_name(sym::any) && !meta.has_name(sym::all) { continue; @@ -810,6 +841,27 @@ fn check_nested_cfg(cx: &EarlyContext<'_>, items: &[NestedMetaItem]) { } } +fn check_nested_misused_cfg(cx: &EarlyContext<'_>, items: &[NestedMetaItem]) { + for item in items { + if let NestedMetaItem::MetaItem(meta) = item { + if meta.has_name(sym!(features)) && let Some(val) = meta.value_str() { + span_lint_and_sugg( + cx, + MAYBE_MISUSED_CFG, + meta.span, + "feature may misspelled as features", + "use", + format!("feature = \"{val}\""), + Applicability::MaybeIncorrect, + ); + } + if let MetaItemKind::List(list) = &meta.kind { + check_nested_misused_cfg(cx, list); + } + } + } +} + fn check_minimal_cfg_condition(cx: &EarlyContext<'_>, attr: &Attribute) { if attr.has_name(sym::cfg) && let Some(items) = attr.meta_item_list() @@ -818,6 +870,14 @@ fn check_minimal_cfg_condition(cx: &EarlyContext<'_>, attr: &Attribute) { } } +fn check_misused_cfg(cx: &EarlyContext<'_>, attr: &Attribute) { + if attr.has_name(sym::cfg) && + let Some(items) = attr.meta_item_list() + { + check_nested_misused_cfg(cx, &items); + } +} + fn check_mismatched_target_os(cx: &EarlyContext<'_>, attr: &Attribute) { fn find_os(name: &str) -> Option<&'static str> { UNIX_SYSTEMS diff --git a/clippy_lints/src/booleans.rs b/clippy_lints/src/booleans.rs index ff0102255a0..04cca9e3177 100644 --- a/clippy_lints/src/booleans.rs +++ b/clippy_lints/src/booleans.rs @@ -440,7 +440,7 @@ impl<'a, 'tcx> NonminimalBoolVisitor<'a, 'tcx> { diag.span_suggestions( e.span, "try", - suggestions.into_iter(), + suggestions, // nonminimal_bool can produce minimal but // not human readable expressions (#3141) Applicability::Unspecified, diff --git a/clippy_lints/src/casts/borrow_as_ptr.rs b/clippy_lints/src/casts/borrow_as_ptr.rs index 294d22d34de..b7256dd2eae 100644 --- a/clippy_lints/src/casts/borrow_as_ptr.rs +++ b/clippy_lints/src/casts/borrow_as_ptr.rs @@ -4,6 +4,7 @@ use clippy_utils::source::snippet_with_context; use rustc_errors::Applicability; use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, Ty, TyKind}; use rustc_lint::LateContext; +use rustc_middle::ty::adjustment::Adjust; use super::BORROW_AS_PTR; @@ -23,6 +24,15 @@ pub(super) fn check<'tcx>( }; let mut app = Applicability::MachineApplicable; let snip = snippet_with_context(cx, e.span, cast_expr.span.ctxt(), "..", &mut app).0; + // Fix #9884 + if !e.is_place_expr(|base| { + cx.typeck_results() + .adjustments() + .get(base.hir_id) + .is_some_and(|x| x.iter().any(|adj| matches!(adj.kind, Adjust::Deref(_)))) + }) { + return; + } span_lint_and_sugg( cx, diff --git a/clippy_lints/src/casts/cast_possible_wrap.rs b/clippy_lints/src/casts/cast_possible_wrap.rs index 28ecdea7ea0..ffa571abb39 100644 --- a/clippy_lints/src/casts/cast_possible_wrap.rs +++ b/clippy_lints/src/casts/cast_possible_wrap.rs @@ -1,41 +1,90 @@ -use clippy_utils::diagnostics::span_lint; -use clippy_utils::ty::is_isize_or_usize; use rustc_hir::Expr; -use rustc_lint::LateContext; +use rustc_lint::{LateContext, LintContext}; use rustc_middle::ty::Ty; use super::{utils, CAST_POSSIBLE_WRAP}; +// this should be kept in sync with the allowed bit widths of `usize` and `isize` +const ALLOWED_POINTER_SIZES: [u64; 3] = [16, 32, 64]; + +// whether the lint should be emitted, and the required pointer size, if it matters +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum EmitState { + NoLint, + LintAlways, + LintOnPtrSize(u64), +} + pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) { if !(cast_from.is_integral() && cast_to.is_integral()) { return; } - let arch_64_suffix = " on targets with 64-bit wide pointers"; - let arch_32_suffix = " on targets with 32-bit wide pointers"; - let cast_unsigned_to_signed = !cast_from.is_signed() && cast_to.is_signed(); + // emit a lint if a cast is: + // 1. unsigned to signed + // and + // 2. either: + // + // 2a. between two types of constant size that are always the same size + // 2b. between one target-dependent size and one constant size integer, + // and the constant integer is in the allowed set of target dependent sizes + // (the ptr size could be chosen to be the same as the constant size) + + if cast_from.is_signed() || !cast_to.is_signed() { + return; + } + let from_nbits = utils::int_ty_to_nbits(cast_from, cx.tcx); let to_nbits = utils::int_ty_to_nbits(cast_to, cx.tcx); - let (should_lint, suffix) = match (is_isize_or_usize(cast_from), is_isize_or_usize(cast_to)) { - (true, true) | (false, false) => (to_nbits == from_nbits && cast_unsigned_to_signed, ""), - (true, false) => (to_nbits <= 32 && cast_unsigned_to_signed, arch_32_suffix), - (false, true) => ( - cast_unsigned_to_signed, - if from_nbits == 64 { - arch_64_suffix + let should_lint = match (cast_from.is_ptr_sized_integral(), cast_to.is_ptr_sized_integral()) { + (true, true) => { + // casts between two ptr sized integers are trivially always the same size + // so do not depend on any specific pointer size to be the same + EmitState::LintAlways + }, + (true, false) => { + // the first type is `usize` and the second is a constant sized signed integer + if ALLOWED_POINTER_SIZES.contains(&to_nbits) { + EmitState::LintOnPtrSize(to_nbits) + } else { + EmitState::NoLint + } + }, + (false, true) => { + // the first type is a constant sized unsigned integer, and the second is `isize` + if ALLOWED_POINTER_SIZES.contains(&from_nbits) { + EmitState::LintOnPtrSize(from_nbits) + } else { + EmitState::NoLint + } + }, + (false, false) => { + // the types are both a constant known size + // and do not depend on any specific pointer size to be the same + if from_nbits == to_nbits { + EmitState::LintAlways } else { - arch_32_suffix - }, + EmitState::NoLint + } + }, + }; + + let message = match should_lint { + EmitState::NoLint => return, + EmitState::LintAlways => format!("casting `{cast_from}` to `{cast_to}` may wrap around the value"), + EmitState::LintOnPtrSize(ptr_size) => format!( + "casting `{cast_from}` to `{cast_to}` may wrap around the value on targets with {ptr_size}-bit wide pointers", ), }; - if should_lint { - span_lint( - cx, - CAST_POSSIBLE_WRAP, - expr.span, - &format!("casting `{cast_from}` to `{cast_to}` may wrap around the value{suffix}",), - ); - } + cx.struct_span_lint(CAST_POSSIBLE_WRAP, expr.span, message, |diag| { + if let EmitState::LintOnPtrSize(16) = should_lint { + diag + .note("`usize` and `isize` may be as small as 16 bits on some platforms") + .note("for more information see https://doc.rust-lang.org/reference/types/numeric.html#machine-dependent-integer-types") + } else { + diag + } + }); } diff --git a/clippy_lints/src/casts/mod.rs b/clippy_lints/src/casts/mod.rs index b90dab07a27..0ac6ef6496a 100644 --- a/clippy_lints/src/casts/mod.rs +++ b/clippy_lints/src/casts/mod.rs @@ -118,9 +118,10 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does /// Checks for casts from an unsigned type to a signed type of - /// the same size. Performing such a cast is a 'no-op' for the compiler, - /// i.e., nothing is changed at the bit level, and the binary representation of - /// the value is reinterpreted. This can cause wrapping if the value is too big + /// the same size, or possibly smaller due to target dependent integers. + /// Performing such a cast is a 'no-op' for the compiler, i.e., nothing is + /// changed at the bit level, and the binary representation of the value is + /// reinterpreted. This can cause wrapping if the value is too big /// for the target signed type. However, the cast works as defined, so this lint /// is `Allow` by default. /// @@ -174,8 +175,8 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does - /// Checks for casts to the same type, casts of int literals to integer types - /// and casts of float literals to float types. + /// Checks for casts to the same type, casts of int literals to integer types, casts of float + /// literals to float types and casts between raw pointers without changing type or constness. /// /// ### Why is this bad? /// It's just unnecessary. @@ -522,7 +523,7 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does - /// Check for the usage of `as _` conversion using inferred type. + /// Checks for the usage of `as _` conversion using inferred type. /// /// ### Why is this bad? /// The conversion might include lossy conversion and dangerous cast that might go diff --git a/clippy_lints/src/casts/ptr_cast_constness.rs b/clippy_lints/src/casts/ptr_cast_constness.rs index ab015f8822e..f0c1df01430 100644 --- a/clippy_lints/src/casts/ptr_cast_constness.rs +++ b/clippy_lints/src/casts/ptr_cast_constness.rs @@ -9,20 +9,21 @@ use rustc_middle::ty::{self, Ty, TypeAndMut}; use super::PTR_CAST_CONSTNESS; -pub(super) fn check( +pub(super) fn check<'tcx>( cx: &LateContext<'_>, expr: &Expr<'_>, cast_expr: &Expr<'_>, - cast_from: Ty<'_>, - cast_to: Ty<'_>, + cast_from: Ty<'tcx>, + cast_to: Ty<'tcx>, msrv: &Msrv, ) { if_chain! { if msrv.meets(POINTER_CAST_CONSTNESS); - if let ty::RawPtr(TypeAndMut { mutbl: from_mutbl, .. }) = cast_from.kind(); - if let ty::RawPtr(TypeAndMut { mutbl: to_mutbl, .. }) = cast_to.kind(); + if let ty::RawPtr(TypeAndMut { mutbl: from_mutbl, ty: from_ty }) = cast_from.kind(); + if let ty::RawPtr(TypeAndMut { mutbl: to_mutbl, ty: to_ty }) = cast_to.kind(); if matches!((from_mutbl, to_mutbl), (Mutability::Not, Mutability::Mut) | (Mutability::Mut, Mutability::Not)); + if from_ty == to_ty; then { let sugg = Sugg::hir(cx, cast_expr, "_"); let constness = match *to_mutbl { @@ -34,7 +35,7 @@ pub(super) fn check( cx, PTR_CAST_CONSTNESS, expr.span, - "`as` casting between raw pointers while changing its constness", + "`as` casting between raw pointers while changing only its constness", &format!("try `pointer::cast_{constness}`, a safer alternative"), format!("{}.cast_{constness}()", sugg.maybe_par()), Applicability::MachineApplicable, diff --git a/clippy_lints/src/casts/unnecessary_cast.rs b/clippy_lints/src/casts/unnecessary_cast.rs index 804ae841100..71cf2aea0f8 100644 --- a/clippy_lints/src/casts/unnecessary_cast.rs +++ b/clippy_lints/src/casts/unnecessary_cast.rs @@ -1,18 +1,21 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::numeric_literal::NumericLiteral; use clippy_utils::source::snippet_opt; -use clippy_utils::{get_parent_expr, path_to_local}; +use clippy_utils::visitors::{for_each_expr, Visitable}; +use clippy_utils::{get_parent_expr, get_parent_node, is_hir_ty_cfg_dependant, is_ty_alias, path_to_local}; use if_chain::if_chain; use rustc_ast::{LitFloatType, LitIntType, LitKind}; use rustc_errors::Applicability; -use rustc_hir::def::Res; -use rustc_hir::{Expr, ExprKind, Lit, QPath, TyKind, UnOp}; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{Expr, ExprKind, Lit, Node, Path, QPath, TyKind, UnOp}; use rustc_lint::{LateContext, LintContext}; use rustc_middle::lint::in_external_macro; use rustc_middle::ty::{self, FloatTy, InferTy, Ty}; +use std::ops::ControlFlow; use super::UNNECESSARY_CAST; +#[expect(clippy::too_many_lines)] pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, expr: &Expr<'tcx>, @@ -20,19 +23,84 @@ pub(super) fn check<'tcx>( cast_from: Ty<'tcx>, cast_to: Ty<'tcx>, ) -> bool { - // skip non-primitive type cast + let cast_str = snippet_opt(cx, cast_expr.span).unwrap_or_default(); + + if_chain! { + if let ty::RawPtr(..) = cast_from.kind(); + // check both mutability and type are the same + if cast_from.kind() == cast_to.kind(); + if let ExprKind::Cast(_, cast_to_hir) = expr.kind; + // Ignore casts to e.g. type aliases and infer types + // - p as pointer_alias + // - p as _ + if let TyKind::Ptr(to_pointee) = cast_to_hir.kind; + then { + match to_pointee.ty.kind { + // Ignore casts to pointers that are aliases or cfg dependant, e.g. + // - p as *const std::ffi::c_char (alias) + // - p as *const std::os::raw::c_char (cfg dependant) + TyKind::Path(qpath) => { + if is_ty_alias(&qpath) || is_hir_ty_cfg_dependant(cx, to_pointee.ty) { + return false; + } + }, + // Ignore `p as *const _` + TyKind::Infer => return false, + _ => {}, + } + + span_lint_and_sugg( + cx, + UNNECESSARY_CAST, + expr.span, + &format!("casting raw pointers to the same type and constness is unnecessary (`{cast_from}` -> `{cast_to}`)"), + "try", + cast_str.clone(), + Applicability::MachineApplicable, + ); + } + } + + // skip cast of local that is a type alias + if let ExprKind::Cast(inner, ..) = expr.kind + && let ExprKind::Path(qpath) = inner.kind + && let QPath::Resolved(None, Path { res, .. }) = qpath + && let Res::Local(hir_id) = res + && let parent = cx.tcx.hir().get_parent(*hir_id) + && let Node::Local(local) = parent + { + if let Some(ty) = local.ty + && let TyKind::Path(qpath) = ty.kind + && is_ty_alias(&qpath) + { + return false; + } + + if let Some(expr) = local.init + && let ExprKind::Cast(.., cast_to) = expr.kind + && let TyKind::Path(qpath) = cast_to.kind + && is_ty_alias(&qpath) + { + return false; + } + } + + // 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; if let TyKind::Path(QPath::Resolved(_, path)) = &cast_to.kind; if let Res::PrimTy(_) = path.res; then {} else { - return false + return false; } } - let cast_str = snippet_opt(cx, cast_expr.span).unwrap_or_default(); - if let Some(lit) = get_numeric_literal(cast_expr) { let literal_str = &cast_str; @@ -162,3 +230,61 @@ fn fp_ty_mantissa_nbits(typ: Ty<'_>) -> u32 { _ => 0, } } + +/// Finds whether an `Expr` returns a type alias. +/// +/// TODO: Maybe we should move this to `clippy_utils` so others won't need to go down this dark, +/// dark path reimplementing this (or something similar). +fn is_cast_from_ty_alias<'tcx>(cx: &LateContext<'tcx>, expr: impl Visitable<'tcx>, cast_from: Ty<'tcx>) -> bool { + for_each_expr(expr, |expr| { + // Calls are a `Path`, and usage of locals are a `Path`. So, this checks + // - call() as i32 + // - local as i32 + if let ExprKind::Path(qpath) = expr.kind { + let res = cx.qpath_res(&qpath, expr.hir_id); + // Function call + if let Res::Def(DefKind::Fn, def_id) = res { + let Some(snippet) = snippet_opt(cx, cx.tcx.def_span(def_id)) else { + return ControlFlow::Continue(()); + }; + // This is the worst part of this entire function. This is the only way I know of to + // check whether a function returns a type alias. Sure, you can get the return type + // from a function in the current crate as an hir ty, but how do you get it for + // external functions?? Simple: It's impossible. So, we check whether a part of the + // 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! + if !snippet + .split("->") + .skip(0) + .map(|s| { + s.trim() == cast_from.to_string() + || s.split("where").any(|ty| ty.trim() == cast_from.to_string()) + }) + .any(|a| a) + { + return ControlFlow::Break(()); + } + // Local usage + } else if let Res::Local(hir_id) = res + && let Some(parent) = get_parent_node(cx.tcx, hir_id) + && let Node::Local(l) = parent + { + if let Some(e) = l.init && is_cast_from_ty_alias(cx, e, cast_from) { + return ControlFlow::Break::<()>(()); + } + + if let Some(ty) = l.ty + && let TyKind::Path(qpath) = ty.kind + && is_ty_alias(&qpath) + { + return ControlFlow::Break::<()>(()); + } + } + } + + ControlFlow::Continue(()) + }) + .is_some() +} diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index f1d1355123a..9d9ee6ba307 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -4,6 +4,8 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ #[cfg(feature = "internal")] + crate::utils::internal_lints::almost_standard_lint_formulation::ALMOST_STANDARD_LINT_FORMULATION_INFO, + #[cfg(feature = "internal")] crate::utils::internal_lints::clippy_lints_internal::CLIPPY_LINTS_INTERNAL_INFO, #[cfg(feature = "internal")] crate::utils::internal_lints::collapsible_calls::COLLAPSIBLE_SPAN_LINT_CALLS_INFO, @@ -38,6 +40,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::allow_attributes::ALLOW_ATTRIBUTES_INFO, crate::almost_complete_range::ALMOST_COMPLETE_RANGE_INFO, crate::approx_const::APPROX_CONSTANT_INFO, + crate::arc_with_non_send_sync::ARC_WITH_NON_SEND_SYNC_INFO, crate::as_conversions::AS_CONVERSIONS_INFO, crate::asm_syntax::INLINE_ASM_X86_ATT_SYNTAX_INFO, crate::asm_syntax::INLINE_ASM_X86_INTEL_SYNTAX_INFO, @@ -51,6 +54,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::attrs::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO, crate::attrs::EMPTY_LINE_AFTER_OUTER_ATTR_INFO, crate::attrs::INLINE_ALWAYS_INFO, + crate::attrs::MAYBE_MISUSED_CFG_INFO, crate::attrs::MISMATCHED_TARGET_OS_INFO, crate::attrs::NON_MINIMAL_CFG_INFO, crate::attrs::USELESS_ATTRIBUTE_INFO, @@ -136,11 +140,15 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::double_parens::DOUBLE_PARENS_INFO, crate::drop_forget_ref::DROP_NON_DROP_INFO, crate::drop_forget_ref::FORGET_NON_DROP_INFO, + crate::drop_forget_ref::MEM_FORGET_INFO, crate::duplicate_mod::DUPLICATE_MOD_INFO, crate::else_if_without_else::ELSE_IF_WITHOUT_ELSE_INFO, crate::empty_drop::EMPTY_DROP_INFO, crate::empty_enum::EMPTY_ENUM_INFO, crate::empty_structs_with_brackets::EMPTY_STRUCTS_WITH_BRACKETS_INFO, + crate::endian_bytes::BIG_ENDIAN_BYTES_INFO, + crate::endian_bytes::HOST_ENDIAN_BYTES_INFO, + crate::endian_bytes::LITTLE_ENDIAN_BYTES_INFO, crate::entry::MAP_ENTRY_INFO, crate::enum_clike::ENUM_CLIKE_UNPORTABLE_VARIANT_INFO, crate::enum_variants::ENUM_VARIANT_NAMES_INFO, @@ -152,6 +160,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::eta_reduction::REDUNDANT_CLOSURE_FOR_METHOD_CALLS_INFO, crate::excessive_bools::FN_PARAMS_EXCESSIVE_BOOLS_INFO, crate::excessive_bools::STRUCT_EXCESSIVE_BOOLS_INFO, + crate::excessive_nesting::EXCESSIVE_NESTING_INFO, crate::exhaustive_items::EXHAUSTIVE_ENUMS_INFO, crate::exhaustive_items::EXHAUSTIVE_STRUCTS_INFO, crate::exit::EXIT_INFO, @@ -197,6 +206,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::implicit_saturating_add::IMPLICIT_SATURATING_ADD_INFO, crate::implicit_saturating_sub::IMPLICIT_SATURATING_SUB_INFO, crate::inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR_INFO, + crate::incorrect_impls::INCORRECT_CLONE_IMPL_ON_COPY_TYPE_INFO, crate::index_refutable_slice::INDEX_REFUTABLE_SLICE_INFO, crate::indexing_slicing::INDEXING_SLICING_INFO, crate::indexing_slicing::OUT_OF_BOUNDS_INDEXING_INFO, @@ -219,6 +229,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::large_futures::LARGE_FUTURES_INFO, crate::large_include_file::LARGE_INCLUDE_FILE_INFO, crate::large_stack_arrays::LARGE_STACK_ARRAYS_INFO, + crate::large_stack_frames::LARGE_STACK_FRAMES_INFO, crate::len_zero::COMPARISON_TO_EMPTY_INFO, crate::len_zero::LEN_WITHOUT_IS_EMPTY_INFO, crate::len_zero::LEN_ZERO_INFO, @@ -266,6 +277,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::manual_let_else::MANUAL_LET_ELSE_INFO, crate::manual_main_separator_str::MANUAL_MAIN_SEPARATOR_STR_INFO, crate::manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE_INFO, + crate::manual_range_patterns::MANUAL_RANGE_PATTERNS_INFO, crate::manual_rem_euclid::MANUAL_REM_EUCLID_INFO, crate::manual_retain::MANUAL_RETAIN_INFO, crate::manual_slice_size_calculation::MANUAL_SLICE_SIZE_CALCULATION_INFO, @@ -299,7 +311,6 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::matches::TRY_ERR_INFO, crate::matches::WILDCARD_ENUM_MATCH_ARM_INFO, crate::matches::WILDCARD_IN_OR_PATTERNS_INFO, - crate::mem_forget::MEM_FORGET_INFO, crate::mem_replace::MEM_REPLACE_OPTION_WITH_NONE_INFO, crate::mem_replace::MEM_REPLACE_WITH_DEFAULT_INFO, crate::mem_replace::MEM_REPLACE_WITH_UNINIT_INFO, @@ -314,6 +325,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::CLONE_ON_COPY_INFO, crate::methods::CLONE_ON_REF_PTR_INFO, crate::methods::COLLAPSIBLE_STR_REPLACE_INFO, + crate::methods::DRAIN_COLLECT_INFO, crate::methods::ERR_EXPECT_INFO, crate::methods::EXPECT_FUN_CALL_INFO, crate::methods::EXPECT_USED_INFO, @@ -352,6 +364,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::MANUAL_SATURATING_ARITHMETIC_INFO, crate::methods::MANUAL_SPLIT_ONCE_INFO, crate::methods::MANUAL_STR_REPEAT_INFO, + crate::methods::MANUAL_TRY_FOLD_INFO, crate::methods::MAP_CLONE_INFO, crate::methods::MAP_COLLECT_RESULT_UNIT_INFO, crate::methods::MAP_ERR_IGNORE_INFO, @@ -398,6 +411,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::UNNECESSARY_FOLD_INFO, crate::methods::UNNECESSARY_JOIN_INFO, crate::methods::UNNECESSARY_LAZY_EVALUATIONS_INFO, + 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, @@ -407,6 +421,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::methods::VERBOSE_FILE_READS_INFO, crate::methods::WRONG_SELF_CONVENTION_INFO, crate::methods::ZST_OFFSET_INFO, + crate::min_ident_chars::MIN_IDENT_CHARS_INFO, crate::minmax::MIN_MAX_INFO, crate::misc::SHORT_CIRCUIT_STATEMENT_INFO, crate::misc::TOPLEVEL_REF_ARG_INFO, @@ -416,6 +431,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::misc_early::DOUBLE_NEG_INFO, crate::misc_early::DUPLICATE_UNDERSCORE_ARGUMENT_INFO, crate::misc_early::MIXED_CASE_HEX_LITERALS_INFO, + crate::misc_early::REDUNDANT_AT_REST_PATTERN_INFO, crate::misc_early::REDUNDANT_PATTERN_INFO, crate::misc_early::SEPARATED_LITERAL_SUFFIX_INFO, crate::misc_early::UNNEEDED_FIELD_PATTERN_INFO, @@ -450,6 +466,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::needless_continue::NEEDLESS_CONTINUE_INFO, crate::needless_else::NEEDLESS_ELSE_INFO, crate::needless_for_each::NEEDLESS_FOR_EACH_INFO, + crate::needless_if::NEEDLESS_IF_INFO, crate::needless_late_init::NEEDLESS_LATE_INIT_INFO, crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO, crate::needless_pass_by_value::NEEDLESS_PASS_BY_VALUE_INFO, @@ -524,6 +541,8 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::ranges::RANGE_MINUS_ONE_INFO, crate::ranges::RANGE_PLUS_ONE_INFO, crate::ranges::REVERSED_EMPTY_RANGES_INFO, + crate::raw_strings::NEEDLESS_RAW_STRINGS_INFO, + crate::raw_strings::NEEDLESS_RAW_STRING_HASHES_INFO, crate::rc_clone_in_vec_init::RC_CLONE_IN_VEC_INIT_INFO, crate::read_zero_byte_vec::READ_ZERO_BYTE_VEC_INFO, crate::redundant_async_block::REDUNDANT_ASYNC_BLOCK_INFO, @@ -535,6 +554,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::redundant_slicing::DEREF_BY_SLICING_INFO, crate::redundant_slicing::REDUNDANT_SLICING_INFO, crate::redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES_INFO, + crate::redundant_type_annotations::REDUNDANT_TYPE_ANNOTATIONS_INFO, crate::ref_option_ref::REF_OPTION_REF_INFO, crate::ref_patterns::REF_PATTERNS_INFO, crate::reference::DEREF_ADDROF_INFO, @@ -553,8 +573,10 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::shadow::SHADOW_SAME_INFO, crate::shadow::SHADOW_UNRELATED_INFO, crate::significant_drop_tightening::SIGNIFICANT_DROP_TIGHTENING_INFO, + crate::single_call_fn::SINGLE_CALL_FN_INFO, crate::single_char_lifetime_names::SINGLE_CHAR_LIFETIME_NAMES_INFO, crate::single_component_path_imports::SINGLE_COMPONENT_PATH_IMPORTS_INFO, + crate::single_range_in_vec_init::SINGLE_RANGE_IN_VEC_INIT_INFO, crate::size_of_in_element_count::SIZE_OF_IN_ELEMENT_COUNT_INFO, crate::size_of_ref::SIZE_OF_REF_INFO, crate::slow_vector_initialization::SLOW_VECTOR_INITIALIZATION_INFO, @@ -602,6 +624,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::transmute::UNSOUND_COLLECTION_TRANSMUTE_INFO, crate::transmute::USELESS_TRANSMUTE_INFO, crate::transmute::WRONG_TRANSMUTE_INFO, + crate::tuple_array_conversions::TUPLE_ARRAY_CONVERSIONS_INFO, crate::types::BORROWED_BOX_INFO, crate::types::BOX_COLLECTION_INFO, crate::types::LINKEDLIST_INFO, @@ -644,6 +667,9 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::useless_conversion::USELESS_CONVERSION_INFO, crate::vec::USELESS_VEC_INFO, crate::vec_init_then_push::VEC_INIT_THEN_PUSH_INFO, + crate::visibility::NEEDLESS_PUB_SELF_INFO, + crate::visibility::PUB_WITHOUT_SHORTHAND_INFO, + crate::visibility::PUB_WITH_SHORTHAND_INFO, crate::wildcard_imports::ENUM_GLOB_USE_INFO, crate::wildcard_imports::WILDCARD_IMPORTS_INFO, crate::write::PRINTLN_EMPTY_STRING_INFO, diff --git a/clippy_lints/src/default_constructed_unit_structs.rs b/clippy_lints/src/default_constructed_unit_structs.rs index fb037bbcbf3..ca9514ccc7d 100644 --- a/clippy_lints/src/default_constructed_unit_structs.rs +++ b/clippy_lints/src/default_constructed_unit_structs.rs @@ -8,7 +8,7 @@ use rustc_session::{declare_lint_pass, declare_tool_lint}; declare_clippy_lint! { /// ### What it does - /// Check for construction on unit struct using `default`. + /// Checks for construction on unit struct using `default`. /// /// ### Why is this bad? /// This adds code complexity and an unnecessary function call. diff --git a/clippy_lints/src/default_numeric_fallback.rs b/clippy_lints/src/default_numeric_fallback.rs index 4e1a6cd4d73..e53a9877b20 100644 --- a/clippy_lints/src/default_numeric_fallback.rs +++ b/clippy_lints/src/default_numeric_fallback.rs @@ -161,7 +161,7 @@ impl<'a, 'tcx> Visitor<'tcx> for NumericFallbackVisitor<'a, 'tcx> { let fields_def = &variant.fields; // Push field type then visit each field expr. - for field in fields.iter() { + for field in *fields { let bound = fields_def .iter() diff --git a/clippy_lints/src/dereference.rs b/clippy_lints/src/dereference.rs index 1ecaa1a123c..73556c1053e 100644 --- a/clippy_lints/src/dereference.rs +++ b/clippy_lints/src/dereference.rs @@ -56,9 +56,11 @@ declare_clippy_lint! { /// let b = &*a; /// ``` /// - /// This lint excludes: + /// This lint excludes all of: /// ```rust,ignore /// let _ = d.unwrap().deref(); + /// let _ = Foo::deref(&foo); + /// let _ = <Foo as Deref>::deref(&foo); /// ``` #[clippy::version = "1.44.0"] pub EXPLICIT_DEREF_METHODS, @@ -355,15 +357,17 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> { // start auto-deref. // 4. If the chain of non-user-defined derefs ends with a mutable re-borrow, and re-borrow // adjustments will not be inserted automatically, then leave one further reference to avoid - // moving a mutable borrow. - // e.g. - // fn foo<T>(x: &mut Option<&mut T>, y: &mut T) { - // let x = match x { - // // Removing the borrow will cause `x` to be moved - // Some(x) => &mut *x, - // None => y - // }; - // } + // moving a mutable borrow. e.g. + // + // ```rust + // fn foo<T>(x: &mut Option<&mut T>, y: &mut T) { + // let x = match x { + // // Removing the borrow will cause `x` to be moved + // Some(x) => &mut *x, + // None => y + // }; + // } + // ``` 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"; @@ -1481,7 +1485,7 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data target_mut, } => { let mut app = Applicability::MachineApplicable; - let (expr_str, expr_is_macro_call) = snippet_with_context(cx, expr.span, data.span.ctxt(), "..", &mut app); + let (expr_str, _expr_is_macro_call) = snippet_with_context(cx, expr.span, data.span.ctxt(), "..", &mut app); let ty = cx.typeck_results().expr_ty(expr); let (_, ref_count) = peel_mid_ty_refs(ty); let deref_str = if ty_changed_count >= ref_count && ref_count != 0 { @@ -1504,11 +1508,20 @@ fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data "&" }; - let expr_str = if !expr_is_macro_call && is_final_ufcs && expr.precedence().order() < PREC_PREFIX { - format!("({expr_str})") + // expr_str (the suggestion) is never shown if is_final_ufcs is true, since it's + // `expr.kind == ExprKind::Call`. Therefore, this is, afaik, always unnecessary. + /* + expr_str = if !expr_is_macro_call && is_final_ufcs && expr.precedence().order() < PREC_PREFIX { + Cow::Owned(format!("({expr_str})")) } else { - expr_str.into_owned() + expr_str }; + */ + + // Fix #10850, do not lint if it's `Foo::deref` instead of `foo.deref()`. + if is_final_ufcs { + return; + } span_lint_and_sugg( cx, diff --git a/clippy_lints/src/derivable_impls.rs b/clippy_lints/src/derivable_impls.rs index 8f68f90a2a1..ec0ca50cfec 100644 --- a/clippy_lints/src/derivable_impls.rs +++ b/clippy_lints/src/derivable_impls.rs @@ -4,11 +4,13 @@ use clippy_utils::source::indent_of; use clippy_utils::{is_default_equivalent, peel_blocks}; use rustc_errors::Applicability; use rustc_hir::{ + self as hir, def::{CtorKind, CtorOf, DefKind, Res}, - Body, Expr, ExprKind, GenericArg, Impl, ImplItemKind, Item, ItemKind, Node, PathSegment, QPath, Ty, TyKind, + Body, Expr, ExprKind, GenericArg, Impl, ImplItemKind, Item, ItemKind, Node, PathSegment, QPath, TyKind, }; use rustc_lint::{LateContext, LateLintPass}; -use rustc_middle::ty::{Adt, AdtDef, SubstsRef}; +use rustc_middle::ty::adjustment::{Adjust, PointerCast}; +use rustc_middle::ty::{self, Adt, AdtDef, SubstsRef, Ty, TypeckResults}; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::sym; @@ -75,13 +77,23 @@ fn is_path_self(e: &Expr<'_>) -> bool { } } +fn contains_trait_object(ty: Ty<'_>) -> bool { + match ty.kind() { + ty::Ref(_, ty, _) => contains_trait_object(*ty), + ty::Adt(def, substs) => def.is_box() && substs[0].as_type().map_or(false, contains_trait_object), + ty::Dynamic(..) => true, + _ => false, + } +} + fn check_struct<'tcx>( cx: &LateContext<'tcx>, item: &'tcx Item<'_>, - self_ty: &Ty<'_>, + self_ty: &hir::Ty<'_>, func_expr: &Expr<'_>, adt_def: AdtDef<'_>, substs: SubstsRef<'_>, + typeck_results: &'tcx TypeckResults<'tcx>, ) { if let TyKind::Path(QPath::Resolved(_, p)) = self_ty.kind { if let Some(PathSegment { args, .. }) = p.segments.last() { @@ -96,10 +108,23 @@ fn check_struct<'tcx>( } } } + + // the default() call might unsize coerce to a trait object (e.g. Box<T> to Box<dyn Trait>), + // which would not be the same if derived (see #10158). + // this closure checks both if the expr is equivalent to a `default()` call and does not + // have such coercions. + let is_default_without_adjusts = |expr| { + is_default_equivalent(cx, expr) + && typeck_results.expr_adjustments(expr).iter().all(|adj| { + !matches!(adj.kind, Adjust::Pointer(PointerCast::Unsize) + if contains_trait_object(adj.target)) + }) + }; + let should_emit = match peel_blocks(func_expr).kind { - ExprKind::Tup(fields) => fields.iter().all(|e| is_default_equivalent(cx, e)), - ExprKind::Call(callee, args) if is_path_self(callee) => args.iter().all(|e| is_default_equivalent(cx, e)), - ExprKind::Struct(_, fields, _) => fields.iter().all(|ef| is_default_equivalent(cx, ef.expr)), + ExprKind::Tup(fields) => fields.iter().all(is_default_without_adjusts), + ExprKind::Call(callee, args) if is_path_self(callee) => args.iter().all(is_default_without_adjusts), + ExprKind::Struct(_, fields, _) => fields.iter().all(|ef| is_default_without_adjusts(ef.expr)), _ => false, }; @@ -197,7 +222,7 @@ impl<'tcx> LateLintPass<'tcx> for DerivableImpls { then { if adt_def.is_struct() { - check_struct(cx, item, self_ty, func_expr, adt_def, substs); + check_struct(cx, item, self_ty, func_expr, adt_def, substs, cx.tcx.typeck_body(*b)); } else if adt_def.is_enum() && self.msrv.meets(msrvs::DEFAULT_ENUM_ATTRIBUTE) { check_enum(cx, item, func_expr, adt_def); } diff --git a/clippy_lints/src/derive.rs b/clippy_lints/src/derive.rs index 10a2f0cb1e7..a005a360e9c 100644 --- a/clippy_lints/src/derive.rs +++ b/clippy_lints/src/derive.rs @@ -14,8 +14,8 @@ 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; @@ -520,7 +520,8 @@ fn param_env_for_derived_eq(tcx: TyCtxt<'_>, did: DefId, eq_trait_id: DefId) -> trait_ref: ty::TraitRef::new(tcx, eq_trait_id, [tcx.mk_param_from_def(param)]), constness: BoundConstness::NotConst, polarity: ImplPolarity::Positive, - }).to_predicate(tcx) + }) + .to_predicate(tcx) }), )), Reveal::UserFacing, diff --git a/clippy_lints/src/doc.rs b/clippy_lints/src/doc.rs index 384aca7fead..87d88f70752 100644 --- a/clippy_lints/src/doc.rs +++ b/clippy_lints/src/doc.rs @@ -571,6 +571,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize let mut in_link = None; let mut in_heading = false; let mut is_rust = false; + let mut no_test = false; let mut edition = None; let mut ticks_unbalanced = false; let mut text_to_check: Vec<(CowStr<'_>, Span)> = Vec::new(); @@ -584,6 +585,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize if item == "ignore" { is_rust = false; break; + } else if item == "no_test" { + no_test = true; } if let Some(stripped) = item.strip_prefix("edition") { is_rust = true; @@ -648,7 +651,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize headers.errors |= in_heading && trimmed_text == "Errors"; headers.panics |= in_heading && trimmed_text == "Panics"; if in_code { - if is_rust { + if is_rust && !no_test { let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition()); check_code(cx, &text, edition, span); } @@ -906,15 +909,15 @@ impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> { if is_panic(self.cx, macro_call.def_id) || matches!( self.cx.tcx.item_name(macro_call.def_id).as_str(), - "assert" | "assert_eq" | "assert_ne" | "todo" + "assert" | "assert_eq" | "assert_ne" ) { self.panic_span = Some(macro_call.span); } } - // check for `unwrap` - if let Some(arglists) = method_chain_args(expr, &["unwrap"]) { + // check for `unwrap` and `expect` for both `Option` and `Result` + if let Some(arglists) = method_chain_args(expr, &["unwrap"]).or(method_chain_args(expr, &["expect"])) { let receiver_ty = self.typeck_results.expr_ty(arglists[0].0).peel_refs(); if is_type_diagnostic_item(self.cx, receiver_ty, sym::Option) || is_type_diagnostic_item(self.cx, receiver_ty, sym::Result) diff --git a/clippy_lints/src/drop_forget_ref.rs b/clippy_lints/src/drop_forget_ref.rs index 7a4b9a87aeb..976ce47e869 100644 --- a/clippy_lints/src/drop_forget_ref.rs +++ b/clippy_lints/src/drop_forget_ref.rs @@ -6,6 +6,7 @@ use rustc_hir::{Arm, Expr, ExprKind, LangItem, Node}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::sym; +use std::borrow::Cow; declare_clippy_lint! { /// ### What it does @@ -47,6 +48,27 @@ declare_clippy_lint! { "call to `std::mem::forget` with a value which does not implement `Drop`" } +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `std::mem::forget(t)` where `t` is + /// `Drop` or has a field that implements `Drop`. + /// + /// ### Why is this bad? + /// `std::mem::forget(t)` prevents `t` from running its + /// destructor, possibly causing leaks. + /// + /// ### Example + /// ```rust + /// # use std::mem; + /// # use std::rc::Rc; + /// mem::forget(Rc::new(55)) + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MEM_FORGET, + restriction, + "`mem::forget` usage on `Drop` types, likely to cause memory leaks" +} + const DROP_NON_DROP_SUMMARY: &str = "call to `std::mem::drop` with a value that does not implement `Drop`. \ Dropping such a type only extends its contained lifetimes"; const FORGET_NON_DROP_SUMMARY: &str = "call to `std::mem::forget` with a value that does not implement `Drop`. \ @@ -55,6 +77,7 @@ const FORGET_NON_DROP_SUMMARY: &str = "call to `std::mem::forget` with a value t declare_lint_pass!(DropForgetRef => [ DROP_NON_DROP, FORGET_NON_DROP, + MEM_FORGET, ]); impl<'tcx> LateLintPass<'tcx> for DropForgetRef { @@ -67,7 +90,7 @@ impl<'tcx> LateLintPass<'tcx> for DropForgetRef { let arg_ty = cx.typeck_results().expr_ty(arg); let is_copy = is_copy(cx, arg_ty); let drop_is_single_call_in_arm = is_single_call_in_arm(cx, arg, expr); - let (lint, msg) = match fn_name { + let (lint, msg, note_span) = match fn_name { // early return for uplifted lints: dropping_references, dropping_copy_types, forgetting_references, forgetting_copy_types sym::mem_drop if arg_ty.is_ref() && !drop_is_single_call_in_arm => return, sym::mem_forget if arg_ty.is_ref() => return, @@ -81,19 +104,34 @@ impl<'tcx> LateLintPass<'tcx> for DropForgetRef { || drop_is_single_call_in_arm ) => { - (DROP_NON_DROP, DROP_NON_DROP_SUMMARY) - }, - sym::mem_forget if !arg_ty.needs_drop(cx.tcx, cx.param_env) => { - (FORGET_NON_DROP, FORGET_NON_DROP_SUMMARY) + (DROP_NON_DROP, DROP_NON_DROP_SUMMARY.into(), Some(arg.span)) }, + sym::mem_forget => { + if arg_ty.needs_drop(cx.tcx, cx.param_env) { + ( + MEM_FORGET, + Cow::Owned(format!( + "usage of `mem::forget` on {}", + if arg_ty.ty_adt_def().map_or(false, |def| def.has_dtor(cx.tcx)) { + "`Drop` type" + } else { + "type with `Drop` fields" + } + )), + None, + ) + } else { + (FORGET_NON_DROP, FORGET_NON_DROP_SUMMARY.into(), Some(arg.span)) + } + } _ => return, }; span_lint_and_note( cx, lint, expr.span, - msg, - Some(arg.span), + &msg, + note_span, &format!("argument has type `{arg_ty}`"), ); } diff --git a/clippy_lints/src/endian_bytes.rs b/clippy_lints/src/endian_bytes.rs new file mode 100644 index 00000000000..f470987833e --- /dev/null +++ b/clippy_lints/src/endian_bytes.rs @@ -0,0 +1,216 @@ +use crate::Lint; +use clippy_utils::{diagnostics::span_lint_and_then, is_lint_allowed}; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::{lint::in_external_macro, ty::Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Symbol; +use std::borrow::Cow; + +declare_clippy_lint! { + /// ### What it does + /// Checks for the usage of the `to_ne_bytes` method and/or the function `from_ne_bytes`. + /// + /// ### Why is this bad? + /// It's not, but some may prefer to specify the target endianness explicitly. + /// + /// ### Example + /// ```rust,ignore + /// let _x = 2i32.to_ne_bytes(); + /// let _y = 2i64.to_ne_bytes(); + /// ``` + #[clippy::version = "1.71.0"] + pub HOST_ENDIAN_BYTES, + restriction, + "disallows usage of the `to_ne_bytes` method" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the usage of the `to_le_bytes` method and/or the function `from_le_bytes`. + /// + /// ### Why is this bad? + /// It's not, but some may wish to lint usage of this method, either to suggest using the host + /// endianness or big endian. + /// + /// ### Example + /// ```rust,ignore + /// let _x = 2i32.to_le_bytes(); + /// let _y = 2i64.to_le_bytes(); + /// ``` + #[clippy::version = "1.71.0"] + pub LITTLE_ENDIAN_BYTES, + restriction, + "disallows usage of the `to_le_bytes` method" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the usage of the `to_be_bytes` method and/or the function `from_be_bytes`. + /// + /// ### Why is this bad? + /// It's not, but some may wish to lint usage of this method, either to suggest using the host + /// endianness or little endian. + /// + /// ### Example + /// ```rust,ignore + /// let _x = 2i32.to_be_bytes(); + /// let _y = 2i64.to_be_bytes(); + /// ``` + #[clippy::version = "1.71.0"] + pub BIG_ENDIAN_BYTES, + restriction, + "disallows usage of the `to_be_bytes` method" +} + +declare_lint_pass!(EndianBytes => [HOST_ENDIAN_BYTES, LITTLE_ENDIAN_BYTES, BIG_ENDIAN_BYTES]); + +const HOST_NAMES: [&str; 2] = ["from_ne_bytes", "to_ne_bytes"]; +const LITTLE_NAMES: [&str; 2] = ["from_le_bytes", "to_le_bytes"]; +const BIG_NAMES: [&str; 2] = ["from_be_bytes", "to_be_bytes"]; + +#[derive(Clone, Debug)] +enum LintKind { + Host, + Little, + Big, +} + +#[derive(Clone, Copy, PartialEq)] +enum Prefix { + From, + To, +} + +impl LintKind { + fn allowed(&self, cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + is_lint_allowed(cx, self.as_lint(), expr.hir_id) + } + + fn as_lint(&self) -> &'static Lint { + match self { + LintKind::Host => HOST_ENDIAN_BYTES, + LintKind::Little => LITTLE_ENDIAN_BYTES, + LintKind::Big => BIG_ENDIAN_BYTES, + } + } + + fn as_name(&self, prefix: Prefix) -> &str { + let index = usize::from(prefix == Prefix::To); + + match self { + LintKind::Host => HOST_NAMES[index], + LintKind::Little => LITTLE_NAMES[index], + LintKind::Big => BIG_NAMES[index], + } + } +} + +impl LateLintPass<'_> for EndianBytes { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if_chain! { + if let ExprKind::MethodCall(method_name, receiver, args, ..) = expr.kind; + if args.is_empty(); + let ty = cx.typeck_results().expr_ty(receiver); + if ty.is_primitive_ty(); + if maybe_lint_endian_bytes(cx, expr, Prefix::To, method_name.ident.name, ty); + then { + return; + } + } + + if_chain! { + if let ExprKind::Call(function, ..) = expr.kind; + if let ExprKind::Path(qpath) = function.kind; + if let Some(def_id) = cx.qpath_res(&qpath, function.hir_id).opt_def_id(); + if let Some(function_name) = cx.get_def_path(def_id).last(); + let ty = cx.typeck_results().expr_ty(expr); + if ty.is_primitive_ty(); + then { + maybe_lint_endian_bytes(cx, expr, Prefix::From, *function_name, ty); + } + } + } +} + +fn maybe_lint_endian_bytes(cx: &LateContext<'_>, expr: &Expr<'_>, prefix: Prefix, name: Symbol, ty: Ty<'_>) -> bool { + let ne = LintKind::Host.as_name(prefix); + let le = LintKind::Little.as_name(prefix); + let be = LintKind::Big.as_name(prefix); + + let (lint, other_lints) = match name.as_str() { + name if name == ne => ((&LintKind::Host), [(&LintKind::Little), (&LintKind::Big)]), + name if name == le => ((&LintKind::Little), [(&LintKind::Host), (&LintKind::Big)]), + name if name == be => ((&LintKind::Big), [(&LintKind::Host), (&LintKind::Little)]), + _ => return false, + }; + + let mut help = None; + + 'build_help: { + // all lints disallowed, don't give help here + if [&[lint], other_lints.as_slice()] + .concat() + .iter() + .all(|lint| !lint.allowed(cx, expr)) + { + break 'build_help; + } + + // ne_bytes and all other lints allowed + if lint.as_name(prefix) == ne && other_lints.iter().all(|lint| lint.allowed(cx, expr)) { + help = Some(Cow::Borrowed("specify the desired endianness explicitly")); + break 'build_help; + } + + // le_bytes where ne_bytes allowed but be_bytes is not, or le_bytes where ne_bytes allowed but + // le_bytes is not + if (lint.as_name(prefix) == le || lint.as_name(prefix) == be) && LintKind::Host.allowed(cx, expr) { + help = Some(Cow::Borrowed("use the native endianness instead")); + break 'build_help; + } + + let allowed_lints = other_lints.iter().filter(|lint| lint.allowed(cx, expr)); + let len = allowed_lints.clone().count(); + + let mut help_str = "use ".to_owned(); + + for (i, lint) in allowed_lints.enumerate() { + let only_one = len == 1; + if !only_one { + help_str.push_str("either of "); + } + + help_str.push_str(&format!("`{ty}::{}` ", lint.as_name(prefix))); + + if i != len && !only_one { + help_str.push_str("or "); + } + } + + help = Some(Cow::Owned(help_str + "instead")); + } + + span_lint_and_then( + cx, + lint.as_lint(), + expr.span, + &format!( + "usage of the {}`{ty}::{}`{}", + if prefix == Prefix::From { "function " } else { "" }, + lint.as_name(prefix), + if prefix == Prefix::To { " method" } else { "" }, + ), + move |diag| { + if let Some(help) = help { + diag.help(help); + } + }, + ); + + true +} diff --git a/clippy_lints/src/enum_clike.rs b/clippy_lints/src/enum_clike.rs index e275efaba25..d85650712db 100644 --- a/clippy_lints/src/enum_clike.rs +++ b/clippy_lints/src/enum_clike.rs @@ -51,7 +51,7 @@ impl<'tcx> LateLintPass<'tcx> for UnportableVariant { .const_eval_poly(def_id.to_def_id()) .ok() .map(|val| rustc_middle::mir::ConstantKind::from_value(val, ty)); - if let Some(Constant::Int(val)) = constant.and_then(|c| miri_to_const(cx.tcx, c)) { + if let Some(Constant::Int(val)) = constant.and_then(|c| miri_to_const(cx, c)) { if let ty::Adt(adt, _) = ty.kind() { if adt.is_enum() { ty = adt.repr().discr_type().to_ty(cx.tcx); diff --git a/clippy_lints/src/enum_variants.rs b/clippy_lints/src/enum_variants.rs index faac6340419..d4df6f7aa2d 100644 --- a/clippy_lints/src/enum_variants.rs +++ b/clippy_lints/src/enum_variants.rs @@ -3,7 +3,7 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_hir}; use clippy_utils::source::is_present_in_source; use clippy_utils::str_utils::{camel_case_split, count_match_end, count_match_start}; -use rustc_hir::{EnumDef, Item, ItemKind, Variant}; +use rustc_hir::{EnumDef, Item, ItemKind, OwnerId, Variant}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::source_map::Span; @@ -105,18 +105,20 @@ declare_clippy_lint! { } pub struct EnumVariantNames { - modules: Vec<(Symbol, String)>, + modules: Vec<(Symbol, String, OwnerId)>, threshold: u64, avoid_breaking_exported_api: bool, + allow_private_module_inception: bool, } impl EnumVariantNames { #[must_use] - pub fn new(threshold: u64, avoid_breaking_exported_api: bool) -> Self { + pub fn new(threshold: u64, avoid_breaking_exported_api: bool, allow_private_module_inception: bool) -> Self { Self { modules: Vec::new(), threshold, avoid_breaking_exported_api, + allow_private_module_inception, } } } @@ -252,18 +254,19 @@ impl LateLintPass<'_> for EnumVariantNames { let item_name = item.ident.name.as_str(); let item_camel = to_camel_case(item_name); if !item.span.from_expansion() && is_present_in_source(cx, item.span) { - if let Some((mod_name, mod_camel)) = self.modules.last() { + if let [.., (mod_name, mod_camel, owner_id)] = &*self.modules { // constants don't have surrounding modules if !mod_camel.is_empty() { - if mod_name == &item.ident.name { - if let ItemKind::Mod(..) = item.kind { - span_lint( - cx, - MODULE_INCEPTION, - item.span, - "module has the same name as its containing module", - ); - } + if mod_name == &item.ident.name + && let ItemKind::Mod(..) = item.kind + && (!self.allow_private_module_inception || cx.tcx.visibility(owner_id.def_id).is_public()) + { + span_lint( + cx, + MODULE_INCEPTION, + item.span, + "module has the same name as its containing module", + ); } // The `module_name_repetitions` lint should only trigger if the item has the module in its // name. Having the same name is accepted. @@ -302,6 +305,6 @@ impl LateLintPass<'_> for EnumVariantNames { check_variant(cx, self.threshold, def, item_name, item.span); } } - self.modules.push((item.ident.name, item_camel)); + self.modules.push((item.ident.name, item_camel, item.owner_id)); } } diff --git a/clippy_lints/src/eta_reduction.rs b/clippy_lints/src/eta_reduction.rs index c919b4de65d..58e62d1f3d3 100644 --- a/clippy_lints/src/eta_reduction.rs +++ b/clippy_lints/src/eta_reduction.rs @@ -120,6 +120,13 @@ impl<'tcx> LateLintPass<'tcx> for EtaReduction { 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(_, substs) = *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) { @@ -136,6 +143,7 @@ impl<'tcx> LateLintPass<'tcx> for EtaReduction { // 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", diff --git a/clippy_lints/src/excessive_nesting.rs b/clippy_lints/src/excessive_nesting.rs new file mode 100644 index 00000000000..d04d833e630 --- /dev/null +++ b/clippy_lints/src/excessive_nesting.rs @@ -0,0 +1,181 @@ +use clippy_utils::{diagnostics::span_lint_and_help, source::snippet}; +use rustc_ast::{ + node_id::NodeSet, + visit::{walk_block, walk_item, Visitor}, + Block, Crate, Inline, Item, ItemKind, ModKind, NodeId, +}; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for blocks which are nested beyond a certain threshold. + /// + /// Note: Even though this lint is warn-by-default, it will only trigger if a maximum nesting level is defined in the clippy.toml file. + /// + /// ### Why is this bad? + /// It can severely hinder readability. + /// + /// ### Example + /// An example clippy.toml configuration: + /// ```toml + /// # clippy.toml + /// excessive-nesting-threshold = 3 + /// ``` + /// ```rust,ignore + /// // lib.rs + /// pub mod a { + /// pub struct X; + /// impl X { + /// pub fn run(&self) { + /// if true { + /// // etc... + /// } + /// } + /// } + /// } + /// ``` + /// Use instead: + /// ```rust,ignore + /// // a.rs + /// fn private_run(x: &X) { + /// if true { + /// // etc... + /// } + /// } + /// + /// pub struct X; + /// impl X { + /// pub fn run(&self) { + /// private_run(self); + /// } + /// } + /// ``` + /// ```rust,ignore + /// // lib.rs + /// pub mod a; + /// ``` + #[clippy::version = "1.70.0"] + pub EXCESSIVE_NESTING, + complexity, + "checks for blocks nested beyond a certain threshold" +} +impl_lint_pass!(ExcessiveNesting => [EXCESSIVE_NESTING]); + +#[derive(Clone)] +pub struct ExcessiveNesting { + pub excessive_nesting_threshold: u64, + pub nodes: NodeSet, +} + +impl ExcessiveNesting { + pub fn check_node_id(&self, cx: &EarlyContext<'_>, span: Span, node_id: NodeId) { + if self.nodes.contains(&node_id) { + span_lint_and_help( + cx, + EXCESSIVE_NESTING, + span, + "this block is too nested", + None, + "try refactoring your code to minimize nesting", + ); + } + } +} + +impl EarlyLintPass for ExcessiveNesting { + fn check_crate(&mut self, cx: &EarlyContext<'_>, krate: &Crate) { + if self.excessive_nesting_threshold == 0 { + return; + } + + let mut visitor = NestingVisitor { + conf: self, + cx, + nest_level: 0, + }; + + for item in &krate.items { + visitor.visit_item(item); + } + } + + fn check_block(&mut self, cx: &EarlyContext<'_>, block: &Block) { + self.check_node_id(cx, block.span, block.id); + } + + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + self.check_node_id(cx, item.span, item.id); + } +} + +struct NestingVisitor<'conf, 'cx> { + conf: &'conf mut ExcessiveNesting, + cx: &'cx EarlyContext<'cx>, + nest_level: u64, +} + +impl NestingVisitor<'_, '_> { + fn check_indent(&mut self, span: Span, id: NodeId) -> bool { + if self.nest_level > self.conf.excessive_nesting_threshold && !in_external_macro(self.cx.sess(), span) { + self.conf.nodes.insert(id); + + return true; + } + + false + } +} + +impl<'conf, 'cx> Visitor<'_> for NestingVisitor<'conf, 'cx> { + fn visit_block(&mut self, block: &Block) { + if block.span.from_expansion() { + return; + } + + // TODO: This should be rewritten using `LateLintPass` so we can use `is_from_proc_macro` instead, + // but for now, this is fine. + let snippet = snippet(self.cx, block.span, "{}").trim().to_owned(); + if !snippet.starts_with('{') || !snippet.ends_with('}') { + return; + } + + self.nest_level += 1; + + if !self.check_indent(block.span, block.id) { + walk_block(self, block); + } + + self.nest_level -= 1; + } + + fn visit_item(&mut self, item: &Item) { + if item.span.from_expansion() { + return; + } + + match &item.kind { + ItemKind::Trait(_) | ItemKind::Impl(_) | ItemKind::Mod(.., ModKind::Loaded(_, Inline::Yes, _)) => { + self.nest_level += 1; + + if !self.check_indent(item.span, item.id) { + walk_item(self, item); + } + + self.nest_level -= 1; + }, + // Reset nesting level for non-inline modules (since these are in another file) + ItemKind::Mod(..) => walk_item( + &mut NestingVisitor { + conf: self.conf, + cx: self.cx, + nest_level: 0, + }, + item, + ), + _ => walk_item(self, item), + } + } +} diff --git a/clippy_lints/src/extra_unused_type_parameters.rs b/clippy_lints/src/extra_unused_type_parameters.rs index eeb4de8b58f..126bed6789c 100644 --- a/clippy_lints/src/extra_unused_type_parameters.rs +++ b/clippy_lints/src/extra_unused_type_parameters.rs @@ -1,4 +1,5 @@ use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then}; +use clippy_utils::is_from_proc_macro; use clippy_utils::trait_ref_of_method; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_errors::Applicability; @@ -265,6 +266,7 @@ impl<'tcx> LateLintPass<'tcx> for ExtraUnusedTypeParameters { fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { if let ItemKind::Fn(_, generics, body_id) = item.kind && !self.is_empty_exported_or_macro(cx, item.span, item.owner_id.def_id, body_id) + && !is_from_proc_macro(cx, item) { let mut walker = TypeWalker::new(cx, generics); walk_item(&mut walker, item); diff --git a/clippy_lints/src/float_literal.rs b/clippy_lints/src/float_literal.rs index 93bf50fd5e7..d182bb62195 100644 --- a/clippy_lints/src/float_literal.rs +++ b/clippy_lints/src/float_literal.rs @@ -82,19 +82,24 @@ impl<'tcx> LateLintPass<'tcx> for FloatLiteral { LitFloatType::Suffixed(ast::FloatTy::F64) => Some("f64"), LitFloatType::Unsuffixed => None }; - let (is_whole, mut float_str) = match fty { + let (is_whole, is_inf, mut float_str) = match fty { FloatTy::F32 => { let value = sym_str.parse::<f32>().unwrap(); - (value.fract() == 0.0, formatter.format(value)) + (value.fract() == 0.0, value.is_infinite(), formatter.format(value)) }, FloatTy::F64 => { let value = sym_str.parse::<f64>().unwrap(); - (value.fract() == 0.0, formatter.format(value)) + + (value.fract() == 0.0, value.is_infinite(), formatter.format(value)) }, }; + if is_inf { + return; + } + if is_whole && !sym_str.contains(|c| c == 'e' || c == 'E') { // Normalize the literal by stripping the fractional portion if sym_str.split('.').next().unwrap() != float_str { diff --git a/clippy_lints/src/floating_point_arithmetic.rs b/clippy_lints/src/floating_point_arithmetic.rs index 3c55a563af4..5e0fcd74339 100644 --- a/clippy_lints/src/floating_point_arithmetic.rs +++ b/clippy_lints/src/floating_point_arithmetic.rs @@ -215,7 +215,7 @@ fn check_ln1p(cx: &LateContext<'_>, expr: &Expr<'_>, receiver: &Expr<'_>) { // ranges [-16777215, 16777216) for type f32 as whole number floats outside // this range are lossy and ambiguous. #[expect(clippy::cast_possible_truncation)] -fn get_integer_from_float_constant(value: &Constant) -> Option<i32> { +fn get_integer_from_float_constant(value: &Constant<'_>) -> Option<i32> { match value { F32(num) if num.fract() == 0.0 => { if (-16_777_215.0..16_777_216.0).contains(num) { diff --git a/clippy_lints/src/format_push_string.rs b/clippy_lints/src/format_push_string.rs index 68c5c3673fe..45f67020c2d 100644 --- a/clippy_lints/src/format_push_string.rs +++ b/clippy_lints/src/format_push_string.rs @@ -1,7 +1,7 @@ use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::ty::is_type_lang_item; -use clippy_utils::{match_def_path, paths, peel_hir_expr_refs}; -use rustc_hir::{BinOpKind, Expr, ExprKind, LangItem}; +use clippy_utils::{higher, match_def_path, paths}; +use rustc_hir::{BinOpKind, Expr, ExprKind, LangItem, MatchSource}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::sym; @@ -44,10 +44,24 @@ fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { is_type_lang_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), LangItem::String) } fn is_format(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { - if let Some(macro_def_id) = e.span.ctxt().outer_expn_data().macro_def_id { + let e = e.peel_blocks().peel_borrows(); + + if e.span.from_expansion() + && let Some(macro_def_id) = e.span.ctxt().outer_expn_data().macro_def_id + { cx.tcx.get_diagnostic_name(macro_def_id) == Some(sym::format_macro) + } else if let Some(higher::If { then, r#else, .. }) = higher::If::hir(e) { + is_format(cx, then) || r#else.is_some_and(|e| is_format(cx, e)) } else { - false + match higher::IfLetOrMatch::parse(cx, e) { + Some(higher::IfLetOrMatch::Match(_, arms, MatchSource::Normal)) => { + arms.iter().any(|arm| is_format(cx, arm.body)) + }, + Some(higher::IfLetOrMatch::IfLet(_, _, then, r#else)) => { + is_format(cx, then) ||r#else.is_some_and(|e| is_format(cx, e)) + }, + _ => false, + } } } @@ -68,7 +82,6 @@ impl<'tcx> LateLintPass<'tcx> for FormatPushString { }, _ => return, }; - let (arg, _) = peel_hir_expr_refs(arg); if is_format(cx, arg) { span_lint_and_help( cx, diff --git a/clippy_lints/src/formatting.rs b/clippy_lints/src/formatting.rs index 4762b354392..d03480c2108 100644 --- a/clippy_lints/src/formatting.rs +++ b/clippy_lints/src/formatting.rs @@ -236,6 +236,12 @@ fn check_else(cx: &EarlyContext<'_>, expr: &Expr) { } } + // Don't warn if the only thing inside post_else_post_eol is a comment block. + let trimmed_post_else_post_eol = post_else_post_eol.trim(); + if trimmed_post_else_post_eol.starts_with("/*") && trimmed_post_else_post_eol.ends_with("*/") { + return + } + let else_desc = if is_if(else_) { "if" } else { "{..}" }; span_lint_and_note( cx, diff --git a/clippy_lints/src/from_over_into.rs b/clippy_lints/src/from_over_into.rs index 10ce2a0f0c7..92d67ef359d 100644 --- a/clippy_lints/src/from_over_into.rs +++ b/clippy_lints/src/from_over_into.rs @@ -134,9 +134,10 @@ impl<'a, 'tcx> Visitor<'tcx> for SelfFinder<'a, 'tcx> { kw::SelfUpper => self.upper.push(segment.ident.span), _ => continue, } + + self.invalid |= segment.ident.span.from_expansion(); } - self.invalid |= path.span.from_expansion(); if !self.invalid { walk_path(self, path); } @@ -156,6 +157,11 @@ fn convert_to_from( self_ty: &Ty<'_>, impl_item_ref: &ImplItemRef, ) -> Option<Vec<(Span, String)>> { + if !target_ty.find_self_aliases().is_empty() { + // It's tricky to expand self-aliases correctly, we'll ignore it to not cause a + // bad suggestion/fix. + return None; + } let impl_item = cx.tcx.hir().impl_item(impl_item_ref.id); let ImplItemKind::Fn(ref sig, body_id) = impl_item.kind else { return None }; let body = cx.tcx.hir().body(body_id); diff --git a/clippy_lints/src/incorrect_impls.rs b/clippy_lints/src/incorrect_impls.rs new file mode 100644 index 00000000000..13cc0b23ba3 --- /dev/null +++ b/clippy_lints/src/incorrect_impls.rs @@ -0,0 +1,124 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, get_parent_node, last_path_segment, ty::implements_trait}; +use rustc_errors::Applicability; +use rustc_hir::{ExprKind, ImplItem, ImplItemKind, ItemKind, Node, UnOp}; +use rustc_hir_analysis::hir_ty_to_ty; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::EarlyBinder; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for manual implementations of `Clone` when `Copy` is already implemented. + /// + /// ### Why is this bad? + /// If both `Clone` and `Copy` are implemented, they must agree. This is done by dereferencing + /// `self` in `Clone`'s implementation. Anything else is incorrect. + /// + /// ### Example + /// ```rust,ignore + /// #[derive(Eq, PartialEq)] + /// struct A(u32); + /// + /// impl Clone for A { + /// fn clone(&self) -> Self { + /// Self(self.0) + /// } + /// } + /// + /// impl Copy for A {} + /// ``` + /// Use instead: + /// ```rust,ignore + /// #[derive(Eq, PartialEq)] + /// struct A(u32); + /// + /// impl Clone for A { + /// fn clone(&self) -> Self { + /// *self + /// } + /// } + /// + /// impl Copy for A {} + /// ``` + #[clippy::version = "1.72.0"] + pub INCORRECT_CLONE_IMPL_ON_COPY_TYPE, + correctness, + "manual implementation of `Clone` on a `Copy` type" +} +declare_lint_pass!(IncorrectImpls => [INCORRECT_CLONE_IMPL_ON_COPY_TYPE]); + +impl LateLintPass<'_> for IncorrectImpls { + #[expect(clippy::needless_return)] + fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) { + let node = get_parent_node(cx.tcx, impl_item.hir_id()); + let Some(Node::Item(item)) = node else { + return; + }; + let ItemKind::Impl(imp) = item.kind else { + return; + }; + let Some(trait_impl) = cx.tcx.impl_trait_ref(item.owner_id).map(EarlyBinder::skip_binder) else { + return; + }; + let trait_impl_def_id = trait_impl.def_id; + if cx.tcx.is_automatically_derived(item.owner_id.to_def_id()) { + return; + } + let ImplItemKind::Fn(_, impl_item_id) = cx.tcx.hir().impl_item(impl_item.impl_item_id()).kind else { + return; + }; + let body = cx.tcx.hir().body(impl_item_id); + let ExprKind::Block(block, ..) = body.value.kind else { + return; + }; + // Above is duplicated from the `duplicate_manual_partial_ord_impl` branch. + // Remove it while solving conflicts once that PR is merged. + + // Actual implementation; remove this comment once aforementioned PR is merged + if cx.tcx.is_diagnostic_item(sym::Clone, trait_impl_def_id) + && let Some(copy_def_id) = cx.tcx.get_diagnostic_item(sym::Copy) + && implements_trait( + cx, + hir_ty_to_ty(cx.tcx, imp.self_ty), + copy_def_id, + trait_impl.substs, + ) + { + if impl_item.ident.name == sym::clone { + if block.stmts.is_empty() + && let Some(expr) = block.expr + && let ExprKind::Unary(UnOp::Deref, inner) = expr.kind + && let ExprKind::Path(qpath) = inner.kind + && last_path_segment(&qpath).ident.name == symbol::kw::SelfLower + {} else { + span_lint_and_sugg( + cx, + INCORRECT_CLONE_IMPL_ON_COPY_TYPE, + block.span, + "incorrect implementation of `clone` on a `Copy` type", + "change this to", + "{ *self }".to_owned(), + Applicability::MaybeIncorrect, + ); + + return; + } + } + + if impl_item.ident.name == sym::clone_from { + span_lint_and_sugg( + cx, + INCORRECT_CLONE_IMPL_ON_COPY_TYPE, + impl_item.span, + "incorrect implementation of `clone_from` on a `Copy` type", + "remove this", + String::new(), + Applicability::MaybeIncorrect, + ); + + return; + } + } + } +} diff --git a/clippy_lints/src/items_after_test_module.rs b/clippy_lints/src/items_after_test_module.rs index b992d689aa9..40378ee8205 100644 --- a/clippy_lints/src/items_after_test_module.rs +++ b/clippy_lints/src/items_after_test_module.rs @@ -1,4 +1,4 @@ -use clippy_utils::{diagnostics::span_lint_and_help, is_in_cfg_test}; +use clippy_utils::{diagnostics::span_lint_and_help, is_from_proc_macro, is_in_cfg_test}; use rustc_hir::{HirId, ItemId, ItemKind, Mod}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; @@ -59,6 +59,7 @@ impl LateLintPass<'_> for ItemsAfterTestModule { if !matches!(item.kind, ItemKind::Mod(_)); if !is_in_cfg_test(cx.tcx, itid.hir_id()); // The item isn't in the testing module itself if !in_external_macro(cx.sess(), item.span); + if !is_from_proc_macro(cx, item); then { span_lint_and_help(cx, ITEMS_AFTER_TEST_MODULE, test_mod_span.unwrap().with_hi(item.span.hi()), "items were found after the testing module", None, "move the items to before the testing module was defined"); diff --git a/clippy_lints/src/large_stack_frames.rs b/clippy_lints/src/large_stack_frames.rs new file mode 100644 index 00000000000..9c0cc978a39 --- /dev/null +++ b/clippy_lints/src/large_stack_frames.rs @@ -0,0 +1,162 @@ +use std::ops::AddAssign; + +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::fn_has_unsatisfiable_preds; +use rustc_hir::def_id::LocalDefId; +use rustc_hir::intravisit::FnKind; +use rustc_hir::Body; +use rustc_hir::FnDecl; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::declare_tool_lint; +use rustc_session::impl_lint_pass; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions that use a lot of stack space. + /// + /// This often happens when constructing a large type, such as an array with a lot of elements, + /// or constructing *many* smaller-but-still-large structs, or copying around a lot of large types. + /// + /// This lint is a more general version of [`large_stack_arrays`](https://rust-lang.github.io/rust-clippy/master/#large_stack_arrays) + /// that is intended to look at functions as a whole instead of only individual array expressions inside of a function. + /// + /// ### Why is this bad? + /// The stack region of memory is very limited in size (usually *much* smaller than the heap) and attempting to + /// use too much will result in a stack overflow and crash the program. + /// To avoid this, you should consider allocating large types on the heap instead (e.g. by boxing them). + /// + /// Keep in mind that the code path to construction of large types does not even need to be reachable; + /// it purely needs to *exist* inside of the function to contribute to the stack size. + /// For example, this causes a stack overflow even though the branch is unreachable: + /// ```rust,ignore + /// fn main() { + /// if false { + /// let x = [0u8; 10000000]; // 10 MB stack array + /// black_box(&x); + /// } + /// } + /// ``` + /// + /// ### Known issues + /// False positives. The stack size that clippy sees is an estimated value and can be vastly different + /// from the actual stack usage after optimizations passes have run (especially true in release mode). + /// Modern compilers are very smart and are able to optimize away a lot of unnecessary stack allocations. + /// In debug mode however, it is usually more accurate. + /// + /// This lint works by summing up the size of all variables that the user typed, variables that were + /// implicitly introduced by the compiler for temporaries, function arguments and the return value, + /// and comparing them against a (configurable, but high-by-default). + /// + /// ### Example + /// This function creates four 500 KB arrays on the stack. Quite big but just small enough to not trigger `large_stack_arrays`. + /// However, looking at the function as a whole, it's clear that this uses a lot of stack space. + /// ```rust + /// struct QuiteLargeType([u8; 500_000]); + /// fn foo() { + /// // ... some function that uses a lot of stack space ... + /// let _x1 = QuiteLargeType([0; 500_000]); + /// let _x2 = QuiteLargeType([0; 500_000]); + /// let _x3 = QuiteLargeType([0; 500_000]); + /// let _x4 = QuiteLargeType([0; 500_000]); + /// } + /// ``` + /// + /// Instead of doing this, allocate the arrays on the heap. + /// This currently requires going through a `Vec` first and then converting it to a `Box`: + /// ```rust + /// struct NotSoLargeType(Box<[u8]>); + /// + /// fn foo() { + /// let _x1 = NotSoLargeType(vec![0; 500_000].into_boxed_slice()); + /// // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now heap allocated. + /// // The size of `NotSoLargeType` is 16 bytes. + /// // ... + /// } + /// ``` + #[clippy::version = "1.71.0"] + pub LARGE_STACK_FRAMES, + nursery, + "checks for functions that allocate a lot of stack space" +} + +pub struct LargeStackFrames { + maximum_allowed_size: u64, +} + +impl LargeStackFrames { + #[must_use] + pub fn new(size: u64) -> Self { + Self { + maximum_allowed_size: size, + } + } +} + +impl_lint_pass!(LargeStackFrames => [LARGE_STACK_FRAMES]); + +#[derive(Copy, Clone)] +enum Space { + Used(u64), + Overflow, +} + +impl Space { + pub fn exceeds_limit(self, limit: u64) -> bool { + match self { + Self::Used(used) => used > limit, + Self::Overflow => true, + } + } +} + +impl AddAssign<u64> for Space { + fn add_assign(&mut self, rhs: u64) { + if let Self::Used(lhs) = self { + match lhs.checked_add(rhs) { + Some(sum) => *self = Self::Used(sum), + None => *self = Self::Overflow, + } + } + } +} + +impl<'tcx> LateLintPass<'tcx> for LargeStackFrames { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + _: FnKind<'tcx>, + _: &'tcx FnDecl<'tcx>, + _: &'tcx Body<'tcx>, + span: Span, + local_def_id: LocalDefId, + ) { + let def_id = local_def_id.to_def_id(); + // Building MIR for `fn`s with unsatisfiable preds results in ICE. + if fn_has_unsatisfiable_preds(cx, def_id) { + return; + } + + let mir = cx.tcx.optimized_mir(def_id); + let param_env = cx.tcx.param_env(def_id); + + let mut frame_size = Space::Used(0); + + for local in &mir.local_decls { + if let Ok(layout) = cx.tcx.layout_of(param_env.and(local.ty)) { + frame_size += layout.size.bytes(); + } + } + + if frame_size.exceeds_limit(self.maximum_allowed_size) { + span_lint_and_note( + cx, + LARGE_STACK_FRAMES, + span, + "this function allocates a large amount of stack space", + None, + "allocating large amounts of stack space can overflow the stack", + ); + } + } +} diff --git a/clippy_lints/src/let_with_type_underscore.rs b/clippy_lints/src/let_with_type_underscore.rs index 2f10e3d2581..4e9d77ea156 100644 --- a/clippy_lints/src/let_with_type_underscore.rs +++ b/clippy_lints/src/let_with_type_underscore.rs @@ -1,4 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet; use rustc_hir::{Local, TyKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::lint::in_external_macro; @@ -25,14 +26,21 @@ declare_clippy_lint! { declare_lint_pass!(UnderscoreTyped => [LET_WITH_TYPE_UNDERSCORE]); impl LateLintPass<'_> for UnderscoreTyped { - fn check_local<'tcx>(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { + fn check_local(&mut self, cx: &LateContext<'_>, local: &Local<'_>) { if_chain! { if !in_external_macro(cx.tcx.sess, local.span); if let Some(ty) = local.ty; // Ensure that it has a type defined if let TyKind::Infer = &ty.kind; // that type is '_' if local.span.ctxt() == ty.span.ctxt(); then { - span_lint_and_help(cx, + // NOTE: Using `is_from_proc_macro` on `init` will require that it's initialized, + // this doesn't. Alternatively, `WithSearchPat` can be implemented for `Ty` + if snippet(cx, ty.span, "_").trim() != "_" { + return; + } + + span_lint_and_help( + cx, LET_WITH_TYPE_UNDERSCORE, local.span, "variable declared with type underscore", diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 4a23edb58aa..87329ee5e14 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -68,6 +68,7 @@ mod renamed_lints; mod allow_attributes; mod almost_complete_range; mod approx_const; +mod arc_with_non_send_sync; mod as_conversions; mod asm_syntax; mod assertions_on_constants; @@ -114,6 +115,7 @@ mod else_if_without_else; mod empty_drop; mod empty_enum; mod empty_structs_with_brackets; +mod endian_bytes; mod entry; mod enum_clike; mod enum_variants; @@ -121,6 +123,7 @@ mod equatable_if_let; mod escape; mod eta_reduction; mod excessive_bools; +mod excessive_nesting; mod exhaustive_items; mod exit; mod explicit_write; @@ -147,6 +150,7 @@ mod implicit_return; mod implicit_saturating_add; mod implicit_saturating_sub; mod inconsistent_struct_constructor; +mod incorrect_impls; mod index_refutable_slice; mod indexing_slicing; mod infinite_iter; @@ -165,6 +169,7 @@ mod large_enum_variant; mod large_futures; mod large_include_file; mod large_stack_arrays; +mod large_stack_frames; mod len_zero; mod let_if_seq; mod let_underscore; @@ -183,6 +188,7 @@ mod manual_is_ascii_check; mod manual_let_else; mod manual_main_separator_str; mod manual_non_exhaustive; +mod manual_range_patterns; mod manual_rem_euclid; mod manual_retain; mod manual_slice_size_calculation; @@ -191,9 +197,9 @@ mod manual_strip; mod map_unit_fn; mod match_result_ok; mod matches; -mod mem_forget; mod mem_replace; mod methods; +mod min_ident_chars; mod minmax; mod misc; mod misc_early; @@ -220,6 +226,7 @@ mod needless_borrowed_ref; mod needless_continue; mod needless_else; mod needless_for_each; +mod needless_if; mod needless_late_init; mod needless_parens_on_range_literals; mod needless_pass_by_value; @@ -256,6 +263,7 @@ mod pub_use; mod question_mark; mod question_mark_used; mod ranges; +mod raw_strings; mod rc_clone_in_vec_init; mod read_zero_byte_vec; mod redundant_async_block; @@ -266,6 +274,7 @@ mod redundant_field_names; mod redundant_pub_crate; mod redundant_slicing; mod redundant_static_lifetimes; +mod redundant_type_annotations; mod ref_option_ref; mod ref_patterns; mod reference; @@ -279,8 +288,10 @@ mod semicolon_if_nothing_returned; mod serde_api; mod shadow; mod significant_drop_tightening; +mod single_call_fn; mod single_char_lifetime_names; mod single_component_path_imports; +mod single_range_in_vec_init; mod size_of_in_element_count; mod size_of_ref; mod slow_vector_initialization; @@ -300,6 +311,7 @@ mod to_digit_is_some; mod trailing_empty_array; mod trait_bounds; mod transmute; +mod tuple_array_conversions; mod types; mod undocumented_unsafe_blocks; mod unicode; @@ -327,6 +339,7 @@ mod use_self; mod useless_conversion; mod vec; mod vec_init_then_push; +mod visibility; mod wildcard_imports; mod write; mod zero_div_zero; @@ -486,26 +499,27 @@ pub(crate) struct LintInfo { explanation: &'static str, } -pub fn explain(name: &str) { +pub fn explain(name: &str) -> i32 { let target = format!("clippy::{}", name.to_ascii_uppercase()); - match declared_lints::LINTS.iter().find(|info| info.lint.name == target) { - Some(info) => { - println!("{}", info.explanation); - // Check if the lint has configuration - let mdconf = get_configuration_metadata(); - if let Some(config_vec_positions) = mdconf - .iter() - .find_all(|cconf| cconf.lints.contains(&info.lint.name_lower()[8..].to_owned())) - { - // If it has, print it - println!("### Configuration for {}:\n", info.lint.name_lower()); - for position in config_vec_positions { - let conf = &mdconf[position]; - println!(" - {}: {} (default: {})", conf.name, conf.doc, conf.default); - } + if let Some(info) = declared_lints::LINTS.iter().find(|info| info.lint.name == target) { + println!("{}", info.explanation); + // Check if the lint has configuration + let mdconf = get_configuration_metadata(); + if let Some(config_vec_positions) = mdconf + .iter() + .find_all(|cconf| cconf.lints.contains(&info.lint.name_lower()[8..].to_owned())) + { + // If it has, print it + println!("### Configuration for {}:\n", info.lint.name_lower()); + for position in config_vec_positions { + let conf = &mdconf[position]; + println!(" - {}: {} (default: {})", conf.name, conf.doc, conf.default); } - }, - None => println!("unknown lint: {name}"), + } + 0 + } else { + println!("unknown lint: {name}"); + 1 } } @@ -564,6 +578,9 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|_| Box::<utils::internal_lints::unnecessary_def_path::UnnecessaryDefPath>::default()); store.register_late_pass(|_| Box::new(utils::internal_lints::outer_expn_data_pass::OuterExpnDataPass)); store.register_late_pass(|_| Box::new(utils::internal_lints::msrv_attr_impl::MsrvAttrImpl)); + store.register_late_pass(|_| { + Box::new(utils::internal_lints::almost_standard_lint_formulation::AlmostStandardFormulation::new()) + }); } let arithmetic_side_effects_allowed = conf.arithmetic_side_effects_allowed.clone(); @@ -672,7 +689,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: }); store.register_late_pass(|_| Box::<shadow::Shadow>::default()); store.register_late_pass(|_| Box::new(unit_types::UnitTypes)); - store.register_late_pass(|_| Box::new(loops::Loops)); + store.register_late_pass(move |_| Box::new(loops::Loops::new(msrv()))); store.register_late_pass(|_| Box::<main_recursion::MainRecursion>::default()); store.register_late_pass(|_| Box::new(lifetimes::Lifetimes)); store.register_late_pass(|_| Box::new(entry::HashMapPass)); @@ -693,7 +710,12 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: }); let too_large_for_stack = conf.too_large_for_stack; store.register_late_pass(move |_| Box::new(escape::BoxedLocal { too_large_for_stack })); - store.register_late_pass(move |_| Box::new(vec::UselessVec { too_large_for_stack })); + store.register_late_pass(move |_| { + Box::new(vec::UselessVec { + too_large_for_stack, + msrv: msrv(), + }) + }); store.register_late_pass(|_| Box::new(panic_unimplemented::PanicUnimplemented)); store.register_late_pass(|_| Box::new(strings::StringLitAsBytes)); store.register_late_pass(|_| Box::new(derive::Derive)); @@ -725,7 +747,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: let missing_docs_in_crate_items = conf.missing_docs_in_crate_items; store.register_late_pass(move |_| Box::new(doc::DocMarkdown::new(doc_valid_idents.clone()))); store.register_late_pass(|_| Box::new(neg_multiply::NegMultiply)); - store.register_late_pass(|_| Box::new(mem_forget::MemForget)); store.register_late_pass(|_| Box::new(let_if_seq::LetIfSeq)); store.register_late_pass(|_| Box::new(mixed_read_write_in_expression::EvalOrderDependence)); store.register_late_pass(move |_| Box::new(missing_doc::MissingDoc::new(missing_docs_in_crate_items))); @@ -751,7 +772,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|_| Box::<useless_conversion::UselessConversion>::default()); store.register_late_pass(|_| Box::new(implicit_hasher::ImplicitHasher)); store.register_late_pass(|_| Box::new(fallible_impl_from::FallibleImplFrom)); - store.register_late_pass(|_| Box::new(question_mark::QuestionMark)); + store.register_late_pass(|_| Box::<question_mark::QuestionMark>::default()); store.register_late_pass(|_| Box::new(question_mark_used::QuestionMarkUsed)); store.register_early_pass(|| Box::new(suspicious_operation_groupings::SuspiciousOperationGroupings)); store.register_late_pass(|_| Box::new(suspicious_trait_impl::SuspiciousImpl)); @@ -773,7 +794,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|_| Box::new(assertions_on_result_states::AssertionsOnResultStates)); store.register_late_pass(|_| Box::new(inherent_to_string::InherentToString)); let max_trait_bounds = conf.max_trait_bounds; - store.register_late_pass(move |_| Box::new(trait_bounds::TraitBounds::new(max_trait_bounds))); + store.register_late_pass(move |_| Box::new(trait_bounds::TraitBounds::new(max_trait_bounds, msrv()))); store.register_late_pass(|_| Box::new(comparison_chain::ComparisonChain)); let ignore_interior_mutability = conf.ignore_interior_mutability.clone(); store.register_late_pass(move |_| Box::new(mut_key::MutableKeyType::new(ignore_interior_mutability.clone()))); @@ -785,7 +806,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_early_pass(|| Box::new(int_plus_one::IntPlusOne)); store.register_early_pass(|| Box::new(formatting::Formatting)); store.register_early_pass(|| Box::new(misc_early::MiscEarlyLints)); - store.register_early_pass(|| Box::new(redundant_closure_call::RedundantClosureCall)); store.register_late_pass(|_| Box::new(redundant_closure_call::RedundantClosureCall)); store.register_early_pass(|| Box::new(unused_unit::UnusedUnit)); store.register_late_pass(|_| Box::new(returns::Return)); @@ -810,10 +830,12 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: )) }); let enum_variant_name_threshold = conf.enum_variant_name_threshold; + let allow_private_module_inception = conf.allow_private_module_inception; store.register_late_pass(move |_| { Box::new(enum_variants::EnumVariantNames::new( enum_variant_name_threshold, avoid_breaking_exported_api, + allow_private_module_inception, )) }); store.register_early_pass(|| Box::new(tabs_in_doc_comments::TabsInDocComments)); @@ -833,7 +855,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(move |_| Box::new(large_stack_arrays::LargeStackArrays::new(array_size_threshold))); store.register_late_pass(move |_| Box::new(large_const_arrays::LargeConstArrays::new(array_size_threshold))); store.register_late_pass(|_| Box::new(floating_point_arithmetic::FloatingPointArithmetic)); - store.register_early_pass(|| Box::new(as_conversions::AsConversions)); + store.register_late_pass(|_| Box::new(as_conversions::AsConversions)); store.register_late_pass(|_| Box::new(let_underscore::LetUnderscore)); store.register_early_pass(|| Box::<single_component_path_imports::SingleComponentPathImports>::default()); let max_fn_params_bools = conf.max_fn_params_bools; @@ -909,7 +931,14 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: enable_raw_pointer_heuristic_for_send, )) }); - store.register_late_pass(move |_| Box::new(undocumented_unsafe_blocks::UndocumentedUnsafeBlocks)); + let accept_comment_above_statement = conf.accept_comment_above_statement; + let accept_comment_above_attributes = conf.accept_comment_above_attributes; + store.register_late_pass(move |_| { + Box::new(undocumented_unsafe_blocks::UndocumentedUnsafeBlocks::new( + accept_comment_above_statement, + accept_comment_above_attributes, + )) + }); let allow_mixed_uninlined = conf.allow_mixed_uninlined_format_args; store.register_late_pass(move |_| Box::new(format_args::FormatArgs::new(msrv(), allow_mixed_uninlined))); store.register_late_pass(|_| Box::new(trailing_empty_array::TrailingEmptyArray)); @@ -1002,11 +1031,49 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule)); store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation)); store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments)); + let excessive_nesting_threshold = conf.excessive_nesting_threshold; + store.register_early_pass(move || { + Box::new(excessive_nesting::ExcessiveNesting { + excessive_nesting_threshold, + nodes: rustc_ast::node_id::NodeSet::new(), + }) + }); store.register_late_pass(|_| Box::new(items_after_test_module::ItemsAfterTestModule)); store.register_early_pass(|| Box::new(ref_patterns::RefPatterns)); store.register_late_pass(|_| Box::new(default_constructed_unit_structs::DefaultConstructedUnitStructs)); store.register_early_pass(|| Box::new(needless_else::NeedlessElse)); store.register_late_pass(|_| Box::new(missing_fields_in_debug::MissingFieldsInDebug)); + store.register_late_pass(|_| Box::new(endian_bytes::EndianBytes)); + store.register_late_pass(|_| Box::new(redundant_type_annotations::RedundantTypeAnnotations)); + store.register_late_pass(|_| Box::new(arc_with_non_send_sync::ArcWithNonSendSync)); + store.register_late_pass(|_| Box::new(needless_if::NeedlessIf)); + let allowed_idents_below_min_chars = conf.allowed_idents_below_min_chars.clone(); + let min_ident_chars_threshold = conf.min_ident_chars_threshold; + store.register_late_pass(move |_| { + Box::new(min_ident_chars::MinIdentChars { + allowed_idents_below_min_chars: allowed_idents_below_min_chars.clone(), + min_ident_chars_threshold, + }) + }); + let stack_size_threshold = conf.stack_size_threshold; + store.register_late_pass(move |_| Box::new(large_stack_frames::LargeStackFrames::new(stack_size_threshold))); + store.register_late_pass(|_| Box::new(single_range_in_vec_init::SingleRangeInVecInit)); + store.register_late_pass(|_| Box::new(incorrect_impls::IncorrectImpls)); + store.register_late_pass(move |_| { + Box::new(single_call_fn::SingleCallFn { + avoid_breaking_exported_api, + def_id_to_usage: rustc_data_structures::fx::FxHashMap::default(), + }) + }); + let needless_raw_string_hashes_allow_one = conf.allow_one_hash_in_raw_strings; + store.register_early_pass(move || { + Box::new(raw_strings::RawStrings { + needless_raw_string_hashes_allow_one, + }) + }); + store.register_late_pass(|_| Box::new(manual_range_patterns::ManualRangePatterns)); + store.register_early_pass(|| Box::new(visibility::Visibility)); + store.register_late_pass(move |_| Box::new(tuple_array_conversions::TupleArrayConversions { msrv: msrv() })); // 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 986ffcad883..852f6736585 100644 --- a/clippy_lints/src/lifetimes.rs +++ b/clippy_lints/src/lifetimes.rs @@ -1,5 +1,6 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; use clippy_utils::trait_ref_of_method; +use itertools::Itertools; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_errors::Applicability; use rustc_hir::intravisit::nested_filter::{self as hir_nested_filter, NestedFilter}; @@ -201,7 +202,19 @@ fn check_fn_inner<'tcx>( span_lint_and_then( cx, NEEDLESS_LIFETIMES, - span.with_hi(sig.decl.output.span().hi()), + elidable_lts + .iter() + .map(|<| cx.tcx.def_span(lt)) + .chain(usages.iter().filter_map(|usage| { + if let LifetimeName::Param(def_id) = usage.res + && elidable_lts.contains(&def_id) + { + return Some(usage.ident.span); + } + + None + })) + .collect_vec(), &format!("the following explicit lifetimes could be elided: {lts}"), |diag| { if sig.header.is_async() { @@ -562,7 +575,7 @@ fn has_where_lifetimes<'tcx>(cx: &LateContext<'tcx>, generics: &'tcx Generics<'_ // if the bounds define new lifetimes, they are fine to occur let allowed_lts = allowed_lts_from(pred.bound_generic_params); // now walk the bounds - for bound in pred.bounds.iter() { + for bound in pred.bounds { walk_param_bound(&mut visitor, bound); } // and check that all lifetimes are allowed diff --git a/clippy_lints/src/loops/explicit_into_iter_loop.rs b/clippy_lints/src/loops/explicit_into_iter_loop.rs index 175e2b382e3..93d6b808646 100644 --- a/clippy_lints/src/loops/explicit_into_iter_loop.rs +++ b/clippy_lints/src/loops/explicit_into_iter_loop.rs @@ -5,15 +5,76 @@ use clippy_utils::source::snippet_with_applicability; use rustc_errors::Applicability; use rustc_hir::Expr; use rustc_lint::LateContext; +use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow, AutoBorrowMutability}; use rustc_span::symbol::sym; +#[derive(Clone, Copy)] +enum AdjustKind { + None, + Borrow, + BorrowMut, + Reborrow, + ReborrowMut, +} +impl AdjustKind { + fn borrow(mutbl: AutoBorrowMutability) -> Self { + match mutbl { + AutoBorrowMutability::Not => Self::Borrow, + AutoBorrowMutability::Mut { .. } => Self::BorrowMut, + } + } + + fn reborrow(mutbl: AutoBorrowMutability) -> Self { + match mutbl { + AutoBorrowMutability::Not => Self::Reborrow, + AutoBorrowMutability::Mut { .. } => Self::ReborrowMut, + } + } + + fn display(self) -> &'static str { + match self { + Self::None => "", + Self::Borrow => "&", + Self::BorrowMut => "&mut ", + Self::Reborrow => "&*", + Self::ReborrowMut => "&mut *", + } + } +} + pub(super) fn check(cx: &LateContext<'_>, self_arg: &Expr<'_>, call_expr: &Expr<'_>) { - let self_ty = cx.typeck_results().expr_ty(self_arg); - let self_ty_adjusted = cx.typeck_results().expr_ty_adjusted(self_arg); - if !(self_ty == self_ty_adjusted && is_trait_method(cx, call_expr, sym::IntoIterator)) { + if !is_trait_method(cx, call_expr, sym::IntoIterator) { return; } + let typeck = cx.typeck_results(); + let self_ty = typeck.expr_ty(self_arg); + let adjust = match typeck.expr_adjustments(self_arg) { + [] => AdjustKind::None, + &[ + Adjustment { + kind: Adjust::Borrow(AutoBorrow::Ref(_, mutbl)), + .. + }, + ] => AdjustKind::borrow(mutbl), + &[ + Adjustment { + kind: Adjust::Deref(_), .. + }, + Adjustment { + kind: Adjust::Borrow(AutoBorrow::Ref(_, mutbl)), + target, + }, + ] => { + if self_ty == target && matches!(mutbl, AutoBorrowMutability::Not) { + AdjustKind::None + } else { + AdjustKind::reborrow(mutbl) + } + }, + _ => return, + }; + let mut applicability = Applicability::MachineApplicable; let object = snippet_with_applicability(cx, self_arg.span, "_", &mut applicability); span_lint_and_sugg( @@ -23,7 +84,7 @@ pub(super) fn check(cx: &LateContext<'_>, self_arg: &Expr<'_>, call_expr: &Expr< "it is more concise to loop over containers instead of using explicit \ iteration methods", "to write this more concisely, try", - object.to_string(), + format!("{}{object}", adjust.display()), applicability, ); } diff --git a/clippy_lints/src/loops/explicit_iter_loop.rs b/clippy_lints/src/loops/explicit_iter_loop.rs index 151c7f1d5d2..f3347e33077 100644 --- a/clippy_lints/src/loops/explicit_iter_loop.rs +++ b/clippy_lints/src/loops/explicit_iter_loop.rs @@ -1,75 +1,235 @@ use super::EXPLICIT_ITER_LOOP; use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::is_trait_method; +use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::snippet_with_applicability; -use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::ty::{ + implements_trait, implements_trait_with_env, is_copy, make_normalized_projection, + make_normalized_projection_with_regions, normalize_with_regions, +}; use rustc_errors::Applicability; use rustc_hir::{Expr, Mutability}; use rustc_lint::LateContext; -use rustc_middle::ty::{self, Ty}; +use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow, AutoBorrowMutability}; +use rustc_middle::ty::{self, EarlyBinder, Ty, TypeAndMut}; use rustc_span::sym; -pub(super) fn check(cx: &LateContext<'_>, self_arg: &Expr<'_>, arg: &Expr<'_>, method_name: &str) { - let should_lint = match method_name { - "iter" | "iter_mut" => is_ref_iterable_type(cx, self_arg), - "into_iter" if is_trait_method(cx, arg, sym::IntoIterator) => { - let receiver_ty = cx.typeck_results().expr_ty(self_arg); - let receiver_ty_adjusted = cx.typeck_results().expr_ty_adjusted(self_arg); - let ref_receiver_ty = cx.tcx.mk_ref( - cx.tcx.lifetimes.re_erased, - ty::TypeAndMut { - ty: receiver_ty, - mutbl: Mutability::Not, - }, - ); - receiver_ty_adjusted == ref_receiver_ty - }, - _ => false, - }; - - if !should_lint { +pub(super) fn check(cx: &LateContext<'_>, self_arg: &Expr<'_>, call_expr: &Expr<'_>, msrv: &Msrv) { + let Some((adjust, ty)) = is_ref_iterable(cx, self_arg, call_expr) else { return; + }; + if let ty::Array(_, count) = *ty.peel_refs().kind() { + if !ty.is_ref() { + if !msrv.meets(msrvs::ARRAY_INTO_ITERATOR) { + return; + } + } else if count + .try_eval_target_usize(cx.tcx, cx.param_env) + .map_or(true, |x| x > 32) + && !msrv.meets(msrvs::ARRAY_IMPL_ANY_LEN) + { + return; + } } let mut applicability = Applicability::MachineApplicable; let object = snippet_with_applicability(cx, self_arg.span, "_", &mut applicability); - let muta = if method_name == "iter_mut" { "mut " } else { "" }; span_lint_and_sugg( cx, EXPLICIT_ITER_LOOP, - arg.span, + call_expr.span, "it is more concise to loop over references to containers instead of using explicit \ iteration methods", "to write this more concisely, try", - format!("&{muta}{object}"), + format!("{}{object}", adjust.display()), applicability, ); } -/// Returns `true` if the type of expr is one that provides `IntoIterator` impls -/// for `&T` and `&mut T`, such as `Vec`. -#[rustfmt::skip] -fn is_ref_iterable_type(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { - // no walk_ptrs_ty: calling iter() on a reference can make sense because it - // will allow further borrows afterwards - let ty = cx.typeck_results().expr_ty(e); - is_iterable_array(ty, cx) || - is_type_diagnostic_item(cx, ty, sym::Vec) || - is_type_diagnostic_item(cx, ty, sym::LinkedList) || - is_type_diagnostic_item(cx, ty, sym::HashMap) || - is_type_diagnostic_item(cx, ty, sym::HashSet) || - is_type_diagnostic_item(cx, ty, sym::VecDeque) || - is_type_diagnostic_item(cx, ty, sym::BinaryHeap) || - is_type_diagnostic_item(cx, ty, sym::BTreeMap) || - is_type_diagnostic_item(cx, ty, sym::BTreeSet) +#[derive(Clone, Copy)] +enum AdjustKind { + None, + Borrow, + BorrowMut, + Deref, + Reborrow, + ReborrowMut, } +impl AdjustKind { + fn borrow(mutbl: Mutability) -> Self { + match mutbl { + Mutability::Not => Self::Borrow, + Mutability::Mut => Self::BorrowMut, + } + } -fn is_iterable_array<'tcx>(ty: Ty<'tcx>, cx: &LateContext<'tcx>) -> bool { - // IntoIterator is currently only implemented for array sizes <= 32 in rustc - match ty.kind() { - ty::Array(_, n) => n - .try_eval_target_usize(cx.tcx, cx.param_env) - .map_or(false, |val| (0..=32).contains(&val)), - _ => false, + fn auto_borrow(mutbl: AutoBorrowMutability) -> Self { + match mutbl { + AutoBorrowMutability::Not => Self::Borrow, + AutoBorrowMutability::Mut { .. } => Self::BorrowMut, + } + } + + fn reborrow(mutbl: Mutability) -> Self { + match mutbl { + Mutability::Not => Self::Reborrow, + Mutability::Mut => Self::ReborrowMut, + } + } + + fn auto_reborrow(mutbl: AutoBorrowMutability) -> Self { + match mutbl { + AutoBorrowMutability::Not => Self::Reborrow, + AutoBorrowMutability::Mut { .. } => Self::ReborrowMut, + } + } + + fn display(self) -> &'static str { + match self { + Self::None => "", + Self::Borrow => "&", + Self::BorrowMut => "&mut ", + Self::Deref => "*", + Self::Reborrow => "&*", + Self::ReborrowMut => "&mut *", + } + } +} + +/// Checks if an `iter` or `iter_mut` call returns `IntoIterator::IntoIter`. Returns how the +/// argument needs to be adjusted. +#[expect(clippy::too_many_lines)] +fn is_ref_iterable<'tcx>( + cx: &LateContext<'tcx>, + self_arg: &Expr<'_>, + call_expr: &Expr<'_>, +) -> Option<(AdjustKind, Ty<'tcx>)> { + let typeck = cx.typeck_results(); + if let Some(trait_id) = cx.tcx.get_diagnostic_item(sym::IntoIterator) + && let Some(fn_id) = typeck.type_dependent_def_id(call_expr.hir_id) + && 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, []) + && 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) + && into_iter_ty == req_res_ty + { + let adjustments = typeck.expr_adjustments(self_arg); + let self_ty = typeck.expr_ty(self_arg); + let self_is_copy = is_copy(cx, self_ty); + + if adjustments.is_empty() && self_is_copy { + // Exact type match, already checked earlier + return Some((AdjustKind::None, self_ty)); + } + + let res_ty = cx.tcx.erase_regions(EarlyBinder::bind(req_res_ty) + .subst(cx.tcx, typeck.node_substs(call_expr.hir_id))); + let mutbl = if let ty::Ref(_, _, mutbl) = *req_self_ty.kind() { + Some(mutbl) + } else { + None + }; + + if !adjustments.is_empty() { + if self_is_copy { + // Using by value won't consume anything + if implements_trait(cx, self_ty, trait_id, &[]) + && let Some(ty) = + make_normalized_projection(cx.tcx, cx.param_env, trait_id, sym!(IntoIter), [self_ty]) + && ty == res_ty + { + return Some((AdjustKind::None, self_ty)); + } + } else if let ty::Ref(region, ty, Mutability::Mut) = *self_ty.kind() + && let Some(mutbl) = mutbl + { + // Attempt to reborrow the mutable reference + let self_ty = if mutbl.is_mut() { + self_ty + } else { + cx.tcx.mk_ref(region, TypeAndMut { ty, mutbl }) + }; + if implements_trait(cx, self_ty, trait_id, &[]) + && let Some(ty) = + make_normalized_projection(cx.tcx, cx.param_env, trait_id, sym!(IntoIter), [self_ty]) + && ty == res_ty + { + return Some((AdjustKind::reborrow(mutbl), self_ty)); + } + } + } + if let Some(mutbl) = mutbl + && !self_ty.is_ref() + { + // Attempt to borrow + let self_ty = cx.tcx.mk_ref(cx.tcx.lifetimes.re_erased, TypeAndMut { + ty: self_ty, + mutbl, + }); + if implements_trait(cx, self_ty, trait_id, &[]) + && let Some(ty) = make_normalized_projection(cx.tcx, cx.param_env, trait_id, sym!(IntoIter), [self_ty]) + && ty == res_ty + { + return Some((AdjustKind::borrow(mutbl), self_ty)); + } + } + + match adjustments { + [] => Some((AdjustKind::None, self_ty)), + &[ + Adjustment { kind: Adjust::Deref(_), ..}, + Adjustment { + kind: Adjust::Borrow(AutoBorrow::Ref(_, mutbl)), + target, + }, + .. + ] => { + if target != self_ty + && implements_trait(cx, target, trait_id, &[]) + && let Some(ty) = + make_normalized_projection(cx.tcx, cx.param_env, trait_id, sym!(IntoIter), [target]) + && ty == res_ty + { + Some((AdjustKind::auto_reborrow(mutbl), target)) + } else { + None + } + } + &[Adjustment { kind: Adjust::Deref(_), target }, ..] => { + if is_copy(cx, target) + && implements_trait(cx, target, trait_id, &[]) + && let Some(ty) = + make_normalized_projection(cx.tcx, cx.param_env, trait_id, sym!(IntoIter), [target]) + && ty == res_ty + { + Some((AdjustKind::Deref, target)) + } else { + None + } + } + &[ + Adjustment { + kind: Adjust::Borrow(AutoBorrow::Ref(_, mutbl)), + target, + }, + .. + ] => { + if self_ty.is_ref() + && implements_trait(cx, target, trait_id, &[]) + && let Some(ty) = + make_normalized_projection(cx.tcx, cx.param_env, trait_id, sym!(IntoIter), [target]) + && ty == res_ty + { + Some((AdjustKind::auto_borrow(mutbl), target)) + } else { + None + } + } + _ => None, + } + } else { + None } } diff --git a/clippy_lints/src/loops/manual_memcpy.rs b/clippy_lints/src/loops/manual_memcpy.rs index d4c3f76b864..7d1f8ef29c8 100644 --- a/clippy_lints/src/loops/manual_memcpy.rs +++ b/clippy_lints/src/loops/manual_memcpy.rs @@ -51,7 +51,7 @@ pub(super) fn check<'tcx>( iter_b = Some(get_assignment(body)); } - let assignments = iter_a.into_iter().flatten().chain(iter_b.into_iter()); + let assignments = iter_a.into_iter().flatten().chain(iter_b); let big_sugg = assignments // The only statements in the for loops can be indexed assignments from @@ -402,7 +402,7 @@ fn get_assignments<'a, 'tcx>( StmtKind::Local(..) | StmtKind::Item(..) => None, StmtKind::Expr(e) | StmtKind::Semi(e) => Some(e), }) - .chain((*expr).into_iter()) + .chain(*expr) .filter(move |e| { if let ExprKind::AssignOp(_, place, _) = e.kind { path_to_local(place).map_or(false, |id| { diff --git a/clippy_lints/src/loops/mod.rs b/clippy_lints/src/loops/mod.rs index f83ad388a74..529189b52ac 100644 --- a/clippy_lints/src/loops/mod.rs +++ b/clippy_lints/src/loops/mod.rs @@ -20,9 +20,10 @@ mod while_let_loop; mod while_let_on_iterator; use clippy_utils::higher; +use clippy_utils::msrvs::Msrv; use rustc_hir::{Expr, ExprKind, LoopSource, Pat}; use rustc_lint::{LateContext, LateLintPass}; -use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::source_map::Span; use utils::{make_iterator_snippet, IncrementVisitor, InitializeVisitor}; @@ -479,7 +480,7 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does - /// Check for unnecessary `if let` usage in a for loop + /// Checks for unnecessary `if let` usage in a for loop /// where only the `Some` or `Ok` variant of the iterator element is used. /// /// ### Why is this bad? @@ -511,7 +512,7 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does - /// Check for empty spin loops + /// Checks for empty spin loops /// /// ### Why is this bad? /// The loop body should have something like `thread::park()` or at least @@ -547,7 +548,7 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does - /// Check for manual implementations of Iterator::find + /// Checks for manual implementations of Iterator::find /// /// ### Why is this bad? /// It doesn't affect performance, but using `find` is shorter and easier to read. @@ -606,7 +607,15 @@ declare_clippy_lint! { "checking for emptiness of a `Vec` in the loop condition and popping an element in the body" } -declare_lint_pass!(Loops => [ +pub struct Loops { + msrv: Msrv, +} +impl Loops { + pub fn new(msrv: Msrv) -> Self { + Self { msrv } + } +} +impl_lint_pass!(Loops => [ MANUAL_MEMCPY, MANUAL_FLATTEN, NEEDLESS_RANGE_LOOP, @@ -645,7 +654,7 @@ impl<'tcx> LateLintPass<'tcx> for Loops { if body.span.from_expansion() { return; } - check_for_loop(cx, pat, arg, body, expr, span); + self.check_for_loop(cx, pat, arg, body, expr, span); if let ExprKind::Block(block, _) = body.kind { never_loop::check(cx, block, loop_id, span, for_loop.as_ref()); } @@ -678,46 +687,48 @@ impl<'tcx> LateLintPass<'tcx> for Loops { manual_while_let_some::check(cx, condition, body, span); } } + + extract_msrv_attr!(LateContext); } -fn check_for_loop<'tcx>( - cx: &LateContext<'tcx>, - pat: &'tcx Pat<'_>, - arg: &'tcx Expr<'_>, - body: &'tcx Expr<'_>, - expr: &'tcx Expr<'_>, - span: Span, -) { - let is_manual_memcpy_triggered = manual_memcpy::check(cx, pat, arg, body, expr); - if !is_manual_memcpy_triggered { - needless_range_loop::check(cx, pat, arg, body, expr); - explicit_counter_loop::check(cx, pat, arg, body, expr); +impl Loops { + fn check_for_loop<'tcx>( + &self, + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + arg: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + expr: &'tcx Expr<'_>, + span: Span, + ) { + let is_manual_memcpy_triggered = manual_memcpy::check(cx, pat, arg, body, expr); + if !is_manual_memcpy_triggered { + needless_range_loop::check(cx, pat, arg, body, expr); + explicit_counter_loop::check(cx, pat, arg, body, expr); + } + self.check_for_loop_arg(cx, pat, arg); + for_kv_map::check(cx, pat, arg, body); + mut_range_bound::check(cx, arg, body); + single_element_loop::check(cx, pat, arg, body, expr); + same_item_push::check(cx, pat, arg, body, expr); + manual_flatten::check(cx, pat, arg, body, span); + manual_find::check(cx, pat, arg, body, span, expr); } - check_for_loop_arg(cx, pat, arg); - for_kv_map::check(cx, pat, arg, body); - mut_range_bound::check(cx, arg, body); - single_element_loop::check(cx, pat, arg, body, expr); - same_item_push::check(cx, pat, arg, body, expr); - manual_flatten::check(cx, pat, arg, body, span); - manual_find::check(cx, pat, arg, body, span, expr); -} -fn check_for_loop_arg(cx: &LateContext<'_>, _: &Pat<'_>, arg: &Expr<'_>) { - if let ExprKind::MethodCall(method, self_arg, [], _) = arg.kind { - let method_name = method.ident.as_str(); - // check for looping over x.iter() or x.iter_mut(), could use &x or &mut x - match method_name { - "iter" | "iter_mut" => { - explicit_iter_loop::check(cx, self_arg, arg, method_name); - }, - "into_iter" => { - explicit_iter_loop::check(cx, self_arg, arg, method_name); - explicit_into_iter_loop::check(cx, self_arg, arg); - }, - "next" => { - iter_next_loop::check(cx, arg); - }, - _ => {}, + fn check_for_loop_arg(&self, cx: &LateContext<'_>, _: &Pat<'_>, arg: &Expr<'_>) { + if let ExprKind::MethodCall(method, self_arg, [], _) = arg.kind { + match method.ident.as_str() { + "iter" | "iter_mut" => { + explicit_iter_loop::check(cx, self_arg, arg, &self.msrv); + }, + "into_iter" => { + explicit_into_iter_loop::check(cx, self_arg, arg); + }, + "next" => { + iter_next_loop::check(cx, arg); + }, + _ => {}, + } } } } diff --git a/clippy_lints/src/loops/never_loop.rs b/clippy_lints/src/loops/never_loop.rs index 10b5e1edf92..ee338c6beb0 100644 --- a/clippy_lints/src/loops/never_loop.rs +++ b/clippy_lints/src/loops/never_loop.rs @@ -1,22 +1,23 @@ use super::utils::make_iterator_snippet; use super::NEVER_LOOP; -use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::consts::constant; use clippy_utils::higher::ForLoop; use clippy_utils::source::snippet; +use clippy_utils::{consts::Constant, diagnostics::span_lint_and_then}; use rustc_errors::Applicability; use rustc_hir::{Block, Destination, Expr, ExprKind, HirId, InlineAsmOperand, Pat, Stmt, StmtKind}; use rustc_lint::LateContext; use rustc_span::Span; use std::iter::{once, Iterator}; -pub(super) fn check( - cx: &LateContext<'_>, - block: &Block<'_>, +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + block: &Block<'tcx>, loop_id: HirId, span: Span, for_loop: Option<&ForLoop<'_>>, ) { - match never_loop_block(block, &mut Vec::new(), loop_id) { + match never_loop_block(cx, block, &mut Vec::new(), loop_id) { NeverLoopResult::AlwaysBreak => { span_lint_and_then(cx, NEVER_LOOP, span, "this loop never actually loops", |diag| { if let Some(ForLoop { @@ -95,7 +96,12 @@ fn combine_branches(b1: NeverLoopResult, b2: NeverLoopResult, ignore_ids: &[HirI } } -fn never_loop_block(block: &Block<'_>, ignore_ids: &mut Vec<HirId>, main_loop_id: HirId) -> NeverLoopResult { +fn never_loop_block<'tcx>( + cx: &LateContext<'tcx>, + block: &Block<'tcx>, + ignore_ids: &mut Vec<HirId>, + main_loop_id: HirId, +) -> NeverLoopResult { let iter = block .stmts .iter() @@ -103,10 +109,10 @@ fn never_loop_block(block: &Block<'_>, ignore_ids: &mut Vec<HirId>, main_loop_id .chain(block.expr.map(|expr| (expr, None))); iter.map(|(e, els)| { - let e = never_loop_expr(e, ignore_ids, main_loop_id); + let e = never_loop_expr(cx, e, ignore_ids, main_loop_id); // els is an else block in a let...else binding els.map_or(e, |els| { - combine_branches(e, never_loop_block(els, ignore_ids, main_loop_id), ignore_ids) + combine_branches(e, never_loop_block(cx, els, ignore_ids, main_loop_id), ignore_ids) }) }) .fold(NeverLoopResult::Otherwise, combine_seq) @@ -122,7 +128,12 @@ fn stmt_to_expr<'tcx>(stmt: &Stmt<'tcx>) -> Option<(&'tcx Expr<'tcx>, Option<&'t } #[allow(clippy::too_many_lines)] -fn never_loop_expr(expr: &Expr<'_>, ignore_ids: &mut Vec<HirId>, main_loop_id: HirId) -> NeverLoopResult { +fn never_loop_expr<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'tcx>, + ignore_ids: &mut Vec<HirId>, + main_loop_id: HirId, +) -> NeverLoopResult { match expr.kind { ExprKind::Unary(_, e) | ExprKind::Cast(e, _) @@ -130,45 +141,51 @@ fn never_loop_expr(expr: &Expr<'_>, ignore_ids: &mut Vec<HirId>, main_loop_id: H | ExprKind::Field(e, _) | ExprKind::AddrOf(_, _, e) | ExprKind::Repeat(e, _) - | ExprKind::DropTemps(e) => never_loop_expr(e, ignore_ids, main_loop_id), - ExprKind::Let(let_expr) => never_loop_expr(let_expr.init, ignore_ids, main_loop_id), - ExprKind::Array(es) | ExprKind::Tup(es) => never_loop_expr_all(&mut es.iter(), ignore_ids, main_loop_id), + | ExprKind::DropTemps(e) => never_loop_expr(cx, e, ignore_ids, main_loop_id), + ExprKind::Let(let_expr) => never_loop_expr(cx, let_expr.init, ignore_ids, main_loop_id), + ExprKind::Array(es) | ExprKind::Tup(es) => never_loop_expr_all(cx, &mut es.iter(), ignore_ids, main_loop_id), ExprKind::MethodCall(_, receiver, es, _) => never_loop_expr_all( + cx, &mut std::iter::once(receiver).chain(es.iter()), ignore_ids, main_loop_id, ), ExprKind::Struct(_, fields, base) => { - let fields = never_loop_expr_all(&mut fields.iter().map(|f| f.expr), ignore_ids, main_loop_id); + let fields = never_loop_expr_all(cx, &mut fields.iter().map(|f| f.expr), ignore_ids, main_loop_id); if let Some(base) = base { - combine_seq(fields, never_loop_expr(base, ignore_ids, main_loop_id)) + combine_seq(fields, never_loop_expr(cx, base, ignore_ids, main_loop_id)) } else { fields } }, - ExprKind::Call(e, es) => never_loop_expr_all(&mut once(e).chain(es.iter()), ignore_ids, main_loop_id), + ExprKind::Call(e, es) => never_loop_expr_all(cx, &mut once(e).chain(es.iter()), ignore_ids, main_loop_id), ExprKind::Binary(_, e1, e2) | ExprKind::Assign(e1, e2, _) | ExprKind::AssignOp(_, e1, e2) - | ExprKind::Index(e1, e2) => never_loop_expr_all(&mut [e1, e2].iter().copied(), ignore_ids, main_loop_id), + | ExprKind::Index(e1, e2) => never_loop_expr_all(cx, &mut [e1, e2].iter().copied(), ignore_ids, main_loop_id), ExprKind::Loop(b, _, _, _) => { // Break can come from the inner loop so remove them. - absorb_break(never_loop_block(b, ignore_ids, main_loop_id)) + absorb_break(never_loop_block(cx, b, ignore_ids, main_loop_id)) }, ExprKind::If(e, e2, e3) => { - let e1 = never_loop_expr(e, ignore_ids, main_loop_id); - let e2 = never_loop_expr(e2, ignore_ids, main_loop_id); + let e1 = never_loop_expr(cx, e, ignore_ids, main_loop_id); + let e2 = never_loop_expr(cx, e2, ignore_ids, main_loop_id); + // If we know the `if` condition evaluates to `true`, don't check everything past it; it + // should just return whatever's evaluated for `e1` and `e2` since `e3` is unreachable + if let Some(Constant::Bool(true)) = constant(cx, cx.typeck_results(), e) { + return combine_seq(e1, e2); + } let e3 = e3.as_ref().map_or(NeverLoopResult::Otherwise, |e| { - never_loop_expr(e, ignore_ids, main_loop_id) + never_loop_expr(cx, e, ignore_ids, main_loop_id) }); combine_seq(e1, combine_branches(e2, e3, ignore_ids)) }, ExprKind::Match(e, arms, _) => { - let e = never_loop_expr(e, ignore_ids, main_loop_id); + let e = never_loop_expr(cx, e, ignore_ids, main_loop_id); if arms.is_empty() { e } else { - let arms = never_loop_expr_branch(&mut arms.iter().map(|a| a.body), ignore_ids, main_loop_id); + let arms = never_loop_expr_branch(cx, &mut arms.iter().map(|a| a.body), ignore_ids, main_loop_id); combine_seq(e, arms) } }, @@ -176,7 +193,7 @@ fn never_loop_expr(expr: &Expr<'_>, ignore_ids: &mut Vec<HirId>, main_loop_id: H if l.is_some() { ignore_ids.push(b.hir_id); } - let ret = never_loop_block(b, ignore_ids, main_loop_id); + let ret = never_loop_block(cx, b, ignore_ids, main_loop_id); if l.is_some() { ignore_ids.pop(); } @@ -198,31 +215,30 @@ fn never_loop_expr(expr: &Expr<'_>, ignore_ids: &mut Vec<HirId>, main_loop_id: H // checks if break targets a block instead of a loop ExprKind::Break(Destination { target_id: Ok(t), .. }, e) if ignore_ids.contains(&t) => e .map_or(NeverLoopResult::IgnoreUntilEnd(t), |e| { - never_loop_expr(e, ignore_ids, main_loop_id) + never_loop_expr(cx, e, ignore_ids, main_loop_id) }), ExprKind::Break(_, e) | ExprKind::Ret(e) => e.as_ref().map_or(NeverLoopResult::AlwaysBreak, |e| { combine_seq( - never_loop_expr(e, ignore_ids, main_loop_id), + never_loop_expr(cx, e, ignore_ids, main_loop_id), NeverLoopResult::AlwaysBreak, ) }), - ExprKind::Become(e) => { - combine_seq( - never_loop_expr(e, ignore_ids, main_loop_id), - NeverLoopResult::AlwaysBreak, - ) - } + ExprKind::Become(e) => combine_seq( + never_loop_expr(cx, e, ignore_ids, main_loop_id), + NeverLoopResult::AlwaysBreak, + ), ExprKind::InlineAsm(asm) => asm .operands .iter() .map(|(o, _)| match o { InlineAsmOperand::In { expr, .. } | InlineAsmOperand::InOut { expr, .. } => { - never_loop_expr(expr, ignore_ids, main_loop_id) + never_loop_expr(cx, expr, ignore_ids, main_loop_id) }, InlineAsmOperand::Out { expr, .. } => { - never_loop_expr_all(&mut expr.iter().copied(), ignore_ids, main_loop_id) + never_loop_expr_all(cx, &mut expr.iter().copied(), ignore_ids, main_loop_id) }, InlineAsmOperand::SplitInOut { in_expr, out_expr, .. } => never_loop_expr_all( + cx, &mut once(*in_expr).chain(out_expr.iter().copied()), ignore_ids, main_loop_id, @@ -242,22 +258,24 @@ fn never_loop_expr(expr: &Expr<'_>, ignore_ids: &mut Vec<HirId>, main_loop_id: H } } -fn never_loop_expr_all<'a, T: Iterator<Item = &'a Expr<'a>>>( +fn never_loop_expr_all<'tcx, T: Iterator<Item = &'tcx Expr<'tcx>>>( + cx: &LateContext<'tcx>, es: &mut T, ignore_ids: &mut Vec<HirId>, main_loop_id: HirId, ) -> NeverLoopResult { - es.map(|e| never_loop_expr(e, ignore_ids, main_loop_id)) + es.map(|e| never_loop_expr(cx, e, ignore_ids, main_loop_id)) .fold(NeverLoopResult::Otherwise, combine_seq) } -fn never_loop_expr_branch<'a, T: Iterator<Item = &'a Expr<'a>>>( +fn never_loop_expr_branch<'tcx, T: Iterator<Item = &'tcx Expr<'tcx>>>( + cx: &LateContext<'tcx>, e: &mut T, ignore_ids: &mut Vec<HirId>, main_loop_id: HirId, ) -> NeverLoopResult { e.fold(NeverLoopResult::AlwaysBreak, |a, b| { - combine_branches(a, never_loop_expr(b, ignore_ids, main_loop_id), ignore_ids) + combine_branches(a, never_loop_expr(cx, b, ignore_ids, main_loop_id), ignore_ids) }) } diff --git a/clippy_lints/src/loops/same_item_push.rs b/clippy_lints/src/loops/same_item_push.rs index 9d9341559ac..f7b3b2358a0 100644 --- a/clippy_lints/src/loops/same_item_push.rs +++ b/clippy_lints/src/loops/same_item_push.rs @@ -148,7 +148,7 @@ impl<'a, 'tcx> Visitor<'tcx> for SameItemPushVisitor<'a, 'tcx> { } fn visit_block(&mut self, b: &'tcx Block<'_>) { - for stmt in b.stmts.iter() { + for stmt in b.stmts { self.visit_stmt(stmt); } } diff --git a/clippy_lints/src/macro_use.rs b/clippy_lints/src/macro_use.rs index e2e6a87a301..8e322a97907 100644 --- a/clippy_lints/src/macro_use.rs +++ b/clippy_lints/src/macro_use.rs @@ -100,7 +100,7 @@ impl<'tcx> LateLintPass<'tcx> for MacroUseImports { }); if !id.is_local(); then { - for kid in cx.tcx.module_children(id).iter() { + for kid in cx.tcx.module_children(id) { if let Res::Def(DefKind::Macro(_mac_type), mac_id) = kid.res { let span = mac_attr.span; let def_path = cx.tcx.def_path_str(mac_id); diff --git a/clippy_lints/src/manual_let_else.rs b/clippy_lints/src/manual_let_else.rs index 389b0a4a62d..59e421c1622 100644 --- a/clippy_lints/src/manual_let_else.rs +++ b/clippy_lints/src/manual_let_else.rs @@ -6,17 +6,18 @@ use clippy_utils::source::snippet_with_context; use clippy_utils::ty::is_type_diagnostic_item; use clippy_utils::visitors::{Descend, Visitable}; use if_chain::if_chain; -use rustc_data_structures::fx::FxHashSet; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_errors::Applicability; use rustc_hir::intravisit::{walk_expr, Visitor}; use rustc_hir::{Expr, ExprKind, HirId, ItemId, Local, MatchSource, Pat, PatKind, QPath, Stmt, StmtKind, Ty}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_tool_lint, impl_lint_pass}; -use rustc_span::symbol::sym; +use rustc_span::symbol::{sym, Symbol}; use rustc_span::Span; use serde::Deserialize; use std::ops::ControlFlow; +use std::slice; declare_clippy_lint! { /// ### What it does @@ -81,11 +82,11 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse { { match if_let_or_match { IfLetOrMatch::IfLet(if_let_expr, let_pat, if_then, if_else) => if_chain! { - if expr_is_simple_identity(let_pat, if_then); + if let Some(ident_map) = expr_simple_identity_map(local.pat, let_pat, if_then); if let Some(if_else) = if_else; if expr_diverges(cx, if_else); then { - emit_manual_let_else(cx, stmt.span, if_let_expr, local.pat, let_pat, if_else); + emit_manual_let_else(cx, stmt.span, if_let_expr, &ident_map, let_pat, if_else); } }, IfLetOrMatch::Match(match_expr, arms, source) => { @@ -118,11 +119,11 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse { return; } let pat_arm = &arms[1 - idx]; - if !expr_is_simple_identity(pat_arm.pat, pat_arm.body) { - return; - } + let Some(ident_map) = expr_simple_identity_map(local.pat, pat_arm.pat, pat_arm.body) else { + return + }; - emit_manual_let_else(cx, stmt.span, match_expr, local.pat, pat_arm.pat, diverging_arm.body); + emit_manual_let_else(cx, stmt.span, match_expr, &ident_map, pat_arm.pat, diverging_arm.body); }, } }; @@ -135,7 +136,7 @@ fn emit_manual_let_else( cx: &LateContext<'_>, span: Span, expr: &Expr<'_>, - local: &Pat<'_>, + ident_map: &FxHashMap<Symbol, &Pat<'_>>, pat: &Pat<'_>, else_body: &Expr<'_>, ) { @@ -146,8 +147,8 @@ fn emit_manual_let_else( "this could be rewritten as `let...else`", |diag| { // This is far from perfect, for example there needs to be: - // * tracking for multi-binding cases: let (foo, bar) = if let (Some(foo), Ok(bar)) = ... - // * renamings of the bindings for many `PatKind`s like structs, slices, etc. + // * renamings of the bindings for many `PatKind`s like slices, etc. + // * limitations in the existing replacement algorithms // * unused binding collision detection with existing ones // for this to be machine applicable. let mut app = Applicability::HasPlaceholders; @@ -159,57 +160,126 @@ fn emit_manual_let_else( } else { format!("{{ {sn_else} }}") }; - let sn_bl = replace_in_pattern(cx, span, local, pat, &mut app); + let sn_bl = replace_in_pattern(cx, span, ident_map, pat, &mut app, true); let sugg = format!("let {sn_bl} = {sn_expr} else {else_bl};"); diag.span_suggestion(span, "consider writing", sugg, app); }, ); } -// replaces the locals in the pattern +/// Replaces the locals in the pattern +/// +/// For this example: +/// +/// ```ignore +/// let (a, FooBar { b, c }) = if let Bar { Some(a_i), b_i } = ex { (a_i, b_i) } else { return }; +/// ``` +/// +/// We have: +/// +/// ```ignore +/// pat: Bar { Some(a_i), b_i } +/// ident_map: (a_i) -> (a), (b_i) -> (FooBar { b, c }) +/// ``` +/// +/// We return: +/// +/// ```ignore +/// Bar { Some(a), b_i: FooBar { b, c } } +/// ``` fn replace_in_pattern( cx: &LateContext<'_>, span: Span, - local: &Pat<'_>, + ident_map: &FxHashMap<Symbol, &Pat<'_>>, pat: &Pat<'_>, app: &mut Applicability, + top_level: bool, ) -> String { - let mut bindings_count = 0; - pat.each_binding_or_first(&mut |_, _, _, _| bindings_count += 1); - // If the pattern creates multiple bindings, exit early, - // as otherwise we might paste the pattern to the positions of multiple bindings. - if bindings_count > 1 { - let (sn_pat, _) = snippet_with_context(cx, pat.span, span.ctxt(), "", app); - return sn_pat.into_owned(); - } + // We put a labeled block here so that we can implement the fallback in this function. + // As the function has multiple call sites, implementing the fallback via an Option<T> + // return type and unwrap_or_else would cause repetition. Similarly, the function also + // invokes the fall back multiple times. + 'a: { + // If the ident map is empty, there is no replacement to do. + // The code following this if assumes a non-empty ident_map. + if ident_map.is_empty() { + break 'a; + } - match pat.kind { - PatKind::Binding(..) => { - let (sn_bdg, _) = snippet_with_context(cx, local.span, span.ctxt(), "", app); - return sn_bdg.to_string(); - }, - PatKind::Or(pats) => { - let patterns = pats - .iter() - .map(|pat| replace_in_pattern(cx, span, local, pat, app)) - .collect::<Vec<_>>(); - let or_pat = patterns.join(" | "); - return format!("({or_pat})"); - }, - // Replace the variable name iff `TupleStruct` has one argument like `Variant(v)`. - PatKind::TupleStruct(ref w, args, dot_dot_pos) => { - let mut args = args - .iter() - .map(|pat| replace_in_pattern(cx, span, local, pat, app)) - .collect::<Vec<_>>(); - if let Some(pos) = dot_dot_pos.as_opt_usize() { - args.insert(pos, "..".to_owned()); - } - let args = args.join(", "); - let sn_wrapper = cx.sess().source_map().span_to_snippet(w.span()).unwrap_or_default(); - return format!("{sn_wrapper}({args})"); - }, - _ => {}, + match pat.kind { + PatKind::Binding(_ann, _id, binding_name, opt_subpt) => { + let Some(pat_to_put) = ident_map.get(&binding_name.name) else { break 'a }; + let (sn_ptp, _) = snippet_with_context(cx, pat_to_put.span, span.ctxt(), "", app); + if let Some(subpt) = opt_subpt { + let subpt = replace_in_pattern(cx, span, ident_map, subpt, app, false); + return format!("{sn_ptp} @ {subpt}"); + } + return sn_ptp.to_string(); + }, + PatKind::Or(pats) => { + let patterns = pats + .iter() + .map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false)) + .collect::<Vec<_>>(); + let or_pat = patterns.join(" | "); + if top_level { + return format!("({or_pat})"); + } + return or_pat; + }, + PatKind::Struct(path, fields, has_dot_dot) => { + let fields = fields + .iter() + .map(|fld| { + if let PatKind::Binding(_, _, name, None) = fld.pat.kind && + let Some(pat_to_put) = ident_map.get(&name.name) + { + let (sn_fld_name, _) = snippet_with_context(cx, fld.ident.span, span.ctxt(), "", app); + let (sn_ptp, _) = snippet_with_context(cx, pat_to_put.span, span.ctxt(), "", app); + // TODO: this is a bit of a hack, but it does its job. Ideally, we'd check if pat_to_put is + // a PatKind::Binding but that is also hard to get right. + if sn_fld_name == sn_ptp { + // Field init shorthand + return format!("{sn_fld_name}"); + } + return format!("{sn_fld_name}: {sn_ptp}"); + } + let (sn_fld, _) = snippet_with_context(cx, fld.span, span.ctxt(), "", app); + sn_fld.into_owned() + }) + .collect::<Vec<_>>(); + let fields_string = fields.join(", "); + + let dot_dot_str = if has_dot_dot { " .." } else { "" }; + let (sn_pth, _) = snippet_with_context(cx, path.span(), span.ctxt(), "", app); + return format!("{sn_pth} {{ {fields_string}{dot_dot_str} }}"); + }, + // Replace the variable name iff `TupleStruct` has one argument like `Variant(v)`. + PatKind::TupleStruct(ref w, args, dot_dot_pos) => { + let mut args = args + .iter() + .map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false)) + .collect::<Vec<_>>(); + if let Some(pos) = dot_dot_pos.as_opt_usize() { + args.insert(pos, "..".to_owned()); + } + let args = args.join(", "); + let sn_wrapper = cx.sess().source_map().span_to_snippet(w.span()).unwrap_or_default(); + return format!("{sn_wrapper}({args})"); + }, + PatKind::Tuple(args, dot_dot_pos) => { + let mut args = args + .iter() + .map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false)) + .collect::<Vec<_>>(); + if let Some(pos) = dot_dot_pos.as_opt_usize() { + args.insert(pos, "..".to_owned()); + } + let args = args.join(", "); + return format!("({args})"); + }, + _ => {}, + } } let (sn_pat, _) = snippet_with_context(cx, pat.span, span.ctxt(), "", app); sn_pat.into_owned() @@ -353,37 +423,74 @@ fn pat_allowed_for_else(cx: &LateContext<'_>, pat: &'_ Pat<'_>, check_types: boo !has_disallowed } -/// Checks if the passed block is a simple identity referring to bindings created by the pattern -fn expr_is_simple_identity(pat: &'_ Pat<'_>, expr: &'_ Expr<'_>) -> bool { - // We support patterns with multiple bindings and tuples, like: - // let ... = if let (Some(foo), bar) = g() { (foo, bar) } else { ... } +/// Checks if the passed block is a simple identity referring to bindings created by the pattern, +/// and if yes, returns a mapping between the relevant sub-pattern and the identifier it corresponds +/// to. +/// +/// We support patterns with multiple bindings and tuples, e.g.: +/// +/// ```ignore +/// let (foo_o, bar_o) = if let (Some(foo), bar) = g() { (foo, bar) } else { ... } +/// ``` +/// +/// The expected params would be: +/// +/// ```ignore +/// local_pat: (foo_o, bar_o) +/// let_pat: (Some(foo), bar) +/// expr: (foo, bar) +/// ``` +/// +/// We build internal `sub_pats` so that it looks like `[foo_o, bar_o]` and `paths` so that it looks +/// like `[foo, bar]`. Then we turn that into `FxHashMap [(foo) -> (foo_o), (bar) -> (bar_o)]` which +/// we return. +fn expr_simple_identity_map<'a, 'hir>( + local_pat: &'a Pat<'hir>, + let_pat: &'_ Pat<'hir>, + expr: &'_ Expr<'hir>, +) -> Option<FxHashMap<Symbol, &'a Pat<'hir>>> { let peeled = peel_blocks(expr); - let paths = match peeled.kind { - ExprKind::Tup(exprs) | ExprKind::Array(exprs) => exprs, - ExprKind::Path(_) => std::slice::from_ref(peeled), - _ => return false, + let (sub_pats, paths) = match (local_pat.kind, peeled.kind) { + (PatKind::Tuple(pats, _), ExprKind::Tup(exprs)) | (PatKind::Slice(pats, ..), ExprKind::Array(exprs)) => { + (pats, exprs) + }, + (_, ExprKind::Path(_)) => (slice::from_ref(local_pat), slice::from_ref(peeled)), + _ => return None, }; + + // There is some length mismatch, which indicates usage of .. in the patterns above e.g.: + // let (a, ..) = if let [a, b, _c] = ex { (a, b) } else { ... }; + // We bail in these cases as they should be rare. + if paths.len() != sub_pats.len() { + return None; + } + let mut pat_bindings = FxHashSet::default(); - pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| { + let_pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| { pat_bindings.insert(ident); }); if pat_bindings.len() < paths.len() { - return false; + // This rebinds some bindings from the outer scope, or it repeats some copy-able bindings multiple + // times. We don't support these cases so we bail here. E.g.: + // let foo = 0; + // let (new_foo, bar, bar_copied) = if let Some(bar) = Some(0) { (foo, bar, bar) } else { .. }; + return None; } - for path in paths { - if_chain! { - if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind; - if let [path_seg] = path.segments; - then { - if !pat_bindings.remove(&path_seg.ident) { - return false; - } - } else { - return false; + let mut ident_map = FxHashMap::default(); + for (sub_pat, path) in sub_pats.iter().zip(paths.iter()) { + if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind && + let [path_seg] = path.segments + { + let ident = path_seg.ident; + if !pat_bindings.remove(&ident) { + return None; } + ident_map.insert(ident.name, sub_pat); + } else { + return None; } } - true + Some(ident_map) } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize)] diff --git a/clippy_lints/src/manual_range_patterns.rs b/clippy_lints/src/manual_range_patterns.rs new file mode 100644 index 00000000000..65ff555209a --- /dev/null +++ b/clippy_lints/src/manual_range_patterns.rs @@ -0,0 +1,123 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::LitKind; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_hir::ExprKind; +use rustc_hir::PatKind; +use rustc_hir::RangeEnd; +use rustc_lint::LintContext; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Looks for combined OR patterns that are all contained in a specific range, + /// e.g. `6 | 4 | 5 | 9 | 7 | 8` can be rewritten as `4..=9`. + /// + /// ### Why is this bad? + /// Using an explicit range is more concise and easier to read. + /// + /// ### Example + /// ```rust + /// let x = 6; + /// let foo = matches!(x, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10); + /// ``` + /// Use instead: + /// ```rust + /// let x = 6; + /// let foo = matches!(x, 1..=10); + /// ``` + #[clippy::version = "1.72.0"] + pub MANUAL_RANGE_PATTERNS, + complexity, + "manually writing range patterns using a combined OR pattern (`|`)" +} +declare_lint_pass!(ManualRangePatterns => [MANUAL_RANGE_PATTERNS]); + +fn expr_as_u128(expr: &Expr<'_>) -> Option<u128> { + if let ExprKind::Lit(lit) = expr.kind + && let LitKind::Int(num, _) = lit.node + { + Some(num) + } else { + None + } +} + +impl LateLintPass<'_> for ManualRangePatterns { + fn check_pat(&mut self, cx: &LateContext<'_>, pat: &'_ rustc_hir::Pat<'_>) { + if in_external_macro(cx.sess(), pat.span) { + return; + } + + // a pattern like 1 | 2 seems fine, lint if there are at least 3 alternatives + if let PatKind::Or(pats) = pat.kind + && pats.len() >= 3 + { + let mut min = u128::MAX; + let mut max = 0; + let mut numbers_found = FxHashSet::default(); + let mut ranges_found = Vec::new(); + + for pat in pats { + if let PatKind::Lit(lit) = pat.kind + && let Some(num) = expr_as_u128(lit) + { + numbers_found.insert(num); + + min = min.min(num); + max = max.max(num); + } else if let PatKind::Range(Some(left), Some(right), end) = pat.kind + && let Some(left) = expr_as_u128(left) + && let Some(right) = expr_as_u128(right) + && right >= left + { + min = min.min(left); + max = max.max(right); + ranges_found.push(left..=match end { + RangeEnd::Included => right, + RangeEnd::Excluded => right - 1, + }); + } else { + return; + } + } + + let contains_whole_range = 'contains: { + let mut num = min; + while num <= max { + if numbers_found.contains(&num) { + num += 1; + } + // Given a list of (potentially overlapping) ranges like: + // 1..=5, 3..=7, 6..=10 + // We want to find the range with the highest end that still contains the current number + else if let Some(range) = ranges_found + .iter() + .filter(|range| range.contains(&num)) + .max_by_key(|range| range.end()) + { + num = range.end() + 1; + } else { + break 'contains false; + } + } + break 'contains true; + }; + + if contains_whole_range { + span_lint_and_sugg( + cx, + MANUAL_RANGE_PATTERNS, + pat.span, + "this OR pattern can be rewritten using a range", + "try", + format!("{min}..={max}"), + Applicability::MachineApplicable, + ); + } + } + } +} diff --git a/clippy_lints/src/matches/match_same_arms.rs b/clippy_lints/src/matches/match_same_arms.rs index ae8262ace96..3d2fbea63f5 100644 --- a/clippy_lints/src/matches/match_same_arms.rs +++ b/clippy_lints/src/matches/match_same_arms.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::source::snippet; -use clippy_utils::{path_to_local, search_same, SpanlessEq, SpanlessHash}; +use clippy_utils::{is_lint_allowed, path_to_local, search_same, SpanlessEq, SpanlessHash}; use core::cmp::Ordering; use core::iter; use core::slice; @@ -9,6 +9,7 @@ use rustc_ast::ast::LitKind; use rustc_errors::Applicability; use rustc_hir::def_id::DefId; use rustc_hir::{Arm, Expr, ExprKind, HirId, HirIdMap, HirIdMapEntry, HirIdSet, Pat, PatKind, RangeEnd}; +use rustc_lint::builtin::NON_EXHAUSTIVE_OMITTED_PATTERNS; use rustc_lint::LateContext; use rustc_middle::ty; use rustc_span::Symbol; @@ -103,17 +104,21 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) { let indexed_arms: Vec<(usize, &Arm<'_>)> = arms.iter().enumerate().collect(); for (&(i, arm1), &(j, arm2)) in search_same(&indexed_arms, hash, eq) { if matches!(arm2.pat.kind, PatKind::Wild) { - span_lint_and_then( - cx, - MATCH_SAME_ARMS, - arm1.span, - "this match arm has an identical body to the `_` wildcard arm", - |diag| { - diag.span_suggestion(arm1.span, "try removing the arm", "", Applicability::MaybeIncorrect) - .help("or try changing either arm body") - .span_note(arm2.span, "`_` wildcard arm here"); - }, - ); + if !cx.tcx.features().non_exhaustive_omitted_patterns_lint + || is_lint_allowed(cx, NON_EXHAUSTIVE_OMITTED_PATTERNS, arm2.hir_id) + { + span_lint_and_then( + cx, + MATCH_SAME_ARMS, + arm1.span, + "this match arm has an identical body to the `_` wildcard arm", + |diag| { + diag.span_suggestion(arm1.span, "try removing the arm", "", Applicability::MaybeIncorrect) + .help("or try changing either arm body") + .span_note(arm2.span, "`_` wildcard arm here"); + }, + ); + } } else { let back_block = backwards_blocking_idxs[j]; let (keep_arm, move_arm) = if back_block < i || (back_block == 0 && forwards_blocking_idxs[i] <= j) { diff --git a/clippy_lints/src/matches/match_wild_err_arm.rs b/clippy_lints/src/matches/match_wild_err_arm.rs index 6424ac31d9f..de911f7a028 100644 --- a/clippy_lints/src/matches/match_wild_err_arm.rs +++ b/clippy_lints/src/matches/match_wild_err_arm.rs @@ -25,7 +25,7 @@ pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, ex: &Expr<'tcx>, arms: &[Arm<' let mut ident_bind_name = kw::Underscore; if !matching_wild { // Looking for unused bindings (i.e.: `_e`) - for pat in inner.iter() { + for pat in inner { if let PatKind::Binding(_, id, ident, None) = pat.kind { if ident.as_str().starts_with('_') && !is_local_used(cx, arm.body, id) { ident_bind_name = ident.name; diff --git a/clippy_lints/src/matches/mod.rs b/clippy_lints/src/matches/mod.rs index 0d91051632a..00fa3eb9bfe 100644 --- a/clippy_lints/src/matches/mod.rs +++ b/clippy_lints/src/matches/mod.rs @@ -38,6 +38,11 @@ declare_clippy_lint! { /// Checks for matches with a single arm where an `if let` /// will usually suffice. /// + /// This intentionally does not lint if there are comments + /// inside of the other arm, so as to allow the user to document + /// why having another explicit pattern with an empty body is necessary, + /// or because the comments need to be preserved for other reasons. + /// /// ### Why is this bad? /// Just readability – `if let` nests less than a `match`. /// @@ -559,6 +564,9 @@ declare_clippy_lint! { /// ### What it does /// Checks for `match` with identical arm bodies. /// + /// Note: Does not lint on wildcards if the `non_exhaustive_omitted_patterns_lint` feature is + /// enabled and disallowed. + /// /// ### Why is this bad? /// This is probably a copy & paste error. If arm bodies /// are the same on purpose, you can factor them @@ -777,7 +785,7 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does - /// Check for temporaries returned from function calls in a match scrutinee that have the + /// Checks for temporaries returned from function calls in a match scrutinee that have the /// `clippy::has_significant_drop` attribute. /// /// ### Why is this bad? diff --git a/clippy_lints/src/matches/overlapping_arms.rs b/clippy_lints/src/matches/overlapping_arms.rs index abf2525a61c..8be3c178a29 100644 --- a/clippy_lints/src/matches/overlapping_arms.rs +++ b/clippy_lints/src/matches/overlapping_arms.rs @@ -41,7 +41,7 @@ fn all_ranges<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>], ty: Ty<'tcx>) cx.tcx.valtree_to_const_val((ty, min_val_const.to_valtree())), ty, ); - miri_to_const(cx.tcx, min_constant)? + miri_to_const(cx, min_constant)? }, }; let rhs_const = match rhs { @@ -52,7 +52,7 @@ fn all_ranges<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>], ty: Ty<'tcx>) cx.tcx.valtree_to_const_val((ty, max_val_const.to_valtree())), ty, ); - miri_to_const(cx.tcx, max_constant)? + miri_to_const(cx, max_constant)? }, }; let lhs_val = lhs_const.int_value(cx, ty)?; diff --git a/clippy_lints/src/matches/significant_drop_in_scrutinee.rs b/clippy_lints/src/matches/significant_drop_in_scrutinee.rs index 93ef07d36ae..37528d9f7f2 100644 --- a/clippy_lints/src/matches/significant_drop_in_scrutinee.rs +++ b/clippy_lints/src/matches/significant_drop_in_scrutinee.rs @@ -140,7 +140,7 @@ impl<'a, 'tcx> SigDropChecker<'a, 'tcx> { } } - for generic_arg in b.iter() { + for generic_arg in *b { if let GenericArgKind::Type(ty) = generic_arg.unpack() { if self.has_sig_drop_attr(cx, ty) { return true; diff --git a/clippy_lints/src/matches/single_match.rs b/clippy_lints/src/matches/single_match.rs index ad47c13896c..35627d6c649 100644 --- a/clippy_lints/src/matches/single_match.rs +++ b/clippy_lints/src/matches/single_match.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{expr_block, snippet}; +use clippy_utils::source::{expr_block, get_source_text, snippet}; use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, peel_mid_ty_refs}; use clippy_utils::{is_lint_allowed, is_unit_expr, is_wild, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs}; use core::cmp::max; @@ -7,10 +7,26 @@ use rustc_errors::Applicability; use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, Pat, PatKind}; use rustc_lint::LateContext; use rustc_middle::ty::{self, Ty}; -use rustc_span::sym; +use rustc_span::{sym, Span}; use super::{MATCH_BOOL, SINGLE_MATCH, SINGLE_MATCH_ELSE}; +/// Checks if there are comments contained within a span. +/// This is a very "naive" check, as it just looks for the literal characters // and /* in the +/// source text. This won't be accurate if there are potentially expressions contained within the +/// span, e.g. a string literal `"//"`, but we know that this isn't the case for empty +/// match arms. +fn empty_arm_has_comment(cx: &LateContext<'_>, span: Span) -> bool { + if let Some(ff) = get_source_text(cx, span) + && let Some(text) = ff.as_str() + { + text.as_bytes().windows(2) + .any(|w| w == b"//" || w == b"/*") + } else { + false + } +} + #[rustfmt::skip] pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() { @@ -25,7 +41,7 @@ pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: return; } let els = arms[1].body; - let els = if is_unit_expr(peel_blocks(els)) { + let els = if is_unit_expr(peel_blocks(els)) && !empty_arm_has_comment(cx, els.span) { None } else if let ExprKind::Block(Block { stmts, expr: block_expr, .. }, _) = els.kind { if stmts.len() == 1 && block_expr.is_none() || stmts.is_empty() && block_expr.is_some() { @@ -35,7 +51,7 @@ pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: // block with 2+ statements or 1 expr and 1+ statement Some(els) } else { - // not a block, don't lint + // not a block or an emtpy block w/ comments, don't lint return; }; diff --git a/clippy_lints/src/matches/try_err.rs b/clippy_lints/src/matches/try_err.rs index 704c34c32bf..3a7f1e034f3 100644 --- a/clippy_lints/src/matches/try_err.rs +++ b/clippy_lints/src/matches/try_err.rs @@ -81,7 +81,7 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, scrutine /// Finds function return type by examining return expressions in match arms. fn find_return_type<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx ExprKind<'_>) -> Option<Ty<'tcx>> { if let ExprKind::Match(_, arms, MatchSource::TryDesugar) = expr { - for arm in arms.iter() { + for arm in *arms { if let ExprKind::Ret(Some(ret)) = arm.body.kind { return Some(cx.typeck_results().expr_ty(ret)); } diff --git a/clippy_lints/src/mem_forget.rs b/clippy_lints/src/mem_forget.rs deleted file mode 100644 index d6c235b5a69..00000000000 --- a/clippy_lints/src/mem_forget.rs +++ /dev/null @@ -1,46 +0,0 @@ -use clippy_utils::diagnostics::span_lint; -use rustc_hir::{Expr, ExprKind}; -use rustc_lint::{LateContext, LateLintPass}; -use rustc_session::{declare_lint_pass, declare_tool_lint}; -use rustc_span::sym; - -declare_clippy_lint! { - /// ### What it does - /// Checks for usage of `std::mem::forget(t)` where `t` is - /// `Drop`. - /// - /// ### Why is this bad? - /// `std::mem::forget(t)` prevents `t` from running its - /// destructor, possibly causing leaks. - /// - /// ### Example - /// ```rust - /// # use std::mem; - /// # use std::rc::Rc; - /// mem::forget(Rc::new(55)) - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MEM_FORGET, - restriction, - "`mem::forget` usage on `Drop` types, likely to cause memory leaks" -} - -declare_lint_pass!(MemForget => [MEM_FORGET]); - -impl<'tcx> LateLintPass<'tcx> for MemForget { - fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { - if let ExprKind::Call(path_expr, [ref first_arg, ..]) = e.kind { - if let ExprKind::Path(ref qpath) = path_expr.kind { - if let Some(def_id) = cx.qpath_res(qpath, path_expr.hir_id).opt_def_id() { - if cx.tcx.is_diagnostic_item(sym::mem_forget, def_id) { - let forgot_ty = cx.typeck_results().expr_ty(first_arg); - - if forgot_ty.ty_adt_def().map_or(false, |def| def.has_dtor(cx.tcx)) { - span_lint(cx, MEM_FORGET, e.span, "usage of `mem::forget` on `Drop` type"); - } - } - } - } - } - } -} diff --git a/clippy_lints/src/methods/drain_collect.rs b/clippy_lints/src/methods/drain_collect.rs new file mode 100644 index 00000000000..d0c79dc11b0 --- /dev/null +++ b/clippy_lints/src/methods/drain_collect.rs @@ -0,0 +1,85 @@ +use crate::methods::DRAIN_COLLECT; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_range_full; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_lang_item; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_hir::ExprKind; +use rustc_hir::LangItem; +use rustc_hir::Path; +use rustc_hir::QPath; +use rustc_lint::LateContext; +use rustc_middle::query::Key; +use rustc_middle::ty; +use rustc_middle::ty::Ty; +use rustc_span::sym; +use rustc_span::Symbol; + +/// Checks if both types match the given diagnostic item, e.g.: +/// +/// `vec![1,2].drain(..).collect::<Vec<_>>()` +/// ^^^^^^^^^ ^^^^^^ true +/// `vec![1,2].drain(..).collect::<HashSet<_>>()` +/// ^^^^^^^^^ ^^^^^^^^^^ false +fn types_match_diagnostic_item(cx: &LateContext<'_>, expr: Ty<'_>, recv: Ty<'_>, sym: Symbol) -> bool { + if let Some(expr_adt_did) = expr.ty_adt_id() + && let Some(recv_adt_did) = recv.ty_adt_id() + { + cx.tcx.is_diagnostic_item(sym, expr_adt_did) && cx.tcx.is_diagnostic_item(sym, recv_adt_did) + } else { + false + } +} + +/// Checks `std::{vec::Vec, collections::VecDeque}`. +fn check_vec(cx: &LateContext<'_>, args: &[Expr<'_>], expr: Ty<'_>, recv: Ty<'_>, recv_path: &Path<'_>) -> bool { + (types_match_diagnostic_item(cx, expr, recv, sym::Vec) + || types_match_diagnostic_item(cx, expr, recv, sym::VecDeque)) + && matches!(args, [arg] if is_range_full(cx, arg, Some(recv_path))) +} + +/// Checks `std::string::String` +fn check_string(cx: &LateContext<'_>, args: &[Expr<'_>], expr: Ty<'_>, recv: Ty<'_>, recv_path: &Path<'_>) -> bool { + is_type_lang_item(cx, expr, LangItem::String) + && is_type_lang_item(cx, recv, LangItem::String) + && matches!(args, [arg] if is_range_full(cx, arg, Some(recv_path))) +} + +/// Checks `std::collections::{HashSet, HashMap, BinaryHeap}`. +fn check_collections(cx: &LateContext<'_>, expr: Ty<'_>, recv: Ty<'_>) -> Option<&'static str> { + types_match_diagnostic_item(cx, expr, recv, sym::HashSet) + .then_some("HashSet") + .or_else(|| types_match_diagnostic_item(cx, expr, recv, sym::HashMap).then_some("HashMap")) + .or_else(|| types_match_diagnostic_item(cx, expr, recv, sym::BinaryHeap).then_some("BinaryHeap")) +} + +pub(super) fn check(cx: &LateContext<'_>, args: &[Expr<'_>], expr: &Expr<'_>, recv: &Expr<'_>) { + let expr_ty = cx.typeck_results().expr_ty(expr); + let recv_ty = cx.typeck_results().expr_ty(recv); + let recv_ty_no_refs = recv_ty.peel_refs(); + + if let ExprKind::Path(QPath::Resolved(_, recv_path)) = recv.kind + && let Some(typename) = check_vec(cx, args, expr_ty, recv_ty_no_refs, recv_path) + .then_some("Vec") + .or_else(|| check_string(cx, args, expr_ty, recv_ty_no_refs, recv_path).then_some("String")) + .or_else(|| check_collections(cx, expr_ty, recv_ty_no_refs)) + { + let recv = snippet(cx, recv.span, "<expr>"); + let sugg = if let ty::Ref(..) = recv_ty.kind() { + format!("std::mem::take({recv})") + } else { + format!("std::mem::take(&mut {recv})") + }; + + span_lint_and_sugg( + cx, + DRAIN_COLLECT, + expr.span, + &format!("you seem to be trying to move all elements into a new `{typename}`"), + "consider using `mem::take`", + sugg, + Applicability::MachineApplicable, + ); + } +} diff --git a/clippy_lints/src/methods/get_unwrap.rs b/clippy_lints/src/methods/get_unwrap.rs index ffc3a4d780e..e35fb12ed79 100644 --- a/clippy_lints/src/methods/get_unwrap.rs +++ b/clippy_lints/src/methods/get_unwrap.rs @@ -3,7 +3,6 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::get_parent_expr; use clippy_utils::source::snippet_with_applicability; use clippy_utils::ty::is_type_diagnostic_item; -use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir as hir; use rustc_lint::LateContext; @@ -23,21 +22,15 @@ pub(super) fn check<'tcx>( let mut applicability = Applicability::MachineApplicable; let expr_ty = cx.typeck_results().expr_ty(recv); let get_args_str = snippet_with_applicability(cx, get_arg.span, "..", &mut applicability); - let mut needs_ref; let caller_type = if derefs_to_slice(cx, recv, expr_ty).is_some() { - needs_ref = get_args_str.parse::<usize>().is_ok(); "slice" } else if is_type_diagnostic_item(cx, expr_ty, sym::Vec) { - needs_ref = get_args_str.parse::<usize>().is_ok(); "Vec" } else if is_type_diagnostic_item(cx, expr_ty, sym::VecDeque) { - needs_ref = get_args_str.parse::<usize>().is_ok(); "VecDeque" } else if !is_mut && is_type_diagnostic_item(cx, expr_ty, sym::HashMap) { - needs_ref = true; "HashMap" } else if !is_mut && is_type_diagnostic_item(cx, expr_ty, sym::BTreeMap) { - needs_ref = true; "BTreeMap" } else { return; // caller is not a type that we want to lint @@ -45,18 +38,24 @@ pub(super) fn check<'tcx>( let mut span = expr.span; - // Handle the case where the result is immediately dereferenced - // by not requiring ref and pulling the dereference into the - // suggestion. - if_chain! { - if needs_ref; - if let Some(parent) = get_parent_expr(cx, expr); - if let hir::ExprKind::Unary(hir::UnOp::Deref, _) = parent.kind; - then { - needs_ref = false; + // Handle the case where the result is immediately dereferenced, + // either directly be the user, or as a result of a method call or the like + // by not requiring an explicit reference + let needs_ref = if let Some(parent) = get_parent_expr(cx, expr) + && let hir::ExprKind::Unary(hir::UnOp::Deref, _) + | hir::ExprKind::MethodCall(..) + | hir::ExprKind::Field(..) + | hir::ExprKind::Index(..) = parent.kind + { + if let hir::ExprKind::Unary(hir::UnOp::Deref, _) = parent.kind { + // if the user explicitly dereferences the result, we can adjust + // the span to also include the deref part span = parent.span; } - } + false + } else { + true + }; let mut_str = if is_mut { "_mut" } else { "" }; let borrow_str = if !needs_ref { diff --git a/clippy_lints/src/methods/iter_nth.rs b/clippy_lints/src/methods/iter_nth.rs index ceee12784cb..12104310405 100644 --- a/clippy_lints/src/methods/iter_nth.rs +++ b/clippy_lints/src/methods/iter_nth.rs @@ -20,9 +20,9 @@ pub(super) fn check<'tcx>( let caller_type = if derefs_to_slice(cx, iter_recv, cx.typeck_results().expr_ty(iter_recv)).is_some() { "slice" } else if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(iter_recv), sym::Vec) { - "Vec" + "`Vec`" } else if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(iter_recv), sym::VecDeque) { - "VecDeque" + "`VecDeque`" } else { iter_nth_zero::check(cx, expr, nth_recv, nth_arg); return; // caller is not a type that we want to lint diff --git a/clippy_lints/src/methods/iter_nth_zero.rs b/clippy_lints/src/methods/iter_nth_zero.rs index d1609eebfdc..e1f950d5a4a 100644 --- a/clippy_lints/src/methods/iter_nth_zero.rs +++ b/clippy_lints/src/methods/iter_nth_zero.rs @@ -1,8 +1,8 @@ use clippy_utils::consts::{constant, Constant}; use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::is_trait_method; use clippy_utils::source::snippet_with_applicability; -use if_chain::if_chain; +use clippy_utils::{is_lang_item_or_ctor, is_trait_method}; +use hir::{LangItem, OwnerNode}; use rustc_errors::Applicability; use rustc_hir as hir; use rustc_lint::LateContext; @@ -11,20 +11,21 @@ use rustc_span::sym; use super::ITER_NTH_ZERO; pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>, arg: &hir::Expr<'_>) { - if_chain! { - if is_trait_method(cx, expr, sym::Iterator); - if let Some(Constant::Int(0)) = constant(cx, cx.typeck_results(), arg); - then { - let mut applicability = Applicability::MachineApplicable; - span_lint_and_sugg( - cx, - ITER_NTH_ZERO, - expr.span, - "called `.nth(0)` on a `std::iter::Iterator`, when `.next()` is equivalent", - "try calling `.next()` instead of `.nth(0)`", - format!("{}.next()", snippet_with_applicability(cx, recv.span, "..", &mut applicability)), - applicability, - ); - } + if let OwnerNode::Item(item) = cx.tcx.hir().owner(cx.tcx.hir().get_parent_item(expr.hir_id)) + && let def_id = item.owner_id.to_def_id() + && is_trait_method(cx, expr, sym::Iterator) + && let Some(Constant::Int(0)) = constant(cx, cx.typeck_results(), arg) + && !is_lang_item_or_ctor(cx, def_id, LangItem::IteratorNext) + { + let mut app = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + ITER_NTH_ZERO, + expr.span, + "called `.nth(0)` on a `std::iter::Iterator`, when `.next()` is equivalent", + "try calling `.next()` instead of `.nth(0)`", + format!("{}.next()", snippet_with_applicability(cx, recv.span, "..", &mut app)), + app, + ); } } diff --git a/clippy_lints/src/methods/manual_try_fold.rs b/clippy_lints/src/methods/manual_try_fold.rs new file mode 100644 index 00000000000..576a58499b1 --- /dev/null +++ b/clippy_lints/src/methods/manual_try_fold.rs @@ -0,0 +1,55 @@ +use clippy_utils::{ + diagnostics::span_lint_and_sugg, + is_from_proc_macro, + msrvs::{Msrv, ITERATOR_TRY_FOLD}, + source::snippet_opt, + ty::implements_trait, +}; +use rustc_errors::Applicability; +use rustc_hir::{ + def::{DefKind, Res}, + Expr, ExprKind, +}; +use rustc_lint::{LateContext, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_span::Span; + +use super::MANUAL_TRY_FOLD; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'tcx>, + init: &Expr<'_>, + acc: &Expr<'_>, + fold_span: Span, + msrv: &Msrv, +) { + if !in_external_macro(cx.sess(), fold_span) + && msrv.meets(ITERATOR_TRY_FOLD) + && let init_ty = cx.typeck_results().expr_ty(init) + && let Some(try_trait) = cx.tcx.lang_items().try_trait() + && implements_trait(cx, init_ty, try_trait, &[]) + && let ExprKind::Call(path, [first, rest @ ..]) = init.kind + && let ExprKind::Path(qpath) = path.kind + && let Res::Def(DefKind::Ctor(_, _), _) = cx.qpath_res(&qpath, path.hir_id) + && let ExprKind::Closure(closure) = acc.kind + && !is_from_proc_macro(cx, expr) + && let Some(args_snip) = closure.fn_arg_span.and_then(|fn_arg_span| snippet_opt(cx, fn_arg_span)) + { + let init_snip = rest + .is_empty() + .then_some(first.span) + .and_then(|span| snippet_opt(cx, span)) + .unwrap_or("...".to_owned()); + + span_lint_and_sugg( + cx, + MANUAL_TRY_FOLD, + fold_span, + "usage of `Iterator::fold` on a type that implements `Try`", + "use `try_fold` instead", + format!("try_fold({init_snip}, {args_snip} ...)", ), + Applicability::HasPlaceholders, + ); + } +} diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs index 88cbefbb5d3..24dbe8c1d75 100644 --- a/clippy_lints/src/methods/mod.rs +++ b/clippy_lints/src/methods/mod.rs @@ -14,6 +14,7 @@ mod clone_on_copy; mod clone_on_ref_ptr; mod cloned_instead_of_copied; mod collapsible_str_replace; +mod drain_collect; mod err_expect; mod expect_fun_call; mod expect_used; @@ -49,6 +50,7 @@ mod manual_next_back; mod manual_ok_or; mod manual_saturating_arithmetic; mod manual_str_repeat; +mod manual_try_fold; mod map_clone; mod map_collect_result_unit; mod map_err_ignore; @@ -93,6 +95,7 @@ mod unnecessary_fold; mod unnecessary_iter_cloned; mod unnecessary_join; mod unnecessary_lazy_eval; +mod unnecessary_literal_unwrap; mod unnecessary_sort_by; mod unnecessary_to_owned; mod unwrap_or_else_default; @@ -275,6 +278,32 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does + /// Checks for `.unwrap()` related calls on `Result`s and `Option`s that are constructed. + /// + /// ### Why is this bad? + /// It is better to write the value directly without the indirection. + /// + /// ### Examples + /// ```rust + /// let val1 = Some(1).unwrap(); + /// let val2 = Ok::<_, ()>(1).unwrap(); + /// let val3 = Err::<(), _>(1).unwrap_err(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let val1 = 1; + /// let val2 = 1; + /// let val3 = 1; + /// ``` + #[clippy::version = "1.69.0"] + pub UNNECESSARY_LITERAL_UNWRAP, + complexity, + "using `unwrap()` related calls on `Result` and `Option` constructors" +} + +declare_clippy_lint! { + /// ### What it does /// Checks for `.expect()` or `.expect_err()` calls on `Result`s and `.expect()` call on `Option`s. /// /// ### Why is this bad? @@ -485,6 +514,7 @@ declare_clippy_lint! { /// # let result: Result<usize, ()> = Ok(1); /// # fn some_function(foo: ()) -> usize { 1 } /// option.map(|a| a + 1).unwrap_or(0); + /// option.map(|a| a > 10).unwrap_or(false); /// result.map(|a| a + 1).unwrap_or_else(some_function); /// ``` /// @@ -494,6 +524,7 @@ declare_clippy_lint! { /// # let result: Result<usize, ()> = Ok(1); /// # fn some_function(foo: ()) -> usize { 1 } /// option.map_or(0, |a| a + 1); + /// option.is_some_and(|a| a > 10); /// result.map_or_else(some_function, |a| a + 1); /// ``` #[clippy::version = "1.45.0"] @@ -3220,6 +3251,71 @@ declare_clippy_lint! { "manual reverse iteration of `DoubleEndedIterator`" } +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `.drain()` that clear the collection, immediately followed by a call to `.collect()`. + /// + /// > "Collection" in this context refers to any type with a `drain` method: + /// > `Vec`, `VecDeque`, `BinaryHeap`, `HashSet`,`HashMap`, `String` + /// + /// ### Why is this bad? + /// Using `mem::take` is faster as it avoids the allocation. + /// When using `mem::take`, the old collection is replaced with an empty one and ownership of + /// the old collection is returned. + /// + /// ### Known issues + /// `mem::take(&mut vec)` is almost equivalent to `vec.drain(..).collect()`, except that + /// it also moves the **capacity**. The user might have explicitly written it this way + /// to keep the capacity on the original `Vec`. + /// + /// ### Example + /// ```rust + /// fn remove_all(v: &mut Vec<i32>) -> Vec<i32> { + /// v.drain(..).collect() + /// } + /// ``` + /// Use instead: + /// ```rust + /// use std::mem; + /// fn remove_all(v: &mut Vec<i32>) -> Vec<i32> { + /// mem::take(v) + /// } + /// ``` + #[clippy::version = "1.71.0"] + pub DRAIN_COLLECT, + perf, + "calling `.drain(..).collect()` to move all elements into a new collection" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `Iterator::fold` with a type that implements `Try`. + /// + /// ### Why is this bad? + /// The code should use `try_fold` instead, which short-circuits on failure, thus opening the + /// door for additional optimizations not possible with `fold` as rustc can guarantee the + /// function is never called on `None`, `Err`, etc., alleviating otherwise necessary checks. It's + /// also slightly more idiomatic. + /// + /// ### Known issues + /// This lint doesn't take into account whether a function does something on the failure case, + /// i.e., whether short-circuiting will affect behavior. Refactoring to `try_fold` is not + /// desirable in those cases. + /// + /// ### Example + /// ```rust + /// vec![1, 2, 3].iter().fold(Some(0i32), |sum, i| sum?.checked_add(*i)); + /// ``` + /// Use instead: + /// ```rust + /// vec![1, 2, 3].iter().try_fold(0i32, |sum, i| sum.checked_add(*i)); + /// ``` + #[clippy::version = "1.72.0"] + pub MANUAL_TRY_FOLD, + perf, + "checks for usage of `Iterator::fold` with a type that implements `Try`" +} + pub struct Methods { avoid_breaking_exported_api: bool, msrv: Msrv, @@ -3349,6 +3445,9 @@ impl_lint_pass!(Methods => [ SUSPICIOUS_COMMAND_ARG_SPACE, CLEAR_WITH_DRAIN, MANUAL_NEXT_BACK, + UNNECESSARY_LITERAL_UNWRAP, + DRAIN_COLLECT, + MANUAL_TRY_FOLD, ]); /// Extracts a method call name, args, and `Span` of the method name. @@ -3578,6 +3677,9 @@ impl Methods { manual_str_repeat::check(cx, expr, recv, take_self_arg, take_arg); } }, + Some(("drain", recv, args, ..)) => { + drain_collect::check(cx, args, expr, recv); + } _ => {}, } }, @@ -3606,12 +3708,18 @@ impl Methods { case_sensitive_file_extension_comparisons::check(cx, expr, span, recv, arg); } }, - ("expect", [_]) => match method_call(recv) { - Some(("ok", recv, [], _, _)) => ok_expect::check(cx, expr, recv), - Some(("err", recv, [], err_span, _)) => err_expect::check(cx, expr, recv, span, err_span, &self.msrv), - _ => expect_used::check(cx, expr, recv, false, self.allow_expect_in_tests), + ("expect", [_]) => { + match method_call(recv) { + Some(("ok", recv, [], _, _)) => ok_expect::check(cx, expr, recv), + Some(("err", recv, [], err_span, _)) => err_expect::check(cx, expr, recv, span, err_span, &self.msrv), + _ => expect_used::check(cx, expr, recv, false, self.allow_expect_in_tests), + } + unnecessary_literal_unwrap::check(cx, expr, recv, name, args); + }, + ("expect_err", [_]) => { + unnecessary_literal_unwrap::check(cx, expr, recv, name, args); + expect_used::check(cx, expr, recv, true, self.allow_expect_in_tests); }, - ("expect_err", [_]) => expect_used::check(cx, expr, recv, true, self.allow_expect_in_tests), ("extend", [arg]) => { string_extend_chars::check(cx, expr, recv, arg); extend_with_drain::check(cx, expr, recv, arg); @@ -3632,7 +3740,10 @@ impl Methods { Some(("cloned", recv2, [], _, _)) => iter_overeager_cloned::check(cx, expr, recv, recv2, false, true), _ => {}, }, - ("fold", [init, acc]) => unnecessary_fold::check(cx, expr, init, acc, span), + ("fold", [init, acc]) => { + manual_try_fold::check(cx, expr, init, acc, call_span, &self.msrv); + unnecessary_fold::check(cx, expr, init, acc, span); + }, ("for_each", [_]) => { if let Some(("inspect", _, [_], span2, _)) = method_call(recv) { inspect_for_each::check(cx, expr, span2); @@ -3816,28 +3927,41 @@ impl Methods { }, _ => {}, } + unnecessary_literal_unwrap::check(cx, expr, recv, name, args); unwrap_used::check(cx, expr, recv, false, self.allow_unwrap_in_tests); }, - ("unwrap_err", []) => unwrap_used::check(cx, expr, recv, true, self.allow_unwrap_in_tests), - ("unwrap_or", [u_arg]) => match method_call(recv) { - Some((arith @ ("checked_add" | "checked_sub" | "checked_mul"), lhs, [rhs], _, _)) => { - manual_saturating_arithmetic::check(cx, expr, lhs, rhs, u_arg, &arith["checked_".len()..]); - }, - Some(("map", m_recv, [m_arg], span, _)) => { - option_map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span); - }, - Some(("then_some", t_recv, [t_arg], _, _)) => { - obfuscated_if_else::check(cx, expr, t_recv, t_arg, u_arg); - }, - _ => {}, + ("unwrap_err", []) => { + unnecessary_literal_unwrap::check(cx, expr, recv, name, args); + unwrap_used::check(cx, expr, recv, true, self.allow_unwrap_in_tests); }, - ("unwrap_or_else", [u_arg]) => match method_call(recv) { - 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"); - }, + ("unwrap_or", [u_arg]) => { + match method_call(recv) { + Some((arith @ ("checked_add" | "checked_sub" | "checked_mul"), lhs, [rhs], _, _)) => { + manual_saturating_arithmetic::check(cx, expr, lhs, rhs, u_arg, &arith["checked_".len()..]); + }, + Some(("map", m_recv, [m_arg], span, _)) => { + option_map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, &self.msrv); + }, + Some(("then_some", t_recv, [t_arg], _, _)) => { + obfuscated_if_else::check(cx, expr, t_recv, t_arg, u_arg); + }, + _ => {}, + } + unnecessary_literal_unwrap::check(cx, expr, recv, name, args); + }, + ("unwrap_or_default", []) => { + unnecessary_literal_unwrap::check(cx, expr, recv, name, args); + } + ("unwrap_or_else", [u_arg]) => { + match method_call(recv) { + 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"); + }, + } + unnecessary_literal_unwrap::check(cx, expr, recv, name, args); }, ("zip", [arg]) => { if let ExprKind::MethodCall(name, iter_recv, [], _) = recv.kind diff --git a/clippy_lints/src/methods/needless_collect.rs b/clippy_lints/src/methods/needless_collect.rs index 0699997d0d2..ac209d01406 100644 --- a/clippy_lints/src/methods/needless_collect.rs +++ b/clippy_lints/src/methods/needless_collect.rs @@ -322,7 +322,7 @@ impl<'tcx> Visitor<'tcx> for IterFunctionVisitor<'_, 'tcx> { fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { // Check function calls on our collection - if let ExprKind::MethodCall(method_name, recv, [args @ ..], _) = &expr.kind { + if let ExprKind::MethodCall(method_name, recv, args, _) = &expr.kind { if method_name.ident.name == sym!(collect) && is_trait_method(self.cx, expr, sym::Iterator) { self.current_mutably_captured_ids = get_captured_ids(self.cx, self.cx.typeck_results().expr_ty(recv)); self.visit_expr(recv); diff --git a/clippy_lints/src/methods/option_map_unwrap_or.rs b/clippy_lints/src/methods/option_map_unwrap_or.rs index 4c6328481e4..f4f158c0439 100644 --- a/clippy_lints/src/methods/option_map_unwrap_or.rs +++ b/clippy_lints/src/methods/option_map_unwrap_or.rs @@ -1,19 +1,26 @@ use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::snippet_with_applicability; use clippy_utils::ty::is_copy; use clippy_utils::ty::is_type_diagnostic_item; use rustc_data_structures::fx::FxHashSet; use rustc_errors::Applicability; +use rustc_hir::def::Res; use rustc_hir::intravisit::{walk_path, Visitor}; +use rustc_hir::ExprKind; +use rustc_hir::Node; +use rustc_hir::PatKind; +use rustc_hir::QPath; use rustc_hir::{self, HirId, Path}; use rustc_lint::LateContext; use rustc_middle::hir::nested_filter; use rustc_span::source_map::Span; -use rustc_span::{sym, Symbol}; +use rustc_span::sym; use super::MAP_UNWRAP_OR; /// lint use of `map().unwrap_or()` for `Option`s +#[expect(clippy::too_many_arguments)] pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, expr: &rustc_hir::Expr<'_>, @@ -22,12 +29,27 @@ pub(super) fn check<'tcx>( unwrap_recv: &rustc_hir::Expr<'_>, unwrap_arg: &'tcx rustc_hir::Expr<'_>, map_span: Span, + msrv: &Msrv, ) { // lint if the caller of `map()` is an `Option` if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option) { if !is_copy(cx, cx.typeck_results().expr_ty(unwrap_arg)) { - // Do not lint if the `map` argument uses identifiers in the `map` - // argument that are also used in the `unwrap_or` argument + // Replacing `.map(<f>).unwrap_or(<a>)` with `.map_or(<a>, <f>)` can sometimes lead to + // borrowck errors, see #10579 for one such instance. + // In particular, if `a` causes a move and `f` references that moved binding, then we cannot lint: + // ``` + // let x = vec![1, 2]; + // x.get(0..1).map(|s| s.to_vec()).unwrap_or(x); + // ``` + // This compiles, but changing it to `map_or` will produce a compile error: + // ``` + // let x = vec![1, 2]; + // x.get(0..1).map_or(x, |s| s.to_vec()) + // ^ moving `x` here + // ^^^^^^^^^^^ while it is borrowed here (and later used in the closure) + // ``` + // So, we have to check that `a` is not referenced anywhere (even outside of the `.map` closure!) + // before the call to `unwrap_or`. let mut unwrap_visitor = UnwrapVisitor { cx, @@ -35,14 +57,18 @@ pub(super) fn check<'tcx>( }; unwrap_visitor.visit_expr(unwrap_arg); - let mut map_expr_visitor = MapExprVisitor { + let mut reference_visitor = ReferenceVisitor { cx, identifiers: unwrap_visitor.identifiers, - found_identifier: false, + found_reference: false, + unwrap_or_span: unwrap_arg.span, }; - map_expr_visitor.visit_expr(map_arg); - if map_expr_visitor.found_identifier { + let map = cx.tcx.hir(); + let body = map.body(map.body_owned_by(map.enclosing_body_owner(expr.hir_id))); + reference_visitor.visit_body(body); + + if reference_visitor.found_reference { return; } } @@ -51,16 +77,29 @@ pub(super) fn check<'tcx>( return; } + // is_some_and is stabilised && `unwrap_or` argument is false; suggest `is_some_and` instead + let suggest_is_some_and = msrv.meets(msrvs::OPTION_IS_SOME_AND) + && matches!(&unwrap_arg.kind, ExprKind::Lit(lit) + if matches!(lit.node, rustc_ast::LitKind::Bool(false))); + let mut applicability = Applicability::MachineApplicable; // get snippet for unwrap_or() let unwrap_snippet = snippet_with_applicability(cx, unwrap_arg.span, "..", &mut applicability); // lint message // comparing the snippet from source to raw text ("None") below is safe // because we already have checked the type. - let arg = if unwrap_snippet == "None" { "None" } else { "<a>" }; + let arg = if unwrap_snippet == "None" { + "None" + } else if suggest_is_some_and { + "false" + } else { + "<a>" + }; let unwrap_snippet_none = unwrap_snippet == "None"; let suggest = if unwrap_snippet_none { "and_then(<f>)" + } else if suggest_is_some_and { + "is_some_and(<f>)" } else { "map_or(<a>, <f>)" }; @@ -75,12 +114,18 @@ pub(super) fn check<'tcx>( let mut suggestion = vec![ ( map_span, - String::from(if unwrap_snippet_none { "and_then" } else { "map_or" }), + String::from(if unwrap_snippet_none { + "and_then" + } else if suggest_is_some_and { + "is_some_and" + } else { + "map_or" + }), ), (expr.span.with_lo(unwrap_recv.span.hi()), String::new()), ]; - if !unwrap_snippet_none { + if !unwrap_snippet_none && !suggest_is_some_and { suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{unwrap_snippet}, "))); } @@ -91,14 +136,19 @@ pub(super) fn check<'tcx>( struct UnwrapVisitor<'a, 'tcx> { cx: &'a LateContext<'tcx>, - identifiers: FxHashSet<Symbol>, + identifiers: FxHashSet<HirId>, } impl<'a, 'tcx> Visitor<'tcx> for UnwrapVisitor<'a, 'tcx> { type NestedFilter = nested_filter::All; - fn visit_path(&mut self, path: &Path<'tcx>, _id: HirId) { - self.identifiers.insert(ident(path)); + fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) { + if let Res::Local(local_id) = path.res + && let Some(Node::Pat(pat)) = self.cx.tcx.hir().find(local_id) + && let PatKind::Binding(_, local_id, ..) = pat.kind + { + self.identifiers.insert(local_id); + } walk_path(self, path); } @@ -107,32 +157,35 @@ impl<'a, 'tcx> Visitor<'tcx> for UnwrapVisitor<'a, 'tcx> { } } -struct MapExprVisitor<'a, 'tcx> { +struct ReferenceVisitor<'a, 'tcx> { cx: &'a LateContext<'tcx>, - identifiers: FxHashSet<Symbol>, - found_identifier: bool, + identifiers: FxHashSet<HirId>, + found_reference: bool, + unwrap_or_span: Span, } -impl<'a, 'tcx> Visitor<'tcx> for MapExprVisitor<'a, 'tcx> { +impl<'a, 'tcx> Visitor<'tcx> for ReferenceVisitor<'a, 'tcx> { type NestedFilter = nested_filter::All; - - fn visit_path(&mut self, path: &Path<'tcx>, _id: HirId) { - if self.identifiers.contains(&ident(path)) { - self.found_identifier = true; - return; + fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) { + // If we haven't found a reference yet, check if this references + // one of the locals that was moved in the `unwrap_or` argument. + // We are only interested in exprs that appear before the `unwrap_or` call. + if !self.found_reference { + if expr.span < self.unwrap_or_span + && let ExprKind::Path(ref path) = expr.kind + && let QPath::Resolved(_, path) = path + && let Res::Local(local_id) = path.res + && let Some(Node::Pat(pat)) = self.cx.tcx.hir().find(local_id) + && let PatKind::Binding(_, local_id, ..) = pat.kind + && self.identifiers.contains(&local_id) + { + self.found_reference = true; + } + rustc_hir::intravisit::walk_expr(self, expr); } - walk_path(self, path); } fn nested_visit_map(&mut self) -> Self::Map { self.cx.tcx.hir() } } - -fn ident(path: &Path<'_>) -> Symbol { - path.segments - .last() - .expect("segments should be composed of at least 1 element") - .ident - .name -} diff --git a/clippy_lints/src/methods/str_splitn.rs b/clippy_lints/src/methods/str_splitn.rs index 5ea12c44184..88a3c2620a4 100644 --- a/clippy_lints/src/methods/str_splitn.rs +++ b/clippy_lints/src/methods/str_splitn.rs @@ -289,7 +289,7 @@ fn parse_iter_usage<'tcx>( ) -> Option<IterUsage> { let (kind, span) = match iter.next() { Some((_, Node::Expr(e))) if e.span.ctxt() == ctxt => { - let ExprKind::MethodCall(name, _, [args @ ..], _) = e.kind else { + let ExprKind::MethodCall(name, _, args, _) = e.kind else { return None; }; let did = cx.typeck_results().type_dependent_def_id(e.hir_id)?; diff --git a/clippy_lints/src/methods/unnecessary_fold.rs b/clippy_lints/src/methods/unnecessary_fold.rs index 5a3d12fd790..8ec15a1c1b7 100644 --- a/clippy_lints/src/methods/unnecessary_fold.rs +++ b/clippy_lints/src/methods/unnecessary_fold.rs @@ -7,71 +7,120 @@ use rustc_errors::Applicability; use rustc_hir as hir; use rustc_hir::PatKind; use rustc_lint::LateContext; +use rustc_middle::ty; use rustc_span::{source_map::Span, sym}; use super::UNNECESSARY_FOLD; -pub(super) fn check( +/// Do we need to suggest turbofish when suggesting a replacement method? +/// Changing `fold` to `sum` needs it sometimes when the return type can't be +/// inferred. This checks for some common cases where it can be safely omitted +fn needs_turbofish(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool { + let parent = cx.tcx.hir().get_parent(expr.hir_id); + + // some common cases where turbofish isn't needed: + // - assigned to a local variable with a type annotation + if let hir::Node::Local(local) = parent + && local.ty.is_some() + { + return false; + } + + // - part of a function call argument, can be inferred from the function signature (provided that + // the parameter is not a generic type parameter) + if let hir::Node::Expr(parent_expr) = parent + && let hir::ExprKind::Call(recv, args) = parent_expr.kind + && let hir::ExprKind::Path(ref qpath) = recv.kind + && let Some(fn_def_id) = cx.qpath_res(qpath, recv.hir_id).opt_def_id() + && let fn_sig = cx.tcx.fn_sig(fn_def_id).skip_binder().skip_binder() + && let Some(arg_pos) = args.iter().position(|arg| arg.hir_id == expr.hir_id) + && let Some(ty) = fn_sig.inputs().get(arg_pos) + && !matches!(ty.kind(), ty::Param(_)) + { + return false; + } + + // if it's neither of those, stay on the safe side and suggest turbofish, + // even if it could work! + true +} + +#[derive(Copy, Clone)] +struct Replacement { + method_name: &'static str, + has_args: bool, + has_generic_return: bool, +} + +fn check_fold_with_op( cx: &LateContext<'_>, expr: &hir::Expr<'_>, - init: &hir::Expr<'_>, acc: &hir::Expr<'_>, fold_span: Span, + op: hir::BinOpKind, + replacement: Replacement, ) { - fn check_fold_with_op( - cx: &LateContext<'_>, - expr: &hir::Expr<'_>, - acc: &hir::Expr<'_>, - fold_span: Span, - op: hir::BinOpKind, - replacement_method_name: &str, - replacement_has_args: bool, - ) { - if_chain! { - // Extract the body of the closure passed to fold - if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = acc.kind; - let closure_body = cx.tcx.hir().body(body); - let closure_expr = peel_blocks(closure_body.value); - - // Check if the closure body is of the form `acc <op> some_expr(x)` - if let hir::ExprKind::Binary(ref bin_op, left_expr, right_expr) = closure_expr.kind; - if bin_op.node == op; - - // Extract the names of the two arguments to the closure - if let [param_a, param_b] = closure_body.params; - if let PatKind::Binding(_, first_arg_id, ..) = strip_pat_refs(param_a.pat).kind; - if let PatKind::Binding(_, second_arg_id, second_arg_ident, _) = strip_pat_refs(param_b.pat).kind; - - if path_to_local_id(left_expr, first_arg_id); - if replacement_has_args || path_to_local_id(right_expr, second_arg_id); - - then { - let mut applicability = Applicability::MachineApplicable; - let sugg = if replacement_has_args { - format!( - "{replacement_method_name}(|{second_arg_ident}| {r})", - r = snippet_with_applicability(cx, right_expr.span, "EXPR", &mut applicability), - ) - } else { - format!( - "{replacement_method_name}()", - ) - }; - - span_lint_and_sugg( - cx, - UNNECESSARY_FOLD, - fold_span.with_hi(expr.span.hi()), - // TODO #2371 don't suggest e.g., .any(|x| f(x)) if we can suggest .any(f) - "this `.fold` can be written more succinctly using another method", - "try", - sugg, - applicability, - ); - } + if_chain! { + // Extract the body of the closure passed to fold + if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = acc.kind; + let closure_body = cx.tcx.hir().body(body); + let closure_expr = peel_blocks(closure_body.value); + + // Check if the closure body is of the form `acc <op> some_expr(x)` + if let hir::ExprKind::Binary(ref bin_op, left_expr, right_expr) = closure_expr.kind; + if bin_op.node == op; + + // Extract the names of the two arguments to the closure + if let [param_a, param_b] = closure_body.params; + if let PatKind::Binding(_, first_arg_id, ..) = strip_pat_refs(param_a.pat).kind; + if let PatKind::Binding(_, second_arg_id, second_arg_ident, _) = strip_pat_refs(param_b.pat).kind; + + if path_to_local_id(left_expr, first_arg_id); + if replacement.has_args || path_to_local_id(right_expr, second_arg_id); + + then { + let mut applicability = Applicability::MachineApplicable; + + let turbofish = if replacement.has_generic_return { + format!("::<{}>", cx.typeck_results().expr_ty_adjusted(right_expr).peel_refs()) + } else { + String::new() + }; + + let sugg = if replacement.has_args { + format!( + "{method}{turbofish}(|{second_arg_ident}| {r})", + method = replacement.method_name, + r = snippet_with_applicability(cx, right_expr.span, "EXPR", &mut applicability), + ) + } else { + format!( + "{method}{turbofish}()", + method = replacement.method_name, + ) + }; + + span_lint_and_sugg( + cx, + UNNECESSARY_FOLD, + fold_span.with_hi(expr.span.hi()), + // TODO #2371 don't suggest e.g., .any(|x| f(x)) if we can suggest .any(f) + "this `.fold` can be written more succinctly using another method", + "try", + sugg, + applicability, + ); } } +} +pub(super) fn check( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + init: &hir::Expr<'_>, + acc: &hir::Expr<'_>, + fold_span: Span, +) { // Check that this is a call to Iterator::fold rather than just some function called fold if !is_trait_method(cx, expr, sym::Iterator) { return; @@ -80,11 +129,59 @@ pub(super) fn check( // Check if the first argument to .fold is a suitable literal if let hir::ExprKind::Lit(lit) = init.kind { match lit.node { - ast::LitKind::Bool(false) => check_fold_with_op(cx, expr, acc, fold_span, hir::BinOpKind::Or, "any", true), - ast::LitKind::Bool(true) => check_fold_with_op(cx, expr, acc, fold_span, hir::BinOpKind::And, "all", true), - ast::LitKind::Int(0, _) => check_fold_with_op(cx, expr, acc, fold_span, hir::BinOpKind::Add, "sum", false), + ast::LitKind::Bool(false) => { + check_fold_with_op( + cx, + expr, + acc, + fold_span, + hir::BinOpKind::Or, + Replacement { + has_args: true, + has_generic_return: false, + method_name: "any", + }, + ); + }, + ast::LitKind::Bool(true) => { + check_fold_with_op( + cx, + expr, + acc, + fold_span, + hir::BinOpKind::And, + Replacement { + has_args: true, + has_generic_return: false, + method_name: "all", + }, + ); + }, + ast::LitKind::Int(0, _) => check_fold_with_op( + cx, + expr, + acc, + fold_span, + hir::BinOpKind::Add, + Replacement { + has_args: false, + has_generic_return: needs_turbofish(cx, expr), + method_name: "sum", + }, + ), ast::LitKind::Int(1, _) => { - check_fold_with_op(cx, expr, acc, fold_span, hir::BinOpKind::Mul, "product", false); + check_fold_with_op( + cx, + expr, + acc, + fold_span, + hir::BinOpKind::Mul, + Replacement { + has_args: false, + has_generic_return: needs_turbofish(cx, expr), + method_name: "product", + }, + ); }, _ => (), } diff --git a/clippy_lints/src/methods/unnecessary_literal_unwrap.rs b/clippy_lints/src/methods/unnecessary_literal_unwrap.rs new file mode 100644 index 00000000000..7877f6a386c --- /dev/null +++ b/clippy_lints/src/methods/unnecessary_literal_unwrap.rs @@ -0,0 +1,95 @@ +use clippy_utils::{diagnostics::span_lint_and_then, is_res_lang_ctor, last_path_segment, path_res, MaybePath}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; + +use super::UNNECESSARY_LITERAL_UNWRAP; + +fn get_ty_from_args<'a>(args: Option<&'a [hir::GenericArg<'a>]>, index: usize) -> Option<&'a hir::Ty<'a>> { + let args = args?; + + if args.len() <= index { + return None; + } + + match args[index] { + hir::GenericArg::Type(ty) => match ty.kind { + hir::TyKind::Infer => None, + _ => Some(ty), + }, + _ => None, + } +} + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + recv: &hir::Expr<'_>, + method: &str, + args: &[hir::Expr<'_>], +) { + let init = clippy_utils::expr_or_init(cx, recv); + + let (constructor, call_args, ty) = if let hir::ExprKind::Call(call, call_args) = init.kind { + let Some(qpath) = call.qpath_opt() else { return }; + + let args = last_path_segment(qpath).args.map(|args| args.args); + let res = cx.qpath_res(qpath, call.hir_id()); + + if is_res_lang_ctor(cx, res, hir::LangItem::OptionSome) { + ("Some", call_args, get_ty_from_args(args, 0)) + } else if is_res_lang_ctor(cx, res, hir::LangItem::ResultOk) { + ("Ok", call_args, get_ty_from_args(args, 0)) + } else if is_res_lang_ctor(cx, res, hir::LangItem::ResultErr) { + ("Err", call_args, get_ty_from_args(args, 1)) + } else { + return; + } + } else if is_res_lang_ctor(cx, path_res(cx, init), hir::LangItem::OptionNone) { + let call_args: &[hir::Expr<'_>] = &[]; + ("None", call_args, None) + } else { + return; + }; + + let help_message = format!("used `{method}()` on `{constructor}` value"); + let suggestion_message = format!("remove the `{constructor}` and `{method}()`"); + + span_lint_and_then(cx, UNNECESSARY_LITERAL_UNWRAP, expr.span, &help_message, |diag| { + let suggestions = match (constructor, method, ty) { + ("None", "unwrap", _) => Some(vec![(expr.span, "panic!()".to_string())]), + ("None", "expect", _) => Some(vec![ + (expr.span.with_hi(args[0].span.lo()), "panic!(".to_string()), + (expr.span.with_lo(args[0].span.hi()), ")".to_string()), + ]), + (_, _, Some(_)) => None, + ("Ok", "unwrap_err", None) | ("Err", "unwrap", None) => Some(vec![ + ( + recv.span.with_hi(call_args[0].span.lo()), + "panic!(\"{:?}\", ".to_string(), + ), + (expr.span.with_lo(call_args[0].span.hi()), ")".to_string()), + ]), + ("Ok", "expect_err", None) | ("Err", "expect", None) => Some(vec![ + ( + recv.span.with_hi(call_args[0].span.lo()), + "panic!(\"{1}: {:?}\", ".to_string(), + ), + (call_args[0].span.with_lo(args[0].span.lo()), ", ".to_string()), + ]), + (_, _, None) => Some(vec![ + (recv.span.with_hi(call_args[0].span.lo()), String::new()), + (expr.span.with_lo(call_args[0].span.hi()), String::new()), + ]), + }; + + match (init.span == recv.span, suggestions) { + (true, Some(suggestions)) => { + diag.multipart_suggestion(suggestion_message, suggestions, Applicability::MachineApplicable); + }, + _ => { + diag.span_help(init.span, suggestion_message); + }, + } + }); +} diff --git a/clippy_lints/src/methods/unnecessary_to_owned.rs b/clippy_lints/src/methods/unnecessary_to_owned.rs index 80eb00a1b42..16ef24bd4e3 100644 --- a/clippy_lints/src/methods/unnecessary_to_owned.rs +++ b/clippy_lints/src/methods/unnecessary_to_owned.rs @@ -144,6 +144,11 @@ fn check_addr_of_expr( if let Some(deref_trait_id) = cx.tcx.get_diagnostic_item(sym::Deref); if implements_trait(cx, receiver_ty, deref_trait_id, &[]); if cx.get_associated_type(receiver_ty, deref_trait_id, "Target") == Some(target_ty); + // Make sure that it's actually calling the right `.to_string()`, (#10033) + // *or* this is a `Cow::into_owned()` call (which would be the wrong into_owned receiver (str != Cow) + // but that's ok for Cow::into_owned specifically) + if cx.typeck_results().expr_ty_adjusted(receiver).peel_refs() == target_ty + || is_cow_into_owned(cx, method_name, method_def_id); then { if n_receiver_refs > 0 { span_lint_and_sugg( diff --git a/clippy_lints/src/min_ident_chars.rs b/clippy_lints/src/min_ident_chars.rs new file mode 100644 index 00000000000..d49bb0ca6e2 --- /dev/null +++ b/clippy_lints/src/min_ident_chars.rs @@ -0,0 +1,153 @@ +use clippy_utils::{diagnostics::span_lint, is_from_proc_macro}; +use rustc_data_structures::fx::FxHashSet; +use rustc_hir::{ + def::{DefKind, Res}, + intravisit::{walk_item, Visitor}, + GenericParamKind, HirId, Item, ItemKind, ItemLocalId, Node, Pat, PatKind, +}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; +use std::borrow::Cow; + +declare_clippy_lint! { + /// ### What it does + /// Checks for idents which comprise of a single letter. + /// + /// Note: This lint can be very noisy when enabled; it may be desirable to only enable it + /// temporarily. + /// + /// ### Why is this bad? + /// In many cases it's not, but at times it can severely hinder readability. Some codebases may + /// wish to disallow this to improve readability. + /// + /// ### Example + /// ```rust,ignore + /// for m in movies { + /// let title = m.t; + /// } + /// ``` + /// Use instead: + /// ```rust,ignore + /// for movie in movies { + /// let title = movie.title; + /// } + /// ``` + #[clippy::version = "1.72.0"] + pub MIN_IDENT_CHARS, + restriction, + "disallows idents that are too short" +} +impl_lint_pass!(MinIdentChars => [MIN_IDENT_CHARS]); + +#[derive(Clone)] +pub struct MinIdentChars { + pub allowed_idents_below_min_chars: FxHashSet<String>, + pub min_ident_chars_threshold: u64, +} + +impl MinIdentChars { + #[expect(clippy::cast_possible_truncation)] + fn is_ident_too_short(&self, cx: &LateContext<'_>, str: &str, span: Span) -> bool { + !in_external_macro(cx.sess(), span) + && str.len() <= self.min_ident_chars_threshold as usize + && !str.starts_with('_') + && !str.is_empty() + && self.allowed_idents_below_min_chars.get(&str.to_owned()).is_none() + } +} + +impl LateLintPass<'_> for MinIdentChars { + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if self.min_ident_chars_threshold == 0 { + return; + } + + walk_item(&mut IdentVisitor { conf: self, cx }, item); + } + + // This is necessary as `Node::Pat`s are not visited in `visit_id`. :/ + fn check_pat(&mut self, cx: &LateContext<'_>, pat: &Pat<'_>) { + if let PatKind::Binding(_, _, ident, ..) = pat.kind + && let str = ident.as_str() + && self.is_ident_too_short(cx, str, ident.span) + { + emit_min_ident_chars(self, cx, str, ident.span); + } + } +} + +struct IdentVisitor<'cx, 'tcx> { + conf: &'cx MinIdentChars, + cx: &'cx LateContext<'tcx>, +} + +impl Visitor<'_> for IdentVisitor<'_, '_> { + fn visit_id(&mut self, hir_id: HirId) { + let Self { conf, cx } = *self; + // FIXME(#112534) Reimplementation of `find`, as it uses indexing, which can (and will in + // async functions, or really anything async) panic. This should probably be fixed on the + // rustc side, this is just a temporary workaround. + let node = if hir_id.local_id == ItemLocalId::from_u32(0) { + // In this case, we can just use `find`, `Owner`'s `node` field is private anyway so we can't + // reimplement it even if we wanted to + cx.tcx.hir().find(hir_id) + } else { + let Some(owner) = cx.tcx.hir_owner_nodes(hir_id.owner).as_owner() else { + return; + }; + owner.nodes.get(hir_id.local_id).copied().flatten().map(|p| p.node) + }; + let Some(node) = node else { + return; + }; + let Some(ident) = node.ident() else { + return; + }; + + let str = ident.as_str(); + if conf.is_ident_too_short(cx, str, ident.span) { + if let Node::Item(item) = node && let ItemKind::Use(..) = item.kind { + return; + } + // `struct Awa<T>(T)` + // ^ + if let Node::PathSegment(path) = node { + if let Res::Def(def_kind, ..) = path.res && let DefKind::TyParam = def_kind { + return; + } + if matches!(path.res, Res::PrimTy(..)) || path.res.opt_def_id().is_some_and(|def_id| !def_id.is_local()) + { + return; + } + } + // `struct Awa<T>(T)` + // ^ + if let Node::GenericParam(generic_param) = node + && let GenericParamKind::Type { .. } = generic_param.kind + { + return; + } + + if is_from_proc_macro(cx, &ident) { + return; + } + + emit_min_ident_chars(conf, cx, str, ident.span); + } + } +} + +fn emit_min_ident_chars(conf: &MinIdentChars, cx: &impl LintContext, ident: &str, span: Span) { + let help = if conf.min_ident_chars_threshold == 1 { + Cow::Borrowed("this ident consists of a single char") + } else { + Cow::Owned(format!( + "this ident is too short ({} <= {})", + ident.len(), + conf.min_ident_chars_threshold, + )) + }; + span_lint(cx, MIN_IDENT_CHARS, span, &help); +} diff --git a/clippy_lints/src/minmax.rs b/clippy_lints/src/minmax.rs index 4f967755bfa..e0904f17b8d 100644 --- a/clippy_lints/src/minmax.rs +++ b/clippy_lints/src/minmax.rs @@ -66,7 +66,7 @@ enum MinMax { Max, } -fn min_max<'a>(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<(MinMax, Constant, &'a Expr<'a>)> { +fn min_max<'a, 'tcx>(cx: &LateContext<'tcx>, expr: &'a Expr<'a>) -> Option<(MinMax, Constant<'tcx>, &'a Expr<'a>)> { match expr.kind { ExprKind::Call(path, args) => { if let ExprKind::Path(ref qpath) = path.kind { @@ -99,12 +99,12 @@ fn min_max<'a>(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<(MinMax, Cons } } -fn fetch_const<'a>( - cx: &LateContext<'_>, +fn fetch_const<'a, 'tcx>( + cx: &LateContext<'tcx>, receiver: Option<&'a Expr<'a>>, args: &'a [Expr<'a>], m: MinMax, -) -> Option<(MinMax, Constant, &'a Expr<'a>)> { +) -> Option<(MinMax, Constant<'tcx>, &'a Expr<'a>)> { let mut args = receiver.into_iter().chain(args); let first_arg = args.next()?; let second_arg = args.next()?; diff --git a/clippy_lints/src/misc_early/mixed_case_hex_literals.rs b/clippy_lints/src/misc_early/mixed_case_hex_literals.rs index ddb8b9173a5..9151cc63320 100644 --- a/clippy_lints/src/misc_early/mixed_case_hex_literals.rs +++ b/clippy_lints/src/misc_early/mixed_case_hex_literals.rs @@ -13,7 +13,7 @@ pub(super) fn check(cx: &EarlyContext<'_>, lit_span: Span, suffix: &str, lit_sni return; } let mut seen = (false, false); - for ch in lit_snip.as_bytes()[2..=maybe_last_sep_idx].iter() { + for ch in &lit_snip.as_bytes()[2..=maybe_last_sep_idx] { match ch { b'a'..=b'f' => seen.0 = true, b'A'..=b'F' => seen.1 = true, diff --git a/clippy_lints/src/misc_early/mod.rs b/clippy_lints/src/misc_early/mod.rs index 78be6b9e23f..b226b878123 100644 --- a/clippy_lints/src/misc_early/mod.rs +++ b/clippy_lints/src/misc_early/mod.rs @@ -2,6 +2,7 @@ mod builtin_type_shadow; mod double_neg; mod literal_suffix; mod mixed_case_hex_literals; +mod redundant_at_rest_pattern; mod redundant_pattern; mod unneeded_field_pattern; mod unneeded_wildcard_pattern; @@ -318,6 +319,36 @@ declare_clippy_lint! { "tuple patterns with a wildcard pattern (`_`) is next to a rest pattern (`..`)" } +declare_clippy_lint! { + /// ### What it does + /// Checks for `[all @ ..]` patterns. + /// + /// ### Why is this bad? + /// In all cases, `all` works fine and can often make code simpler, as you possibly won't need + /// to convert from say a `Vec` to a slice by dereferencing. + /// + /// ### Example + /// ```rust,ignore + /// if let [all @ ..] = &*v { + /// // NOTE: Type is a slice here + /// println!("all elements: {all:#?}"); + /// } + /// ``` + /// Use instead: + /// ```rust,ignore + /// if let all = v { + /// // NOTE: Type is a `Vec` here + /// println!("all elements: {all:#?}"); + /// } + /// // or + /// println!("all elements: {v:#?}"); + /// ``` + #[clippy::version = "1.72.0"] + pub REDUNDANT_AT_REST_PATTERN, + complexity, + "checks for `[all @ ..]` where `all` would suffice" +} + declare_lint_pass!(MiscEarlyLints => [ UNNEEDED_FIELD_PATTERN, DUPLICATE_UNDERSCORE_ARGUMENT, @@ -329,6 +360,7 @@ declare_lint_pass!(MiscEarlyLints => [ BUILTIN_TYPE_SHADOW, REDUNDANT_PATTERN, UNNEEDED_WILDCARD_PATTERN, + REDUNDANT_AT_REST_PATTERN, ]); impl EarlyLintPass for MiscEarlyLints { @@ -339,8 +371,13 @@ impl EarlyLintPass for MiscEarlyLints { } fn check_pat(&mut self, cx: &EarlyContext<'_>, pat: &Pat) { + if in_external_macro(cx.sess(), pat.span) { + return; + } + unneeded_field_pattern::check(cx, pat); redundant_pattern::check(cx, pat); + redundant_at_rest_pattern::check(cx, pat); unneeded_wildcard_pattern::check(cx, pat); } diff --git a/clippy_lints/src/misc_early/redundant_at_rest_pattern.rs b/clippy_lints/src/misc_early/redundant_at_rest_pattern.rs new file mode 100644 index 00000000000..0c81ee5eced --- /dev/null +++ b/clippy_lints/src/misc_early/redundant_at_rest_pattern.rs @@ -0,0 +1,26 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::{Pat, PatKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, LintContext}; +use rustc_middle::lint::in_external_macro; + +use super::REDUNDANT_AT_REST_PATTERN; + +pub(super) fn check(cx: &EarlyContext<'_>, pat: &Pat) { + if !in_external_macro(cx.sess(), pat.span) + && let PatKind::Slice(slice) = &pat.kind + && let [one] = &**slice + && let PatKind::Ident(annotation, ident, Some(rest)) = &one.kind + && let PatKind::Rest = rest.kind + { + span_lint_and_sugg( + cx, + REDUNDANT_AT_REST_PATTERN, + pat.span, + "using a rest pattern to bind an entire slice to a local", + "this is better represented with just the binding", + format!("{}{ident}", annotation.prefix_str()), + Applicability::MachineApplicable, + ); + } +} diff --git a/clippy_lints/src/mismatching_type_param_order.rs b/clippy_lints/src/mismatching_type_param_order.rs index 9de4b56b77b..28e041dee0d 100644 --- a/clippy_lints/src/mismatching_type_param_order.rs +++ b/clippy_lints/src/mismatching_type_param_order.rs @@ -59,7 +59,7 @@ impl<'tcx> LateLintPass<'tcx> for TypeParamMismatch { then { // get the name and span of the generic parameters in the Impl let mut impl_params = Vec::new(); - for p in generic_args.args.iter() { + for p in generic_args.args { match p { GenericArg::Type(Ty {kind: TyKind::Path(QPath::Resolved(_, path)), ..}) => impl_params.push((path.segments[0].ident.to_string(), path.span)), diff --git a/clippy_lints/src/missing_fields_in_debug.rs b/clippy_lints/src/missing_fields_in_debug.rs index b6f0de7e504..1138d1163a4 100644 --- a/clippy_lints/src/missing_fields_in_debug.rs +++ b/clippy_lints/src/missing_fields_in_debug.rs @@ -207,6 +207,11 @@ impl<'tcx> LateLintPass<'tcx> for MissingFieldsInDebug { if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), self_ty, items, .. }) = item.kind && let Res::Def(DefKind::Trait, trait_def_id) = trait_ref.path.res && let TyKind::Path(QPath::Resolved(_, self_path)) = &self_ty.kind + // don't trigger if self is a generic parameter, e.g. `impl<T> Debug for T` + // this can only happen in core itself, where the trait is defined, + // but it caused ICEs in the past: + // https://github.com/rust-lang/rust-clippy/issues/10887 + && !matches!(self_path.res, Res::Def(DefKind::TyParam, _)) && cx.match_def_path(trait_def_id, &[sym::core, sym::fmt, sym::Debug]) // don't trigger if this impl was derived && !cx.tcx.has_attr(item.owner_id, sym::automatically_derived) diff --git a/clippy_lints/src/mixed_read_write_in_expression.rs b/clippy_lints/src/mixed_read_write_in_expression.rs index f0be7771bb1..57ec3a1f1e6 100644 --- a/clippy_lints/src/mixed_read_write_in_expression.rs +++ b/clippy_lints/src/mixed_read_write_in_expression.rs @@ -114,7 +114,7 @@ struct DivergenceVisitor<'a, 'tcx> { impl<'a, 'tcx> DivergenceVisitor<'a, 'tcx> { fn maybe_walk_expr(&mut self, e: &'tcx Expr<'_>) { match e.kind { - ExprKind::Closure { .. } => {}, + ExprKind::Closure(..) | ExprKind::If(..) | ExprKind::Loop(..) => {}, ExprKind::Match(e, arms, _) => { self.visit_expr(e); for arm in arms { @@ -128,6 +128,7 @@ impl<'a, 'tcx> DivergenceVisitor<'a, 'tcx> { _ => walk_expr(self, e), } } + fn report_diverging_sub_expr(&mut self, e: &Expr<'_>) { span_lint(self.cx, DIVERGING_SUB_EXPRESSION, e.span, "sub-expression diverges"); } @@ -136,6 +137,15 @@ impl<'a, 'tcx> DivergenceVisitor<'a, 'tcx> { impl<'a, 'tcx> Visitor<'tcx> for DivergenceVisitor<'a, 'tcx> { fn visit_expr(&mut self, e: &'tcx Expr<'_>) { match e.kind { + // fix #10776 + ExprKind::Block(block, ..) => match (block.stmts, block.expr) { + ([], Some(e)) => self.visit_expr(e), + ([stmt], None) => match stmt.kind { + StmtKind::Expr(e) | StmtKind::Semi(e) => self.visit_expr(e), + _ => {}, + }, + _ => {}, + }, ExprKind::Continue(_) | ExprKind::Break(_, _) | ExprKind::Ret(_) => self.report_diverging_sub_expr(e), ExprKind::Call(func, _) => { let typ = self.cx.typeck_results().expr_ty(func); diff --git a/clippy_lints/src/module_style.rs b/clippy_lints/src/module_style.rs index 349fcd2274d..439cae812b7 100644 --- a/clippy_lints/src/module_style.rs +++ b/clippy_lints/src/module_style.rs @@ -2,6 +2,7 @@ use rustc_ast::ast; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_lint::{EarlyContext, EarlyLintPass, Level, LintContext}; use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::def_id::LOCAL_CRATE; use rustc_span::{FileName, SourceFile, Span, SyntaxContext}; use std::ffi::OsStr; use std::path::{Component, Path}; @@ -90,7 +91,14 @@ impl EarlyLintPass for ModStyle { // `{ foo => path/to/foo.rs, .. } let mut file_map = FxHashMap::default(); for file in files.iter() { - if let FileName::Real(name) = &file.name && let Some(lp) = name.local_path() { + if let FileName::Real(name) = &file.name + && let Some(lp) = name.local_path() + && file.cnum == LOCAL_CRATE + { + // [#8887](https://github.com/rust-lang/rust-clippy/issues/8887) + // Only check files in the current crate. + // Fix false positive that crate dependency in workspace sub directory + // is checked unintentionally. let path = if lp.is_relative() { lp } else if let Ok(relative) = lp.strip_prefix(trim_to_src) { diff --git a/clippy_lints/src/needless_if.rs b/clippy_lints/src/needless_if.rs new file mode 100644 index 00000000000..ad5c3e1dc82 --- /dev/null +++ b/clippy_lints/src/needless_if.rs @@ -0,0 +1,75 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, higher::If, is_from_proc_macro, source::snippet_opt}; +use rustc_errors::Applicability; +use rustc_hir::{ExprKind, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for empty `if` branches with no else branch. + /// + /// ### Why is this bad? + /// It can be entirely omitted, and often the condition too. + /// + /// ### Known issues + /// This will usually only suggest to remove the `if` statement, not the condition. Other lints + /// such as `no_effect` will take care of removing the condition if it's unnecessary. + /// + /// ### Example + /// ```rust,ignore + /// if really_expensive_condition(&i) {} + /// if really_expensive_condition_with_side_effects(&mut i) {} + /// ``` + /// Use instead: + /// ```rust,ignore + /// // <omitted> + /// really_expensive_condition_with_side_effects(&mut i); + /// ``` + #[clippy::version = "1.72.0"] + pub NEEDLESS_IF, + complexity, + "checks for empty if branches" +} +declare_lint_pass!(NeedlessIf => [NEEDLESS_IF]); + +impl LateLintPass<'_> for NeedlessIf { + fn check_stmt<'tcx>(&mut self, cx: &LateContext<'tcx>, stmt: &Stmt<'tcx>) { + if let StmtKind::Expr(expr) = stmt.kind + && let Some(If {cond, then, r#else: None }) = If::hir(expr) + && let ExprKind::Block(block, ..) = then.kind + && block.stmts.is_empty() + && block.expr.is_none() + && !in_external_macro(cx.sess(), expr.span) + && !is_from_proc_macro(cx, expr) + && let Some(then_snippet) = snippet_opt(cx, then.span) + // Ignore + // - empty macro expansions + // - empty reptitions in macro expansions + // - comments + // - #[cfg]'d out code + && then_snippet.chars().all(|ch| matches!(ch, '{' | '}') || ch.is_ascii_whitespace()) + && let Some(cond_snippet) = snippet_opt(cx, cond.span) + { + span_lint_and_sugg( + cx, + NEEDLESS_IF, + stmt.span, + "this `if` branch is empty", + "you can remove it", + if cond.can_have_side_effects() || !cx.tcx.hir().attrs(stmt.hir_id).is_empty() { + // `{ foo }` or `{ foo } && bar` placed into a statement position would be + // interpreted as a block statement, force it to be an expression + if cond_snippet.starts_with('{') { + format!("({cond_snippet});") + } else { + format!("{cond_snippet};") + } + } else { + String::new() + }, + Applicability::MachineApplicable, + ); + } + } +} diff --git a/clippy_lints/src/needless_pass_by_value.rs b/clippy_lints/src/needless_pass_by_value.rs index c0970048cdf..ece10474715 100644 --- a/clippy_lints/src/needless_pass_by_value.rs +++ b/clippy_lints/src/needless_pass_by_value.rs @@ -7,13 +7,12 @@ use clippy_utils::ty::{ use clippy_utils::{get_trait_def_id, is_self, paths}; use if_chain::if_chain; use rustc_ast::ast::Attribute; -use rustc_data_structures::fx::FxHashSet; use rustc_errors::{Applicability, Diagnostic}; use rustc_hir::intravisit::FnKind; use rustc_hir::{ BindingAnnotation, Body, FnDecl, GenericArg, HirId, Impl, ItemKind, Mutability, Node, PatKind, QPath, TyKind, }; -use rustc_hir::{HirIdMap, HirIdSet, LangItem}; +use rustc_hir::{HirIdSet, LangItem}; use rustc_hir_typeck::expr_use_visitor as euv; use rustc_infer::infer::TyCtxtInferExt; use rustc_lint::{LateContext, LateLintPass}; @@ -126,9 +125,7 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { .filter_map(|pred| { // Note that we do not want to deal with qualified predicates here. match pred.kind().no_bound_vars() { - Some(ty::ClauseKind::Trait(pred)) if pred.def_id() != sized_trait => { - Some(pred) - }, + Some(ty::ClauseKind::Trait(pred)) if pred.def_id() != sized_trait => Some(pred), _ => None, } }) @@ -136,11 +133,7 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { // Collect moved variables and spans which will need dereferencings from the // function body. - let MovedVariablesCtxt { - moved_vars, - spans_need_deref, - .. - } = { + let MovedVariablesCtxt { moved_vars } = { let mut ctx = MovedVariablesCtxt::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); @@ -211,7 +204,6 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { } } - let deref_span = spans_need_deref.get(&canonical_id); if_chain! { if is_type_diagnostic_item(cx, ty, sym::Vec); if let Some(clone_spans) = @@ -247,7 +239,6 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { } // cannot be destructured, no need for `*` suggestion - assert!(deref_span.is_none()); return; } } @@ -275,23 +266,12 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { ); } - assert!(deref_span.is_none()); return; } } - let mut spans = vec![(input.span, format!("&{}", snippet(cx, input.span, "_")))]; - - // Suggests adding `*` to dereference the added reference. - if let Some(deref_span) = deref_span { - spans.extend( - deref_span - .iter() - .copied() - .map(|span| (span, format!("*{}", snippet(cx, span, "<expr>")))), - ); - spans.sort_by_key(|&(span, _)| span); - } + let spans = vec![(input.span, format!("&{}", snippet(cx, input.span, "_")))]; + multispan_sugg(diag, "consider taking a reference instead", spans); }; @@ -320,9 +300,6 @@ fn requires_exact_signature(attrs: &[Attribute]) -> bool { #[derive(Default)] struct MovedVariablesCtxt { moved_vars: HirIdSet, - /// Spans which need to be prefixed with `*` for dereferencing the - /// suggested additional reference. - spans_need_deref: HirIdMap<FxHashSet<Span>>, } impl MovedVariablesCtxt { diff --git a/clippy_lints/src/no_effect.rs b/clippy_lints/src/no_effect.rs index e3712190e67..a4c7da7e48d 100644 --- a/clippy_lints/src/no_effect.rs +++ b/clippy_lints/src/no_effect.rs @@ -1,11 +1,16 @@ use clippy_utils::diagnostics::{span_lint_hir, span_lint_hir_and_then}; -use clippy_utils::is_lint_allowed; use clippy_utils::peel_blocks; use clippy_utils::source::snippet_opt; use clippy_utils::ty::has_drop; +use clippy_utils::{get_parent_node, is_lint_allowed}; use rustc_errors::Applicability; use rustc_hir::def::{DefKind, Res}; -use rustc_hir::{is_range_literal, BinOpKind, BlockCheckMode, Expr, ExprKind, PatKind, Stmt, StmtKind, UnsafeSource}; +use rustc_hir::{ + is_range_literal, BinOpKind, BlockCheckMode, Expr, ExprKind, FnRetTy, ItemKind, Node, PatKind, Stmt, StmtKind, + UnsafeSource, +}; +use rustc_hir_analysis::hir_ty_to_ty; +use rustc_infer::infer::TyCtxtInferExt as _; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_lint_pass, declare_tool_lint}; @@ -86,7 +91,43 @@ impl<'tcx> LateLintPass<'tcx> for NoEffect { fn check_no_effect(cx: &LateContext<'_>, stmt: &Stmt<'_>) -> bool { if let StmtKind::Semi(expr) = stmt.kind { if has_no_effect(cx, expr) { - span_lint_hir(cx, NO_EFFECT, expr.hir_id, stmt.span, "statement with no effect"); + span_lint_hir_and_then( + cx, + NO_EFFECT, + expr.hir_id, + stmt.span, + "statement with no effect", + |diag| { + for parent in cx.tcx.hir().parent_iter(stmt.hir_id) { + if let Node::Item(item) = parent.1 + && let ItemKind::Fn(sig, ..) = item.kind + && let FnRetTy::Return(ret_ty) = sig.decl.output + && let Some(Node::Block(block)) = get_parent_node(cx.tcx, stmt.hir_id) + && let [.., final_stmt] = block.stmts + && final_stmt.hir_id == stmt.hir_id + { + let expr_ty = cx.typeck_results().expr_ty(expr); + let mut ret_ty = hir_ty_to_ty(cx.tcx, ret_ty); + + // Remove `impl Future<Output = T>` to get `T` + if cx.tcx.ty_is_opaque_future(ret_ty) && + let Some(true_ret_ty) = cx.tcx.infer_ctxt().build().get_impl_future_output_ty(ret_ty) + { + ret_ty = true_ret_ty; + } + + if ret_ty == expr_ty { + diag.span_suggestion( + stmt.span.shrink_to_lo(), + "did you mean to return it?", + "return ", + Applicability::MaybeIncorrect, + ); + } + } + } + }, + ); return true; } } else if let StmtKind::Local(local) = stmt.kind { diff --git a/clippy_lints/src/non_copy_const.rs b/clippy_lints/src/non_copy_const.rs index ac49ccdb501..75f1e95276a 100644 --- a/clippy_lints/src/non_copy_const.rs +++ b/clippy_lints/src/non_copy_const.rs @@ -182,15 +182,15 @@ fn is_value_unfrozen_raw<'tcx>( // have a value that is a frozen variant with a generic param (an example is // `declare_interior_mutable_const::enums::BothOfCellAndGeneric::GENERIC_VARIANT`). // However, it prevents a number of false negatives that is, I think, important: - // 1. assoc consts in trait defs referring to consts of themselves - // (an example is `declare_interior_mutable_const::traits::ConcreteTypes::ANOTHER_ATOMIC`). - // 2. a path expr referring to assoc consts whose type is doesn't have - // any frozen variants in trait defs (i.e. without substitute for `Self`). - // (e.g. borrowing `borrow_interior_mutable_const::trait::ConcreteTypes::ATOMIC`) - // 3. similar to the false positive above; - // but the value is an unfrozen variant, or the type has no enums. (An example is - // `declare_interior_mutable_const::enums::BothOfCellAndGeneric::UNFROZEN_VARIANT` - // and `declare_interior_mutable_const::enums::BothOfCellAndGeneric::NO_ENUM`). + // 1. assoc consts in trait defs referring to consts of themselves (an example is + // `declare_interior_mutable_const::traits::ConcreteTypes::ANOTHER_ATOMIC`). + // 2. a path expr referring to assoc consts whose type is doesn't have any frozen variants in trait + // defs (i.e. without substitute for `Self`). (e.g. borrowing + // `borrow_interior_mutable_const::trait::ConcreteTypes::ATOMIC`) + // 3. similar to the false positive above; but the value is an unfrozen variant, or the type has no + // enums. (An example is + // `declare_interior_mutable_const::enums::BothOfCellAndGeneric::UNFROZEN_VARIANT` and + // `declare_interior_mutable_const::enums::BothOfCellAndGeneric::NO_ENUM`). // One might be able to prevent these FNs correctly, and replace this with `false`; // e.g. implementing `has_frozen_variant` described above, and not running this function // when the type doesn't have any frozen variants would be the 'correct' way for the 2nd diff --git a/clippy_lints/src/operators/eq_op.rs b/clippy_lints/src/operators/eq_op.rs index 67913f7392c..78965b7d6f7 100644 --- a/clippy_lints/src/operators/eq_op.rs +++ b/clippy_lints/src/operators/eq_op.rs @@ -1,4 +1,4 @@ -use clippy_utils::diagnostics::span_lint; +use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; use clippy_utils::macros::{find_assert_eq_args, first_node_macro_backtrace}; use clippy_utils::{ast_utils::is_useless_with_eq_exprs, eq_expr_value, is_in_test_function}; use rustc_hir::{BinOpKind, Expr}; @@ -35,11 +35,16 @@ pub(crate) fn check<'tcx>( right: &'tcx Expr<'_>, ) { if is_useless_with_eq_exprs(op.into()) && eq_expr_value(cx, left, right) && !is_in_test_function(cx.tcx, e.hir_id) { - span_lint( + span_lint_and_then( cx, EQ_OP, e.span, &format!("equal expressions as operands to `{}`", op.as_str()), + |diag| { + if let BinOpKind::Ne = op && cx.typeck_results().expr_ty(left).is_floating_point() { + diag.note("if you intended to check if the operand is NaN, use `.is_nan()` instead"); + } + }, ); } } diff --git a/clippy_lints/src/operators/float_cmp.rs b/clippy_lints/src/operators/float_cmp.rs index 15dff126be7..f3e0c58a787 100644 --- a/clippy_lints/src/operators/float_cmp.rs +++ b/clippy_lints/src/operators/float_cmp.rs @@ -85,7 +85,7 @@ fn get_lint_and_message(is_local: bool, is_comparing_arrays: bool) -> (&'static } } -fn is_allowed(val: &Constant) -> bool { +fn is_allowed(val: &Constant<'_>) -> bool { match val { &Constant::F32(f) => f == 0.0 || f.is_infinite(), &Constant::F64(f) => f == 0.0 || f.is_infinite(), diff --git a/clippy_lints/src/option_if_let_else.rs b/clippy_lints/src/option_if_let_else.rs index aa6d4004268..abdccc47f54 100644 --- a/clippy_lints/src/option_if_let_else.rs +++ b/clippy_lints/src/option_if_let_else.rs @@ -140,6 +140,9 @@ fn try_get_option_occurrence<'tcx>( let (as_ref, as_mut) = match &expr.kind { ExprKind::AddrOf(_, Mutability::Not, _) => (true, false), ExprKind::AddrOf(_, Mutability::Mut, _) => (false, true), + _ if let Some(mutb) = cx.typeck_results().expr_ty(expr).ref_mutability() => { + (mutb == Mutability::Not, mutb == Mutability::Mut) + } _ => (bind_annotation == BindingAnnotation::REF, bind_annotation == BindingAnnotation::REF_MUT), }; diff --git a/clippy_lints/src/ptr.rs b/clippy_lints/src/ptr.rs index b8911f10907..866a04466c9 100644 --- a/clippy_lints/src/ptr.rs +++ b/clippy_lints/src/ptr.rs @@ -5,6 +5,7 @@ use clippy_utils::source::snippet_opt; use clippy_utils::ty::expr_sig; use clippy_utils::visitors::contains_unsafe_block; use clippy_utils::{get_expr_use_or_unification_node, is_lint_allowed, path_def_id, path_to_local, paths}; +use hir::LifetimeName; use if_chain::if_chain; use rustc_errors::{Applicability, MultiSpan}; use rustc_hir::def_id::DefId; @@ -15,6 +16,7 @@ use rustc_hir::{ ImplItemKind, ItemKind, Lifetime, Mutability, Node, Param, PatKind, QPath, TraitFn, TraitItem, TraitItemKind, TyKind, Unsafety, }; +use rustc_hir_analysis::hir_ty_to_ty; use rustc_infer::infer::TyCtxtInferExt; use rustc_infer::traits::{Obligation, ObligationCause}; use rustc_lint::{LateContext, LateLintPass}; @@ -166,6 +168,7 @@ impl<'tcx> LateLintPass<'tcx> for Ptr { cx, cx.tcx.fn_sig(item.owner_id).subst_identity().skip_binder().inputs(), sig.decl.inputs, + &sig.decl.output, &[], ) .filter(|arg| arg.mutability() == Mutability::Not) @@ -218,7 +221,7 @@ impl<'tcx> LateLintPass<'tcx> for Ptr { check_mut_from_ref(cx, sig, Some(body)); let decl = sig.decl; let sig = cx.tcx.fn_sig(item_id).subst_identity().skip_binder(); - let lint_args: Vec<_> = check_fn_args(cx, sig.inputs(), decl.inputs, body.params) + let lint_args: Vec<_> = check_fn_args(cx, sig.inputs(), decl.inputs, &decl.output, body.params) .filter(|arg| !is_trait_item || arg.mutability() == Mutability::Not) .collect(); let results = check_ptr_arg_usage(cx, body, &lint_args); @@ -407,29 +410,27 @@ impl<'tcx> DerefTy<'tcx> { } } +#[expect(clippy::too_many_lines)] fn check_fn_args<'cx, 'tcx: 'cx>( cx: &'cx LateContext<'tcx>, tys: &'tcx [Ty<'tcx>], hir_tys: &'tcx [hir::Ty<'tcx>], + ret_ty: &'tcx FnRetTy<'tcx>, params: &'tcx [Param<'tcx>], ) -> impl Iterator<Item = PtrArg<'tcx>> + 'cx { tys.iter() .zip(hir_tys.iter()) .enumerate() - .filter_map(|(i, (ty, hir_ty))| { - if_chain! { - if let ty::Ref(_, ty, mutability) = *ty.kind(); - if let ty::Adt(adt, substs) = *ty.kind(); - - if let TyKind::Ref(lt, ref ty) = hir_ty.kind; - if let TyKind::Path(QPath::Resolved(None, path)) = ty.ty.kind; - + .filter_map(move |(i, (ty, hir_ty))| { + if let ty::Ref(_, ty, mutability) = *ty.kind() + && let ty::Adt(adt, substs) = *ty.kind() + && let TyKind::Ref(lt, ref ty) = hir_ty.kind + && let TyKind::Path(QPath::Resolved(None, path)) = ty.ty.kind // Check that the name as typed matches the actual name of the type. // e.g. `fn foo(_: &Foo)` shouldn't trigger the lint when `Foo` is an alias for `Vec` - if let [.., name] = path.segments; - if cx.tcx.item_name(adt.did()) == name.ident.name; - - then { + && let [.., name] = path.segments + && cx.tcx.item_name(adt.did()) == name.ident.name + { let emission_id = params.get(i).map_or(hir_ty.hir_id, |param| param.hir_id); let (method_renames, deref_ty) = match cx.tcx.get_diagnostic_name(adt.did()) { Some(sym::Vec) => ( @@ -454,30 +455,65 @@ fn check_fn_args<'cx, 'tcx: 'cx>( DerefTy::Path, ), Some(sym::Cow) if mutability == Mutability::Not => { - let ty_name = name.args + if let Some((lifetime, ty)) = name.args .and_then(|args| { - args.args.iter().find_map(|a| match a { - GenericArg::Type(x) => Some(x), - _ => None, - }) + if let [GenericArg::Lifetime(lifetime), ty] = args.args { + return Some((lifetime, ty)); + } + None }) - .and_then(|arg| snippet_opt(cx, arg.span)) - .unwrap_or_else(|| substs.type_at(1).to_string()); - span_lint_hir_and_then( - cx, - PTR_ARG, - emission_id, - hir_ty.span, - "using a reference to `Cow` is not recommended", - |diag| { - diag.span_suggestion( - hir_ty.span, - "change this to", - format!("&{}{ty_name}", mutability.prefix_str()), - Applicability::Unspecified, - ); + { + if !lifetime.is_anonymous() + && let FnRetTy::Return(ret_ty) = ret_ty + && let ret_ty = hir_ty_to_ty(cx.tcx, ret_ty) + && ret_ty + .walk() + .filter_map(|arg| { + arg.as_region().and_then(|lifetime| { + match lifetime.kind() { + ty::ReEarlyBound(r) => Some(r.def_id), + ty::ReLateBound(_, r) => r.kind.get_id(), + ty::ReFree(r) => r.bound_region.get_id(), + ty::ReStatic + | ty::ReVar(_) + | ty::RePlaceholder(_) + | ty::ReErased + | ty::ReError(_) => None, + } + }) + }) + .any(|def_id| { + matches!( + lifetime.res, + LifetimeName::Param(param_def_id) if def_id + .as_local() + .is_some_and(|def_id| def_id == param_def_id), + ) + }) + { + // `&Cow<'a, T>` when the return type uses 'a is okay + return None; } - ); + + let ty_name = + snippet_opt(cx, ty.span()).unwrap_or_else(|| substs.type_at(1).to_string()); + + span_lint_hir_and_then( + cx, + PTR_ARG, + emission_id, + hir_ty.span, + "using a reference to `Cow` is not recommended", + |diag| { + diag.span_suggestion( + hir_ty.span, + "change this to", + format!("&{}{ty_name}", mutability.prefix_str()), + Applicability::Unspecified, + ); + } + ); + } return None; }, _ => return None, @@ -495,7 +531,6 @@ fn check_fn_args<'cx, 'tcx: 'cx>( }, deref_ty, }); - } } None }) diff --git a/clippy_lints/src/question_mark.rs b/clippy_lints/src/question_mark.rs index 5269bbd1f1a..e3d940ad2a4 100644 --- a/clippy_lints/src/question_mark.rs +++ b/clippy_lints/src/question_mark.rs @@ -1,19 +1,20 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::higher; use clippy_utils::source::snippet_with_applicability; use clippy_utils::ty::is_type_diagnostic_item; use clippy_utils::{ eq_expr_value, get_parent_node, in_constant, is_else_clause, is_res_lang_ctor, path_to_local, path_to_local_id, peel_blocks, peel_blocks_with_stmt, }; +use clippy_utils::{higher, is_path_lang_item}; use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir::def::Res; -use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk}; +use rustc_hir::LangItem::{self, OptionNone, OptionSome, ResultErr, ResultOk}; use rustc_hir::{BindingAnnotation, ByRef, Expr, ExprKind, Node, PatKind, PathSegment, QPath}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::Ty; -use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_session::declare_tool_lint; +use rustc_session::impl_lint_pass; use rustc_span::{sym, symbol::Symbol}; declare_clippy_lint! { @@ -41,7 +42,16 @@ declare_clippy_lint! { "checks for expressions that could be replaced by the question mark operator" } -declare_lint_pass!(QuestionMark => [QUESTION_MARK]); +#[derive(Default)] +pub struct QuestionMark { + /// Keeps track of how many try blocks we are in at any point during linting. + /// This allows us to answer the question "are we inside of a try block" + /// very quickly, without having to walk up the parent chain, by simply checking + /// if it is greater than zero. + /// As for why we need this in the first place: <https://github.com/rust-lang/rust-clippy/issues/8628> + try_block_depth_stack: Vec<u32>, +} +impl_lint_pass!(QuestionMark => [QUESTION_MARK]); enum IfBlockType<'hir> { /// An `if x.is_xxx() { a } else { b } ` expression. @@ -68,98 +78,6 @@ enum IfBlockType<'hir> { ), } -/// Checks if the given expression on the given context matches the following structure: -/// -/// ```ignore -/// if option.is_none() { -/// return None; -/// } -/// ``` -/// -/// ```ignore -/// if result.is_err() { -/// return result; -/// } -/// ``` -/// -/// If it matches, it will suggest to use the question mark operator instead -fn check_is_none_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { - if_chain! { - if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr); - if !is_else_clause(cx.tcx, expr); - if let ExprKind::MethodCall(segment, caller, ..) = &cond.kind; - let caller_ty = cx.typeck_results().expr_ty(caller); - let if_block = IfBlockType::IfIs(caller, caller_ty, segment.ident.name, then, r#else); - if is_early_return(sym::Option, cx, &if_block) || is_early_return(sym::Result, cx, &if_block); - then { - let mut applicability = Applicability::MachineApplicable; - let receiver_str = snippet_with_applicability(cx, caller.span, "..", &mut applicability); - let by_ref = !caller_ty.is_copy_modulo_regions(cx.tcx, cx.param_env) && - !matches!(caller.kind, ExprKind::Call(..) | ExprKind::MethodCall(..)); - let sugg = if let Some(else_inner) = r#else { - if eq_expr_value(cx, caller, peel_blocks(else_inner)) { - format!("Some({receiver_str}?)") - } else { - return; - } - } else { - format!("{receiver_str}{}?;", if by_ref { ".as_ref()" } else { "" }) - }; - - span_lint_and_sugg( - cx, - QUESTION_MARK, - expr.span, - "this block may be rewritten with the `?` operator", - "replace it with", - sugg, - applicability, - ); - } - } -} - -fn check_if_let_some_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { - if_chain! { - if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else }) = higher::IfLet::hir(cx, expr); - if !is_else_clause(cx.tcx, expr); - if let PatKind::TupleStruct(ref path1, [field], ddpos) = let_pat.kind; - if ddpos.as_opt_usize().is_none(); - if let PatKind::Binding(BindingAnnotation(by_ref, _), bind_id, ident, None) = field.kind; - let caller_ty = cx.typeck_results().expr_ty(let_expr); - let if_block = IfBlockType::IfLet( - cx.qpath_res(path1, let_pat.hir_id), - caller_ty, - ident.name, - let_expr, - if_then, - if_else - ); - if (is_early_return(sym::Option, cx, &if_block) && path_to_local_id(peel_blocks(if_then), bind_id)) - || is_early_return(sym::Result, cx, &if_block); - if if_else.map(|e| eq_expr_value(cx, let_expr, peel_blocks(e))).filter(|e| *e).is_none(); - then { - let mut applicability = Applicability::MachineApplicable; - let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability); - let requires_semi = matches!(get_parent_node(cx.tcx, expr.hir_id), Some(Node::Stmt(_))); - let sugg = format!( - "{receiver_str}{}?{}", - if by_ref == ByRef::Yes { ".as_ref()" } else { "" }, - if requires_semi { ";" } else { "" } - ); - span_lint_and_sugg( - cx, - QUESTION_MARK, - expr.span, - "this block may be rewritten with the `?` operator", - "replace it with", - sugg, - applicability, - ); - } - } -} - fn is_early_return(smbl: Symbol, cx: &LateContext<'_>, if_block: &IfBlockType<'_>) -> bool { match *if_block { IfBlockType::IfIs(caller, caller_ty, call_sym, if_then, _) => { @@ -230,11 +148,147 @@ fn expr_return_none_or_err( } } +impl QuestionMark { + fn inside_try_block(&self) -> bool { + self.try_block_depth_stack.last() > Some(&0) + } + + /// Checks if the given expression on the given context matches the following structure: + /// + /// ```ignore + /// if option.is_none() { + /// return None; + /// } + /// ``` + /// + /// ```ignore + /// if result.is_err() { + /// return result; + /// } + /// ``` + /// + /// If it matches, it will suggest to use the question mark operator instead + fn check_is_none_or_err_and_early_return<'tcx>(&self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + if_chain! { + if !self.inside_try_block(); + if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr); + if !is_else_clause(cx.tcx, expr); + if let ExprKind::MethodCall(segment, caller, ..) = &cond.kind; + let caller_ty = cx.typeck_results().expr_ty(caller); + let if_block = IfBlockType::IfIs(caller, caller_ty, segment.ident.name, then, r#else); + if is_early_return(sym::Option, cx, &if_block) || is_early_return(sym::Result, cx, &if_block); + then { + let mut applicability = Applicability::MachineApplicable; + let receiver_str = snippet_with_applicability(cx, caller.span, "..", &mut applicability); + let by_ref = !caller_ty.is_copy_modulo_regions(cx.tcx, cx.param_env) && + !matches!(caller.kind, ExprKind::Call(..) | ExprKind::MethodCall(..)); + let sugg = if let Some(else_inner) = r#else { + if eq_expr_value(cx, caller, peel_blocks(else_inner)) { + format!("Some({receiver_str}?)") + } else { + return; + } + } else { + format!("{receiver_str}{}?;", if by_ref { ".as_ref()" } else { "" }) + }; + + span_lint_and_sugg( + cx, + QUESTION_MARK, + expr.span, + "this block may be rewritten with the `?` operator", + "replace it with", + sugg, + applicability, + ); + } + } + } + + fn check_if_let_some_or_err_and_early_return<'tcx>(&self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + if_chain! { + if !self.inside_try_block(); + if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else }) = higher::IfLet::hir(cx, expr); + if !is_else_clause(cx.tcx, expr); + if let PatKind::TupleStruct(ref path1, [field], ddpos) = let_pat.kind; + if ddpos.as_opt_usize().is_none(); + if let PatKind::Binding(BindingAnnotation(by_ref, _), bind_id, ident, None) = field.kind; + let caller_ty = cx.typeck_results().expr_ty(let_expr); + let if_block = IfBlockType::IfLet( + cx.qpath_res(path1, let_pat.hir_id), + caller_ty, + ident.name, + let_expr, + if_then, + if_else + ); + if (is_early_return(sym::Option, cx, &if_block) && path_to_local_id(peel_blocks(if_then), bind_id)) + || is_early_return(sym::Result, cx, &if_block); + if if_else.map(|e| eq_expr_value(cx, let_expr, peel_blocks(e))).filter(|e| *e).is_none(); + then { + let mut applicability = Applicability::MachineApplicable; + let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability); + let requires_semi = matches!(get_parent_node(cx.tcx, expr.hir_id), Some(Node::Stmt(_))); + let sugg = format!( + "{receiver_str}{}?{}", + if by_ref == ByRef::Yes { ".as_ref()" } else { "" }, + if requires_semi { ";" } else { "" } + ); + span_lint_and_sugg( + cx, + QUESTION_MARK, + expr.span, + "this block may be rewritten with the `?` operator", + "replace it with", + sugg, + applicability, + ); + } + } + } +} + +fn is_try_block(cx: &LateContext<'_>, bl: &rustc_hir::Block<'_>) -> bool { + if let Some(expr) = bl.expr + && let rustc_hir::ExprKind::Call(callee, _) = expr.kind + { + is_path_lang_item(cx, callee, LangItem::TryTraitFromOutput) + } else { + false + } +} + impl<'tcx> LateLintPass<'tcx> for QuestionMark { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { if !in_constant(cx, expr.hir_id) { - check_is_none_or_err_and_early_return(cx, expr); - check_if_let_some_or_err_and_early_return(cx, expr); + self.check_is_none_or_err_and_early_return(cx, expr); + self.check_if_let_some_or_err_and_early_return(cx, expr); + } + } + + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx rustc_hir::Block<'tcx>) { + if is_try_block(cx, block) { + *self + .try_block_depth_stack + .last_mut() + .expect("blocks are always part of bodies and must have a depth") += 1; + } + } + + fn check_body(&mut self, _: &LateContext<'tcx>, _: &'tcx rustc_hir::Body<'tcx>) { + self.try_block_depth_stack.push(0); + } + + fn check_body_post(&mut self, _: &LateContext<'tcx>, _: &'tcx rustc_hir::Body<'tcx>) { + self.try_block_depth_stack.pop(); + } + + fn check_block_post(&mut self, cx: &LateContext<'tcx>, block: &'tcx rustc_hir::Block<'tcx>) { + if is_try_block(cx, block) { + *self + .try_block_depth_stack + .last_mut() + .expect("blocks are always part of bodies and must have a depth") -= 1; } } } diff --git a/clippy_lints/src/ranges.rs b/clippy_lints/src/ranges.rs index dd7ded491e7..d2018aba9e3 100644 --- a/clippy_lints/src/ranges.rs +++ b/clippy_lints/src/ranges.rs @@ -296,8 +296,8 @@ fn check_possible_range_contains( } } -struct RangeBounds<'a> { - val: Constant, +struct RangeBounds<'a, 'tcx> { + val: Constant<'tcx>, expr: &'a Expr<'a>, id: HirId, name_span: Span, @@ -309,7 +309,7 @@ struct RangeBounds<'a> { // Takes a binary expression such as x <= 2 as input // Breaks apart into various pieces, such as the value of the number, // hir id of the variable, and direction/inclusiveness of the operator -fn check_range_bounds<'a>(cx: &'a LateContext<'_>, ex: &'a Expr<'_>) -> Option<RangeBounds<'a>> { +fn check_range_bounds<'a, 'tcx>(cx: &'a LateContext<'tcx>, ex: &'a Expr<'_>) -> Option<RangeBounds<'a, 'tcx>> { if let ExprKind::Binary(ref op, l, r) = ex.kind { let (inclusive, ordering) = match op.node { BinOpKind::Gt => (false, Ordering::Greater), diff --git a/clippy_lints/src/raw_strings.rs b/clippy_lints/src/raw_strings.rs new file mode 100644 index 00000000000..f45bb1ef3e1 --- /dev/null +++ b/clippy_lints/src/raw_strings.rs @@ -0,0 +1,143 @@ +use std::{iter::once, ops::ControlFlow}; + +use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet}; +use rustc_ast::{ + ast::{Expr, ExprKind}, + token::LitKind, +}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for raw string literals where a string literal can be used instead. + /// + /// ### Why is this bad? + /// It's just unnecessary, but there are many cases where using a raw string literal is more + /// idiomatic than a string literal, so it's opt-in. + /// + /// ### Example + /// ```rust + /// let r = r"Hello, world!"; + /// ``` + /// Use instead: + /// ```rust + /// let r = "Hello, world!"; + /// ``` + #[clippy::version = "1.72.0"] + pub NEEDLESS_RAW_STRINGS, + restriction, + "suggests using a string literal when a raw string literal is unnecessary" +} +declare_clippy_lint! { + /// ### What it does + /// Checks for raw string literals with an unnecessary amount of hashes around them. + /// + /// ### Why is this bad? + /// It's just unnecessary, and makes it look like there's more escaping needed than is actually + /// necessary. + /// + /// ### Example + /// ```rust + /// let r = r###"Hello, "world"!"###; + /// ``` + /// Use instead: + /// ```rust + /// let r = r#"Hello, "world"!"#; + /// ``` + #[clippy::version = "1.72.0"] + pub NEEDLESS_RAW_STRING_HASHES, + style, + "suggests reducing the number of hashes around a raw string literal" +} +impl_lint_pass!(RawStrings => [NEEDLESS_RAW_STRINGS, NEEDLESS_RAW_STRING_HASHES]); + +pub struct RawStrings { + pub needless_raw_string_hashes_allow_one: bool, +} + +impl EarlyLintPass for RawStrings { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if !in_external_macro(cx.sess(), expr.span) + && let ExprKind::Lit(lit) = expr.kind + && let LitKind::StrRaw(max) | LitKind::ByteStrRaw(max) | LitKind::CStrRaw(max) = lit.kind + { + let str = lit.symbol.as_str(); + let prefix = match lit.kind { + LitKind::StrRaw(..) => "r", + LitKind::ByteStrRaw(..) => "br", + LitKind::CStrRaw(..) => "cr", + _ => unreachable!(), + }; + if !snippet(cx, expr.span, prefix).trim().starts_with(prefix) { + return; + } + + if !str.contains(['\\', '"']) { + span_lint_and_sugg( + cx, + NEEDLESS_RAW_STRINGS, + expr.span, + "unnecessary raw string literal", + "try", + format!("{}\"{}\"", prefix.replace('r', ""), lit.symbol), + Applicability::MachineApplicable, + ); + + return; + } + + let req = { + let mut following_quote = false; + let mut req = 0; + // `once` so a raw string ending in hashes is still checked + let num = str.as_bytes().iter().chain(once(&0)).try_fold(0u8, |acc, &b| { + match b { + b'"' => (following_quote, req) = (true, 1), + // I'm a bit surprised the compiler didn't optimize this out, there's no + // branch but it still ends up doing an unnecessary comparison, it's: + // - cmp r9b,1h + // - sbb cl,-1h + // which will add 1 if it's true. With this change, it becomes: + // - add cl,r9b + // isn't that so much nicer? + b'#' => req += u8::from(following_quote), + _ => { + if following_quote { + following_quote = false; + + if req == max { + return ControlFlow::Break(req); + } + + return ControlFlow::Continue(acc.max(req)); + } + }, + } + + ControlFlow::Continue(acc) + }); + + match num { + ControlFlow::Continue(num) | ControlFlow::Break(num) => num, + } + }; + + if req < max { + let hashes = "#".repeat(req as usize); + + span_lint_and_sugg( + cx, + NEEDLESS_RAW_STRING_HASHES, + expr.span, + "unnecessary hashes around raw string literal", + "try", + format!(r#"{prefix}{hashes}"{}"{hashes}"#, lit.symbol), + Applicability::MachineApplicable, + ); + } + } + } +} diff --git a/clippy_lints/src/redundant_clone.rs b/clippy_lints/src/redundant_clone.rs index 944a33cc3e5..e36adef555e 100644 --- a/clippy_lints/src/redundant_clone.rs +++ b/clippy_lints/src/redundant_clone.rs @@ -57,7 +57,7 @@ declare_clippy_lint! { /// ``` #[clippy::version = "1.32.0"] pub REDUNDANT_CLONE, - perf, + nursery, "`clone()` of an owned value that is going to be dropped immediately" } @@ -320,8 +320,6 @@ fn base_local_and_movability<'tcx>( mir: &mir::Body<'tcx>, place: mir::Place<'tcx>, ) -> (mir::Local, CannotMoveOut) { - use rustc_middle::mir::PlaceRef; - // Dereference. You cannot move things out from a borrowed value. let mut deref = false; // Accessing a field of an ADT that has `Drop`. Moving the field out will cause E0509. @@ -330,17 +328,14 @@ fn base_local_and_movability<'tcx>( // underlying type implements Copy let mut slice = false; - let PlaceRef { local, mut projection } = place.as_ref(); - while let [base @ .., elem] = projection { - projection = base; + for (base, elem) in place.as_ref().iter_projections() { + let base_ty = base.ty(&mir.local_decls, cx.tcx).ty; deref |= matches!(elem, mir::ProjectionElem::Deref); - field |= matches!(elem, mir::ProjectionElem::Field(..)) - && has_drop(cx, mir::Place::ty_from(local, projection, &mir.local_decls, cx.tcx).ty); - slice |= matches!(elem, mir::ProjectionElem::Index(..)) - && !is_copy(cx, mir::Place::ty_from(local, projection, &mir.local_decls, cx.tcx).ty); + field |= matches!(elem, mir::ProjectionElem::Field(..)) && has_drop(cx, base_ty); + slice |= matches!(elem, mir::ProjectionElem::Index(..)) && !is_copy(cx, base_ty); } - (local, deref || field || slice) + (place.local, deref || field || slice) } #[derive(Default)] diff --git a/clippy_lints/src/redundant_closure_call.rs b/clippy_lints/src/redundant_closure_call.rs index 2a42e73488f..b6ce4ebc28f 100644 --- a/clippy_lints/src/redundant_closure_call.rs +++ b/clippy_lints/src/redundant_closure_call.rs @@ -1,14 +1,14 @@ +use crate::rustc_lint::LintContext; use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; +use clippy_utils::get_parent_expr; use clippy_utils::sugg::Sugg; use if_chain::if_chain; -use rustc_ast::ast; -use rustc_ast::visit as ast_visit; -use rustc_ast::visit::Visitor as AstVisitor; use rustc_errors::Applicability; use rustc_hir as hir; use rustc_hir::intravisit as hir_visit; use rustc_hir::intravisit::Visitor as HirVisitor; -use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext}; +use rustc_hir::intravisit::Visitor; +use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::hir::nested_filter; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_lint_pass, declare_tool_lint}; @@ -51,59 +51,136 @@ impl ReturnVisitor { } } -impl<'ast> ast_visit::Visitor<'ast> for ReturnVisitor { - fn visit_expr(&mut self, ex: &'ast ast::Expr) { - if let ast::ExprKind::Ret(_) | ast::ExprKind::Try(_) = ex.kind { +impl<'tcx> Visitor<'tcx> for ReturnVisitor { + fn visit_expr(&mut self, ex: &'tcx hir::Expr<'tcx>) { + if let hir::ExprKind::Ret(_) | hir::ExprKind::Match(.., hir::MatchSource::TryDesugar) = ex.kind { self.found_return = true; + } else { + hir_visit::walk_expr(self, ex); } + } +} - ast_visit::walk_expr(self, ex); +/// Checks if the body is owned by an async closure +fn is_async_closure(body: &hir::Body<'_>) -> bool { + if let hir::ExprKind::Closure(closure) = body.value.kind + && let [resume_ty] = closure.fn_decl.inputs + && let hir::TyKind::Path(hir::QPath::LangItem(hir::LangItem::ResumeTy, ..)) = resume_ty.kind + { + true + } else { + false } } -impl EarlyLintPass for RedundantClosureCall { - fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) { +/// Tries to find the innermost closure: +/// ```rust,ignore +/// (|| || || || 42)()()()() +/// ^^^^^^^^^^^^^^ given this nested closure expression +/// ^^^^^ we want to return this closure +/// ``` +/// It also has a parameter for how many steps to go in at most, so as to +/// not take more closures than there are calls. +fn find_innermost_closure<'tcx>( + cx: &LateContext<'tcx>, + mut expr: &'tcx hir::Expr<'tcx>, + mut steps: usize, +) -> Option<(&'tcx hir::Expr<'tcx>, &'tcx hir::FnDecl<'tcx>, hir::IsAsync)> { + let mut data = None; + + while let hir::ExprKind::Closure(closure) = expr.kind + && let body = cx.tcx.hir().body(closure.body) + && { + let mut visitor = ReturnVisitor::new(); + visitor.visit_expr(body.value); + !visitor.found_return + } + && steps > 0 + { + expr = body.value; + data = Some((body.value, closure.fn_decl, if is_async_closure(body) { + hir::IsAsync::Async + } else { + hir::IsAsync::NotAsync + })); + steps -= 1; + } + + data +} + +/// "Walks up" the chain of calls to find the outermost call expression, and returns the depth: +/// ```rust,ignore +/// (|| || || 3)()()() +/// ^^ this is the call expression we were given +/// ^^ this is what we want to return (and the depth is 3) +/// ``` +fn get_parent_call_exprs<'tcx>( + cx: &LateContext<'tcx>, + mut expr: &'tcx hir::Expr<'tcx>, +) -> (&'tcx hir::Expr<'tcx>, usize) { + let mut depth = 1; + while let Some(parent) = get_parent_expr(cx, expr) + && let hir::ExprKind::Call(recv, _) = parent.kind + && expr.span == recv.span + { + expr = parent; + depth += 1; + } + (expr, depth) +} + +impl<'tcx> LateLintPass<'tcx> for RedundantClosureCall { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) { if in_external_macro(cx.sess(), expr.span) { return; } - if_chain! { - if let ast::ExprKind::Call(ref paren, _) = expr.kind; - if let ast::ExprKind::Paren(ref closure) = paren.kind; - if let ast::ExprKind::Closure(box ast::Closure { ref asyncness, ref fn_decl, ref body, .. }) = closure.kind; - then { - let mut visitor = ReturnVisitor::new(); - visitor.visit_expr(body); - if !visitor.found_return { - span_lint_and_then( - cx, - REDUNDANT_CLOSURE_CALL, - expr.span, - "try not to call a closure in the expression where it is declared", - |diag| { - if fn_decl.inputs.is_empty() { - let mut app = Applicability::MachineApplicable; - let mut hint = Sugg::ast(cx, body, "..", closure.span.ctxt(), &mut app); - - if asyncness.is_async() { - // `async x` is a syntax error, so it becomes `async { x }` - if !matches!(body.kind, ast::ExprKind::Block(_, _)) { - hint = hint.blockify(); - } - - hint = hint.asyncify(); - } - - diag.span_suggestion(expr.span, "try doing something like", hint.to_string(), app); + + if let hir::ExprKind::Call(recv, _) = expr.kind + // don't lint if the receiver is a call, too. + // we do this in order to prevent linting multiple times; consider: + // `(|| || 1)()()` + // ^^ we only want to lint for this call (but we walk up the calls to consider both calls). + // without this check, we'd end up linting twice. + && !matches!(recv.kind, hir::ExprKind::Call(..)) + && let (full_expr, call_depth) = get_parent_call_exprs(cx, expr) + && let Some((body, fn_decl, generator_kind)) = find_innermost_closure(cx, recv, call_depth) + { + span_lint_and_then( + cx, + REDUNDANT_CLOSURE_CALL, + full_expr.span, + "try not to call a closure in the expression where it is declared", + |diag| { + if fn_decl.inputs.is_empty() { + let mut applicability = Applicability::MachineApplicable; + let mut hint = Sugg::hir_with_context(cx, body, full_expr.span.ctxt(), "..", &mut applicability); + + if generator_kind.is_async() + && let hir::ExprKind::Closure(closure) = body.kind + { + let async_closure_body = cx.tcx.hir().body(closure.body); + + // `async x` is a syntax error, so it becomes `async { x }` + if !matches!(async_closure_body.value.kind, hir::ExprKind::Block(_, _)) { + hint = hint.blockify(); } - }, - ); + + hint = hint.asyncify(); + } + + diag.span_suggestion( + full_expr.span, + "try doing something like", + hint.maybe_par(), + applicability + ); + } } - } + ); } } -} -impl<'tcx> LateLintPass<'tcx> for RedundantClosureCall { fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) { fn count_closure_usage<'tcx>( cx: &LateContext<'tcx>, diff --git a/clippy_lints/src/redundant_type_annotations.rs b/clippy_lints/src/redundant_type_annotations.rs new file mode 100644 index 00000000000..8e9234bba3c --- /dev/null +++ b/clippy_lints/src/redundant_type_annotations.rs @@ -0,0 +1,210 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::LitKind; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::Ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Warns about needless / redundant type annotations. + /// + /// ### Why is this bad? + /// Code without type annotations is shorter and in most cases + /// more idiomatic and easier to modify. + /// + /// ### Limitations + /// This lint doesn't support: + /// + /// - Generics + /// - Refs returned from anything else than a `MethodCall` + /// - Complex types (tuples, arrays, etc...) + /// - `Path` to anything else than a primitive type. + /// + /// ### Example + /// ```rust + /// let foo: String = String::new(); + /// ``` + /// Use instead: + /// ```rust + /// let foo = String::new(); + /// ``` + #[clippy::version = "1.70.0"] + pub REDUNDANT_TYPE_ANNOTATIONS, + restriction, + "warns about needless / redundant type annotations." +} +declare_lint_pass!(RedundantTypeAnnotations => [REDUNDANT_TYPE_ANNOTATIONS]); + +fn is_same_type<'tcx>(cx: &LateContext<'tcx>, ty_resolved_path: hir::def::Res, func_return_type: Ty<'tcx>) -> bool { + // type annotation is primitive + if let hir::def::Res::PrimTy(primty) = ty_resolved_path + && func_return_type.is_primitive() + && let Some(func_return_type_sym) = func_return_type.primitive_symbol() + { + return primty.name() == func_return_type_sym; + } + + // type annotation is any other non generic type + if let hir::def::Res::Def(_, defid) = ty_resolved_path + && let Some(annotation_ty) = cx.tcx.type_of(defid).no_bound_vars() + { + return annotation_ty == func_return_type; + } + + false +} + +fn func_hir_id_to_func_ty<'tcx>(cx: &LateContext<'tcx>, hir_id: hir::hir_id::HirId) -> Option<Ty<'tcx>> { + if let Some((defkind, func_defid)) = cx.typeck_results().type_dependent_def(hir_id) + && defkind == hir::def::DefKind::AssocFn + && let Some(init_ty) = cx.tcx.type_of(func_defid).no_bound_vars() + { + Some(init_ty) + } else { + None + } +} + +fn func_ty_to_return_type<'tcx>(cx: &LateContext<'tcx>, func_ty: Ty<'tcx>) -> Option<Ty<'tcx>> { + if func_ty.is_fn() { + Some(func_ty.fn_sig(cx.tcx).output().skip_binder()) + } else { + None + } +} + +/// Extracts the fn Ty, e.g. `fn() -> std::string::String {f}` +fn extract_fn_ty<'tcx>( + cx: &LateContext<'tcx>, + call: &hir::Expr<'tcx>, + func_return_path: &hir::QPath<'tcx>, +) -> Option<Ty<'tcx>> { + match func_return_path { + // let a: String = f(); where f: fn f() -> String + hir::QPath::Resolved(_, resolved_path) => { + if let hir::def::Res::Def(_, defid) = resolved_path.res + && let Some(middle_ty_init) = cx.tcx.type_of(defid).no_bound_vars() + { + Some(middle_ty_init) + } else { + None + } + }, + // Associated functions like + // let a: String = String::new(); + // let a: String = String::get_string(); + hir::QPath::TypeRelative(..) => func_hir_id_to_func_ty(cx, call.hir_id), + hir::QPath::LangItem(..) => None, + } +} + +fn is_redundant_in_func_call<'tcx>( + cx: &LateContext<'tcx>, + ty_resolved_path: hir::def::Res, + call: &hir::Expr<'tcx>, +) -> bool { + if let hir::ExprKind::Path(init_path) = &call.kind { + let func_type = extract_fn_ty(cx, call, init_path); + + if let Some(func_type) = func_type + && let Some(init_return_type) = func_ty_to_return_type(cx, func_type) + { + return is_same_type(cx, ty_resolved_path, init_return_type); + } + } + + false +} + +fn extract_primty(ty_kind: &hir::TyKind<'_>) -> Option<hir::PrimTy> { + if let hir::TyKind::Path(ty_path) = ty_kind + && let hir::QPath::Resolved(_, resolved_path_ty) = ty_path + && let hir::def::Res::PrimTy(primty) = resolved_path_ty.res + { + Some(primty) + } else { + None + } +} + +impl LateLintPass<'_> for RedundantTypeAnnotations { + fn check_local<'tcx>(&mut self, cx: &LateContext<'tcx>, local: &'tcx rustc_hir::Local<'tcx>) { + // type annotation part + if !local.span.from_expansion() + && let Some(ty) = &local.ty + + // initialization part + && let Some(init) = local.init + { + match &init.kind { + // When the initialization is a call to a function + hir::ExprKind::Call(init_call, _) => { + if let hir::TyKind::Path(ty_path) = &ty.kind + && let hir::QPath::Resolved(_, resolved_path_ty) = ty_path + + && is_redundant_in_func_call(cx, resolved_path_ty.res, init_call) { + span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation"); + } + }, + hir::ExprKind::MethodCall(_, _, _, _) => { + let mut is_ref = false; + let mut ty_kind = &ty.kind; + + // If the annotation is a ref we "peel" it + if let hir::TyKind::Ref(_, mut_ty) = &ty.kind { + is_ref = true; + ty_kind = &mut_ty.ty.kind; + } + + if let hir::TyKind::Path(ty_path) = ty_kind + && let hir::QPath::Resolved(_, resolved_path_ty) = ty_path + && let Some(func_ty) = func_hir_id_to_func_ty(cx, init.hir_id) + && let Some(return_type) = func_ty_to_return_type(cx, func_ty) + && is_same_type(cx, resolved_path_ty.res, if is_ref { + return_type.peel_refs() + } else { + return_type + }) + { + span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation"); + } + }, + // When the initialization is a path for example u32::MAX + hir::ExprKind::Path(init_path) => { + // TODO: check for non primty + if let Some(primty) = extract_primty(&ty.kind) + + && let hir::QPath::TypeRelative(init_ty, _) = init_path + && let Some(primty_init) = extract_primty(&init_ty.kind) + + && primty == primty_init + { + span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation"); + } + }, + hir::ExprKind::Lit(init_lit) => { + match init_lit.node { + // In these cases the annotation is redundant + LitKind::Str(..) + | LitKind::ByteStr(..) + | LitKind::Byte(..) + | LitKind::Char(..) + | LitKind::Bool(..) + | LitKind::CStr(..) => { + span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation"); + }, + LitKind::Int(..) | LitKind::Float(..) => { + // If the initialization value is a suffixed literal we lint + if init_lit.node.is_suffixed() { + span_lint(cx, REDUNDANT_TYPE_ANNOTATIONS, local.span, "redundant type annotation"); + } + }, + LitKind::Err => (), + } + } + _ => () + } + }; + } +} diff --git a/clippy_lints/src/returns.rs b/clippy_lints/src/returns.rs index 631ecf1428d..958351ad81b 100644 --- a/clippy_lints/src/returns.rs +++ b/clippy_lints/src/returns.rs @@ -25,6 +25,12 @@ declare_clippy_lint! { /// It is just extraneous code. Remove it to make your code /// more rusty. /// + /// ### Known problems + /// In the case of some temporaries, e.g. locks, eliding the variable binding could lead + /// to deadlocks. See [this issue](https://github.com/rust-lang/rust/issues/37612). + /// This could become relevant if the code is later changed to use the code that would have been + /// bound without first assigning it to a let-binding. + /// /// ### Example /// ```rust /// fn foo() -> String { @@ -286,7 +292,7 @@ fn check_final_expr<'tcx>( // (except for unit type functions) so we don't match it ExprKind::Match(_, arms, MatchSource::Normal) => { let match_ty = cx.typeck_results().expr_ty(peeled_drop_expr); - for arm in arms.iter() { + for arm in *arms { check_final_expr(cx, arm.body, semi_spans.clone(), RetReplacement::Unit, Some(match_ty)); } }, diff --git a/clippy_lints/src/significant_drop_tightening.rs b/clippy_lints/src/significant_drop_tightening.rs index b930b2c8dd7..fffa8a380c2 100644 --- a/clippy_lints/src/significant_drop_tightening.rs +++ b/clippy_lints/src/significant_drop_tightening.rs @@ -1,10 +1,10 @@ use clippy_utils::{ diagnostics::span_lint_and_then, - get_attr, + expr_or_init, get_attr, path_to_local, source::{indent_of, snippet}, }; -use rustc_data_structures::fx::{FxHashMap, FxHashSet}; -use rustc_errors::{Applicability, Diagnostic}; +use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap}; +use rustc_errors::Applicability; use rustc_hir::{ self as hir, intravisit::{walk_expr, Visitor}, @@ -13,6 +13,7 @@ use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::ty::{subst::GenericArgKind, Ty, TypeAndMut}; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::{symbol::Ident, Span, DUMMY_SP}; +use std::borrow::Cow; declare_clippy_lint! { /// ### What it does @@ -56,255 +57,102 @@ impl_lint_pass!(SignificantDropTightening<'_> => [SIGNIFICANT_DROP_TIGHTENING]); #[derive(Default)] pub struct SignificantDropTightening<'tcx> { + apas: FxIndexMap<hir::HirId, AuxParamsAttr>, /// Auxiliary structure used to avoid having to verify the same type multiple times. seen_types: FxHashSet<Ty<'tcx>>, type_cache: FxHashMap<Ty<'tcx>, bool>, } -impl<'tcx> SignificantDropTightening<'tcx> { - /// Unifies the statements of a block with its return expression. - fn all_block_stmts<'ret, 'rslt, 'stmts>( - block_stmts: &'stmts [hir::Stmt<'tcx>], - dummy_ret_stmt: Option<&'ret hir::Stmt<'tcx>>, - ) -> impl Iterator<Item = &'rslt hir::Stmt<'tcx>> - where - 'ret: 'rslt, - 'stmts: 'rslt, - { - block_stmts.iter().chain(dummy_ret_stmt) - } - - /// Searches for at least one statement that could slow down the release of a significant drop. - fn at_least_one_stmt_is_expensive<'stmt>(stmts: impl Iterator<Item = &'stmt hir::Stmt<'tcx>>) -> bool - where - 'tcx: 'stmt, - { - for stmt in stmts { - match stmt.kind { - hir::StmtKind::Expr(expr) if let hir::ExprKind::Path(_) = expr.kind => {} - hir::StmtKind::Local(local) if let Some(expr) = local.init - && let hir::ExprKind::Path(_) = expr.kind => {}, - _ => return true - }; - } - false - } - - /// Verifies if the expression is of type `drop(some_lock_path)` to assert that the temporary - /// is already being dropped before the end of its scope. - fn has_drop(expr: &'tcx hir::Expr<'_>, init_bind_ident: Ident) -> bool { - if let hir::ExprKind::Call(fun, args) = expr.kind - && let hir::ExprKind::Path(hir::QPath::Resolved(_, fun_path)) = &fun.kind - && let [fun_ident, ..] = fun_path.segments - && fun_ident.ident.name == rustc_span::sym::drop - && 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 == init_bind_ident - } - else { - false - } - } - - /// Tries to find types marked with `#[has_significant_drop]` of an expression `expr` that is - /// originated from `stmt` and then performs common logic on `sdap`. - fn modify_sdap_if_sig_drop_exists( +impl<'tcx> LateLintPass<'tcx> for SignificantDropTightening<'tcx> { + fn check_fn( &mut self, cx: &LateContext<'tcx>, - expr: &'tcx hir::Expr<'_>, - idx: usize, - sdap: &mut SigDropAuxParams, - stmt: &hir::Stmt<'_>, - cb: impl Fn(&mut SigDropAuxParams), + _: hir::intravisit::FnKind<'_>, + _: &hir::FnDecl<'_>, + body: &'tcx hir::Body<'_>, + _: Span, + _: hir::def_id::LocalDefId, ) { - let mut sig_drop_finder = SigDropFinder::new(cx, &mut self.seen_types, &mut self.type_cache); - sig_drop_finder.visit_expr(expr); - if sig_drop_finder.has_sig_drop { - cb(sdap); - if sdap.number_of_stmts > 0 { - sdap.last_use_stmt_idx = idx; - sdap.last_use_stmt_span = stmt.span; - if let hir::ExprKind::MethodCall(_, _, _, span) = expr.kind { - sdap.last_use_method_span = span; - } + self.apas.clear(); + let initial_dummy_stmt = dummy_stmt_expr(body.value); + let mut ap = AuxParams::new(&mut self.apas, &initial_dummy_stmt); + StmtsChecker::new(&mut ap, cx, &mut self.seen_types, &mut self.type_cache).visit_body(body); + for apa in ap.apas.values() { + if apa.counter <= 1 || !apa.has_expensive_expr_after_last_attr { + continue; } - sdap.number_of_stmts = sdap.number_of_stmts.wrapping_add(1); - } - } - - /// Shows generic overall messages as well as specialized messages depending on the usage. - fn set_suggestions(cx: &LateContext<'tcx>, block_span: Span, diag: &mut Diagnostic, sdap: &SigDropAuxParams) { - match sdap.number_of_stmts { - 0 | 1 => {}, - 2 => { - let indent = " ".repeat(indent_of(cx, sdap.last_use_stmt_span).unwrap_or(0)); - let init_method = snippet(cx, sdap.init_method_span, ".."); - let usage_method = snippet(cx, sdap.last_use_method_span, ".."); - let stmt = if let Some(last_use_bind_span) = sdap.last_use_bind_span { - format!( - "\n{indent}let {} = {init_method}.{usage_method};", - snippet(cx, last_use_bind_span, ".."), - ) - } else { - format!("\n{indent}{init_method}.{usage_method};") - }; - diag.span_suggestion_verbose( - sdap.init_stmt_span, - "merge the temporary construction with its single usage", - stmt, - Applicability::MaybeIncorrect, - ); - diag.span_suggestion( - sdap.last_use_stmt_span, - "remove separated single usage", - "", - Applicability::MaybeIncorrect, - ); - }, - _ => { - diag.span_suggestion( - sdap.last_use_stmt_span.shrink_to_hi(), - "drop the temporary after the end of its last usage", - format!( - "\n{}drop({});", - " ".repeat(indent_of(cx, sdap.last_use_stmt_span).unwrap_or(0)), - sdap.init_bind_ident - ), - Applicability::MaybeIncorrect, - ); - }, - } - diag.note("this might lead to unnecessary resource contention"); - diag.span_label( - block_span, - format!( - "temporary `{}` is currently being dropped at the end of its contained scope", - sdap.init_bind_ident - ), - ); - } -} - -impl<'tcx> LateLintPass<'tcx> for SignificantDropTightening<'tcx> { - fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) { - let dummy_ret_stmt = block.expr.map(|expr| hir::Stmt { - hir_id: hir::HirId::INVALID, - kind: hir::StmtKind::Expr(expr), - span: DUMMY_SP, - }); - let mut sdap = SigDropAuxParams::default(); - for (idx, stmt) in Self::all_block_stmts(block.stmts, dummy_ret_stmt.as_ref()).enumerate() { - match stmt.kind { - hir::StmtKind::Expr(expr) => self.modify_sdap_if_sig_drop_exists( - cx, - expr, - idx, - &mut sdap, - stmt, - |_| {} - ), - hir::StmtKind::Local(local) if let Some(expr) = local.init => self.modify_sdap_if_sig_drop_exists( - cx, - expr, - idx, - &mut sdap, - stmt, - |local_sdap| { - if local_sdap.number_of_stmts == 0 { - if let hir::PatKind::Binding(_, _, ident, _) = local.pat.kind { - local_sdap.init_bind_ident = ident; - } - if let hir::ExprKind::MethodCall(_, local_expr, _, span) = expr.kind { - local_sdap.init_method_span = local_expr.span.to(span); - } - local_sdap.init_stmt_span = stmt.span; - } - else if let hir::PatKind::Binding(_, _, ident, _) = local.pat.kind { - local_sdap.last_use_bind_span = Some(ident.span); - } - } - ), - hir::StmtKind::Semi(expr) => { - if Self::has_drop(expr, sdap.init_bind_ident) { - return; - } - self.modify_sdap_if_sig_drop_exists(cx, expr, idx, &mut sdap, stmt, |_| {}); - }, - _ => {} - }; - } - - let idx = sdap.last_use_stmt_idx.wrapping_add(1); - let stmts_after_last_use = Self::all_block_stmts(block.stmts, dummy_ret_stmt.as_ref()).skip(idx); - if sdap.number_of_stmts > 1 && Self::at_least_one_stmt_is_expensive(stmts_after_last_use) { span_lint_and_then( cx, SIGNIFICANT_DROP_TIGHTENING, - sdap.init_bind_ident.span, + apa.first_bind_ident.span, "temporary with significant `Drop` can be early dropped", |diag| { - Self::set_suggestions(cx, block.span, diag, &sdap); + match apa.counter { + 0 | 1 => {}, + 2 => { + let indent = " ".repeat(indent_of(cx, apa.last_stmt_span).unwrap_or(0)); + let init_method = snippet(cx, apa.first_method_span, ".."); + let usage_method = snippet(cx, apa.last_method_span, ".."); + let stmt = if apa.last_bind_ident == Ident::empty() { + format!("\n{indent}{init_method}.{usage_method};") + } else { + format!( + "\n{indent}let {} = {init_method}.{usage_method};", + snippet(cx, apa.last_bind_ident.span, ".."), + ) + }; + diag.span_suggestion_verbose( + apa.first_stmt_span, + "merge the temporary construction with its single usage", + stmt, + Applicability::MaybeIncorrect, + ); + diag.span_suggestion( + apa.last_stmt_span, + "remove separated single usage", + "", + Applicability::MaybeIncorrect, + ); + }, + _ => { + diag.span_suggestion( + apa.last_stmt_span.shrink_to_hi(), + "drop the temporary after the end of its last usage", + format!( + "\n{}drop({});", + " ".repeat(indent_of(cx, apa.last_stmt_span).unwrap_or(0)), + apa.first_bind_ident + ), + Applicability::MaybeIncorrect, + ); + }, + } + diag.note("this might lead to unnecessary resource contention"); + diag.span_label( + apa.first_block_span, + format!( + "temporary `{}` is currently being dropped at the end of its contained scope", + apa.first_bind_ident + ), + ); }, ); } } } -/// Auxiliary parameters used on each block check. -struct SigDropAuxParams { - /// The binding or variable that references the initial construction of the type marked with - /// `#[has_significant_drop]`. - init_bind_ident: Ident, - /// Similar to `init_bind_ident` but encompasses the right-hand method call. - init_method_span: Span, - /// Similar to `init_bind_ident` but encompasses the whole contained statement. - init_stmt_span: Span, - - /// The last visited binding or variable span within a block that had any referenced inner type - /// marked with `#[has_significant_drop]`. - last_use_bind_span: Option<Span>, - /// Index of the last visited statement within a block that had any referenced inner type - /// marked with `#[has_significant_drop]`. - last_use_stmt_idx: usize, - /// Similar to `last_use_bind_span` but encompasses the whole contained statement. - last_use_stmt_span: Span, - /// Similar to `last_use_bind_span` but encompasses the right-hand method call. - last_use_method_span: Span, - - /// Total number of statements within a block that have any referenced inner type marked with - /// `#[has_significant_drop]`. - number_of_stmts: usize, -} - -impl Default for SigDropAuxParams { - fn default() -> Self { - Self { - init_bind_ident: Ident::empty(), - init_method_span: DUMMY_SP, - init_stmt_span: DUMMY_SP, - last_use_bind_span: None, - last_use_method_span: DUMMY_SP, - last_use_stmt_idx: 0, - last_use_stmt_span: DUMMY_SP, - number_of_stmts: 0, - } - } -} - -/// Checks the existence of the `#[has_significant_drop]` attribute -struct SigDropChecker<'cx, 'sdt, 'tcx> { +/// Checks the existence of the `#[has_significant_drop]` attribute. +struct AttrChecker<'cx, 'others, 'tcx> { cx: &'cx LateContext<'tcx>, - seen_types: &'sdt mut FxHashSet<Ty<'tcx>>, - type_cache: &'sdt mut FxHashMap<Ty<'tcx>, bool>, + seen_types: &'others mut FxHashSet<Ty<'tcx>>, + type_cache: &'others mut FxHashMap<Ty<'tcx>, bool>, } -impl<'cx, 'sdt, 'tcx> SigDropChecker<'cx, 'sdt, 'tcx> { +impl<'cx, 'others, 'tcx> AttrChecker<'cx, 'others, 'tcx> { pub(crate) fn new( cx: &'cx LateContext<'tcx>, - seen_types: &'sdt mut FxHashSet<Ty<'tcx>>, - type_cache: &'sdt mut FxHashMap<Ty<'tcx>, bool>, + seen_types: &'others mut FxHashSet<Ty<'tcx>>, + type_cache: &'others mut FxHashMap<Ty<'tcx>, bool>, ) -> Self { seen_types.clear(); Self { @@ -314,7 +162,17 @@ impl<'cx, 'sdt, 'tcx> SigDropChecker<'cx, 'sdt, 'tcx> { } } - pub(crate) fn has_sig_drop_attr_uncached(&mut self, ty: Ty<'tcx>) -> bool { + fn has_sig_drop_attr(&mut self, ty: Ty<'tcx>) -> bool { + // The borrow checker prevents us from using something fancier like or_insert_with. + if let Some(ty) = self.type_cache.get(&ty) { + return *ty; + } + let value = self.has_sig_drop_attr_uncached(ty); + self.type_cache.insert(ty, value); + value + } + + fn has_sig_drop_attr_uncached(&mut self, ty: Ty<'tcx>) -> bool { if let Some(adt) = ty.ty_adt_def() { let mut iter = get_attr( self.cx.sess(), @@ -333,7 +191,7 @@ impl<'cx, 'sdt, 'tcx> SigDropChecker<'cx, 'sdt, 'tcx> { return true; } } - for generic_arg in b.iter() { + for generic_arg in *b { if let GenericArgKind::Type(ty) = generic_arg.unpack() { if self.has_sig_drop_attr(ty) { return true; @@ -350,73 +208,244 @@ impl<'cx, 'sdt, 'tcx> SigDropChecker<'cx, 'sdt, 'tcx> { } } - pub(crate) fn has_sig_drop_attr(&mut self, ty: Ty<'tcx>) -> bool { - // The borrow checker prevents us from using something fancier like or_insert_with. - if let Some(ty) = self.type_cache.get(&ty) { - return *ty; - } - let value = self.has_sig_drop_attr_uncached(ty); - self.type_cache.insert(ty, value); - value - } - fn has_seen_ty(&mut self, ty: Ty<'tcx>) -> bool { !self.seen_types.insert(ty) } } -/// Performs recursive calls to find any inner type marked with `#[has_significant_drop]`. -struct SigDropFinder<'cx, 'sdt, 'tcx> { - cx: &'cx LateContext<'tcx>, - has_sig_drop: bool, - sig_drop_checker: SigDropChecker<'cx, 'sdt, 'tcx>, +struct StmtsChecker<'ap, 'lc, 'others, 'stmt, 'tcx> { + ap: &'ap mut AuxParams<'others, 'stmt, 'tcx>, + cx: &'lc LateContext<'tcx>, + seen_types: &'others mut FxHashSet<Ty<'tcx>>, + type_cache: &'others mut FxHashMap<Ty<'tcx>, bool>, } -impl<'cx, 'sdt, 'tcx> SigDropFinder<'cx, 'sdt, 'tcx> { +impl<'ap, 'lc, 'others, 'stmt, 'tcx> StmtsChecker<'ap, 'lc, 'others, 'stmt, 'tcx> { fn new( - cx: &'cx LateContext<'tcx>, - seen_types: &'sdt mut FxHashSet<Ty<'tcx>>, - type_cache: &'sdt mut FxHashMap<Ty<'tcx>, bool>, + ap: &'ap mut AuxParams<'others, 'stmt, 'tcx>, + cx: &'lc LateContext<'tcx>, + seen_types: &'others mut FxHashSet<Ty<'tcx>>, + type_cache: &'others mut FxHashMap<Ty<'tcx>, bool>, ) -> Self { Self { + ap, cx, - has_sig_drop: false, - sig_drop_checker: SigDropChecker::new(cx, seen_types, type_cache), + seen_types, + type_cache, + } + } + + 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::Local(local) if let Some(expr) = local.init + && let hir::ExprKind::Path(_) = expr.kind => false, + _ => true + }; + if has_expensive_stmt { + for apa in self.ap.apas.values_mut() { + let last_stmt_is_not_dummy = apa.last_stmt_span != DUMMY_SP; + let last_stmt_is_not_curr = self.ap.curr_stmt.span != apa.last_stmt_span; + let block_equals_curr = self.ap.curr_block_hir_id == apa.first_block_hir_id; + let block_is_ancestor = self + .cx + .tcx + .hir() + .parent_iter(self.ap.curr_block_hir_id) + .any(|(id, _)| id == apa.first_block_hir_id); + if last_stmt_is_not_dummy && last_stmt_is_not_curr && (block_equals_curr || block_is_ancestor) { + apa.has_expensive_expr_after_last_attr = true; + } + } } } } -impl<'cx, 'sdt, 'tcx> Visitor<'tcx> for SigDropFinder<'cx, 'sdt, 'tcx> { - fn visit_expr(&mut self, ex: &'tcx hir::Expr<'_>) { - if self - .sig_drop_checker - .has_sig_drop_attr(self.cx.typeck_results().expr_ty(ex)) - { - self.has_sig_drop = true; - return; +impl<'ap, 'lc, 'others, 'stmt, 'tcx> Visitor<'tcx> for StmtsChecker<'ap, 'lc, 'others, 'stmt, 'tcx> { + fn visit_block(&mut self, block: &'tcx hir::Block<'tcx>) { + self.ap.curr_block_hir_id = block.hir_id; + self.ap.curr_block_span = block.span; + for stmt in block.stmts { + self.ap.curr_stmt = Cow::Borrowed(stmt); + self.visit_stmt(stmt); + self.ap.curr_block_hir_id = block.hir_id; + self.ap.curr_block_span = block.span; + self.manage_has_expensive_expr_after_last_attr(); + } + if let Some(expr) = block.expr { + self.ap.curr_stmt = Cow::Owned(dummy_stmt_expr(expr)); + self.visit_expr(expr); + self.ap.curr_block_hir_id = block.hir_id; + self.ap.curr_block_span = block.span; + self.manage_has_expensive_expr_after_last_attr(); } + } - match ex.kind { - hir::ExprKind::MethodCall(_, expr, ..) => { - self.visit_expr(expr); - }, - hir::ExprKind::Array(..) - | hir::ExprKind::Assign(..) - | hir::ExprKind::AssignOp(..) - | hir::ExprKind::Binary(..) - | hir::ExprKind::Call(..) - | hir::ExprKind::Field(..) - | hir::ExprKind::If(..) - | hir::ExprKind::Index(..) - | hir::ExprKind::Match(..) - | hir::ExprKind::Repeat(..) - | hir::ExprKind::Ret(..) - | hir::ExprKind::Tup(..) - | hir::ExprKind::Unary(..) - | hir::ExprKind::Yield(..) => { - walk_expr(self, ex); - }, - _ => {}, + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) { + let modify_apa_params = |apa: &mut AuxParamsAttr| { + apa.counter = apa.counter.wrapping_add(1); + apa.has_expensive_expr_after_last_attr = false; + }; + let mut ac = AttrChecker::new(self.cx, self.seen_types, self.type_cache); + if ac.has_sig_drop_attr(self.cx.typeck_results().expr_ty(expr)) { + if let hir::StmtKind::Local(local) = self.ap.curr_stmt.kind + && let hir::PatKind::Binding(_, hir_id, ident, _) = local.pat.kind + && !self.ap.apas.contains_key(&hir_id) + && { + if let Some(local_hir_id) = path_to_local(expr) { + local_hir_id == hir_id + } + else { + true + } + } + { + let mut apa = AuxParamsAttr { + first_bind_ident: ident, + first_block_hir_id: self.ap.curr_block_hir_id, + first_block_span: self.ap.curr_block_span, + first_method_span: { + let expr_or_init = expr_or_init(self.cx, expr); + if let hir::ExprKind::MethodCall(_, local_expr, _, span) = expr_or_init.kind { + local_expr.span.to(span) + } + else { + expr_or_init.span + } + }, + first_stmt_span: self.ap.curr_stmt.span, + ..Default::default() + }; + modify_apa_params(&mut apa); + let _ = self.ap.apas.insert(hir_id, apa); + } else { + let Some(hir_id) = path_to_local(expr) else { return; }; + let Some(apa) = self.ap.apas.get_mut(&hir_id) else { return; }; + match self.ap.curr_stmt.kind { + hir::StmtKind::Local(local) => { + if let hir::PatKind::Binding(_, _, ident, _) = local.pat.kind { + apa.last_bind_ident = ident; + } + if let Some(local_init) = local.init + && let hir::ExprKind::MethodCall(_, _, _, span) = local_init.kind + { + apa.last_method_span = span; + } + }, + hir::StmtKind::Semi(expr) => { + if has_drop(expr, &apa.first_bind_ident) { + apa.has_expensive_expr_after_last_attr = false; + apa.last_stmt_span = DUMMY_SP; + return; + } + if let hir::ExprKind::MethodCall(_, _, _, span) = expr.kind { + apa.last_method_span = span; + } + }, + _ => {}, + } + apa.last_stmt_span = self.ap.curr_stmt.span; + modify_apa_params(apa); + } + } + walk_expr(self, expr); + } +} + +/// Auxiliary parameters used on each block check of an item +struct AuxParams<'others, 'stmt, 'tcx> { + //// See [AuxParamsAttr]. + apas: &'others mut FxIndexMap<hir::HirId, AuxParamsAttr>, + /// The current block identifier that is being visited. + curr_block_hir_id: hir::HirId, + /// The current block span that is being visited. + curr_block_span: Span, + /// The current statement that is being visited. + curr_stmt: Cow<'stmt, hir::Stmt<'tcx>>, +} + +impl<'others, 'stmt, 'tcx> AuxParams<'others, 'stmt, 'tcx> { + fn new(apas: &'others mut FxIndexMap<hir::HirId, AuxParamsAttr>, curr_stmt: &'stmt hir::Stmt<'tcx>) -> Self { + Self { + apas, + curr_block_hir_id: hir::HirId::INVALID, + curr_block_span: DUMMY_SP, + curr_stmt: Cow::Borrowed(curr_stmt), + } + } +} + +/// Auxiliary parameters used on expression created with `#[has_significant_drop]`. +#[derive(Debug)] +struct AuxParamsAttr { + /// The number of times `#[has_significant_drop]` was referenced. + counter: usize, + /// If an expensive expression follows the last use of anything marked with + /// `#[has_significant_drop]`. + has_expensive_expr_after_last_attr: bool, + + /// The identifier of the block that involves the first `#[has_significant_drop]`. + first_block_hir_id: hir::HirId, + /// The span of the block that involves the first `#[has_significant_drop]`. + first_block_span: Span, + /// The binding or variable that references the initial construction of the type marked with + /// `#[has_significant_drop]`. + first_bind_ident: Ident, + /// Similar to `init_bind_ident` but encompasses the right-hand method call. + first_method_span: Span, + /// Similar to `init_bind_ident` but encompasses the whole contained statement. + first_stmt_span: Span, + + /// The last visited binding or variable span within a block that had any referenced inner type + /// marked with `#[has_significant_drop]`. + last_bind_ident: Ident, + /// Similar to `last_bind_span` but encompasses the right-hand method call. + last_method_span: Span, + /// Similar to `last_bind_span` but encompasses the whole contained statement. + last_stmt_span: Span, +} + +impl Default for AuxParamsAttr { + fn default() -> Self { + Self { + counter: 0, + has_expensive_expr_after_last_attr: false, + first_block_hir_id: hir::HirId::INVALID, + first_bind_ident: Ident::empty(), + first_block_span: DUMMY_SP, + first_method_span: DUMMY_SP, + first_stmt_span: DUMMY_SP, + last_bind_ident: Ident::empty(), + last_method_span: DUMMY_SP, + last_stmt_span: DUMMY_SP, } } } + +fn dummy_stmt_expr<'any>(expr: &'any hir::Expr<'any>) -> hir::Stmt<'any> { + hir::Stmt { + hir_id: hir::HirId::INVALID, + kind: hir::StmtKind::Expr(expr), + span: DUMMY_SP, + } +} + +fn has_drop(expr: &hir::Expr<'_>, first_bind_ident: &Ident) -> bool { + if let hir::ExprKind::Call(fun, args) = expr.kind + && let hir::ExprKind::Path(hir::QPath::Resolved(_, fun_path)) = &fun.kind + && let [fun_ident, ..] = fun_path.segments + && fun_ident.ident.name == rustc_span::sym::drop + && 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 + } +} + +fn is_expensive_expr(expr: &hir::Expr<'_>) -> bool { + !matches!(expr.kind, hir::ExprKind::Path(_)) +} diff --git a/clippy_lints/src/single_call_fn.rs b/clippy_lints/src/single_call_fn.rs new file mode 100644 index 00000000000..42753d2e951 --- /dev/null +++ b/clippy_lints/src/single_call_fn.rs @@ -0,0 +1,133 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{is_from_proc_macro, is_in_test_function}; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::def_id::LocalDefId; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{intravisit::FnKind, Body, Expr, ExprKind, FnDecl}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::hir::nested_filter::OnlyBodies; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions that are only used once. Does not lint tests. + /// + /// ### Why is this bad? + /// It's usually not, splitting a function into multiple parts often improves readability and in + /// the case of generics, can prevent the compiler from duplicating the function dozens of + /// time; instead, only duplicating a thunk. But this can prevent segmentation across a + /// codebase, where many small functions are used only once. + /// + /// Note: If this lint is used, prepare to allow this a lot. + /// + /// ### Example + /// ```rust + /// pub fn a<T>(t: &T) + /// where + /// T: AsRef<str>, + /// { + /// a_inner(t.as_ref()) + /// } + /// + /// fn a_inner(t: &str) { + /// /* snip */ + /// } + /// + /// ``` + /// Use instead: + /// ```rust + /// pub fn a<T>(t: &T) + /// where + /// T: AsRef<str>, + /// { + /// let t = t.as_ref(); + /// /* snip */ + /// } + /// + /// ``` + #[clippy::version = "1.72.0"] + pub SINGLE_CALL_FN, + restriction, + "checks for functions that are only used once" +} +impl_lint_pass!(SingleCallFn => [SINGLE_CALL_FN]); + +#[derive(Clone)] +pub struct SingleCallFn { + pub avoid_breaking_exported_api: bool, + pub def_id_to_usage: FxHashMap<LocalDefId, (Span, Vec<Span>)>, +} + +impl<'tcx> LateLintPass<'tcx> for SingleCallFn { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + _: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + span: Span, + def_id: LocalDefId, + ) { + if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(def_id) + || in_external_macro(cx.sess(), span) + || is_from_proc_macro(cx, &(&kind, body, cx.tcx.local_def_id_to_hir_id(def_id), span)) + || is_in_test_function(cx.tcx, body.value.hir_id) + { + return; + } + + self.def_id_to_usage.insert(def_id, (span, vec![])); + } + + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + let mut v = FnUsageVisitor { + cx, + def_id_to_usage: &mut self.def_id_to_usage, + }; + cx.tcx.hir().visit_all_item_likes_in_crate(&mut v); + + for usage in self.def_id_to_usage.values() { + let single_call_fn_span = usage.0; + if let [caller_span] = *usage.1 { + span_lint_and_help( + cx, + SINGLE_CALL_FN, + single_call_fn_span, + "this function is only used once", + Some(caller_span), + "used here", + ); + } + } + } +} + +struct FnUsageVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + def_id_to_usage: &'a mut FxHashMap<LocalDefId, (Span, Vec<Span>)>, +} + +impl<'a, 'tcx> Visitor<'tcx> for FnUsageVisitor<'a, 'tcx> { + type NestedFilter = OnlyBodies; + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { + let Self { cx, .. } = *self; + + if let ExprKind::Path(qpath) = expr.kind + && let res = cx.qpath_res(&qpath, expr.hir_id) + && let Some(call_def_id) = res.opt_def_id() + && let Some(def_id) = call_def_id.as_local() + && let Some(usage) = self.def_id_to_usage.get_mut(&def_id) + { + usage.1.push(expr.span); + } + + walk_expr(self, expr); + } +} diff --git a/clippy_lints/src/single_range_in_vec_init.rs b/clippy_lints/src/single_range_in_vec_init.rs new file mode 100644 index 00000000000..dfe8be7a6a6 --- /dev/null +++ b/clippy_lints/src/single_range_in_vec_init.rs @@ -0,0 +1,147 @@ +use clippy_utils::{ + diagnostics::span_lint_and_then, get_trait_def_id, higher::VecArgs, macros::root_macro_call_first_node, + source::snippet_opt, ty::implements_trait, +}; +use rustc_ast::{LitIntType, LitKind, UintTy}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, LangItem, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use std::fmt::{self, Display, Formatter}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `Vec` or array initializations that contain only one range. + /// + /// ### Why is this bad? + /// This is almost always incorrect, as it will result in a `Vec` that has only one element. + /// Almost always, the programmer intended for it to include all elements in the range or for + /// the end of the range to be the length instead. + /// + /// ### Example + /// ```rust + /// let x = [0..200]; + /// ``` + /// Use instead: + /// ```rust + /// // If it was intended to include every element in the range... + /// let x = (0..200).collect::<Vec<i32>>(); + /// // ...Or if 200 was meant to be the len + /// let x = [0; 200]; + /// ``` + #[clippy::version = "1.72.0"] + pub SINGLE_RANGE_IN_VEC_INIT, + suspicious, + "checks for initialization of `Vec` or arrays which consist of a single range" +} +declare_lint_pass!(SingleRangeInVecInit => [SINGLE_RANGE_IN_VEC_INIT]); + +enum SuggestedType { + Vec, + Array, +} + +impl SuggestedType { + fn starts_with(&self) -> &'static str { + if matches!(self, SuggestedType::Vec) { + "vec!" + } else { + "[" + } + } + + fn ends_with(&self) -> &'static str { + if matches!(self, SuggestedType::Vec) { "" } else { "]" } + } +} + +impl Display for SuggestedType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if matches!(&self, SuggestedType::Vec) { + write!(f, "a `Vec`") + } else { + write!(f, "an array") + } + } +} + +impl LateLintPass<'_> for SingleRangeInVecInit { + fn check_expr<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + // inner_expr: `vec![0..200]` or `[0..200]` + // ^^^^^^ ^^^^^^^ + // span: `vec![0..200]` or `[0..200]` + // ^^^^^^^^^^^^ ^^^^^^^^ + // suggested_type: What to print, "an array" or "a `Vec`" + let (inner_expr, span, suggested_type) = if let ExprKind::Array([inner_expr]) = expr.kind + && !expr.span.from_expansion() + { + (inner_expr, expr.span, SuggestedType::Array) + } else if let Some(macro_call) = root_macro_call_first_node(cx, expr) + && let Some(VecArgs::Vec([expr])) = VecArgs::hir(cx, expr) + { + (expr, macro_call.span, SuggestedType::Vec) + } else { + return; + }; + + let ExprKind::Struct(QPath::LangItem(lang_item, ..), [start, end], None) = inner_expr.kind else { + return; + }; + + if matches!(lang_item, LangItem::Range) + && let ty = cx.typeck_results().expr_ty(start.expr) + && let Some(snippet) = snippet_opt(cx, span) + // `is_from_proc_macro` will skip any `vec![]`. Let's not! + && snippet.starts_with(suggested_type.starts_with()) + && snippet.ends_with(suggested_type.ends_with()) + && let Some(start_snippet) = snippet_opt(cx, start.span) + && let Some(end_snippet) = snippet_opt(cx, end.span) + { + let should_emit_every_value = if let Some(step_def_id) = get_trait_def_id(cx, &["core", "iter", "Step"]) + && implements_trait(cx, ty, step_def_id, &[]) + { + true + } else { + false + }; + let should_emit_of_len = if let Some(copy_def_id) = cx.tcx.lang_items().copy_trait() + && implements_trait(cx, ty, copy_def_id, &[]) + && let ExprKind::Lit(lit_kind) = end.expr.kind + && let LitKind::Int(.., suffix_type) = lit_kind.node + && let LitIntType::Unsigned(UintTy::Usize) | LitIntType::Unsuffixed = suffix_type + { + true + } else { + false + }; + + if should_emit_every_value || should_emit_of_len { + span_lint_and_then( + cx, + SINGLE_RANGE_IN_VEC_INIT, + span, + &format!("{suggested_type} of `Range` that is only one element"), + |diag| { + if should_emit_every_value { + diag.span_suggestion( + span, + "if you wanted a `Vec` that contains the entire range, try", + format!("({start_snippet}..{end_snippet}).collect::<std::vec::Vec<{ty}>>()"), + Applicability::MaybeIncorrect, + ); + } + + if should_emit_of_len { + diag.span_suggestion( + inner_expr.span, + format!("if you wanted {suggested_type} of len {end_snippet}, try"), + format!("{start_snippet}; {end_snippet}"), + Applicability::MaybeIncorrect, + ); + } + }, + ); + } + } + } +} diff --git a/clippy_lints/src/trait_bounds.rs b/clippy_lints/src/trait_bounds.rs index 4ccda15068b..6db330dfa61 100644 --- a/clippy_lints/src/trait_bounds.rs +++ b/clippy_lints/src/trait_bounds.rs @@ -1,6 +1,7 @@ use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; +use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::{snippet, snippet_opt, snippet_with_applicability}; -use clippy_utils::{SpanlessEq, SpanlessHash}; +use clippy_utils::{is_from_proc_macro, SpanlessEq, SpanlessHash}; use core::hash::{Hash, Hasher}; use if_chain::if_chain; use itertools::Itertools; @@ -9,7 +10,7 @@ use rustc_data_structures::unhash::UnhashMap; use rustc_errors::Applicability; use rustc_hir::def::Res; use rustc_hir::{ - GenericArg, GenericBound, Generics, Item, ItemKind, Node, Path, PathSegment, PredicateOrigin, QPath, + GenericArg, GenericBound, Generics, Item, ItemKind, LangItem, Node, Path, PathSegment, PredicateOrigin, QPath, TraitBoundModifier, TraitItem, TraitRef, Ty, TyKind, WherePredicate, }; use rustc_lint::{LateContext, LateLintPass}; @@ -86,15 +87,16 @@ declare_clippy_lint! { "check if the same trait bounds are specified more than once during a generic declaration" } -#[derive(Copy, Clone)] +#[derive(Clone)] pub struct TraitBounds { max_trait_bounds: u64, + msrv: Msrv, } impl TraitBounds { #[must_use] - pub fn new(max_trait_bounds: u64) -> Self { - Self { max_trait_bounds } + pub fn new(max_trait_bounds: u64, msrv: Msrv) -> Self { + Self { max_trait_bounds, msrv } } } @@ -139,7 +141,7 @@ impl<'tcx> LateLintPass<'tcx> for TraitBounds { ) = cx.tcx.hir().get_if_local(*def_id); then { if self_bounds_map.is_empty() { - for bound in self_bounds.iter() { + for bound in *self_bounds { let Some((self_res, self_segments, _)) = get_trait_info_from_bound(bound) else { continue }; self_bounds_map.insert(self_res, self_segments); } @@ -184,7 +186,7 @@ impl<'tcx> LateLintPass<'tcx> for TraitBounds { // Iterate the bounds and add them to our seen hash // If we haven't yet seen it, add it to the fixed traits - for bound in bounds.iter() { + for bound in bounds { let Some(def_id) = bound.trait_ref.trait_def_id() else { continue; }; let new_trait = seen_def_ids.insert(def_id); @@ -222,10 +224,24 @@ impl<'tcx> LateLintPass<'tcx> for TraitBounds { } } } + + extract_msrv_attr!(LateContext); } impl TraitBounds { - fn check_type_repetition<'tcx>(self, cx: &LateContext<'tcx>, gen: &'tcx Generics<'_>) { + /// Is the given bound a `?Sized` bound, and is combining it (i.e. `T: X + ?Sized`) an error on + /// this MSRV? See <https://github.com/rust-lang/rust-clippy/issues/8772> for details. + fn cannot_combine_maybe_bound(&self, cx: &LateContext<'_>, bound: &GenericBound<'_>) -> bool { + if !self.msrv.meets(msrvs::MAYBE_BOUND_IN_WHERE) + && let GenericBound::Trait(tr, TraitBoundModifier::Maybe) = bound + { + cx.tcx.lang_items().get(LangItem::Sized) == tr.trait_ref.path.res.opt_def_id() + } else { + false + } + } + + fn check_type_repetition<'tcx>(&self, cx: &LateContext<'tcx>, gen: &'tcx Generics<'_>) { struct SpanlessTy<'cx, 'tcx> { ty: &'tcx Ty<'tcx>, cx: &'cx LateContext<'tcx>, @@ -256,11 +272,10 @@ impl TraitBounds { if p.origin != PredicateOrigin::ImplTrait; if p.bounds.len() as u64 <= self.max_trait_bounds; if !p.span.from_expansion(); - if let Some(ref v) = map.insert( - SpanlessTy { ty: p.bounded_ty, cx }, - p.bounds.iter().collect::<Vec<_>>() - ); - + let bounds = p.bounds.iter().filter(|b| !self.cannot_combine_maybe_bound(cx, b)).collect::<Vec<_>>(); + if !bounds.is_empty(); + if let Some(ref v) = map.insert(SpanlessTy { ty: p.bounded_ty, cx }, bounds); + if !is_from_proc_macro(cx, p.bounded_ty); then { let trait_bounds = v .iter() @@ -342,7 +357,7 @@ fn check_trait_bound_duplication(cx: &LateContext<'_>, gen: &'_ Generics<'_>) { "this trait bound is already specified in the where clause", None, "consider removing this trait bound", - ); + ); } } } diff --git a/clippy_lints/src/tuple_array_conversions.rs b/clippy_lints/src/tuple_array_conversions.rs new file mode 100644 index 00000000000..bd983306508 --- /dev/null +++ b/clippy_lints/src/tuple_array_conversions.rs @@ -0,0 +1,235 @@ +use clippy_utils::{ + diagnostics::span_lint_and_help, + is_from_proc_macro, + msrvs::{self, Msrv}, + path_to_local, +}; +use rustc_ast::LitKind; +use rustc_hir::{Expr, ExprKind, HirId, Node, Pat}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::{lint::in_external_macro, ty}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use std::iter::once; + +declare_clippy_lint! { + /// ### What it does + /// Checks for tuple<=>array conversions that are not done with `.into()`. + /// + /// ### Why is this bad? + /// It's unnecessary complexity. `.into()` works for tuples<=>arrays at or below 12 elements and + /// conveys the intent a lot better, while also leaving less room for hard to spot bugs! + /// + /// ### Example + /// ```rust,ignore + /// let t1 = &[(1, 2), (3, 4)]; + /// let v1: Vec<[u32; 2]> = t1.iter().map(|&(a, b)| [a, b]).collect(); + /// ``` + /// Use instead: + /// ```rust,ignore + /// let t1 = &[(1, 2), (3, 4)]; + /// let v1: Vec<[u32; 2]> = t1.iter().map(|&t| t.into()).collect(); + /// ``` + #[clippy::version = "1.72.0"] + pub TUPLE_ARRAY_CONVERSIONS, + complexity, + "checks for tuple<=>array conversions that are not done with `.into()`" +} +impl_lint_pass!(TupleArrayConversions => [TUPLE_ARRAY_CONVERSIONS]); + +#[derive(Clone)] +pub struct TupleArrayConversions { + pub msrv: Msrv, +} + +impl LateLintPass<'_> for TupleArrayConversions { + fn check_expr<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if !in_external_macro(cx.sess(), expr.span) && self.msrv.meets(msrvs::TUPLE_ARRAY_CONVERSIONS) { + 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() + ); + } + + false + }, + ) { + emit_lint(cx, expr, ToType::Array); + } +} + +#[expect( + clippy::blocks_in_if_conditions, + reason = "not a FP, but this is much easier to understand" +)] +#[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() + ); + } + + 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)); + }; + + 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() + ); + } + + 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)] +enum ToType { + Array, + 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 { + 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", + } + } +} + +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/undocumented_unsafe_blocks.rs b/clippy_lints/src/undocumented_unsafe_blocks.rs index 2920684ade3..a9deee96784 100644 --- a/clippy_lints/src/undocumented_unsafe_blocks.rs +++ b/clippy_lints/src/undocumented_unsafe_blocks.rs @@ -11,7 +11,7 @@ use rustc_hir::{Block, BlockCheckMode, ItemKind, Node, UnsafeSource}; use rustc_lexer::{tokenize, TokenKind}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::lint::in_external_macro; -use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::{BytePos, Pos, Span, SyntaxContext}; declare_clippy_lint! { @@ -92,7 +92,22 @@ declare_clippy_lint! { "annotating safe code with a safety comment" } -declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS, UNNECESSARY_SAFETY_COMMENT]); +#[derive(Copy, Clone)] +pub struct UndocumentedUnsafeBlocks { + accept_comment_above_statement: bool, + accept_comment_above_attributes: bool, +} + +impl UndocumentedUnsafeBlocks { + pub fn new(accept_comment_above_statement: bool, accept_comment_above_attributes: bool) -> Self { + Self { + accept_comment_above_statement, + accept_comment_above_attributes, + } + } +} + +impl_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS, UNNECESSARY_SAFETY_COMMENT]); impl<'tcx> LateLintPass<'tcx> for UndocumentedUnsafeBlocks { fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) { @@ -101,7 +116,12 @@ impl<'tcx> LateLintPass<'tcx> for UndocumentedUnsafeBlocks { && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id) && !is_unsafe_from_proc_macro(cx, block.span) && !block_has_safety_comment(cx, block.span) - && !block_parents_have_safety_comment(cx, block.hir_id) + && !block_parents_have_safety_comment( + self.accept_comment_above_statement, + self.accept_comment_above_attributes, + cx, + block.hir_id, + ) { let source_map = cx.tcx.sess.source_map(); let span = if source_map.is_multiline(block.span) { @@ -313,29 +333,95 @@ fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool { // Checks if any parent {expression, statement, block, local, const, static} // has a safety comment -fn block_parents_have_safety_comment(cx: &LateContext<'_>, id: hir::HirId) -> bool { +fn block_parents_have_safety_comment( + accept_comment_above_statement: bool, + accept_comment_above_attributes: bool, + cx: &LateContext<'_>, + id: hir::HirId, +) -> bool { if let Some(node) = get_parent_node(cx.tcx, id) { return match node { - Node::Expr(expr) => !is_branchy(expr) && span_in_body_has_safety_comment(cx, expr.span), + Node::Expr(expr) => { + if let Some( + Node::Local(hir::Local { span, .. }) + | Node::Item(hir::Item { + kind: hir::ItemKind::Const(..) | ItemKind::Static(..), + span, + .. + }), + ) = get_parent_node(cx.tcx, expr.hir_id) + { + let hir_id = match get_parent_node(cx.tcx, expr.hir_id) { + Some(Node::Local(hir::Local { hir_id, .. })) => *hir_id, + Some(Node::Item(hir::Item { owner_id, .. })) => { + cx.tcx.hir().local_def_id_to_hir_id(owner_id.def_id) + }, + _ => unreachable!(), + }; + + // if unsafe block is part of a let/const/static statement, + // and accept_comment_above_statement is set to true + // we accept the safety comment in the line the precedes this statement. + accept_comment_above_statement + && span_with_attrs_in_body_has_safety_comment( + cx, + *span, + hir_id, + accept_comment_above_attributes, + ) + } else { + !is_branchy(expr) + && span_with_attrs_in_body_has_safety_comment( + cx, + expr.span, + expr.hir_id, + accept_comment_above_attributes, + ) + } + }, Node::Stmt(hir::Stmt { kind: - hir::StmtKind::Local(hir::Local { span, .. }) - | hir::StmtKind::Expr(hir::Expr { span, .. }) - | hir::StmtKind::Semi(hir::Expr { span, .. }), + hir::StmtKind::Local(hir::Local { span, hir_id, .. }) + | hir::StmtKind::Expr(hir::Expr { span, hir_id, .. }) + | hir::StmtKind::Semi(hir::Expr { span, hir_id, .. }), .. }) - | Node::Local(hir::Local { span, .. }) - | Node::Item(hir::Item { + | Node::Local(hir::Local { span, hir_id, .. }) => { + span_with_attrs_in_body_has_safety_comment(cx, *span, *hir_id, accept_comment_above_attributes) + }, + Node::Item(hir::Item { kind: hir::ItemKind::Const(..) | ItemKind::Static(..), span, + owner_id, .. - }) => span_in_body_has_safety_comment(cx, *span), + }) => span_with_attrs_in_body_has_safety_comment( + cx, + *span, + cx.tcx.hir().local_def_id_to_hir_id(owner_id.def_id), + accept_comment_above_attributes, + ), _ => false, }; } false } +/// Extends `span` to also include its attributes, then checks if that span has a safety comment. +fn span_with_attrs_in_body_has_safety_comment( + cx: &LateContext<'_>, + span: Span, + hir_id: HirId, + accept_comment_above_attributes: bool, +) -> bool { + let span = if accept_comment_above_attributes { + include_attrs_in_span(cx, hir_id, span) + } else { + span + }; + + span_in_body_has_safety_comment(cx, span) +} + /// Checks if an expression is "branchy", e.g. loop, match/if/etc. fn is_branchy(expr: &hir::Expr<'_>) -> bool { matches!( @@ -360,6 +446,15 @@ fn block_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool { ) || span_in_body_has_safety_comment(cx, span) } +fn include_attrs_in_span(cx: &LateContext<'_>, hir_id: HirId, span: Span) -> Span { + span.to(cx + .tcx + .hir() + .attrs(hir_id) + .iter() + .fold(span, |acc, attr| acc.to(attr.span))) +} + enum HasSafetyComment { Yes(BytePos), No, @@ -546,7 +641,14 @@ fn get_body_search_span(cx: &LateContext<'_>) -> Option<Span> { for (_, node) in map.parent_iter(body.hir_id) { match node { Node::Expr(e) => span = e.span, - Node::Block(_) | Node::Arm(_) | Node::Stmt(_) | Node::Local(_) => (), + Node::Block(_) + | Node::Arm(_) + | Node::Stmt(_) + | Node::Local(_) + | Node::Item(hir::Item { + kind: hir::ItemKind::Const(..) | ItemKind::Static(..), + .. + }) => (), _ => break, } } diff --git a/clippy_lints/src/unused_async.rs b/clippy_lints/src/unused_async.rs index 117dda09222..5e42cf7e4f3 100644 --- a/clippy_lints/src/unused_async.rs +++ b/clippy_lints/src/unused_async.rs @@ -1,4 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::is_def_id_trait_method; use rustc_hir::intravisit::{walk_body, walk_expr, walk_fn, FnKind, Visitor}; use rustc_hir::{Body, Expr, ExprKind, FnDecl, YieldSource}; use rustc_lint::{LateContext, LateLintPass}; @@ -91,7 +92,7 @@ impl<'tcx> LateLintPass<'tcx> for UnusedAsync { span: Span, def_id: LocalDefId, ) { - if !span.from_expansion() && fn_kind.asyncness().is_async() { + if !span.from_expansion() && fn_kind.asyncness().is_async() && !is_def_id_trait_method(cx, def_id) { let mut visitor = AsyncFnVisitor { cx, found_await: false, diff --git a/clippy_lints/src/upper_case_acronyms.rs b/clippy_lints/src/upper_case_acronyms.rs index 1d2d3eb12e1..4df1e3299ed 100644 --- a/clippy_lints/src/upper_case_acronyms.rs +++ b/clippy_lints/src/upper_case_acronyms.rs @@ -65,7 +65,7 @@ fn correct_ident(ident: &str) -> String { let mut ident = fragments.clone().next().unwrap(); for (ref prev, ref curr) in fragments.tuple_windows() { - if [prev, curr] + if <[&String; 2]>::from((prev, curr)) .iter() .all(|s| s.len() == 1 && s.chars().next().unwrap().is_ascii_uppercase()) { diff --git a/clippy_lints/src/useless_conversion.rs b/clippy_lints/src/useless_conversion.rs index 28c3fc859e3..b6c11cfb355 100644 --- a/clippy_lints/src/useless_conversion.rs +++ b/clippy_lints/src/useless_conversion.rs @@ -1,16 +1,17 @@ -use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg, span_lint_and_then}; use clippy_utils::is_ty_alias; -use clippy_utils::source::{snippet, snippet_with_context}; +use clippy_utils::source::{snippet, snippet_with_applicability, snippet_with_context}; use clippy_utils::sugg::Sugg; use clippy_utils::ty::{is_copy, is_type_diagnostic_item, same_type_and_consts}; use clippy_utils::{get_parent_expr, is_trait_method, match_def_path, path_to_local, paths}; use if_chain::if_chain; use rustc_errors::Applicability; +use rustc_hir::def_id::DefId; use rustc_hir::{BindingAnnotation, Expr, ExprKind, HirId, MatchSource, Node, PatKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty; use rustc_session::{declare_tool_lint, impl_lint_pass}; -use rustc_span::sym; +use rustc_span::{sym, Span}; declare_clippy_lint! { /// ### What it does @@ -43,6 +44,64 @@ pub struct UselessConversion { impl_lint_pass!(UselessConversion => [USELESS_CONVERSION]); +enum MethodOrFunction { + Method, + Function, +} + +impl MethodOrFunction { + /// Maps the argument position in `pos` to the parameter position. + /// For methods, `self` is skipped. + fn param_pos(self, pos: usize) -> usize { + match self { + MethodOrFunction::Method => pos + 1, + MethodOrFunction::Function => pos, + } + } +} + +/// Returns the span of the `IntoIterator` trait bound in the function pointed to by `fn_did` +fn into_iter_bound(cx: &LateContext<'_>, fn_did: DefId, into_iter_did: DefId, param_index: u32) -> Option<Span> { + cx.tcx + .predicates_of(fn_did) + .predicates + .iter() + .find_map(|&(ref pred, span)| { + if let ty::ClauseKind::Trait(tr) = pred.kind().skip_binder() + && tr.def_id() == into_iter_did + && tr.self_ty().is_param(param_index) + { + Some(span) + } else { + None + } + }) +} + +/// Extracts the receiver of a `.into_iter()` method call. +fn into_iter_call<'hir>(cx: &LateContext<'_>, expr: &'hir Expr<'hir>) -> Option<&'hir Expr<'hir>> { + if let ExprKind::MethodCall(name, recv, _, _) = expr.kind + && is_trait_method(cx, expr, sym::IntoIterator) + && name.ident.name == sym::into_iter + { + Some(recv) + } else { + None + } +} + +/// Same as [`into_iter_call`], but tries to look for the innermost `.into_iter()` call, e.g.: +/// `foo.into_iter().into_iter()` +/// ^^^ we want this expression +fn into_iter_deep_call<'hir>(cx: &LateContext<'_>, mut expr: &'hir Expr<'hir>) -> (&'hir Expr<'hir>, usize) { + let mut depth = 0; + while let Some(recv) = into_iter_call(cx, expr) { + expr = recv; + depth += 1; + } + (expr, depth) +} + #[expect(clippy::too_many_lines)] impl<'tcx> LateLintPass<'tcx> for UselessConversion { fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { @@ -82,9 +141,59 @@ impl<'tcx> LateLintPass<'tcx> for UselessConversion { ); } } - if is_trait_method(cx, e, sym::IntoIterator) && name.ident.name == sym::into_iter { - if get_parent_expr(cx, e).is_some() && - let Some(id) = path_to_local(recv) && + if let Some(into_iter_recv) = into_iter_call(cx, e) + // Make sure that there is no parent expression, or if there is, make sure it's not a `.into_iter()` call. + // The reason for that is that we only want to lint once (the outermost call) + // in cases like `foo.into_iter().into_iter()` + && get_parent_expr(cx, e) + .and_then(|parent| into_iter_call(cx, parent)) + .is_none() + { + if let Some(parent) = get_parent_expr(cx, e) { + let parent_fn = match parent.kind { + ExprKind::Call(recv, args) if let ExprKind::Path(ref qpath) = recv.kind => { + cx.qpath_res(qpath, recv.hir_id).opt_def_id() + .map(|did| (did, args, MethodOrFunction::Function)) + } + ExprKind::MethodCall(.., args, _) => { + cx.typeck_results().type_dependent_def_id(parent.hir_id) + .map(|did| (did, args, MethodOrFunction::Method)) + } + _ => None, + }; + + if let Some((parent_fn_did, args, kind)) = parent_fn + && let Some(into_iter_did) = cx.tcx.get_diagnostic_item(sym::IntoIterator) + && let sig = cx.tcx.fn_sig(parent_fn_did).skip_binder().skip_binder() + && let Some(arg_pos) = args.iter().position(|x| x.hir_id == e.hir_id) + && let Some(&into_iter_param) = sig.inputs().get(kind.param_pos(arg_pos)) + && let ty::Param(param) = into_iter_param.kind() + && let Some(span) = into_iter_bound(cx, parent_fn_did, into_iter_did, param.index) + { + // Get the "innermost" `.into_iter()` call, e.g. given this expression: + // `foo.into_iter().into_iter()` + // ^^^ + let (into_iter_recv, depth) = into_iter_deep_call(cx, into_iter_recv); + + let plural = if depth == 0 { "" } else { "s" }; + let mut applicability = Applicability::MachineApplicable; + let sugg = snippet_with_applicability(cx, into_iter_recv.span.source_callsite(), "<expr>", &mut applicability).into_owned(); + span_lint_and_then(cx, USELESS_CONVERSION, e.span, "explicit call to `.into_iter()` in function argument accepting `IntoIterator`", |diag| { + diag.span_suggestion( + e.span, + format!("consider removing the `.into_iter()`{plural}"), + sugg, + applicability, + ); + diag.span_note(span, "this parameter accepts any `IntoIterator`, so you don't need to call `.into_iter()`"); + }); + + // Early return to avoid linting again with contradicting suggestions + return; + } + } + + if let Some(id) = path_to_local(recv) && let Node::Pat(pat) = cx.tcx.hir().get(id) && let PatKind::Binding(ann, ..) = pat.kind && ann != BindingAnnotation::MUT diff --git a/clippy_lints/src/utils/conf.rs b/clippy_lints/src/utils/conf.rs index 35f40830681..f1d05c7529b 100644 --- a/clippy_lints/src/utils/conf.rs +++ b/clippy_lints/src/utils/conf.rs @@ -21,6 +21,7 @@ const DEFAULT_DOC_VALID_IDENTS: &[&str] = &[ "GitHub", "GitLab", "IPv4", "IPv6", "ClojureScript", "CoffeeScript", "JavaScript", "PureScript", "TypeScript", + "WebAssembly", "NaN", "NaNs", "OAuth", "GraphQL", "OCaml", @@ -34,6 +35,7 @@ const DEFAULT_DOC_VALID_IDENTS: &[&str] = &[ "CamelCase", ]; const DEFAULT_DISALLOWED_NAMES: &[&str] = &["foo", "baz", "quux"]; +const DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS: &[&str] = &["i", "j", "x", "y", "z", "w", "n"]; /// Holds information used by `MISSING_ENFORCED_IMPORT_RENAMES` lint. #[derive(Clone, Debug, Deserialize)] @@ -288,11 +290,11 @@ define_Conf! { /// arithmetic-side-effects-allowed-unary = ["SomeType", "AnotherType"] /// ``` (arithmetic_side_effects_allowed_unary: rustc_data_structures::fx::FxHashSet<String> = <_>::default()), - /// Lint: ENUM_VARIANT_NAMES, LARGE_TYPES_PASSED_BY_VALUE, TRIVIALLY_COPY_PASS_BY_REF, UNNECESSARY_WRAPS, UNUSED_SELF, UPPER_CASE_ACRONYMS, WRONG_SELF_CONVENTION, BOX_COLLECTION, REDUNDANT_ALLOCATION, RC_BUFFER, VEC_BOX, OPTION_OPTION, LINKEDLIST, RC_MUTEX, UNNECESSARY_BOX_RETURNS. + /// Lint: ENUM_VARIANT_NAMES, LARGE_TYPES_PASSED_BY_VALUE, TRIVIALLY_COPY_PASS_BY_REF, UNNECESSARY_WRAPS, UNUSED_SELF, UPPER_CASE_ACRONYMS, WRONG_SELF_CONVENTION, BOX_COLLECTION, REDUNDANT_ALLOCATION, RC_BUFFER, VEC_BOX, OPTION_OPTION, LINKEDLIST, RC_MUTEX, UNNECESSARY_BOX_RETURNS, SINGLE_CALL_FN. /// /// Suppress lints whenever the suggested change would cause breakage for other crates. (avoid_breaking_exported_api: bool = true), - /// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN. + /// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, OPTION_MAP_UNWRAP_OR, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN, TYPE_REPETITION_IN_BOUNDS, TUPLE_ARRAY_CONVERSIONS, MANUAL_TRY_FOLD. /// /// The minimum rust version that the project supports (msrv: Option<String> = None), @@ -305,6 +307,10 @@ define_Conf! { /// /// The maximum cognitive complexity a function can have (cognitive_complexity_threshold: u64 = 25), + /// Lint: EXCESSIVE_NESTING. + /// + /// The maximum amount of nesting a block can reside in + (excessive_nesting_threshold: u64 = 0), /// DEPRECATED LINT: CYCLOMATIC_COMPLEXITY. /// /// Use the Cognitive Complexity lint instead. @@ -382,6 +388,10 @@ define_Conf! { /// /// The maximum allowed size for arrays on the stack (array_size_threshold: u64 = 512_000), + /// Lint: LARGE_STACK_FRAMES. + /// + /// The maximum allowed stack size for functions in bytes + (stack_size_threshold: u64 = 512_000), /// Lint: VEC_BOX. /// /// The size of the boxed type in bytes, where boxing in a `Vec` is allowed @@ -514,6 +524,33 @@ define_Conf! { /// /// The byte size a `T` in `Box<T>` can have, below which it triggers the `clippy::unnecessary_box` lint (unnecessary_box_size: u64 = 128), + /// Lint: MODULE_INCEPTION. + /// + /// Whether to allow module inception if it's not public. + (allow_private_module_inception: bool = false), + /// Lint: MIN_IDENT_CHARS. + /// + /// Allowed names below the minimum allowed characters. The value `".."` can be used as part of + /// the list to indicate, that the configured values should be appended to the default + /// configuration of Clippy. By default, any configuration will replace the default value. + (allowed_idents_below_min_chars: rustc_data_structures::fx::FxHashSet<String> = + super::DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS.iter().map(ToString::to_string).collect()), + /// Lint: MIN_IDENT_CHARS. + /// + /// Minimum chars an ident can have, anything below or equal to this will be linted. + (min_ident_chars_threshold: u64 = 1), + /// Lint: UNDOCUMENTED_UNSAFE_BLOCKS. + /// + /// Whether to accept a safety comment to be placed above the statement containing the `unsafe` block + (accept_comment_above_statement: bool = false), + /// Lint: UNDOCUMENTED_UNSAFE_BLOCKS. + /// + /// Whether to accept a safety comment to be placed above the attributes for the `unsafe` block + (accept_comment_above_attributes: bool = false), + /// Lint: UNNECESSARY_RAW_STRING_HASHES. + /// + /// Whether to allow `r#""#` when `r""` can be used + (allow_one_hash_in_raw_strings: bool = false), } /// Search for the configuration file. @@ -581,6 +618,12 @@ pub fn read(sess: &Session, path: &Path) -> TryConf { Ok(mut conf) => { extend_vec_if_indicator_present(&mut conf.conf.doc_valid_idents, DEFAULT_DOC_VALID_IDENTS); extend_vec_if_indicator_present(&mut conf.conf.disallowed_names, DEFAULT_DISALLOWED_NAMES); + // TODO: THIS SHOULD BE TESTED, this comment will be gone soon + if conf.conf.allowed_idents_below_min_chars.contains(&"..".to_owned()) { + conf.conf + .allowed_idents_below_min_chars + .extend(DEFAULT_ALLOWED_IDENTS_BELOW_MIN_CHARS.iter().map(ToString::to_string)); + } conf }, diff --git a/clippy_lints/src/utils/internal_lints.rs b/clippy_lints/src/utils/internal_lints.rs index 71f6c9909dd..e222a5448c9 100644 --- a/clippy_lints/src/utils/internal_lints.rs +++ b/clippy_lints/src/utils/internal_lints.rs @@ -1,3 +1,4 @@ +pub mod almost_standard_lint_formulation; pub mod clippy_lints_internal; pub mod collapsible_calls; pub mod compiler_lint_functions; diff --git a/clippy_lints/src/utils/internal_lints/almost_standard_lint_formulation.rs b/clippy_lints/src/utils/internal_lints/almost_standard_lint_formulation.rs new file mode 100644 index 00000000000..570a88a0ed2 --- /dev/null +++ b/clippy_lints/src/utils/internal_lints/almost_standard_lint_formulation.rs @@ -0,0 +1,87 @@ +use crate::utils::internal_lints::lint_without_lint_pass::is_lint_ref_type; +use clippy_utils::diagnostics::span_lint_and_help; +use regex::Regex; +use rustc_ast as ast; +use rustc_hir::{Item, ItemKind, Mutability}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks if lint formulations have a standardized format. + /// + /// ### Why is this bad? + /// It's not neccessarily bad, but we try to enforce a standard in Clippy. + /// + /// ### Example + /// `Checks for use...` can be written as `Checks for usage...` . + pub ALMOST_STANDARD_LINT_FORMULATION, + internal, + "lint formulations must have a standardized format." +} + +impl_lint_pass!(AlmostStandardFormulation => [ALMOST_STANDARD_LINT_FORMULATION]); + +pub struct AlmostStandardFormulation { + standard_formulations: Vec<StandardFormulations<'static>>, +} + +#[derive(Debug)] +struct StandardFormulations<'a> { + wrong_pattern: Regex, + correction: &'a str, +} + +impl AlmostStandardFormulation { + pub fn new() -> Self { + let standard_formulations = vec![StandardFormulations { + wrong_pattern: Regex::new("^(Check for|Detects? uses?)").unwrap(), + correction: "Checks for", + }]; + Self { standard_formulations } + } +} + +impl<'tcx> LateLintPass<'tcx> for AlmostStandardFormulation { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + let mut check_next = false; + if let ItemKind::Static(ty, Mutability::Not, _) = item.kind { + let lines = cx + .tcx + .hir() + .attrs(item.hir_id()) + .iter() + .filter_map(|attr| ast::Attribute::doc_str(attr).map(|sym| (sym, attr))); + if is_lint_ref_type(cx, ty) { + for (line, attr) in lines { + let cur_line = line.as_str().trim(); + if check_next && !cur_line.is_empty() { + for formulation in &self.standard_formulations { + let starts_with_correct_formulation = cur_line.starts_with(formulation.correction); + if !starts_with_correct_formulation && formulation.wrong_pattern.is_match(cur_line) { + if let Some(ident) = attr.ident() { + span_lint_and_help( + cx, + ALMOST_STANDARD_LINT_FORMULATION, + ident.span, + "non-standard lint formulation", + None, + &format!("try using `{}` instead", formulation.correction), + ); + } + return; + } + } + return; + } else if cur_line.contains("What it does") { + check_next = true; + } else if cur_line.contains("Why is this bad") { + // Formulation documentation is done. Can add check to ensure that missing formulation is added + // and add a check if it matches no accepted formulation + return; + } + } + } + } + } +} diff --git a/clippy_lints/src/utils/internal_lints/interning_defined_symbol.rs b/clippy_lints/src/utils/internal_lints/interning_defined_symbol.rs index f8978e30a8e..dced9fcf9ab 100644 --- a/clippy_lints/src/utils/internal_lints/interning_defined_symbol.rs +++ b/clippy_lints/src/utils/internal_lints/interning_defined_symbol.rs @@ -75,7 +75,7 @@ impl<'tcx> LateLintPass<'tcx> for InterningDefinedSymbol { for &module in &[&paths::KW_MODULE, &paths::SYM_MODULE] { for def_id in def_path_def_ids(cx, module) { - for item in cx.tcx.module_children(def_id).iter() { + for item in cx.tcx.module_children(def_id) { if_chain! { if let Res::Def(DefKind::Const, item_def_id) = item.res; let ty = cx.tcx.type_of(item_def_id).subst_identity(); diff --git a/clippy_lints/src/utils/internal_lints/metadata_collector.rs b/clippy_lints/src/utils/internal_lints/metadata_collector.rs index 7a1cd3effae..107a62806a8 100644 --- a/clippy_lints/src/utils/internal_lints/metadata_collector.rs +++ b/clippy_lints/src/utils/internal_lints/metadata_collector.rs @@ -104,6 +104,8 @@ const APPLICABILITY_UNRESOLVED_STR: &str = "Unresolved"; /// The version that will be displayed if none has been defined const VERSION_DEFAULT_STR: &str = "Unknown"; +const CHANGELOG_PATH: &str = "../CHANGELOG.md"; + declare_clippy_lint! { /// ### What it does /// Collects metadata about clippy lints for the website. @@ -195,8 +197,14 @@ This lint has the following configuration variables: fn get_markdown_docs(&self) -> String { format!( - "## Lint Configuration Options\n| <div style=\"width:290px\">Option</div> | Default Value |\n|--|--|\n{}\n\n{}\n", - self.configs_to_markdown(ClippyConfiguration::to_markdown_table_entry), + r#"# Lint Configuration Options + +The following list shows each configuration option, along with a description, its default value, an example +and lints affected. + +--- + +{}"#, self.configs_to_markdown(ClippyConfiguration::to_markdown_paragraph), ) } @@ -254,6 +262,22 @@ Please use that command to update the file and do not edit it by hand. self.get_markdown_docs(), ) .unwrap(); + + // Write configuration links to CHANGELOG.md + let mut changelog = std::fs::read_to_string(CHANGELOG_PATH).unwrap(); + let mut changelog_file = OpenOptions::new().read(true).write(true).open(CHANGELOG_PATH).unwrap(); + + if let Some(position) = changelog.find("<!-- begin autogenerated links to configuration documentation -->") { + // I know this is kinda wasteful, we just don't have regex on `clippy_lints` so... this is the best + // we can do AFAIK. + changelog = changelog[..position].to_string(); + } + writeln!( + changelog_file, + "{changelog}<!-- begin autogenerated links to configuration documentation -->\n{}\n<!-- end autogenerated links to configuration documentation -->", + self.configs_to_markdown(ClippyConfiguration::to_markdown_link) + ) + .unwrap(); } } diff --git a/clippy_lints/src/utils/mod.rs b/clippy_lints/src/utils/mod.rs index d3ea7cafa80..fb08256931f 100644 --- a/clippy_lints/src/utils/mod.rs +++ b/clippy_lints/src/utils/mod.rs @@ -12,6 +12,9 @@ fn to_kebab(config_name: &str) -> String { config_name.replace('_', "-") } +#[cfg(feature = "internal")] +const BOOK_CONFIGS_PATH: &str = "https://doc.rust-lang.org/clippy/lint_configuration.html"; + // ================================================================== // Configuration // ================================================================== @@ -51,7 +54,7 @@ impl ClippyConfiguration { #[cfg(feature = "internal")] fn to_markdown_paragraph(&self) -> String { format!( - "### {}\n{}\n\n**Default Value:** `{}` (`{}`)\n\n{}\n\n", + "## `{}`\n{}\n\n**Default Value:** `{}` (`{}`)\n\n---\n**Affected lints:**\n{}\n\n", self.name, self.doc .lines() @@ -62,14 +65,13 @@ impl ClippyConfiguration { self.lints .iter() .map(|name| name.to_string().split_whitespace().next().unwrap().to_string()) - .map(|name| format!("* [{name}](https://rust-lang.github.io/rust-clippy/master/index.html#{name})")) + .map(|name| format!("* [`{name}`](https://rust-lang.github.io/rust-clippy/master/index.html#{name})")) .join("\n"), ) } - #[cfg(feature = "internal")] - fn to_markdown_table_entry(&self) -> String { - format!("| [{}](#{}) | `{}` |", self.name, self.name, self.default) + fn to_markdown_link(&self) -> String { + format!("[`{}`]: {BOOK_CONFIGS_PATH}#{}", self.name, self.name) } } diff --git a/clippy_lints/src/vec.rs b/clippy_lints/src/vec.rs index 7329e508106..2a594e750b9 100644 --- a/clippy_lints/src/vec.rs +++ b/clippy_lints/src/vec.rs @@ -1,26 +1,32 @@ +use std::ops::ControlFlow; + use clippy_utils::consts::{constant, Constant}; use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::higher; +use clippy_utils::msrvs::{self, Msrv}; use clippy_utils::source::snippet_with_applicability; use clippy_utils::ty::is_copy; +use clippy_utils::visitors::for_each_local_use_after_expr; +use clippy_utils::{get_parent_expr, higher, is_trait_method}; use if_chain::if_chain; use rustc_errors::Applicability; -use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability}; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, Node, PatKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::layout::LayoutOf; use rustc_middle::ty::{self, Ty}; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::source_map::Span; +use rustc_span::sym; #[expect(clippy::module_name_repetitions)] -#[derive(Copy, Clone)] +#[derive(Clone)] pub struct UselessVec { pub too_large_for_stack: u64, + pub msrv: Msrv, } declare_clippy_lint! { /// ### What it does - /// Checks for usage of `&vec![..]` when using `&[..]` would + /// Checks for usage of `vec![..]` when using `[..]` would /// be possible. /// /// ### Why is this bad? @@ -46,16 +52,73 @@ declare_clippy_lint! { impl_lint_pass!(UselessVec => [USELESS_VEC]); +fn adjusts_to_slice(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + matches!(cx.typeck_results().expr_ty_adjusted(e).kind(), ty::Ref(_, ty, _) if ty.is_slice()) +} + +/// Checks if the given expression is a method call to a `Vec` method +/// that also exists on slices. If this returns true, it means that +/// this expression does not actually require a `Vec` and could just work with an array. +fn is_allowed_vec_method(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + const ALLOWED_METHOD_NAMES: &[&str] = &["len", "as_ptr", "is_empty"]; + + if let ExprKind::MethodCall(path, ..) = e.kind { + ALLOWED_METHOD_NAMES.contains(&path.ident.name.as_str()) + } else { + is_trait_method(cx, e, sym::IntoIterator) + } +} + impl<'tcx> LateLintPass<'tcx> for UselessVec { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - // search for `&vec![_]` expressions where the adjusted type is `&[_]` + // search for `&vec![_]` or `vec![_]` expressions where the adjusted type is `&[_]` if_chain! { - if let ty::Ref(_, ty, _) = cx.typeck_results().expr_ty_adjusted(expr).kind(); - if let ty::Slice(..) = ty.kind(); - if let ExprKind::AddrOf(BorrowKind::Ref, mutability, addressee) = expr.kind; - if let Some(vec_args) = higher::VecArgs::hir(cx, addressee); + if adjusts_to_slice(cx, expr); + if let Some(vec_args) = higher::VecArgs::hir(cx, expr.peel_borrows()); then { - self.check_vec_macro(cx, &vec_args, mutability, expr.span); + let (suggest_slice, span) = if let ExprKind::AddrOf(BorrowKind::Ref, mutability, _) = expr.kind { + // `expr` is `&vec![_]`, so suggest `&[_]` (or `&mut[_]` resp.) + (SuggestedType::SliceRef(mutability), expr.span) + } else { + // `expr` is the `vec![_]` expansion, so suggest `[_]` + // and also use the span of the actual `vec![_]` expression + (SuggestedType::Array, expr.span.ctxt().outer_expn_data().call_site) + }; + + self.check_vec_macro(cx, &vec_args, span, suggest_slice); + } + } + + // search for `let foo = vec![_]` expressions where all uses of `foo` + // adjust to slices or call a method that exist on slices (e.g. len) + if let Some(vec_args) = higher::VecArgs::hir(cx, expr) + && let Node::Local(local) = cx.tcx.hir().get_parent(expr.hir_id) + // for now ignore locals with type annotations. + // this is to avoid compile errors when doing the suggestion here: let _: Vec<_> = vec![..]; + && local.ty.is_none() + && let PatKind::Binding(_, id, ..) = local.pat.kind + && is_copy(cx, vec_type(cx.typeck_results().expr_ty_adjusted(expr))) + { + let only_slice_uses = for_each_local_use_after_expr(cx, id, expr.hir_id, |expr| { + // allow indexing into a vec and some set of allowed method calls that exist on slices, too + if let Some(parent) = get_parent_expr(cx, expr) + && (adjusts_to_slice(cx, expr) + || matches!(parent.kind, ExprKind::Index(..)) + || is_allowed_vec_method(cx, parent)) + { + ControlFlow::Continue(()) + } else { + ControlFlow::Break(()) + } + }).is_continue(); + + if only_slice_uses { + self.check_vec_macro( + cx, + &vec_args, + expr.span.ctxt().outer_expn_data().call_site, + SuggestedType::Array + ); } } @@ -63,25 +126,36 @@ impl<'tcx> LateLintPass<'tcx> for UselessVec { if_chain! { if let Some(higher::ForLoop { arg, .. }) = higher::ForLoop::hir(expr); if let Some(vec_args) = higher::VecArgs::hir(cx, arg); - if is_copy(cx, vec_type(cx.typeck_results().expr_ty_adjusted(arg))); + if self.msrv.meets(msrvs::ARRAY_INTO_ITERATOR); then { // report the error around the `vec!` not inside `<std macros>:` let span = arg.span.ctxt().outer_expn_data().call_site; - self.check_vec_macro(cx, &vec_args, Mutability::Not, span); + self.check_vec_macro(cx, &vec_args, span, SuggestedType::Array); } } } + + extract_msrv_attr!(LateContext); +} + +#[derive(Copy, Clone)] +enum SuggestedType { + /// Suggest using a slice `&[..]` / `&mut [..]` + SliceRef(Mutability), + /// Suggest using an array: `[..]` + Array, } impl UselessVec { fn check_vec_macro<'tcx>( - self, + &mut self, cx: &LateContext<'tcx>, vec_args: &higher::VecArgs<'tcx>, - mutability: Mutability, span: Span, + suggest_slice: SuggestedType, ) { let mut applicability = Applicability::MachineApplicable; + let snippet = match *vec_args { higher::VecArgs::Repeat(elem, len) => { if let Some(Constant::Int(len_constant)) = constant(cx, cx.typeck_results(), len) { @@ -90,21 +164,13 @@ impl UselessVec { return; } - match mutability { - Mutability::Mut => { - format!( - "&mut [{}; {}]", - snippet_with_applicability(cx, elem.span, "elem", &mut applicability), - snippet_with_applicability(cx, len.span, "len", &mut applicability) - ) - }, - Mutability::Not => { - format!( - "&[{}; {}]", - snippet_with_applicability(cx, elem.span, "elem", &mut applicability), - snippet_with_applicability(cx, len.span, "len", &mut applicability) - ) - }, + let elem = snippet_with_applicability(cx, elem.span, "elem", &mut applicability); + let len = snippet_with_applicability(cx, len.span, "len", &mut applicability); + + match suggest_slice { + SuggestedType::SliceRef(Mutability::Mut) => format!("&mut [{elem}; {len}]"), + SuggestedType::SliceRef(Mutability::Not) => format!("&[{elem}; {len}]"), + SuggestedType::Array => format!("[{elem}; {len}]"), } } else { return; @@ -116,22 +182,24 @@ impl UselessVec { return; } let span = args[0].span.to(last.span); + let args = snippet_with_applicability(cx, span, "..", &mut applicability); - match mutability { - Mutability::Mut => { - format!( - "&mut [{}]", - snippet_with_applicability(cx, span, "..", &mut applicability) - ) + match suggest_slice { + SuggestedType::SliceRef(Mutability::Mut) => { + format!("&mut [{args}]") + }, + SuggestedType::SliceRef(Mutability::Not) => { + format!("&[{args}]") }, - Mutability::Not => { - format!("&[{}]", snippet_with_applicability(cx, span, "..", &mut applicability)) + SuggestedType::Array => { + format!("[{args}]") }, } } else { - match mutability { - Mutability::Mut => "&mut []".into(), - Mutability::Not => "&[]".into(), + match suggest_slice { + SuggestedType::SliceRef(Mutability::Mut) => "&mut []".to_owned(), + SuggestedType::SliceRef(Mutability::Not) => "&[]".to_owned(), + SuggestedType::Array => "[]".to_owned(), } } }, @@ -142,7 +210,13 @@ impl UselessVec { USELESS_VEC, span, "useless use of `vec!`", - "you can use a slice directly", + &format!( + "you can use {} directly", + match suggest_slice { + SuggestedType::SliceRef(_) => "a slice", + SuggestedType::Array => "an array", + } + ), snippet, applicability, ); diff --git a/clippy_lints/src/visibility.rs b/clippy_lints/src/visibility.rs new file mode 100644 index 00000000000..43248bccc13 --- /dev/null +++ b/clippy_lints/src/visibility.rs @@ -0,0 +1,131 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet_opt}; +use rustc_ast::ast::{Item, VisibilityKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{symbol::kw, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `pub(self)` and `pub(in self)`. + /// + /// ### Why is this bad? + /// It's unnecessary, omitting the `pub` entirely will give the same results. + /// + /// ### Example + /// ```rust,ignore + /// pub(self) type OptBox<T> = Option<Box<T>>; + /// ``` + /// Use instead: + /// ```rust,ignore + /// type OptBox<T> = Option<Box<T>>; + /// ``` + #[clippy::version = "1.72.0"] + pub NEEDLESS_PUB_SELF, + style, + "checks for usage of `pub(self)` and `pub(in self)`." +} +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `pub(<loc>)` with `in`. + /// + /// ### Why is this bad? + /// Consistency. Use it or don't, just be consistent about it. + /// + /// Also see the `pub_without_shorthand` lint for an alternative. + /// + /// ### Example + /// ```rust,ignore + /// pub(super) type OptBox<T> = Option<Box<T>>; + /// ``` + /// Use instead: + /// ```rust,ignore + /// pub(in super) type OptBox<T> = Option<Box<T>>; + /// ``` + #[clippy::version = "1.72.0"] + pub PUB_WITH_SHORTHAND, + restriction, + "disallows usage of `pub(<loc>)`, without `in`" +} +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `pub(<loc>)` without `in`. + /// + /// Note: As you cannot write a module's path in `pub(<loc>)`, this will only trigger on + /// `pub(super)` and the like. + /// + /// ### Why is this bad? + /// Consistency. Use it or don't, just be consistent about it. + /// + /// Also see the `pub_with_shorthand` lint for an alternative. + /// + /// ### Example + /// ```rust,ignore + /// pub(in super) type OptBox<T> = Option<Box<T>>; + /// ``` + /// Use instead: + /// ```rust,ignore + /// pub(super) type OptBox<T> = Option<Box<T>>; + /// ``` + #[clippy::version = "1.72.0"] + pub PUB_WITHOUT_SHORTHAND, + restriction, + "disallows usage of `pub(in <loc>)` with `in`" +} +declare_lint_pass!(Visibility => [NEEDLESS_PUB_SELF, PUB_WITH_SHORTHAND, PUB_WITHOUT_SHORTHAND]); + +impl EarlyLintPass for Visibility { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if !in_external_macro(cx.sess(), item.span) + && let VisibilityKind::Restricted { path, shorthand, .. } = &item.vis.kind + { + if **path == kw::SelfLower && let Some(false) = is_from_proc_macro(cx, item.vis.span) { + span_lint_and_sugg( + cx, + NEEDLESS_PUB_SELF, + item.vis.span, + &format!("unnecessary `pub({}self)`", if *shorthand { "" } else { "in " }), + "remove it", + String::new(), + Applicability::MachineApplicable, + ); + } + + if (**path == kw::Super || **path == kw::SelfLower || **path == kw::Crate) + && !*shorthand + && let [.., last] = &*path.segments + && let Some(false) = is_from_proc_macro(cx, item.vis.span) + { + span_lint_and_sugg( + cx, + PUB_WITHOUT_SHORTHAND, + item.vis.span, + "usage of `pub` with `in`", + "remove it", + format!("pub({})", last.ident), + Applicability::MachineApplicable, + ); + } + + if *shorthand + && let [.., last] = &*path.segments + && let Some(false) = is_from_proc_macro(cx, item.vis.span) + { + span_lint_and_sugg( + cx, + PUB_WITH_SHORTHAND, + item.vis.span, + "usage of `pub` without `in`", + "add it", + format!("pub(in {})", last.ident), + Applicability::MachineApplicable, + ); + } + } + } +} + +fn is_from_proc_macro(cx: &EarlyContext<'_>, span: Span) -> Option<bool> { + snippet_opt(cx, span).map(|s| !s.starts_with("pub")) +} |
