diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
commit | 698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch) | |
tree | 173a775858bd501c378080a10dca74132f05bc50 /src/tools/clippy/clippy_lints | |
parent | Initial commit. (diff) | |
download | rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip |
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/clippy/clippy_lints')
475 files changed, 80252 insertions, 0 deletions
diff --git a/src/tools/clippy/clippy_lints/Cargo.toml b/src/tools/clippy/clippy_lints/Cargo.toml new file mode 100644 index 000000000..79a56dc40 --- /dev/null +++ b/src/tools/clippy/clippy_lints/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "clippy_lints" +version = "0.1.64" +description = "A bunch of helpful lints to avoid common pitfalls in Rust" +repository = "https://github.com/rust-lang/rust-clippy" +readme = "README.md" +license = "MIT OR Apache-2.0" +keywords = ["clippy", "lint", "plugin"] +edition = "2021" + +[dependencies] +cargo_metadata = "0.14" +clippy_utils = { path = "../clippy_utils" } +if_chain = "1.0" +itertools = "0.10.1" +pulldown-cmark = { version = "0.9", default-features = false } +quine-mc_cluskey = "0.2" +regex-syntax = "0.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", optional = true } +tempfile = { version = "3.2", optional = true } +toml = "0.5" +unicode-normalization = "0.1" +unicode-script = { version = "0.5", default-features = false } +semver = "1.0" +rustc-semver = "1.1" +# NOTE: cargo requires serde feat in its url dep +# see <https://github.com/rust-lang/rust/pull/63587#issuecomment-522343864> +url = { version = "2.2", features = ["serde"] } + +[features] +deny-warnings = ["clippy_utils/deny-warnings"] +# build clippy with internal lints enabled, off by default +internal = ["clippy_utils/internal", "serde_json", "tempfile"] + +[package.metadata.rust-analyzer] +# This crate uses #[feature(rustc_private)] +rustc_private = true diff --git a/src/tools/clippy/clippy_lints/README.md b/src/tools/clippy/clippy_lints/README.md new file mode 100644 index 000000000..513583b7e --- /dev/null +++ b/src/tools/clippy/clippy_lints/README.md @@ -0,0 +1 @@ +This crate contains Clippy lints. For the main crate, check [GitHub](https://github.com/rust-lang/rust-clippy). diff --git a/src/tools/clippy/clippy_lints/src/almost_complete_letter_range.rs b/src/tools/clippy/clippy_lints/src/almost_complete_letter_range.rs new file mode 100644 index 000000000..59a7c5354 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/almost_complete_letter_range.rs @@ -0,0 +1,100 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::{trim_span, walk_span_to_context}; +use clippy_utils::{meets_msrv, msrvs}; +use rustc_ast::ast::{Expr, ExprKind, LitKind, Pat, PatKind, RangeEnd, RangeLimits}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for ranges which almost include the entire range of letters from 'a' to 'z', but + /// don't because they're a half open range. + /// + /// ### Why is this bad? + /// This (`'a'..'z'`) is almost certainly a typo meant to include all letters. + /// + /// ### Example + /// ```rust + /// let _ = 'a'..'z'; + /// ``` + /// Use instead: + /// ```rust + /// let _ = 'a'..='z'; + /// ``` + #[clippy::version = "1.63.0"] + pub ALMOST_COMPLETE_LETTER_RANGE, + suspicious, + "almost complete letter range" +} +impl_lint_pass!(AlmostCompleteLetterRange => [ALMOST_COMPLETE_LETTER_RANGE]); + +pub struct AlmostCompleteLetterRange { + msrv: Option<RustcVersion>, +} +impl AlmostCompleteLetterRange { + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} +impl EarlyLintPass for AlmostCompleteLetterRange { + fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &Expr) { + if let ExprKind::Range(Some(start), Some(end), RangeLimits::HalfOpen) = &e.kind { + let ctxt = e.span.ctxt(); + let sugg = if let Some(start) = walk_span_to_context(start.span, ctxt) + && let Some(end) = walk_span_to_context(end.span, ctxt) + && meets_msrv(self.msrv, msrvs::RANGE_INCLUSIVE) + { + Some((trim_span(cx.sess().source_map(), start.between(end)), "..=")) + } else { + None + }; + check_range(cx, e.span, start, end, sugg); + } + } + + fn check_pat(&mut self, cx: &EarlyContext<'_>, p: &Pat) { + if let PatKind::Range(Some(start), Some(end), kind) = &p.kind + && matches!(kind.node, RangeEnd::Excluded) + { + let sugg = if meets_msrv(self.msrv, msrvs::RANGE_INCLUSIVE) { + "..=" + } else { + "..." + }; + check_range(cx, p.span, start, end, Some((kind.span, sugg))); + } + } + + extract_msrv_attr!(EarlyContext); +} + +fn check_range(cx: &EarlyContext<'_>, span: Span, start: &Expr, end: &Expr, sugg: Option<(Span, &str)>) { + if let ExprKind::Lit(start_lit) = &start.peel_parens().kind + && let ExprKind::Lit(end_lit) = &end.peel_parens().kind + && matches!( + (&start_lit.kind, &end_lit.kind), + (LitKind::Byte(b'a') | LitKind::Char('a'), LitKind::Byte(b'z') | LitKind::Char('z')) + | (LitKind::Byte(b'A') | LitKind::Char('A'), LitKind::Byte(b'Z') | LitKind::Char('Z')) + ) + { + span_lint_and_then( + cx, + ALMOST_COMPLETE_LETTER_RANGE, + span, + "almost complete ascii letter range", + |diag| { + if let Some((span, sugg)) = sugg { + diag.span_suggestion( + span, + "use an inclusive range", + sugg, + Applicability::MaybeIncorrect, + ); + } + } + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/approx_const.rs b/src/tools/clippy/clippy_lints/src/approx_const.rs new file mode 100644 index 000000000..159f3b0cd --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/approx_const.rs @@ -0,0 +1,132 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{meets_msrv, msrvs}; +use rustc_ast::ast::{FloatTy, LitFloatType, LitKind}; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol; +use std::f64::consts as f64; + +declare_clippy_lint! { + /// ### What it does + /// Checks for floating point literals that approximate + /// constants which are defined in + /// [`std::f32::consts`](https://doc.rust-lang.org/stable/std/f32/consts/#constants) + /// or + /// [`std::f64::consts`](https://doc.rust-lang.org/stable/std/f64/consts/#constants), + /// respectively, suggesting to use the predefined constant. + /// + /// ### Why is this bad? + /// Usually, the definition in the standard library is more + /// precise than what people come up with. If you find that your definition is + /// actually more precise, please [file a Rust + /// issue](https://github.com/rust-lang/rust/issues). + /// + /// ### Example + /// ```rust + /// let x = 3.14; + /// let y = 1_f64 / x; + /// ``` + /// Use instead: + /// ```rust + /// let x = std::f32::consts::PI; + /// let y = std::f64::consts::FRAC_1_PI; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub APPROX_CONSTANT, + correctness, + "the approximate of a known float constant (in `std::fXX::consts`)" +} + +// Tuples are of the form (constant, name, min_digits, msrv) +const KNOWN_CONSTS: [(f64, &str, usize, Option<RustcVersion>); 19] = [ + (f64::E, "E", 4, None), + (f64::FRAC_1_PI, "FRAC_1_PI", 4, None), + (f64::FRAC_1_SQRT_2, "FRAC_1_SQRT_2", 5, None), + (f64::FRAC_2_PI, "FRAC_2_PI", 5, None), + (f64::FRAC_2_SQRT_PI, "FRAC_2_SQRT_PI", 5, None), + (f64::FRAC_PI_2, "FRAC_PI_2", 5, None), + (f64::FRAC_PI_3, "FRAC_PI_3", 5, None), + (f64::FRAC_PI_4, "FRAC_PI_4", 5, None), + (f64::FRAC_PI_6, "FRAC_PI_6", 5, None), + (f64::FRAC_PI_8, "FRAC_PI_8", 5, None), + (f64::LN_2, "LN_2", 5, None), + (f64::LN_10, "LN_10", 5, None), + (f64::LOG2_10, "LOG2_10", 5, Some(msrvs::LOG2_10)), + (f64::LOG2_E, "LOG2_E", 5, None), + (f64::LOG10_2, "LOG10_2", 5, Some(msrvs::LOG10_2)), + (f64::LOG10_E, "LOG10_E", 5, None), + (f64::PI, "PI", 3, None), + (f64::SQRT_2, "SQRT_2", 5, None), + (f64::TAU, "TAU", 3, Some(msrvs::TAU)), +]; + +pub struct ApproxConstant { + msrv: Option<RustcVersion>, +} + +impl ApproxConstant { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } + + fn check_lit(&self, cx: &LateContext<'_>, lit: &LitKind, e: &Expr<'_>) { + match *lit { + LitKind::Float(s, LitFloatType::Suffixed(fty)) => match fty { + FloatTy::F32 => self.check_known_consts(cx, e, s, "f32"), + FloatTy::F64 => self.check_known_consts(cx, e, s, "f64"), + }, + LitKind::Float(s, LitFloatType::Unsuffixed) => self.check_known_consts(cx, e, s, "f{32, 64}"), + _ => (), + } + } + + fn check_known_consts(&self, cx: &LateContext<'_>, e: &Expr<'_>, s: symbol::Symbol, module: &str) { + let s = s.as_str(); + if s.parse::<f64>().is_ok() { + for &(constant, name, min_digits, msrv) in &KNOWN_CONSTS { + if is_approx_const(constant, s, min_digits) && msrv.map_or(true, |msrv| meets_msrv(self.msrv, msrv)) { + span_lint_and_help( + cx, + APPROX_CONSTANT, + e.span, + &format!("approximate value of `{}::consts::{}` found", module, &name), + None, + "consider using the constant directly", + ); + return; + } + } + } + } +} + +impl_lint_pass!(ApproxConstant => [APPROX_CONSTANT]); + +impl<'tcx> LateLintPass<'tcx> for ApproxConstant { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if let ExprKind::Lit(lit) = &e.kind { + self.check_lit(cx, &lit.node, e); + } + } + + extract_msrv_attr!(LateContext); +} + +/// Returns `false` if the number of significant figures in `value` are +/// less than `min_digits`; otherwise, returns true if `value` is equal +/// to `constant`, rounded to the number of digits present in `value`. +#[must_use] +fn is_approx_const(constant: f64, value: &str, min_digits: usize) -> bool { + if value.len() <= min_digits { + false + } else if constant.to_string().starts_with(value) { + // The value is a truncated constant + true + } else { + let round_const = format!("{:.*}", value.len() - 2, constant); + value == round_const + } +} diff --git a/src/tools/clippy/clippy_lints/src/as_conversions.rs b/src/tools/clippy/clippy_lints/src/as_conversions.rs new file mode 100644 index 000000000..c7a76e5f9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/as_conversions.rs @@ -0,0 +1,65 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{Expr, ExprKind}; +use rustc_lint::{EarlyContext, EarlyLintPass, 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 usage of `as` conversions. + /// + /// Note that this lint is specialized in linting *every single* use of `as` + /// regardless of whether good alternatives exist or not. + /// If you want more precise lints for `as`, please consider using these separate lints: + /// `unnecessary_cast`, `cast_lossless/cast_possible_truncation/cast_possible_wrap/cast_precision_loss/cast_sign_loss`, + /// `fn_to_numeric_cast(_with_truncation)`, `char_lit_as_u8`, `ref_to_mut` and `ptr_as_ptr`. + /// There is a good explanation the reason why this lint should work in this way and how it is useful + /// [in this issue](https://github.com/rust-lang/rust-clippy/issues/5122). + /// + /// ### Why is this bad? + /// `as` conversions will perform many kinds of + /// conversions, including silently lossy conversions and dangerous coercions. + /// There are cases when it makes sense to use `as`, so the lint is + /// Allow by default. + /// + /// ### Example + /// ```rust,ignore + /// let a: u32; + /// ... + /// f(a as u16); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// f(a.try_into()?); + /// + /// // or + /// + /// f(a.try_into().expect("Unexpected u16 overflow in f")); + /// ``` + #[clippy::version = "1.41.0"] + pub AS_CONVERSIONS, + restriction, + "using a potentially dangerous silent `as` conversion" +} + +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) { + return; + } + + if let ExprKind::Cast(_, _) = expr.kind { + span_lint_and_help( + cx, + AS_CONVERSIONS, + expr.span, + "using a potentially dangerous silent `as` conversion", + None, + "consider using a safe wrapper for this conversion", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/as_underscore.rs b/src/tools/clippy/clippy_lints/src/as_underscore.rs new file mode 100644 index 000000000..0bdef9d0a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/as_underscore.rs @@ -0,0 +1,74 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, TyKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Check 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 + /// undetected du to the type being inferred. + /// + /// The lint is allowed by default as using `_` is less wordy than always specifying the type. + /// + /// ### Example + /// ```rust + /// fn foo(n: usize) {} + /// let n: u16 = 256; + /// foo(n as _); + /// ``` + /// Use instead: + /// ```rust + /// fn foo(n: usize) {} + /// let n: u16 = 256; + /// foo(n as usize); + /// ``` + #[clippy::version = "1.63.0"] + pub AS_UNDERSCORE, + restriction, + "detects `as _` conversion" +} +declare_lint_pass!(AsUnderscore => [AS_UNDERSCORE]); + +impl<'tcx> LateLintPass<'tcx> for AsUnderscore { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if let ExprKind::Cast(_, ty) = expr.kind && let TyKind::Infer = ty.kind { + + let ty_resolved = cx.typeck_results().expr_ty(expr); + if let ty::Error(_) = ty_resolved.kind() { + span_lint_and_help( + cx, + AS_UNDERSCORE, + expr.span, + "using `as _` conversion", + None, + "consider giving the type explicitly", + ); + } else { + span_lint_and_then( + cx, + AS_UNDERSCORE, + expr.span, + "using `as _` conversion", + |diag| { + diag.span_suggestion( + ty.span, + "consider giving the type explicitly", + ty_resolved, + Applicability::MachineApplicable, + ); + } + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/asm_syntax.rs b/src/tools/clippy/clippy_lints/src/asm_syntax.rs new file mode 100644 index 000000000..f419781db --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/asm_syntax.rs @@ -0,0 +1,131 @@ +use std::fmt; + +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{Expr, ExprKind, InlineAsmOptions}; +use rustc_lint::{EarlyContext, EarlyLintPass, Lint}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum AsmStyle { + Intel, + Att, +} + +impl fmt::Display for AsmStyle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AsmStyle::Intel => f.write_str("Intel"), + AsmStyle::Att => f.write_str("AT&T"), + } + } +} + +impl std::ops::Not for AsmStyle { + type Output = AsmStyle; + + fn not(self) -> AsmStyle { + match self { + AsmStyle::Intel => AsmStyle::Att, + AsmStyle::Att => AsmStyle::Intel, + } + } +} + +fn check_expr_asm_syntax(lint: &'static Lint, cx: &EarlyContext<'_>, expr: &Expr, check_for: AsmStyle) { + if let ExprKind::InlineAsm(ref inline_asm) = expr.kind { + let style = if inline_asm.options.contains(InlineAsmOptions::ATT_SYNTAX) { + AsmStyle::Att + } else { + AsmStyle::Intel + }; + + if style == check_for { + span_lint_and_help( + cx, + lint, + expr.span, + &format!("{} x86 assembly syntax used", style), + None, + &format!("use {} x86 assembly syntax", !style), + ); + } + } +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of Intel x86 assembly syntax. + /// + /// ### Why is this bad? + /// The lint has been enabled to indicate a preference + /// for AT&T x86 assembly syntax. + /// + /// ### Example + /// + /// ```rust,no_run + /// # #![feature(asm)] + /// # unsafe { let ptr = "".as_ptr(); + /// # use std::arch::asm; + /// asm!("lea {}, [{}]", lateout(reg) _, in(reg) ptr); + /// # } + /// ``` + /// Use instead: + /// ```rust,no_run + /// # #![feature(asm)] + /// # unsafe { let ptr = "".as_ptr(); + /// # use std::arch::asm; + /// asm!("lea ({}), {}", in(reg) ptr, lateout(reg) _, options(att_syntax)); + /// # } + /// ``` + #[clippy::version = "1.49.0"] + pub INLINE_ASM_X86_INTEL_SYNTAX, + restriction, + "prefer AT&T x86 assembly syntax" +} + +declare_lint_pass!(InlineAsmX86IntelSyntax => [INLINE_ASM_X86_INTEL_SYNTAX]); + +impl EarlyLintPass for InlineAsmX86IntelSyntax { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + check_expr_asm_syntax(Self::get_lints()[0], cx, expr, AsmStyle::Intel); + } +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of AT&T x86 assembly syntax. + /// + /// ### Why is this bad? + /// The lint has been enabled to indicate a preference + /// for Intel x86 assembly syntax. + /// + /// ### Example + /// + /// ```rust,no_run + /// # #![feature(asm)] + /// # unsafe { let ptr = "".as_ptr(); + /// # use std::arch::asm; + /// asm!("lea ({}), {}", in(reg) ptr, lateout(reg) _, options(att_syntax)); + /// # } + /// ``` + /// Use instead: + /// ```rust,no_run + /// # #![feature(asm)] + /// # unsafe { let ptr = "".as_ptr(); + /// # use std::arch::asm; + /// asm!("lea {}, [{}]", lateout(reg) _, in(reg) ptr); + /// # } + /// ``` + #[clippy::version = "1.49.0"] + pub INLINE_ASM_X86_ATT_SYNTAX, + restriction, + "prefer Intel x86 assembly syntax" +} + +declare_lint_pass!(InlineAsmX86AttSyntax => [INLINE_ASM_X86_ATT_SYNTAX]); + +impl EarlyLintPass for InlineAsmX86AttSyntax { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + check_expr_asm_syntax(Self::get_lints()[0], cx, expr, AsmStyle::Att); + } +} diff --git a/src/tools/clippy/clippy_lints/src/assertions_on_constants.rs b/src/tools/clippy/clippy_lints/src/assertions_on_constants.rs new file mode 100644 index 000000000..2705ffffd --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/assertions_on_constants.rs @@ -0,0 +1,69 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::macros::{find_assert_args, root_macro_call_first_node, PanicExpn}; +use rustc_hir::Expr; +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 `assert!(true)` and `assert!(false)` calls. + /// + /// ### Why is this bad? + /// Will be optimized out by the compiler or should probably be replaced by a + /// `panic!()` or `unreachable!()` + /// + /// ### Example + /// ```rust,ignore + /// assert!(false) + /// assert!(true) + /// const B: bool = false; + /// assert!(B) + /// ``` + #[clippy::version = "1.34.0"] + pub ASSERTIONS_ON_CONSTANTS, + style, + "`assert!(true)` / `assert!(false)` will be optimized out by the compiler, and should probably be replaced by a `panic!()` or `unreachable!()`" +} + +declare_lint_pass!(AssertionsOnConstants => [ASSERTIONS_ON_CONSTANTS]); + +impl<'tcx> LateLintPass<'tcx> for AssertionsOnConstants { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + let Some(macro_call) = root_macro_call_first_node(cx, e) else { return }; + let is_debug = match cx.tcx.get_diagnostic_name(macro_call.def_id) { + Some(sym::debug_assert_macro) => true, + Some(sym::assert_macro) => false, + _ => return, + }; + let Some((condition, panic_expn)) = find_assert_args(cx, e, macro_call.expn) else { return }; + let Some((Constant::Bool(val), _)) = constant(cx, cx.typeck_results(), condition) else { return }; + if val { + span_lint_and_help( + cx, + ASSERTIONS_ON_CONSTANTS, + macro_call.span, + &format!( + "`{}!(true)` will be optimized out by the compiler", + cx.tcx.item_name(macro_call.def_id) + ), + None, + "remove it", + ); + } else if !is_debug { + let (assert_arg, panic_arg) = match panic_expn { + PanicExpn::Empty => ("", ""), + _ => (", ..", ".."), + }; + span_lint_and_help( + cx, + ASSERTIONS_ON_CONSTANTS, + macro_call.span, + &format!("`assert!(false{})` should probably be replaced", assert_arg), + None, + &format!("use `panic!({})` or `unreachable!({0})`", panic_arg), + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/assertions_on_result_states.rs b/src/tools/clippy/clippy_lints/src/assertions_on_result_states.rs new file mode 100644 index 000000000..4caab6230 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/assertions_on_result_states.rs @@ -0,0 +1,101 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::{find_assert_args, root_macro_call_first_node, PanicExpn}; +use clippy_utils::path_res; +use clippy_utils::source::snippet_with_context; +use clippy_utils::ty::{implements_trait, is_copy, is_type_diagnostic_item}; +use clippy_utils::usage::local_used_after_expr; +use rustc_errors::Applicability; +use rustc_hir::def::Res; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `assert!(r.is_ok())` or `assert!(r.is_err())` calls. + /// + /// ### Why is this bad? + /// An assertion failure cannot output an useful message of the error. + /// + /// ### Known problems + /// The suggested replacement decreases the readability of code and log output. + /// + /// ### Example + /// ```rust,ignore + /// # let r = Ok::<_, ()>(()); + /// assert!(r.is_ok()); + /// # let r = Err::<_, ()>(()); + /// assert!(r.is_err()); + /// ``` + #[clippy::version = "1.64.0"] + pub ASSERTIONS_ON_RESULT_STATES, + restriction, + "`assert!(r.is_ok())`/`assert!(r.is_err())` gives worse error message than directly calling `r.unwrap()`/`r.unwrap_err()`" +} + +declare_lint_pass!(AssertionsOnResultStates => [ASSERTIONS_ON_RESULT_STATES]); + +impl<'tcx> LateLintPass<'tcx> for AssertionsOnResultStates { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if let Some(macro_call) = root_macro_call_first_node(cx, e) + && matches!(cx.tcx.get_diagnostic_name(macro_call.def_id), Some(sym::assert_macro)) + && let Some((condition, panic_expn)) = find_assert_args(cx, e, macro_call.expn) + && matches!(panic_expn, PanicExpn::Empty) + && let ExprKind::MethodCall(method_segment, [recv], _) = condition.kind + && let result_type_with_refs = cx.typeck_results().expr_ty(recv) + && let result_type = result_type_with_refs.peel_refs() + && is_type_diagnostic_item(cx, result_type, sym::Result) + && let ty::Adt(_, substs) = result_type.kind() + { + if !is_copy(cx, result_type) { + if result_type_with_refs != result_type { + return; + } else if let Res::Local(binding_id) = path_res(cx, recv) + && local_used_after_expr(cx, binding_id, recv) { + return; + } + } + let mut app = Applicability::MachineApplicable; + match method_segment.ident.as_str() { + "is_ok" if has_debug_impl(cx, substs.type_at(1)) => { + span_lint_and_sugg( + cx, + ASSERTIONS_ON_RESULT_STATES, + macro_call.span, + "called `assert!` with `Result::is_ok`", + "replace with", + format!( + "{}.unwrap()", + snippet_with_context(cx, recv.span, condition.span.ctxt(), "..", &mut app).0 + ), + app, + ); + } + "is_err" if has_debug_impl(cx, substs.type_at(0)) => { + span_lint_and_sugg( + cx, + ASSERTIONS_ON_RESULT_STATES, + macro_call.span, + "called `assert!` with `Result::is_err`", + "replace with", + format!( + "{}.unwrap_err()", + snippet_with_context(cx, recv.span, condition.span.ctxt(), "..", &mut app).0 + ), + app, + ); + } + _ => (), + }; + } + } +} + +/// This checks whether a given type is known to implement Debug. +fn has_debug_impl<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + cx.tcx + .get_diagnostic_item(sym::Debug) + .map_or(false, |debug| implements_trait(cx, ty, debug, &[])) +} diff --git a/src/tools/clippy/clippy_lints/src/async_yields_async.rs b/src/tools/clippy/clippy_lints/src/async_yields_async.rs new file mode 100644 index 000000000..27c2896e1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/async_yields_async.rs @@ -0,0 +1,89 @@ +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::source::snippet; +use clippy_utils::ty::implements_trait; +use rustc_errors::Applicability; +use rustc_hir::{AsyncGeneratorKind, Body, BodyId, ExprKind, GeneratorKind, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for async blocks that yield values of types + /// that can themselves be awaited. + /// + /// ### Why is this bad? + /// An await is likely missing. + /// + /// ### Example + /// ```rust + /// async fn foo() {} + /// + /// fn bar() { + /// let x = async { + /// foo() + /// }; + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// async fn foo() {} + /// + /// fn bar() { + /// let x = async { + /// foo().await + /// }; + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub ASYNC_YIELDS_ASYNC, + correctness, + "async blocks that return a type that can be awaited" +} + +declare_lint_pass!(AsyncYieldsAsync => [ASYNC_YIELDS_ASYNC]); + +impl<'tcx> LateLintPass<'tcx> for AsyncYieldsAsync { + fn check_body(&mut self, cx: &LateContext<'tcx>, body: &'tcx Body<'_>) { + use AsyncGeneratorKind::{Block, Closure}; + // For functions, with explicitly defined types, don't warn. + // XXXkhuey maybe we should? + if let Some(GeneratorKind::Async(Block | Closure)) = body.generator_kind { + if let Some(future_trait_def_id) = cx.tcx.lang_items().future_trait() { + let body_id = BodyId { + hir_id: body.value.hir_id, + }; + let typeck_results = cx.tcx.typeck_body(body_id); + let expr_ty = typeck_results.expr_ty(&body.value); + + if implements_trait(cx, expr_ty, future_trait_def_id, &[]) { + let return_expr_span = match &body.value.kind { + // XXXkhuey there has to be a better way. + ExprKind::Block(block, _) => block.expr.map(|e| e.span), + ExprKind::Path(QPath::Resolved(_, path)) => Some(path.span), + _ => None, + }; + if let Some(return_expr_span) = return_expr_span { + span_lint_hir_and_then( + cx, + ASYNC_YIELDS_ASYNC, + body.value.hir_id, + return_expr_span, + "an async construct yields a type which is itself awaitable", + |db| { + db.span_label(body.value.span, "outer async construct"); + db.span_label(return_expr_span, "awaitable value not awaited"); + db.span_suggestion( + return_expr_span, + "consider awaiting this value", + format!("{}.await", snippet(cx, return_expr_span, "..")), + Applicability::MaybeIncorrect, + ); + }, + ); + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/attrs.rs b/src/tools/clippy/clippy_lints/src/attrs.rs new file mode 100644 index 000000000..4bcbeacf9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/attrs.rs @@ -0,0 +1,738 @@ +//! 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; +use clippy_utils::source::{first_line_of_span, is_present_in_source, snippet_opt, without_block_comments}; +use clippy_utils::{extract_msrv_attr, meets_msrv}; +use if_chain::if_chain; +use rustc_ast::{AttrKind, AttrStyle, Attribute, Lit, LitKind, MetaItemKind, NestedMetaItem}; +use rustc_errors::Applicability; +use rustc_hir::{ + Block, Expr, ExprKind, ImplItem, ImplItemKind, Item, ItemKind, StmtKind, TraitFn, TraitItem, TraitItemKind, +}; +use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_semver::RustcVersion; +use rustc_session::{declare_lint_pass, declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Span; +use rustc_span::sym; +use rustc_span::symbol::Symbol; +use semver::Version; + +static UNIX_SYSTEMS: &[&str] = &[ + "android", + "dragonfly", + "emscripten", + "freebsd", + "fuchsia", + "haiku", + "illumos", + "ios", + "l4re", + "linux", + "macos", + "netbsd", + "openbsd", + "redox", + "solaris", + "vxworks", +]; + +// NOTE: windows is excluded from the list because it's also a valid target family. +static NON_UNIX_SYSTEMS: &[&str] = &["hermit", "none", "wasi"]; + +declare_clippy_lint! { + /// ### What it does + /// Checks for items annotated with `#[inline(always)]`, + /// unless the annotated function is empty or simply panics. + /// + /// ### Why is this bad? + /// While there are valid uses of this annotation (and once + /// you know when to use it, by all means `allow` this lint), it's a common + /// newbie-mistake to pepper one's code with it. + /// + /// As a rule of thumb, before slapping `#[inline(always)]` on a function, + /// measure if that additional function call really affects your runtime profile + /// sufficiently to make up for the increase in compile time. + /// + /// ### Known problems + /// False positives, big time. This lint is meant to be + /// deactivated by everyone doing serious performance work. This means having + /// done the measurement. + /// + /// ### Example + /// ```ignore + /// #[inline(always)] + /// fn not_quite_hot_code(..) { ... } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INLINE_ALWAYS, + pedantic, + "use of `#[inline(always)]`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `extern crate` and `use` items annotated with + /// lint attributes. + /// + /// This lint permits lint attributes for lints emitted on the items themself. + /// For `use` items these lints are: + /// * deprecated + /// * unreachable_pub + /// * unused_imports + /// * clippy::enum_glob_use + /// * clippy::macro_use_imports + /// * clippy::wildcard_imports + /// + /// For `extern crate` items these lints are: + /// * `unused_imports` on items with `#[macro_use]` + /// + /// ### Why is this bad? + /// Lint attributes have no effect on crate imports. Most + /// likely a `!` was forgotten. + /// + /// ### Example + /// ```ignore + /// #[deny(dead_code)] + /// extern crate foo; + /// #[forbid(dead_code)] + /// use foo::bar; + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// #[allow(unused_imports)] + /// use foo::baz; + /// #[allow(unused_imports)] + /// #[macro_use] + /// extern crate baz; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USELESS_ATTRIBUTE, + correctness, + "use of lint attributes on `extern crate` items" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `#[deprecated]` annotations with a `since` + /// field that is not a valid semantic version. + /// + /// ### Why is this bad? + /// For checking the version of the deprecation, it must be + /// a valid semver. Failing that, the contained information is useless. + /// + /// ### Example + /// ```rust + /// #[deprecated(since = "forever")] + /// fn something_else() { /* ... */ } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DEPRECATED_SEMVER, + correctness, + "use of `#[deprecated(since = \"x\")]` where x is not semver" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for empty lines after outer attributes + /// + /// ### Why is this bad? + /// Most likely the attribute was meant to be an inner attribute using a '!'. + /// If it was meant to be an outer attribute, then the following item + /// should not be separated by empty lines. + /// + /// ### Known problems + /// Can cause false positives. + /// + /// From the clippy side it's difficult to detect empty lines between an attributes and the + /// following item because empty lines and comments are not part of the AST. The parsing + /// currently works for basic cases but is not perfect. + /// + /// ### Example + /// ```rust + /// #[allow(dead_code)] + /// + /// fn not_quite_good_code() { } + /// ``` + /// + /// Use instead: + /// ```rust + /// // Good (as inner attribute) + /// #![allow(dead_code)] + /// + /// fn this_is_fine() { } + /// + /// // or + /// + /// // Good (as outer attribute) + /// #[allow(dead_code)] + /// fn this_is_fine_too() { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EMPTY_LINE_AFTER_OUTER_ATTR, + nursery, + "empty line after outer attribute" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `warn`/`deny`/`forbid` attributes targeting the whole clippy::restriction category. + /// + /// ### Why is this bad? + /// Restriction lints sometimes are in contrast with other lints or even go against idiomatic rust. + /// These lints should only be enabled on a lint-by-lint basis and with careful consideration. + /// + /// ### Example + /// ```rust + /// #![deny(clippy::restriction)] + /// ``` + /// + /// Use instead: + /// ```rust + /// #![deny(clippy::as_conversions)] + /// ``` + #[clippy::version = "1.47.0"] + pub BLANKET_CLIPPY_RESTRICTION_LINTS, + suspicious, + "enabling the complete restriction group" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `#[cfg_attr(rustfmt, rustfmt_skip)]` and suggests to replace it + /// with `#[rustfmt::skip]`. + /// + /// ### Why is this bad? + /// Since tool_attributes ([rust-lang/rust#44690](https://github.com/rust-lang/rust/issues/44690)) + /// are stable now, they should be used instead of the old `cfg_attr(rustfmt)` attributes. + /// + /// ### Known problems + /// This lint doesn't detect crate level inner attributes, because they get + /// processed before the PreExpansionPass lints get executed. See + /// [#3123](https://github.com/rust-lang/rust-clippy/pull/3123#issuecomment-422321765) + /// + /// ### Example + /// ```rust + /// #[cfg_attr(rustfmt, rustfmt_skip)] + /// fn main() { } + /// ``` + /// + /// Use instead: + /// ```rust + /// #[rustfmt::skip] + /// fn main() { } + /// ``` + #[clippy::version = "1.32.0"] + pub DEPRECATED_CFG_ATTR, + complexity, + "usage of `cfg_attr(rustfmt)` instead of tool attributes" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for cfg attributes having operating systems used in target family position. + /// + /// ### Why is this bad? + /// The configuration option will not be recognised and the related item will not be included + /// by the conditional compilation engine. + /// + /// ### Example + /// ```rust + /// #[cfg(linux)] + /// fn conditional() { } + /// ``` + /// + /// Use instead: + /// ```rust + /// # mod hidden { + /// #[cfg(target_os = "linux")] + /// fn conditional() { } + /// # } + /// + /// // or + /// + /// #[cfg(unix)] + /// fn conditional() { } + /// ``` + /// Check the [Rust Reference](https://doc.rust-lang.org/reference/conditional-compilation.html#target_os) for more details. + #[clippy::version = "1.45.0"] + pub MISMATCHED_TARGET_OS, + correctness, + "usage of `cfg(operating_system)` instead of `cfg(target_os = \"operating_system\")`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for attributes that allow lints without a reason. + /// + /// (This requires the `lint_reasons` feature) + /// + /// ### Why is this bad? + /// Allowing a lint should always have a reason. This reason should be documented to + /// ensure that others understand the reasoning + /// + /// ### Example + /// ```rust + /// #![feature(lint_reasons)] + /// + /// #![allow(clippy::some_lint)] + /// ``` + /// + /// Use instead: + /// ```rust + /// #![feature(lint_reasons)] + /// + /// #![allow(clippy::some_lint, reason = "False positive rust-lang/rust-clippy#1002020")] + /// ``` + #[clippy::version = "1.61.0"] + pub ALLOW_ATTRIBUTES_WITHOUT_REASON, + restriction, + "ensures that all `allow` and `expect` attributes have a reason" +} + +declare_lint_pass!(Attributes => [ + ALLOW_ATTRIBUTES_WITHOUT_REASON, + INLINE_ALWAYS, + DEPRECATED_SEMVER, + USELESS_ATTRIBUTE, + BLANKET_CLIPPY_RESTRICTION_LINTS, +]); + +impl<'tcx> LateLintPass<'tcx> for Attributes { + fn check_attribute(&mut self, cx: &LateContext<'tcx>, attr: &'tcx Attribute) { + if let Some(items) = &attr.meta_item_list() { + if let Some(ident) = attr.ident() { + if is_lint_level(ident.name) { + check_clippy_lint_names(cx, ident.name, items); + } + if matches!(ident.name, sym::allow | sym::expect) { + check_lint_reason(cx, ident.name, items, attr); + } + if items.is_empty() || !attr.has_name(sym::deprecated) { + return; + } + for item in items { + if_chain! { + if let NestedMetaItem::MetaItem(mi) = &item; + if let MetaItemKind::NameValue(lit) = &mi.kind; + if mi.has_name(sym::since); + then { + check_semver(cx, item.span(), lit); + } + } + } + } + } + } + + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + if is_relevant_item(cx, item) { + check_attrs(cx, item.span, item.ident.name, attrs); + } + match item.kind { + ItemKind::ExternCrate(..) | ItemKind::Use(..) => { + let skip_unused_imports = attrs.iter().any(|attr| attr.has_name(sym::macro_use)); + + for attr in attrs { + if in_external_macro(cx.sess(), attr.span) { + return; + } + if let Some(lint_list) = &attr.meta_item_list() { + if attr.ident().map_or(false, |ident| is_lint_level(ident.name)) { + for lint in lint_list { + match item.kind { + ItemKind::Use(..) => { + if is_word(lint, sym::unused_imports) + || is_word(lint, sym::deprecated) + || is_word(lint, sym!(unreachable_pub)) + || is_word(lint, sym!(unused)) + || extract_clippy_lint(lint).map_or(false, |s| { + matches!( + s.as_str(), + "wildcard_imports" + | "enum_glob_use" + | "redundant_pub_crate" + | "macro_use_imports", + ) + }) + { + return; + } + }, + ItemKind::ExternCrate(..) => { + if is_word(lint, sym::unused_imports) && skip_unused_imports { + return; + } + if is_word(lint, sym!(unused_extern_crates)) { + return; + } + }, + _ => {}, + } + } + let line_span = first_line_of_span(cx, attr.span); + + if let Some(mut sugg) = snippet_opt(cx, line_span) { + if sugg.contains("#[") { + span_lint_and_then( + cx, + USELESS_ATTRIBUTE, + line_span, + "useless lint attribute", + |diag| { + sugg = sugg.replacen("#[", "#![", 1); + diag.span_suggestion( + line_span, + "if you just forgot a `!`, use", + sugg, + Applicability::MaybeIncorrect, + ); + }, + ); + } + } + } + } + } + }, + _ => {}, + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) { + if is_relevant_impl(cx, item) { + check_attrs(cx, item.span, item.ident.name, cx.tcx.hir().attrs(item.hir_id())); + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) { + if is_relevant_trait(cx, item) { + check_attrs(cx, item.span, item.ident.name, cx.tcx.hir().attrs(item.hir_id())); + } + } +} + +/// Returns the lint name if it is clippy lint. +fn extract_clippy_lint(lint: &NestedMetaItem) -> Option<Symbol> { + if_chain! { + if let Some(meta_item) = lint.meta_item(); + if meta_item.path.segments.len() > 1; + if let tool_name = meta_item.path.segments[0].ident; + if tool_name.name == sym::clippy; + then { + let lint_name = meta_item.path.segments.last().unwrap().ident.name; + return Some(lint_name); + } + } + None +} + +fn check_clippy_lint_names(cx: &LateContext<'_>, name: Symbol, items: &[NestedMetaItem]) { + for lint in items { + if let Some(lint_name) = extract_clippy_lint(lint) { + if lint_name.as_str() == "restriction" && name != sym::allow { + span_lint_and_help( + cx, + BLANKET_CLIPPY_RESTRICTION_LINTS, + lint.span(), + "restriction lints are not meant to be all enabled", + None, + "try enabling only the lints you really need", + ); + } + } + } +} + +fn check_lint_reason(cx: &LateContext<'_>, name: Symbol, items: &[NestedMetaItem], attr: &'_ Attribute) { + // Check for the feature + if !cx.tcx.sess.features_untracked().lint_reasons { + return; + } + + // Check if the reason is present + if let Some(item) = items.last().and_then(NestedMetaItem::meta_item) + && let MetaItemKind::NameValue(_) = &item.kind + && item.path == sym::reason + { + return; + } + + span_lint_and_help( + cx, + ALLOW_ATTRIBUTES_WITHOUT_REASON, + attr.span, + &format!("`{}` attribute without specifying a reason", name.as_str()), + None, + "try adding a reason at the end with `, reason = \"..\"`", + ); +} + +fn is_relevant_item(cx: &LateContext<'_>, item: &Item<'_>) -> bool { + if let ItemKind::Fn(_, _, eid) = item.kind { + is_relevant_expr(cx, cx.tcx.typeck_body(eid), &cx.tcx.hir().body(eid).value) + } else { + true + } +} + +fn is_relevant_impl(cx: &LateContext<'_>, item: &ImplItem<'_>) -> bool { + match item.kind { + ImplItemKind::Fn(_, eid) => is_relevant_expr(cx, cx.tcx.typeck_body(eid), &cx.tcx.hir().body(eid).value), + _ => false, + } +} + +fn is_relevant_trait(cx: &LateContext<'_>, item: &TraitItem<'_>) -> bool { + match item.kind { + TraitItemKind::Fn(_, TraitFn::Required(_)) => true, + TraitItemKind::Fn(_, TraitFn::Provided(eid)) => { + is_relevant_expr(cx, cx.tcx.typeck_body(eid), &cx.tcx.hir().body(eid).value) + }, + _ => false, + } +} + +fn is_relevant_block(cx: &LateContext<'_>, typeck_results: &ty::TypeckResults<'_>, block: &Block<'_>) -> bool { + block.stmts.first().map_or( + block + .expr + .as_ref() + .map_or(false, |e| is_relevant_expr(cx, typeck_results, e)), + |stmt| match &stmt.kind { + StmtKind::Local(_) => true, + StmtKind::Expr(expr) | StmtKind::Semi(expr) => is_relevant_expr(cx, typeck_results, expr), + StmtKind::Item(_) => false, + }, + ) +} + +fn is_relevant_expr(cx: &LateContext<'_>, typeck_results: &ty::TypeckResults<'_>, expr: &Expr<'_>) -> bool { + if macro_backtrace(expr.span).last().map_or(false, |macro_call| { + is_panic(cx, macro_call.def_id) || cx.tcx.item_name(macro_call.def_id) == sym::unreachable + }) { + return false; + } + match &expr.kind { + ExprKind::Block(block, _) => is_relevant_block(cx, typeck_results, block), + ExprKind::Ret(Some(e)) => is_relevant_expr(cx, typeck_results, e), + ExprKind::Ret(None) | ExprKind::Break(_, None) => false, + _ => true, + } +} + +fn check_attrs(cx: &LateContext<'_>, span: Span, name: Symbol, attrs: &[Attribute]) { + if span.from_expansion() { + return; + } + + for attr in attrs { + if let Some(values) = attr.meta_item_list() { + if values.len() != 1 || !attr.has_name(sym::inline) { + continue; + } + if is_word(&values[0], sym::always) { + span_lint( + cx, + INLINE_ALWAYS, + attr.span, + &format!( + "you have declared `#[inline(always)]` on `{}`. This is usually a bad idea", + name + ), + ); + } + } + } +} + +fn check_semver(cx: &LateContext<'_>, span: Span, lit: &Lit) { + if let LitKind::Str(is, _) = lit.kind { + if Version::parse(is.as_str()).is_ok() { + return; + } + } + span_lint( + cx, + DEPRECATED_SEMVER, + span, + "the since field must contain a semver-compliant version", + ); +} + +fn is_word(nmi: &NestedMetaItem, expected: Symbol) -> bool { + if let NestedMetaItem::MetaItem(mi) = &nmi { + mi.is_word() && mi.has_name(expected) + } else { + false + } +} + +pub struct EarlyAttributes { + pub msrv: Option<RustcVersion>, +} + +impl_lint_pass!(EarlyAttributes => [ + DEPRECATED_CFG_ATTR, + MISMATCHED_TARGET_OS, + EMPTY_LINE_AFTER_OUTER_ATTR, +]); + +impl EarlyLintPass for EarlyAttributes { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &rustc_ast::Item) { + check_empty_line_after_outer_attr(cx, item); + } + + fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &Attribute) { + check_deprecated_cfg_attr(cx, attr, self.msrv); + check_mismatched_target_os(cx, attr); + } + + extract_msrv_attr!(EarlyContext); +} + +fn check_empty_line_after_outer_attr(cx: &EarlyContext<'_>, item: &rustc_ast::Item) { + let mut iter = item.attrs.iter().peekable(); + while let Some(attr) = iter.next() { + if matches!(attr.kind, AttrKind::Normal(..)) + && attr.style == AttrStyle::Outer + && is_present_in_source(cx, attr.span) + { + let begin_of_attr_to_item = Span::new(attr.span.lo(), item.span.lo(), item.span.ctxt(), item.span.parent()); + let end_of_attr_to_next_attr_or_item = Span::new( + attr.span.hi(), + iter.peek().map_or(item.span.lo(), |next_attr| next_attr.span.lo()), + item.span.ctxt(), + item.span.parent(), + ); + + if let Some(snippet) = snippet_opt(cx, end_of_attr_to_next_attr_or_item) { + let lines = snippet.split('\n').collect::<Vec<_>>(); + let lines = without_block_comments(lines); + + if lines.iter().filter(|l| l.trim().is_empty()).count() > 2 { + span_lint( + cx, + EMPTY_LINE_AFTER_OUTER_ATTR, + begin_of_attr_to_item, + "found an empty line after an outer attribute. \ + Perhaps you forgot to add a `!` to make it an inner attribute?", + ); + } + } + } + } +} + +fn check_deprecated_cfg_attr(cx: &EarlyContext<'_>, attr: &Attribute, msrv: Option<RustcVersion>) { + if_chain! { + if meets_msrv(msrv, msrvs::TOOL_ATTRIBUTES); + // check cfg_attr + if attr.has_name(sym::cfg_attr); + if let Some(items) = attr.meta_item_list(); + if items.len() == 2; + // check for `rustfmt` + if let Some(feature_item) = items[0].meta_item(); + if feature_item.has_name(sym::rustfmt); + // check for `rustfmt_skip` and `rustfmt::skip` + if let Some(skip_item) = &items[1].meta_item(); + if skip_item.has_name(sym!(rustfmt_skip)) + || skip_item + .path + .segments + .last() + .expect("empty path in attribute") + .ident + .name + == sym::skip; + // Only lint outer attributes, because custom inner attributes are unstable + // Tracking issue: https://github.com/rust-lang/rust/issues/54726 + if attr.style == AttrStyle::Outer; + then { + span_lint_and_sugg( + cx, + DEPRECATED_CFG_ATTR, + attr.span, + "`cfg_attr` is deprecated for rustfmt and got replaced by tool attributes", + "use", + "#[rustfmt::skip]".to_string(), + Applicability::MachineApplicable, + ); + } + } +} + +fn check_mismatched_target_os(cx: &EarlyContext<'_>, attr: &Attribute) { + fn find_os(name: &str) -> Option<&'static str> { + UNIX_SYSTEMS + .iter() + .chain(NON_UNIX_SYSTEMS.iter()) + .find(|&&os| os == name) + .copied() + } + + fn is_unix(name: &str) -> bool { + UNIX_SYSTEMS.iter().any(|&os| os == name) + } + + fn find_mismatched_target_os(items: &[NestedMetaItem]) -> Vec<(&str, Span)> { + let mut mismatched = Vec::new(); + + for item in items { + if let NestedMetaItem::MetaItem(meta) = item { + match &meta.kind { + MetaItemKind::List(list) => { + mismatched.extend(find_mismatched_target_os(list)); + }, + MetaItemKind::Word => { + if_chain! { + if let Some(ident) = meta.ident(); + if let Some(os) = find_os(ident.name.as_str()); + then { + mismatched.push((os, ident.span)); + } + } + }, + MetaItemKind::NameValue(..) => {}, + } + } + } + + mismatched + } + + if_chain! { + if attr.has_name(sym::cfg); + if let Some(list) = attr.meta_item_list(); + let mismatched = find_mismatched_target_os(&list); + if !mismatched.is_empty(); + then { + let mess = "operating system used in target family position"; + + span_lint_and_then(cx, MISMATCHED_TARGET_OS, attr.span, mess, |diag| { + // Avoid showing the unix suggestion multiple times in case + // we have more than one mismatch for unix-like systems + let mut unix_suggested = false; + + for (os, span) in mismatched { + let sugg = format!("target_os = \"{}\"", os); + diag.span_suggestion(span, "try", sugg, Applicability::MaybeIncorrect); + + if !unix_suggested && is_unix(os) { + diag.help("did you mean `unix`?"); + unix_suggested = true; + } + } + }); + } + } +} + +fn is_lint_level(symbol: Symbol) -> bool { + matches!(symbol, sym::allow | sym::expect | sym::warn | sym::deny | sym::forbid) +} diff --git a/src/tools/clippy/clippy_lints/src/await_holding_invalid.rs b/src/tools/clippy/clippy_lints/src/await_holding_invalid.rs new file mode 100644 index 000000000..1761360fb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/await_holding_invalid.rs @@ -0,0 +1,289 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{match_def_path, paths}; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::def_id::DefId; +use rustc_hir::{def::Res, AsyncGeneratorKind, Body, BodyId, GeneratorKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::GeneratorInteriorTypeCause; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; + +use crate::utils::conf::DisallowedType; + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to await while holding a non-async-aware MutexGuard. + /// + /// ### Why is this bad? + /// The Mutex types found in std::sync and parking_lot + /// are not designed to operate in an async context across await points. + /// + /// There are two potential solutions. One is to use an async-aware Mutex + /// type. Many asynchronous foundation crates provide such a Mutex type. The + /// other solution is to ensure the mutex is unlocked before calling await, + /// either by introducing a scope or an explicit call to Drop::drop. + /// + /// ### Known problems + /// Will report false positive for explicitly dropped guards + /// ([#6446](https://github.com/rust-lang/rust-clippy/issues/6446)). A workaround for this is + /// to wrap the `.lock()` call in a block instead of explicitly dropping the guard. + /// + /// ### Example + /// ```rust + /// # use std::sync::Mutex; + /// # async fn baz() {} + /// async fn foo(x: &Mutex<u32>) { + /// let mut guard = x.lock().unwrap(); + /// *guard += 1; + /// baz().await; + /// } + /// + /// async fn bar(x: &Mutex<u32>) { + /// let mut guard = x.lock().unwrap(); + /// *guard += 1; + /// drop(guard); // explicit drop + /// baz().await; + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::sync::Mutex; + /// # async fn baz() {} + /// async fn foo(x: &Mutex<u32>) { + /// { + /// let mut guard = x.lock().unwrap(); + /// *guard += 1; + /// } + /// baz().await; + /// } + /// + /// async fn bar(x: &Mutex<u32>) { + /// { + /// let mut guard = x.lock().unwrap(); + /// *guard += 1; + /// } // guard dropped here at end of scope + /// baz().await; + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub AWAIT_HOLDING_LOCK, + suspicious, + "inside an async function, holding a `MutexGuard` while calling `await`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to await while holding a `RefCell` `Ref` or `RefMut`. + /// + /// ### Why is this bad? + /// `RefCell` refs only check for exclusive mutable access + /// at runtime. Holding onto a `RefCell` ref across an `await` suspension point + /// risks panics from a mutable ref shared while other refs are outstanding. + /// + /// ### Known problems + /// Will report false positive for explicitly dropped refs + /// ([#6353](https://github.com/rust-lang/rust-clippy/issues/6353)). A workaround for this is + /// to wrap the `.borrow[_mut]()` call in a block instead of explicitly dropping the ref. + /// + /// ### Example + /// ```rust + /// # use std::cell::RefCell; + /// # async fn baz() {} + /// async fn foo(x: &RefCell<u32>) { + /// let mut y = x.borrow_mut(); + /// *y += 1; + /// baz().await; + /// } + /// + /// async fn bar(x: &RefCell<u32>) { + /// let mut y = x.borrow_mut(); + /// *y += 1; + /// drop(y); // explicit drop + /// baz().await; + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::cell::RefCell; + /// # async fn baz() {} + /// async fn foo(x: &RefCell<u32>) { + /// { + /// let mut y = x.borrow_mut(); + /// *y += 1; + /// } + /// baz().await; + /// } + /// + /// async fn bar(x: &RefCell<u32>) { + /// { + /// let mut y = x.borrow_mut(); + /// *y += 1; + /// } // y dropped here at end of scope + /// baz().await; + /// } + /// ``` + #[clippy::version = "1.49.0"] + pub AWAIT_HOLDING_REFCELL_REF, + suspicious, + "inside an async function, holding a `RefCell` ref while calling `await`" +} + +declare_clippy_lint! { + /// ### What it does + /// Allows users to configure types which should not be held across `await` + /// suspension points. + /// + /// ### Why is this bad? + /// There are some types which are perfectly "safe" to be used concurrently + /// from a memory access perspective but will cause bugs at runtime if they + /// are held in such a way. + /// + /// ### Example + /// + /// ```toml + /// await-holding-invalid-types = [ + /// # You can specify a type name + /// "CustomLockType", + /// # You can (optionally) specify a reason + /// { path = "OtherCustomLockType", reason = "Relies on a thread local" } + /// ] + /// ``` + /// + /// ```rust + /// # async fn baz() {} + /// struct CustomLockType; + /// struct OtherCustomLockType; + /// async fn foo() { + /// let _x = CustomLockType; + /// let _y = OtherCustomLockType; + /// baz().await; // Lint violation + /// } + /// ``` + #[clippy::version = "1.62.0"] + pub AWAIT_HOLDING_INVALID_TYPE, + suspicious, + "holding a type across an await point which is not allowed to be held as per the configuration" +} + +impl_lint_pass!(AwaitHolding => [AWAIT_HOLDING_LOCK, AWAIT_HOLDING_REFCELL_REF, AWAIT_HOLDING_INVALID_TYPE]); + +#[derive(Debug)] +pub struct AwaitHolding { + conf_invalid_types: Vec<DisallowedType>, + def_ids: FxHashMap<DefId, DisallowedType>, +} + +impl AwaitHolding { + pub(crate) fn new(conf_invalid_types: Vec<DisallowedType>) -> Self { + Self { + conf_invalid_types, + def_ids: FxHashMap::default(), + } + } +} + +impl LateLintPass<'_> for AwaitHolding { + fn check_crate(&mut self, cx: &LateContext<'_>) { + for conf in &self.conf_invalid_types { + let path = match conf { + DisallowedType::Simple(path) | DisallowedType::WithReason { path, .. } => path, + }; + let segs: Vec<_> = path.split("::").collect(); + if let Res::Def(_, id) = clippy_utils::def_path_res(cx, &segs) { + self.def_ids.insert(id, conf.clone()); + } + } + } + + fn check_body(&mut self, cx: &LateContext<'_>, body: &'_ Body<'_>) { + use AsyncGeneratorKind::{Block, Closure, Fn}; + if let Some(GeneratorKind::Async(Block | Closure | Fn)) = body.generator_kind { + let body_id = BodyId { + hir_id: body.value.hir_id, + }; + let typeck_results = cx.tcx.typeck_body(body_id); + self.check_interior_types( + cx, + typeck_results.generator_interior_types.as_ref().skip_binder(), + body.value.span, + ); + } + } +} + +impl AwaitHolding { + fn check_interior_types(&self, cx: &LateContext<'_>, ty_causes: &[GeneratorInteriorTypeCause<'_>], span: Span) { + for ty_cause in ty_causes { + if let rustc_middle::ty::Adt(adt, _) = ty_cause.ty.kind() { + if is_mutex_guard(cx, adt.did()) { + span_lint_and_then( + cx, + AWAIT_HOLDING_LOCK, + ty_cause.span, + "this `MutexGuard` is held across an `await` point", + |diag| { + diag.help( + "consider using an async-aware `Mutex` type or ensuring the \ + `MutexGuard` is dropped before calling await", + ); + diag.span_note( + ty_cause.scope_span.unwrap_or(span), + "these are all the `await` points this lock is held through", + ); + }, + ); + } else if is_refcell_ref(cx, adt.did()) { + span_lint_and_then( + cx, + AWAIT_HOLDING_REFCELL_REF, + ty_cause.span, + "this `RefCell` reference is held across an `await` point", + |diag| { + diag.help("ensure the reference is dropped before calling `await`"); + diag.span_note( + ty_cause.scope_span.unwrap_or(span), + "these are all the `await` points this reference is held through", + ); + }, + ); + } else if let Some(disallowed) = self.def_ids.get(&adt.did()) { + emit_invalid_type(cx, ty_cause.span, disallowed); + } + } + } + } +} + +fn emit_invalid_type(cx: &LateContext<'_>, span: Span, disallowed: &DisallowedType) { + let (type_name, reason) = match disallowed { + DisallowedType::Simple(path) => (path, &None), + DisallowedType::WithReason { path, reason } => (path, reason), + }; + + span_lint_and_then( + cx, + AWAIT_HOLDING_INVALID_TYPE, + span, + &format!("`{type_name}` may not be held across an `await` point per `clippy.toml`",), + |diag| { + if let Some(reason) = reason { + diag.note(reason.clone()); + } + }, + ); +} + +fn is_mutex_guard(cx: &LateContext<'_>, def_id: DefId) -> bool { + match_def_path(cx, def_id, &paths::MUTEX_GUARD) + || match_def_path(cx, def_id, &paths::RWLOCK_READ_GUARD) + || match_def_path(cx, def_id, &paths::RWLOCK_WRITE_GUARD) + || match_def_path(cx, def_id, &paths::PARKING_LOT_MUTEX_GUARD) + || match_def_path(cx, def_id, &paths::PARKING_LOT_RWLOCK_READ_GUARD) + || match_def_path(cx, def_id, &paths::PARKING_LOT_RWLOCK_WRITE_GUARD) +} + +fn is_refcell_ref(cx: &LateContext<'_>, def_id: DefId) -> bool { + match_def_path(cx, def_id, &paths::REFCELL_REF) || match_def_path(cx, def_id, &paths::REFCELL_REFMUT) +} diff --git a/src/tools/clippy/clippy_lints/src/blacklisted_name.rs b/src/tools/clippy/clippy_lints/src/blacklisted_name.rs new file mode 100644 index 000000000..1600fb25d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/blacklisted_name.rs @@ -0,0 +1,77 @@ +use clippy_utils::{diagnostics::span_lint, is_test_module_or_function}; +use rustc_data_structures::fx::FxHashSet; +use rustc_hir::{Item, Pat, PatKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of blacklisted names for variables, such + /// as `foo`. + /// + /// ### Why is this bad? + /// These names are usually placeholder names and should be + /// avoided. + /// + /// ### Example + /// ```rust + /// let foo = 3.14; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub BLACKLISTED_NAME, + style, + "usage of a blacklisted/placeholder name" +} + +#[derive(Clone, Debug)] +pub struct BlacklistedName { + blacklist: FxHashSet<String>, + test_modules_deep: u32, +} + +impl BlacklistedName { + pub fn new(blacklist: FxHashSet<String>) -> Self { + Self { + blacklist, + test_modules_deep: 0, + } + } + + fn in_test_module(&self) -> bool { + self.test_modules_deep != 0 + } +} + +impl_lint_pass!(BlacklistedName => [BLACKLISTED_NAME]); + +impl<'tcx> LateLintPass<'tcx> for BlacklistedName { + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if is_test_module_or_function(cx.tcx, item) { + self.test_modules_deep = self.test_modules_deep.saturating_add(1); + } + } + + fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) { + // Check whether we are under the `test` attribute. + if self.in_test_module() { + return; + } + + if let PatKind::Binding(.., ident, _) = pat.kind { + if self.blacklist.contains(&ident.name.to_string()) { + span_lint( + cx, + BLACKLISTED_NAME, + ident.span, + &format!("use of a blacklisted/placeholder name `{}`", ident.name), + ); + } + } + } + + fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if is_test_module_or_function(cx.tcx, item) { + self.test_modules_deep = self.test_modules_deep.saturating_sub(1); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/blocks_in_if_conditions.rs b/src/tools/clippy/clippy_lints/src/blocks_in_if_conditions.rs new file mode 100644 index 000000000..ad206b5fb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/blocks_in_if_conditions.rs @@ -0,0 +1,155 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; +use clippy_utils::get_parent_expr; +use clippy_utils::higher; +use clippy_utils::source::snippet_block_with_applicability; +use clippy_utils::ty::implements_trait; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{BlockCheckMode, Closure, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `if` conditions that use blocks containing an + /// expression, statements or conditions that use closures with blocks. + /// + /// ### Why is this bad? + /// Style, using blocks in the condition makes it hard to read. + /// + /// ### Examples + /// ```rust + /// # fn somefunc() -> bool { true }; + /// if { true } { /* ... */ } + /// + /// if { let x = somefunc(); x } { /* ... */ } + /// ``` + /// + /// Use instead: + /// ```rust + /// # fn somefunc() -> bool { true }; + /// if true { /* ... */ } + /// + /// let res = { let x = somefunc(); x }; + /// if res { /* ... */ } + /// ``` + #[clippy::version = "1.45.0"] + pub BLOCKS_IN_IF_CONDITIONS, + style, + "useless or complex blocks that can be eliminated in conditions" +} + +declare_lint_pass!(BlocksInIfConditions => [BLOCKS_IN_IF_CONDITIONS]); + +struct ExVisitor<'a, 'tcx> { + found_block: Option<&'tcx Expr<'tcx>>, + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> Visitor<'tcx> for ExVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { + if let ExprKind::Closure(&Closure { body, .. }) = expr.kind { + // do not lint if the closure is called using an iterator (see #1141) + if_chain! { + if let Some(parent) = get_parent_expr(self.cx, expr); + if let ExprKind::MethodCall(_, [self_arg, ..], _) = &parent.kind; + let caller = self.cx.typeck_results().expr_ty(self_arg); + if let Some(iter_id) = self.cx.tcx.get_diagnostic_item(sym::Iterator); + if implements_trait(self.cx, caller, iter_id, &[]); + then { + return; + } + } + + let body = self.cx.tcx.hir().body(body); + let ex = &body.value; + if let ExprKind::Block(block, _) = ex.kind { + if !body.value.span.from_expansion() && !block.stmts.is_empty() { + self.found_block = Some(ex); + return; + } + } + } + walk_expr(self, expr); + } +} + +const BRACED_EXPR_MESSAGE: &str = "omit braces around single expression condition"; +const COMPLEX_BLOCK_MESSAGE: &str = "in an `if` condition, avoid complex blocks or closures with blocks; \ + instead, move the block or closure higher and bind it with a `let`"; + +impl<'tcx> LateLintPass<'tcx> for BlocksInIfConditions { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + if let Some(higher::If { cond, .. }) = higher::If::hir(expr) { + if let ExprKind::Block(block, _) = &cond.kind { + if block.rules == BlockCheckMode::DefaultBlock { + if block.stmts.is_empty() { + if let Some(ex) = &block.expr { + // don't dig into the expression here, just suggest that they remove + // the block + if expr.span.from_expansion() || ex.span.from_expansion() { + return; + } + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + BLOCKS_IN_IF_CONDITIONS, + cond.span, + BRACED_EXPR_MESSAGE, + "try", + format!( + "{}", + snippet_block_with_applicability( + cx, + ex.span, + "..", + Some(expr.span), + &mut applicability + ) + ), + applicability, + ); + } + } else { + let span = block.expr.as_ref().map_or_else(|| block.stmts[0].span, |e| e.span); + if span.from_expansion() || expr.span.from_expansion() { + return; + } + // move block higher + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + BLOCKS_IN_IF_CONDITIONS, + expr.span.with_hi(cond.span.hi()), + COMPLEX_BLOCK_MESSAGE, + "try", + format!( + "let res = {}; if res", + snippet_block_with_applicability( + cx, + block.span, + "..", + Some(expr.span), + &mut applicability + ), + ), + applicability, + ); + } + } + } else { + let mut visitor = ExVisitor { found_block: None, cx }; + walk_expr(&mut visitor, cond); + if let Some(block) = visitor.found_block { + span_lint(cx, BLOCKS_IN_IF_CONDITIONS, block.span, COMPLEX_BLOCK_MESSAGE); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs b/src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs new file mode 100644 index 000000000..95abe8aa5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs @@ -0,0 +1,107 @@ +use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node}; +use clippy_utils::{diagnostics::span_lint_and_sugg, ty::implements_trait}; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, Lit}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::Ident; + +declare_clippy_lint! { + /// ### What it does + /// This lint warns about boolean comparisons in assert-like macros. + /// + /// ### Why is this bad? + /// It is shorter to use the equivalent. + /// + /// ### Example + /// ```rust + /// assert_eq!("a".is_empty(), false); + /// assert_ne!("a".is_empty(), true); + /// ``` + /// + /// Use instead: + /// ```rust + /// assert!(!"a".is_empty()); + /// ``` + #[clippy::version = "1.53.0"] + pub BOOL_ASSERT_COMPARISON, + style, + "Using a boolean as comparison value in an assert_* macro when there is no need" +} + +declare_lint_pass!(BoolAssertComparison => [BOOL_ASSERT_COMPARISON]); + +fn is_bool_lit(e: &Expr<'_>) -> bool { + matches!( + e.kind, + ExprKind::Lit(Lit { + node: LitKind::Bool(_), + .. + }) + ) && !e.span.from_expansion() +} + +fn is_impl_not_trait_with_bool_out(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + let ty = cx.typeck_results().expr_ty(e); + + cx.tcx + .lang_items() + .not_trait() + .filter(|trait_id| implements_trait(cx, ty, *trait_id, &[])) + .and_then(|trait_id| { + cx.tcx.associated_items(trait_id).find_by_name_and_kind( + cx.tcx, + Ident::from_str("Output"), + ty::AssocKind::Type, + trait_id, + ) + }) + .map_or(false, |assoc_item| { + let proj = cx.tcx.mk_projection(assoc_item.def_id, cx.tcx.mk_substs_trait(ty, &[])); + let nty = cx.tcx.normalize_erasing_regions(cx.param_env, proj); + + nty.is_bool() + }) +} + +impl<'tcx> LateLintPass<'tcx> for BoolAssertComparison { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return }; + let macro_name = cx.tcx.item_name(macro_call.def_id); + if !matches!( + macro_name.as_str(), + "assert_eq" | "debug_assert_eq" | "assert_ne" | "debug_assert_ne" + ) { + return; + } + let Some ((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else { return }; + if !(is_bool_lit(a) ^ is_bool_lit(b)) { + // If there are two boolean arguments, we definitely don't understand + // what's going on, so better leave things as is... + // + // Or there is simply no boolean and then we can leave things as is! + return; + } + + if !is_impl_not_trait_with_bool_out(cx, a) || !is_impl_not_trait_with_bool_out(cx, b) { + // At this point the expression which is not a boolean + // literal does not implement Not trait with a bool output, + // so we cannot suggest to rewrite our code + return; + } + + let macro_name = macro_name.as_str(); + let non_eq_mac = ¯o_name[..macro_name.len() - 3]; + span_lint_and_sugg( + cx, + BOOL_ASSERT_COMPARISON, + macro_call.span, + &format!("used `{}!` with a literal bool", macro_name), + "replace it with", + format!("{}!(..)", non_eq_mac), + Applicability::MaybeIncorrect, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/booleans.rs b/src/tools/clippy/clippy_lints/src/booleans.rs new file mode 100644 index 000000000..526ee2f89 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/booleans.rs @@ -0,0 +1,512 @@ +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then}; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use clippy_utils::{eq_expr_value, get_trait_def_id, paths}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_expr, FnKind, Visitor}; +use rustc_hir::{BinOpKind, Body, Expr, ExprKind, FnDecl, HirId, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for boolean expressions that can be written more + /// concisely. + /// + /// ### Why is this bad? + /// Readability of boolean expressions suffers from + /// unnecessary duplication. + /// + /// ### Known problems + /// Ignores short circuiting behavior of `||` and + /// `&&`. Ignores `|`, `&` and `^`. + /// + /// ### Example + /// ```ignore + /// if a && true {} + /// if !(a == b) {} + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// if a {} + /// if a != b {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NONMINIMAL_BOOL, + complexity, + "boolean expressions that can be written more concisely" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for boolean expressions that contain terminals that + /// can be eliminated. + /// + /// ### Why is this bad? + /// This is most likely a logic bug. + /// + /// ### Known problems + /// Ignores short circuiting behavior. + /// + /// ### Example + /// ```rust,ignore + /// // The `b` is unnecessary, the expression is equivalent to `if a`. + /// if a && b || a { ... } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// if a {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LOGIC_BUG, + correctness, + "boolean expressions that contain terminals which can be eliminated" +} + +// For each pairs, both orders are considered. +const METHODS_WITH_NEGATION: [(&str, &str); 2] = [("is_some", "is_none"), ("is_err", "is_ok")]; + +declare_lint_pass!(NonminimalBool => [NONMINIMAL_BOOL, LOGIC_BUG]); + +impl<'tcx> LateLintPass<'tcx> for NonminimalBool { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + _: FnKind<'tcx>, + _: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + _: Span, + _: HirId, + ) { + NonminimalBoolVisitor { cx }.visit_body(body); + } +} + +struct NonminimalBoolVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, +} + +use quine_mc_cluskey::Bool; +struct Hir2Qmm<'a, 'tcx, 'v> { + terminals: Vec<&'v Expr<'v>>, + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx, 'v> Hir2Qmm<'a, 'tcx, 'v> { + fn extract(&mut self, op: BinOpKind, a: &[&'v Expr<'_>], mut v: Vec<Bool>) -> Result<Vec<Bool>, String> { + for a in a { + if let ExprKind::Binary(binop, lhs, rhs) = &a.kind { + if binop.node == op { + v = self.extract(op, &[lhs, rhs], v)?; + continue; + } + } + v.push(self.run(a)?); + } + Ok(v) + } + + fn run(&mut self, e: &'v Expr<'_>) -> Result<Bool, String> { + fn negate(bin_op_kind: BinOpKind) -> Option<BinOpKind> { + match bin_op_kind { + BinOpKind::Eq => Some(BinOpKind::Ne), + BinOpKind::Ne => Some(BinOpKind::Eq), + BinOpKind::Gt => Some(BinOpKind::Le), + BinOpKind::Ge => Some(BinOpKind::Lt), + BinOpKind::Lt => Some(BinOpKind::Ge), + BinOpKind::Le => Some(BinOpKind::Gt), + _ => None, + } + } + + // prevent folding of `cfg!` macros and the like + if !e.span.from_expansion() { + match &e.kind { + ExprKind::Unary(UnOp::Not, inner) => return Ok(Bool::Not(Box::new(self.run(inner)?))), + ExprKind::Binary(binop, lhs, rhs) => match &binop.node { + BinOpKind::Or => { + return Ok(Bool::Or(self.extract(BinOpKind::Or, &[lhs, rhs], Vec::new())?)); + }, + BinOpKind::And => { + return Ok(Bool::And(self.extract(BinOpKind::And, &[lhs, rhs], Vec::new())?)); + }, + _ => (), + }, + ExprKind::Lit(lit) => match lit.node { + LitKind::Bool(true) => return Ok(Bool::True), + LitKind::Bool(false) => return Ok(Bool::False), + _ => (), + }, + _ => (), + } + } + for (n, expr) in self.terminals.iter().enumerate() { + if eq_expr_value(self.cx, e, expr) { + #[expect(clippy::cast_possible_truncation)] + return Ok(Bool::Term(n as u8)); + } + + if_chain! { + if let ExprKind::Binary(e_binop, e_lhs, e_rhs) = &e.kind; + if implements_ord(self.cx, e_lhs); + if let ExprKind::Binary(expr_binop, expr_lhs, expr_rhs) = &expr.kind; + if negate(e_binop.node) == Some(expr_binop.node); + if eq_expr_value(self.cx, e_lhs, expr_lhs); + if eq_expr_value(self.cx, e_rhs, expr_rhs); + then { + #[expect(clippy::cast_possible_truncation)] + return Ok(Bool::Not(Box::new(Bool::Term(n as u8)))); + } + } + } + let n = self.terminals.len(); + self.terminals.push(e); + if n < 32 { + #[expect(clippy::cast_possible_truncation)] + Ok(Bool::Term(n as u8)) + } else { + Err("too many literals".to_owned()) + } + } +} + +struct SuggestContext<'a, 'tcx, 'v> { + terminals: &'v [&'v Expr<'v>], + cx: &'a LateContext<'tcx>, + output: String, +} + +impl<'a, 'tcx, 'v> SuggestContext<'a, 'tcx, 'v> { + fn recurse(&mut self, suggestion: &Bool) -> Option<()> { + use quine_mc_cluskey::Bool::{And, False, Not, Or, Term, True}; + match suggestion { + True => { + self.output.push_str("true"); + }, + False => { + self.output.push_str("false"); + }, + Not(inner) => match **inner { + And(_) | Or(_) => { + self.output.push('!'); + self.output.push('('); + self.recurse(inner); + self.output.push(')'); + }, + Term(n) => { + let terminal = self.terminals[n as usize]; + if let Some(str) = simplify_not(self.cx, terminal) { + self.output.push_str(&str); + } else { + self.output.push('!'); + let snip = snippet_opt(self.cx, terminal.span)?; + self.output.push_str(&snip); + } + }, + True | False | Not(_) => { + self.output.push('!'); + self.recurse(inner)?; + }, + }, + And(v) => { + for (index, inner) in v.iter().enumerate() { + if index > 0 { + self.output.push_str(" && "); + } + if let Or(_) = *inner { + self.output.push('('); + self.recurse(inner); + self.output.push(')'); + } else { + self.recurse(inner); + } + } + }, + Or(v) => { + for (index, inner) in v.iter().rev().enumerate() { + if index > 0 { + self.output.push_str(" || "); + } + self.recurse(inner); + } + }, + &Term(n) => { + let snip = snippet_opt(self.cx, self.terminals[n as usize].span)?; + self.output.push_str(&snip); + }, + } + Some(()) + } +} + +fn simplify_not(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<String> { + match &expr.kind { + ExprKind::Binary(binop, lhs, rhs) => { + if !implements_ord(cx, lhs) { + return None; + } + + match binop.node { + BinOpKind::Eq => Some(" != "), + BinOpKind::Ne => Some(" == "), + BinOpKind::Lt => Some(" >= "), + BinOpKind::Gt => Some(" <= "), + BinOpKind::Le => Some(" > "), + BinOpKind::Ge => Some(" < "), + _ => None, + } + .and_then(|op| { + Some(format!( + "{}{}{}", + snippet_opt(cx, lhs.span)?, + op, + snippet_opt(cx, rhs.span)? + )) + }) + }, + ExprKind::MethodCall(path, args, _) if args.len() == 1 => { + let type_of_receiver = cx.typeck_results().expr_ty(&args[0]); + if !is_type_diagnostic_item(cx, type_of_receiver, sym::Option) + && !is_type_diagnostic_item(cx, type_of_receiver, sym::Result) + { + return None; + } + METHODS_WITH_NEGATION + .iter() + .copied() + .flat_map(|(a, b)| vec![(a, b), (b, a)]) + .find(|&(a, _)| { + let path: &str = path.ident.name.as_str(); + a == path + }) + .and_then(|(_, neg_method)| Some(format!("{}.{}()", snippet_opt(cx, args[0].span)?, neg_method))) + }, + _ => None, + } +} + +fn suggest(cx: &LateContext<'_>, suggestion: &Bool, terminals: &[&Expr<'_>]) -> String { + let mut suggest_context = SuggestContext { + terminals, + cx, + output: String::new(), + }; + suggest_context.recurse(suggestion); + suggest_context.output +} + +fn simple_negate(b: Bool) -> Bool { + use quine_mc_cluskey::Bool::{And, False, Not, Or, Term, True}; + match b { + True => False, + False => True, + t @ Term(_) => Not(Box::new(t)), + And(mut v) => { + for el in &mut v { + *el = simple_negate(::std::mem::replace(el, True)); + } + Or(v) + }, + Or(mut v) => { + for el in &mut v { + *el = simple_negate(::std::mem::replace(el, True)); + } + And(v) + }, + Not(inner) => *inner, + } +} + +#[derive(Default)] +struct Stats { + terminals: [usize; 32], + negations: usize, + ops: usize, +} + +fn terminal_stats(b: &Bool) -> Stats { + fn recurse(b: &Bool, stats: &mut Stats) { + match b { + True | False => stats.ops += 1, + Not(inner) => { + match **inner { + And(_) | Or(_) => stats.ops += 1, // brackets are also operations + _ => stats.negations += 1, + } + recurse(inner, stats); + }, + And(v) | Or(v) => { + stats.ops += v.len() - 1; + for inner in v { + recurse(inner, stats); + } + }, + &Term(n) => stats.terminals[n as usize] += 1, + } + } + use quine_mc_cluskey::Bool::{And, False, Not, Or, Term, True}; + let mut stats = Stats::default(); + recurse(b, &mut stats); + stats +} + +impl<'a, 'tcx> NonminimalBoolVisitor<'a, 'tcx> { + fn bool_expr(&self, e: &'tcx Expr<'_>) { + let mut h2q = Hir2Qmm { + terminals: Vec::new(), + cx: self.cx, + }; + if let Ok(expr) = h2q.run(e) { + if h2q.terminals.len() > 8 { + // QMC has exponentially slow behavior as the number of terminals increases + // 8 is reasonable, it takes approximately 0.2 seconds. + // See #825 + return; + } + + let stats = terminal_stats(&expr); + let mut simplified = expr.simplify(); + for simple in Bool::Not(Box::new(expr)).simplify() { + match simple { + Bool::Not(_) | Bool::True | Bool::False => {}, + _ => simplified.push(Bool::Not(Box::new(simple.clone()))), + } + let simple_negated = simple_negate(simple); + if simplified.iter().any(|s| *s == simple_negated) { + continue; + } + simplified.push(simple_negated); + } + let mut improvements = Vec::with_capacity(simplified.len()); + 'simplified: for suggestion in &simplified { + let simplified_stats = terminal_stats(suggestion); + let mut improvement = false; + for i in 0..32 { + // ignore any "simplifications" that end up requiring a terminal more often + // than in the original expression + if stats.terminals[i] < simplified_stats.terminals[i] { + continue 'simplified; + } + if stats.terminals[i] != 0 && simplified_stats.terminals[i] == 0 { + span_lint_hir_and_then( + self.cx, + LOGIC_BUG, + e.hir_id, + e.span, + "this boolean expression contains a logic bug", + |diag| { + diag.span_help( + h2q.terminals[i].span, + "this expression can be optimized out by applying boolean operations to the \ + outer expression", + ); + diag.span_suggestion( + e.span, + "it would look like the following", + suggest(self.cx, suggestion, &h2q.terminals), + // nonminimal_bool can produce minimal but + // not human readable expressions (#3141) + Applicability::Unspecified, + ); + }, + ); + // don't also lint `NONMINIMAL_BOOL` + return; + } + // if the number of occurrences of a terminal decreases or any of the stats + // decreases while none increases + improvement |= (stats.terminals[i] > simplified_stats.terminals[i]) + || (stats.negations > simplified_stats.negations && stats.ops == simplified_stats.ops) + || (stats.ops > simplified_stats.ops && stats.negations == simplified_stats.negations); + } + if improvement { + improvements.push(suggestion); + } + } + let nonminimal_bool_lint = |suggestions: Vec<_>| { + span_lint_hir_and_then( + self.cx, + NONMINIMAL_BOOL, + e.hir_id, + e.span, + "this boolean expression can be simplified", + |diag| { + diag.span_suggestions( + e.span, + "try", + suggestions.into_iter(), + // nonminimal_bool can produce minimal but + // not human readable expressions (#3141) + Applicability::Unspecified, + ); + }, + ); + }; + if improvements.is_empty() { + let mut visitor = NotSimplificationVisitor { cx: self.cx }; + visitor.visit_expr(e); + } else { + nonminimal_bool_lint( + improvements + .into_iter() + .map(|suggestion| suggest(self.cx, suggestion, &h2q.terminals)) + .collect(), + ); + } + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for NonminimalBoolVisitor<'a, 'tcx> { + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + if !e.span.from_expansion() { + match &e.kind { + ExprKind::Binary(binop, _, _) if binop.node == BinOpKind::Or || binop.node == BinOpKind::And => { + self.bool_expr(e); + }, + ExprKind::Unary(UnOp::Not, inner) => { + if self.cx.typeck_results().node_types()[inner.hir_id].is_bool() { + self.bool_expr(e); + } + }, + _ => {}, + } + } + walk_expr(self, e); + } +} + +fn implements_ord<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> bool { + let ty = cx.typeck_results().expr_ty(expr); + get_trait_def_id(cx, &paths::ORD).map_or(false, |id| implements_trait(cx, ty, id, &[])) +} + +struct NotSimplificationVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> Visitor<'tcx> for NotSimplificationVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if let ExprKind::Unary(UnOp::Not, inner) = &expr.kind { + if let Some(suggestion) = simplify_not(self.cx, inner) { + span_lint_and_sugg( + self.cx, + NONMINIMAL_BOOL, + expr.span, + "this boolean expression can be simplified", + "try", + suggestion, + Applicability::MachineApplicable, + ); + } + } + + walk_expr(self, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/borrow_as_ptr.rs b/src/tools/clippy/clippy_lints/src/borrow_as_ptr.rs new file mode 100644 index 000000000..0993adbae --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/borrow_as_ptr.rs @@ -0,0 +1,99 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_no_std_crate; +use clippy_utils::source::snippet_opt; +use clippy_utils::{meets_msrv, msrvs}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for the usage of `&expr as *const T` or + /// `&mut expr as *mut T`, and suggest using `ptr::addr_of` or + /// `ptr::addr_of_mut` instead. + /// + /// ### Why is this bad? + /// This would improve readability and avoid creating a reference + /// that points to an uninitialized value or unaligned place. + /// Read the `ptr::addr_of` docs for more information. + /// + /// ### Example + /// ```rust + /// let val = 1; + /// let p = &val as *const i32; + /// + /// let mut val_mut = 1; + /// let p_mut = &mut val_mut as *mut i32; + /// ``` + /// Use instead: + /// ```rust + /// let val = 1; + /// let p = std::ptr::addr_of!(val); + /// + /// let mut val_mut = 1; + /// let p_mut = std::ptr::addr_of_mut!(val_mut); + /// ``` + #[clippy::version = "1.60.0"] + pub BORROW_AS_PTR, + pedantic, + "borrowing just to cast to a raw pointer" +} + +impl_lint_pass!(BorrowAsPtr => [BORROW_AS_PTR]); + +pub struct BorrowAsPtr { + msrv: Option<RustcVersion>, +} + +impl BorrowAsPtr { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl<'tcx> LateLintPass<'tcx> for BorrowAsPtr { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if !meets_msrv(self.msrv, msrvs::BORROW_AS_PTR) { + return; + } + + if expr.span.from_expansion() { + return; + } + + if_chain! { + if let ExprKind::Cast(left_expr, ty) = &expr.kind; + if let TyKind::Ptr(_) = ty.kind; + if let ExprKind::AddrOf(BorrowKind::Ref, mutability, e) = &left_expr.kind; + + then { + let core_or_std = if is_no_std_crate(cx) { "core" } else { "std" }; + let macro_name = match mutability { + Mutability::Not => "addr_of", + Mutability::Mut => "addr_of_mut", + }; + + span_lint_and_sugg( + cx, + BORROW_AS_PTR, + expr.span, + "borrow as raw pointer", + "try", + format!( + "{}::ptr::{}!({})", + core_or_std, + macro_name, + snippet_opt(cx, e.span).unwrap() + ), + Applicability::MachineApplicable, + ); + } + } + } + + extract_msrv_attr!(LateContext); +} diff --git a/src/tools/clippy/clippy_lints/src/borrow_deref_ref.rs b/src/tools/clippy/clippy_lints/src/borrow_deref_ref.rs new file mode 100644 index 000000000..937765b66 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/borrow_deref_ref.rs @@ -0,0 +1,121 @@ +use crate::reference::DEREF_ADDROF; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::implements_trait; +use clippy_utils::{get_parent_expr, is_lint_allowed}; +use rustc_errors::Applicability; +use rustc_hir::{ExprKind, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::mir::Mutability; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `&*(&T)`. + /// + /// ### Why is this bad? + /// Dereferencing and then borrowing a reference value has no effect in most cases. + /// + /// ### Known problems + /// False negative on such code: + /// ``` + /// let x = &12; + /// let addr_x = &x as *const _ as usize; + /// let addr_y = &&*x as *const _ as usize; // assert ok now, and lint triggered. + /// // But if we fix it, assert will fail. + /// assert_ne!(addr_x, addr_y); + /// ``` + /// + /// ### Example + /// ```rust + /// fn foo(_x: &str) {} + /// + /// let s = &String::new(); + /// + /// let a: &String = &* s; + /// foo(&*s); + /// ``` + /// + /// Use instead: + /// ```rust + /// # fn foo(_x: &str) {} + /// # let s = &String::new(); + /// let a: &String = s; + /// foo(&**s); + /// ``` + #[clippy::version = "1.59.0"] + pub BORROW_DEREF_REF, + complexity, + "deref on an immutable reference returns the same type as itself" +} + +declare_lint_pass!(BorrowDerefRef => [BORROW_DEREF_REF]); + +impl LateLintPass<'_> for BorrowDerefRef { + fn check_expr(&mut self, cx: &LateContext<'_>, e: &rustc_hir::Expr<'_>) { + if_chain! { + if !e.span.from_expansion(); + if let ExprKind::AddrOf(_, Mutability::Not, addrof_target) = e.kind; + if !addrof_target.span.from_expansion(); + if let ExprKind::Unary(UnOp::Deref, deref_target) = addrof_target.kind; + if !deref_target.span.from_expansion(); + if !matches!(deref_target.kind, ExprKind::Unary(UnOp::Deref, ..) ); + let ref_ty = cx.typeck_results().expr_ty(deref_target); + if let ty::Ref(_, inner_ty, Mutability::Not) = ref_ty.kind(); + then{ + + if let Some(parent_expr) = get_parent_expr(cx, e){ + if matches!(parent_expr.kind, ExprKind::Unary(UnOp::Deref, ..)) && + !is_lint_allowed(cx, DEREF_ADDROF, parent_expr.hir_id) { + return; + } + + // modification to `&mut &*x` is different from `&mut x` + if matches!(deref_target.kind, ExprKind::Path(..) + | ExprKind::Field(..) + | ExprKind::Index(..) + | ExprKind::Unary(UnOp::Deref, ..)) + && matches!(parent_expr.kind, ExprKind::AddrOf(_, Mutability::Mut, _)) { + return; + } + } + + span_lint_and_then( + cx, + BORROW_DEREF_REF, + e.span, + "deref on an immutable reference", + |diag| { + diag.span_suggestion( + e.span, + "if you would like to reborrow, try removing `&*`", + snippet_opt(cx, deref_target.span).unwrap(), + Applicability::MachineApplicable + ); + + // has deref trait -> give 2 help + // doesn't have deref trait -> give 1 help + if let Some(deref_trait_id) = cx.tcx.lang_items().deref_trait(){ + if !implements_trait(cx, *inner_ty, deref_trait_id, &[]) { + return; + } + } + + diag.span_suggestion( + e.span, + "if you would like to deref, try using `&**`", + format!( + "&**{}", + &snippet_opt(cx, deref_target.span).unwrap(), + ), + Applicability::MaybeIncorrect + ); + + } + ); + + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/bytecount.rs b/src/tools/clippy/clippy_lints/src/bytecount.rs new file mode 100644 index 000000000..326ce3408 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/bytecount.rs @@ -0,0 +1,103 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::match_type; +use clippy_utils::visitors::is_local_used; +use clippy_utils::{path_to_local_id, paths, peel_blocks, peel_ref_operators, strip_pat_refs}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Closure, Expr, ExprKind, PatKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, UintTy}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for naive byte counts + /// + /// ### Why is this bad? + /// The [`bytecount`](https://crates.io/crates/bytecount) + /// crate has methods to count your bytes faster, especially for large slices. + /// + /// ### Known problems + /// If you have predominantly small slices, the + /// `bytecount::count(..)` method may actually be slower. However, if you can + /// ensure that less than 2³²-1 matches arise, the `naive_count_32(..)` can be + /// faster in those cases. + /// + /// ### Example + /// ```rust + /// # let vec = vec![1_u8]; + /// let count = vec.iter().filter(|x| **x == 0u8).count(); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// # let vec = vec![1_u8]; + /// let count = bytecount::count(&vec, 0u8); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NAIVE_BYTECOUNT, + pedantic, + "use of naive `<slice>.filter(|&x| x == y).count()` to count byte values" +} + +declare_lint_pass!(ByteCount => [NAIVE_BYTECOUNT]); + +impl<'tcx> LateLintPass<'tcx> for ByteCount { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::MethodCall(count, [count_recv], _) = expr.kind; + if count.ident.name == sym::count; + if let ExprKind::MethodCall(filter, [filter_recv, filter_arg], _) = count_recv.kind; + if filter.ident.name == sym!(filter); + if let ExprKind::Closure(&Closure { body, .. }) = filter_arg.kind; + let body = cx.tcx.hir().body(body); + if let [param] = body.params; + if let PatKind::Binding(_, arg_id, _, _) = strip_pat_refs(param.pat).kind; + if let ExprKind::Binary(ref op, l, r) = body.value.kind; + if op.node == BinOpKind::Eq; + if match_type(cx, + cx.typeck_results().expr_ty(filter_recv).peel_refs(), + &paths::SLICE_ITER); + let operand_is_arg = |expr| { + let expr = peel_ref_operators(cx, peel_blocks(expr)); + path_to_local_id(expr, arg_id) + }; + let needle = if operand_is_arg(l) { + r + } else if operand_is_arg(r) { + l + } else { + return; + }; + if ty::Uint(UintTy::U8) == *cx.typeck_results().expr_ty(needle).peel_refs().kind(); + if !is_local_used(cx, needle, arg_id); + then { + let haystack = if let ExprKind::MethodCall(path, args, _) = + filter_recv.kind { + let p = path.ident.name; + if (p == sym::iter || p == sym!(iter_mut)) && args.len() == 1 { + &args[0] + } else { + filter_recv + } + } else { + filter_recv + }; + let mut applicability = Applicability::MaybeIncorrect; + span_lint_and_sugg( + cx, + NAIVE_BYTECOUNT, + expr.span, + "you appear to be counting bytes the naive way", + "consider using the bytecount crate", + format!("bytecount::count({}, {})", + snippet_with_applicability(cx, haystack.span, "..", &mut applicability), + snippet_with_applicability(cx, needle.span, "..", &mut applicability)), + applicability, + ); + } + }; + } +} diff --git a/src/tools/clippy/clippy_lints/src/bytes_count_to_len.rs b/src/tools/clippy/clippy_lints/src/bytes_count_to_len.rs new file mode 100644 index 000000000..d70dbf5b2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/bytes_count_to_len.rs @@ -0,0 +1,70 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// It checks for `str::bytes().count()` and suggests replacing it with + /// `str::len()`. + /// + /// ### Why is this bad? + /// `str::bytes().count()` is longer and may not be as performant as using + /// `str::len()`. + /// + /// ### Example + /// ```rust + /// "hello".bytes().count(); + /// String::from("hello").bytes().count(); + /// ``` + /// Use instead: + /// ```rust + /// "hello".len(); + /// String::from("hello").len(); + /// ``` + #[clippy::version = "1.62.0"] + pub BYTES_COUNT_TO_LEN, + complexity, + "Using `bytes().count()` when `len()` performs the same functionality" +} + +declare_lint_pass!(BytesCountToLen => [BYTES_COUNT_TO_LEN]); + +impl<'tcx> LateLintPass<'tcx> for BytesCountToLen { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if_chain! { + if let hir::ExprKind::MethodCall(_, expr_args, _) = &expr.kind; + if let Some(expr_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if match_def_path(cx, expr_def_id, &paths::ITER_COUNT); + + if let [bytes_expr] = &**expr_args; + if let hir::ExprKind::MethodCall(_, bytes_args, _) = &bytes_expr.kind; + if let Some(bytes_def_id) = cx.typeck_results().type_dependent_def_id(bytes_expr.hir_id); + if match_def_path(cx, bytes_def_id, &paths::STR_BYTES); + + if let [str_expr] = &**bytes_args; + let ty = cx.typeck_results().expr_ty(str_expr).peel_refs(); + + if is_type_diagnostic_item(cx, ty, sym::String) || ty.kind() == &ty::Str; + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + BYTES_COUNT_TO_LEN, + expr.span, + "using long and hard to read `.bytes().count()`", + "consider calling `.len()` instead", + format!("{}.len()", snippet_with_applicability(cx, str_expr.span, "..", &mut applicability)), + applicability + ); + } + }; + } +} diff --git a/src/tools/clippy/clippy_lints/src/cargo/common_metadata.rs b/src/tools/clippy/clippy_lints/src/cargo/common_metadata.rs new file mode 100644 index 000000000..e0442dda4 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/cargo/common_metadata.rs @@ -0,0 +1,54 @@ +//! lint on missing cargo common metadata + +use cargo_metadata::Metadata; +use clippy_utils::diagnostics::span_lint; +use rustc_lint::LateContext; +use rustc_span::source_map::DUMMY_SP; + +use super::CARGO_COMMON_METADATA; + +pub(super) fn check(cx: &LateContext<'_>, metadata: &Metadata, ignore_publish: bool) { + for package in &metadata.packages { + // only run the lint if publish is `None` (`publish = true` or skipped entirely) + // or if the vector isn't empty (`publish = ["something"]`) + if package.publish.as_ref().filter(|publish| publish.is_empty()).is_none() || ignore_publish { + if is_empty_str(&package.description) { + missing_warning(cx, package, "package.description"); + } + + if is_empty_str(&package.license) && is_empty_str(&package.license_file) { + missing_warning(cx, package, "either package.license or package.license_file"); + } + + if is_empty_str(&package.repository) { + missing_warning(cx, package, "package.repository"); + } + + if is_empty_str(&package.readme) { + missing_warning(cx, package, "package.readme"); + } + + if is_empty_vec(&package.keywords) { + missing_warning(cx, package, "package.keywords"); + } + + if is_empty_vec(&package.categories) { + missing_warning(cx, package, "package.categories"); + } + } + } +} + +fn missing_warning(cx: &LateContext<'_>, package: &cargo_metadata::Package, field: &str) { + let message = format!("package `{}` is missing `{}` metadata", package.name, field); + span_lint(cx, CARGO_COMMON_METADATA, DUMMY_SP, &message); +} + +fn is_empty_str<T: AsRef<std::ffi::OsStr>>(value: &Option<T>) -> bool { + value.as_ref().map_or(true, |s| s.as_ref().is_empty()) +} + +fn is_empty_vec(value: &[String]) -> bool { + // This works because empty iterators return true + value.iter().all(String::is_empty) +} diff --git a/src/tools/clippy/clippy_lints/src/cargo/feature_name.rs b/src/tools/clippy/clippy_lints/src/cargo/feature_name.rs new file mode 100644 index 000000000..79a469a42 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/cargo/feature_name.rs @@ -0,0 +1,92 @@ +use cargo_metadata::Metadata; +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_lint::LateContext; +use rustc_span::source_map::DUMMY_SP; + +use super::{NEGATIVE_FEATURE_NAMES, REDUNDANT_FEATURE_NAMES}; + +static PREFIXES: [&str; 8] = ["no-", "no_", "not-", "not_", "use-", "use_", "with-", "with_"]; +static SUFFIXES: [&str; 2] = ["-support", "_support"]; + +pub(super) fn check(cx: &LateContext<'_>, metadata: &Metadata) { + for package in &metadata.packages { + let mut features: Vec<&String> = package.features.keys().collect(); + features.sort(); + for feature in features { + let prefix_opt = { + let i = PREFIXES.partition_point(|prefix| prefix < &feature.as_str()); + if i > 0 && feature.starts_with(PREFIXES[i - 1]) { + Some(PREFIXES[i - 1]) + } else { + None + } + }; + if let Some(prefix) = prefix_opt { + lint(cx, feature, prefix, true); + } + + let suffix_opt: Option<&str> = { + let i = SUFFIXES.partition_point(|suffix| { + suffix.bytes().rev().cmp(feature.bytes().rev()) == std::cmp::Ordering::Less + }); + if i > 0 && feature.ends_with(SUFFIXES[i - 1]) { + Some(SUFFIXES[i - 1]) + } else { + None + } + }; + if let Some(suffix) = suffix_opt { + lint(cx, feature, suffix, false); + } + } + } +} + +fn is_negative_prefix(s: &str) -> bool { + s.starts_with("no") +} + +fn lint(cx: &LateContext<'_>, feature: &str, substring: &str, is_prefix: bool) { + let is_negative = is_prefix && is_negative_prefix(substring); + span_lint_and_help( + cx, + if is_negative { + NEGATIVE_FEATURE_NAMES + } else { + REDUNDANT_FEATURE_NAMES + }, + DUMMY_SP, + &format!( + "the \"{}\" {} in the feature name \"{}\" is {}", + substring, + if is_prefix { "prefix" } else { "suffix" }, + feature, + if is_negative { "negative" } else { "redundant" } + ), + None, + &format!( + "consider renaming the feature to \"{}\"{}", + if is_prefix { + feature.strip_prefix(substring) + } else { + feature.strip_suffix(substring) + } + .unwrap(), + if is_negative { + ", but make sure the feature adds functionality" + } else { + "" + } + ), + ); +} + +#[test] +fn test_prefixes_sorted() { + let mut sorted_prefixes = PREFIXES; + sorted_prefixes.sort_unstable(); + assert_eq!(PREFIXES, sorted_prefixes); + let mut sorted_suffixes = SUFFIXES; + sorted_suffixes.sort_by(|a, b| a.bytes().rev().cmp(b.bytes().rev())); + assert_eq!(SUFFIXES, sorted_suffixes); +} diff --git a/src/tools/clippy/clippy_lints/src/cargo/mod.rs b/src/tools/clippy/clippy_lints/src/cargo/mod.rs new file mode 100644 index 000000000..9f45db86a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/cargo/mod.rs @@ -0,0 +1,221 @@ +mod common_metadata; +mod feature_name; +mod multiple_crate_versions; +mod wildcard_dependencies; + +use cargo_metadata::MetadataCommand; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::is_lint_allowed; +use rustc_hir::hir_id::CRATE_HIR_ID; +use rustc_lint::{LateContext, LateLintPass, Lint}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::DUMMY_SP; + +declare_clippy_lint! { + /// ### What it does + /// Checks to see if all common metadata is defined in + /// `Cargo.toml`. See: https://rust-lang-nursery.github.io/api-guidelines/documentation.html#cargotoml-includes-all-common-metadata-c-metadata + /// + /// ### Why is this bad? + /// It will be more difficult for users to discover the + /// purpose of the crate, and key information related to it. + /// + /// ### Example + /// ```toml + /// # This `Cargo.toml` is missing a description field: + /// [package] + /// name = "clippy" + /// version = "0.0.212" + /// repository = "https://github.com/rust-lang/rust-clippy" + /// readme = "README.md" + /// license = "MIT OR Apache-2.0" + /// keywords = ["clippy", "lint", "plugin"] + /// categories = ["development-tools", "development-tools::cargo-plugins"] + /// ``` + /// + /// Should include a description field like: + /// + /// ```toml + /// # This `Cargo.toml` includes all common metadata + /// [package] + /// name = "clippy" + /// version = "0.0.212" + /// description = "A bunch of helpful lints to avoid common pitfalls in Rust" + /// repository = "https://github.com/rust-lang/rust-clippy" + /// readme = "README.md" + /// license = "MIT OR Apache-2.0" + /// keywords = ["clippy", "lint", "plugin"] + /// categories = ["development-tools", "development-tools::cargo-plugins"] + /// ``` + #[clippy::version = "1.32.0"] + pub CARGO_COMMON_METADATA, + cargo, + "common metadata is defined in `Cargo.toml`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for feature names with prefix `use-`, `with-` or suffix `-support` + /// + /// ### Why is this bad? + /// These prefixes and suffixes have no significant meaning. + /// + /// ### Example + /// ```toml + /// # The `Cargo.toml` with feature name redundancy + /// [features] + /// default = ["use-abc", "with-def", "ghi-support"] + /// use-abc = [] // redundant + /// with-def = [] // redundant + /// ghi-support = [] // redundant + /// ``` + /// + /// Use instead: + /// ```toml + /// [features] + /// default = ["abc", "def", "ghi"] + /// abc = [] + /// def = [] + /// ghi = [] + /// ``` + /// + #[clippy::version = "1.57.0"] + pub REDUNDANT_FEATURE_NAMES, + cargo, + "usage of a redundant feature name" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for negative feature names with prefix `no-` or `not-` + /// + /// ### Why is this bad? + /// Features are supposed to be additive, and negatively-named features violate it. + /// + /// ### Example + /// ```toml + /// # The `Cargo.toml` with negative feature names + /// [features] + /// default = [] + /// no-abc = [] + /// not-def = [] + /// + /// ``` + /// Use instead: + /// ```toml + /// [features] + /// default = ["abc", "def"] + /// abc = [] + /// def = [] + /// + /// ``` + #[clippy::version = "1.57.0"] + pub NEGATIVE_FEATURE_NAMES, + cargo, + "usage of a negative feature name" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks to see if multiple versions of a crate are being + /// used. + /// + /// ### Why is this bad? + /// This bloats the size of targets, and can lead to + /// confusing error messages when structs or traits are used interchangeably + /// between different versions of a crate. + /// + /// ### Known problems + /// Because this can be caused purely by the dependencies + /// themselves, it's not always possible to fix this issue. + /// + /// ### Example + /// ```toml + /// # This will pull in both winapi v0.3.x and v0.2.x, triggering a warning. + /// [dependencies] + /// ctrlc = "=3.1.0" + /// ansi_term = "=0.11.0" + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MULTIPLE_CRATE_VERSIONS, + cargo, + "multiple versions of the same crate being used" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard dependencies in the `Cargo.toml`. + /// + /// ### Why is this bad? + /// [As the edition guide says](https://rust-lang-nursery.github.io/edition-guide/rust-2018/cargo-and-crates-io/crates-io-disallows-wildcard-dependencies.html), + /// it is highly unlikely that you work with any possible version of your dependency, + /// and wildcard dependencies would cause unnecessary breakage in the ecosystem. + /// + /// ### Example + /// ```toml + /// [dependencies] + /// regex = "*" + /// ``` + #[clippy::version = "1.32.0"] + pub WILDCARD_DEPENDENCIES, + cargo, + "wildcard dependencies being used" +} + +pub struct Cargo { + pub ignore_publish: bool, +} + +impl_lint_pass!(Cargo => [ + CARGO_COMMON_METADATA, + REDUNDANT_FEATURE_NAMES, + NEGATIVE_FEATURE_NAMES, + MULTIPLE_CRATE_VERSIONS, + WILDCARD_DEPENDENCIES +]); + +impl LateLintPass<'_> for Cargo { + fn check_crate(&mut self, cx: &LateContext<'_>) { + static NO_DEPS_LINTS: &[&Lint] = &[ + CARGO_COMMON_METADATA, + REDUNDANT_FEATURE_NAMES, + NEGATIVE_FEATURE_NAMES, + WILDCARD_DEPENDENCIES, + ]; + static WITH_DEPS_LINTS: &[&Lint] = &[MULTIPLE_CRATE_VERSIONS]; + + if !NO_DEPS_LINTS + .iter() + .all(|&lint| is_lint_allowed(cx, lint, CRATE_HIR_ID)) + { + match MetadataCommand::new().no_deps().exec() { + Ok(metadata) => { + common_metadata::check(cx, &metadata, self.ignore_publish); + feature_name::check(cx, &metadata); + wildcard_dependencies::check(cx, &metadata); + }, + Err(e) => { + for lint in NO_DEPS_LINTS { + span_lint(cx, lint, DUMMY_SP, &format!("could not read cargo metadata: {}", e)); + } + }, + } + } + + if !WITH_DEPS_LINTS + .iter() + .all(|&lint| is_lint_allowed(cx, lint, CRATE_HIR_ID)) + { + match MetadataCommand::new().exec() { + Ok(metadata) => { + multiple_crate_versions::check(cx, &metadata); + }, + Err(e) => { + for lint in WITH_DEPS_LINTS { + span_lint(cx, lint, DUMMY_SP, &format!("could not read cargo metadata: {}", e)); + } + }, + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/cargo/multiple_crate_versions.rs b/src/tools/clippy/clippy_lints/src/cargo/multiple_crate_versions.rs new file mode 100644 index 000000000..76fd0819a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/cargo/multiple_crate_versions.rs @@ -0,0 +1,63 @@ +//! lint on multiple versions of a crate being used + +use cargo_metadata::{DependencyKind, Metadata, Node, Package, PackageId}; +use clippy_utils::diagnostics::span_lint; +use if_chain::if_chain; +use itertools::Itertools; +use rustc_hir::def_id::LOCAL_CRATE; +use rustc_lint::LateContext; +use rustc_span::source_map::DUMMY_SP; + +use super::MULTIPLE_CRATE_VERSIONS; + +pub(super) fn check(cx: &LateContext<'_>, metadata: &Metadata) { + let local_name = cx.tcx.crate_name(LOCAL_CRATE); + let mut packages = metadata.packages.clone(); + packages.sort_by(|a, b| a.name.cmp(&b.name)); + + if_chain! { + if let Some(resolve) = &metadata.resolve; + if let Some(local_id) = packages + .iter() + .find_map(|p| if p.name == local_name.as_str() { Some(&p.id) } else { None }); + then { + for (name, group) in &packages.iter().group_by(|p| p.name.clone()) { + let group: Vec<&Package> = group.collect(); + + if group.len() <= 1 { + continue; + } + + if group.iter().all(|p| is_normal_dep(&resolve.nodes, local_id, &p.id)) { + let mut versions: Vec<_> = group.into_iter().map(|p| &p.version).collect(); + versions.sort(); + let versions = versions.iter().join(", "); + + span_lint( + cx, + MULTIPLE_CRATE_VERSIONS, + DUMMY_SP, + &format!("multiple versions for dependency `{}`: {}", name, versions), + ); + } + } + } + } +} + +fn is_normal_dep(nodes: &[Node], local_id: &PackageId, dep_id: &PackageId) -> bool { + fn depends_on(node: &Node, dep_id: &PackageId) -> bool { + node.deps.iter().any(|dep| { + dep.pkg == *dep_id + && dep + .dep_kinds + .iter() + .any(|info| matches!(info.kind, DependencyKind::Normal)) + }) + } + + nodes + .iter() + .filter(|node| depends_on(node, dep_id)) + .any(|node| node.id == *local_id || is_normal_dep(nodes, local_id, &node.id)) +} diff --git a/src/tools/clippy/clippy_lints/src/cargo/wildcard_dependencies.rs b/src/tools/clippy/clippy_lints/src/cargo/wildcard_dependencies.rs new file mode 100644 index 000000000..7fa6acbf5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/cargo/wildcard_dependencies.rs @@ -0,0 +1,27 @@ +use cargo_metadata::Metadata; +use clippy_utils::diagnostics::span_lint; +use if_chain::if_chain; +use rustc_lint::LateContext; +use rustc_span::source_map::DUMMY_SP; + +use super::WILDCARD_DEPENDENCIES; + +pub(super) fn check(cx: &LateContext<'_>, metadata: &Metadata) { + for dep in &metadata.packages[0].dependencies { + // VersionReq::any() does not work + if_chain! { + if let Ok(wildcard_ver) = semver::VersionReq::parse("*"); + if let Some(ref source) = dep.source; + if !source.starts_with("git"); + if dep.req == wildcard_ver; + then { + span_lint( + cx, + WILDCARD_DEPENDENCIES, + DUMMY_SP, + &format!("wildcard dependency for `{}`", dep.name), + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/case_sensitive_file_extension_comparisons.rs b/src/tools/clippy/clippy_lints/src/case_sensitive_file_extension_comparisons.rs new file mode 100644 index 000000000..7eff71d50 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/case_sensitive_file_extension_comparisons.rs @@ -0,0 +1,86 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_hir::{Expr, ExprKind, PathSegment}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{source_map::Spanned, symbol::sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `ends_with` with possible file extensions + /// and suggests to use a case-insensitive approach instead. + /// + /// ### Why is this bad? + /// `ends_with` is case-sensitive and may not detect files with a valid extension. + /// + /// ### Example + /// ```rust + /// fn is_rust_file(filename: &str) -> bool { + /// filename.ends_with(".rs") + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn is_rust_file(filename: &str) -> bool { + /// let filename = std::path::Path::new(filename); + /// filename.extension() + /// .map(|ext| ext.eq_ignore_ascii_case("rs")) + /// .unwrap_or(false) + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS, + pedantic, + "Checks for calls to ends_with with case-sensitive file extensions" +} + +declare_lint_pass!(CaseSensitiveFileExtensionComparisons => [CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS]); + +fn check_case_sensitive_file_extension_comparison(ctx: &LateContext<'_>, expr: &Expr<'_>) -> Option<Span> { + if_chain! { + if let ExprKind::MethodCall(PathSegment { ident, .. }, [obj, extension, ..], span) = expr.kind; + if ident.as_str() == "ends_with"; + if let ExprKind::Lit(Spanned { node: LitKind::Str(ext_literal, ..), ..}) = extension.kind; + if (2..=6).contains(&ext_literal.as_str().len()); + if ext_literal.as_str().starts_with('.'); + if ext_literal.as_str().chars().skip(1).all(|c| c.is_uppercase() || c.is_ascii_digit()) + || ext_literal.as_str().chars().skip(1).all(|c| c.is_lowercase() || c.is_ascii_digit()); + then { + let mut ty = ctx.typeck_results().expr_ty(obj); + ty = match ty.kind() { + ty::Ref(_, ty, ..) => *ty, + _ => ty + }; + + match ty.kind() { + ty::Str => { + return Some(span); + }, + ty::Adt(def, _) => { + if ctx.tcx.is_diagnostic_item(sym::String, def.did()) { + return Some(span); + } + }, + _ => { return None; } + } + } + } + None +} + +impl<'tcx> LateLintPass<'tcx> for CaseSensitiveFileExtensionComparisons { + fn check_expr(&mut self, ctx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if let Some(span) = check_case_sensitive_file_extension_comparison(ctx, expr) { + span_lint_and_help( + ctx, + CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS, + span, + "case-sensitive file extension comparison", + None, + "consider using a case-insensitive comparison instead", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_abs_to_unsigned.rs b/src/tools/clippy/clippy_lints/src/casts/cast_abs_to_unsigned.rs new file mode 100644 index 000000000..64ea326b7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_abs_to_unsigned.rs @@ -0,0 +1,44 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::sugg::Sugg; +use clippy_utils::{meets_msrv, msrvs}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_semver::RustcVersion; + +use super::CAST_ABS_TO_UNSIGNED; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &Expr<'_>, + cast_expr: &Expr<'_>, + cast_from: Ty<'_>, + cast_to: Ty<'_>, + msrv: Option<RustcVersion>, +) { + if meets_msrv(msrv, msrvs::UNSIGNED_ABS) + && let ty::Int(from) = cast_from.kind() + && let ty::Uint(to) = cast_to.kind() + && let ExprKind::MethodCall(method_path, args, _) = cast_expr.kind + && method_path.ident.name.as_str() == "abs" + { + let span = if from.bit_width() == to.bit_width() { + expr.span + } else { + // if the result of `.unsigned_abs` would be a different type, keep the cast + // e.g. `i64 -> usize`, `i16 -> u8` + cast_expr.span + }; + + span_lint_and_sugg( + cx, + CAST_ABS_TO_UNSIGNED, + span, + &format!("casting the result of `{cast_from}::abs()` to {cast_to}"), + "replace with", + format!("{}.unsigned_abs()", Sugg::hir(cx, &args[0], "..")), + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_enum_constructor.rs b/src/tools/clippy/clippy_lints/src/casts/cast_enum_constructor.rs new file mode 100644 index 000000000..1973692e1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_enum_constructor.rs @@ -0,0 +1,21 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res}; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +use super::CAST_ENUM_CONSTRUCTOR; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_expr: &Expr<'_>, cast_from: Ty<'_>) { + if matches!(cast_from.kind(), ty::FnDef(..)) + && let ExprKind::Path(path) = &cast_expr.kind + && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Fn), _) = cx.qpath_res(path, cast_expr.hir_id) + { + span_lint( + cx, + CAST_ENUM_CONSTRUCTOR, + expr.span, + "cast of an enum tuple constructor to an integer", + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_lossless.rs b/src/tools/clippy/clippy_lints/src/casts/cast_lossless.rs new file mode 100644 index 000000000..938458e30 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_lossless.rs @@ -0,0 +1,112 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::is_isize_or_usize; +use clippy_utils::{in_constant, meets_msrv, msrvs}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, FloatTy, Ty}; +use rustc_semver::RustcVersion; + +use super::{utils, CAST_LOSSLESS}; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &Expr<'_>, + cast_op: &Expr<'_>, + cast_from: Ty<'_>, + cast_to: Ty<'_>, + msrv: Option<RustcVersion>, +) { + if !should_lint(cx, expr, cast_from, cast_to, msrv) { + return; + } + + // The suggestion is to use a function call, so if the original expression + // has parens on the outside, they are no longer needed. + let mut applicability = Applicability::MachineApplicable; + let opt = snippet_opt(cx, cast_op.span); + let sugg = opt.as_ref().map_or_else( + || { + applicability = Applicability::HasPlaceholders; + ".." + }, + |snip| { + if should_strip_parens(cast_op, snip) { + &snip[1..snip.len() - 1] + } else { + snip.as_str() + } + }, + ); + + let message = if cast_from.is_bool() { + format!( + "casting `{0:}` to `{1:}` is more cleanly stated with `{1:}::from(_)`", + cast_from, cast_to + ) + } else { + format!( + "casting `{}` to `{}` may become silently lossy if you later change the type", + cast_from, cast_to + ) + }; + + span_lint_and_sugg( + cx, + CAST_LOSSLESS, + expr.span, + &message, + "try", + format!("{}::from({})", cast_to, sugg), + applicability, + ); +} + +fn should_lint( + cx: &LateContext<'_>, + expr: &Expr<'_>, + cast_from: Ty<'_>, + cast_to: Ty<'_>, + msrv: Option<RustcVersion>, +) -> bool { + // Do not suggest using From in consts/statics until it is valid to do so (see #2267). + if in_constant(cx, expr.hir_id) { + return false; + } + + match (cast_from.is_integral(), cast_to.is_integral()) { + (true, true) => { + let cast_signed_to_unsigned = cast_from.is_signed() && !cast_to.is_signed(); + let from_nbits = utils::int_ty_to_nbits(cast_from, cx.tcx); + let to_nbits = utils::int_ty_to_nbits(cast_to, cx.tcx); + !is_isize_or_usize(cast_from) + && !is_isize_or_usize(cast_to) + && from_nbits < to_nbits + && !cast_signed_to_unsigned + }, + + (true, false) => { + let from_nbits = utils::int_ty_to_nbits(cast_from, cx.tcx); + let to_nbits = if let ty::Float(FloatTy::F32) = cast_to.kind() { + 32 + } else { + 64 + }; + !is_isize_or_usize(cast_from) && from_nbits < to_nbits + }, + (false, true) if matches!(cast_from.kind(), ty::Bool) && meets_msrv(msrv, msrvs::FROM_BOOL) => true, + (_, _) => { + matches!(cast_from.kind(), ty::Float(FloatTy::F32)) && matches!(cast_to.kind(), ty::Float(FloatTy::F64)) + }, + } +} + +fn should_strip_parens(cast_expr: &Expr<'_>, snip: &str) -> bool { + if let ExprKind::Binary(_, _, _) = cast_expr.kind { + if snip.starts_with('(') && snip.ends_with(')') { + return true; + } + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_possible_truncation.rs b/src/tools/clippy/clippy_lints/src/casts/cast_possible_truncation.rs new file mode 100644 index 000000000..64f87c80f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_possible_truncation.rs @@ -0,0 +1,169 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::expr_or_init; +use clippy_utils::ty::{get_discriminant_value, is_isize_or_usize}; +use rustc_ast::ast; +use rustc_attr::IntType; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, FloatTy, Ty}; + +use super::{utils, CAST_ENUM_TRUNCATION, CAST_POSSIBLE_TRUNCATION}; + +fn constant_int(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> { + if let Some((Constant::Int(c), _)) = constant(cx, cx.typeck_results(), expr) { + Some(c) + } else { + None + } +} + +fn get_constant_bits(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u64> { + constant_int(cx, expr).map(|c| u64::from(128 - c.leading_zeros())) +} + +fn apply_reductions(cx: &LateContext<'_>, nbits: u64, expr: &Expr<'_>, signed: bool) -> u64 { + match expr_or_init(cx, expr).kind { + ExprKind::Cast(inner, _) => apply_reductions(cx, nbits, inner, signed), + ExprKind::Block(block, _) => block.expr.map_or(nbits, |e| apply_reductions(cx, nbits, e, signed)), + ExprKind::Binary(op, left, right) => match op.node { + BinOpKind::Div => { + apply_reductions(cx, nbits, left, signed).saturating_sub(if signed { + // let's be conservative here + 0 + } else { + // by dividing by 1, we remove 0 bits, etc. + get_constant_bits(cx, right).map_or(0, |b| b.saturating_sub(1)) + }) + }, + BinOpKind::Rem | BinOpKind::BitAnd => get_constant_bits(cx, right) + .unwrap_or(u64::max_value()) + .min(apply_reductions(cx, nbits, left, signed)), + BinOpKind::Shr => apply_reductions(cx, nbits, left, signed) + .saturating_sub(constant_int(cx, right).map_or(0, |s| u64::try_from(s).expect("shift too high"))), + _ => nbits, + }, + ExprKind::MethodCall(method, [left, right], _) => { + if signed { + return nbits; + } + let max_bits = if method.ident.as_str() == "min" { + get_constant_bits(cx, right) + } else { + None + }; + apply_reductions(cx, nbits, left, signed).min(max_bits.unwrap_or(u64::max_value())) + }, + ExprKind::MethodCall(method, [_, lo, hi], _) => { + if method.ident.as_str() == "clamp" { + //FIXME: make this a diagnostic item + if let (Some(lo_bits), Some(hi_bits)) = (get_constant_bits(cx, lo), get_constant_bits(cx, hi)) { + return lo_bits.max(hi_bits); + } + } + nbits + }, + ExprKind::MethodCall(method, [_value], _) => { + if method.ident.name.as_str() == "signum" { + 0 // do not lint if cast comes from a `signum` function + } else { + nbits + } + }, + _ => nbits, + } +} + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_expr: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) { + let msg = match (cast_from.kind(), cast_to.is_integral()) { + (ty::Int(_) | ty::Uint(_), true) => { + let from_nbits = apply_reductions( + cx, + utils::int_ty_to_nbits(cast_from, cx.tcx), + cast_expr, + cast_from.is_signed(), + ); + 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, ""), + (true, false) => ( + to_nbits <= 32, + if to_nbits == 32 { + " on targets with 64-bit wide pointers" + } else { + "" + }, + ), + (false, true) => (from_nbits == 64, " on targets with 32-bit wide pointers"), + }; + + if !should_lint { + return; + } + + format!( + "casting `{}` to `{}` may truncate the value{}", + cast_from, cast_to, suffix, + ) + }, + + (ty::Adt(def, _), true) if def.is_enum() => { + let (from_nbits, variant) = if let ExprKind::Path(p) = &cast_expr.kind + && let Res::Def(DefKind::Ctor(..), id) = cx.qpath_res(p, cast_expr.hir_id) + { + let i = def.variant_index_with_ctor_id(id); + let variant = def.variant(i); + let nbits = utils::enum_value_nbits(get_discriminant_value(cx.tcx, *def, i)); + (nbits, Some(variant)) + } else { + (utils::enum_ty_to_nbits(*def, cx.tcx), None) + }; + let to_nbits = utils::int_ty_to_nbits(cast_to, cx.tcx); + + let cast_from_ptr_size = def.repr().int.map_or(true, |ty| { + matches!( + ty, + IntType::SignedInt(ast::IntTy::Isize) | IntType::UnsignedInt(ast::UintTy::Usize) + ) + }); + let suffix = match (cast_from_ptr_size, is_isize_or_usize(cast_to)) { + (false, false) if from_nbits > to_nbits => "", + (true, false) if from_nbits > to_nbits => "", + (false, true) if from_nbits > 64 => "", + (false, true) if from_nbits > 32 => " on targets with 32-bit wide pointers", + _ => return, + }; + + if let Some(variant) = variant { + span_lint( + cx, + CAST_ENUM_TRUNCATION, + expr.span, + &format!( + "casting `{}::{}` to `{}` will truncate the value{}", + cast_from, variant.name, cast_to, suffix, + ), + ); + return; + } + format!( + "casting `{}` to `{}` may truncate the value{}", + cast_from, cast_to, suffix, + ) + }, + + (ty::Float(_), true) => { + format!("casting `{}` to `{}` may truncate the value", cast_from, cast_to) + }, + + (ty::Float(FloatTy::F64), false) if matches!(cast_to.kind(), &ty::Float(FloatTy::F32)) => { + "casting `f64` to `f32` may truncate the value".to_string() + }, + + _ => return, + }; + + span_lint(cx, CAST_POSSIBLE_TRUNCATION, expr.span, &msg); +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_possible_wrap.rs b/src/tools/clippy/clippy_lints/src/casts/cast_possible_wrap.rs new file mode 100644 index 000000000..2c5c1d7cb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_possible_wrap.rs @@ -0,0 +1,44 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::is_isize_or_usize; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::Ty; + +use super::{utils, CAST_POSSIBLE_WRAP}; + +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(); + 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 + } else { + arch_32_suffix + }, + ), + }; + + if should_lint { + span_lint( + cx, + CAST_POSSIBLE_WRAP, + expr.span, + &format!( + "casting `{}` to `{}` may wrap around the value{}", + cast_from, cast_to, suffix, + ), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_precision_loss.rs b/src/tools/clippy/clippy_lints/src/casts/cast_precision_loss.rs new file mode 100644 index 000000000..334e1646c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_precision_loss.rs @@ -0,0 +1,51 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::is_isize_or_usize; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, FloatTy, Ty}; + +use super::{utils, CAST_PRECISION_LOSS}; + +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 from_nbits = utils::int_ty_to_nbits(cast_from, cx.tcx); + let to_nbits = if cast_to.kind() == &ty::Float(FloatTy::F32) { + 32 + } else { + 64 + }; + + if !(is_isize_or_usize(cast_from) || from_nbits >= to_nbits) { + return; + } + + let cast_to_f64 = to_nbits == 64; + let mantissa_nbits = if cast_to_f64 { 52 } else { 23 }; + let arch_dependent = is_isize_or_usize(cast_from) && cast_to_f64; + let arch_dependent_str = "on targets with 64-bit wide pointers "; + let from_nbits_str = if arch_dependent { + "64".to_owned() + } else if is_isize_or_usize(cast_from) { + "32 or 64".to_owned() + } else { + utils::int_ty_to_nbits(cast_from, cx.tcx).to_string() + }; + + span_lint( + cx, + CAST_PRECISION_LOSS, + expr.span, + &format!( + "casting `{0}` to `{1}` causes a loss of precision {2}(`{0}` is {3} bits wide, \ + but `{1}`'s mantissa is only {4} bits wide)", + cast_from, + if cast_to_f64 { "f64" } else { "f32" }, + if arch_dependent { arch_dependent_str } else { "" }, + from_nbits_str, + mantissa_nbits + ), + ); +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_ptr_alignment.rs b/src/tools/clippy/clippy_lints/src/casts/cast_ptr_alignment.rs new file mode 100644 index 000000000..d476a1a76 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_ptr_alignment.rs @@ -0,0 +1,96 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::is_c_void; +use clippy_utils::{get_parent_expr, is_hir_ty_cfg_dependant, match_any_def_paths, paths}; +use rustc_hir::{Expr, ExprKind, GenericArg}; +use rustc_lint::LateContext; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::{self, Ty}; + +use super::CAST_PTR_ALIGNMENT; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>) { + if let ExprKind::Cast(cast_expr, cast_to) = expr.kind { + if is_hir_ty_cfg_dependant(cx, cast_to) { + return; + } + let (cast_from, cast_to) = ( + cx.typeck_results().expr_ty(cast_expr), + cx.typeck_results().expr_ty(expr), + ); + lint_cast_ptr_alignment(cx, expr, cast_from, cast_to); + } else if let ExprKind::MethodCall(method_path, [self_arg, ..], _) = &expr.kind { + if method_path.ident.name == sym!(cast) + && let Some(generic_args) = method_path.args + && let [GenericArg::Type(cast_to)] = generic_args.args + // There probably is no obvious reason to do this, just to be consistent with `as` cases. + && !is_hir_ty_cfg_dependant(cx, cast_to) + { + let (cast_from, cast_to) = + (cx.typeck_results().expr_ty(self_arg), cx.typeck_results().expr_ty(expr)); + lint_cast_ptr_alignment(cx, expr, cast_from, cast_to); + } + } +} + +fn lint_cast_ptr_alignment<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>, cast_from: Ty<'tcx>, cast_to: Ty<'tcx>) { + if let ty::RawPtr(from_ptr_ty) = &cast_from.kind() + && let ty::RawPtr(to_ptr_ty) = &cast_to.kind() + && let Ok(from_layout) = cx.layout_of(from_ptr_ty.ty) + && let Ok(to_layout) = cx.layout_of(to_ptr_ty.ty) + && from_layout.align.abi < to_layout.align.abi + // with c_void, we inherently need to trust the user + && !is_c_void(cx, from_ptr_ty.ty) + // when casting from a ZST, we don't know enough to properly lint + && !from_layout.is_zst() + && !is_used_as_unaligned(cx, expr) + { + span_lint( + cx, + CAST_PTR_ALIGNMENT, + expr.span, + &format!( + "casting from `{}` to a more-strictly-aligned pointer (`{}`) ({} < {} bytes)", + cast_from, + cast_to, + from_layout.align.abi.bytes(), + to_layout.align.abi.bytes(), + ), + ); + } +} + +fn is_used_as_unaligned(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + let Some(parent) = get_parent_expr(cx, e) else { + return false; + }; + match parent.kind { + ExprKind::MethodCall(name, [self_arg, ..], _) if self_arg.hir_id == e.hir_id => { + if matches!(name.ident.as_str(), "read_unaligned" | "write_unaligned") + && let Some(def_id) = cx.typeck_results().type_dependent_def_id(parent.hir_id) + && let Some(def_id) = cx.tcx.impl_of_method(def_id) + && cx.tcx.type_of(def_id).is_unsafe_ptr() + { + true + } else { + false + } + }, + ExprKind::Call(func, [arg, ..]) if arg.hir_id == e.hir_id => { + static PATHS: &[&[&str]] = &[ + paths::PTR_READ_UNALIGNED.as_slice(), + paths::PTR_WRITE_UNALIGNED.as_slice(), + paths::PTR_UNALIGNED_VOLATILE_LOAD.as_slice(), + paths::PTR_UNALIGNED_VOLATILE_STORE.as_slice(), + ]; + if let ExprKind::Path(path) = &func.kind + && let Some(def_id) = cx.qpath_res(path, func.hir_id).opt_def_id() + && match_any_def_paths(cx, def_id, PATHS).is_some() + { + true + } else { + false + } + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_ref_to_mut.rs b/src/tools/clippy/clippy_lints/src/casts/cast_ref_to_mut.rs new file mode 100644 index 000000000..15f2f81f4 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_ref_to_mut.rs @@ -0,0 +1,26 @@ +use clippy_utils::diagnostics::span_lint; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind, MutTy, Mutability, TyKind, UnOp}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::CAST_REF_TO_MUT; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::Unary(UnOp::Deref, e) = &expr.kind; + if let ExprKind::Cast(e, t) = &e.kind; + if let TyKind::Ptr(MutTy { mutbl: Mutability::Mut, .. }) = t.kind; + if let ExprKind::Cast(e, t) = &e.kind; + if let TyKind::Ptr(MutTy { mutbl: Mutability::Not, .. }) = t.kind; + if let ty::Ref(..) = cx.typeck_results().node_type(e.hir_id).kind(); + then { + span_lint( + cx, + CAST_REF_TO_MUT, + expr.span, + "casting `&T` to `&mut T` may cause undefined behavior, consider instead using an `UnsafeCell`", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_sign_loss.rs b/src/tools/clippy/clippy_lints/src/casts/cast_sign_loss.rs new file mode 100644 index 000000000..75f70b77e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_sign_loss.rs @@ -0,0 +1,69 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{method_chain_args, sext}; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +use super::CAST_SIGN_LOSS; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_op: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) { + if should_lint(cx, cast_op, cast_from, cast_to) { + span_lint( + cx, + CAST_SIGN_LOSS, + expr.span, + &format!( + "casting `{}` to `{}` may lose the sign of the value", + cast_from, cast_to + ), + ); + } +} + +fn should_lint(cx: &LateContext<'_>, cast_op: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) -> bool { + match (cast_from.is_integral(), cast_to.is_integral()) { + (true, true) => { + if !cast_from.is_signed() || cast_to.is_signed() { + return false; + } + + // Don't lint for positive constants. + let const_val = constant(cx, cx.typeck_results(), cast_op); + if_chain! { + if let Some((Constant::Int(n), _)) = const_val; + if let ty::Int(ity) = *cast_from.kind(); + if sext(cx.tcx, n, ity) >= 0; + then { + return false; + } + } + + // Don't lint for the result of methods that always return non-negative values. + if let ExprKind::MethodCall(path, _, _) = cast_op.kind { + let mut method_name = path.ident.name.as_str(); + let allowed_methods = ["abs", "checked_abs", "rem_euclid", "checked_rem_euclid"]; + + if_chain! { + if method_name == "unwrap"; + if let Some(arglist) = method_chain_args(cast_op, &["unwrap"]); + if let ExprKind::MethodCall(inner_path, _, _) = &arglist[0][0].kind; + then { + method_name = inner_path.ident.name.as_str(); + } + } + + if allowed_methods.iter().any(|&name| method_name == name) { + return false; + } + } + + true + }, + + (false, true) => !cast_to.is_signed(), + + (_, _) => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/cast_slice_different_sizes.rs b/src/tools/clippy/clippy_lints/src/casts/cast_slice_different_sizes.rs new file mode 100644 index 000000000..027c660ce --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/cast_slice_different_sizes.rs @@ -0,0 +1,143 @@ +use clippy_utils::{diagnostics::span_lint_and_then, meets_msrv, msrvs, source}; +use if_chain::if_chain; +use rustc_ast::Mutability; +use rustc_hir::{Expr, ExprKind, Node}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, layout::LayoutOf, Ty, TypeAndMut}; +use rustc_semver::RustcVersion; + +use super::CAST_SLICE_DIFFERENT_SIZES; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>, msrv: Option<RustcVersion>) { + // suggestion is invalid if `ptr::slice_from_raw_parts` does not exist + if !meets_msrv(msrv, msrvs::PTR_SLICE_RAW_PARTS) { + return; + } + + // if this cast is the child of another cast expression then don't emit something for it, the full + // chain will be analyzed + if is_child_of_cast(cx, expr) { + return; + } + + if let Some(CastChainInfo { + left_cast, + start_ty, + end_ty, + }) = expr_cast_chain_tys(cx, expr) + { + if let (Ok(from_layout), Ok(to_layout)) = (cx.layout_of(start_ty.ty), cx.layout_of(end_ty.ty)) { + let from_size = from_layout.size.bytes(); + let to_size = to_layout.size.bytes(); + if from_size != to_size && from_size != 0 && to_size != 0 { + span_lint_and_then( + cx, + CAST_SLICE_DIFFERENT_SIZES, + expr.span, + &format!( + "casting between raw pointers to `[{}]` (element size {}) and `[{}]` (element size {}) does not adjust the count", + start_ty.ty, from_size, end_ty.ty, to_size, + ), + |diag| { + let ptr_snippet = source::snippet(cx, left_cast.span, ".."); + + let (mutbl_fn_str, mutbl_ptr_str) = match end_ty.mutbl { + Mutability::Mut => ("_mut", "mut"), + Mutability::Not => ("", "const"), + }; + let sugg = format!( + "core::ptr::slice_from_raw_parts{mutbl_fn_str}({ptr_snippet} as *{mutbl_ptr_str} {}, ..)", + // get just the ty from the TypeAndMut so that the printed type isn't something like `mut + // T`, extract just the `T` + end_ty.ty + ); + + diag.span_suggestion( + expr.span, + &format!("replace with `ptr::slice_from_raw_parts{mutbl_fn_str}`"), + sugg, + rustc_errors::Applicability::HasPlaceholders, + ); + }, + ); + } + } + } +} + +fn is_child_of_cast(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let map = cx.tcx.hir(); + if_chain! { + if let Some(parent_id) = map.find_parent_node(expr.hir_id); + if let Some(parent) = map.find(parent_id); + then { + let expr = match parent { + Node::Block(block) => { + if let Some(parent_expr) = block.expr { + parent_expr + } else { + return false; + } + }, + Node::Expr(expr) => expr, + _ => return false, + }; + + matches!(expr.kind, ExprKind::Cast(..)) + } else { + false + } + } +} + +/// Returns the type T of the pointed to *const [T] or *mut [T] and the mutability of the slice if +/// the type is one of those slices +fn get_raw_slice_ty_mut(ty: Ty<'_>) -> Option<TypeAndMut<'_>> { + match ty.kind() { + ty::RawPtr(TypeAndMut { ty: slice_ty, mutbl }) => match slice_ty.kind() { + ty::Slice(ty) => Some(TypeAndMut { ty: *ty, mutbl: *mutbl }), + _ => None, + }, + _ => None, + } +} + +struct CastChainInfo<'tcx> { + /// The left most part of the cast chain, or in other words, the first cast in the chain + /// Used for diagnostics + left_cast: &'tcx Expr<'tcx>, + /// The starting type of the cast chain + start_ty: TypeAndMut<'tcx>, + /// The final type of the cast chain + end_ty: TypeAndMut<'tcx>, +} + +/// Returns a `CastChainInfo` with the left-most cast in the chain and the original ptr T and final +/// ptr U if the expression is composed of casts. +/// Returns None if the expr is not a Cast +fn expr_cast_chain_tys<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<CastChainInfo<'tcx>> { + if let ExprKind::Cast(cast_expr, _cast_to_hir_ty) = expr.peel_blocks().kind { + let cast_to = cx.typeck_results().expr_ty(expr); + let to_slice_ty = get_raw_slice_ty_mut(cast_to)?; + + // If the expression that makes up the source of this cast is itself a cast, recursively + // call `expr_cast_chain_tys` and update the end type with the final target type. + // Otherwise, this cast is not immediately nested, just construct the info for this cast + if let Some(prev_info) = expr_cast_chain_tys(cx, cast_expr) { + Some(CastChainInfo { + end_ty: to_slice_ty, + ..prev_info + }) + } else { + let cast_from = cx.typeck_results().expr_ty(cast_expr); + let from_slice_ty = get_raw_slice_ty_mut(cast_from)?; + Some(CastChainInfo { + left_cast: cast_expr, + start_ty: from_slice_ty, + end_ty: to_slice_ty, + }) + } + } else { + None + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/char_lit_as_u8.rs b/src/tools/clippy/clippy_lints/src/casts/char_lit_as_u8.rs new file mode 100644 index 000000000..7cc406018 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/char_lit_as_u8.rs @@ -0,0 +1,41 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_with_applicability; +use if_chain::if_chain; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, UintTy}; + +use super::CHAR_LIT_AS_U8; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::Cast(e, _) = &expr.kind; + if let ExprKind::Lit(l) = &e.kind; + if let LitKind::Char(c) = l.node; + if ty::Uint(UintTy::U8) == *cx.typeck_results().expr_ty(expr).kind(); + then { + let mut applicability = Applicability::MachineApplicable; + let snippet = snippet_with_applicability(cx, e.span, "'x'", &mut applicability); + + span_lint_and_then( + cx, + CHAR_LIT_AS_U8, + expr.span, + "casting a character literal to `u8` truncates", + |diag| { + diag.note("`char` is four bytes wide, but `u8` is a single byte"); + + if c.is_ascii() { + diag.span_suggestion( + expr.span, + "use a byte literal instead", + format!("b{}", snippet), + applicability, + ); + } + }); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast.rs b/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast.rs new file mode 100644 index 000000000..35350d8a2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast.rs @@ -0,0 +1,37 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty, UintTy}; + +use super::{utils, FN_TO_NUMERIC_CAST}; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_expr: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) { + // We only want to check casts to `ty::Uint` or `ty::Int` + match cast_to.kind() { + ty::Uint(_) | ty::Int(..) => { /* continue on */ }, + _ => return, + } + + match cast_from.kind() { + ty::FnDef(..) | ty::FnPtr(_) => { + let mut applicability = Applicability::MaybeIncorrect; + let from_snippet = snippet_with_applicability(cx, cast_expr.span, "x", &mut applicability); + let to_nbits = utils::int_ty_to_nbits(cast_to, cx.tcx); + + if (to_nbits >= cx.tcx.data_layout.pointer_size.bits()) && (*cast_to.kind() != ty::Uint(UintTy::Usize)) { + span_lint_and_sugg( + cx, + FN_TO_NUMERIC_CAST, + expr.span, + &format!("casting function pointer `{}` to `{}`", from_snippet, cast_to), + "try", + format!("{} as usize", from_snippet), + applicability, + ); + } + }, + _ => {}, + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast_any.rs b/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast_any.rs new file mode 100644 index 000000000..03621887a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast_any.rs @@ -0,0 +1,34 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +use super::FN_TO_NUMERIC_CAST_ANY; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_expr: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) { + // We allow casts from any function type to any function type. + match cast_to.kind() { + ty::FnDef(..) | ty::FnPtr(..) => return, + _ => { /* continue to checks */ }, + } + + match cast_from.kind() { + ty::FnDef(..) | ty::FnPtr(_) => { + let mut applicability = Applicability::MaybeIncorrect; + let from_snippet = snippet_with_applicability(cx, cast_expr.span, "..", &mut applicability); + + span_lint_and_sugg( + cx, + FN_TO_NUMERIC_CAST_ANY, + expr.span, + &format!("casting function pointer `{}` to `{}`", from_snippet, cast_to), + "did you mean to invoke the function?", + format!("{}() as {}", from_snippet, cast_to), + applicability, + ); + }, + _ => {}, + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast_with_truncation.rs b/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast_with_truncation.rs new file mode 100644 index 000000000..6287f479b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/fn_to_numeric_cast_with_truncation.rs @@ -0,0 +1,39 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +use super::{utils, FN_TO_NUMERIC_CAST_WITH_TRUNCATION}; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_expr: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) { + // We only want to check casts to `ty::Uint` or `ty::Int` + match cast_to.kind() { + ty::Uint(_) | ty::Int(..) => { /* continue on */ }, + _ => return, + } + match cast_from.kind() { + ty::FnDef(..) | ty::FnPtr(_) => { + let mut applicability = Applicability::MaybeIncorrect; + let from_snippet = snippet_with_applicability(cx, cast_expr.span, "x", &mut applicability); + + let to_nbits = utils::int_ty_to_nbits(cast_to, cx.tcx); + if to_nbits < cx.tcx.data_layout.pointer_size.bits() { + span_lint_and_sugg( + cx, + FN_TO_NUMERIC_CAST_WITH_TRUNCATION, + expr.span, + &format!( + "casting function pointer `{}` to `{}`, which truncates the value", + from_snippet, cast_to + ), + "try", + format!("{} as usize", from_snippet), + applicability, + ); + } + }, + _ => {}, + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/mod.rs b/src/tools/clippy/clippy_lints/src/casts/mod.rs new file mode 100644 index 000000000..af3798a0c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/mod.rs @@ -0,0 +1,588 @@ +mod cast_abs_to_unsigned; +mod cast_enum_constructor; +mod cast_lossless; +mod cast_possible_truncation; +mod cast_possible_wrap; +mod cast_precision_loss; +mod cast_ptr_alignment; +mod cast_ref_to_mut; +mod cast_sign_loss; +mod cast_slice_different_sizes; +mod char_lit_as_u8; +mod fn_to_numeric_cast; +mod fn_to_numeric_cast_any; +mod fn_to_numeric_cast_with_truncation; +mod ptr_as_ptr; +mod unnecessary_cast; +mod utils; + +use clippy_utils::is_hir_ty_cfg_dependant; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts from any numerical to a float type where + /// the receiving type cannot store all values from the original type without + /// rounding errors. This possible rounding is to be expected, so this lint is + /// `Allow` by default. + /// + /// Basically, this warns on casting any integer with 32 or more bits to `f32` + /// or any 64-bit integer to `f64`. + /// + /// ### Why is this bad? + /// It's not bad at all. But in some applications it can be + /// helpful to know where precision loss can take place. This lint can help find + /// those places in the code. + /// + /// ### Example + /// ```rust + /// let x = u64::MAX; + /// x as f64; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CAST_PRECISION_LOSS, + pedantic, + "casts that cause loss of precision, e.g., `x as f32` where `x: u64`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts from a signed to an unsigned numerical + /// type. In this case, negative values wrap around to large positive values, + /// which can be quite surprising in practice. However, as the cast works as + /// defined, this lint is `Allow` by default. + /// + /// ### Why is this bad? + /// Possibly surprising results. You can activate this lint + /// as a one-time check to see where numerical wrapping can arise. + /// + /// ### Example + /// ```rust + /// let y: i8 = -1; + /// y as u128; // will return 18446744073709551615 + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CAST_SIGN_LOSS, + pedantic, + "casts from signed types to unsigned types, e.g., `x as u32` where `x: i32`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts between numerical types that may + /// truncate large values. This is expected behavior, so the cast is `Allow` by + /// default. + /// + /// ### Why is this bad? + /// In some problem domains, it is good practice to avoid + /// truncation. This lint can be activated to help assess where additional + /// checks could be beneficial. + /// + /// ### Example + /// ```rust + /// fn as_u8(x: u64) -> u8 { + /// x as u8 + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CAST_POSSIBLE_TRUNCATION, + pedantic, + "casts that may cause truncation of the value, e.g., `x as u8` where `x: u32`, or `x as i32` where `x: f32`" +} + +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 + /// for the target signed type. However, the cast works as defined, so this lint + /// is `Allow` by default. + /// + /// ### Why is this bad? + /// While such a cast is not bad in itself, the results can + /// be surprising when this is not the intended behavior, as demonstrated by the + /// example below. + /// + /// ### Example + /// ```rust + /// u32::MAX as i32; // will yield a value of `-1` + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CAST_POSSIBLE_WRAP, + pedantic, + "casts that may cause wrapping around the value, e.g., `x as i32` where `x: u32` and `x > i32::MAX`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts between numerical types that may + /// be replaced by safe conversion functions. + /// + /// ### Why is this bad? + /// Rust's `as` keyword will perform many kinds of + /// conversions, including silently lossy conversions. Conversion functions such + /// as `i32::from` will only perform lossless conversions. Using the conversion + /// functions prevents conversions from turning into silent lossy conversions if + /// the types of the input expressions ever change, and make it easier for + /// people reading the code to know that the conversion is lossless. + /// + /// ### Example + /// ```rust + /// fn as_u64(x: u8) -> u64 { + /// x as u64 + /// } + /// ``` + /// + /// Using `::from` would look like this: + /// + /// ```rust + /// fn as_u64(x: u8) -> u64 { + /// u64::from(x) + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CAST_LOSSLESS, + pedantic, + "casts using `as` that are known to be lossless, e.g., `x as u64` where `x: u8`" +} + +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. + /// + /// ### Why is this bad? + /// It's just unnecessary. + /// + /// ### Example + /// ```rust + /// let _ = 2i32 as i32; + /// let _ = 0.5 as f32; + /// ``` + /// + /// Better: + /// + /// ```rust + /// let _ = 2_i32; + /// let _ = 0.5_f32; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNNECESSARY_CAST, + complexity, + "cast to the same type, e.g., `x as i32` where `x: i32`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts, using `as` or `pointer::cast`, + /// from a less-strictly-aligned pointer to a more-strictly-aligned pointer + /// + /// ### Why is this bad? + /// Dereferencing the resulting pointer may be undefined + /// behavior. + /// + /// ### Known problems + /// Using `std::ptr::read_unaligned` and `std::ptr::write_unaligned` or similar + /// on the resulting pointer is fine. Is over-zealous: Casts with manual alignment checks or casts like + /// u64-> u8 -> u16 can be fine. Miri is able to do a more in-depth analysis. + /// + /// ### Example + /// ```rust + /// let _ = (&1u8 as *const u8) as *const u16; + /// let _ = (&mut 1u8 as *mut u8) as *mut u16; + /// + /// (&1u8 as *const u8).cast::<u16>(); + /// (&mut 1u8 as *mut u8).cast::<u16>(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CAST_PTR_ALIGNMENT, + pedantic, + "cast from a pointer to a more-strictly-aligned pointer" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts of function pointers to something other than usize + /// + /// ### Why is this bad? + /// Casting a function pointer to anything other than usize/isize is not portable across + /// architectures, because you end up losing bits if the target type is too small or end up with a + /// bunch of extra bits that waste space and add more instructions to the final binary than + /// strictly necessary for the problem + /// + /// Casting to isize also doesn't make sense since there are no signed addresses. + /// + /// ### Example + /// ```rust + /// fn fun() -> i32 { 1 } + /// let _ = fun as i64; + /// ``` + /// + /// Use instead: + /// ```rust + /// # fn fun() -> i32 { 1 } + /// let _ = fun as usize; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FN_TO_NUMERIC_CAST, + style, + "casting a function pointer to a numeric type other than usize" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts of a function pointer to a numeric type not wide enough to + /// store address. + /// + /// ### Why is this bad? + /// Such a cast discards some bits of the function's address. If this is intended, it would be more + /// clearly expressed by casting to usize first, then casting the usize to the intended type (with + /// a comment) to perform the truncation. + /// + /// ### Example + /// ```rust + /// fn fn1() -> i16 { + /// 1 + /// }; + /// let _ = fn1 as i32; + /// ``` + /// + /// Use instead: + /// ```rust + /// // Cast to usize first, then comment with the reason for the truncation + /// fn fn1() -> i16 { + /// 1 + /// }; + /// let fn_ptr = fn1 as usize; + /// let fn_ptr_truncated = fn_ptr as i32; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FN_TO_NUMERIC_CAST_WITH_TRUNCATION, + style, + "casting a function pointer to a numeric type not wide enough to store the address" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts of a function pointer to any integer type. + /// + /// ### Why is this bad? + /// Casting a function pointer to an integer can have surprising results and can occur + /// accidentally if parentheses are omitted from a function call. If you aren't doing anything + /// low-level with function pointers then you can opt-out of casting functions to integers in + /// order to avoid mistakes. Alternatively, you can use this lint to audit all uses of function + /// pointer casts in your code. + /// + /// ### Example + /// ```rust + /// // fn1 is cast as `usize` + /// fn fn1() -> u16 { + /// 1 + /// }; + /// let _ = fn1 as usize; + /// ``` + /// + /// Use instead: + /// ```rust + /// // maybe you intended to call the function? + /// fn fn2() -> u16 { + /// 1 + /// }; + /// let _ = fn2() as usize; + /// + /// // or + /// + /// // maybe you intended to cast it to a function type? + /// fn fn3() -> u16 { + /// 1 + /// } + /// let _ = fn3 as fn() -> u16; + /// ``` + #[clippy::version = "1.58.0"] + pub FN_TO_NUMERIC_CAST_ANY, + restriction, + "casting a function pointer to any integer type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts of `&T` to `&mut T` anywhere in the code. + /// + /// ### Why is this bad? + /// It’s basically guaranteed to be undefined behavior. + /// `UnsafeCell` is the only way to obtain aliasable data that is considered + /// mutable. + /// + /// ### Example + /// ```rust,ignore + /// fn x(r: &i32) { + /// unsafe { + /// *(r as *const _ as *mut _) += 1; + /// } + /// } + /// ``` + /// + /// Instead consider using interior mutability types. + /// + /// ```rust + /// use std::cell::UnsafeCell; + /// + /// fn x(r: &UnsafeCell<i32>) { + /// unsafe { + /// *r.get() += 1; + /// } + /// } + /// ``` + #[clippy::version = "1.33.0"] + pub CAST_REF_TO_MUT, + correctness, + "a cast of reference to a mutable pointer" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for expressions where a character literal is cast + /// to `u8` and suggests using a byte literal instead. + /// + /// ### Why is this bad? + /// In general, casting values to smaller types is + /// error-prone and should be avoided where possible. In the particular case of + /// converting a character literal to u8, it is easy to avoid by just using a + /// byte literal instead. As an added bonus, `b'a'` is even slightly shorter + /// than `'a' as u8`. + /// + /// ### Example + /// ```rust,ignore + /// 'x' as u8 + /// ``` + /// + /// A better version, using the byte literal: + /// + /// ```rust,ignore + /// b'x' + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CHAR_LIT_AS_U8, + complexity, + "casting a character literal to `u8` truncates" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `as` casts between raw pointers without changing its mutability, + /// namely `*const T` to `*const U` and `*mut T` to `*mut U`. + /// + /// ### Why is this bad? + /// Though `as` casts between raw pointers is not terrible, `pointer::cast` is safer because + /// it cannot accidentally change the pointer's mutability nor cast the pointer to other types like `usize`. + /// + /// ### Example + /// ```rust + /// let ptr: *const u32 = &42_u32; + /// let mut_ptr: *mut u32 = &mut 42_u32; + /// let _ = ptr as *const i32; + /// let _ = mut_ptr as *mut i32; + /// ``` + /// Use instead: + /// ```rust + /// let ptr: *const u32 = &42_u32; + /// let mut_ptr: *mut u32 = &mut 42_u32; + /// let _ = ptr.cast::<i32>(); + /// let _ = mut_ptr.cast::<i32>(); + /// ``` + #[clippy::version = "1.51.0"] + pub PTR_AS_PTR, + pedantic, + "casting using `as` from and to raw pointers that doesn't change its mutability, where `pointer::cast` could take the place of `as`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts from an enum type to an integral type which will definitely truncate the + /// value. + /// + /// ### Why is this bad? + /// The resulting integral value will not match the value of the variant it came from. + /// + /// ### Example + /// ```rust + /// enum E { X = 256 }; + /// let _ = E::X as u8; + /// ``` + #[clippy::version = "1.61.0"] + pub CAST_ENUM_TRUNCATION, + suspicious, + "casts from an enum type to an integral type which will truncate the value" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `as` casts between raw pointers to slices with differently sized elements. + /// + /// ### Why is this bad? + /// The produced raw pointer to a slice does not update its length metadata. The produced + /// pointer will point to a different number of bytes than the original pointer because the + /// length metadata of a raw slice pointer is in elements rather than bytes. + /// Producing a slice reference from the raw pointer will either create a slice with + /// less data (which can be surprising) or create a slice with more data and cause Undefined Behavior. + /// + /// ### Example + /// // Missing data + /// ```rust + /// let a = [1_i32, 2, 3, 4]; + /// let p = &a as *const [i32] as *const [u8]; + /// unsafe { + /// println!("{:?}", &*p); + /// } + /// ``` + /// // Undefined Behavior (note: also potential alignment issues) + /// ```rust + /// let a = [1_u8, 2, 3, 4]; + /// let p = &a as *const [u8] as *const [u32]; + /// unsafe { + /// println!("{:?}", &*p); + /// } + /// ``` + /// Instead use `ptr::slice_from_raw_parts` to construct a slice from a data pointer and the correct length + /// ```rust + /// let a = [1_i32, 2, 3, 4]; + /// let old_ptr = &a as *const [i32]; + /// // The data pointer is cast to a pointer to the target `u8` not `[u8]` + /// // The length comes from the known length of 4 i32s times the 4 bytes per i32 + /// let new_ptr = core::ptr::slice_from_raw_parts(old_ptr as *const u8, 16); + /// unsafe { + /// println!("{:?}", &*new_ptr); + /// } + /// ``` + #[clippy::version = "1.61.0"] + pub CAST_SLICE_DIFFERENT_SIZES, + correctness, + "casting using `as` between raw pointers to slices of types with different sizes" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for casts from an enum tuple constructor to an integer. + /// + /// ### Why is this bad? + /// The cast is easily confused with casting a c-like enum value to an integer. + /// + /// ### Example + /// ```rust + /// enum E { X(i32) }; + /// let _ = E::X as usize; + /// ``` + #[clippy::version = "1.61.0"] + pub CAST_ENUM_CONSTRUCTOR, + suspicious, + "casts from an enum tuple constructor to an integer" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for uses of the `abs()` method that cast the result to unsigned. + /// + /// ### Why is this bad? + /// The `unsigned_abs()` method avoids panic when called on the MIN value. + /// + /// ### Example + /// ```rust + /// let x: i32 = -42; + /// let y: u32 = x.abs() as u32; + /// ``` + /// Use instead: + /// ```rust + /// let x: i32 = -42; + /// let y: u32 = x.unsigned_abs(); + /// ``` + #[clippy::version = "1.62.0"] + pub CAST_ABS_TO_UNSIGNED, + suspicious, + "casting the result of `abs()` to an unsigned integer can panic" +} + +pub struct Casts { + msrv: Option<RustcVersion>, +} + +impl Casts { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(Casts => [ + CAST_PRECISION_LOSS, + CAST_SIGN_LOSS, + CAST_POSSIBLE_TRUNCATION, + CAST_POSSIBLE_WRAP, + CAST_LOSSLESS, + CAST_REF_TO_MUT, + CAST_PTR_ALIGNMENT, + CAST_SLICE_DIFFERENT_SIZES, + UNNECESSARY_CAST, + FN_TO_NUMERIC_CAST_ANY, + FN_TO_NUMERIC_CAST, + FN_TO_NUMERIC_CAST_WITH_TRUNCATION, + CHAR_LIT_AS_U8, + PTR_AS_PTR, + CAST_ENUM_TRUNCATION, + CAST_ENUM_CONSTRUCTOR, + CAST_ABS_TO_UNSIGNED +]); + +impl<'tcx> LateLintPass<'tcx> for Casts { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if !in_external_macro(cx.sess(), expr.span) { + ptr_as_ptr::check(cx, expr, self.msrv); + } + + if expr.span.from_expansion() { + return; + } + + if let ExprKind::Cast(cast_expr, cast_to) = expr.kind { + if is_hir_ty_cfg_dependant(cx, cast_to) { + return; + } + let (cast_from, cast_to) = ( + cx.typeck_results().expr_ty(cast_expr), + cx.typeck_results().expr_ty(expr), + ); + + if unnecessary_cast::check(cx, expr, cast_expr, cast_from, cast_to) { + return; + } + + fn_to_numeric_cast_any::check(cx, expr, cast_expr, cast_from, cast_to); + fn_to_numeric_cast::check(cx, expr, cast_expr, cast_from, cast_to); + fn_to_numeric_cast_with_truncation::check(cx, expr, cast_expr, cast_from, cast_to); + + if cast_to.is_numeric() && !in_external_macro(cx.sess(), expr.span) { + cast_possible_truncation::check(cx, expr, cast_expr, cast_from, cast_to); + if cast_from.is_numeric() { + cast_possible_wrap::check(cx, expr, cast_from, cast_to); + cast_precision_loss::check(cx, expr, cast_from, cast_to); + cast_sign_loss::check(cx, expr, cast_expr, cast_from, cast_to); + cast_abs_to_unsigned::check(cx, expr, cast_expr, cast_from, cast_to, self.msrv); + } + cast_lossless::check(cx, expr, cast_expr, cast_from, cast_to, self.msrv); + cast_enum_constructor::check(cx, expr, cast_expr, cast_from); + } + } + + cast_ref_to_mut::check(cx, expr); + cast_ptr_alignment::check(cx, expr); + char_lit_as_u8::check(cx, expr); + ptr_as_ptr::check(cx, expr, self.msrv); + cast_slice_different_sizes::check(cx, expr, self.msrv); + } + + extract_msrv_attr!(LateContext); +} diff --git a/src/tools/clippy/clippy_lints/src/casts/ptr_as_ptr.rs b/src/tools/clippy/clippy_lints/src/casts/ptr_as_ptr.rs new file mode 100644 index 000000000..46d45d096 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/ptr_as_ptr.rs @@ -0,0 +1,49 @@ +use std::borrow::Cow; + +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::sugg::Sugg; +use clippy_utils::{meets_msrv, msrvs}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, Mutability, TyKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, TypeAndMut}; +use rustc_semver::RustcVersion; + +use super::PTR_AS_PTR; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, msrv: Option<RustcVersion>) { + if !meets_msrv(msrv, msrvs::POINTER_CAST) { + return; + } + + if_chain! { + if let ExprKind::Cast(cast_expr, cast_to_hir_ty) = expr.kind; + let (cast_from, cast_to) = (cx.typeck_results().expr_ty(cast_expr), cx.typeck_results().expr_ty(expr)); + if let ty::RawPtr(TypeAndMut { mutbl: from_mutbl, .. }) = cast_from.kind(); + if let ty::RawPtr(TypeAndMut { ty: to_pointee_ty, mutbl: to_mutbl }) = cast_to.kind(); + if matches!((from_mutbl, to_mutbl), + (Mutability::Not, Mutability::Not) | (Mutability::Mut, Mutability::Mut)); + // The `U` in `pointer::cast` have to be `Sized` + // as explained here: https://github.com/rust-lang/rust/issues/60602. + if to_pointee_ty.is_sized(cx.tcx.at(expr.span), cx.param_env); + then { + let mut applicability = Applicability::MachineApplicable; + let cast_expr_sugg = Sugg::hir_with_applicability(cx, cast_expr, "_", &mut applicability); + let turbofish = match &cast_to_hir_ty.kind { + TyKind::Infer => Cow::Borrowed(""), + TyKind::Ptr(mut_ty) if matches!(mut_ty.ty.kind, TyKind::Infer) => Cow::Borrowed(""), + _ => Cow::Owned(format!("::<{}>", to_pointee_ty)), + }; + span_lint_and_sugg( + cx, + PTR_AS_PTR, + expr.span, + "`as` casting between raw pointers without changing its mutability", + "try `pointer::cast`, a safer alternative", + format!("{}.cast{}()", cast_expr_sugg.maybe_par(), turbofish), + applicability, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/unnecessary_cast.rs b/src/tools/clippy/clippy_lints/src/casts/unnecessary_cast.rs new file mode 100644 index 000000000..fff7da8e3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/unnecessary_cast.rs @@ -0,0 +1,126 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::numeric_literal::NumericLiteral; +use clippy_utils::source::snippet_opt; +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_lint::{LateContext, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::{self, FloatTy, InferTy, Ty}; + +use super::UNNECESSARY_CAST; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'tcx>, + cast_expr: &Expr<'tcx>, + cast_from: Ty<'tcx>, + cast_to: Ty<'tcx>, +) -> bool { + // skip non-primitive type cast + 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 + } + } + + if let Some(lit) = get_numeric_literal(cast_expr) { + let literal_str = snippet_opt(cx, cast_expr.span).unwrap_or_default(); + + if_chain! { + if let LitKind::Int(n, _) = lit.node; + if let Some(src) = snippet_opt(cx, cast_expr.span); + if cast_to.is_floating_point(); + if let Some(num_lit) = NumericLiteral::from_lit_kind(&src, &lit.node); + let from_nbits = 128 - n.leading_zeros(); + let to_nbits = fp_ty_mantissa_nbits(cast_to); + if from_nbits != 0 && to_nbits != 0 && from_nbits <= to_nbits && num_lit.is_decimal(); + then { + lint_unnecessary_cast(cx, expr, num_lit.integer, cast_from, cast_to); + return true + } + } + + match lit.node { + LitKind::Int(_, LitIntType::Unsuffixed) if cast_to.is_integral() => { + lint_unnecessary_cast(cx, expr, &literal_str, cast_from, cast_to); + }, + LitKind::Float(_, LitFloatType::Unsuffixed) if cast_to.is_floating_point() => { + lint_unnecessary_cast(cx, expr, &literal_str, cast_from, cast_to); + }, + LitKind::Int(_, LitIntType::Unsuffixed) | LitKind::Float(_, LitFloatType::Unsuffixed) => {}, + LitKind::Int(_, LitIntType::Signed(_) | LitIntType::Unsigned(_)) + | LitKind::Float(_, LitFloatType::Suffixed(_)) + if cast_from.kind() == cast_to.kind() => + { + if let Some(src) = snippet_opt(cx, cast_expr.span) { + if let Some(num_lit) = NumericLiteral::from_lit_kind(&src, &lit.node) { + lint_unnecessary_cast(cx, expr, num_lit.integer, cast_from, cast_to); + } + } + }, + _ => { + if cast_from.kind() == cast_to.kind() && !in_external_macro(cx.sess(), expr.span) { + span_lint_and_sugg( + cx, + UNNECESSARY_CAST, + expr.span, + &format!( + "casting to the same type is unnecessary (`{}` -> `{}`)", + cast_from, cast_to + ), + "try", + literal_str, + Applicability::MachineApplicable, + ); + return true; + } + }, + } + } + + false +} + +fn lint_unnecessary_cast(cx: &LateContext<'_>, expr: &Expr<'_>, literal_str: &str, cast_from: Ty<'_>, cast_to: Ty<'_>) { + let literal_kind_name = if cast_from.is_integral() { "integer" } else { "float" }; + span_lint_and_sugg( + cx, + UNNECESSARY_CAST, + expr.span, + &format!("casting {} literal to `{}` is unnecessary", literal_kind_name, cast_to), + "try", + format!("{}_{}", literal_str.trim_end_matches('.'), cast_to), + Applicability::MachineApplicable, + ); +} + +fn get_numeric_literal<'e>(expr: &'e Expr<'e>) -> Option<&'e Lit> { + match expr.kind { + ExprKind::Lit(ref lit) => Some(lit), + ExprKind::Unary(UnOp::Neg, e) => { + if let ExprKind::Lit(ref lit) = e.kind { + Some(lit) + } else { + None + } + }, + _ => None, + } +} + +/// Returns the mantissa bits wide of a fp type. +/// Will return 0 if the type is not a fp +fn fp_ty_mantissa_nbits(typ: Ty<'_>) -> u32 { + match typ.kind() { + ty::Float(FloatTy::F32) => 23, + ty::Float(FloatTy::F64) | ty::Infer(InferTy::FloatVar(_)) => 52, + _ => 0, + } +} diff --git a/src/tools/clippy/clippy_lints/src/casts/utils.rs b/src/tools/clippy/clippy_lints/src/casts/utils.rs new file mode 100644 index 000000000..5a4f20f09 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/casts/utils.rs @@ -0,0 +1,75 @@ +use clippy_utils::ty::{read_explicit_enum_value, EnumValue}; +use rustc_middle::ty::{self, AdtDef, IntTy, Ty, TyCtxt, UintTy, VariantDiscr}; + +/// Returns the size in bits of an integral type. +/// Will return 0 if the type is not an int or uint variant +pub(super) fn int_ty_to_nbits(typ: Ty<'_>, tcx: TyCtxt<'_>) -> u64 { + match typ.kind() { + ty::Int(i) => match i { + IntTy::Isize => tcx.data_layout.pointer_size.bits(), + IntTy::I8 => 8, + IntTy::I16 => 16, + IntTy::I32 => 32, + IntTy::I64 => 64, + IntTy::I128 => 128, + }, + ty::Uint(i) => match i { + UintTy::Usize => tcx.data_layout.pointer_size.bits(), + UintTy::U8 => 8, + UintTy::U16 => 16, + UintTy::U32 => 32, + UintTy::U64 => 64, + UintTy::U128 => 128, + }, + _ => 0, + } +} + +pub(super) fn enum_value_nbits(value: EnumValue) -> u64 { + match value { + EnumValue::Unsigned(x) => 128 - x.leading_zeros(), + EnumValue::Signed(x) if x < 0 => 128 - (-(x + 1)).leading_zeros() + 1, + EnumValue::Signed(x) => 128 - x.leading_zeros(), + } + .into() +} + +pub(super) fn enum_ty_to_nbits(adt: AdtDef<'_>, tcx: TyCtxt<'_>) -> u64 { + let mut explicit = 0i128; + let (start, end) = adt + .variants() + .iter() + .fold((0, i128::MIN), |(start, end), variant| match variant.discr { + VariantDiscr::Relative(x) => match explicit.checked_add(i128::from(x)) { + Some(x) => (start, end.max(x)), + None => (i128::MIN, end), + }, + VariantDiscr::Explicit(id) => match read_explicit_enum_value(tcx, id) { + Some(EnumValue::Signed(x)) => { + explicit = x; + (start.min(x), end.max(x)) + }, + Some(EnumValue::Unsigned(x)) => match i128::try_from(x) { + Ok(x) => { + explicit = x; + (start, end.max(x)) + }, + Err(_) => (i128::MIN, end), + }, + None => (start, end), + }, + }); + + if start > end { + // No variants. + 0 + } else { + let neg_bits = if start < 0 { + 128 - (-(start + 1)).leading_zeros() + 1 + } else { + 0 + }; + let pos_bits = if end > 0 { 128 - end.leading_zeros() } else { 0 }; + neg_bits.max(pos_bits).into() + } +} diff --git a/src/tools/clippy/clippy_lints/src/checked_conversions.rs b/src/tools/clippy/clippy_lints/src/checked_conversions.rs new file mode 100644 index 000000000..17fc81951 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/checked_conversions.rs @@ -0,0 +1,354 @@ +//! lint on manually implemented checked conversions that could be transformed into `try_from` + +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{in_constant, meets_msrv, msrvs, SpanlessEq}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BinOp, BinOpKind, Expr, ExprKind, QPath, TyKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for explicit bounds checking when casting. + /// + /// ### Why is this bad? + /// Reduces the readability of statements & is error prone. + /// + /// ### Example + /// ```rust + /// # let foo: u32 = 5; + /// foo <= i32::MAX as u32; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let foo = 1; + /// # #[allow(unused)] + /// i32::try_from(foo).is_ok(); + /// ``` + #[clippy::version = "1.37.0"] + pub CHECKED_CONVERSIONS, + pedantic, + "`try_from` could replace manual bounds checking when casting" +} + +pub struct CheckedConversions { + msrv: Option<RustcVersion>, +} + +impl CheckedConversions { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(CheckedConversions => [CHECKED_CONVERSIONS]); + +impl<'tcx> LateLintPass<'tcx> for CheckedConversions { + fn check_expr(&mut self, cx: &LateContext<'_>, item: &Expr<'_>) { + if !meets_msrv(self.msrv, msrvs::TRY_FROM) { + return; + } + + let result = if_chain! { + if !in_constant(cx, item.hir_id); + if !in_external_macro(cx.sess(), item.span); + if let ExprKind::Binary(op, left, right) = &item.kind; + + then { + match op.node { + BinOpKind::Ge | BinOpKind::Le => single_check(item), + BinOpKind::And => double_check(cx, left, right), + _ => None, + } + } else { + None + } + }; + + if let Some(cv) = result { + if let Some(to_type) = cv.to_type { + let mut applicability = Applicability::MachineApplicable; + let snippet = snippet_with_applicability(cx, cv.expr_to_cast.span, "_", &mut applicability); + span_lint_and_sugg( + cx, + CHECKED_CONVERSIONS, + item.span, + "checked cast can be simplified", + "try", + format!("{}::try_from({}).is_ok()", to_type, snippet), + applicability, + ); + } + } + } + + extract_msrv_attr!(LateContext); +} + +/// Searches for a single check from unsigned to _ is done +/// todo: check for case signed -> larger unsigned == only x >= 0 +fn single_check<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<Conversion<'tcx>> { + check_upper_bound(expr).filter(|cv| cv.cvt == ConversionType::FromUnsigned) +} + +/// Searches for a combination of upper & lower bound checks +fn double_check<'a>(cx: &LateContext<'_>, left: &'a Expr<'_>, right: &'a Expr<'_>) -> Option<Conversion<'a>> { + let upper_lower = |l, r| { + let upper = check_upper_bound(l); + let lower = check_lower_bound(r); + + upper.zip(lower).and_then(|(l, r)| l.combine(r, cx)) + }; + + upper_lower(left, right).or_else(|| upper_lower(right, left)) +} + +/// Contains the result of a tried conversion check +#[derive(Clone, Debug)] +struct Conversion<'a> { + cvt: ConversionType, + expr_to_cast: &'a Expr<'a>, + to_type: Option<&'a str>, +} + +/// The kind of conversion that is checked +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum ConversionType { + SignedToUnsigned, + SignedToSigned, + FromUnsigned, +} + +impl<'a> Conversion<'a> { + /// Combine multiple conversions if the are compatible + pub fn combine(self, other: Self, cx: &LateContext<'_>) -> Option<Conversion<'a>> { + if self.is_compatible(&other, cx) { + // Prefer a Conversion that contains a type-constraint + Some(if self.to_type.is_some() { self } else { other }) + } else { + None + } + } + + /// Checks if two conversions are compatible + /// same type of conversion, same 'castee' and same 'to type' + pub fn is_compatible(&self, other: &Self, cx: &LateContext<'_>) -> bool { + (self.cvt == other.cvt) + && (SpanlessEq::new(cx).eq_expr(self.expr_to_cast, other.expr_to_cast)) + && (self.has_compatible_to_type(other)) + } + + /// Checks if the to-type is the same (if there is a type constraint) + fn has_compatible_to_type(&self, other: &Self) -> bool { + match (self.to_type, other.to_type) { + (Some(l), Some(r)) => l == r, + _ => true, + } + } + + /// Try to construct a new conversion if the conversion type is valid + fn try_new(expr_to_cast: &'a Expr<'_>, from_type: &str, to_type: &'a str) -> Option<Conversion<'a>> { + ConversionType::try_new(from_type, to_type).map(|cvt| Conversion { + cvt, + expr_to_cast, + to_type: Some(to_type), + }) + } + + /// Construct a new conversion without type constraint + fn new_any(expr_to_cast: &'a Expr<'_>) -> Conversion<'a> { + Conversion { + cvt: ConversionType::SignedToUnsigned, + expr_to_cast, + to_type: None, + } + } +} + +impl ConversionType { + /// Creates a conversion type if the type is allowed & conversion is valid + #[must_use] + fn try_new(from: &str, to: &str) -> Option<Self> { + if UINTS.contains(&from) { + Some(Self::FromUnsigned) + } else if SINTS.contains(&from) { + if UINTS.contains(&to) { + Some(Self::SignedToUnsigned) + } else if SINTS.contains(&to) { + Some(Self::SignedToSigned) + } else { + None + } + } else { + None + } + } +} + +/// Check for `expr <= (to_type::MAX as from_type)` +fn check_upper_bound<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<Conversion<'tcx>> { + if_chain! { + if let ExprKind::Binary(ref op, left, right) = &expr.kind; + if let Some((candidate, check)) = normalize_le_ge(op, left, right); + if let Some((from, to)) = get_types_from_cast(check, INTS, "max_value", "MAX"); + + then { + Conversion::try_new(candidate, from, to) + } else { + None + } + } +} + +/// Check for `expr >= 0|(to_type::MIN as from_type)` +fn check_lower_bound<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<Conversion<'tcx>> { + fn check_function<'a>(candidate: &'a Expr<'a>, check: &'a Expr<'a>) -> Option<Conversion<'a>> { + (check_lower_bound_zero(candidate, check)).or_else(|| (check_lower_bound_min(candidate, check))) + } + + // First of we need a binary containing the expression & the cast + if let ExprKind::Binary(ref op, left, right) = &expr.kind { + normalize_le_ge(op, right, left).and_then(|(l, r)| check_function(l, r)) + } else { + None + } +} + +/// Check for `expr >= 0` +fn check_lower_bound_zero<'a>(candidate: &'a Expr<'_>, check: &'a Expr<'_>) -> Option<Conversion<'a>> { + if_chain! { + if let ExprKind::Lit(ref lit) = &check.kind; + if let LitKind::Int(0, _) = &lit.node; + + then { + Some(Conversion::new_any(candidate)) + } else { + None + } + } +} + +/// Check for `expr >= (to_type::MIN as from_type)` +fn check_lower_bound_min<'a>(candidate: &'a Expr<'_>, check: &'a Expr<'_>) -> Option<Conversion<'a>> { + if let Some((from, to)) = get_types_from_cast(check, SINTS, "min_value", "MIN") { + Conversion::try_new(candidate, from, to) + } else { + None + } +} + +/// Tries to extract the from- and to-type from a cast expression +fn get_types_from_cast<'a>( + expr: &'a Expr<'_>, + types: &'a [&str], + func: &'a str, + assoc_const: &'a str, +) -> Option<(&'a str, &'a str)> { + // `to_type::max_value() as from_type` + // or `to_type::MAX as from_type` + let call_from_cast: Option<(&Expr<'_>, &str)> = if_chain! { + // to_type::max_value(), from_type + if let ExprKind::Cast(limit, from_type) = &expr.kind; + if let TyKind::Path(ref from_type_path) = &from_type.kind; + if let Some(from_sym) = int_ty_to_sym(from_type_path); + + then { + Some((limit, from_sym)) + } else { + None + } + }; + + // `from_type::from(to_type::max_value())` + let limit_from: Option<(&Expr<'_>, &str)> = call_from_cast.or_else(|| { + if_chain! { + // `from_type::from, to_type::max_value()` + if let ExprKind::Call(from_func, args) = &expr.kind; + // `to_type::max_value()` + if args.len() == 1; + if let limit = &args[0]; + // `from_type::from` + if let ExprKind::Path(ref path) = &from_func.kind; + if let Some(from_sym) = get_implementing_type(path, INTS, "from"); + + then { + Some((limit, from_sym)) + } else { + None + } + } + }); + + if let Some((limit, from_type)) = limit_from { + match limit.kind { + // `from_type::from(_)` + ExprKind::Call(path, _) => { + if let ExprKind::Path(ref path) = path.kind { + // `to_type` + if let Some(to_type) = get_implementing_type(path, types, func) { + return Some((from_type, to_type)); + } + } + }, + // `to_type::MAX` + ExprKind::Path(ref path) => { + if let Some(to_type) = get_implementing_type(path, types, assoc_const) { + return Some((from_type, to_type)); + } + }, + _ => {}, + } + }; + None +} + +/// Gets the type which implements the called function +fn get_implementing_type<'a>(path: &QPath<'_>, candidates: &'a [&str], function: &str) -> Option<&'a str> { + if_chain! { + if let QPath::TypeRelative(ty, path) = &path; + if path.ident.name.as_str() == function; + if let TyKind::Path(QPath::Resolved(None, tp)) = &ty.kind; + if let [int] = tp.segments; + then { + let name = int.ident.name.as_str(); + candidates.iter().find(|c| &name == *c).copied() + } else { + None + } + } +} + +/// Gets the type as a string, if it is a supported integer +fn int_ty_to_sym<'tcx>(path: &QPath<'_>) -> Option<&'tcx str> { + if_chain! { + if let QPath::Resolved(_, path) = *path; + if let [ty] = path.segments; + then { + let name = ty.ident.name.as_str(); + INTS.iter().find(|c| &name == *c).copied() + } else { + None + } + } +} + +/// Will return the expressions as if they were expr1 <= expr2 +fn normalize_le_ge<'a>(op: &BinOp, left: &'a Expr<'a>, right: &'a Expr<'a>) -> Option<(&'a Expr<'a>, &'a Expr<'a>)> { + match op.node { + BinOpKind::Le => Some((left, right)), + BinOpKind::Ge => Some((right, left)), + _ => None, + } +} + +// Constants +const UINTS: &[&str] = &["u8", "u16", "u32", "u64", "usize"]; +const SINTS: &[&str] = &["i8", "i16", "i32", "i64", "isize"]; +const INTS: &[&str] = &["u8", "u16", "u32", "u64", "usize", "i8", "i16", "i32", "i64", "isize"]; diff --git a/src/tools/clippy/clippy_lints/src/cognitive_complexity.rs b/src/tools/clippy/clippy_lints/src/cognitive_complexity.rs new file mode 100644 index 000000000..33c44f8b2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/cognitive_complexity.rs @@ -0,0 +1,167 @@ +//! calculate cognitive complexity and warn about overly complex functions + +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::LimitStack; +use rustc_ast::ast::Attribute; +use rustc_hir::intravisit::{walk_expr, FnKind, Visitor}; +use rustc_hir::{Body, Expr, ExprKind, FnDecl, HirId}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Span; +use rustc_span::{sym, BytePos}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for methods with high cognitive complexity. + /// + /// ### Why is this bad? + /// Methods of high cognitive complexity tend to be hard to + /// both read and maintain. Also LLVM will tend to optimize small methods better. + /// + /// ### Known problems + /// Sometimes it's hard to find a way to reduce the + /// complexity. + /// + /// ### Example + /// You'll see it when you get the warning. + #[clippy::version = "1.35.0"] + pub COGNITIVE_COMPLEXITY, + nursery, + "functions that should be split up into multiple functions" +} + +pub struct CognitiveComplexity { + limit: LimitStack, +} + +impl CognitiveComplexity { + #[must_use] + pub fn new(limit: u64) -> Self { + Self { + limit: LimitStack::new(limit), + } + } +} + +impl_lint_pass!(CognitiveComplexity => [COGNITIVE_COMPLEXITY]); + +impl CognitiveComplexity { + #[expect(clippy::cast_possible_truncation)] + fn check<'tcx>( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + body_span: Span, + ) { + if body_span.from_expansion() { + return; + } + + let expr = &body.value; + + let mut helper = CcHelper { cc: 1, returns: 0 }; + helper.visit_expr(expr); + let CcHelper { cc, returns } = helper; + let ret_ty = cx.typeck_results().node_type(expr.hir_id); + let ret_adjust = if is_type_diagnostic_item(cx, ret_ty, sym::Result) { + returns + } else { + #[expect(clippy::integer_division)] + (returns / 2) + }; + + let mut rust_cc = cc; + // prevent degenerate cases where unreachable code contains `return` statements + if rust_cc >= ret_adjust { + rust_cc -= ret_adjust; + } + + if rust_cc > self.limit.limit() { + let fn_span = match kind { + FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span, + FnKind::Closure => { + let header_span = body_span.with_hi(decl.output.span().lo()); + let pos = snippet_opt(cx, header_span).and_then(|snip| { + let low_offset = snip.find('|')?; + let high_offset = 1 + snip.get(low_offset + 1..)?.find('|')?; + let low = header_span.lo() + BytePos(low_offset as u32); + let high = low + BytePos(high_offset as u32 + 1); + + Some((low, high)) + }); + + if let Some((low, high)) = pos { + Span::new(low, high, header_span.ctxt(), header_span.parent()) + } else { + return; + } + }, + }; + + span_lint_and_help( + cx, + COGNITIVE_COMPLEXITY, + fn_span, + &format!( + "the function has a cognitive complexity of ({}/{})", + rust_cc, + self.limit.limit() + ), + None, + "you could split it up into multiple smaller functions", + ); + } + } +} + +impl<'tcx> LateLintPass<'tcx> for CognitiveComplexity { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + span: Span, + hir_id: HirId, + ) { + let def_id = cx.tcx.hir().local_def_id(hir_id); + if !cx.tcx.has_attr(def_id.to_def_id(), sym::test) { + self.check(cx, kind, decl, body, span); + } + } + + fn enter_lint_attrs(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) { + self.limit.push_attrs(cx.sess(), attrs, "cognitive_complexity"); + } + fn exit_lint_attrs(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) { + self.limit.pop_attrs(cx.sess(), attrs, "cognitive_complexity"); + } +} + +struct CcHelper { + cc: u64, + returns: u64, +} + +impl<'tcx> Visitor<'tcx> for CcHelper { + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + walk_expr(self, e); + match e.kind { + ExprKind::If(_, _, _) => { + self.cc += 1; + }, + ExprKind::Match(_, arms, _) => { + if arms.len() > 1 { + self.cc += 1; + } + self.cc += arms.iter().filter(|arm| arm.guard.is_some()).count() as u64; + }, + ExprKind::Ret(_) => self.returns += 1, + _ => {}, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/collapsible_if.rs b/src/tools/clippy/clippy_lints/src/collapsible_if.rs new file mode 100644 index 000000000..90430b71a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/collapsible_if.rs @@ -0,0 +1,195 @@ +//! Checks for if expressions that contain only an if expression. +//! +//! For example, the lint would catch: +//! +//! ```rust,ignore +//! if x { +//! if y { +//! println!("Hello world"); +//! } +//! } +//! ``` +//! +//! This lint is **warn** by default + +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::{snippet, snippet_block, snippet_block_with_applicability}; +use clippy_utils::sugg::Sugg; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for nested `if` statements which can be collapsed + /// by `&&`-combining their conditions. + /// + /// ### Why is this bad? + /// Each `if`-statement adds one level of nesting, which + /// makes code look more complex than it really is. + /// + /// ### Example + /// ```rust + /// # let (x, y) = (true, true); + /// if x { + /// if y { + /// // … + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let (x, y) = (true, true); + /// if x && y { + /// // … + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub COLLAPSIBLE_IF, + style, + "nested `if`s that can be collapsed (e.g., `if x { if y { ... } }`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for collapsible `else { if ... }` expressions + /// that can be collapsed to `else if ...`. + /// + /// ### Why is this bad? + /// Each `if`-statement adds one level of nesting, which + /// makes code look more complex than it really is. + /// + /// ### Example + /// ```rust,ignore + /// + /// if x { + /// … + /// } else { + /// if y { + /// … + /// } + /// } + /// ``` + /// + /// Should be written: + /// + /// ```rust,ignore + /// if x { + /// … + /// } else if y { + /// … + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub COLLAPSIBLE_ELSE_IF, + style, + "nested `else`-`if` expressions that can be collapsed (e.g., `else { if x { ... } }`)" +} + +declare_lint_pass!(CollapsibleIf => [COLLAPSIBLE_IF, COLLAPSIBLE_ELSE_IF]); + +impl EarlyLintPass for CollapsibleIf { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) { + if !expr.span.from_expansion() { + check_if(cx, expr); + } + } +} + +fn check_if(cx: &EarlyContext<'_>, expr: &ast::Expr) { + if let ast::ExprKind::If(check, then, else_) = &expr.kind { + if let Some(else_) = else_ { + check_collapsible_maybe_if_let(cx, then.span, else_); + } else if let ast::ExprKind::Let(..) = check.kind { + // Prevent triggering on `if let a = b { if c { .. } }`. + } else { + check_collapsible_no_if_let(cx, expr, check, then); + } + } +} + +fn block_starts_with_comment(cx: &EarlyContext<'_>, expr: &ast::Block) -> bool { + // We trim all opening braces and whitespaces and then check if the next string is a comment. + let trimmed_block_text = snippet_block(cx, expr.span, "..", None) + .trim_start_matches(|c: char| c.is_whitespace() || c == '{') + .to_owned(); + trimmed_block_text.starts_with("//") || trimmed_block_text.starts_with("/*") +} + +fn check_collapsible_maybe_if_let(cx: &EarlyContext<'_>, then_span: Span, else_: &ast::Expr) { + if_chain! { + if let ast::ExprKind::Block(ref block, _) = else_.kind; + if !block_starts_with_comment(cx, block); + if let Some(else_) = expr_block(block); + if else_.attrs.is_empty(); + if !else_.span.from_expansion(); + if let ast::ExprKind::If(..) = else_.kind; + then { + // Prevent "elseif" + // Check that the "else" is followed by whitespace + let up_to_else = then_span.between(block.span); + let requires_space = if let Some(c) = snippet(cx, up_to_else, "..").chars().last() { !c.is_whitespace() } else { false }; + + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + COLLAPSIBLE_ELSE_IF, + block.span, + "this `else { if .. }` block can be collapsed", + "collapse nested if block", + format!( + "{}{}", + if requires_space { " " } else { "" }, + snippet_block_with_applicability(cx, else_.span, "..", Some(block.span), &mut applicability) + ), + applicability, + ); + } + } +} + +fn check_collapsible_no_if_let(cx: &EarlyContext<'_>, expr: &ast::Expr, check: &ast::Expr, then: &ast::Block) { + if_chain! { + if !block_starts_with_comment(cx, then); + if let Some(inner) = expr_block(then); + if inner.attrs.is_empty(); + if let ast::ExprKind::If(ref check_inner, ref content, None) = inner.kind; + // Prevent triggering on `if c { if let a = b { .. } }`. + if !matches!(check_inner.kind, ast::ExprKind::Let(..)); + if expr.span.ctxt() == inner.span.ctxt(); + then { + span_lint_and_then(cx, COLLAPSIBLE_IF, expr.span, "this `if` statement can be collapsed", |diag| { + let lhs = Sugg::ast(cx, check, ".."); + let rhs = Sugg::ast(cx, check_inner, ".."); + diag.span_suggestion( + expr.span, + "collapse nested if block", + format!( + "if {} {}", + lhs.and(&rhs), + snippet_block(cx, content.span, "..", Some(expr.span)), + ), + Applicability::MachineApplicable, // snippet + ); + }); + } + } +} + +/// If the block contains only one expression, return it. +fn expr_block(block: &ast::Block) -> Option<&ast::Expr> { + let mut it = block.stmts.iter(); + + if let (Some(stmt), None) = (it.next(), it.next()) { + match stmt.kind { + ast::StmtKind::Expr(ref expr) | ast::StmtKind::Semi(ref expr) => Some(expr), + _ => None, + } + } else { + None + } +} diff --git a/src/tools/clippy/clippy_lints/src/comparison_chain.rs b/src/tools/clippy/clippy_lints/src/comparison_chain.rs new file mode 100644 index 000000000..a05b41eb3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/comparison_chain.rs @@ -0,0 +1,132 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::implements_trait; +use clippy_utils::{get_trait_def_id, if_sequence, in_constant, is_else_clause, paths, SpanlessEq}; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks comparison chains written with `if` that can be + /// rewritten with `match` and `cmp`. + /// + /// ### Why is this bad? + /// `if` is not guaranteed to be exhaustive and conditionals can get + /// repetitive + /// + /// ### Known problems + /// The match statement may be slower due to the compiler + /// not inlining the call to cmp. See issue [#5354](https://github.com/rust-lang/rust-clippy/issues/5354) + /// + /// ### Example + /// ```rust,ignore + /// # fn a() {} + /// # fn b() {} + /// # fn c() {} + /// fn f(x: u8, y: u8) { + /// if x > y { + /// a() + /// } else if x < y { + /// b() + /// } else { + /// c() + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// use std::cmp::Ordering; + /// # fn a() {} + /// # fn b() {} + /// # fn c() {} + /// fn f(x: u8, y: u8) { + /// match x.cmp(&y) { + /// Ordering::Greater => a(), + /// Ordering::Less => b(), + /// Ordering::Equal => c() + /// } + /// } + /// ``` + #[clippy::version = "1.40.0"] + pub COMPARISON_CHAIN, + style, + "`if`s that can be rewritten with `match` and `cmp`" +} + +declare_lint_pass!(ComparisonChain => [COMPARISON_CHAIN]); + +impl<'tcx> LateLintPass<'tcx> for ComparisonChain { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if expr.span.from_expansion() { + return; + } + + // We only care about the top-most `if` in the chain + if is_else_clause(cx.tcx, expr) { + return; + } + + if in_constant(cx, expr.hir_id) { + return; + } + + // Check that there exists at least one explicit else condition + let (conds, _) = if_sequence(expr); + if conds.len() < 2 { + return; + } + + for cond in conds.windows(2) { + if let (&ExprKind::Binary(ref kind1, lhs1, rhs1), &ExprKind::Binary(ref kind2, lhs2, rhs2)) = + (&cond[0].kind, &cond[1].kind) + { + if !kind_is_cmp(kind1.node) || !kind_is_cmp(kind2.node) { + return; + } + + // Check that both sets of operands are equal + let mut spanless_eq = SpanlessEq::new(cx); + let same_fixed_operands = spanless_eq.eq_expr(lhs1, lhs2) && spanless_eq.eq_expr(rhs1, rhs2); + let same_transposed_operands = spanless_eq.eq_expr(lhs1, rhs2) && spanless_eq.eq_expr(rhs1, lhs2); + + if !same_fixed_operands && !same_transposed_operands { + return; + } + + // Check that if the operation is the same, either it's not `==` or the operands are transposed + if kind1.node == kind2.node { + if kind1.node == BinOpKind::Eq { + return; + } + if !same_transposed_operands { + return; + } + } + + // Check that the type being compared implements `core::cmp::Ord` + let ty = cx.typeck_results().expr_ty(lhs1); + let is_ord = get_trait_def_id(cx, &paths::ORD).map_or(false, |id| implements_trait(cx, ty, id, &[])); + + if !is_ord { + return; + } + } else { + // We only care about comparison chains + return; + } + } + span_lint_and_help( + cx, + COMPARISON_CHAIN, + expr.span, + "`if` chain can be rewritten with `match`", + None, + "consider rewriting the `if` chain to use `cmp` and `match`", + ); + } +} + +fn kind_is_cmp(kind: BinOpKind) -> bool { + matches!(kind, BinOpKind::Lt | BinOpKind::Gt | BinOpKind::Eq) +} diff --git a/src/tools/clippy/clippy_lints/src/copies.rs b/src/tools/clippy/clippy_lints/src/copies.rs new file mode 100644 index 000000000..0e3d93175 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/copies.rs @@ -0,0 +1,584 @@ +use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_then}; +use clippy_utils::source::{first_line_of_span, indent_of, reindent_multiline, snippet, snippet_opt}; +use clippy_utils::ty::needs_ordered_drop; +use clippy_utils::visitors::for_each_expr; +use clippy_utils::{ + capture_local_usage, eq_expr_value, get_enclosing_block, hash_expr, hash_stmt, if_sequence, is_else_clause, + is_lint_allowed, path_to_local, search_same, ContainsName, HirEqInterExpr, SpanlessEq, +}; +use core::iter; +use core::ops::ControlFlow; +use rustc_errors::Applicability; +use rustc_hir::intravisit; +use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, HirIdSet, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::hygiene::walk_chain; +use rustc_span::source_map::SourceMap; +use rustc_span::{BytePos, Span, Symbol}; +use std::borrow::Cow; + +declare_clippy_lint! { + /// ### What it does + /// Checks for consecutive `if`s with the same condition. + /// + /// ### Why is this bad? + /// This is probably a copy & paste error. + /// + /// ### Example + /// ```ignore + /// if a == b { + /// … + /// } else if a == b { + /// … + /// } + /// ``` + /// + /// Note that this lint ignores all conditions with a function call as it could + /// have side effects: + /// + /// ```ignore + /// if foo() { + /// … + /// } else if foo() { // not linted + /// … + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub IFS_SAME_COND, + correctness, + "consecutive `if`s with the same condition" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for consecutive `if`s with the same function call. + /// + /// ### Why is this bad? + /// This is probably a copy & paste error. + /// Despite the fact that function can have side effects and `if` works as + /// intended, such an approach is implicit and can be considered a "code smell". + /// + /// ### Example + /// ```ignore + /// if foo() == bar { + /// … + /// } else if foo() == bar { + /// … + /// } + /// ``` + /// + /// This probably should be: + /// ```ignore + /// if foo() == bar { + /// … + /// } else if foo() == baz { + /// … + /// } + /// ``` + /// + /// or if the original code was not a typo and called function mutates a state, + /// consider move the mutation out of the `if` condition to avoid similarity to + /// a copy & paste error: + /// + /// ```ignore + /// let first = foo(); + /// if first == bar { + /// … + /// } else { + /// let second = foo(); + /// if second == bar { + /// … + /// } + /// } + /// ``` + #[clippy::version = "1.41.0"] + pub SAME_FUNCTIONS_IN_IF_CONDITION, + pedantic, + "consecutive `if`s with the same function call" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `if/else` with the same body as the *then* part + /// and the *else* part. + /// + /// ### Why is this bad? + /// This is probably a copy & paste error. + /// + /// ### Example + /// ```ignore + /// let foo = if … { + /// 42 + /// } else { + /// 42 + /// }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub IF_SAME_THEN_ELSE, + correctness, + "`if` with the same `then` and `else` blocks" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks if the `if` and `else` block contain shared code that can be + /// moved out of the blocks. + /// + /// ### Why is this bad? + /// Duplicate code is less maintainable. + /// + /// ### Known problems + /// * The lint doesn't check if the moved expressions modify values that are being used in + /// the if condition. The suggestion can in that case modify the behavior of the program. + /// See [rust-clippy#7452](https://github.com/rust-lang/rust-clippy/issues/7452) + /// + /// ### Example + /// ```ignore + /// let foo = if … { + /// println!("Hello World"); + /// 13 + /// } else { + /// println!("Hello World"); + /// 42 + /// }; + /// ``` + /// + /// Use instead: + /// ```ignore + /// println!("Hello World"); + /// let foo = if … { + /// 13 + /// } else { + /// 42 + /// }; + /// ``` + #[clippy::version = "1.53.0"] + pub BRANCHES_SHARING_CODE, + nursery, + "`if` statement with shared code in all blocks" +} + +declare_lint_pass!(CopyAndPaste => [ + IFS_SAME_COND, + SAME_FUNCTIONS_IN_IF_CONDITION, + IF_SAME_THEN_ELSE, + BRANCHES_SHARING_CODE +]); + +impl<'tcx> LateLintPass<'tcx> for CopyAndPaste { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if !expr.span.from_expansion() && matches!(expr.kind, ExprKind::If(..)) && !is_else_clause(cx.tcx, expr) { + let (conds, blocks) = if_sequence(expr); + lint_same_cond(cx, &conds); + lint_same_fns_in_if_cond(cx, &conds); + let all_same = + !is_lint_allowed(cx, IF_SAME_THEN_ELSE, expr.hir_id) && lint_if_same_then_else(cx, &conds, &blocks); + if !all_same && conds.len() != blocks.len() { + lint_branches_sharing_code(cx, &conds, &blocks, expr); + } + } + } +} + +/// Checks if the given expression is a let chain. +fn contains_let(e: &Expr<'_>) -> bool { + match e.kind { + ExprKind::Let(..) => true, + ExprKind::Binary(op, lhs, rhs) if op.node == BinOpKind::And => { + matches!(lhs.kind, ExprKind::Let(..)) || contains_let(rhs) + }, + _ => false, + } +} + +fn lint_if_same_then_else(cx: &LateContext<'_>, conds: &[&Expr<'_>], blocks: &[&Block<'_>]) -> bool { + let mut eq = SpanlessEq::new(cx); + blocks + .array_windows::<2>() + .enumerate() + .fold(true, |all_eq, (i, &[lhs, rhs])| { + if eq.eq_block(lhs, rhs) && !contains_let(conds[i]) && conds.get(i + 1).map_or(true, |e| !contains_let(e)) { + span_lint_and_note( + cx, + IF_SAME_THEN_ELSE, + lhs.span, + "this `if` has identical blocks", + Some(rhs.span), + "same as this", + ); + all_eq + } else { + false + } + }) +} + +fn lint_branches_sharing_code<'tcx>( + cx: &LateContext<'tcx>, + conds: &[&'tcx Expr<'_>], + blocks: &[&'tcx Block<'_>], + expr: &'tcx Expr<'_>, +) { + // We only lint ifs with multiple blocks + let &[first_block, ref blocks @ ..] = blocks else { + return; + }; + let &[.., last_block] = blocks else { + return; + }; + + let res = scan_block_for_eq(cx, conds, first_block, blocks); + let sm = cx.tcx.sess.source_map(); + let start_suggestion = res.start_span(first_block, sm).map(|span| { + let first_line_span = first_line_of_span(cx, expr.span); + let replace_span = first_line_span.with_hi(span.hi()); + let cond_span = first_line_span.until(first_block.span); + let cond_snippet = reindent_multiline(snippet(cx, cond_span, "_"), false, None); + let cond_indent = indent_of(cx, cond_span); + let moved_snippet = reindent_multiline(snippet(cx, span, "_"), true, None); + let suggestion = moved_snippet.to_string() + "\n" + &cond_snippet + "{"; + let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, cond_indent); + (replace_span, suggestion.to_string()) + }); + let end_suggestion = res.end_span(last_block, sm).map(|span| { + let moved_snipped = reindent_multiline(snippet(cx, span, "_"), true, None); + let indent = indent_of(cx, expr.span.shrink_to_hi()); + let suggestion = "}\n".to_string() + &moved_snipped; + let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, indent); + + let span = span.with_hi(last_block.span.hi()); + // Improve formatting if the inner block has indention (i.e. normal Rust formatting) + let test_span = Span::new(span.lo() - BytePos(4), span.lo(), span.ctxt(), span.parent()); + let span = if snippet_opt(cx, test_span).map_or(false, |snip| snip == " ") { + span.with_lo(test_span.lo()) + } else { + span + }; + (span, suggestion.to_string()) + }); + + let (span, msg, end_span) = match (&start_suggestion, &end_suggestion) { + (&Some((span, _)), &Some((end_span, _))) => ( + span, + "all if blocks contain the same code at both the start and the end", + Some(end_span), + ), + (&Some((span, _)), None) => (span, "all if blocks contain the same code at the start", None), + (None, &Some((span, _))) => (span, "all if blocks contain the same code at the end", None), + (None, None) => return, + }; + span_lint_and_then(cx, BRANCHES_SHARING_CODE, span, msg, |diag| { + if let Some(span) = end_span { + diag.span_note(span, "this code is shared at the end"); + } + if let Some((span, sugg)) = start_suggestion { + diag.span_suggestion( + span, + "consider moving these statements before the if", + sugg, + Applicability::Unspecified, + ); + } + if let Some((span, sugg)) = end_suggestion { + diag.span_suggestion( + span, + "consider moving these statements after the if", + sugg, + Applicability::Unspecified, + ); + if !cx.typeck_results().expr_ty(expr).is_unit() { + diag.note("the end suggestion probably needs some adjustments to use the expression result correctly"); + } + } + if check_for_warn_of_moved_symbol(cx, &res.moved_locals, expr) { + diag.warn("some moved values might need to be renamed to avoid wrong references"); + } + }); +} + +struct BlockEq { + /// The end of the range of equal stmts at the start. + start_end_eq: usize, + /// The start of the range of equal stmts at the end. + end_begin_eq: Option<usize>, + /// The name and id of every local which can be moved at the beginning and the end. + moved_locals: Vec<(HirId, Symbol)>, +} +impl BlockEq { + fn start_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> { + match &b.stmts[..self.start_end_eq] { + [first, .., last] => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))), + [s] => Some(sm.stmt_span(s.span, b.span)), + [] => None, + } + } + + fn end_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> { + match (&b.stmts[b.stmts.len() - self.end_begin_eq?..], b.expr) { + ([first, .., last], None) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))), + ([first, ..], Some(last)) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))), + ([s], None) => Some(sm.stmt_span(s.span, b.span)), + ([], Some(e)) => Some(walk_chain(e.span, b.span.ctxt())), + ([], None) => None, + } + } +} + +/// If the statement is a local, checks if the bound names match the expected list of names. +fn eq_binding_names(s: &Stmt<'_>, names: &[(HirId, Symbol)]) -> bool { + if let StmtKind::Local(l) = s.kind { + let mut i = 0usize; + let mut res = true; + l.pat.each_binding_or_first(&mut |_, _, _, name| { + if names.get(i).map_or(false, |&(_, n)| n == name.name) { + i += 1; + } else { + res = false; + } + }); + res && i == names.len() + } else { + false + } +} + +/// Checks if the statement modifies or moves any of the given locals. +fn modifies_any_local<'tcx>(cx: &LateContext<'tcx>, s: &'tcx Stmt<'_>, locals: &HirIdSet) -> bool { + for_each_expr(s, |e| { + if let Some(id) = path_to_local(e) + && locals.contains(&id) + && !capture_local_usage(cx, e).is_imm_ref() + { + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }) + .is_some() +} + +/// Checks if the given statement should be considered equal to the statement in the same position +/// for each block. +fn eq_stmts( + stmt: &Stmt<'_>, + blocks: &[&Block<'_>], + get_stmt: impl for<'a> Fn(&'a Block<'a>) -> Option<&'a Stmt<'a>>, + eq: &mut HirEqInterExpr<'_, '_, '_>, + moved_bindings: &mut Vec<(HirId, Symbol)>, +) -> bool { + (if let StmtKind::Local(l) = stmt.kind { + let old_count = moved_bindings.len(); + l.pat.each_binding_or_first(&mut |_, id, _, name| { + moved_bindings.push((id, name.name)); + }); + let new_bindings = &moved_bindings[old_count..]; + blocks + .iter() + .all(|b| get_stmt(b).map_or(false, |s| eq_binding_names(s, new_bindings))) + } else { + true + }) && blocks + .iter() + .all(|b| get_stmt(b).map_or(false, |s| eq.eq_stmt(s, stmt))) +} + +#[expect(clippy::too_many_lines)] +fn scan_block_for_eq<'tcx>( + cx: &LateContext<'tcx>, + conds: &[&'tcx Expr<'_>], + block: &'tcx Block<'_>, + blocks: &[&'tcx Block<'_>], +) -> BlockEq { + let mut eq = SpanlessEq::new(cx); + let mut eq = eq.inter_expr(); + let mut moved_locals = Vec::new(); + + let mut cond_locals = HirIdSet::default(); + for &cond in conds { + let _: Option<!> = for_each_expr(cond, |e| { + if let Some(id) = path_to_local(e) { + cond_locals.insert(id); + } + ControlFlow::Continue(()) + }); + } + + let mut local_needs_ordered_drop = false; + let start_end_eq = block + .stmts + .iter() + .enumerate() + .find(|&(i, stmt)| { + if let StmtKind::Local(l) = stmt.kind + && needs_ordered_drop(cx, cx.typeck_results().node_type(l.hir_id)) + { + local_needs_ordered_drop = true; + return true; + } + modifies_any_local(cx, stmt, &cond_locals) + || !eq_stmts(stmt, blocks, |b| b.stmts.get(i), &mut eq, &mut moved_locals) + }) + .map_or(block.stmts.len(), |(i, _)| i); + + if local_needs_ordered_drop { + return BlockEq { + start_end_eq, + end_begin_eq: None, + moved_locals, + }; + } + + // Walk backwards through the final expression/statements so long as their hashes are equal. Note + // `SpanlessHash` treats all local references as equal allowing locals declared earlier in the block + // to match those in other blocks. e.g. If each block ends with the following the hash value will be + // the same even though each `x` binding will have a different `HirId`: + // let x = foo(); + // x + 50 + let expr_hash_eq = if let Some(e) = block.expr { + let hash = hash_expr(cx, e); + blocks + .iter() + .all(|b| b.expr.map_or(false, |e| hash_expr(cx, e) == hash)) + } else { + blocks.iter().all(|b| b.expr.is_none()) + }; + if !expr_hash_eq { + return BlockEq { + start_end_eq, + end_begin_eq: None, + moved_locals, + }; + } + let end_search_start = block.stmts[start_end_eq..] + .iter() + .rev() + .enumerate() + .find(|&(offset, stmt)| { + let hash = hash_stmt(cx, stmt); + blocks.iter().any(|b| { + b.stmts + // the bounds check will catch the underflow + .get(b.stmts.len().wrapping_sub(offset + 1)) + .map_or(true, |s| hash != hash_stmt(cx, s)) + }) + }) + .map_or(block.stmts.len() - start_end_eq, |(i, _)| i); + + let moved_locals_at_start = moved_locals.len(); + let mut i = end_search_start; + let end_begin_eq = block.stmts[block.stmts.len() - end_search_start..] + .iter() + .zip(iter::repeat_with(move || { + let x = i; + i -= 1; + x + })) + .fold(end_search_start, |init, (stmt, offset)| { + if eq_stmts( + stmt, + blocks, + |b| b.stmts.get(b.stmts.len() - offset), + &mut eq, + &mut moved_locals, + ) { + init + } else { + // Clear out all locals seen at the end so far. None of them can be moved. + let stmts = &blocks[0].stmts; + for stmt in &stmts[stmts.len() - init..=stmts.len() - offset] { + if let StmtKind::Local(l) = stmt.kind { + l.pat.each_binding_or_first(&mut |_, id, _, _| { + eq.locals.remove(&id); + }); + } + } + moved_locals.truncate(moved_locals_at_start); + offset - 1 + } + }); + if let Some(e) = block.expr { + for block in blocks { + if block.expr.map_or(false, |expr| !eq.eq_expr(expr, e)) { + moved_locals.truncate(moved_locals_at_start); + return BlockEq { + start_end_eq, + end_begin_eq: None, + moved_locals, + }; + } + } + } + + BlockEq { + start_end_eq, + end_begin_eq: Some(end_begin_eq), + moved_locals, + } +} + +fn check_for_warn_of_moved_symbol(cx: &LateContext<'_>, symbols: &[(HirId, Symbol)], if_expr: &Expr<'_>) -> bool { + get_enclosing_block(cx, if_expr.hir_id).map_or(false, |block| { + let ignore_span = block.span.shrink_to_lo().to(if_expr.span); + + symbols + .iter() + .filter(|&&(_, name)| !name.as_str().starts_with('_')) + .any(|&(_, name)| { + let mut walker = ContainsName { name, result: false }; + + // Scan block + block + .stmts + .iter() + .filter(|stmt| !ignore_span.overlaps(stmt.span)) + .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt)); + + if let Some(expr) = block.expr { + intravisit::walk_expr(&mut walker, expr); + } + + walker.result + }) + }) +} + +/// Implementation of `IFS_SAME_COND`. +fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) { + for (i, j) in search_same(conds, |e| hash_expr(cx, e), |lhs, rhs| eq_expr_value(cx, lhs, rhs)) { + span_lint_and_note( + cx, + IFS_SAME_COND, + j.span, + "this `if` has the same condition as a previous `if`", + Some(i.span), + "same as this", + ); + } +} + +/// Implementation of `SAME_FUNCTIONS_IN_IF_CONDITION`. +fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) { + let eq: &dyn Fn(&&Expr<'_>, &&Expr<'_>) -> bool = &|&lhs, &rhs| -> bool { + // Do not lint if any expr originates from a macro + if lhs.span.from_expansion() || rhs.span.from_expansion() { + return false; + } + // Do not spawn warning if `IFS_SAME_COND` already produced it. + if eq_expr_value(cx, lhs, rhs) { + return false; + } + SpanlessEq::new(cx).eq_expr(lhs, rhs) + }; + + for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) { + span_lint_and_note( + cx, + SAME_FUNCTIONS_IN_IF_CONDITION, + j.span, + "this `if` has the same function call as a previous `if`", + Some(i.span), + "same as this", + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/copy_iterator.rs b/src/tools/clippy/clippy_lints/src/copy_iterator.rs new file mode 100644 index 000000000..026683f60 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/copy_iterator.rs @@ -0,0 +1,62 @@ +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::ty::is_copy; +use rustc_hir::{Impl, Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +use if_chain::if_chain; + +declare_clippy_lint! { + /// ### What it does + /// Checks for types that implement `Copy` as well as + /// `Iterator`. + /// + /// ### Why is this bad? + /// Implicit copies can be confusing when working with + /// iterator combinators. + /// + /// ### Example + /// ```rust,ignore + /// #[derive(Copy, Clone)] + /// struct Countdown(u8); + /// + /// impl Iterator for Countdown { + /// // ... + /// } + /// + /// let a: Vec<_> = my_iterator.take(1).collect(); + /// let b: Vec<_> = my_iterator.collect(); + /// ``` + #[clippy::version = "1.30.0"] + pub COPY_ITERATOR, + pedantic, + "implementing `Iterator` on a `Copy` type" +} + +declare_lint_pass!(CopyIterator => [COPY_ITERATOR]); + +impl<'tcx> LateLintPass<'tcx> for CopyIterator { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if_chain! { + if let ItemKind::Impl(Impl { + of_trait: Some(ref trait_ref), + .. + }) = item.kind; + let ty = cx.tcx.type_of(item.def_id); + if is_copy(cx, ty); + if let Some(trait_id) = trait_ref.trait_def_id(); + if cx.tcx.is_diagnostic_item(sym::Iterator, trait_id); + then { + span_lint_and_note( + cx, + COPY_ITERATOR, + item.span, + "you are implementing `Iterator` on a `Copy` type", + None, + "consider implementing `IntoIterator` instead", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/crate_in_macro_def.rs b/src/tools/clippy/clippy_lints/src/crate_in_macro_def.rs new file mode 100644 index 000000000..454ec2338 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/crate_in_macro_def.rs @@ -0,0 +1,125 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::ast::{AttrKind, Attribute, Item, ItemKind}; +use rustc_ast::token::{Token, TokenKind}; +use rustc_ast::tokenstream::{TokenStream, TokenTree}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{symbol::sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `crate` as opposed to `$crate` in a macro definition. + /// + /// ### Why is this bad? + /// `crate` refers to the macro call's crate, whereas `$crate` refers to the macro definition's + /// crate. Rarely is the former intended. See: + /// https://doc.rust-lang.org/reference/macros-by-example.html#hygiene + /// + /// ### Example + /// ```rust + /// #[macro_export] + /// macro_rules! print_message { + /// () => { + /// println!("{}", crate::MESSAGE); + /// }; + /// } + /// pub const MESSAGE: &str = "Hello!"; + /// ``` + /// Use instead: + /// ```rust + /// #[macro_export] + /// macro_rules! print_message { + /// () => { + /// println!("{}", $crate::MESSAGE); + /// }; + /// } + /// pub const MESSAGE: &str = "Hello!"; + /// ``` + /// + /// Note that if the use of `crate` is intentional, an `allow` attribute can be applied to the + /// macro definition, e.g.: + /// ```rust,ignore + /// #[allow(clippy::crate_in_macro_def)] + /// macro_rules! ok { ... crate::foo ... } + /// ``` + #[clippy::version = "1.62.0"] + pub CRATE_IN_MACRO_DEF, + suspicious, + "using `crate` in a macro definition" +} +declare_lint_pass!(CrateInMacroDef => [CRATE_IN_MACRO_DEF]); + +impl EarlyLintPass for CrateInMacroDef { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if_chain! { + if item.attrs.iter().any(is_macro_export); + if let ItemKind::MacroDef(macro_def) = &item.kind; + let tts = macro_def.body.inner_tokens(); + if let Some(span) = contains_unhygienic_crate_reference(&tts); + then { + span_lint_and_sugg( + cx, + CRATE_IN_MACRO_DEF, + span, + "`crate` references the macro call's crate", + "to reference the macro definition's crate, use", + String::from("$crate"), + Applicability::MachineApplicable, + ); + } + } + } +} + +fn is_macro_export(attr: &Attribute) -> bool { + if_chain! { + if let AttrKind::Normal(attr_item, _) = &attr.kind; + if let [segment] = attr_item.path.segments.as_slice(); + then { + segment.ident.name == sym::macro_export + } else { + false + } + } +} + +fn contains_unhygienic_crate_reference(tts: &TokenStream) -> Option<Span> { + let mut prev_is_dollar = false; + let mut cursor = tts.trees(); + while let Some(curr) = cursor.next() { + if_chain! { + if !prev_is_dollar; + if let Some(span) = is_crate_keyword(curr); + if let Some(next) = cursor.look_ahead(0); + if is_token(next, &TokenKind::ModSep); + then { + return Some(span); + } + } + if let TokenTree::Delimited(_, _, tts) = &curr { + let span = contains_unhygienic_crate_reference(tts); + if span.is_some() { + return span; + } + } + prev_is_dollar = is_token(curr, &TokenKind::Dollar); + } + None +} + +fn is_crate_keyword(tt: &TokenTree) -> Option<Span> { + if_chain! { + if let TokenTree::Token(Token { kind: TokenKind::Ident(symbol, _), span }, _) = tt; + if symbol.as_str() == "crate"; + then { Some(*span) } else { None } + } +} + +fn is_token(tt: &TokenTree, kind: &TokenKind) -> bool { + if let TokenTree::Token(Token { kind: other, .. }, _) = tt { + kind == other + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/create_dir.rs b/src/tools/clippy/clippy_lints/src/create_dir.rs new file mode 100644 index 000000000..18d34370a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/create_dir.rs @@ -0,0 +1,54 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks usage of `std::fs::create_dir` and suggest using `std::fs::create_dir_all` instead. + /// + /// ### Why is this bad? + /// Sometimes `std::fs::create_dir` is mistakenly chosen over `std::fs::create_dir_all`. + /// + /// ### Example + /// ```rust,ignore + /// std::fs::create_dir("foo"); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// std::fs::create_dir_all("foo"); + /// ``` + #[clippy::version = "1.48.0"] + pub CREATE_DIR, + restriction, + "calling `std::fs::create_dir` instead of `std::fs::create_dir_all`" +} + +declare_lint_pass!(CreateDir => [CREATE_DIR]); + +impl LateLintPass<'_> for CreateDir { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::Call(func, args) = expr.kind; + if let ExprKind::Path(ref path) = func.kind; + if let Some(def_id) = cx.qpath_res(path, func.hir_id).opt_def_id(); + if match_def_path(cx, def_id, &paths::STD_FS_CREATE_DIR); + then { + span_lint_and_sugg( + cx, + CREATE_DIR, + expr.span, + "calling `std::fs::create_dir` where there may be a better way", + "consider calling `std::fs::create_dir_all` instead", + format!("create_dir_all({})", snippet(cx, args[0].span, "..")), + Applicability::MaybeIncorrect, + ) + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/dbg_macro.rs b/src/tools/clippy/clippy_lints/src/dbg_macro.rs new file mode 100644 index 000000000..fe9f4f9ae --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/dbg_macro.rs @@ -0,0 +1,101 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::root_macro_call_first_node; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{is_in_cfg_test, is_in_test_function}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of dbg!() macro. + /// + /// ### Why is this bad? + /// `dbg!` macro is intended as a debugging tool. It + /// should not be in version control. + /// + /// ### Example + /// ```rust,ignore + /// dbg!(true) + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// true + /// ``` + #[clippy::version = "1.34.0"] + pub DBG_MACRO, + restriction, + "`dbg!` macro is intended as a debugging tool" +} + +#[derive(Copy, Clone)] +pub struct DbgMacro { + allow_dbg_in_tests: bool, +} + +impl_lint_pass!(DbgMacro => [DBG_MACRO]); + +impl DbgMacro { + pub fn new(allow_dbg_in_tests: bool) -> Self { + DbgMacro { allow_dbg_in_tests } + } +} + +impl LateLintPass<'_> for DbgMacro { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return }; + if cx.tcx.is_diagnostic_item(sym::dbg_macro, macro_call.def_id) { + // allows `dbg!` in test code if allow-dbg-in-test is set to true in clippy.toml + if self.allow_dbg_in_tests + && (is_in_test_function(cx.tcx, expr.hir_id) || is_in_cfg_test(cx.tcx, expr.hir_id)) + { + return; + } + let mut applicability = Applicability::MachineApplicable; + let suggestion = match expr.peel_drop_temps().kind { + // dbg!() + ExprKind::Block(_, _) => String::new(), + // dbg!(1) + ExprKind::Match(val, ..) => { + snippet_with_applicability(cx, val.span.source_callsite(), "..", &mut applicability).to_string() + }, + // dbg!(2, 3) + ExprKind::Tup( + [ + Expr { + kind: ExprKind::Match(first, ..), + .. + }, + .., + Expr { + kind: ExprKind::Match(last, ..), + .. + }, + ], + ) => { + let snippet = snippet_with_applicability( + cx, + first.span.source_callsite().to(last.span.source_callsite()), + "..", + &mut applicability, + ); + format!("({snippet})") + }, + _ => return, + }; + + span_lint_and_sugg( + cx, + DBG_MACRO, + macro_call.span, + "`dbg!` macro is intended as a debugging tool", + "ensure to avoid having uses of it in version control", + suggestion, + applicability, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/default.rs b/src/tools/clippy/clippy_lints/src/default.rs new file mode 100644 index 000000000..d99a1aa29 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/default.rs @@ -0,0 +1,307 @@ +use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_sugg}; +use clippy_utils::source::snippet_with_macro_callsite; +use clippy_utils::ty::{has_drop, is_copy}; +use clippy_utils::{any_parent_is_automatically_derived, contains_name, get_parent_expr, match_def_path, paths}; +use if_chain::if_chain; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_hir::def::Res; +use rustc_hir::{Block, Expr, ExprKind, PatKind, QPath, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::{Ident, Symbol}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for literal calls to `Default::default()`. + /// + /// ### Why is this bad? + /// It's easier for the reader if the name of the type is used, rather than the + /// generic `Default`. + /// + /// ### Example + /// ```rust + /// let s: String = Default::default(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let s = String::default(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DEFAULT_TRAIT_ACCESS, + pedantic, + "checks for literal calls to `Default::default()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for immediate reassignment of fields initialized + /// with Default::default(). + /// + /// ### Why is this bad? + ///It's more idiomatic to use the [functional update syntax](https://doc.rust-lang.org/reference/expressions/struct-expr.html#functional-update-syntax). + /// + /// ### Known problems + /// Assignments to patterns that are of tuple type are not linted. + /// + /// ### Example + /// ``` + /// # #[derive(Default)] + /// # struct A { i: i32 } + /// let mut a: A = Default::default(); + /// a.i = 42; + /// ``` + /// + /// Use instead: + /// ``` + /// # #[derive(Default)] + /// # struct A { i: i32 } + /// let a = A { + /// i: 42, + /// .. Default::default() + /// }; + /// ``` + #[clippy::version = "1.49.0"] + pub FIELD_REASSIGN_WITH_DEFAULT, + style, + "binding initialized with Default should have its fields set in the initializer" +} + +#[derive(Default)] +pub struct Default { + // Spans linted by `field_reassign_with_default`. + reassigned_linted: FxHashSet<Span>, +} + +impl_lint_pass!(Default => [DEFAULT_TRAIT_ACCESS, FIELD_REASSIGN_WITH_DEFAULT]); + +impl<'tcx> LateLintPass<'tcx> for Default { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + if !expr.span.from_expansion(); + // Avoid cases already linted by `field_reassign_with_default` + if !self.reassigned_linted.contains(&expr.span); + if let ExprKind::Call(path, ..) = expr.kind; + if !any_parent_is_automatically_derived(cx.tcx, expr.hir_id); + if let ExprKind::Path(ref qpath) = path.kind; + if let Some(def_id) = cx.qpath_res(qpath, path.hir_id).opt_def_id(); + if match_def_path(cx, def_id, &paths::DEFAULT_TRAIT_METHOD); + if !is_update_syntax_base(cx, expr); + // Detect and ignore <Foo as Default>::default() because these calls do explicitly name the type. + if let QPath::Resolved(None, _path) = qpath; + let expr_ty = cx.typeck_results().expr_ty(expr); + if let ty::Adt(def, ..) = expr_ty.kind(); + then { + // TODO: Work out a way to put "whatever the imported way of referencing + // this type in this file" rather than a fully-qualified type. + let replacement = format!("{}::default()", cx.tcx.def_path_str(def.did())); + span_lint_and_sugg( + cx, + DEFAULT_TRAIT_ACCESS, + expr.span, + &format!("calling `{}` is more clear than this expression", replacement), + "try", + replacement, + Applicability::Unspecified, // First resolve the TODO above + ); + } + } + } + + #[expect(clippy::too_many_lines)] + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &Block<'tcx>) { + // start from the `let mut _ = _::default();` and look at all the following + // statements, see if they re-assign the fields of the binding + let stmts_head = match block.stmts { + // Skip the last statement since there cannot possibly be any following statements that re-assign fields. + [head @ .., _] if !head.is_empty() => head, + _ => return, + }; + for (stmt_idx, stmt) in stmts_head.iter().enumerate() { + // find all binding statements like `let mut _ = T::default()` where `T::default()` is the + // `default` method of the `Default` trait, and store statement index in current block being + // checked and the name of the bound variable + let (local, variant, binding_name, binding_type, span) = if_chain! { + // only take `let ...` statements + if let StmtKind::Local(local) = stmt.kind; + if let Some(expr) = local.init; + if !any_parent_is_automatically_derived(cx.tcx, expr.hir_id); + if !expr.span.from_expansion(); + // only take bindings to identifiers + if let PatKind::Binding(_, binding_id, ident, _) = local.pat.kind; + // only when assigning `... = Default::default()` + if is_expr_default(expr, cx); + let binding_type = cx.typeck_results().node_type(binding_id); + if let Some(adt) = binding_type.ty_adt_def(); + if adt.is_struct(); + let variant = adt.non_enum_variant(); + if adt.did().is_local() || !variant.is_field_list_non_exhaustive(); + let module_did = cx.tcx.parent_module(stmt.hir_id).to_def_id(); + if variant + .fields + .iter() + .all(|field| field.vis.is_accessible_from(module_did, cx.tcx)); + let all_fields_are_copy = variant + .fields + .iter() + .all(|field| { + is_copy(cx, cx.tcx.type_of(field.did)) + }); + if !has_drop(cx, binding_type) || all_fields_are_copy; + then { + (local, variant, ident.name, binding_type, expr.span) + } else { + continue; + } + }; + + // find all "later statement"'s where the fields of the binding set as + // Default::default() get reassigned, unless the reassignment refers to the original binding + let mut first_assign = None; + let mut assigned_fields = Vec::new(); + let mut cancel_lint = false; + for consecutive_statement in &block.stmts[stmt_idx + 1..] { + // find out if and which field was set by this `consecutive_statement` + if let Some((field_ident, assign_rhs)) = field_reassigned_by_stmt(consecutive_statement, binding_name) { + // interrupt and cancel lint if assign_rhs references the original binding + if contains_name(binding_name, assign_rhs) { + cancel_lint = true; + break; + } + + // if the field was previously assigned, replace the assignment, otherwise insert the assignment + if let Some(prev) = assigned_fields + .iter_mut() + .find(|(field_name, _)| field_name == &field_ident.name) + { + *prev = (field_ident.name, assign_rhs); + } else { + assigned_fields.push((field_ident.name, assign_rhs)); + } + + // also set first instance of error for help message + if first_assign.is_none() { + first_assign = Some(consecutive_statement); + } + } + // interrupt if no field was assigned, since we only want to look at consecutive statements + else { + break; + } + } + + // if there are incorrectly assigned fields, do a span_lint_and_note to suggest + // construction using `Ty { fields, ..Default::default() }` + if !assigned_fields.is_empty() && !cancel_lint { + // if all fields of the struct are not assigned, add `.. Default::default()` to the suggestion. + let ext_with_default = !variant + .fields + .iter() + .all(|field| assigned_fields.iter().any(|(a, _)| a == &field.name)); + + let field_list = assigned_fields + .into_iter() + .map(|(field, rhs)| { + // extract and store the assigned value for help message + let value_snippet = snippet_with_macro_callsite(cx, rhs.span, ".."); + format!("{}: {}", field, value_snippet) + }) + .collect::<Vec<String>>() + .join(", "); + + // give correct suggestion if generics are involved (see #6944) + let binding_type = if_chain! { + if let ty::Adt(adt_def, substs) = binding_type.kind(); + if !substs.is_empty(); + then { + let adt_def_ty_name = cx.tcx.item_name(adt_def.did()); + let generic_args = substs.iter().collect::<Vec<_>>(); + let tys_str = generic_args + .iter() + .map(ToString::to_string) + .collect::<Vec<_>>() + .join(", "); + format!("{}::<{}>", adt_def_ty_name, &tys_str) + } else { + binding_type.to_string() + } + }; + + let sugg = if ext_with_default { + if field_list.is_empty() { + format!("{}::default()", binding_type) + } else { + format!("{} {{ {}, ..Default::default() }}", binding_type, field_list) + } + } else { + format!("{} {{ {} }}", binding_type, field_list) + }; + + // span lint once per statement that binds default + span_lint_and_note( + cx, + FIELD_REASSIGN_WITH_DEFAULT, + first_assign.unwrap().span, + "field assignment outside of initializer for an instance created with Default::default()", + Some(local.span), + &format!( + "consider initializing the variable with `{}` and removing relevant reassignments", + sugg + ), + ); + self.reassigned_linted.insert(span); + } + } + } +} + +/// Checks if the given expression is the `default` method belonging to the `Default` trait. +fn is_expr_default<'tcx>(expr: &'tcx Expr<'tcx>, cx: &LateContext<'tcx>) -> bool { + if_chain! { + if let ExprKind::Call(fn_expr, _) = &expr.kind; + if let ExprKind::Path(qpath) = &fn_expr.kind; + if let Res::Def(_, def_id) = cx.qpath_res(qpath, fn_expr.hir_id); + then { + // right hand side of assignment is `Default::default` + match_def_path(cx, def_id, &paths::DEFAULT_TRAIT_METHOD) + } else { + false + } + } +} + +/// Returns the reassigned field and the assigning expression (right-hand side of assign). +fn field_reassigned_by_stmt<'tcx>(this: &Stmt<'tcx>, binding_name: Symbol) -> Option<(Ident, &'tcx Expr<'tcx>)> { + if_chain! { + // only take assignments + if let StmtKind::Semi(later_expr) = this.kind; + if let ExprKind::Assign(assign_lhs, assign_rhs, _) = later_expr.kind; + // only take assignments to fields where the left-hand side field is a field of + // the same binding as the previous statement + if let ExprKind::Field(binding, field_ident) = assign_lhs.kind; + if let ExprKind::Path(QPath::Resolved(_, path)) = binding.kind; + if let Some(second_binding_name) = path.segments.last(); + if second_binding_name.ident.name == binding_name; + then { + Some((field_ident, assign_rhs)) + } else { + None + } + } +} + +/// Returns whether `expr` is the update syntax base: `Foo { a: 1, .. base }` +fn is_update_syntax_base<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool { + if_chain! { + if let Some(parent) = get_parent_expr(cx, expr); + if let ExprKind::Struct(_, _, Some(base)) = parent.kind; + then { + base.hir_id == expr.hir_id + } else { + false + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/default_instead_of_iter_empty.rs b/src/tools/clippy/clippy_lints/src/default_instead_of_iter_empty.rs new file mode 100644 index 000000000..3c996d3d2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/default_instead_of_iter_empty.rs @@ -0,0 +1,68 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::last_path_segment; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{match_def_path, paths}; +use rustc_errors::Applicability; +use rustc_hir::{def, Expr, ExprKind, GenericArg, QPath, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// It checks for `std::iter::Empty::default()` and suggests replacing it with + /// `std::iter::empty()`. + /// ### Why is this bad? + /// `std::iter::empty()` is the more idiomatic way. + /// ### Example + /// ```rust + /// let _ = std::iter::Empty::<usize>::default(); + /// let iter: std::iter::Empty<usize> = std::iter::Empty::default(); + /// ``` + /// Use instead: + /// ```rust + /// let _ = std::iter::empty::<usize>(); + /// let iter: std::iter::Empty<usize> = std::iter::empty(); + /// ``` + #[clippy::version = "1.63.0"] + pub DEFAULT_INSTEAD_OF_ITER_EMPTY, + style, + "check `std::iter::Empty::default()` and replace with `std::iter::empty()`" +} +declare_lint_pass!(DefaultIterEmpty => [DEFAULT_INSTEAD_OF_ITER_EMPTY]); + +impl<'tcx> LateLintPass<'tcx> for DefaultIterEmpty { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Call(iter_expr, []) = &expr.kind + && let ExprKind::Path(QPath::TypeRelative(ty, _)) = &iter_expr.kind + && let TyKind::Path(ty_path) = &ty.kind + && let QPath::Resolved(None, path) = ty_path + && let def::Res::Def(_, def_id) = &path.res + && match_def_path(cx, *def_id, &paths::ITER_EMPTY) + { + let mut applicability = Applicability::MachineApplicable; + let sugg = make_sugg(cx, ty_path, &mut applicability); + span_lint_and_sugg( + cx, + DEFAULT_INSTEAD_OF_ITER_EMPTY, + expr.span, + "`std::iter::empty()` is the more idiomatic way", + "try", + sugg, + applicability, + ); + } + } +} + +fn make_sugg(cx: &LateContext<'_>, ty_path: &rustc_hir::QPath<'_>, applicability: &mut Applicability) -> String { + if let Some(last) = last_path_segment(ty_path).args + && let Some(iter_ty) = last.args.iter().find_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }) + { + format!("std::iter::empty::<{}>()", snippet_with_applicability(cx, iter_ty.span, "..", applicability)) + } else { + "std::iter::empty()".to_owned() + } +} diff --git a/src/tools/clippy/clippy_lints/src/default_numeric_fallback.rs b/src/tools/clippy/clippy_lints/src/default_numeric_fallback.rs new file mode 100644 index 000000000..fb418a325 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/default_numeric_fallback.rs @@ -0,0 +1,245 @@ +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::numeric_literal; +use clippy_utils::source::snippet_opt; +use if_chain::if_chain; +use rustc_ast::ast::{LitFloatType, LitIntType, LitKind}; +use rustc_errors::Applicability; +use rustc_hir::{ + intravisit::{walk_expr, walk_stmt, Visitor}, + Body, Expr, ExprKind, HirId, Lit, Stmt, StmtKind, +}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::{ + lint::in_external_macro, + ty::{self, FloatTy, IntTy, PolyFnSig, Ty}, +}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use std::iter; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of unconstrained numeric literals which may cause default numeric fallback in type + /// inference. + /// + /// Default numeric fallback means that if numeric types have not yet been bound to concrete + /// types at the end of type inference, then integer type is bound to `i32`, and similarly + /// floating type is bound to `f64`. + /// + /// See [RFC0212](https://github.com/rust-lang/rfcs/blob/master/text/0212-restore-int-fallback.md) for more information about the fallback. + /// + /// ### Why is this bad? + /// For those who are very careful about types, default numeric fallback + /// can be a pitfall that cause unexpected runtime behavior. + /// + /// ### Known problems + /// This lint can only be allowed at the function level or above. + /// + /// ### Example + /// ```rust + /// let i = 10; + /// let f = 1.23; + /// ``` + /// + /// Use instead: + /// ```rust + /// let i = 10i32; + /// let f = 1.23f64; + /// ``` + #[clippy::version = "1.52.0"] + pub DEFAULT_NUMERIC_FALLBACK, + restriction, + "usage of unconstrained numeric literals which may cause default numeric fallback." +} + +declare_lint_pass!(DefaultNumericFallback => [DEFAULT_NUMERIC_FALLBACK]); + +impl<'tcx> LateLintPass<'tcx> for DefaultNumericFallback { + fn check_body(&mut self, cx: &LateContext<'tcx>, body: &'tcx Body<'_>) { + let mut visitor = NumericFallbackVisitor::new(cx); + visitor.visit_body(body); + } +} + +struct NumericFallbackVisitor<'a, 'tcx> { + /// Stack manages type bound of exprs. The top element holds current expr type. + ty_bounds: Vec<TyBound<'tcx>>, + + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> NumericFallbackVisitor<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> Self { + Self { + ty_bounds: vec![TyBound::Nothing], + cx, + } + } + + /// Check whether a passed literal has potential to cause fallback or not. + fn check_lit(&self, lit: &Lit, lit_ty: Ty<'tcx>, emit_hir_id: HirId) { + if_chain! { + if !in_external_macro(self.cx.sess(), lit.span); + if let Some(ty_bound) = self.ty_bounds.last(); + if matches!(lit.node, + LitKind::Int(_, LitIntType::Unsuffixed) | LitKind::Float(_, LitFloatType::Unsuffixed)); + if !ty_bound.is_numeric(); + then { + let (suffix, is_float) = match lit_ty.kind() { + ty::Int(IntTy::I32) => ("i32", false), + ty::Float(FloatTy::F64) => ("f64", true), + // Default numeric fallback never results in other types. + _ => return, + }; + + let src = if let Some(src) = snippet_opt(self.cx, lit.span) { + src + } else { + match lit.node { + LitKind::Int(src, _) => format!("{}", src), + LitKind::Float(src, _) => format!("{}", src), + _ => return, + } + }; + let sugg = numeric_literal::format(&src, Some(suffix), is_float); + span_lint_hir_and_then( + self.cx, + DEFAULT_NUMERIC_FALLBACK, + emit_hir_id, + lit.span, + "default numeric fallback might occur", + |diag| { + diag.span_suggestion(lit.span, "consider adding suffix", sugg, Applicability::MaybeIncorrect); + } + ); + } + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for NumericFallbackVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + match &expr.kind { + ExprKind::Call(func, args) => { + if let Some(fn_sig) = fn_sig_opt(self.cx, func.hir_id) { + for (expr, bound) in iter::zip(*args, fn_sig.skip_binder().inputs()) { + // Push found arg type, then visit arg. + self.ty_bounds.push(TyBound::Ty(*bound)); + self.visit_expr(expr); + self.ty_bounds.pop(); + } + return; + } + }, + + ExprKind::MethodCall(_, args, _) => { + if let Some(def_id) = self.cx.typeck_results().type_dependent_def_id(expr.hir_id) { + let fn_sig = self.cx.tcx.fn_sig(def_id).skip_binder(); + for (expr, bound) in iter::zip(*args, fn_sig.inputs()) { + self.ty_bounds.push(TyBound::Ty(*bound)); + self.visit_expr(expr); + self.ty_bounds.pop(); + } + return; + } + }, + + ExprKind::Struct(_, fields, base) => { + let ty = self.cx.typeck_results().expr_ty(expr); + if_chain! { + if let Some(adt_def) = ty.ty_adt_def(); + if adt_def.is_struct(); + if let Some(variant) = adt_def.variants().iter().next(); + then { + let fields_def = &variant.fields; + + // Push field type then visit each field expr. + for field in fields.iter() { + let bound = + fields_def + .iter() + .find_map(|f_def| { + if f_def.ident(self.cx.tcx) == field.ident + { Some(self.cx.tcx.type_of(f_def.did)) } + else { None } + }); + self.ty_bounds.push(bound.into()); + self.visit_expr(field.expr); + self.ty_bounds.pop(); + } + + // Visit base with no bound. + if let Some(base) = base { + self.ty_bounds.push(TyBound::Nothing); + self.visit_expr(base); + self.ty_bounds.pop(); + } + return; + } + } + }, + + ExprKind::Lit(lit) => { + let ty = self.cx.typeck_results().expr_ty(expr); + self.check_lit(lit, ty, expr.hir_id); + return; + }, + + _ => {}, + } + + walk_expr(self, expr); + } + + fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) { + match stmt.kind { + StmtKind::Local(local) => { + if local.ty.is_some() { + self.ty_bounds.push(TyBound::Any); + } else { + self.ty_bounds.push(TyBound::Nothing); + } + }, + + _ => self.ty_bounds.push(TyBound::Nothing), + } + + walk_stmt(self, stmt); + self.ty_bounds.pop(); + } +} + +fn fn_sig_opt<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId) -> Option<PolyFnSig<'tcx>> { + let node_ty = cx.typeck_results().node_type_opt(hir_id)?; + // We can't use `Ty::fn_sig` because it automatically performs substs, this may result in FNs. + match node_ty.kind() { + ty::FnDef(def_id, _) => Some(cx.tcx.fn_sig(*def_id)), + ty::FnPtr(fn_sig) => Some(*fn_sig), + _ => None, + } +} + +#[derive(Debug, Clone, Copy)] +enum TyBound<'tcx> { + Any, + Ty(Ty<'tcx>), + Nothing, +} + +impl<'tcx> TyBound<'tcx> { + fn is_numeric(self) -> bool { + match self { + TyBound::Any => true, + TyBound::Ty(t) => t.is_numeric(), + TyBound::Nothing => false, + } + } +} + +impl<'tcx> From<Option<Ty<'tcx>>> for TyBound<'tcx> { + fn from(v: Option<Ty<'tcx>>) -> Self { + match v { + Some(t) => TyBound::Ty(t), + None => TyBound::Nothing, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/default_union_representation.rs b/src/tools/clippy/clippy_lints/src/default_union_representation.rs new file mode 100644 index 000000000..d559ad423 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/default_union_representation.rs @@ -0,0 +1,105 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{self as hir, HirId, Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::layout::LayoutOf; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; +use rustc_typeck::hir_ty_to_ty; + +declare_clippy_lint! { + /// ### What it does + /// Displays a warning when a union is declared with the default representation (without a `#[repr(C)]` attribute). + /// + /// ### Why is this bad? + /// Unions in Rust have unspecified layout by default, despite many people thinking that they + /// lay out each field at the start of the union (like C does). That is, there are no guarantees + /// about the offset of the fields for unions with multiple non-ZST fields without an explicitly + /// specified layout. These cases may lead to undefined behavior in unsafe blocks. + /// + /// ### Example + /// ```rust + /// union Foo { + /// a: i32, + /// b: u32, + /// } + /// + /// fn main() { + /// let _x: u32 = unsafe { + /// Foo { a: 0_i32 }.b // Undefined behavior: `b` is allowed to be padding + /// }; + /// } + /// ``` + /// Use instead: + /// ```rust + /// #[repr(C)] + /// union Foo { + /// a: i32, + /// b: u32, + /// } + /// + /// fn main() { + /// let _x: u32 = unsafe { + /// Foo { a: 0_i32 }.b // Now defined behavior, this is just an i32 -> u32 transmute + /// }; + /// } + /// ``` + #[clippy::version = "1.60.0"] + pub DEFAULT_UNION_REPRESENTATION, + restriction, + "unions without a `#[repr(C)]` attribute" +} +declare_lint_pass!(DefaultUnionRepresentation => [DEFAULT_UNION_REPRESENTATION]); + +impl<'tcx> LateLintPass<'tcx> for DefaultUnionRepresentation { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if is_union_with_two_non_zst_fields(cx, item) && !has_c_repr_attr(cx, item.hir_id()) { + span_lint_and_help( + cx, + DEFAULT_UNION_REPRESENTATION, + item.span, + "this union has the default representation", + None, + &format!( + "consider annotating `{}` with `#[repr(C)]` to explicitly specify memory layout", + cx.tcx.def_path_str(item.def_id.to_def_id()) + ), + ); + } + } +} + +/// Returns true if the given item is a union with at least two non-ZST fields. +fn is_union_with_two_non_zst_fields(cx: &LateContext<'_>, item: &Item<'_>) -> bool { + if let ItemKind::Union(data, _) = &item.kind { + data.fields().iter().filter(|f| !is_zst(cx, f.ty)).count() >= 2 + } else { + false + } +} + +fn is_zst(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>) -> bool { + if hir_ty.span.from_expansion() { + return false; + } + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + if let Ok(layout) = cx.layout_of(ty) { + layout.is_zst() + } else { + false + } +} + +fn has_c_repr_attr(cx: &LateContext<'_>, hir_id: HirId) -> bool { + cx.tcx.hir().attrs(hir_id).iter().any(|attr| { + if attr.has_name(sym::repr) { + if let Some(items) = attr.meta_item_list() { + for item in items { + if item.is_word() && matches!(item.name_or_empty(), sym::C) { + return true; + } + } + } + } + false + }) +} diff --git a/src/tools/clippy/clippy_lints/src/deprecated_lints.rs b/src/tools/clippy/clippy_lints/src/deprecated_lints.rs new file mode 100644 index 000000000..9aa5af319 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/deprecated_lints.rs @@ -0,0 +1,217 @@ +// NOTE: Entries should be created with `cargo dev deprecate` + +/// This struct fakes the `Lint` declaration that is usually created by `declare_lint!`. This +/// enables the simple extraction of the metadata without changing the current deprecation +/// declaration. +pub struct ClippyDeprecatedLint { + #[allow(dead_code)] + pub desc: &'static str, +} + +#[macro_export] +macro_rules! declare_deprecated_lint { + { $(#[$attr:meta])* pub $name: ident, $reason: literal} => { + $(#[$attr])* + #[allow(dead_code)] + pub static $name: ClippyDeprecatedLint = ClippyDeprecatedLint { + desc: $reason + }; + } +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This used to check for `assert!(a == b)` and recommend + /// replacement with `assert_eq!(a, b)`, but this is no longer needed after RFC 2011. + #[clippy::version = "pre 1.29.0"] + pub SHOULD_ASSERT_EQ, + "`assert!()` will be more flexible with RFC 2011" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This used to check for `Vec::extend`, which was slower than + /// `Vec::extend_from_slice`. Thanks to specialization, this is no longer true. + #[clippy::version = "pre 1.29.0"] + pub EXTEND_FROM_SLICE, + "`.extend_from_slice(_)` is a faster way to extend a Vec by a slice" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// `Range::step_by(0)` used to be linted since it's + /// an infinite iterator, which is better expressed by `iter::repeat`, + /// but the method has been removed for `Iterator::step_by` which panics + /// if given a zero + #[clippy::version = "pre 1.29.0"] + pub RANGE_STEP_BY_ZERO, + "`iterator.step_by(0)` panics nowadays" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This used to check for `Vec::as_slice`, which was unstable with good + /// stable alternatives. `Vec::as_slice` has now been stabilized. + #[clippy::version = "pre 1.29.0"] + pub UNSTABLE_AS_SLICE, + "`Vec::as_slice` has been stabilized in 1.7" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This used to check for `Vec::as_mut_slice`, which was unstable with good + /// stable alternatives. `Vec::as_mut_slice` has now been stabilized. + #[clippy::version = "pre 1.29.0"] + pub UNSTABLE_AS_MUT_SLICE, + "`Vec::as_mut_slice` has been stabilized in 1.7" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This lint should never have applied to non-pointer types, as transmuting + /// between non-pointer types of differing alignment is well-defined behavior (it's semantically + /// equivalent to a memcpy). This lint has thus been refactored into two separate lints: + /// cast_ptr_alignment and transmute_ptr_to_ptr. + #[clippy::version = "pre 1.29.0"] + pub MISALIGNED_TRANSMUTE, + "this lint has been split into cast_ptr_alignment and transmute_ptr_to_ptr" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This lint is too subjective, not having a good reason for being in clippy. + /// Additionally, compound assignment operators may be overloaded separately from their non-assigning + /// counterparts, so this lint may suggest a change in behavior or the code may not compile. + #[clippy::version = "1.30.0"] + pub ASSIGN_OPS, + "using compound assignment operators (e.g., `+=`) is harmless" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// The original rule will only lint for `if let`. After + /// making it support to lint `match`, naming as `if let` is not suitable for it. + /// So, this lint is deprecated. + #[clippy::version = "pre 1.29.0"] + pub IF_LET_REDUNDANT_PATTERN_MATCHING, + "this lint has been changed to redundant_pattern_matching" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This lint used to suggest replacing `let mut vec = + /// Vec::with_capacity(n); vec.set_len(n);` with `let vec = vec![0; n];`. The + /// replacement has very different performance characteristics so the lint is + /// deprecated. + #[clippy::version = "pre 1.29.0"] + pub UNSAFE_VECTOR_INITIALIZATION, + "the replacement suggested by this lint had substantially different behavior" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This lint has been superseded by #[must_use] in rustc. + #[clippy::version = "1.39.0"] + pub UNUSED_COLLECT, + "`collect` has been marked as #[must_use] in rustc and that covers all cases of this lint" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// Associated-constants are now preferred. + #[clippy::version = "1.44.0"] + pub REPLACE_CONSTS, + "associated-constants `MIN`/`MAX` of integers are preferred to `{min,max}_value()` and module constants" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// The regex! macro does not exist anymore. + #[clippy::version = "1.47.0"] + pub REGEX_MACRO, + "the regex! macro has been removed from the regex crate in 2018" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This lint has been replaced by `manual_find_map`, a + /// more specific lint. + #[clippy::version = "1.51.0"] + pub FIND_MAP, + "this lint has been replaced by `manual_find_map`, a more specific lint" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// This lint has been replaced by `manual_filter_map`, a + /// more specific lint. + #[clippy::version = "1.53.0"] + pub FILTER_MAP, + "this lint has been replaced by `manual_filter_map`, a more specific lint" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// The `avoid_breaking_exported_api` config option was added, which + /// enables the `enum_variant_names` lint for public items. + #[clippy::version = "1.54.0"] + pub PUB_ENUM_VARIANT_NAMES, + "set the `avoid-breaking-exported-api` config option to `false` to enable the `enum_variant_names` lint for public items" +} + +declare_deprecated_lint! { + /// ### What it does + /// Nothing. This lint has been deprecated. + /// + /// ### Deprecation reason + /// The `avoid_breaking_exported_api` config option was added, which + /// enables the `wrong_self_conversion` lint for public items. + #[clippy::version = "1.54.0"] + pub WRONG_PUB_SELF_CONVENTION, + "set the `avoid-breaking-exported-api` config option to `false` to enable the `wrong_self_convention` lint for public items" +} diff --git a/src/tools/clippy/clippy_lints/src/dereference.rs b/src/tools/clippy/clippy_lints/src/dereference.rs new file mode 100644 index 000000000..514661589 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/dereference.rs @@ -0,0 +1,1148 @@ +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then}; +use clippy_utils::source::{snippet_with_applicability, snippet_with_context}; +use clippy_utils::sugg::has_enclosing_paren; +use clippy_utils::ty::{expr_sig, peel_mid_ty_refs, variant_of_res}; +use clippy_utils::{get_parent_expr, is_lint_allowed, path_to_local, walk_to_expr_usage}; +use rustc_ast::util::parser::{PREC_POSTFIX, PREC_PREFIX}; +use rustc_data_structures::fx::FxIndexMap; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_ty, Visitor}; +use rustc_hir::{ + self as hir, BindingAnnotation, Body, BodyId, BorrowKind, Closure, Expr, ExprKind, FnRetTy, GenericArg, HirId, + ImplItem, ImplItemKind, Item, ItemKind, Local, MatchSource, Mutability, Node, Pat, PatKind, Path, QPath, TraitItem, + TraitItemKind, TyKind, UnOp, +}; +use rustc_infer::infer::TyCtxtInferExt; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow, AutoBorrowMutability}; +use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitable, TypeckResults}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{symbol::sym, Span, Symbol}; +use rustc_trait_selection::infer::InferCtxtExt; + +declare_clippy_lint! { + /// ### What it does + /// Checks for explicit `deref()` or `deref_mut()` method calls. + /// + /// ### Why is this bad? + /// Dereferencing by `&*x` or `&mut *x` is clearer and more concise, + /// when not part of a method chain. + /// + /// ### Example + /// ```rust + /// use std::ops::Deref; + /// let a: &mut String = &mut String::from("foo"); + /// let b: &str = a.deref(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let a: &mut String = &mut String::from("foo"); + /// let b = &*a; + /// ``` + /// + /// This lint excludes: + /// ```rust,ignore + /// let _ = d.unwrap().deref(); + /// ``` + #[clippy::version = "1.44.0"] + pub EXPLICIT_DEREF_METHODS, + pedantic, + "Explicit use of deref or deref_mut method while not in a method chain." +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for address of operations (`&`) that are going to + /// be dereferenced immediately by the compiler. + /// + /// ### Why is this bad? + /// Suggests that the receiver of the expression borrows + /// the expression. + /// + /// ### Example + /// ```rust + /// fn fun(_a: &i32) {} + /// + /// let x: &i32 = &&&&&&5; + /// fun(&x); + /// ``` + /// + /// Use instead: + /// ```rust + /// # fn fun(_a: &i32) {} + /// let x: &i32 = &5; + /// fun(x); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_BORROW, + style, + "taking a reference that is going to be automatically dereferenced" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `ref` bindings which create a reference to a reference. + /// + /// ### Why is this bad? + /// The address-of operator at the use site is clearer about the need for a reference. + /// + /// ### Example + /// ```rust + /// let x = Some(""); + /// if let Some(ref x) = x { + /// // use `x` here + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// let x = Some(""); + /// if let Some(x) = x { + /// // use `&x` here + /// } + /// ``` + #[clippy::version = "1.54.0"] + pub REF_BINDING_TO_REFERENCE, + pedantic, + "`ref` binding to a reference" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for dereferencing expressions which would be covered by auto-deref. + /// + /// ### Why is this bad? + /// This unnecessarily complicates the code. + /// + /// ### Example + /// ```rust + /// let x = String::new(); + /// let y: &str = &*x; + /// ``` + /// Use instead: + /// ```rust + /// let x = String::new(); + /// let y: &str = &x; + /// ``` + #[clippy::version = "1.60.0"] + pub EXPLICIT_AUTO_DEREF, + nursery, + "dereferencing when the compiler would automatically dereference" +} + +impl_lint_pass!(Dereferencing => [ + EXPLICIT_DEREF_METHODS, + NEEDLESS_BORROW, + REF_BINDING_TO_REFERENCE, + EXPLICIT_AUTO_DEREF, +]); + +#[derive(Default)] +pub struct Dereferencing { + state: Option<(State, StateData)>, + + // While parsing a `deref` method call in ufcs form, the path to the function is itself an + // expression. This is to store the id of that expression so it can be skipped when + // `check_expr` is called for it. + skip_expr: Option<HirId>, + + /// The body the first local was found in. Used to emit lints when the traversal of the body has + /// been finished. Note we can't lint at the end of every body as they can be nested within each + /// other. + current_body: Option<BodyId>, + /// The list of locals currently being checked by the lint. + /// If the value is `None`, then the binding has been seen as a ref pattern, but is not linted. + /// This is needed for or patterns where one of the branches can be linted, but another can not + /// be. + /// + /// e.g. `m!(x) | Foo::Bar(ref x)` + ref_locals: FxIndexMap<HirId, Option<RefPat>>, +} + +struct StateData { + /// Span of the top level expression + span: Span, + hir_id: HirId, + position: Position, +} + +struct DerefedBorrow { + count: usize, + msg: &'static str, +} + +enum State { + // Any number of deref method calls. + DerefMethod { + // The number of calls in a sequence which changed the referenced type + ty_changed_count: usize, + is_final_ufcs: bool, + /// The required mutability + target_mut: Mutability, + }, + DerefedBorrow(DerefedBorrow), + ExplicitDeref { + // Span and id of the top-level deref expression if the parent expression is a borrow. + deref_span_id: Option<(Span, HirId)>, + }, + ExplicitDerefField { + name: Symbol, + }, + Reborrow { + deref_span: Span, + deref_hir_id: HirId, + }, + Borrow, +} + +// A reference operation considered by this lint pass +enum RefOp { + Method(Mutability), + Deref, + AddrOf, +} + +struct RefPat { + /// Whether every usage of the binding is dereferenced. + always_deref: bool, + /// The spans of all the ref bindings for this local. + spans: Vec<Span>, + /// The applicability of this suggestion. + app: Applicability, + /// All the replacements which need to be made. + replacements: Vec<(Span, String)>, + /// The [`HirId`] that the lint should be emitted at. + hir_id: HirId, +} + +impl<'tcx> LateLintPass<'tcx> for Dereferencing { + #[expect(clippy::too_many_lines)] + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // Skip path expressions from deref calls. e.g. `Deref::deref(e)` + if Some(expr.hir_id) == self.skip_expr.take() { + return; + } + + if let Some(local) = path_to_local(expr) { + self.check_local_usage(cx, expr, local); + } + + // Stop processing sub expressions when a macro call is seen + if expr.span.from_expansion() { + if let Some((state, data)) = self.state.take() { + report(cx, expr, state, data); + } + return; + } + + let typeck = cx.typeck_results(); + let (kind, sub_expr) = if let Some(x) = try_parse_ref_op(cx.tcx, typeck, expr) { + x + } else { + // The whole chain of reference operations has been seen + if let Some((state, data)) = self.state.take() { + report(cx, expr, state, data); + } + return; + }; + + match (self.state.take(), kind) { + (None, kind) => { + let expr_ty = typeck.expr_ty(expr); + let (position, adjustments) = walk_parents(cx, expr); + + match kind { + RefOp::Deref => { + if let Position::FieldAccess(name) = position + && !ty_contains_field(typeck.expr_ty(sub_expr), name) + { + self.state = Some(( + State::ExplicitDerefField { name }, + StateData { span: expr.span, hir_id: expr.hir_id, position }, + )); + } else if position.is_deref_stable() { + self.state = Some(( + State::ExplicitDeref { deref_span_id: None }, + StateData { span: expr.span, hir_id: expr.hir_id, position }, + )); + } + } + RefOp::Method(target_mut) + if !is_lint_allowed(cx, EXPLICIT_DEREF_METHODS, expr.hir_id) + && position.lint_explicit_deref() => + { + self.state = Some(( + State::DerefMethod { + ty_changed_count: if deref_method_same_type(expr_ty, typeck.expr_ty(sub_expr)) { + 0 + } else { + 1 + }, + is_final_ufcs: matches!(expr.kind, ExprKind::Call(..)), + target_mut, + }, + StateData { + span: expr.span, + hir_id: expr.hir_id, + position + }, + )); + }, + RefOp::AddrOf => { + // Find the number of times the borrow is auto-derefed. + let mut iter = adjustments.iter(); + let mut deref_count = 0usize; + let next_adjust = loop { + match iter.next() { + Some(adjust) => { + if !matches!(adjust.kind, Adjust::Deref(_)) { + break Some(adjust); + } else if !adjust.target.is_ref() { + deref_count += 1; + break iter.next(); + } + deref_count += 1; + }, + None => break None, + }; + }; + + // Determine the required number of references before any can be removed. In all cases the + // reference made by the current expression will be removed. After that there are four cases to + // handle. + // + // 1. Auto-borrow will trigger in the current position, so no further references are required. + // 2. Auto-deref ends at a reference, or the underlying type, so one extra needs to be left to + // handle the automatically inserted re-borrow. + // 3. Auto-deref hits a user-defined `Deref` impl, so at least one reference needs to exist to + // 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 + // }; + // } + let deref_msg = + "this expression creates a reference which is immediately dereferenced by the compiler"; + let borrow_msg = "this expression borrows a value the compiler would automatically borrow"; + + let (required_refs, msg) = if position.can_auto_borrow() { + (1, if deref_count == 1 { borrow_msg } else { deref_msg }) + } else if let Some(&Adjust::Borrow(AutoBorrow::Ref(_, mutability))) = + next_adjust.map(|a| &a.kind) + { + if matches!(mutability, AutoBorrowMutability::Mut { .. }) && !position.is_reborrow_stable() + { + (3, deref_msg) + } else { + (2, deref_msg) + } + } else { + (2, deref_msg) + }; + + if deref_count >= required_refs { + self.state = Some(( + State::DerefedBorrow(DerefedBorrow { + // One of the required refs is for the current borrow expression, the remaining ones + // can't be removed without breaking the code. See earlier comment. + count: deref_count - required_refs, + msg, + }), + StateData { span: expr.span, hir_id: expr.hir_id, position }, + )); + } else if position.is_deref_stable() { + self.state = Some(( + State::Borrow, + StateData { + span: expr.span, + hir_id: expr.hir_id, + position + }, + )); + } + }, + RefOp::Method(..) => (), + } + }, + ( + Some(( + State::DerefMethod { + target_mut, + ty_changed_count, + .. + }, + data, + )), + RefOp::Method(_), + ) => { + self.state = Some(( + State::DerefMethod { + ty_changed_count: if deref_method_same_type(typeck.expr_ty(expr), typeck.expr_ty(sub_expr)) { + ty_changed_count + } else { + ty_changed_count + 1 + }, + is_final_ufcs: matches!(expr.kind, ExprKind::Call(..)), + target_mut, + }, + data, + )); + }, + (Some((State::DerefedBorrow(state), data)), RefOp::AddrOf) if state.count != 0 => { + self.state = Some(( + State::DerefedBorrow(DerefedBorrow { + count: state.count - 1, + ..state + }), + data, + )); + }, + (Some((State::DerefedBorrow(state), data)), RefOp::AddrOf) => { + let position = data.position; + report(cx, expr, State::DerefedBorrow(state), data); + if position.is_deref_stable() { + self.state = Some(( + State::Borrow, + StateData { + span: expr.span, + hir_id: expr.hir_id, + position, + }, + )); + } + }, + (Some((State::DerefedBorrow(state), data)), RefOp::Deref) => { + let position = data.position; + report(cx, expr, State::DerefedBorrow(state), data); + if let Position::FieldAccess(name) = position + && !ty_contains_field(typeck.expr_ty(sub_expr), name) + { + self.state = Some(( + State::ExplicitDerefField { name }, + StateData { span: expr.span, hir_id: expr.hir_id, position }, + )); + } else if position.is_deref_stable() { + self.state = Some(( + State::ExplicitDeref { deref_span_id: None }, + StateData { span: expr.span, hir_id: expr.hir_id, position }, + )); + } + }, + + (Some((State::Borrow, data)), RefOp::Deref) => { + if typeck.expr_ty(sub_expr).is_ref() { + self.state = Some(( + State::Reborrow { + deref_span: expr.span, + deref_hir_id: expr.hir_id, + }, + data, + )); + } else { + self.state = Some(( + State::ExplicitDeref { + deref_span_id: Some((expr.span, expr.hir_id)), + }, + data, + )); + } + }, + ( + Some(( + State::Reborrow { + deref_span, + deref_hir_id, + }, + data, + )), + RefOp::Deref, + ) => { + self.state = Some(( + State::ExplicitDeref { + deref_span_id: Some((deref_span, deref_hir_id)), + }, + data, + )); + }, + (state @ Some((State::ExplicitDeref { .. }, _)), RefOp::Deref) => { + self.state = state; + }, + (Some((State::ExplicitDerefField { name }, data)), RefOp::Deref) + if !ty_contains_field(typeck.expr_ty(sub_expr), name) => + { + self.state = Some((State::ExplicitDerefField { name }, data)); + }, + + (Some((state, data)), _) => report(cx, expr, state, data), + } + } + + fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) { + if let PatKind::Binding(BindingAnnotation::Ref, id, name, _) = pat.kind { + if let Some(opt_prev_pat) = self.ref_locals.get_mut(&id) { + // This binding id has been seen before. Add this pattern to the list of changes. + if let Some(prev_pat) = opt_prev_pat { + if pat.span.from_expansion() { + // Doesn't match the context of the previous pattern. Can't lint here. + *opt_prev_pat = None; + } else { + prev_pat.spans.push(pat.span); + prev_pat.replacements.push(( + pat.span, + snippet_with_context(cx, name.span, pat.span.ctxt(), "..", &mut prev_pat.app) + .0 + .into(), + )); + } + } + return; + } + + if_chain! { + if !pat.span.from_expansion(); + if let ty::Ref(_, tam, _) = *cx.typeck_results().pat_ty(pat).kind(); + // only lint immutable refs, because borrowed `&mut T` cannot be moved out + if let ty::Ref(_, _, Mutability::Not) = *tam.kind(); + then { + let mut app = Applicability::MachineApplicable; + let snip = snippet_with_context(cx, name.span, pat.span.ctxt(), "..", &mut app).0; + self.current_body = self.current_body.or(cx.enclosing_body); + self.ref_locals.insert( + id, + Some(RefPat { + always_deref: true, + spans: vec![pat.span], + app, + replacements: vec![(pat.span, snip.into())], + hir_id: pat.hir_id + }), + ); + } + } + } + } + + fn check_body_post(&mut self, cx: &LateContext<'tcx>, body: &'tcx Body<'_>) { + if Some(body.id()) == self.current_body { + for pat in self.ref_locals.drain(..).filter_map(|(_, x)| x) { + let replacements = pat.replacements; + let app = pat.app; + let lint = if pat.always_deref { + NEEDLESS_BORROW + } else { + REF_BINDING_TO_REFERENCE + }; + span_lint_hir_and_then( + cx, + lint, + pat.hir_id, + pat.spans, + "this pattern creates a reference to a reference", + |diag| { + diag.multipart_suggestion("try this", replacements, app); + }, + ); + } + self.current_body = None; + } + } +} + +fn try_parse_ref_op<'tcx>( + tcx: TyCtxt<'tcx>, + typeck: &'tcx TypeckResults<'_>, + expr: &'tcx Expr<'_>, +) -> Option<(RefOp, &'tcx Expr<'tcx>)> { + let (def_id, arg) = match expr.kind { + ExprKind::MethodCall(_, [arg], _) => (typeck.type_dependent_def_id(expr.hir_id)?, arg), + ExprKind::Call( + Expr { + kind: ExprKind::Path(path), + hir_id, + .. + }, + [arg], + ) => (typeck.qpath_res(path, *hir_id).opt_def_id()?, arg), + ExprKind::Unary(UnOp::Deref, sub_expr) if !typeck.expr_ty(sub_expr).is_unsafe_ptr() => { + return Some((RefOp::Deref, sub_expr)); + }, + ExprKind::AddrOf(BorrowKind::Ref, _, sub_expr) => return Some((RefOp::AddrOf, sub_expr)), + _ => return None, + }; + if tcx.is_diagnostic_item(sym::deref_method, def_id) { + Some((RefOp::Method(Mutability::Not), arg)) + } else if tcx.trait_of_item(def_id)? == tcx.lang_items().deref_mut_trait()? { + Some((RefOp::Method(Mutability::Mut), arg)) + } else { + None + } +} + +// Checks whether the type for a deref call actually changed the type, not just the mutability of +// the reference. +fn deref_method_same_type<'tcx>(result_ty: Ty<'tcx>, arg_ty: Ty<'tcx>) -> bool { + match (result_ty.kind(), arg_ty.kind()) { + (ty::Ref(_, result_ty, _), ty::Ref(_, arg_ty, _)) => result_ty == arg_ty, + + // The result type for a deref method is always a reference + // Not matching the previous pattern means the argument type is not a reference + // This means that the type did change + _ => false, + } +} + +/// The position of an expression relative to it's parent. +#[derive(Clone, Copy)] +enum Position { + MethodReceiver, + /// The method is defined on a reference type. e.g. `impl Foo for &T` + MethodReceiverRefImpl, + Callee, + FieldAccess(Symbol), + Postfix, + Deref, + /// Any other location which will trigger auto-deref to a specific time. + DerefStable(i8), + /// Any other location which will trigger auto-reborrowing. + ReborrowStable(i8), + Other(i8), +} +impl Position { + fn is_deref_stable(self) -> bool { + matches!(self, Self::DerefStable(_)) + } + + fn is_reborrow_stable(self) -> bool { + matches!(self, Self::DerefStable(_) | Self::ReborrowStable(_)) + } + + fn can_auto_borrow(self) -> bool { + matches!(self, Self::MethodReceiver | Self::FieldAccess(_) | Self::Callee) + } + + fn lint_explicit_deref(self) -> bool { + matches!(self, Self::Other(_) | Self::DerefStable(_) | Self::ReborrowStable(_)) + } + + fn precedence(self) -> i8 { + match self { + Self::MethodReceiver + | Self::MethodReceiverRefImpl + | Self::Callee + | Self::FieldAccess(_) + | Self::Postfix => PREC_POSTFIX, + Self::Deref => PREC_PREFIX, + Self::DerefStable(p) | Self::ReborrowStable(p) | Self::Other(p) => p, + } + } +} + +/// Walks up the parent expressions attempting to determine both how stable the auto-deref result +/// is, and which adjustments will be applied to it. Note this will not consider auto-borrow +/// locations as those follow different rules. +#[allow(clippy::too_many_lines)] +fn walk_parents<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> (Position, &'tcx [Adjustment<'tcx>]) { + let mut adjustments = [].as_slice(); + let mut precedence = 0i8; + let ctxt = e.span.ctxt(); + let position = walk_to_expr_usage(cx, e, &mut |parent, child_id| { + // LocalTableInContext returns the wrong lifetime, so go use `expr_adjustments` instead. + if adjustments.is_empty() && let Node::Expr(e) = cx.tcx.hir().get(child_id) { + adjustments = cx.typeck_results().expr_adjustments(e); + } + match parent { + Node::Local(Local { ty: Some(ty), span, .. }) if span.ctxt() == ctxt => { + Some(binding_ty_auto_deref_stability(ty, precedence)) + }, + Node::Item(&Item { + kind: ItemKind::Static(..) | ItemKind::Const(..), + def_id, + span, + .. + }) + | Node::TraitItem(&TraitItem { + kind: TraitItemKind::Const(..), + def_id, + span, + .. + }) + | Node::ImplItem(&ImplItem { + kind: ImplItemKind::Const(..), + def_id, + span, + .. + }) if span.ctxt() == ctxt => { + let ty = cx.tcx.type_of(def_id); + Some(if ty.is_ref() { + Position::DerefStable(precedence) + } else { + Position::Other(precedence) + }) + }, + + Node::Item(&Item { + kind: ItemKind::Fn(..), + def_id, + span, + .. + }) + | Node::TraitItem(&TraitItem { + kind: TraitItemKind::Fn(..), + def_id, + span, + .. + }) + | Node::ImplItem(&ImplItem { + kind: ImplItemKind::Fn(..), + def_id, + span, + .. + }) if span.ctxt() == ctxt => { + let output = cx.tcx.fn_sig(def_id.to_def_id()).skip_binder().output(); + Some(if !output.is_ref() { + Position::Other(precedence) + } else if output.has_placeholders() || output.has_opaque_types() { + Position::ReborrowStable(precedence) + } else { + Position::DerefStable(precedence) + }) + }, + + Node::Expr(parent) if parent.span.ctxt() == ctxt => match parent.kind { + ExprKind::Ret(_) => { + let owner_id = cx.tcx.hir().body_owner(cx.enclosing_body.unwrap()); + Some( + if let Node::Expr(Expr { + kind: ExprKind::Closure(&Closure { fn_decl, .. }), + .. + }) = cx.tcx.hir().get(owner_id) + { + match fn_decl.output { + FnRetTy::Return(ty) => binding_ty_auto_deref_stability(ty, precedence), + FnRetTy::DefaultReturn(_) => Position::Other(precedence), + } + } else { + let output = cx + .tcx + .fn_sig(cx.tcx.hir().local_def_id(owner_id)) + .skip_binder() + .output(); + if !output.is_ref() { + Position::Other(precedence) + } else if output.has_placeholders() || output.has_opaque_types() { + Position::ReborrowStable(precedence) + } else { + Position::DerefStable(precedence) + } + }, + ) + }, + ExprKind::Call(func, _) if func.hir_id == child_id => { + (child_id == e.hir_id).then_some(Position::Callee) + }, + ExprKind::Call(func, args) => args + .iter() + .position(|arg| arg.hir_id == child_id) + .zip(expr_sig(cx, func)) + .and_then(|(i, sig)| sig.input_with_hir(i)) + .map(|(hir_ty, ty)| match hir_ty { + // Type inference for closures can depend on how they're called. Only go by the explicit + // types here. + Some(ty) => binding_ty_auto_deref_stability(ty, precedence), + None => param_auto_deref_stability(ty.skip_binder(), precedence), + }), + ExprKind::MethodCall(_, args, _) => { + let id = cx.typeck_results().type_dependent_def_id(parent.hir_id).unwrap(); + args.iter().position(|arg| arg.hir_id == child_id).map(|i| { + if i == 0 { + // Check for calls to trait methods where the trait is implemented on a reference. + // Two cases need to be handled: + // * `self` methods on `&T` will never have auto-borrow + // * `&self` methods on `&T` can have auto-borrow, but `&self` methods on `T` will take + // priority. + if e.hir_id != child_id { + Position::ReborrowStable(precedence) + } else if let Some(trait_id) = cx.tcx.trait_of_item(id) + && let arg_ty = cx.tcx.erase_regions(cx.typeck_results().expr_ty_adjusted(e)) + && let ty::Ref(_, sub_ty, _) = *arg_ty.kind() + && let subs = match cx + .typeck_results() + .node_substs_opt(parent.hir_id) + .and_then(|subs| subs.get(1..)) + { + Some(subs) => cx.tcx.mk_substs(subs.iter().copied()), + None => cx.tcx.mk_substs([].iter()), + } && let impl_ty = if cx.tcx.fn_sig(id).skip_binder().inputs()[0].is_ref() { + // Trait methods taking `&self` + sub_ty + } else { + // Trait methods taking `self` + arg_ty + } && impl_ty.is_ref() + && cx.tcx.infer_ctxt().enter(|infcx| + infcx + .type_implements_trait(trait_id, impl_ty, subs, cx.param_env) + .must_apply_modulo_regions() + ) + { + Position::MethodReceiverRefImpl + } else { + Position::MethodReceiver + } + } else { + param_auto_deref_stability(cx.tcx.fn_sig(id).skip_binder().inputs()[i], precedence) + } + }) + }, + ExprKind::Struct(path, fields, _) => { + let variant = variant_of_res(cx, cx.qpath_res(path, parent.hir_id)); + fields + .iter() + .find(|f| f.expr.hir_id == child_id) + .zip(variant) + .and_then(|(field, variant)| variant.fields.iter().find(|f| f.name == field.ident.name)) + .map(|field| param_auto_deref_stability(cx.tcx.type_of(field.did), precedence)) + }, + ExprKind::Field(child, name) if child.hir_id == e.hir_id => Some(Position::FieldAccess(name.name)), + ExprKind::Unary(UnOp::Deref, child) if child.hir_id == e.hir_id => Some(Position::Deref), + ExprKind::Match(child, _, MatchSource::TryDesugar | MatchSource::AwaitDesugar) + | ExprKind::Index(child, _) + if child.hir_id == e.hir_id => + { + Some(Position::Postfix) + }, + _ if child_id == e.hir_id => { + precedence = parent.precedence().order(); + None + }, + _ => None, + }, + _ => None, + } + }) + .unwrap_or(Position::Other(precedence)); + (position, adjustments) +} + +// Checks the stability of auto-deref when assigned to a binding with the given explicit type. +// +// e.g. +// let x = Box::new(Box::new(0u32)); +// let y1: &Box<_> = x.deref(); +// let y2: &Box<_> = &x; +// +// Here `y1` and `y2` would resolve to different types, so the type `&Box<_>` is not stable when +// switching to auto-dereferencing. +fn binding_ty_auto_deref_stability(ty: &hir::Ty<'_>, precedence: i8) -> Position { + let TyKind::Rptr(_, ty) = &ty.kind else { + return Position::Other(precedence); + }; + let mut ty = ty; + + loop { + break match ty.ty.kind { + TyKind::Rptr(_, ref ref_ty) => { + ty = ref_ty; + continue; + }, + TyKind::Path( + QPath::TypeRelative(_, path) + | QPath::Resolved( + _, + Path { + segments: [.., path], .. + }, + ), + ) => { + if let Some(args) = path.args + && args.args.iter().any(|arg| match arg { + GenericArg::Infer(_) => true, + GenericArg::Type(ty) => ty_contains_infer(ty), + _ => false, + }) + { + Position::ReborrowStable(precedence) + } else { + Position::DerefStable(precedence) + } + }, + TyKind::Slice(_) + | TyKind::Array(..) + | TyKind::BareFn(_) + | TyKind::Never + | TyKind::Tup(_) + | TyKind::Ptr(_) + | TyKind::TraitObject(..) + | TyKind::Path(_) => Position::DerefStable(precedence), + TyKind::OpaqueDef(..) + | TyKind::Infer + | TyKind::Typeof(..) + | TyKind::Err => Position::ReborrowStable(precedence), + }; + } +} + +// Checks whether a type is inferred at some point. +// e.g. `_`, `Box<_>`, `[_]` +fn ty_contains_infer(ty: &hir::Ty<'_>) -> bool { + struct V(bool); + impl Visitor<'_> for V { + fn visit_ty(&mut self, ty: &hir::Ty<'_>) { + if self.0 + || matches!( + ty.kind, + TyKind::OpaqueDef(..) | TyKind::Infer | TyKind::Typeof(_) | TyKind::Err + ) + { + self.0 = true; + } else { + walk_ty(self, ty); + } + } + + fn visit_generic_arg(&mut self, arg: &GenericArg<'_>) { + if self.0 || matches!(arg, GenericArg::Infer(_)) { + self.0 = true; + } else if let GenericArg::Type(ty) = arg { + self.visit_ty(ty); + } + } + } + let mut v = V(false); + v.visit_ty(ty); + v.0 +} + +// Checks whether a type is stable when switching to auto dereferencing, +fn param_auto_deref_stability(ty: Ty<'_>, precedence: i8) -> Position { + let ty::Ref(_, mut ty, _) = *ty.kind() else { + return Position::Other(precedence); + }; + + loop { + break match *ty.kind() { + ty::Ref(_, ref_ty, _) => { + ty = ref_ty; + continue; + }, + ty::Infer(_) + | ty::Error(_) + | ty::Param(_) + | ty::Bound(..) + | ty::Opaque(..) + | ty::Placeholder(_) + | ty::Dynamic(..) => Position::ReborrowStable(precedence), + ty::Adt(..) if ty.has_placeholders() || ty.has_param_types_or_consts() => { + Position::ReborrowStable(precedence) + }, + ty::Adt(..) + | ty::Bool + | ty::Char + | ty::Int(_) + | ty::Uint(_) + | ty::Float(_) + | ty::Foreign(_) + | ty::Str + | ty::Array(..) + | ty::Slice(..) + | ty::RawPtr(..) + | ty::FnDef(..) + | ty::FnPtr(_) + | ty::Closure(..) + | ty::Generator(..) + | ty::GeneratorWitness(..) + | ty::Never + | ty::Tuple(_) + | ty::Projection(_) => Position::DerefStable(precedence), + }; + } +} + +fn ty_contains_field(ty: Ty<'_>, name: Symbol) -> bool { + if let ty::Adt(adt, _) = *ty.kind() { + adt.is_struct() && adt.all_fields().any(|f| f.name == name) + } else { + false + } +} + +#[expect(clippy::needless_pass_by_value, clippy::too_many_lines)] +fn report<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, state: State, data: StateData) { + match state { + State::DerefMethod { + ty_changed_count, + is_final_ufcs, + 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 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 { + // a deref call changing &T -> &U requires two deref operators the first time + // this occurs. One to remove the reference, a second to call the deref impl. + "*".repeat(ty_changed_count + 1) + } else { + "*".repeat(ty_changed_count) + }; + let addr_of_str = if ty_changed_count < ref_count { + // Check if a reborrow from &mut T -> &T is required. + if target_mut == Mutability::Not && matches!(ty.kind(), ty::Ref(_, _, Mutability::Mut)) { + "&*" + } else { + "" + } + } else if target_mut == Mutability::Mut { + "&mut " + } else { + "&" + }; + + let expr_str = if !expr_is_macro_call && is_final_ufcs && expr.precedence().order() < PREC_PREFIX { + format!("({})", expr_str) + } else { + expr_str.into_owned() + }; + + span_lint_and_sugg( + cx, + EXPLICIT_DEREF_METHODS, + data.span, + match target_mut { + Mutability::Not => "explicit `deref` method call", + Mutability::Mut => "explicit `deref_mut` method call", + }, + "try this", + format!("{}{}{}", addr_of_str, deref_str, expr_str), + app, + ); + }, + State::DerefedBorrow(state) => { + let mut app = Applicability::MachineApplicable; + let (snip, snip_is_macro) = snippet_with_context(cx, expr.span, data.span.ctxt(), "..", &mut app); + span_lint_hir_and_then(cx, NEEDLESS_BORROW, data.hir_id, data.span, state.msg, |diag| { + let calls_field = matches!(expr.kind, ExprKind::Field(..)) && matches!(data.position, Position::Callee); + let sugg = if !snip_is_macro + && !has_enclosing_paren(&snip) + && (expr.precedence().order() < data.position.precedence() || calls_field) + { + format!("({})", snip) + } else { + snip.into() + }; + diag.span_suggestion(data.span, "change this to", sugg, app); + }); + }, + State::ExplicitDeref { deref_span_id } => { + let (span, hir_id, precedence) = if let Some((span, hir_id)) = deref_span_id + && !cx.typeck_results().expr_ty(expr).is_ref() + { + (span, hir_id, PREC_PREFIX) + } else { + (data.span, data.hir_id, data.position.precedence()) + }; + span_lint_hir_and_then( + cx, + EXPLICIT_AUTO_DEREF, + hir_id, + span, + "deref which would be done by auto-deref", + |diag| { + let mut app = Applicability::MachineApplicable; + let (snip, snip_is_macro) = snippet_with_context(cx, expr.span, span.ctxt(), "..", &mut app); + let sugg = + if !snip_is_macro && expr.precedence().order() < precedence && !has_enclosing_paren(&snip) { + format!("({})", snip) + } else { + snip.into() + }; + diag.span_suggestion(span, "try this", sugg, app); + }, + ); + }, + State::ExplicitDerefField { .. } => { + span_lint_hir_and_then( + cx, + EXPLICIT_AUTO_DEREF, + data.hir_id, + data.span, + "deref which would be done by auto-deref", + |diag| { + let mut app = Applicability::MachineApplicable; + let snip = snippet_with_context(cx, expr.span, data.span.ctxt(), "..", &mut app).0; + diag.span_suggestion(data.span, "try this", snip.into_owned(), app); + }, + ); + }, + State::Borrow | State::Reborrow { .. } => (), + } +} + +impl Dereferencing { + fn check_local_usage<'tcx>(&mut self, cx: &LateContext<'tcx>, e: &Expr<'tcx>, local: HirId) { + if let Some(outer_pat) = self.ref_locals.get_mut(&local) { + if let Some(pat) = outer_pat { + // Check for auto-deref + if !matches!( + cx.typeck_results().expr_adjustments(e), + [ + Adjustment { + kind: Adjust::Deref(_), + .. + }, + Adjustment { + kind: Adjust::Deref(_), + .. + }, + .. + ] + ) { + match get_parent_expr(cx, e) { + // Field accesses are the same no matter the number of references. + Some(Expr { + kind: ExprKind::Field(..), + .. + }) => (), + Some(&Expr { + span, + kind: ExprKind::Unary(UnOp::Deref, _), + .. + }) if !span.from_expansion() => { + // Remove explicit deref. + let snip = snippet_with_context(cx, e.span, span.ctxt(), "..", &mut pat.app).0; + pat.replacements.push((span, snip.into())); + }, + Some(parent) if !parent.span.from_expansion() => { + // Double reference might be needed at this point. + if parent.precedence().order() == PREC_POSTFIX { + // Parentheses would be needed here, don't lint. + *outer_pat = None; + } else { + pat.always_deref = false; + let snip = snippet_with_context(cx, e.span, parent.span.ctxt(), "..", &mut pat.app).0; + pat.replacements.push((e.span, format!("&{}", snip))); + } + }, + _ if !e.span.from_expansion() => { + // Double reference might be needed at this point. + pat.always_deref = false; + let snip = snippet_with_applicability(cx, e.span, "..", &mut pat.app); + pat.replacements.push((e.span, format!("&{}", snip))); + }, + // Edge case for macros. The span of the identifier will usually match the context of the + // binding, but not if the identifier was created in a macro. e.g. `concat_idents` and proc + // macros + _ => *outer_pat = None, + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/derivable_impls.rs b/src/tools/clippy/clippy_lints/src/derivable_impls.rs new file mode 100644 index 000000000..4d7f4076d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/derivable_impls.rs @@ -0,0 +1,117 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{is_default_equivalent, peel_blocks}; +use rustc_hir::{ + def::{DefKind, Res}, + Body, Expr, ExprKind, GenericArg, Impl, ImplItemKind, Item, ItemKind, Node, PathSegment, QPath, TyKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Detects manual `std::default::Default` implementations that are identical to a derived implementation. + /// + /// ### Why is this bad? + /// It is less concise. + /// + /// ### Example + /// ```rust + /// struct Foo { + /// bar: bool + /// } + /// + /// impl Default for Foo { + /// fn default() -> Self { + /// Self { + /// bar: false + /// } + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// #[derive(Default)] + /// struct Foo { + /// bar: bool + /// } + /// ``` + /// + /// ### Known problems + /// Derive macros [sometimes use incorrect bounds](https://github.com/rust-lang/rust/issues/26925) + /// in generic types and the user defined `impl` maybe is more generalized or + /// specialized than what derive will produce. This lint can't detect the manual `impl` + /// has exactly equal bounds, and therefore this lint is disabled for types with + /// generic parameters. + #[clippy::version = "1.57.0"] + pub DERIVABLE_IMPLS, + complexity, + "manual implementation of the `Default` trait which is equal to a derive" +} + +declare_lint_pass!(DerivableImpls => [DERIVABLE_IMPLS]); + +fn is_path_self(e: &Expr<'_>) -> bool { + if let ExprKind::Path(QPath::Resolved(_, p)) = e.kind { + matches!(p.res, Res::SelfCtor(..) | Res::Def(DefKind::Ctor(..), _)) + } else { + false + } +} + +impl<'tcx> LateLintPass<'tcx> for DerivableImpls { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if_chain! { + if let ItemKind::Impl(Impl { + of_trait: Some(ref trait_ref), + items: [child], + self_ty, + .. + }) = item.kind; + if !cx.tcx.has_attr(item.def_id.to_def_id(), sym::automatically_derived); + if !item.span.from_expansion(); + if let Some(def_id) = trait_ref.trait_def_id(); + if cx.tcx.is_diagnostic_item(sym::Default, def_id); + if let impl_item_hir = child.id.hir_id(); + if let Some(Node::ImplItem(impl_item)) = cx.tcx.hir().find(impl_item_hir); + if let ImplItemKind::Fn(_, b) = &impl_item.kind; + if let Body { value: func_expr, .. } = cx.tcx.hir().body(*b); + if let Some(adt_def) = cx.tcx.type_of(item.def_id).ty_adt_def(); + if let attrs = cx.tcx.hir().attrs(item.hir_id()); + if !attrs.iter().any(|attr| attr.doc_str().is_some()); + if let child_attrs = cx.tcx.hir().attrs(impl_item_hir); + if !child_attrs.iter().any(|attr| attr.doc_str().is_some()); + if adt_def.is_struct(); + then { + if let TyKind::Path(QPath::Resolved(_, p)) = self_ty.kind { + if let Some(PathSegment { args: Some(a), .. }) = p.segments.last() { + for arg in a.args { + if !matches!(arg, GenericArg::Lifetime(_)) { + return; + } + } + } + } + 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)), + _ => false, + }; + if should_emit { + let path_string = cx.tcx.def_path_str(adt_def.did()); + span_lint_and_help( + cx, + DERIVABLE_IMPLS, + item.span, + "this `impl` can be derived", + None, + &format!("try annotating `{}` with `#[derive(Default)]`", path_string), + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/derive.rs b/src/tools/clippy/clippy_lints/src/derive.rs new file mode 100644 index 000000000..a982990e4 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/derive.rs @@ -0,0 +1,528 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_note, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::paths; +use clippy_utils::ty::{implements_trait, implements_trait_with_env, is_copy}; +use clippy_utils::{is_lint_allowed, match_def_path}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::def_id::DefId; +use rustc_hir::intravisit::{walk_expr, walk_fn, walk_item, FnKind, Visitor}; +use rustc_hir::{ + self as hir, BlockCheckMode, BodyId, Constness, Expr, ExprKind, FnDecl, HirId, Impl, Item, ItemKind, UnsafeSource, + Unsafety, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter; +use rustc_middle::traits::Reveal; +use rustc_middle::ty::{ + self, Binder, BoundConstness, GenericParamDefKind, ImplPolarity, ParamEnv, PredicateKind, TraitPredicate, TraitRef, + Ty, TyCtxt, Visibility, +}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for deriving `Hash` but implementing `PartialEq` + /// explicitly or vice versa. + /// + /// ### Why is this bad? + /// The implementation of these traits must agree (for + /// example for use with `HashMap`) so it’s probably a bad idea to use a + /// default-generated `Hash` implementation with an explicitly defined + /// `PartialEq`. In particular, the following must hold for any type: + /// + /// ```text + /// k1 == k2 ⇒ hash(k1) == hash(k2) + /// ``` + /// + /// ### Example + /// ```ignore + /// #[derive(Hash)] + /// struct Foo; + /// + /// impl PartialEq for Foo { + /// ... + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DERIVE_HASH_XOR_EQ, + correctness, + "deriving `Hash` but implementing `PartialEq` explicitly" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for deriving `Ord` but implementing `PartialOrd` + /// explicitly or vice versa. + /// + /// ### Why is this bad? + /// The implementation of these traits must agree (for + /// example for use with `sort`) so it’s probably a bad idea to use a + /// default-generated `Ord` implementation with an explicitly defined + /// `PartialOrd`. In particular, the following must hold for any type + /// implementing `Ord`: + /// + /// ```text + /// k1.cmp(&k2) == k1.partial_cmp(&k2).unwrap() + /// ``` + /// + /// ### Example + /// ```rust,ignore + /// #[derive(Ord, PartialEq, Eq)] + /// struct Foo; + /// + /// impl PartialOrd for Foo { + /// ... + /// } + /// ``` + /// Use instead: + /// ```rust,ignore + /// #[derive(PartialEq, Eq)] + /// struct Foo; + /// + /// impl PartialOrd for Foo { + /// fn partial_cmp(&self, other: &Foo) -> Option<Ordering> { + /// Some(self.cmp(other)) + /// } + /// } + /// + /// impl Ord for Foo { + /// ... + /// } + /// ``` + /// or, if you don't need a custom ordering: + /// ```rust,ignore + /// #[derive(Ord, PartialOrd, PartialEq, Eq)] + /// struct Foo; + /// ``` + #[clippy::version = "1.47.0"] + pub DERIVE_ORD_XOR_PARTIAL_ORD, + correctness, + "deriving `Ord` but implementing `PartialOrd` explicitly" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for explicit `Clone` implementations for `Copy` + /// types. + /// + /// ### Why is this bad? + /// To avoid surprising behavior, these traits should + /// agree and the behavior of `Copy` cannot be overridden. In almost all + /// situations a `Copy` type should have a `Clone` implementation that does + /// nothing more than copy the object, which is what `#[derive(Copy, Clone)]` + /// gets you. + /// + /// ### Example + /// ```rust,ignore + /// #[derive(Copy)] + /// struct Foo; + /// + /// impl Clone for Foo { + /// // .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXPL_IMPL_CLONE_ON_COPY, + pedantic, + "implementing `Clone` explicitly on `Copy` types" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for deriving `serde::Deserialize` on a type that + /// has methods using `unsafe`. + /// + /// ### Why is this bad? + /// Deriving `serde::Deserialize` will create a constructor + /// that may violate invariants hold by another constructor. + /// + /// ### Example + /// ```rust,ignore + /// use serde::Deserialize; + /// + /// #[derive(Deserialize)] + /// pub struct Foo { + /// // .. + /// } + /// + /// impl Foo { + /// pub fn new() -> Self { + /// // setup here .. + /// } + /// + /// pub unsafe fn parts() -> (&str, &str) { + /// // assumes invariants hold + /// } + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub UNSAFE_DERIVE_DESERIALIZE, + pedantic, + "deriving `serde::Deserialize` on a type that has methods using `unsafe`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for types that derive `PartialEq` and could implement `Eq`. + /// + /// ### Why is this bad? + /// If a type `T` derives `PartialEq` and all of its members implement `Eq`, + /// then `T` can always implement `Eq`. Implementing `Eq` allows `T` to be used + /// in APIs that require `Eq` types. It also allows structs containing `T` to derive + /// `Eq` themselves. + /// + /// ### Example + /// ```rust + /// #[derive(PartialEq)] + /// struct Foo { + /// i_am_eq: i32, + /// i_am_eq_too: Vec<String>, + /// } + /// ``` + /// Use instead: + /// ```rust + /// #[derive(PartialEq, Eq)] + /// struct Foo { + /// i_am_eq: i32, + /// i_am_eq_too: Vec<String>, + /// } + /// ``` + #[clippy::version = "1.63.0"] + pub DERIVE_PARTIAL_EQ_WITHOUT_EQ, + style, + "deriving `PartialEq` on a type that can implement `Eq`, without implementing `Eq`" +} + +declare_lint_pass!(Derive => [ + EXPL_IMPL_CLONE_ON_COPY, + DERIVE_HASH_XOR_EQ, + DERIVE_ORD_XOR_PARTIAL_ORD, + UNSAFE_DERIVE_DESERIALIZE, + DERIVE_PARTIAL_EQ_WITHOUT_EQ +]); + +impl<'tcx> LateLintPass<'tcx> for Derive { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if let ItemKind::Impl(Impl { + of_trait: Some(ref trait_ref), + .. + }) = item.kind + { + let ty = cx.tcx.type_of(item.def_id); + let is_automatically_derived = cx.tcx.has_attr(item.def_id.to_def_id(), sym::automatically_derived); + + check_hash_peq(cx, item.span, trait_ref, ty, is_automatically_derived); + check_ord_partial_ord(cx, item.span, trait_ref, ty, is_automatically_derived); + + if is_automatically_derived { + check_unsafe_derive_deserialize(cx, item, trait_ref, ty); + check_partial_eq_without_eq(cx, item.span, trait_ref, ty); + } else { + check_copy_clone(cx, item, trait_ref, ty); + } + } + } +} + +/// Implementation of the `DERIVE_HASH_XOR_EQ` lint. +fn check_hash_peq<'tcx>( + cx: &LateContext<'tcx>, + span: Span, + trait_ref: &hir::TraitRef<'_>, + ty: Ty<'tcx>, + hash_is_automatically_derived: bool, +) { + if_chain! { + if let Some(peq_trait_def_id) = cx.tcx.lang_items().eq_trait(); + if let Some(def_id) = trait_ref.trait_def_id(); + if cx.tcx.is_diagnostic_item(sym::Hash, def_id); + then { + // Look for the PartialEq implementations for `ty` + cx.tcx.for_each_relevant_impl(peq_trait_def_id, ty, |impl_id| { + let peq_is_automatically_derived = cx.tcx.has_attr(impl_id, sym::automatically_derived); + + if peq_is_automatically_derived == hash_is_automatically_derived { + return; + } + + let trait_ref = cx.tcx.impl_trait_ref(impl_id).expect("must be a trait implementation"); + + // Only care about `impl PartialEq<Foo> for Foo` + // For `impl PartialEq<B> for A, input_types is [A, B] + if trait_ref.substs.type_at(1) == ty { + let mess = if peq_is_automatically_derived { + "you are implementing `Hash` explicitly but have derived `PartialEq`" + } else { + "you are deriving `Hash` but have implemented `PartialEq` explicitly" + }; + + span_lint_and_then( + cx, + DERIVE_HASH_XOR_EQ, + span, + mess, + |diag| { + if let Some(local_def_id) = impl_id.as_local() { + let hir_id = cx.tcx.hir().local_def_id_to_hir_id(local_def_id); + diag.span_note( + cx.tcx.hir().span(hir_id), + "`PartialEq` implemented here" + ); + } + } + ); + } + }); + } + } +} + +/// Implementation of the `DERIVE_ORD_XOR_PARTIAL_ORD` lint. +fn check_ord_partial_ord<'tcx>( + cx: &LateContext<'tcx>, + span: Span, + trait_ref: &hir::TraitRef<'_>, + ty: Ty<'tcx>, + ord_is_automatically_derived: bool, +) { + if_chain! { + if let Some(ord_trait_def_id) = cx.tcx.get_diagnostic_item(sym::Ord); + if let Some(partial_ord_trait_def_id) = cx.tcx.lang_items().partial_ord_trait(); + if let Some(def_id) = &trait_ref.trait_def_id(); + if *def_id == ord_trait_def_id; + then { + // Look for the PartialOrd implementations for `ty` + cx.tcx.for_each_relevant_impl(partial_ord_trait_def_id, ty, |impl_id| { + let partial_ord_is_automatically_derived = cx.tcx.has_attr(impl_id, sym::automatically_derived); + + if partial_ord_is_automatically_derived == ord_is_automatically_derived { + return; + } + + let trait_ref = cx.tcx.impl_trait_ref(impl_id).expect("must be a trait implementation"); + + // Only care about `impl PartialOrd<Foo> for Foo` + // For `impl PartialOrd<B> for A, input_types is [A, B] + if trait_ref.substs.type_at(1) == ty { + let mess = if partial_ord_is_automatically_derived { + "you are implementing `Ord` explicitly but have derived `PartialOrd`" + } else { + "you are deriving `Ord` but have implemented `PartialOrd` explicitly" + }; + + span_lint_and_then( + cx, + DERIVE_ORD_XOR_PARTIAL_ORD, + span, + mess, + |diag| { + if let Some(local_def_id) = impl_id.as_local() { + let hir_id = cx.tcx.hir().local_def_id_to_hir_id(local_def_id); + diag.span_note( + cx.tcx.hir().span(hir_id), + "`PartialOrd` implemented here" + ); + } + } + ); + } + }); + } + } +} + +/// Implementation of the `EXPL_IMPL_CLONE_ON_COPY` lint. +fn check_copy_clone<'tcx>(cx: &LateContext<'tcx>, item: &Item<'_>, trait_ref: &hir::TraitRef<'_>, ty: Ty<'tcx>) { + let clone_id = match cx.tcx.lang_items().clone_trait() { + Some(id) if trait_ref.trait_def_id() == Some(id) => id, + _ => return, + }; + let copy_id = match cx.tcx.lang_items().copy_trait() { + Some(id) => id, + None => return, + }; + let (ty_adt, ty_subs) = match *ty.kind() { + // Unions can't derive clone. + ty::Adt(adt, subs) if !adt.is_union() => (adt, subs), + _ => return, + }; + // If the current self type doesn't implement Copy (due to generic constraints), search to see if + // there's a Copy impl for any instance of the adt. + if !is_copy(cx, ty) { + if ty_subs.non_erasable_generics().next().is_some() { + let has_copy_impl = cx.tcx.all_local_trait_impls(()).get(©_id).map_or(false, |impls| { + impls + .iter() + .any(|&id| matches!(cx.tcx.type_of(id).kind(), ty::Adt(adt, _) if ty_adt.did() == adt.did())) + }); + if !has_copy_impl { + return; + } + } else { + return; + } + } + // Derive constrains all generic types to requiring Clone. Check if any type is not constrained for + // this impl. + if ty_subs.types().any(|ty| !implements_trait(cx, ty, clone_id, &[])) { + return; + } + + span_lint_and_note( + cx, + EXPL_IMPL_CLONE_ON_COPY, + item.span, + "you are implementing `Clone` explicitly on a `Copy` type", + Some(item.span), + "consider deriving `Clone` or removing `Copy`", + ); +} + +/// Implementation of the `UNSAFE_DERIVE_DESERIALIZE` lint. +fn check_unsafe_derive_deserialize<'tcx>( + cx: &LateContext<'tcx>, + item: &Item<'_>, + trait_ref: &hir::TraitRef<'_>, + ty: Ty<'tcx>, +) { + fn has_unsafe<'tcx>(cx: &LateContext<'tcx>, item: &'tcx Item<'_>) -> bool { + let mut visitor = UnsafeVisitor { cx, has_unsafe: false }; + walk_item(&mut visitor, item); + visitor.has_unsafe + } + + if_chain! { + if let Some(trait_def_id) = trait_ref.trait_def_id(); + if match_def_path(cx, trait_def_id, &paths::SERDE_DESERIALIZE); + if let ty::Adt(def, _) = ty.kind(); + if let Some(local_def_id) = def.did().as_local(); + let adt_hir_id = cx.tcx.hir().local_def_id_to_hir_id(local_def_id); + if !is_lint_allowed(cx, UNSAFE_DERIVE_DESERIALIZE, adt_hir_id); + if cx.tcx.inherent_impls(def.did()) + .iter() + .map(|imp_did| cx.tcx.hir().expect_item(imp_did.expect_local())) + .any(|imp| has_unsafe(cx, imp)); + then { + span_lint_and_help( + cx, + UNSAFE_DERIVE_DESERIALIZE, + item.span, + "you are deriving `serde::Deserialize` on a type that has methods using `unsafe`", + None, + "consider implementing `serde::Deserialize` manually. See https://serde.rs/impl-deserialize.html" + ); + } + } +} + +struct UnsafeVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + has_unsafe: bool, +} + +impl<'tcx> Visitor<'tcx> for UnsafeVisitor<'_, 'tcx> { + type NestedFilter = nested_filter::All; + + fn visit_fn(&mut self, kind: FnKind<'tcx>, decl: &'tcx FnDecl<'_>, body_id: BodyId, span: Span, id: HirId) { + if self.has_unsafe { + return; + } + + if_chain! { + if let Some(header) = kind.header(); + if header.unsafety == Unsafety::Unsafe; + then { + self.has_unsafe = true; + } + } + + walk_fn(self, kind, decl, body_id, span, id); + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if self.has_unsafe { + return; + } + + if let ExprKind::Block(block, _) = expr.kind { + if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) { + self.has_unsafe = true; + } + } + + walk_expr(self, expr); + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +/// Implementation of the `DERIVE_PARTIAL_EQ_WITHOUT_EQ` lint. +fn check_partial_eq_without_eq<'tcx>(cx: &LateContext<'tcx>, span: Span, trait_ref: &hir::TraitRef<'_>, ty: Ty<'tcx>) { + if_chain! { + if let ty::Adt(adt, substs) = ty.kind(); + if cx.tcx.visibility(adt.did()) == Visibility::Public; + if let Some(eq_trait_def_id) = cx.tcx.get_diagnostic_item(sym::Eq); + if let Some(def_id) = trait_ref.trait_def_id(); + if cx.tcx.is_diagnostic_item(sym::PartialEq, def_id); + let param_env = param_env_for_derived_eq(cx.tcx, adt.did(), eq_trait_def_id); + if !implements_trait_with_env(cx.tcx, param_env, ty, eq_trait_def_id, &[]); + // If all of our fields implement `Eq`, we can implement `Eq` too + if adt + .all_fields() + .map(|f| f.ty(cx.tcx, substs)) + .all(|ty| implements_trait_with_env(cx.tcx, param_env, ty, eq_trait_def_id, &[])); + then { + span_lint_and_sugg( + cx, + DERIVE_PARTIAL_EQ_WITHOUT_EQ, + span.ctxt().outer_expn_data().call_site, + "you are deriving `PartialEq` and can implement `Eq`", + "consider deriving `Eq` as well", + "PartialEq, Eq".to_string(), + Applicability::MachineApplicable, + ) + } + } +} + +/// Creates the `ParamEnv` used for the give type's derived `Eq` impl. +fn param_env_for_derived_eq(tcx: TyCtxt<'_>, did: DefId, eq_trait_id: DefId) -> ParamEnv<'_> { + // Initial map from generic index to param def. + // Vec<(param_def, needs_eq)> + let mut params = tcx + .generics_of(did) + .params + .iter() + .map(|p| (p, matches!(p.kind, GenericParamDefKind::Type { .. }))) + .collect::<Vec<_>>(); + + let ty_predicates = tcx.predicates_of(did).predicates; + for (p, _) in ty_predicates { + if let PredicateKind::Trait(p) = p.kind().skip_binder() + && p.trait_ref.def_id == eq_trait_id + && let ty::Param(self_ty) = p.trait_ref.self_ty().kind() + && p.constness == BoundConstness::NotConst + { + // Flag types which already have an `Eq` bound. + params[self_ty.index as usize].1 = false; + } + } + + ParamEnv::new( + tcx.mk_predicates(ty_predicates.iter().map(|&(p, _)| p).chain( + params.iter().filter(|&&(_, needs_eq)| needs_eq).map(|&(param, _)| { + tcx.mk_predicate(Binder::dummy(PredicateKind::Trait(TraitPredicate { + trait_ref: TraitRef::new(eq_trait_id, tcx.mk_substs([tcx.mk_param_from_def(param)].into_iter())), + constness: BoundConstness::NotConst, + polarity: ImplPolarity::Positive, + }))) + }), + )), + Reveal::UserFacing, + Constness::NotConst, + ) +} diff --git a/src/tools/clippy/clippy_lints/src/disallowed_methods.rs b/src/tools/clippy/clippy_lints/src/disallowed_methods.rs new file mode 100644 index 000000000..53973ab79 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/disallowed_methods.rs @@ -0,0 +1,113 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{fn_def_id, get_parent_expr, path_def_id}; + +use rustc_hir::{def::Res, def_id::DefIdMap, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +use crate::utils::conf; + +declare_clippy_lint! { + /// ### What it does + /// Denies the configured methods and functions in clippy.toml + /// + /// Note: Even though this lint is warn-by-default, it will only trigger if + /// methods are defined in the clippy.toml file. + /// + /// ### Why is this bad? + /// Some methods are undesirable in certain contexts, and it's beneficial to + /// lint for them as needed. + /// + /// ### Example + /// An example clippy.toml configuration: + /// ```toml + /// # clippy.toml + /// disallowed-methods = [ + /// # Can use a string as the path of the disallowed method. + /// "std::boxed::Box::new", + /// # Can also use an inline table with a `path` key. + /// { path = "std::time::Instant::now" }, + /// # When using an inline table, can add a `reason` for why the method + /// # is disallowed. + /// { path = "std::vec::Vec::leak", reason = "no leaking memory" }, + /// ] + /// ``` + /// + /// ```rust,ignore + /// // Example code where clippy issues a warning + /// let xs = vec![1, 2, 3, 4]; + /// xs.leak(); // Vec::leak is disallowed in the config. + /// // The diagnostic contains the message "no leaking memory". + /// + /// let _now = Instant::now(); // Instant::now is disallowed in the config. + /// + /// let _box = Box::new(3); // Box::new is disallowed in the config. + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// // Example code which does not raise clippy warning + /// let mut xs = Vec::new(); // Vec::new is _not_ disallowed in the config. + /// xs.push(123); // Vec::push is _not_ disallowed in the config. + /// ``` + #[clippy::version = "1.49.0"] + pub DISALLOWED_METHODS, + style, + "use of a disallowed method call" +} + +#[derive(Clone, Debug)] +pub struct DisallowedMethods { + conf_disallowed: Vec<conf::DisallowedMethod>, + disallowed: DefIdMap<usize>, +} + +impl DisallowedMethods { + pub fn new(conf_disallowed: Vec<conf::DisallowedMethod>) -> Self { + Self { + conf_disallowed, + disallowed: DefIdMap::default(), + } + } +} + +impl_lint_pass!(DisallowedMethods => [DISALLOWED_METHODS]); + +impl<'tcx> LateLintPass<'tcx> for DisallowedMethods { + fn check_crate(&mut self, cx: &LateContext<'_>) { + for (index, conf) in self.conf_disallowed.iter().enumerate() { + let segs: Vec<_> = conf.path().split("::").collect(); + if let Res::Def(_, id) = clippy_utils::def_path_res(cx, &segs) { + self.disallowed.insert(id, index); + } + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let uncalled_path = if let Some(parent) = get_parent_expr(cx, expr) + && let ExprKind::Call(receiver, _) = parent.kind + && receiver.hir_id == expr.hir_id + { + None + } else { + path_def_id(cx, expr) + }; + let def_id = match uncalled_path.or_else(|| fn_def_id(cx, expr)) { + Some(def_id) => def_id, + None => return, + }; + let conf = match self.disallowed.get(&def_id) { + Some(&index) => &self.conf_disallowed[index], + None => return, + }; + let msg = format!("use of a disallowed method `{}`", conf.path()); + span_lint_and_then(cx, DISALLOWED_METHODS, expr.span, &msg, |diag| { + if let conf::DisallowedMethod::WithReason { + reason: Some(reason), .. + } = conf + { + diag.note(&format!("{} (from clippy.toml)", reason)); + } + }); + } +} diff --git a/src/tools/clippy/clippy_lints/src/disallowed_script_idents.rs b/src/tools/clippy/clippy_lints/src/disallowed_script_idents.rs new file mode 100644 index 000000000..0c27c3f92 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/disallowed_script_idents.rs @@ -0,0 +1,113 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast; +use rustc_data_structures::fx::FxHashSet; +use rustc_lint::{EarlyContext, EarlyLintPass, Level, LintContext}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use unicode_script::{Script, UnicodeScript}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of unicode scripts other than those explicitly allowed + /// by the lint config. + /// + /// This lint doesn't take into account non-text scripts such as `Unknown` and `Linear_A`. + /// It also ignores the `Common` script type. + /// While configuring, be sure to use official script name [aliases] from + /// [the list of supported scripts][supported_scripts]. + /// + /// See also: [`non_ascii_idents`]. + /// + /// [aliases]: http://www.unicode.org/reports/tr24/tr24-31.html#Script_Value_Aliases + /// [supported_scripts]: https://www.unicode.org/iso15924/iso15924-codes.html + /// + /// ### Why is this bad? + /// It may be not desired to have many different scripts for + /// identifiers in the codebase. + /// + /// Note that if you only want to allow plain English, you might want to use + /// built-in [`non_ascii_idents`] lint instead. + /// + /// [`non_ascii_idents`]: https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#non-ascii-idents + /// + /// ### Example + /// ```rust + /// // Assuming that `clippy.toml` contains the following line: + /// // allowed-locales = ["Latin", "Cyrillic"] + /// let counter = 10; // OK, latin is allowed. + /// let счётчик = 10; // OK, cyrillic is allowed. + /// let zähler = 10; // OK, it's still latin. + /// let カウンタ = 10; // Will spawn the lint. + /// ``` + #[clippy::version = "1.55.0"] + pub DISALLOWED_SCRIPT_IDENTS, + restriction, + "usage of non-allowed Unicode scripts" +} + +#[derive(Clone, Debug)] +pub struct DisallowedScriptIdents { + whitelist: FxHashSet<Script>, +} + +impl DisallowedScriptIdents { + pub fn new(whitelist: &[String]) -> Self { + let whitelist = whitelist + .iter() + .map(String::as_str) + .filter_map(Script::from_full_name) + .collect(); + Self { whitelist } + } +} + +impl_lint_pass!(DisallowedScriptIdents => [DISALLOWED_SCRIPT_IDENTS]); + +impl EarlyLintPass for DisallowedScriptIdents { + fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &ast::Crate) { + // Implementation is heavily inspired by the implementation of [`non_ascii_idents`] lint: + // https://github.com/rust-lang/rust/blob/master/compiler/rustc_lint/src/non_ascii_idents.rs + + let check_disallowed_script_idents = cx.builder.lint_level(DISALLOWED_SCRIPT_IDENTS).0 != Level::Allow; + if !check_disallowed_script_idents { + return; + } + + let symbols = cx.sess().parse_sess.symbol_gallery.symbols.lock(); + // Sort by `Span` so that error messages make sense with respect to the + // order of identifier locations in the code. + let mut symbols: Vec<_> = symbols.iter().collect(); + symbols.sort_unstable_by_key(|k| k.1); + + for (symbol, &span) in &symbols { + // Note: `symbol.as_str()` is an expensive operation, thus should not be called + // more than once for a single symbol. + let symbol_str = symbol.as_str(); + if symbol_str.is_ascii() { + continue; + } + + for c in symbol_str.chars() { + // We want to iterate through all the scripts associated with this character + // and check whether at least of one scripts is in the whitelist. + let forbidden_script = c + .script_extension() + .iter() + .find(|script| !self.whitelist.contains(script)); + if let Some(script) = forbidden_script { + span_lint( + cx, + DISALLOWED_SCRIPT_IDENTS, + span, + &format!( + "identifier `{}` has a Unicode script that is not allowed by configuration: {}", + symbol_str, + script.full_name() + ), + ); + // We don't want to spawn warning multiple times over a single identifier. + break; + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/disallowed_types.rs b/src/tools/clippy/clippy_lints/src/disallowed_types.rs new file mode 100644 index 000000000..14f89edce --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/disallowed_types.rs @@ -0,0 +1,140 @@ +use clippy_utils::diagnostics::span_lint_and_then; + +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::{ + def::Res, def_id::DefId, Item, ItemKind, PolyTraitRef, PrimTy, TraitBoundModifier, Ty, TyKind, UseKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; + +use crate::utils::conf; + +declare_clippy_lint! { + /// ### What it does + /// Denies the configured types in clippy.toml. + /// + /// Note: Even though this lint is warn-by-default, it will only trigger if + /// types are defined in the clippy.toml file. + /// + /// ### Why is this bad? + /// Some types are undesirable in certain contexts. + /// + /// ### Example: + /// An example clippy.toml configuration: + /// ```toml + /// # clippy.toml + /// disallowed-types = [ + /// # Can use a string as the path of the disallowed type. + /// "std::collections::BTreeMap", + /// # Can also use an inline table with a `path` key. + /// { path = "std::net::TcpListener" }, + /// # When using an inline table, can add a `reason` for why the type + /// # is disallowed. + /// { path = "std::net::Ipv4Addr", reason = "no IPv4 allowed" }, + /// ] + /// ``` + /// + /// ```rust,ignore + /// use std::collections::BTreeMap; + /// // or its use + /// let x = std::collections::BTreeMap::new(); + /// ``` + /// Use instead: + /// ```rust,ignore + /// // A similar type that is allowed by the config + /// use std::collections::HashMap; + /// ``` + #[clippy::version = "1.55.0"] + pub DISALLOWED_TYPES, + style, + "use of disallowed types" +} +#[derive(Clone, Debug)] +pub struct DisallowedTypes { + conf_disallowed: Vec<conf::DisallowedType>, + def_ids: FxHashMap<DefId, Option<String>>, + prim_tys: FxHashMap<PrimTy, Option<String>>, +} + +impl DisallowedTypes { + pub fn new(conf_disallowed: Vec<conf::DisallowedType>) -> Self { + Self { + conf_disallowed, + def_ids: FxHashMap::default(), + prim_tys: FxHashMap::default(), + } + } + + fn check_res_emit(&self, cx: &LateContext<'_>, res: &Res, span: Span) { + match res { + Res::Def(_, did) => { + if let Some(reason) = self.def_ids.get(did) { + emit(cx, &cx.tcx.def_path_str(*did), span, reason.as_deref()); + } + }, + Res::PrimTy(prim) => { + if let Some(reason) = self.prim_tys.get(prim) { + emit(cx, prim.name_str(), span, reason.as_deref()); + } + }, + _ => {}, + } + } +} + +impl_lint_pass!(DisallowedTypes => [DISALLOWED_TYPES]); + +impl<'tcx> LateLintPass<'tcx> for DisallowedTypes { + fn check_crate(&mut self, cx: &LateContext<'_>) { + for conf in &self.conf_disallowed { + let (path, reason) = match conf { + conf::DisallowedType::Simple(path) => (path, None), + conf::DisallowedType::WithReason { path, reason } => ( + path, + reason.as_ref().map(|reason| format!("{} (from clippy.toml)", reason)), + ), + }; + let segs: Vec<_> = path.split("::").collect(); + match clippy_utils::def_path_res(cx, &segs) { + Res::Def(_, id) => { + self.def_ids.insert(id, reason); + }, + Res::PrimTy(ty) => { + self.prim_tys.insert(ty, reason); + }, + _ => {}, + } + } + } + + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if let ItemKind::Use(path, UseKind::Single) = &item.kind { + self.check_res_emit(cx, &path.res, item.span); + } + } + + fn check_ty(&mut self, cx: &LateContext<'tcx>, ty: &'tcx Ty<'tcx>) { + if let TyKind::Path(path) = &ty.kind { + self.check_res_emit(cx, &cx.qpath_res(path, ty.hir_id), ty.span); + } + } + + fn check_poly_trait_ref(&mut self, cx: &LateContext<'tcx>, poly: &'tcx PolyTraitRef<'tcx>, _: TraitBoundModifier) { + self.check_res_emit(cx, &poly.trait_ref.path.res, poly.trait_ref.path.span); + } +} + +fn emit(cx: &LateContext<'_>, name: &str, span: Span, reason: Option<&str>) { + span_lint_and_then( + cx, + DISALLOWED_TYPES, + span, + &format!("`{}` is not allowed according to config", name), + |diag| { + if let Some(reason) = reason { + diag.note(reason); + } + }, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/doc.rs b/src/tools/clippy/clippy_lints/src/doc.rs new file mode 100644 index 000000000..da111e737 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/doc.rs @@ -0,0 +1,849 @@ +use clippy_utils::attrs::is_doc_hidden; +use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then}; +use clippy_utils::macros::{is_panic, root_macro_call_first_node}; +use clippy_utils::source::{first_line_of_span, snippet_with_applicability}; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use clippy_utils::{is_entrypoint_fn, method_chain_args, return_ty}; +use if_chain::if_chain; +use itertools::Itertools; +use rustc_ast::ast::{Async, AttrKind, Attribute, Fn, FnRetTy, ItemKind}; +use rustc_ast::token::CommentKind; +use rustc_data_structures::fx::FxHashSet; +use rustc_data_structures::sync::Lrc; +use rustc_errors::emitter::EmitterWriter; +use rustc_errors::{Applicability, Handler, MultiSpan, SuggestionStyle}; +use rustc_hir as hir; +use rustc_hir::intravisit::{self, Visitor}; +use rustc_hir::{AnonConst, Expr}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_parse::maybe_new_parser_from_source_str; +use rustc_parse::parser::ForceCollect; +use rustc_session::parse::ParseSess; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::def_id::LocalDefId; +use rustc_span::edition::Edition; +use rustc_span::source_map::{BytePos, FilePathMapping, SourceMap, Span}; +use rustc_span::{sym, FileName, Pos}; +use std::io; +use std::ops::Range; +use std::thread; +use url::Url; + +declare_clippy_lint! { + /// ### What it does + /// Checks for the presence of `_`, `::` or camel-case words + /// outside ticks in documentation. + /// + /// ### Why is this bad? + /// *Rustdoc* supports markdown formatting, `_`, `::` and + /// camel-case probably indicates some code which should be included between + /// ticks. `_` can also be used for emphasis in markdown, this lint tries to + /// consider that. + /// + /// ### Known problems + /// Lots of bad docs won’t be fixed, what the lint checks + /// for is limited, and there are still false positives. HTML elements and their + /// content are not linted. + /// + /// In addition, when writing documentation comments, including `[]` brackets + /// inside a link text would trip the parser. Therefore, documenting link with + /// `[`SmallVec<[T; INLINE_CAPACITY]>`]` and then [`SmallVec<[T; INLINE_CAPACITY]>`]: SmallVec + /// would fail. + /// + /// ### Examples + /// ```rust + /// /// Do something with the foo_bar parameter. See also + /// /// that::other::module::foo. + /// // ^ `foo_bar` and `that::other::module::foo` should be ticked. + /// fn doit(foo_bar: usize) {} + /// ``` + /// + /// ```rust + /// // Link text with `[]` brackets should be written as following: + /// /// Consume the array and return the inner + /// /// [`SmallVec<[T; INLINE_CAPACITY]>`][SmallVec]. + /// /// [SmallVec]: SmallVec + /// fn main() {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DOC_MARKDOWN, + pedantic, + "presence of `_`, `::` or camel-case outside backticks in documentation" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the doc comments of publicly visible + /// unsafe functions and warns if there is no `# Safety` section. + /// + /// ### Why is this bad? + /// Unsafe functions should document their safety + /// preconditions, so that users can be sure they are using them safely. + /// + /// ### Examples + /// ```rust + ///# type Universe = (); + /// /// This function should really be documented + /// pub unsafe fn start_apocalypse(u: &mut Universe) { + /// unimplemented!(); + /// } + /// ``` + /// + /// At least write a line about safety: + /// + /// ```rust + ///# type Universe = (); + /// /// # Safety + /// /// + /// /// This function should not be called before the horsemen are ready. + /// pub unsafe fn start_apocalypse(u: &mut Universe) { + /// unimplemented!(); + /// } + /// ``` + #[clippy::version = "1.39.0"] + pub MISSING_SAFETY_DOC, + style, + "`pub unsafe fn` without `# Safety` docs" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks the doc comments of publicly visible functions that + /// return a `Result` type and warns if there is no `# Errors` section. + /// + /// ### Why is this bad? + /// Documenting the type of errors that can be returned from a + /// function can help callers write code to handle the errors appropriately. + /// + /// ### Examples + /// Since the following function returns a `Result` it has an `# Errors` section in + /// its doc comment: + /// + /// ```rust + ///# use std::io; + /// /// # Errors + /// /// + /// /// Will return `Err` if `filename` does not exist or the user does not have + /// /// permission to read it. + /// pub fn read(filename: String) -> io::Result<String> { + /// unimplemented!(); + /// } + /// ``` + #[clippy::version = "1.41.0"] + pub MISSING_ERRORS_DOC, + pedantic, + "`pub fn` returns `Result` without `# Errors` in doc comment" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks the doc comments of publicly visible functions that + /// may panic and warns if there is no `# Panics` section. + /// + /// ### Why is this bad? + /// Documenting the scenarios in which panicking occurs + /// can help callers who do not want to panic to avoid those situations. + /// + /// ### Examples + /// Since the following function may panic it has a `# Panics` section in + /// its doc comment: + /// + /// ```rust + /// /// # Panics + /// /// + /// /// Will panic if y is 0 + /// pub fn divide_by(x: i32, y: i32) -> i32 { + /// if y == 0 { + /// panic!("Cannot divide by 0") + /// } else { + /// x / y + /// } + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub MISSING_PANICS_DOC, + pedantic, + "`pub fn` may panic without `# Panics` in doc comment" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `fn main() { .. }` in doctests + /// + /// ### Why is this bad? + /// The test can be shorter (and likely more readable) + /// if the `fn main()` is left implicit. + /// + /// ### Examples + /// ```rust + /// /// An example of a doctest with a `main()` function + /// /// + /// /// # Examples + /// /// + /// /// ``` + /// /// fn main() { + /// /// // this needs not be in an `fn` + /// /// } + /// /// ``` + /// fn needless_main() { + /// unimplemented!(); + /// } + /// ``` + #[clippy::version = "1.40.0"] + pub NEEDLESS_DOCTEST_MAIN, + style, + "presence of `fn main() {` in code examples" +} + +#[expect(clippy::module_name_repetitions)] +#[derive(Clone)] +pub struct DocMarkdown { + valid_idents: FxHashSet<String>, + in_trait_impl: bool, +} + +impl DocMarkdown { + pub fn new(valid_idents: FxHashSet<String>) -> Self { + Self { + valid_idents, + in_trait_impl: false, + } + } +} + +impl_lint_pass!(DocMarkdown => + [DOC_MARKDOWN, MISSING_SAFETY_DOC, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, NEEDLESS_DOCTEST_MAIN] +); + +impl<'tcx> LateLintPass<'tcx> for DocMarkdown { + fn check_crate(&mut self, cx: &LateContext<'tcx>) { + let attrs = cx.tcx.hir().attrs(hir::CRATE_HIR_ID); + check_attrs(cx, &self.valid_idents, attrs); + } + + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + let headers = check_attrs(cx, &self.valid_idents, attrs); + match item.kind { + hir::ItemKind::Fn(ref sig, _, body_id) => { + if !(is_entrypoint_fn(cx, item.def_id.to_def_id()) || in_external_macro(cx.tcx.sess, item.span)) { + let body = cx.tcx.hir().body(body_id); + let mut fpu = FindPanicUnwrap { + cx, + typeck_results: cx.tcx.typeck(item.def_id), + panic_span: None, + }; + fpu.visit_expr(&body.value); + lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, Some(body_id), fpu.panic_span); + } + }, + hir::ItemKind::Impl(impl_) => { + self.in_trait_impl = impl_.of_trait.is_some(); + }, + hir::ItemKind::Trait(_, unsafety, ..) => { + if !headers.safety && unsafety == hir::Unsafety::Unsafe { + span_lint( + cx, + MISSING_SAFETY_DOC, + item.span, + "docs for unsafe trait missing `# Safety` section", + ); + } + }, + _ => (), + } + } + + fn check_item_post(&mut self, _cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + if let hir::ItemKind::Impl { .. } = item.kind { + self.in_trait_impl = false; + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + let headers = check_attrs(cx, &self.valid_idents, attrs); + if let hir::TraitItemKind::Fn(ref sig, ..) = item.kind { + if !in_external_macro(cx.tcx.sess, item.span) { + lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, None, None); + } + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + let headers = check_attrs(cx, &self.valid_idents, attrs); + if self.in_trait_impl || in_external_macro(cx.tcx.sess, item.span) { + return; + } + if let hir::ImplItemKind::Fn(ref sig, body_id) = item.kind { + let body = cx.tcx.hir().body(body_id); + let mut fpu = FindPanicUnwrap { + cx, + typeck_results: cx.tcx.typeck(item.def_id), + panic_span: None, + }; + fpu.visit_expr(&body.value); + lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, Some(body_id), fpu.panic_span); + } + } +} + +fn lint_for_missing_headers<'tcx>( + cx: &LateContext<'tcx>, + def_id: LocalDefId, + span: impl Into<MultiSpan> + Copy, + sig: &hir::FnSig<'_>, + headers: DocHeaders, + body_id: Option<hir::BodyId>, + panic_span: Option<Span>, +) { + if !cx.access_levels.is_exported(def_id) { + return; // Private functions do not require doc comments + } + + // do not lint if any parent has `#[doc(hidden)]` attribute (#7347) + if cx + .tcx + .hir() + .parent_iter(cx.tcx.hir().local_def_id_to_hir_id(def_id)) + .any(|(id, _node)| is_doc_hidden(cx.tcx.hir().attrs(id))) + { + return; + } + + if !headers.safety && sig.header.unsafety == hir::Unsafety::Unsafe { + span_lint( + cx, + MISSING_SAFETY_DOC, + span, + "unsafe function's docs miss `# Safety` section", + ); + } + if !headers.panics && panic_span.is_some() { + span_lint_and_note( + cx, + MISSING_PANICS_DOC, + span, + "docs for function which may panic missing `# Panics` section", + panic_span, + "first possible panic found here", + ); + } + if !headers.errors { + let hir_id = cx.tcx.hir().local_def_id_to_hir_id(def_id); + if is_type_diagnostic_item(cx, return_ty(cx, hir_id), sym::Result) { + span_lint( + cx, + MISSING_ERRORS_DOC, + span, + "docs for function returning `Result` missing `# Errors` section", + ); + } else { + if_chain! { + if let Some(body_id) = body_id; + if let Some(future) = cx.tcx.lang_items().future_trait(); + let typeck = cx.tcx.typeck_body(body_id); + let body = cx.tcx.hir().body(body_id); + let ret_ty = typeck.expr_ty(&body.value); + if implements_trait(cx, ret_ty, future, &[]); + if let ty::Opaque(_, subs) = ret_ty.kind(); + if let Some(gen) = subs.types().next(); + if let ty::Generator(_, subs, _) = gen.kind(); + if is_type_diagnostic_item(cx, subs.as_generator().return_ty(), sym::Result); + then { + span_lint( + cx, + MISSING_ERRORS_DOC, + span, + "docs for function returning `Result` missing `# Errors` section", + ); + } + } + } + } +} + +/// Cleanup documentation decoration. +/// +/// We can't use `rustc_ast::attr::AttributeMethods::with_desugared_doc` or +/// `rustc_ast::parse::lexer::comments::strip_doc_comment_decoration` because we +/// need to keep track of +/// the spans but this function is inspired from the later. +#[expect(clippy::cast_possible_truncation)] +#[must_use] +pub fn strip_doc_comment_decoration(doc: &str, comment_kind: CommentKind, span: Span) -> (String, Vec<(usize, Span)>) { + // one-line comments lose their prefix + if comment_kind == CommentKind::Line { + let mut doc = doc.to_owned(); + doc.push('\n'); + let len = doc.len(); + // +3 skips the opening delimiter + return (doc, vec![(len, span.with_lo(span.lo() + BytePos(3)))]); + } + + let mut sizes = vec![]; + let mut contains_initial_stars = false; + for line in doc.lines() { + let offset = line.as_ptr() as usize - doc.as_ptr() as usize; + debug_assert_eq!(offset as u32 as usize, offset); + contains_initial_stars |= line.trim_start().starts_with('*'); + // +1 adds the newline, +3 skips the opening delimiter + sizes.push((line.len() + 1, span.with_lo(span.lo() + BytePos(3 + offset as u32)))); + } + if !contains_initial_stars { + return (doc.to_string(), sizes); + } + // remove the initial '*'s if any + let mut no_stars = String::with_capacity(doc.len()); + for line in doc.lines() { + let mut chars = line.chars(); + for c in &mut chars { + if c.is_whitespace() { + no_stars.push(c); + } else { + no_stars.push(if c == '*' { ' ' } else { c }); + break; + } + } + no_stars.push_str(chars.as_str()); + no_stars.push('\n'); + } + + (no_stars, sizes) +} + +#[derive(Copy, Clone)] +struct DocHeaders { + safety: bool, + errors: bool, + panics: bool, +} + +fn check_attrs<'a>(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> DocHeaders { + use pulldown_cmark::{BrokenLink, CowStr, Options}; + /// We don't want the parser to choke on intra doc links. Since we don't + /// actually care about rendering them, just pretend that all broken links are + /// point to a fake address. + #[expect(clippy::unnecessary_wraps)] // we're following a type signature + fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> { + Some(("fake".into(), "fake".into())) + } + + let mut doc = String::new(); + let mut spans = vec![]; + + for attr in attrs { + if let AttrKind::DocComment(comment_kind, comment) = attr.kind { + let (comment, current_spans) = strip_doc_comment_decoration(comment.as_str(), comment_kind, attr.span); + spans.extend_from_slice(¤t_spans); + doc.push_str(&comment); + } else if attr.has_name(sym::doc) { + // ignore mix of sugared and non-sugared doc + // don't trigger the safety or errors check + return DocHeaders { + safety: true, + errors: true, + panics: true, + }; + } + } + + let mut current = 0; + for &mut (ref mut offset, _) in &mut spans { + let offset_copy = *offset; + *offset = current; + current += offset_copy; + } + + if doc.is_empty() { + return DocHeaders { + safety: false, + errors: false, + panics: false, + }; + } + + let mut cb = fake_broken_link_callback; + + let parser = + pulldown_cmark::Parser::new_with_broken_link_callback(&doc, Options::empty(), Some(&mut cb)).into_offset_iter(); + // Iterate over all `Events` and combine consecutive events into one + let events = parser.coalesce(|previous, current| { + use pulldown_cmark::Event::Text; + + let previous_range = previous.1; + let current_range = current.1; + + match (previous.0, current.0) { + (Text(previous), Text(current)) => { + let mut previous = previous.to_string(); + previous.push_str(¤t); + Ok((Text(previous.into()), previous_range)) + }, + (previous, current) => Err(((previous, previous_range), (current, current_range))), + } + }); + check_doc(cx, valid_idents, events, &spans) +} + +const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"]; + +fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>( + cx: &LateContext<'_>, + valid_idents: &FxHashSet<String>, + events: Events, + spans: &[(usize, Span)], +) -> DocHeaders { + // true if a safety header was found + use pulldown_cmark::Event::{ + Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text, + }; + use pulldown_cmark::Tag::{CodeBlock, Heading, Item, Link, Paragraph}; + use pulldown_cmark::{CodeBlockKind, CowStr}; + + let mut headers = DocHeaders { + safety: false, + errors: false, + panics: false, + }; + let mut in_code = false; + let mut in_link = None; + let mut in_heading = false; + let mut is_rust = false; + let mut edition = None; + let mut ticks_unbalanced = false; + let mut text_to_check: Vec<(CowStr<'_>, Span)> = Vec::new(); + let mut paragraph_span = spans.get(0).expect("function isn't called if doc comment is empty").1; + for (event, range) in events { + match event { + Start(CodeBlock(ref kind)) => { + in_code = true; + if let CodeBlockKind::Fenced(lang) = kind { + for item in lang.split(',') { + if item == "ignore" { + is_rust = false; + break; + } + if let Some(stripped) = item.strip_prefix("edition") { + is_rust = true; + edition = stripped.parse::<Edition>().ok(); + } else if item.is_empty() || RUST_CODE.contains(&item) { + is_rust = true; + } + } + } + }, + End(CodeBlock(_)) => { + in_code = false; + is_rust = false; + }, + Start(Link(_, url, _)) => in_link = Some(url), + End(Link(..)) => in_link = None, + Start(Heading(_, _, _) | Paragraph | Item) => { + if let Start(Heading(_, _, _)) = event { + in_heading = true; + } + ticks_unbalanced = false; + let (_, span) = get_current_span(spans, range.start); + paragraph_span = first_line_of_span(cx, span); + }, + End(Heading(_, _, _) | Paragraph | Item) => { + if let End(Heading(_, _, _)) = event { + in_heading = false; + } + if ticks_unbalanced { + span_lint_and_help( + cx, + DOC_MARKDOWN, + paragraph_span, + "backticks are unbalanced", + None, + "a backtick may be missing a pair", + ); + } else { + for (text, span) in text_to_check { + check_text(cx, valid_idents, &text, span); + } + } + text_to_check = Vec::new(); + }, + Start(_tag) | End(_tag) => (), // We don't care about other tags + Html(_html) => (), // HTML is weird, just ignore it + SoftBreak | HardBreak | TaskListMarker(_) | Code(_) | Rule => (), + FootnoteReference(text) | Text(text) => { + let (begin, span) = get_current_span(spans, range.start); + paragraph_span = paragraph_span.with_hi(span.hi()); + ticks_unbalanced |= text.contains('`') && !in_code; + if Some(&text) == in_link.as_ref() || ticks_unbalanced { + // Probably a link of the form `<http://example.com>` + // Which are represented as a link to "http://example.com" with + // text "http://example.com" by pulldown-cmark + continue; + } + let trimmed_text = text.trim(); + headers.safety |= in_heading && trimmed_text == "Safety"; + headers.safety |= in_heading && trimmed_text == "Implementation safety"; + headers.safety |= in_heading && trimmed_text == "Implementation Safety"; + headers.errors |= in_heading && trimmed_text == "Errors"; + headers.panics |= in_heading && trimmed_text == "Panics"; + if in_code { + if is_rust { + let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition()); + check_code(cx, &text, edition, span); + } + } else { + // Adjust for the beginning of the current `Event` + let span = span.with_lo(span.lo() + BytePos::from_usize(range.start - begin)); + text_to_check.push((text, span)); + } + }, + } + } + headers +} + +fn get_current_span(spans: &[(usize, Span)], idx: usize) -> (usize, Span) { + let index = match spans.binary_search_by(|c| c.0.cmp(&idx)) { + Ok(o) => o, + Err(e) => e - 1, + }; + spans[index] +} + +fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, span: Span) { + fn has_needless_main(code: String, edition: Edition) -> bool { + rustc_driver::catch_fatal_errors(|| { + rustc_span::create_session_globals_then(edition, || { + let filename = FileName::anon_source_code(&code); + + let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); + let fallback_bundle = + rustc_errors::fallback_fluent_bundle(rustc_errors::DEFAULT_LOCALE_RESOURCES, false); + let emitter = EmitterWriter::new( + Box::new(io::sink()), + None, + None, + fallback_bundle, + false, + false, + false, + None, + false, + ); + let handler = Handler::with_emitter(false, None, Box::new(emitter)); + let sess = ParseSess::with_span_handler(handler, sm); + + let mut parser = match maybe_new_parser_from_source_str(&sess, filename, code) { + Ok(p) => p, + Err(errs) => { + drop(errs); + return false; + }, + }; + + let mut relevant_main_found = false; + loop { + match parser.parse_item(ForceCollect::No) { + Ok(Some(item)) => match &item.kind { + ItemKind::Fn(box Fn { + sig, body: Some(block), .. + }) if item.ident.name == sym::main => { + let is_async = matches!(sig.header.asyncness, Async::Yes { .. }); + let returns_nothing = match &sig.decl.output { + FnRetTy::Default(..) => true, + FnRetTy::Ty(ty) if ty.kind.is_unit() => true, + FnRetTy::Ty(_) => false, + }; + + if returns_nothing && !is_async && !block.stmts.is_empty() { + // This main function should be linted, but only if there are no other functions + relevant_main_found = true; + } else { + // This main function should not be linted, we're done + return false; + } + }, + // Tests with one of these items are ignored + ItemKind::Static(..) + | ItemKind::Const(..) + | ItemKind::ExternCrate(..) + | ItemKind::ForeignMod(..) + // Another function was found; this case is ignored + | ItemKind::Fn(..) => return false, + _ => {}, + }, + Ok(None) => break, + Err(e) => { + e.cancel(); + return false; + }, + } + } + + relevant_main_found + }) + }) + .ok() + .unwrap_or_default() + } + + // Because of the global session, we need to create a new session in a different thread with + // the edition we need. + let text = text.to_owned(); + if thread::spawn(move || has_needless_main(text, edition)) + .join() + .expect("thread::spawn failed") + { + span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest"); + } +} + +fn check_text(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, text: &str, span: Span) { + for word in text.split(|c: char| c.is_whitespace() || c == '\'') { + // Trim punctuation as in `some comment (see foo::bar).` + // ^^ + // Or even as in `_foo bar_` which is emphasized. Also preserve `::` as a prefix/suffix. + let mut word = word.trim_matches(|c: char| !c.is_alphanumeric() && c != ':'); + + // Remove leading or trailing single `:` which may be part of a sentence. + if word.starts_with(':') && !word.starts_with("::") { + word = word.trim_start_matches(':'); + } + if word.ends_with(':') && !word.ends_with("::") { + word = word.trim_end_matches(':'); + } + + if valid_idents.contains(word) || word.chars().all(|c| c == ':') { + continue; + } + + // Adjust for the current word + let offset = word.as_ptr() as usize - text.as_ptr() as usize; + let span = Span::new( + span.lo() + BytePos::from_usize(offset), + span.lo() + BytePos::from_usize(offset + word.len()), + span.ctxt(), + span.parent(), + ); + + check_word(cx, word, span); + } +} + +fn check_word(cx: &LateContext<'_>, word: &str, span: Span) { + /// Checks if a string is camel-case, i.e., contains at least two uppercase + /// letters (`Clippy` is ok) and one lower-case letter (`NASA` is ok). + /// Plurals are also excluded (`IDs` is ok). + fn is_camel_case(s: &str) -> bool { + if s.starts_with(|c: char| c.is_ascii_digit()) { + return false; + } + + let s = s.strip_suffix('s').unwrap_or(s); + + s.chars().all(char::is_alphanumeric) + && s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 + && s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0 + } + + fn has_underscore(s: &str) -> bool { + s != "_" && !s.contains("\\_") && s.contains('_') + } + + fn has_hyphen(s: &str) -> bool { + s != "-" && s.contains('-') + } + + if let Ok(url) = Url::parse(word) { + // try to get around the fact that `foo::bar` parses as a valid URL + if !url.cannot_be_a_base() { + span_lint( + cx, + DOC_MARKDOWN, + span, + "you should put bare URLs between `<`/`>` or make a proper Markdown link", + ); + + return; + } + } + + // We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343) + if has_underscore(word) && has_hyphen(word) { + return; + } + + if has_underscore(word) || word.contains("::") || is_camel_case(word) { + let mut applicability = Applicability::MachineApplicable; + + span_lint_and_then( + cx, + DOC_MARKDOWN, + span, + "item in documentation is missing backticks", + |diag| { + let snippet = snippet_with_applicability(cx, span, "..", &mut applicability); + diag.span_suggestion_with_style( + span, + "try", + format!("`{}`", snippet), + applicability, + // always show the suggestion in a separate line, since the + // inline presentation adds another pair of backticks + SuggestionStyle::ShowAlways, + ); + }, + ); + } +} + +struct FindPanicUnwrap<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + panic_span: Option<Span>, + typeck_results: &'tcx ty::TypeckResults<'tcx>, +} + +impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if self.panic_span.is_some() { + return; + } + + if let Some(macro_call) = root_macro_call_first_node(self.cx, expr) { + 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" + ) + { + self.panic_span = Some(macro_call.span); + } + } + + // check for `unwrap` + if let Some(arglists) = method_chain_args(expr, &["unwrap"]) { + 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) + { + self.panic_span = Some(expr.span); + } + } + + // and check sub-expressions + intravisit::walk_expr(self, expr); + } + + // Panics in const blocks will cause compilation to fail. + fn visit_anon_const(&mut self, _: &'tcx AnonConst) {} + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} diff --git a/src/tools/clippy/clippy_lints/src/doc_link_with_quotes.rs b/src/tools/clippy/clippy_lints/src/doc_link_with_quotes.rs new file mode 100644 index 000000000..cb07f57e8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/doc_link_with_quotes.rs @@ -0,0 +1,60 @@ +use clippy_utils::diagnostics::span_lint; +use itertools::Itertools; +use rustc_ast::{AttrKind, Attribute}; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Detects the syntax `['foo']` in documentation comments (notice quotes instead of backticks) + /// outside of code blocks + /// ### Why is this bad? + /// It is likely a typo when defining an intra-doc link + /// + /// ### Example + /// ```rust + /// /// See also: ['foo'] + /// fn bar() {} + /// ``` + /// Use instead: + /// ```rust + /// /// See also: [`foo`] + /// fn bar() {} + /// ``` + #[clippy::version = "1.60.0"] + pub DOC_LINK_WITH_QUOTES, + pedantic, + "possible typo for an intra-doc link" +} +declare_lint_pass!(DocLinkWithQuotes => [DOC_LINK_WITH_QUOTES]); + +impl EarlyLintPass for DocLinkWithQuotes { + fn check_attribute(&mut self, ctx: &EarlyContext<'_>, attr: &Attribute) { + if let AttrKind::DocComment(_, symbol) = attr.kind { + if contains_quote_link(symbol.as_str()) { + span_lint( + ctx, + DOC_LINK_WITH_QUOTES, + attr.span, + "possible intra-doc link using quotes instead of backticks", + ); + } + } + } +} + +fn contains_quote_link(s: &str) -> bool { + let mut in_backticks = false; + let mut found_opening = false; + + for c in s.chars().tuple_windows::<(char, char)>() { + match c { + ('`', _) => in_backticks = !in_backticks, + ('[', '\'') if !in_backticks => found_opening = true, + ('\'', ']') if !in_backticks && found_opening => return true, + _ => {}, + } + } + + false +} diff --git a/src/tools/clippy/clippy_lints/src/double_parens.rs b/src/tools/clippy/clippy_lints/src/double_parens.rs new file mode 100644 index 000000000..a33ef5ce6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/double_parens.rs @@ -0,0 +1,75 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast::{Expr, ExprKind}; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary double parentheses. + /// + /// ### Why is this bad? + /// This makes code harder to read and might indicate a + /// mistake. + /// + /// ### Example + /// ```rust + /// fn simple_double_parens() -> i32 { + /// ((0)) + /// } + /// + /// # fn foo(bar: usize) {} + /// foo((0)); + /// ``` + /// + /// Use instead: + /// ```rust + /// fn simple_no_parens() -> i32 { + /// 0 + /// } + /// + /// # fn foo(bar: usize) {} + /// foo(0); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DOUBLE_PARENS, + complexity, + "Warn on unnecessary double parentheses" +} + +declare_lint_pass!(DoubleParens => [DOUBLE_PARENS]); + +impl EarlyLintPass for DoubleParens { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if expr.span.from_expansion() { + return; + } + + let msg: &str = "consider removing unnecessary double parentheses"; + + match expr.kind { + ExprKind::Paren(ref in_paren) => match in_paren.kind { + ExprKind::Paren(_) | ExprKind::Tup(_) => { + span_lint(cx, DOUBLE_PARENS, expr.span, msg); + }, + _ => {}, + }, + ExprKind::Call(_, ref params) => { + if params.len() == 1 { + let param = ¶ms[0]; + if let ExprKind::Paren(_) = param.kind { + span_lint(cx, DOUBLE_PARENS, param.span, msg); + } + } + }, + ExprKind::MethodCall(_, ref params, _) => { + if params.len() == 2 { + let param = ¶ms[1]; + if let ExprKind::Paren(_) = param.kind { + span_lint(cx, DOUBLE_PARENS, param.span, msg); + } + } + }, + _ => {}, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/drop_forget_ref.rs b/src/tools/clippy/clippy_lints/src/drop_forget_ref.rs new file mode 100644 index 000000000..b35f0b8ca --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/drop_forget_ref.rs @@ -0,0 +1,243 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_note}; +use clippy_utils::is_must_use_func_call; +use clippy_utils::ty::{is_copy, is_must_use_ty, is_type_lang_item}; +use rustc_hir::{Expr, ExprKind, LangItem}; +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 calls to `std::mem::drop` with a reference + /// instead of an owned value. + /// + /// ### Why is this bad? + /// Calling `drop` on a reference will only drop the + /// reference itself, which is a no-op. It will not call the `drop` method (from + /// the `Drop` trait implementation) on the underlying referenced value, which + /// is likely what was intended. + /// + /// ### Example + /// ```ignore + /// let mut lock_guard = mutex.lock(); + /// std::mem::drop(&lock_guard) // Should have been drop(lock_guard), mutex + /// // still locked + /// operation_that_requires_mutex_to_be_unlocked(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DROP_REF, + correctness, + "calls to `std::mem::drop` with a reference instead of an owned value" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `std::mem::forget` with a reference + /// instead of an owned value. + /// + /// ### Why is this bad? + /// Calling `forget` on a reference will only forget the + /// reference itself, which is a no-op. It will not forget the underlying + /// referenced + /// value, which is likely what was intended. + /// + /// ### Example + /// ```rust + /// let x = Box::new(1); + /// std::mem::forget(&x) // Should have been forget(x), x will still be dropped + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FORGET_REF, + correctness, + "calls to `std::mem::forget` with a reference instead of an owned value" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `std::mem::drop` with a value + /// that derives the Copy trait + /// + /// ### Why is this bad? + /// Calling `std::mem::drop` [does nothing for types that + /// implement Copy](https://doc.rust-lang.org/std/mem/fn.drop.html), since the + /// value will be copied and moved into the function on invocation. + /// + /// ### Example + /// ```rust + /// let x: i32 = 42; // i32 implements Copy + /// std::mem::drop(x) // A copy of x is passed to the function, leaving the + /// // original unaffected + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DROP_COPY, + correctness, + "calls to `std::mem::drop` with a value that implements Copy" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `std::mem::forget` with a value that + /// derives the Copy trait + /// + /// ### Why is this bad? + /// Calling `std::mem::forget` [does nothing for types that + /// implement Copy](https://doc.rust-lang.org/std/mem/fn.drop.html) since the + /// value will be copied and moved into the function on invocation. + /// + /// An alternative, but also valid, explanation is that Copy types do not + /// implement + /// the Drop trait, which means they have no destructors. Without a destructor, + /// there + /// is nothing for `std::mem::forget` to ignore. + /// + /// ### Example + /// ```rust + /// let x: i32 = 42; // i32 implements Copy + /// std::mem::forget(x) // A copy of x is passed to the function, leaving the + /// // original unaffected + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FORGET_COPY, + correctness, + "calls to `std::mem::forget` with a value that implements Copy" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `std::mem::drop` with a value that does not implement `Drop`. + /// + /// ### Why is this bad? + /// Calling `std::mem::drop` is no different than dropping such a type. A different value may + /// have been intended. + /// + /// ### Example + /// ```rust + /// struct Foo; + /// let x = Foo; + /// std::mem::drop(x); + /// ``` + #[clippy::version = "1.62.0"] + pub DROP_NON_DROP, + suspicious, + "call to `std::mem::drop` with a value which does not implement `Drop`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `std::mem::forget` with a value that does not implement `Drop`. + /// + /// ### Why is this bad? + /// Calling `std::mem::forget` is no different than dropping such a type. A different value may + /// have been intended. + /// + /// ### Example + /// ```rust + /// struct Foo; + /// let x = Foo; + /// std::mem::forget(x); + /// ``` + #[clippy::version = "1.62.0"] + pub FORGET_NON_DROP, + suspicious, + "call to `std::mem::forget` with a value which does not implement `Drop`" +} + +declare_clippy_lint! { + /// ### What it does + /// Prevents the safe `std::mem::drop` function from being called on `std::mem::ManuallyDrop`. + /// + /// ### Why is this bad? + /// The safe `drop` function does not drop the inner value of a `ManuallyDrop`. + /// + /// ### Known problems + /// Does not catch cases if the user binds `std::mem::drop` + /// to a different name and calls it that way. + /// + /// ### Example + /// ```rust + /// struct S; + /// drop(std::mem::ManuallyDrop::new(S)); + /// ``` + /// Use instead: + /// ```rust + /// struct S; + /// unsafe { + /// std::mem::ManuallyDrop::drop(&mut std::mem::ManuallyDrop::new(S)); + /// } + /// ``` + #[clippy::version = "1.49.0"] + pub UNDROPPED_MANUALLY_DROPS, + correctness, + "use of safe `std::mem::drop` function to drop a std::mem::ManuallyDrop, which will not drop the inner value" +} + +const DROP_REF_SUMMARY: &str = "calls to `std::mem::drop` with a reference instead of an owned value. \ + Dropping a reference does nothing"; +const FORGET_REF_SUMMARY: &str = "calls to `std::mem::forget` with a reference instead of an owned value. \ + Forgetting a reference does nothing"; +const DROP_COPY_SUMMARY: &str = "calls to `std::mem::drop` with a value that implements `Copy`. \ + Dropping a copy leaves the original intact"; +const FORGET_COPY_SUMMARY: &str = "calls to `std::mem::forget` with a value that implements `Copy`. \ + Forgetting a copy leaves the original intact"; +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`. \ + Forgetting such a type is the same as dropping it"; + +declare_lint_pass!(DropForgetRef => [ + DROP_REF, + FORGET_REF, + DROP_COPY, + FORGET_COPY, + DROP_NON_DROP, + FORGET_NON_DROP, + UNDROPPED_MANUALLY_DROPS +]); + +impl<'tcx> LateLintPass<'tcx> for DropForgetRef { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Call(path, [arg]) = expr.kind + && let ExprKind::Path(ref qpath) = path.kind + && let Some(def_id) = cx.qpath_res(qpath, path.hir_id).opt_def_id() + && let Some(fn_name) = cx.tcx.get_diagnostic_name(def_id) + { + let arg_ty = cx.typeck_results().expr_ty(arg); + let (lint, msg) = match fn_name { + sym::mem_drop if arg_ty.is_ref() => (DROP_REF, DROP_REF_SUMMARY), + sym::mem_forget if arg_ty.is_ref() => (FORGET_REF, FORGET_REF_SUMMARY), + sym::mem_drop if is_copy(cx, arg_ty) => (DROP_COPY, DROP_COPY_SUMMARY), + sym::mem_forget if is_copy(cx, arg_ty) => (FORGET_COPY, FORGET_COPY_SUMMARY), + sym::mem_drop if is_type_lang_item(cx, arg_ty, LangItem::ManuallyDrop) => { + span_lint_and_help( + cx, + UNDROPPED_MANUALLY_DROPS, + expr.span, + "the inner value of this ManuallyDrop will not be dropped", + None, + "to drop a `ManuallyDrop<T>`, use std::mem::ManuallyDrop::drop", + ); + return; + } + sym::mem_drop + if !(arg_ty.needs_drop(cx.tcx, cx.param_env) + || is_must_use_func_call(cx, arg) + || is_must_use_ty(cx, arg_ty)) => + { + (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) + }, + _ => return, + }; + span_lint_and_note( + cx, + lint, + expr.span, + msg, + Some(arg.span), + &format!("argument has type `{}`", arg_ty), + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/duplicate_mod.rs b/src/tools/clippy/clippy_lints/src/duplicate_mod.rs new file mode 100644 index 000000000..e1eb3b632 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/duplicate_mod.rs @@ -0,0 +1,128 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{Crate, Inline, Item, ItemKind, ModKind}; +use rustc_errors::MultiSpan; +use rustc_lint::{EarlyContext, EarlyLintPass, Level, LintContext}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{FileName, Span}; +use std::collections::BTreeMap; +use std::path::PathBuf; + +declare_clippy_lint! { + /// ### What it does + /// Checks for files that are included as modules multiple times. + /// + /// ### Why is this bad? + /// Loading a file as a module more than once causes it to be compiled + /// multiple times, taking longer and putting duplicate content into the + /// module tree. + /// + /// ### Example + /// ```rust,ignore + /// // lib.rs + /// mod a; + /// mod b; + /// ``` + /// ```rust,ignore + /// // a.rs + /// #[path = "./b.rs"] + /// mod b; + /// ``` + /// + /// Use instead: + /// + /// ```rust,ignore + /// // lib.rs + /// mod a; + /// mod b; + /// ``` + /// ```rust,ignore + /// // a.rs + /// use crate::b; + /// ``` + #[clippy::version = "1.62.0"] + pub DUPLICATE_MOD, + suspicious, + "file loaded as module multiple times" +} + +#[derive(PartialOrd, Ord, PartialEq, Eq)] +struct Modules { + local_path: PathBuf, + spans: Vec<Span>, + lint_levels: Vec<Level>, +} + +#[derive(Default)] +pub struct DuplicateMod { + /// map from the canonicalized path to `Modules`, `BTreeMap` to make the + /// order deterministic for tests + modules: BTreeMap<PathBuf, Modules>, +} + +impl_lint_pass!(DuplicateMod => [DUPLICATE_MOD]); + +impl EarlyLintPass for DuplicateMod { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if let ItemKind::Mod(_, ModKind::Loaded(_, Inline::No, mod_spans)) = &item.kind + && let FileName::Real(real) = cx.sess().source_map().span_to_filename(mod_spans.inner_span) + && let Some(local_path) = real.into_local_path() + && let Ok(absolute_path) = local_path.canonicalize() + { + let modules = self.modules.entry(absolute_path).or_insert(Modules { + local_path, + spans: Vec::new(), + lint_levels: Vec::new(), + }); + modules.spans.push(item.span_with_attributes()); + modules.lint_levels.push(cx.get_lint_level(DUPLICATE_MOD)); + } + } + + fn check_crate_post(&mut self, cx: &EarlyContext<'_>, _: &Crate) { + for Modules { + local_path, + spans, + lint_levels, + } in self.modules.values() + { + if spans.len() < 2 { + continue; + } + + // At this point the lint would be emitted + assert_eq!(spans.len(), lint_levels.len()); + let spans: Vec<_> = spans + .iter() + .zip(lint_levels) + .filter_map(|(span, lvl)| { + if let Some(id) = lvl.get_expectation_id() { + cx.fulfill_expectation(id); + } + + (!matches!(lvl, Level::Allow | Level::Expect(_))).then_some(*span) + }) + .collect(); + + if spans.len() < 2 { + continue; + } + + let mut multi_span = MultiSpan::from_spans(spans.clone()); + let (&first, duplicates) = spans.split_first().unwrap(); + + multi_span.push_span_label(first, "first loaded here"); + for &duplicate in duplicates { + multi_span.push_span_label(duplicate, "loaded again here"); + } + + span_lint_and_help( + cx, + DUPLICATE_MOD, + multi_span, + &format!("file is loaded as a module multiple times: `{}`", local_path.display()), + None, + "replace all but one `mod` item with `use` items", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/else_if_without_else.rs b/src/tools/clippy/clippy_lints/src/else_if_without_else.rs new file mode 100644 index 000000000..bf4488570 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/else_if_without_else.rs @@ -0,0 +1,72 @@ +//! Lint on if expressions with an else if, but without a final else branch. + +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{Expr, ExprKind}; +use rustc_lint::{EarlyContext, EarlyLintPass, 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 usage of if expressions with an `else if` branch, + /// but without a final `else` branch. + /// + /// ### Why is this bad? + /// Some coding guidelines require this (e.g., MISRA-C:2004 Rule 14.10). + /// + /// ### Example + /// ```rust + /// # fn a() {} + /// # fn b() {} + /// # let x: i32 = 1; + /// if x.is_positive() { + /// a(); + /// } else if x.is_negative() { + /// b(); + /// } + /// ``` + /// + /// Use instead: + /// + /// ```rust + /// # fn a() {} + /// # fn b() {} + /// # let x: i32 = 1; + /// if x.is_positive() { + /// a(); + /// } else if x.is_negative() { + /// b(); + /// } else { + /// // We don't care about zero. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ELSE_IF_WITHOUT_ELSE, + restriction, + "`if` expression with an `else if`, but without a final `else` branch" +} + +declare_lint_pass!(ElseIfWithoutElse => [ELSE_IF_WITHOUT_ELSE]); + +impl EarlyLintPass for ElseIfWithoutElse { + fn check_expr(&mut self, cx: &EarlyContext<'_>, mut item: &Expr) { + if in_external_macro(cx.sess(), item.span) { + return; + } + + while let ExprKind::If(_, _, Some(ref els)) = item.kind { + if let ExprKind::If(_, _, None) = els.kind { + span_lint_and_help( + cx, + ELSE_IF_WITHOUT_ELSE, + els.span, + "`if` expression with an `else if`, but without a final `else`", + None, + "add an `else` block here", + ); + } + + item = els; + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/empty_drop.rs b/src/tools/clippy/clippy_lints/src/empty_drop.rs new file mode 100644 index 000000000..ec063c0f7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/empty_drop.rs @@ -0,0 +1,65 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, peel_blocks}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Body, ExprKind, Impl, ImplItemKind, Item, ItemKind, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for empty `Drop` implementations. + /// + /// ### Why is this bad? + /// Empty `Drop` implementations have no effect when dropping an instance of the type. They are + /// most likely useless. However, an empty `Drop` implementation prevents a type from being + /// destructured, which might be the intention behind adding the implementation as a marker. + /// + /// ### Example + /// ```rust + /// struct S; + /// + /// impl Drop for S { + /// fn drop(&mut self) {} + /// } + /// ``` + /// Use instead: + /// ```rust + /// struct S; + /// ``` + #[clippy::version = "1.62.0"] + pub EMPTY_DROP, + restriction, + "empty `Drop` implementations" +} +declare_lint_pass!(EmptyDrop => [EMPTY_DROP]); + +impl LateLintPass<'_> for EmptyDrop { + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if_chain! { + if let ItemKind::Impl(Impl { + of_trait: Some(ref trait_ref), + items: [child], + .. + }) = item.kind; + if trait_ref.trait_def_id() == cx.tcx.lang_items().drop_trait(); + if let impl_item_hir = child.id.hir_id(); + if let Some(Node::ImplItem(impl_item)) = cx.tcx.hir().find(impl_item_hir); + if let ImplItemKind::Fn(_, b) = &impl_item.kind; + if let Body { value: func_expr, .. } = cx.tcx.hir().body(*b); + let func_expr = peel_blocks(func_expr); + if let ExprKind::Block(block, _) = func_expr.kind; + if block.stmts.is_empty() && block.expr.is_none(); + then { + span_lint_and_sugg( + cx, + EMPTY_DROP, + item.span, + "empty drop implementation", + "try removing this impl", + String::new(), + Applicability::MaybeIncorrect + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/empty_enum.rs b/src/tools/clippy/clippy_lints/src/empty_enum.rs new file mode 100644 index 000000000..bbebc0244 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/empty_enum.rs @@ -0,0 +1,67 @@ +//! lint when there is an enum with no variants + +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `enum`s with no variants. + /// + /// As of this writing, the `never_type` is still a + /// nightly-only experimental API. Therefore, this lint is only triggered + /// if the `never_type` is enabled. + /// + /// ### Why is this bad? + /// If you want to introduce a type which + /// can't be instantiated, you should use `!` (the primitive type "never"), + /// or a wrapper around it, because `!` has more extensive + /// compiler support (type inference, etc...) and wrappers + /// around it are the conventional way to define an uninhabited type. + /// For further information visit [never type documentation](https://doc.rust-lang.org/std/primitive.never.html) + /// + /// + /// ### Example + /// ```rust + /// enum Test {} + /// ``` + /// + /// Use instead: + /// ```rust + /// #![feature(never_type)] + /// + /// struct Test(!); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EMPTY_ENUM, + pedantic, + "enum with no variants" +} + +declare_lint_pass!(EmptyEnum => [EMPTY_ENUM]); + +impl<'tcx> LateLintPass<'tcx> for EmptyEnum { + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + // Only suggest the `never_type` if the feature is enabled + if !cx.tcx.features().never_type { + return; + } + + if let ItemKind::Enum(..) = item.kind { + let ty = cx.tcx.type_of(item.def_id); + let adt = ty.ty_adt_def().expect("already checked whether this is an enum"); + if adt.variants().is_empty() { + span_lint_and_help( + cx, + EMPTY_ENUM, + item.span, + "enum with no variants", + None, + "consider using the uninhabited type `!` (never type) or a wrapper \ + around it to introduce a type which can't be instantiated", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/empty_structs_with_brackets.rs b/src/tools/clippy/clippy_lints/src/empty_structs_with_brackets.rs new file mode 100644 index 000000000..08bf80a42 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/empty_structs_with_brackets.rs @@ -0,0 +1,99 @@ +use clippy_utils::{diagnostics::span_lint_and_then, source::snippet_opt}; +use rustc_ast::ast::{Item, ItemKind, VariantData}; +use rustc_errors::Applicability; +use rustc_lexer::TokenKind; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Finds structs without fields (a so-called "empty struct") that are declared with brackets. + /// + /// ### Why is this bad? + /// Empty brackets after a struct declaration can be omitted. + /// + /// ### Example + /// ```rust + /// struct Cookie {} + /// ``` + /// Use instead: + /// ```rust + /// struct Cookie; + /// ``` + #[clippy::version = "1.62.0"] + pub EMPTY_STRUCTS_WITH_BRACKETS, + restriction, + "finds struct declarations with empty brackets" +} +declare_lint_pass!(EmptyStructsWithBrackets => [EMPTY_STRUCTS_WITH_BRACKETS]); + +impl EarlyLintPass for EmptyStructsWithBrackets { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + let span_after_ident = item.span.with_lo(item.ident.span.hi()); + + if let ItemKind::Struct(var_data, _) = &item.kind + && has_brackets(var_data) + && has_no_fields(cx, var_data, span_after_ident) { + span_lint_and_then( + cx, + EMPTY_STRUCTS_WITH_BRACKETS, + span_after_ident, + "found empty brackets on struct declaration", + |diagnostic| { + diagnostic.span_suggestion_hidden( + span_after_ident, + "remove the brackets", + ";", + Applicability::MachineApplicable); + }, + ); + } + } +} + +fn has_no_ident_token(braces_span_str: &str) -> bool { + !rustc_lexer::tokenize(braces_span_str).any(|t| t.kind == TokenKind::Ident) +} + +fn has_brackets(var_data: &VariantData) -> bool { + !matches!(var_data, VariantData::Unit(_)) +} + +fn has_no_fields(cx: &EarlyContext<'_>, var_data: &VariantData, braces_span: Span) -> bool { + if !var_data.fields().is_empty() { + return false; + } + + // there might still be field declarations hidden from the AST + // (conditionally compiled code using #[cfg(..)]) + + let Some(braces_span_str) = snippet_opt(cx, braces_span) else { + return false; + }; + + has_no_ident_token(braces_span_str.as_ref()) +} + +#[cfg(test)] +mod unit_test { + use super::*; + + #[test] + fn test_has_no_ident_token() { + let input = "{ field: u8 }"; + assert!(!has_no_ident_token(input)); + + let input = "(u8, String);"; + assert!(!has_no_ident_token(input)); + + let input = " { + // test = 5 + } + "; + assert!(has_no_ident_token(input)); + + let input = " ();"; + assert!(has_no_ident_token(input)); + } +} diff --git a/src/tools/clippy/clippy_lints/src/entry.rs b/src/tools/clippy/clippy_lints/src/entry.rs new file mode 100644 index 000000000..4e3ae4c96 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/entry.rs @@ -0,0 +1,658 @@ +use clippy_utils::higher; +use clippy_utils::{ + can_move_expr_to_closure_no_visit, + diagnostics::span_lint_and_sugg, + is_expr_final_block_expr, is_expr_used_or_unified, match_def_path, paths, peel_hir_expr_while, + source::{reindent_multiline, snippet_indent, snippet_with_applicability, snippet_with_context}, + SpanlessEq, +}; +use core::fmt::Write; +use rustc_errors::Applicability; +use rustc_hir::{ + hir_id::HirIdSet, + intravisit::{walk_expr, Visitor}, + Block, Expr, ExprKind, Guard, HirId, Let, Pat, Stmt, StmtKind, UnOp, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{Span, SyntaxContext, DUMMY_SP}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for uses of `contains_key` + `insert` on `HashMap` + /// or `BTreeMap`. + /// + /// ### Why is this bad? + /// Using `entry` is more efficient. + /// + /// ### Known problems + /// The suggestion may have type inference errors in some cases. e.g. + /// ```rust + /// let mut map = std::collections::HashMap::new(); + /// let _ = if !map.contains_key(&0) { + /// map.insert(0, 0) + /// } else { + /// None + /// }; + /// ``` + /// + /// ### Example + /// ```rust + /// # use std::collections::HashMap; + /// # let mut map = HashMap::new(); + /// # let k = 1; + /// # let v = 1; + /// if !map.contains_key(&k) { + /// map.insert(k, v); + /// } + /// ``` + /// Use instead: + /// ```rust + /// # use std::collections::HashMap; + /// # let mut map = HashMap::new(); + /// # let k = 1; + /// # let v = 1; + /// map.entry(k).or_insert(v); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MAP_ENTRY, + perf, + "use of `contains_key` followed by `insert` on a `HashMap` or `BTreeMap`" +} + +declare_lint_pass!(HashMapPass => [MAP_ENTRY]); + +impl<'tcx> LateLintPass<'tcx> for HashMapPass { + #[expect(clippy::too_many_lines)] + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (cond_expr, then_expr, else_expr) = match higher::If::hir(expr) { + Some(higher::If { cond, then, r#else }) => (cond, then, r#else), + _ => return, + }; + + let (map_ty, contains_expr) = match try_parse_contains(cx, cond_expr) { + Some(x) => x, + None => return, + }; + + let then_search = match find_insert_calls(cx, &contains_expr, then_expr) { + Some(x) => x, + None => return, + }; + + let mut app = Applicability::MachineApplicable; + let map_str = snippet_with_context(cx, contains_expr.map.span, contains_expr.call_ctxt, "..", &mut app).0; + let key_str = snippet_with_context(cx, contains_expr.key.span, contains_expr.call_ctxt, "..", &mut app).0; + let sugg = if let Some(else_expr) = else_expr { + let else_search = match find_insert_calls(cx, &contains_expr, else_expr) { + Some(search) => search, + None => return, + }; + + if then_search.edits.is_empty() && else_search.edits.is_empty() { + // No insertions + return; + } else if then_search.edits.is_empty() || else_search.edits.is_empty() { + // if .. { insert } else { .. } or if .. { .. } else { insert } + let ((then_str, entry_kind), else_str) = match (else_search.edits.is_empty(), contains_expr.negated) { + (true, true) => ( + then_search.snippet_vacant(cx, then_expr.span, &mut app), + snippet_with_applicability(cx, else_expr.span, "{ .. }", &mut app), + ), + (true, false) => ( + then_search.snippet_occupied(cx, then_expr.span, &mut app), + snippet_with_applicability(cx, else_expr.span, "{ .. }", &mut app), + ), + (false, true) => ( + else_search.snippet_occupied(cx, else_expr.span, &mut app), + snippet_with_applicability(cx, then_expr.span, "{ .. }", &mut app), + ), + (false, false) => ( + else_search.snippet_vacant(cx, else_expr.span, &mut app), + snippet_with_applicability(cx, then_expr.span, "{ .. }", &mut app), + ), + }; + format!( + "if let {}::{} = {}.entry({}) {} else {}", + map_ty.entry_path(), + entry_kind, + map_str, + key_str, + then_str, + else_str, + ) + } else { + // if .. { insert } else { insert } + let ((then_str, then_entry), (else_str, else_entry)) = if contains_expr.negated { + ( + then_search.snippet_vacant(cx, then_expr.span, &mut app), + else_search.snippet_occupied(cx, else_expr.span, &mut app), + ) + } else { + ( + then_search.snippet_occupied(cx, then_expr.span, &mut app), + else_search.snippet_vacant(cx, else_expr.span, &mut app), + ) + }; + let indent_str = snippet_indent(cx, expr.span); + let indent_str = indent_str.as_deref().unwrap_or(""); + format!( + "match {}.entry({}) {{\n{indent} {entry}::{} => {}\n\ + {indent} {entry}::{} => {}\n{indent}}}", + map_str, + key_str, + then_entry, + reindent_multiline(then_str.into(), true, Some(4 + indent_str.len())), + else_entry, + reindent_multiline(else_str.into(), true, Some(4 + indent_str.len())), + entry = map_ty.entry_path(), + indent = indent_str, + ) + } + } else { + if then_search.edits.is_empty() { + // no insertions + return; + } + + // if .. { insert } + if !then_search.allow_insert_closure { + let (body_str, entry_kind) = if contains_expr.negated { + then_search.snippet_vacant(cx, then_expr.span, &mut app) + } else { + then_search.snippet_occupied(cx, then_expr.span, &mut app) + }; + format!( + "if let {}::{} = {}.entry({}) {}", + map_ty.entry_path(), + entry_kind, + map_str, + key_str, + body_str, + ) + } else if let Some(insertion) = then_search.as_single_insertion() { + let value_str = snippet_with_context(cx, insertion.value.span, then_expr.span.ctxt(), "..", &mut app).0; + if contains_expr.negated { + if insertion.value.can_have_side_effects() { + format!("{}.entry({}).or_insert_with(|| {});", map_str, key_str, value_str) + } else { + format!("{}.entry({}).or_insert({});", map_str, key_str, value_str) + } + } else { + // TODO: suggest using `if let Some(v) = map.get_mut(k) { .. }` here. + // This would need to be a different lint. + return; + } + } else { + let block_str = then_search.snippet_closure(cx, then_expr.span, &mut app); + if contains_expr.negated { + format!("{}.entry({}).or_insert_with(|| {});", map_str, key_str, block_str) + } else { + // TODO: suggest using `if let Some(v) = map.get_mut(k) { .. }` here. + // This would need to be a different lint. + return; + } + } + }; + + span_lint_and_sugg( + cx, + MAP_ENTRY, + expr.span, + &format!("usage of `contains_key` followed by `insert` on a `{}`", map_ty.name()), + "try this", + sugg, + app, + ); + } +} + +#[derive(Clone, Copy)] +enum MapType { + Hash, + BTree, +} +impl MapType { + fn name(self) -> &'static str { + match self { + Self::Hash => "HashMap", + Self::BTree => "BTreeMap", + } + } + fn entry_path(self) -> &'static str { + match self { + Self::Hash => "std::collections::hash_map::Entry", + Self::BTree => "std::collections::btree_map::Entry", + } + } +} + +struct ContainsExpr<'tcx> { + negated: bool, + map: &'tcx Expr<'tcx>, + key: &'tcx Expr<'tcx>, + call_ctxt: SyntaxContext, +} +fn try_parse_contains<'tcx>(cx: &LateContext<'_>, expr: &'tcx Expr<'_>) -> Option<(MapType, ContainsExpr<'tcx>)> { + let mut negated = false; + let expr = peel_hir_expr_while(expr, |e| match e.kind { + ExprKind::Unary(UnOp::Not, e) => { + negated = !negated; + Some(e) + }, + _ => None, + }); + match expr.kind { + ExprKind::MethodCall( + _, + [ + map, + Expr { + kind: ExprKind::AddrOf(_, _, key), + span: key_span, + .. + }, + ], + _, + ) if key_span.ctxt() == expr.span.ctxt() => { + let id = cx.typeck_results().type_dependent_def_id(expr.hir_id)?; + let expr = ContainsExpr { + negated, + map, + key, + call_ctxt: expr.span.ctxt(), + }; + if match_def_path(cx, id, &paths::BTREEMAP_CONTAINS_KEY) { + Some((MapType::BTree, expr)) + } else if match_def_path(cx, id, &paths::HASHMAP_CONTAINS_KEY) { + Some((MapType::Hash, expr)) + } else { + None + } + }, + _ => None, + } +} + +struct InsertExpr<'tcx> { + map: &'tcx Expr<'tcx>, + key: &'tcx Expr<'tcx>, + value: &'tcx Expr<'tcx>, +} +fn try_parse_insert<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<InsertExpr<'tcx>> { + if let ExprKind::MethodCall(_, [map, key, value], _) = expr.kind { + let id = cx.typeck_results().type_dependent_def_id(expr.hir_id)?; + if match_def_path(cx, id, &paths::BTREEMAP_INSERT) || match_def_path(cx, id, &paths::HASHMAP_INSERT) { + Some(InsertExpr { map, key, value }) + } else { + None + } + } else { + None + } +} + +/// An edit that will need to be made to move the expression to use the entry api +#[derive(Clone, Copy)] +enum Edit<'tcx> { + /// A semicolon that needs to be removed. Used to create a closure for `insert_with`. + RemoveSemi(Span), + /// An insertion into the map. + Insertion(Insertion<'tcx>), +} +impl<'tcx> Edit<'tcx> { + fn as_insertion(self) -> Option<Insertion<'tcx>> { + if let Self::Insertion(i) = self { Some(i) } else { None } + } +} +#[derive(Clone, Copy)] +struct Insertion<'tcx> { + call: &'tcx Expr<'tcx>, + value: &'tcx Expr<'tcx>, +} + +/// This visitor needs to do a multiple things: +/// * Find all usages of the map. An insertion can only be made before any other usages of the map. +/// * Determine if there's an insertion using the same key. There's no need for the entry api +/// otherwise. +/// * Determine if the final statement executed is an insertion. This is needed to use +/// `or_insert_with`. +/// * Determine if there's any sub-expression that can't be placed in a closure. +/// * Determine if there's only a single insert statement. `or_insert` can be used in this case. +#[expect(clippy::struct_excessive_bools)] +struct InsertSearcher<'cx, 'tcx> { + cx: &'cx LateContext<'tcx>, + /// The map expression used in the contains call. + map: &'tcx Expr<'tcx>, + /// The key expression used in the contains call. + key: &'tcx Expr<'tcx>, + /// The context of the top level block. All insert calls must be in the same context. + ctxt: SyntaxContext, + /// Whether this expression can be safely moved into a closure. + allow_insert_closure: bool, + /// Whether this expression can use the entry api. + can_use_entry: bool, + /// Whether this expression is the final expression in this code path. This may be a statement. + in_tail_pos: bool, + // Is this expression a single insert. A slightly better suggestion can be made in this case. + is_single_insert: bool, + /// If the visitor has seen the map being used. + is_map_used: bool, + /// The locations where changes need to be made for the suggestion. + edits: Vec<Edit<'tcx>>, + /// A stack of loops the visitor is currently in. + loops: Vec<HirId>, + /// Local variables created in the expression. These don't need to be captured. + locals: HirIdSet, +} +impl<'tcx> InsertSearcher<'_, 'tcx> { + /// Visit the expression as a branch in control flow. Multiple insert calls can be used, but + /// only if they are on separate code paths. This will return whether the map was used in the + /// given expression. + fn visit_cond_arm(&mut self, e: &'tcx Expr<'_>) -> bool { + let is_map_used = self.is_map_used; + let in_tail_pos = self.in_tail_pos; + self.visit_expr(e); + let res = self.is_map_used; + self.is_map_used = is_map_used; + self.in_tail_pos = in_tail_pos; + res + } + + /// Visits an expression which is not itself in a tail position, but other sibling expressions + /// may be. e.g. if conditions + fn visit_non_tail_expr(&mut self, e: &'tcx Expr<'_>) { + let in_tail_pos = self.in_tail_pos; + self.in_tail_pos = false; + self.visit_expr(e); + self.in_tail_pos = in_tail_pos; + } +} +impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> { + fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) { + match stmt.kind { + StmtKind::Semi(e) => { + self.visit_expr(e); + + if self.in_tail_pos && self.allow_insert_closure { + // The spans are used to slice the top level expression into multiple parts. This requires that + // they all come from the same part of the source code. + if stmt.span.ctxt() == self.ctxt && e.span.ctxt() == self.ctxt { + self.edits + .push(Edit::RemoveSemi(stmt.span.trim_start(e.span).unwrap_or(DUMMY_SP))); + } else { + self.allow_insert_closure = false; + } + } + }, + StmtKind::Expr(e) => self.visit_expr(e), + StmtKind::Local(l) => { + self.visit_pat(l.pat); + if let Some(e) = l.init { + self.allow_insert_closure &= !self.in_tail_pos; + self.in_tail_pos = false; + self.is_single_insert = false; + self.visit_expr(e); + } + }, + StmtKind::Item(_) => { + self.allow_insert_closure &= !self.in_tail_pos; + self.is_single_insert = false; + }, + } + } + + fn visit_block(&mut self, block: &'tcx Block<'_>) { + // If the block is in a tail position, then the last expression (possibly a statement) is in the + // tail position. The rest, however, are not. + match (block.stmts, block.expr) { + ([], None) => { + self.allow_insert_closure &= !self.in_tail_pos; + }, + ([], Some(expr)) => self.visit_expr(expr), + (stmts, Some(expr)) => { + let in_tail_pos = self.in_tail_pos; + self.in_tail_pos = false; + for stmt in stmts { + self.visit_stmt(stmt); + } + self.in_tail_pos = in_tail_pos; + self.visit_expr(expr); + }, + ([stmts @ .., stmt], None) => { + let in_tail_pos = self.in_tail_pos; + self.in_tail_pos = false; + for stmt in stmts { + self.visit_stmt(stmt); + } + self.in_tail_pos = in_tail_pos; + self.visit_stmt(stmt); + }, + } + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if !self.can_use_entry { + return; + } + + match try_parse_insert(self.cx, expr) { + Some(insert_expr) if SpanlessEq::new(self.cx).eq_expr(self.map, insert_expr.map) => { + // Multiple inserts, inserts with a different key, and inserts from a macro can't use the entry api. + if self.is_map_used + || !SpanlessEq::new(self.cx).eq_expr(self.key, insert_expr.key) + || expr.span.ctxt() != self.ctxt + { + self.can_use_entry = false; + return; + } + + self.edits.push(Edit::Insertion(Insertion { + call: expr, + value: insert_expr.value, + })); + self.is_map_used = true; + self.allow_insert_closure &= self.in_tail_pos; + + // The value doesn't affect whether there is only a single insert expression. + let is_single_insert = self.is_single_insert; + self.visit_non_tail_expr(insert_expr.value); + self.is_single_insert = is_single_insert; + }, + _ if SpanlessEq::new(self.cx).eq_expr(self.map, expr) => { + self.is_map_used = true; + }, + _ => match expr.kind { + ExprKind::If(cond_expr, then_expr, Some(else_expr)) => { + self.is_single_insert = false; + self.visit_non_tail_expr(cond_expr); + // Each branch may contain it's own insert expression. + let mut is_map_used = self.visit_cond_arm(then_expr); + is_map_used |= self.visit_cond_arm(else_expr); + self.is_map_used = is_map_used; + }, + ExprKind::Match(scrutinee_expr, arms, _) => { + self.is_single_insert = false; + self.visit_non_tail_expr(scrutinee_expr); + // Each branch may contain it's own insert expression. + let mut is_map_used = self.is_map_used; + for arm in arms { + self.visit_pat(arm.pat); + if let Some(Guard::If(guard) | Guard::IfLet(&Let { init: guard, .. })) = arm.guard { + self.visit_non_tail_expr(guard); + } + is_map_used |= self.visit_cond_arm(arm.body); + } + self.is_map_used = is_map_used; + }, + ExprKind::Loop(block, ..) => { + self.loops.push(expr.hir_id); + self.is_single_insert = false; + self.allow_insert_closure &= !self.in_tail_pos; + // Don't allow insertions inside of a loop. + let edit_len = self.edits.len(); + self.visit_block(block); + if self.edits.len() != edit_len { + self.can_use_entry = false; + } + self.loops.pop(); + }, + ExprKind::Block(block, _) => self.visit_block(block), + ExprKind::InlineAsm(_) => { + self.can_use_entry = false; + }, + _ => { + self.allow_insert_closure &= !self.in_tail_pos; + self.allow_insert_closure &= + can_move_expr_to_closure_no_visit(self.cx, expr, &self.loops, &self.locals); + // Sub expressions are no longer in the tail position. + self.is_single_insert = false; + self.in_tail_pos = false; + walk_expr(self, expr); + }, + }, + } + } + + fn visit_pat(&mut self, p: &'tcx Pat<'tcx>) { + p.each_binding_or_first(&mut |_, id, _, _| { + self.locals.insert(id); + }); + } +} + +struct InsertSearchResults<'tcx> { + edits: Vec<Edit<'tcx>>, + allow_insert_closure: bool, + is_single_insert: bool, +} +impl<'tcx> InsertSearchResults<'tcx> { + fn as_single_insertion(&self) -> Option<Insertion<'tcx>> { + self.is_single_insert.then(|| self.edits[0].as_insertion().unwrap()) + } + + fn snippet( + &self, + cx: &LateContext<'_>, + mut span: Span, + app: &mut Applicability, + write_wrapped: impl Fn(&mut String, Insertion<'_>, SyntaxContext, &mut Applicability), + ) -> String { + let ctxt = span.ctxt(); + let mut res = String::new(); + for insertion in self.edits.iter().filter_map(|e| e.as_insertion()) { + res.push_str(&snippet_with_applicability( + cx, + span.until(insertion.call.span), + "..", + app, + )); + if is_expr_used_or_unified(cx.tcx, insertion.call) { + write_wrapped(&mut res, insertion, ctxt, app); + } else { + let _ = write!( + res, + "e.insert({})", + snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0 + ); + } + span = span.trim_start(insertion.call.span).unwrap_or(DUMMY_SP); + } + res.push_str(&snippet_with_applicability(cx, span, "..", app)); + res + } + + fn snippet_occupied(&self, cx: &LateContext<'_>, span: Span, app: &mut Applicability) -> (String, &'static str) { + ( + self.snippet(cx, span, app, |res, insertion, ctxt, app| { + // Insertion into a map would return `Some(&mut value)`, but the entry returns `&mut value` + let _ = write!( + res, + "Some(e.insert({}))", + snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0 + ); + }), + "Occupied(mut e)", + ) + } + + fn snippet_vacant(&self, cx: &LateContext<'_>, span: Span, app: &mut Applicability) -> (String, &'static str) { + ( + self.snippet(cx, span, app, |res, insertion, ctxt, app| { + // Insertion into a map would return `None`, but the entry returns a mutable reference. + let _ = if is_expr_final_block_expr(cx.tcx, insertion.call) { + write!( + res, + "e.insert({});\n{}None", + snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0, + snippet_indent(cx, insertion.call.span).as_deref().unwrap_or(""), + ) + } else { + write!( + res, + "{{ e.insert({}); None }}", + snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0, + ) + }; + }), + "Vacant(e)", + ) + } + + fn snippet_closure(&self, cx: &LateContext<'_>, mut span: Span, app: &mut Applicability) -> String { + let ctxt = span.ctxt(); + let mut res = String::new(); + for edit in &self.edits { + match *edit { + Edit::Insertion(insertion) => { + // Cut out the value from `map.insert(key, value)` + res.push_str(&snippet_with_applicability( + cx, + span.until(insertion.call.span), + "..", + app, + )); + res.push_str(&snippet_with_context(cx, insertion.value.span, ctxt, "..", app).0); + span = span.trim_start(insertion.call.span).unwrap_or(DUMMY_SP); + }, + Edit::RemoveSemi(semi_span) => { + // Cut out the semicolon. This allows the value to be returned from the closure. + res.push_str(&snippet_with_applicability(cx, span.until(semi_span), "..", app)); + span = span.trim_start(semi_span).unwrap_or(DUMMY_SP); + }, + } + } + res.push_str(&snippet_with_applicability(cx, span, "..", app)); + res + } +} + +fn find_insert_calls<'tcx>( + cx: &LateContext<'tcx>, + contains_expr: &ContainsExpr<'tcx>, + expr: &'tcx Expr<'_>, +) -> Option<InsertSearchResults<'tcx>> { + let mut s = InsertSearcher { + cx, + map: contains_expr.map, + key: contains_expr.key, + ctxt: expr.span.ctxt(), + edits: Vec::new(), + is_map_used: false, + allow_insert_closure: true, + can_use_entry: true, + in_tail_pos: true, + is_single_insert: true, + loops: Vec::new(), + locals: HirIdSet::default(), + }; + s.visit_expr(expr); + let allow_insert_closure = s.allow_insert_closure; + let is_single_insert = s.is_single_insert; + let edits = s.edits; + s.can_use_entry.then_some(InsertSearchResults { + edits, + allow_insert_closure, + is_single_insert, + }) +} diff --git a/src/tools/clippy/clippy_lints/src/enum_clike.rs b/src/tools/clippy/clippy_lints/src/enum_clike.rs new file mode 100644 index 000000000..da6788882 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/enum_clike.rs @@ -0,0 +1,81 @@ +//! lint on C-like enums that are `repr(isize/usize)` and have values that +//! don't fit into an `i32` + +use clippy_utils::consts::{miri_to_const, Constant}; +use clippy_utils::diagnostics::span_lint; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::util::IntTypeExt; +use rustc_middle::ty::{self, IntTy, UintTy}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for C-like enumerations that are + /// `repr(isize/usize)` and have values that don't fit into an `i32`. + /// + /// ### Why is this bad? + /// This will truncate the variant value on 32 bit + /// architectures, but works fine on 64 bit. + /// + /// ### Example + /// ```rust + /// # #[cfg(target_pointer_width = "64")] + /// #[repr(usize)] + /// enum NonPortable { + /// X = 0x1_0000_0000, + /// Y = 0, + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ENUM_CLIKE_UNPORTABLE_VARIANT, + correctness, + "C-like enums that are `repr(isize/usize)` and have values that don't fit into an `i32`" +} + +declare_lint_pass!(UnportableVariant => [ENUM_CLIKE_UNPORTABLE_VARIANT]); + +impl<'tcx> LateLintPass<'tcx> for UnportableVariant { + #[expect(clippy::cast_possible_wrap)] + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if cx.tcx.data_layout.pointer_size.bits() != 64 { + return; + } + if let ItemKind::Enum(def, _) = &item.kind { + for var in def.variants { + if let Some(anon_const) = &var.disr_expr { + let def_id = cx.tcx.hir().body_owner_def_id(anon_const.body); + let mut ty = cx.tcx.type_of(def_id.to_def_id()); + let constant = cx + .tcx + .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 ty::Adt(adt, _) = ty.kind() { + if adt.is_enum() { + ty = adt.repr().discr_type().to_ty(cx.tcx); + } + } + match ty.kind() { + ty::Int(IntTy::Isize) => { + let val = ((val as i128) << 64) >> 64; + if i32::try_from(val).is_ok() { + continue; + } + }, + ty::Uint(UintTy::Usize) if val > u128::from(u32::MAX) => {}, + _ => continue, + } + span_lint( + cx, + ENUM_CLIKE_UNPORTABLE_VARIANT, + var.span, + "C-like enum variant discriminant is not portable to 32-bit targets", + ); + }; + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/enum_variants.rs b/src/tools/clippy/clippy_lints/src/enum_variants.rs new file mode 100644 index 000000000..cd36f9fcd --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/enum_variants.rs @@ -0,0 +1,306 @@ +//! lint on enum variants that are prefixed or suffixed by the same characters + +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +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_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Span; +use rustc_span::symbol::Symbol; + +declare_clippy_lint! { + /// ### What it does + /// Detects enumeration variants that are prefixed or suffixed + /// by the same characters. + /// + /// ### Why is this bad? + /// Enumeration variant names should specify their variant, + /// not repeat the enumeration name. + /// + /// ### Limitations + /// Characters with no casing will be considered when comparing prefixes/suffixes + /// This applies to numbers and non-ascii characters without casing + /// e.g. `Foo1` and `Foo2` is considered to have different prefixes + /// (the prefixes are `Foo1` and `Foo2` respectively), as also `Bar螃`, `Bar蟹` + /// + /// ### Example + /// ```rust + /// enum Cake { + /// BlackForestCake, + /// HummingbirdCake, + /// BattenbergCake, + /// } + /// ``` + /// Use instead: + /// ```rust + /// enum Cake { + /// BlackForest, + /// Hummingbird, + /// Battenberg, + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ENUM_VARIANT_NAMES, + style, + "enums where all variants share a prefix/postfix" +} + +declare_clippy_lint! { + /// ### What it does + /// Detects type names that are prefixed or suffixed by the + /// containing module's name. + /// + /// ### Why is this bad? + /// It requires the user to type the module name twice. + /// + /// ### Example + /// ```rust + /// mod cake { + /// struct BlackForestCake; + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// mod cake { + /// struct BlackForest; + /// } + /// ``` + #[clippy::version = "1.33.0"] + pub MODULE_NAME_REPETITIONS, + pedantic, + "type names prefixed/postfixed with their containing module's name" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for modules that have the same name as their + /// parent module + /// + /// ### Why is this bad? + /// A typical beginner mistake is to have `mod foo;` and + /// again `mod foo { .. + /// }` in `foo.rs`. + /// The expectation is that items inside the inner `mod foo { .. }` are then + /// available + /// through `foo::x`, but they are only available through + /// `foo::foo::x`. + /// If this is done on purpose, it would be better to choose a more + /// representative module name. + /// + /// ### Example + /// ```ignore + /// // lib.rs + /// mod foo; + /// // foo.rs + /// mod foo { + /// ... + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MODULE_INCEPTION, + style, + "modules that have the same name as their parent module" +} + +pub struct EnumVariantNames { + modules: Vec<(Symbol, String)>, + threshold: u64, + avoid_breaking_exported_api: bool, +} + +impl EnumVariantNames { + #[must_use] + pub fn new(threshold: u64, avoid_breaking_exported_api: bool) -> Self { + Self { + modules: Vec::new(), + threshold, + avoid_breaking_exported_api, + } + } +} + +impl_lint_pass!(EnumVariantNames => [ + ENUM_VARIANT_NAMES, + MODULE_NAME_REPETITIONS, + MODULE_INCEPTION +]); + +fn check_enum_start(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) { + let name = variant.ident.name.as_str(); + let item_name_chars = item_name.chars().count(); + + if count_match_start(item_name, name).char_count == item_name_chars + && name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase()) + && name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric()) + { + span_lint( + cx, + ENUM_VARIANT_NAMES, + variant.span, + "variant name starts with the enum's name", + ); + } +} + +fn check_enum_end(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) { + let name = variant.ident.name.as_str(); + let item_name_chars = item_name.chars().count(); + + if count_match_end(item_name, name).char_count == item_name_chars { + span_lint( + cx, + ENUM_VARIANT_NAMES, + variant.span, + "variant name ends with the enum's name", + ); + } +} + +fn check_variant(cx: &LateContext<'_>, threshold: u64, def: &EnumDef<'_>, item_name: &str, span: Span) { + if (def.variants.len() as u64) < threshold { + return; + } + + let first = &def.variants[0].ident.name.as_str(); + let mut pre = camel_case_split(first); + let mut post = pre.clone(); + post.reverse(); + for var in def.variants { + check_enum_start(cx, item_name, var); + check_enum_end(cx, item_name, var); + let name = var.ident.name.as_str(); + + let variant_split = camel_case_split(name); + if variant_split.len() == 1 { + return; + } + + pre = pre + .iter() + .zip(variant_split.iter()) + .take_while(|(a, b)| a == b) + .map(|e| *e.0) + .collect(); + post = post + .iter() + .zip(variant_split.iter().rev()) + .take_while(|(a, b)| a == b) + .map(|e| *e.0) + .collect(); + } + let (what, value) = match (have_no_extra_prefix(&pre), post.is_empty()) { + (true, true) => return, + (false, _) => ("pre", pre.join("")), + (true, false) => { + post.reverse(); + ("post", post.join("")) + }, + }; + span_lint_and_help( + cx, + ENUM_VARIANT_NAMES, + span, + &format!("all variants have the same {}fix: `{}`", what, value), + None, + &format!( + "remove the {}fixes and use full paths to \ + the variants instead of glob imports", + what + ), + ); +} + +#[must_use] +fn have_no_extra_prefix(prefixes: &[&str]) -> bool { + prefixes.iter().all(|p| p == &"" || p == &"_") +} + +#[must_use] +fn to_camel_case(item_name: &str) -> String { + let mut s = String::new(); + let mut up = true; + for c in item_name.chars() { + if c.is_uppercase() { + // we only turn snake case text into CamelCase + return item_name.to_string(); + } + if c == '_' { + up = true; + continue; + } + if up { + up = false; + s.extend(c.to_uppercase()); + } else { + s.push(c); + } + } + s +} + +impl LateLintPass<'_> for EnumVariantNames { + fn check_item_post(&mut self, _cx: &LateContext<'_>, _item: &Item<'_>) { + let last = self.modules.pop(); + assert!(last.is_some()); + } + + #[expect(clippy::similar_names)] + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + 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(&(ref mod_name, ref mod_camel)) = self.modules.last() { + // 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", + ); + } + } + // The `module_name_repetitions` lint should only trigger if the item has the module in its + // name. Having the same name is accepted. + if cx.tcx.visibility(item.def_id).is_public() && item_camel.len() > mod_camel.len() { + let matching = count_match_start(mod_camel, &item_camel); + let rmatching = count_match_end(mod_camel, &item_camel); + let nchars = mod_camel.chars().count(); + + let is_word_beginning = |c: char| c == '_' || c.is_uppercase() || c.is_numeric(); + + if matching.char_count == nchars { + match item_camel.chars().nth(nchars) { + Some(c) if is_word_beginning(c) => span_lint( + cx, + MODULE_NAME_REPETITIONS, + item.span, + "item name starts with its containing module's name", + ), + _ => (), + } + } + if rmatching.char_count == nchars { + span_lint( + cx, + MODULE_NAME_REPETITIONS, + item.span, + "item name ends with its containing module's name", + ); + } + } + } + } + } + if let ItemKind::Enum(ref def, _) = item.kind { + if !(self.avoid_breaking_exported_api && cx.access_levels.is_exported(item.def_id)) { + check_variant(cx, self.threshold, def, item_name, item.span); + } + } + self.modules.push((item.ident.name, item_camel)); + } +} diff --git a/src/tools/clippy/clippy_lints/src/equatable_if_let.rs b/src/tools/clippy/clippy_lints/src/equatable_if_let.rs new file mode 100644 index 000000000..fdfb821ac --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/equatable_if_let.rs @@ -0,0 +1,103 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_context; +use clippy_utils::ty::implements_trait; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, Pat, PatKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::Ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for pattern matchings that can be expressed using equality. + /// + /// ### Why is this bad? + /// + /// * It reads better and has less cognitive load because equality won't cause binding. + /// * It is a [Yoda condition](https://en.wikipedia.org/wiki/Yoda_conditions). Yoda conditions are widely + /// criticized for increasing the cognitive load of reading the code. + /// * Equality is a simple bool expression and can be merged with `&&` and `||` and + /// reuse if blocks + /// + /// ### Example + /// ```rust,ignore + /// if let Some(2) = x { + /// do_thing(); + /// } + /// ``` + /// Use instead: + /// ```rust,ignore + /// if x == Some(2) { + /// do_thing(); + /// } + /// ``` + #[clippy::version = "1.57.0"] + pub EQUATABLE_IF_LET, + nursery, + "using pattern matching instead of equality" +} + +declare_lint_pass!(PatternEquality => [EQUATABLE_IF_LET]); + +/// detects if pattern matches just one thing +fn unary_pattern(pat: &Pat<'_>) -> bool { + fn array_rec(pats: &[Pat<'_>]) -> bool { + pats.iter().all(unary_pattern) + } + match &pat.kind { + PatKind::Slice(_, _, _) | PatKind::Range(_, _, _) | PatKind::Binding(..) | PatKind::Wild | PatKind::Or(_) => { + false + }, + PatKind::Struct(_, a, etc) => !etc && a.iter().all(|x| unary_pattern(x.pat)), + PatKind::Tuple(a, etc) | PatKind::TupleStruct(_, a, etc) => !etc.is_some() && array_rec(a), + PatKind::Ref(x, _) | PatKind::Box(x) => unary_pattern(x), + PatKind::Path(_) | PatKind::Lit(_) => true, + } +} + +fn is_structural_partial_eq<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, other: Ty<'tcx>) -> bool { + if let Some(def_id) = cx.tcx.lang_items().eq_trait() { + implements_trait(cx, ty, def_id, &[other.into()]) + } else { + false + } +} + +impl<'tcx> LateLintPass<'tcx> for PatternEquality { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if_chain! { + if !in_external_macro(cx.sess(), expr.span); + if let ExprKind::Let(let_expr) = expr.kind; + if unary_pattern(let_expr.pat); + let exp_ty = cx.typeck_results().expr_ty(let_expr.init); + let pat_ty = cx.typeck_results().pat_ty(let_expr.pat); + if is_structural_partial_eq(cx, exp_ty, pat_ty); + then { + + let mut applicability = Applicability::MachineApplicable; + let pat_str = match let_expr.pat.kind { + PatKind::Struct(..) => format!( + "({})", + snippet_with_context(cx, let_expr.pat.span, expr.span.ctxt(), "..", &mut applicability).0, + ), + _ => snippet_with_context(cx, let_expr.pat.span, expr.span.ctxt(), "..", &mut applicability).0.to_string(), + }; + span_lint_and_sugg( + cx, + EQUATABLE_IF_LET, + expr.span, + "this pattern matching can be expressed using equality", + "try", + format!( + "{} == {}", + snippet_with_context(cx, let_expr.init.span, expr.span.ctxt(), "..", &mut applicability).0, + pat_str, + ), + applicability, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/escape.rs b/src/tools/clippy/clippy_lints/src/escape.rs new file mode 100644 index 000000000..1ac7bfba0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/escape.rs @@ -0,0 +1,199 @@ +use clippy_utils::diagnostics::span_lint_hir; +use clippy_utils::ty::contains_ty; +use rustc_hir::intravisit; +use rustc_hir::{self, AssocItemKind, Body, FnDecl, HirId, HirIdSet, Impl, ItemKind, Node, Pat, PatKind}; +use rustc_infer::infer::TyCtxtInferExt; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::mir::FakeReadCause; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::{self, TraitRef, Ty}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Span; +use rustc_span::symbol::kw; +use rustc_target::spec::abi::Abi; +use rustc_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, PlaceWithHirId}; + +#[derive(Copy, Clone)] +pub struct BoxedLocal { + pub too_large_for_stack: u64, +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `Box<T>` where an unboxed `T` would + /// work fine. + /// + /// ### Why is this bad? + /// This is an unnecessary allocation, and bad for + /// performance. It is only necessary to allocate if you wish to move the box + /// into something. + /// + /// ### Example + /// ```rust + /// # fn foo(bar: usize) {} + /// let x = Box::new(1); + /// foo(*x); + /// println!("{}", *x); + /// ``` + /// + /// Use instead: + /// ```rust + /// # fn foo(bar: usize) {} + /// let x = 1; + /// foo(x); + /// println!("{}", x); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub BOXED_LOCAL, + perf, + "using `Box<T>` where unnecessary" +} + +fn is_non_trait_box(ty: Ty<'_>) -> bool { + ty.is_box() && !ty.boxed_ty().is_trait() +} + +struct EscapeDelegate<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + set: HirIdSet, + trait_self_ty: Option<Ty<'tcx>>, + too_large_for_stack: u64, +} + +impl_lint_pass!(BoxedLocal => [BOXED_LOCAL]); + +impl<'tcx> LateLintPass<'tcx> for BoxedLocal { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + fn_kind: intravisit::FnKind<'tcx>, + _: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + _: Span, + hir_id: HirId, + ) { + if let Some(header) = fn_kind.header() { + if header.abi != Abi::Rust { + return; + } + } + + let parent_id = cx.tcx.hir().get_parent_item(hir_id); + let parent_node = cx.tcx.hir().find_by_def_id(parent_id); + + let mut trait_self_ty = None; + if let Some(Node::Item(item)) = parent_node { + // If the method is an impl for a trait, don't warn. + if let ItemKind::Impl(Impl { of_trait: Some(_), .. }) = item.kind { + return; + } + + // find `self` ty for this trait if relevant + if let ItemKind::Trait(_, _, _, _, items) = item.kind { + for trait_item in items { + if trait_item.id.hir_id() == hir_id { + // be sure we have `self` parameter in this function + if trait_item.kind == (AssocItemKind::Fn { has_self: true }) { + trait_self_ty = Some( + TraitRef::identity(cx.tcx, trait_item.id.def_id.to_def_id()) + .self_ty() + .skip_binder(), + ); + } + } + } + } + } + + let mut v = EscapeDelegate { + cx, + set: HirIdSet::default(), + trait_self_ty, + too_large_for_stack: self.too_large_for_stack, + }; + + let fn_def_id = cx.tcx.hir().local_def_id(hir_id); + cx.tcx.infer_ctxt().enter(|infcx| { + ExprUseVisitor::new(&mut v, &infcx, fn_def_id, cx.param_env, cx.typeck_results()).consume_body(body); + }); + + for node in v.set { + span_lint_hir( + cx, + BOXED_LOCAL, + node, + cx.tcx.hir().span(node), + "local variable doesn't need to be boxed here", + ); + } + } +} + +// TODO: Replace with Map::is_argument(..) when it's fixed +fn is_argument(map: rustc_middle::hir::map::Map<'_>, id: HirId) -> bool { + match map.find(id) { + Some(Node::Pat(Pat { + kind: PatKind::Binding(..), + .. + })) => (), + _ => return false, + } + + matches!(map.find(map.get_parent_node(id)), Some(Node::Param(_))) +} + +impl<'a, 'tcx> Delegate<'tcx> for EscapeDelegate<'a, 'tcx> { + fn consume(&mut self, cmt: &PlaceWithHirId<'tcx>, _: HirId) { + if cmt.place.projections.is_empty() { + if let PlaceBase::Local(lid) = cmt.place.base { + self.set.remove(&lid); + } + } + } + + fn borrow(&mut self, cmt: &PlaceWithHirId<'tcx>, _: HirId, _: ty::BorrowKind) { + if cmt.place.projections.is_empty() { + if let PlaceBase::Local(lid) = cmt.place.base { + self.set.remove(&lid); + } + } + } + + fn mutate(&mut self, cmt: &PlaceWithHirId<'tcx>, _: HirId) { + if cmt.place.projections.is_empty() { + let map = &self.cx.tcx.hir(); + if is_argument(*map, cmt.hir_id) { + // Skip closure arguments + let parent_id = map.get_parent_node(cmt.hir_id); + if let Some(Node::Expr(..)) = map.find(map.get_parent_node(parent_id)) { + return; + } + + // skip if there is a `self` parameter binding to a type + // that contains `Self` (i.e.: `self: Box<Self>`), see #4804 + if let Some(trait_self_ty) = self.trait_self_ty { + if map.name(cmt.hir_id) == kw::SelfLower && contains_ty(cmt.place.ty(), trait_self_ty) { + return; + } + } + + if is_non_trait_box(cmt.place.ty()) && !self.is_large_box(cmt.place.ty()) { + self.set.insert(cmt.hir_id); + } + } + } + } + + fn fake_read(&mut self, _: &rustc_typeck::expr_use_visitor::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {} +} + +impl<'a, 'tcx> EscapeDelegate<'a, 'tcx> { + fn is_large_box(&self, ty: Ty<'tcx>) -> bool { + // Large types need to be boxed to avoid stack overflows. + if ty.is_box() { + self.cx.layout_of(ty.boxed_ty()).map_or(0, |l| l.size.bytes()) > self.too_large_for_stack + } else { + false + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/eta_reduction.rs b/src/tools/clippy/clippy_lints/src/eta_reduction.rs new file mode 100644 index 000000000..4f9ff97f1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/eta_reduction.rs @@ -0,0 +1,235 @@ +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::higher::VecArgs; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::usage::local_used_after_expr; +use clippy_utils::{higher, is_adjusted, path_to_local, path_to_local_id}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::def_id::DefId; +use rustc_hir::{Closure, Expr, ExprKind, Param, PatKind, Unsafety}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow}; +use rustc_middle::ty::binding::BindingMode; +use rustc_middle::ty::subst::Subst; +use rustc_middle::ty::{self, ClosureKind, Ty, TypeVisitable}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for closures which just call another function where + /// the function can be called directly. `unsafe` functions or calls where types + /// get adjusted are ignored. + /// + /// ### Why is this bad? + /// Needlessly creating a closure adds code for no benefit + /// and gives the optimizer more work. + /// + /// ### Known problems + /// If creating the closure inside the closure has a side- + /// effect then moving the closure creation out will change when that side- + /// effect runs. + /// See [#1439](https://github.com/rust-lang/rust-clippy/issues/1439) for more details. + /// + /// ### Example + /// ```rust,ignore + /// xs.map(|x| foo(x)) + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// // where `foo(_)` is a plain function that takes the exact argument type of `x`. + /// xs.map(foo) + /// ``` + #[clippy::version = "pre 1.29.0"] + pub REDUNDANT_CLOSURE, + style, + "redundant closures, i.e., `|a| foo(a)` (which can be written as just `foo`)" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for closures which only invoke a method on the closure + /// argument and can be replaced by referencing the method directly. + /// + /// ### Why is this bad? + /// It's unnecessary to create the closure. + /// + /// ### Example + /// ```rust,ignore + /// Some('a').map(|s| s.to_uppercase()); + /// ``` + /// may be rewritten as + /// ```rust,ignore + /// Some('a').map(char::to_uppercase); + /// ``` + #[clippy::version = "1.35.0"] + pub REDUNDANT_CLOSURE_FOR_METHOD_CALLS, + pedantic, + "redundant closures for method calls" +} + +declare_lint_pass!(EtaReduction => [REDUNDANT_CLOSURE, REDUNDANT_CLOSURE_FOR_METHOD_CALLS]); + +impl<'tcx> LateLintPass<'tcx> for EtaReduction { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if expr.span.from_expansion() { + return; + } + let body = match expr.kind { + ExprKind::Closure(&Closure { body, .. }) => cx.tcx.hir().body(body), + _ => return, + }; + if body.value.span.from_expansion() { + if body.params.is_empty() { + if let Some(VecArgs::Vec(&[])) = higher::VecArgs::hir(cx, &body.value) { + // replace `|| vec![]` with `Vec::new` + span_lint_and_sugg( + cx, + REDUNDANT_CLOSURE, + expr.span, + "redundant closure", + "replace the closure with `Vec::new`", + "std::vec::Vec::new".into(), + Applicability::MachineApplicable, + ); + } + } + // skip `foo(|| macro!())` + return; + } + + let closure_ty = cx.typeck_results().expr_ty(expr); + + if_chain!( + if !is_adjusted(cx, &body.value); + if let ExprKind::Call(callee, args) = body.value.kind; + if let ExprKind::Path(_) = callee.kind; + if check_inputs(cx, body.params, args); + let callee_ty = cx.typeck_results().expr_ty_adjusted(callee); + let call_ty = cx.typeck_results().type_dependent_def_id(body.value.hir_id) + .map_or(callee_ty, |id| cx.tcx.type_of(id)); + if check_sig(cx, closure_ty, call_ty); + let substs = cx.typeck_results().node_substs(callee.hir_id); + // This fixes some false positives that I don't entirely understand + if substs.is_empty() || !cx.typeck_results().expr_ty(expr).has_late_bound_regions(); + // A type param function ref like `T::f` is not 'static, however + // it is if cast like `T::f as fn()`. This seems like a rustc bug. + if !substs.types().any(|t| matches!(t.kind(), ty::Param(_))); + let callee_ty_unadjusted = cx.typeck_results().expr_ty(callee).peel_refs(); + if !is_type_diagnostic_item(cx, callee_ty_unadjusted, sym::Arc); + if !is_type_diagnostic_item(cx, callee_ty_unadjusted, sym::Rc); + then { + span_lint_and_then(cx, REDUNDANT_CLOSURE, expr.span, "redundant closure", |diag| { + if let Some(mut snippet) = snippet_opt(cx, callee.span) { + if_chain! { + if let ty::Closure(_, substs) = callee_ty.peel_refs().kind(); + if substs.as_closure().kind() == ClosureKind::FnMut; + if path_to_local(callee).map_or(false, |l| local_used_after_expr(cx, l, expr)); + + then { + // Mutable closure is used after current expr; we cannot consume it. + snippet = format!("&mut {}", snippet); + } + } + diag.span_suggestion( + expr.span, + "replace the closure with the function itself", + snippet, + Applicability::MachineApplicable, + ); + } + }); + } + ); + + if_chain!( + if !is_adjusted(cx, &body.value); + if let ExprKind::MethodCall(path, args, _) = body.value.kind; + if check_inputs(cx, body.params, args); + let method_def_id = cx.typeck_results().type_dependent_def_id(body.value.hir_id).unwrap(); + let substs = cx.typeck_results().node_substs(body.value.hir_id); + let call_ty = cx.tcx.bound_type_of(method_def_id).subst(cx.tcx, substs); + if check_sig(cx, closure_ty, call_ty); + then { + span_lint_and_then(cx, REDUNDANT_CLOSURE_FOR_METHOD_CALLS, expr.span, "redundant closure", |diag| { + let name = get_ufcs_type_name(cx, method_def_id); + diag.span_suggestion( + expr.span, + "replace the closure with the method itself", + format!("{}::{}", name, path.ident.name), + Applicability::MachineApplicable, + ); + }) + } + ); + } +} + +fn check_inputs(cx: &LateContext<'_>, params: &[Param<'_>], call_args: &[Expr<'_>]) -> bool { + if params.len() != call_args.len() { + return false; + } + let binding_modes = cx.typeck_results().pat_binding_modes(); + std::iter::zip(params, call_args).all(|(param, arg)| { + match param.pat.kind { + PatKind::Binding(_, id, ..) if path_to_local_id(arg, id) => {}, + _ => return false, + } + // checks that parameters are not bound as `ref` or `ref mut` + if let Some(BindingMode::BindByReference(_)) = binding_modes.get(param.pat.hir_id) { + return false; + } + + match *cx.typeck_results().expr_adjustments(arg) { + [] => true, + [ + Adjustment { + kind: Adjust::Deref(None), + .. + }, + Adjustment { + kind: Adjust::Borrow(AutoBorrow::Ref(_, mu2)), + .. + }, + ] => { + // re-borrow with the same mutability is allowed + let ty = cx.typeck_results().expr_ty(arg); + matches!(*ty.kind(), ty::Ref(.., mu1) if mu1 == mu2.into()) + }, + _ => false, + } + }) +} + +fn check_sig<'tcx>(cx: &LateContext<'tcx>, closure_ty: Ty<'tcx>, call_ty: Ty<'tcx>) -> bool { + let call_sig = call_ty.fn_sig(cx.tcx); + if call_sig.unsafety() == Unsafety::Unsafe { + return false; + } + if !closure_ty.has_late_bound_regions() { + return true; + } + let substs = match closure_ty.kind() { + ty::Closure(_, substs) => substs, + _ => return false, + }; + let closure_sig = cx.tcx.signature_unclosure(substs.as_closure().sig(), Unsafety::Normal); + cx.tcx.erase_late_bound_regions(closure_sig) == cx.tcx.erase_late_bound_regions(call_sig) +} + +fn get_ufcs_type_name(cx: &LateContext<'_>, method_def_id: DefId) -> String { + let assoc_item = cx.tcx.associated_item(method_def_id); + let def_id = assoc_item.container_id(cx.tcx); + match assoc_item.container { + ty::TraitContainer => cx.tcx.def_path_str(def_id), + ty::ImplContainer => { + let ty = cx.tcx.type_of(def_id); + match ty.kind() { + ty::Adt(adt, _) => cx.tcx.def_path_str(adt.did()), + _ => ty.to_string(), + } + }, + } +} diff --git a/src/tools/clippy/clippy_lints/src/excessive_bools.rs b/src/tools/clippy/clippy_lints/src/excessive_bools.rs new file mode 100644 index 000000000..453471c8c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/excessive_bools.rs @@ -0,0 +1,176 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{AssocItemKind, Extern, Fn, FnSig, Impl, Item, ItemKind, Trait, Ty, TyKind}; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for excessive + /// use of bools in structs. + /// + /// ### Why is this bad? + /// Excessive bools in a struct + /// is often a sign that it's used as a state machine, + /// which is much better implemented as an enum. + /// If it's not the case, excessive bools usually benefit + /// from refactoring into two-variant enums for better + /// readability and API. + /// + /// ### Example + /// ```rust + /// struct S { + /// is_pending: bool, + /// is_processing: bool, + /// is_finished: bool, + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// enum S { + /// Pending, + /// Processing, + /// Finished, + /// } + /// ``` + #[clippy::version = "1.43.0"] + pub STRUCT_EXCESSIVE_BOOLS, + pedantic, + "using too many bools in a struct" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for excessive use of + /// bools in function definitions. + /// + /// ### Why is this bad? + /// Calls to such functions + /// are confusing and error prone, because it's + /// hard to remember argument order and you have + /// no type system support to back you up. Using + /// two-variant enums instead of bools often makes + /// API easier to use. + /// + /// ### Example + /// ```rust,ignore + /// fn f(is_round: bool, is_hot: bool) { ... } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// enum Shape { + /// Round, + /// Spiky, + /// } + /// + /// enum Temperature { + /// Hot, + /// IceCold, + /// } + /// + /// fn f(shape: Shape, temperature: Temperature) { ... } + /// ``` + #[clippy::version = "1.43.0"] + pub FN_PARAMS_EXCESSIVE_BOOLS, + pedantic, + "using too many bools in function parameters" +} + +pub struct ExcessiveBools { + max_struct_bools: u64, + max_fn_params_bools: u64, +} + +impl ExcessiveBools { + #[must_use] + pub fn new(max_struct_bools: u64, max_fn_params_bools: u64) -> Self { + Self { + max_struct_bools, + max_fn_params_bools, + } + } + + fn check_fn_sig(&self, cx: &EarlyContext<'_>, fn_sig: &FnSig, span: Span) { + match fn_sig.header.ext { + Extern::Implicit(_) | Extern::Explicit(_, _) => return, + Extern::None => (), + } + + let fn_sig_bools = fn_sig + .decl + .inputs + .iter() + .filter(|param| is_bool_ty(¶m.ty)) + .count() + .try_into() + .unwrap(); + if self.max_fn_params_bools < fn_sig_bools { + span_lint_and_help( + cx, + FN_PARAMS_EXCESSIVE_BOOLS, + span, + &format!("more than {} bools in function parameters", self.max_fn_params_bools), + None, + "consider refactoring bools into two-variant enums", + ); + } + } +} + +impl_lint_pass!(ExcessiveBools => [STRUCT_EXCESSIVE_BOOLS, FN_PARAMS_EXCESSIVE_BOOLS]); + +fn is_bool_ty(ty: &Ty) -> bool { + if let TyKind::Path(None, path) = &ty.kind { + if let [name] = path.segments.as_slice() { + return name.ident.name == sym::bool; + } + } + false +} + +impl EarlyLintPass for ExcessiveBools { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if item.span.from_expansion() { + return; + } + match &item.kind { + ItemKind::Struct(variant_data, _) => { + if item.attrs.iter().any(|attr| attr.has_name(sym::repr)) { + return; + } + + let struct_bools = variant_data + .fields() + .iter() + .filter(|field| is_bool_ty(&field.ty)) + .count() + .try_into() + .unwrap(); + if self.max_struct_bools < struct_bools { + span_lint_and_help( + cx, + STRUCT_EXCESSIVE_BOOLS, + item.span, + &format!("more than {} bools in a struct", self.max_struct_bools), + None, + "consider using a state machine or refactoring bools into two-variant enums", + ); + } + }, + ItemKind::Impl(box Impl { + of_trait: None, items, .. + }) + | ItemKind::Trait(box Trait { items, .. }) => { + for item in items { + if let AssocItemKind::Fn(box Fn { sig, .. }) = &item.kind { + self.check_fn_sig(cx, sig, item.span); + } + } + }, + ItemKind::Fn(box Fn { sig, .. }) => self.check_fn_sig(cx, sig, item.span), + _ => (), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/exhaustive_items.rs b/src/tools/clippy/clippy_lints/src/exhaustive_items.rs new file mode 100644 index 000000000..173d41b4b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/exhaustive_items.rs @@ -0,0 +1,111 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::indent_of; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Warns on any exported `enum`s that are not tagged `#[non_exhaustive]` + /// + /// ### Why is this bad? + /// Exhaustive enums are typically fine, but a project which does + /// not wish to make a stability commitment around exported enums may wish to + /// disable them by default. + /// + /// ### Example + /// ```rust + /// enum Foo { + /// Bar, + /// Baz + /// } + /// ``` + /// Use instead: + /// ```rust + /// #[non_exhaustive] + /// enum Foo { + /// Bar, + /// Baz + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub EXHAUSTIVE_ENUMS, + restriction, + "detects exported enums that have not been marked #[non_exhaustive]" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns on any exported `structs`s that are not tagged `#[non_exhaustive]` + /// + /// ### Why is this bad? + /// Exhaustive structs are typically fine, but a project which does + /// not wish to make a stability commitment around exported structs may wish to + /// disable them by default. + /// + /// ### Example + /// ```rust + /// struct Foo { + /// bar: u8, + /// baz: String, + /// } + /// ``` + /// Use instead: + /// ```rust + /// #[non_exhaustive] + /// struct Foo { + /// bar: u8, + /// baz: String, + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub EXHAUSTIVE_STRUCTS, + restriction, + "detects exported structs that have not been marked #[non_exhaustive]" +} + +declare_lint_pass!(ExhaustiveItems => [EXHAUSTIVE_ENUMS, EXHAUSTIVE_STRUCTS]); + +impl LateLintPass<'_> for ExhaustiveItems { + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if_chain! { + if let ItemKind::Enum(..) | ItemKind::Struct(..) = item.kind; + if cx.access_levels.is_exported(item.def_id); + let attrs = cx.tcx.hir().attrs(item.hir_id()); + if !attrs.iter().any(|a| a.has_name(sym::non_exhaustive)); + then { + let (lint, msg) = if let ItemKind::Struct(ref v, ..) = item.kind { + if v.fields().iter().any(|f| { + let def_id = cx.tcx.hir().local_def_id(f.hir_id); + !cx.tcx.visibility(def_id).is_public() + }) { + // skip structs with private fields + return; + } + (EXHAUSTIVE_STRUCTS, "exported structs should not be exhaustive") + } else { + (EXHAUSTIVE_ENUMS, "exported enums should not be exhaustive") + }; + let suggestion_span = item.span.shrink_to_lo(); + let indent = " ".repeat(indent_of(cx, item.span).unwrap_or(0)); + span_lint_and_then( + cx, + lint, + item.span, + msg, + |diag| { + let sugg = format!("#[non_exhaustive]\n{}", indent); + diag.span_suggestion(suggestion_span, + "try adding #[non_exhaustive]", + sugg, + Applicability::MaybeIncorrect); + } + ); + + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/exit.rs b/src/tools/clippy/clippy_lints/src/exit.rs new file mode 100644 index 000000000..cbf52d193 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/exit.rs @@ -0,0 +1,46 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{is_entrypoint_fn, match_def_path, paths}; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind, Item, ItemKind, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// `exit()` terminates the program and doesn't provide a + /// stack trace. + /// + /// ### Why is this bad? + /// Ideally a program is terminated by finishing + /// the main function. + /// + /// ### Example + /// ```ignore + /// std::process::exit(0) + /// ``` + #[clippy::version = "1.41.0"] + pub EXIT, + restriction, + "`std::process::exit` is called, terminating the program" +} + +declare_lint_pass!(Exit => [EXIT]); + +impl<'tcx> LateLintPass<'tcx> for Exit { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::Call(path_expr, _args) = e.kind; + if let ExprKind::Path(ref path) = path_expr.kind; + if let Some(def_id) = cx.qpath_res(path, path_expr.hir_id).opt_def_id(); + if match_def_path(cx, def_id, &paths::EXIT); + let parent = cx.tcx.hir().get_parent_item(e.hir_id); + if let Some(Node::Item(Item{kind: ItemKind::Fn(..), ..})) = cx.tcx.hir().find_by_def_id(parent); + // If the next item up is a function we check if it is an entry point + // and only then emit a linter warning + if !is_entrypoint_fn(cx, parent.to_def_id()); + then { + span_lint(cx, EXIT, e.span, "usage of `process::exit`"); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/explicit_write.rs b/src/tools/clippy/clippy_lints/src/explicit_write.rs new file mode 100644 index 000000000..5bf4313b4 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/explicit_write.rs @@ -0,0 +1,142 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::FormatArgsExpn; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{is_expn_of, match_function_call, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::def::Res; +use rustc_hir::{BindingAnnotation, Block, BlockCheckMode, Expr, ExprKind, Node, PatKind, QPath, Stmt, StmtKind}; +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 `write!()` / `writeln()!` which can be + /// replaced with `(e)print!()` / `(e)println!()` + /// + /// ### Why is this bad? + /// Using `(e)println! is clearer and more concise + /// + /// ### Example + /// ```rust + /// # use std::io::Write; + /// # let bar = "furchtbar"; + /// writeln!(&mut std::io::stderr(), "foo: {:?}", bar).unwrap(); + /// writeln!(&mut std::io::stdout(), "foo: {:?}", bar).unwrap(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::io::Write; + /// # let bar = "furchtbar"; + /// eprintln!("foo: {:?}", bar); + /// println!("foo: {:?}", bar); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXPLICIT_WRITE, + complexity, + "using the `write!()` family of functions instead of the `print!()` family of functions, when using the latter would work" +} + +declare_lint_pass!(ExplicitWrite => [EXPLICIT_WRITE]); + +impl<'tcx> LateLintPass<'tcx> for ExplicitWrite { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + // match call to unwrap + if let ExprKind::MethodCall(unwrap_fun, [write_call], _) = expr.kind; + if unwrap_fun.ident.name == sym::unwrap; + // match call to write_fmt + if let ExprKind::MethodCall(write_fun, [write_recv, write_arg], _) = look_in_block(cx, &write_call.kind); + if write_fun.ident.name == sym!(write_fmt); + // match calls to std::io::stdout() / std::io::stderr () + if let Some(dest_name) = if match_function_call(cx, write_recv, &paths::STDOUT).is_some() { + Some("stdout") + } else if match_function_call(cx, write_recv, &paths::STDERR).is_some() { + Some("stderr") + } else { + None + }; + if let Some(format_args) = FormatArgsExpn::parse(cx, write_arg); + then { + let calling_macro = + // ordering is important here, since `writeln!` uses `write!` internally + if is_expn_of(write_call.span, "writeln").is_some() { + Some("writeln") + } else if is_expn_of(write_call.span, "write").is_some() { + Some("write") + } else { + None + }; + let prefix = if dest_name == "stderr" { + "e" + } else { + "" + }; + + // We need to remove the last trailing newline from the string because the + // underlying `fmt::write` function doesn't know whether `println!` or `print!` was + // used. + let (used, sugg_mac) = if let Some(macro_name) = calling_macro { + ( + format!("{}!({}(), ...)", macro_name, dest_name), + macro_name.replace("write", "print"), + ) + } else { + ( + format!("{}().write_fmt(...)", dest_name), + "print".into(), + ) + }; + let mut applicability = Applicability::MachineApplicable; + let inputs_snippet = snippet_with_applicability( + cx, + format_args.inputs_span(), + "..", + &mut applicability, + ); + span_lint_and_sugg( + cx, + EXPLICIT_WRITE, + expr.span, + &format!("use of `{}.unwrap()`", used), + "try this", + format!("{}{}!({})", prefix, sugg_mac, inputs_snippet), + applicability, + ) + } + } + } +} + +/// If `kind` is a block that looks like `{ let result = $expr; result }` then +/// returns $expr. Otherwise returns `kind`. +fn look_in_block<'tcx, 'hir>(cx: &LateContext<'tcx>, kind: &'tcx ExprKind<'hir>) -> &'tcx ExprKind<'hir> { + if_chain! { + if let ExprKind::Block(block, _label @ None) = kind; + if let Block { + stmts: [Stmt { kind: StmtKind::Local(local), .. }], + expr: Some(expr_end_of_block), + rules: BlockCheckMode::DefaultBlock, + .. + } = block; + + // Find id of the local that expr_end_of_block resolves to + if let ExprKind::Path(QPath::Resolved(None, expr_path)) = expr_end_of_block.kind; + if let Res::Local(expr_res) = expr_path.res; + if let Some(Node::Pat(res_pat)) = cx.tcx.hir().find(expr_res); + + // Find id of the local we found in the block + if let PatKind::Binding(BindingAnnotation::Unannotated, local_hir_id, _ident, None) = local.pat.kind; + + // If those two are the same hir id + if res_pat.hir_id == local_hir_id; + + if let Some(init) = local.init; + then { + return &init.kind; + } + } + kind +} diff --git a/src/tools/clippy/clippy_lints/src/fallible_impl_from.rs b/src/tools/clippy/clippy_lints/src/fallible_impl_from.rs new file mode 100644 index 000000000..b88e53aec --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/fallible_impl_from.rs @@ -0,0 +1,132 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::macros::{is_panic, root_macro_call_first_node}; +use clippy_utils::method_chain_args; +use clippy_utils::ty::is_type_diagnostic_item; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for impls of `From<..>` that contain `panic!()` or `unwrap()` + /// + /// ### Why is this bad? + /// `TryFrom` should be used if there's a possibility of failure. + /// + /// ### Example + /// ```rust + /// struct Foo(i32); + /// + /// impl From<String> for Foo { + /// fn from(s: String) -> Self { + /// Foo(s.parse().unwrap()) + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// struct Foo(i32); + /// + /// impl TryFrom<String> for Foo { + /// type Error = (); + /// fn try_from(s: String) -> Result<Self, Self::Error> { + /// if let Ok(parsed) = s.parse() { + /// Ok(Foo(parsed)) + /// } else { + /// Err(()) + /// } + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FALLIBLE_IMPL_FROM, + nursery, + "Warn on impls of `From<..>` that contain `panic!()` or `unwrap()`" +} + +declare_lint_pass!(FallibleImplFrom => [FALLIBLE_IMPL_FROM]); + +impl<'tcx> LateLintPass<'tcx> for FallibleImplFrom { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + // check for `impl From<???> for ..` + if_chain! { + if let hir::ItemKind::Impl(impl_) = &item.kind; + if let Some(impl_trait_ref) = cx.tcx.impl_trait_ref(item.def_id); + if cx.tcx.is_diagnostic_item(sym::From, impl_trait_ref.def_id); + then { + lint_impl_body(cx, item.span, impl_.items); + } + } + } +} + +fn lint_impl_body<'tcx>(cx: &LateContext<'tcx>, impl_span: Span, impl_items: &[hir::ImplItemRef]) { + use rustc_hir::intravisit::{self, Visitor}; + use rustc_hir::{Expr, ImplItemKind}; + + struct FindPanicUnwrap<'a, 'tcx> { + lcx: &'a LateContext<'tcx>, + typeck_results: &'tcx ty::TypeckResults<'tcx>, + result: Vec<Span>, + } + + impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if let Some(macro_call) = root_macro_call_first_node(self.lcx, expr) { + if is_panic(self.lcx, macro_call.def_id) { + self.result.push(expr.span); + } + } + + // check for `unwrap` + if let Some(arglists) = method_chain_args(expr, &["unwrap"]) { + let receiver_ty = self.typeck_results.expr_ty(&arglists[0][0]).peel_refs(); + if is_type_diagnostic_item(self.lcx, receiver_ty, sym::Option) + || is_type_diagnostic_item(self.lcx, receiver_ty, sym::Result) + { + self.result.push(expr.span); + } + } + + // and check sub-expressions + intravisit::walk_expr(self, expr); + } + } + + for impl_item in impl_items { + if_chain! { + if impl_item.ident.name == sym::from; + if let ImplItemKind::Fn(_, body_id) = + cx.tcx.hir().impl_item(impl_item.id).kind; + then { + // check the body for `begin_panic` or `unwrap` + let body = cx.tcx.hir().body(body_id); + let mut fpu = FindPanicUnwrap { + lcx: cx, + typeck_results: cx.tcx.typeck(impl_item.id.def_id), + result: Vec::new(), + }; + fpu.visit_expr(&body.value); + + // if we've found one, lint + if !fpu.result.is_empty() { + span_lint_and_then( + cx, + FALLIBLE_IMPL_FROM, + impl_span, + "consider implementing `TryFrom` instead", + move |diag| { + diag.help( + "`From` is intended for infallible conversions only. \ + Use `TryFrom` if there's a possibility for the conversion to fail"); + diag.span_note(fpu.result, "potential failure(s)"); + }); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/float_literal.rs b/src/tools/clippy/clippy_lints/src/float_literal.rs new file mode 100644 index 000000000..f2e079809 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/float_literal.rs @@ -0,0 +1,181 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::numeric_literal; +use if_chain::if_chain; +use rustc_ast::ast::{self, LitFloatType, LitKind}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, FloatTy}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use std::fmt; + +declare_clippy_lint! { + /// ### What it does + /// Checks for float literals with a precision greater + /// than that supported by the underlying type. + /// + /// ### Why is this bad? + /// Rust will truncate the literal silently. + /// + /// ### Example + /// ```rust + /// let v: f32 = 0.123_456_789_9; + /// println!("{}", v); // 0.123_456_789 + /// ``` + /// + /// Use instead: + /// ```rust + /// let v: f64 = 0.123_456_789_9; + /// println!("{}", v); // 0.123_456_789_9 + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXCESSIVE_PRECISION, + style, + "excessive precision for float literal" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for whole number float literals that + /// cannot be represented as the underlying type without loss. + /// + /// ### Why is this bad? + /// Rust will silently lose precision during + /// conversion to a float. + /// + /// ### Example + /// ```rust + /// let _: f32 = 16_777_217.0; // 16_777_216.0 + /// ``` + /// + /// Use instead: + /// ```rust + /// let _: f32 = 16_777_216.0; + /// let _: f64 = 16_777_217.0; + /// ``` + #[clippy::version = "1.43.0"] + pub LOSSY_FLOAT_LITERAL, + restriction, + "lossy whole number float literals" +} + +declare_lint_pass!(FloatLiteral => [EXCESSIVE_PRECISION, LOSSY_FLOAT_LITERAL]); + +impl<'tcx> LateLintPass<'tcx> for FloatLiteral { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + let ty = cx.typeck_results().expr_ty(expr); + if_chain! { + if let ty::Float(fty) = *ty.kind(); + if let hir::ExprKind::Lit(ref lit) = expr.kind; + if let LitKind::Float(sym, lit_float_ty) = lit.node; + then { + let sym_str = sym.as_str(); + let formatter = FloatFormat::new(sym_str); + // Try to bail out if the float is for sure fine. + // If its within the 2 decimal digits of being out of precision we + // check if the parsed representation is the same as the string + // since we'll need the truncated string anyway. + let digits = count_digits(sym_str); + let max = max_digits(fty); + let type_suffix = match lit_float_ty { + LitFloatType::Suffixed(ast::FloatTy::F32) => Some("f32"), + LitFloatType::Suffixed(ast::FloatTy::F64) => Some("f64"), + LitFloatType::Unsuffixed => None + }; + let (is_whole, mut float_str) = match fty { + FloatTy::F32 => { + let value = sym_str.parse::<f32>().unwrap(); + + (value.fract() == 0.0, formatter.format(value)) + }, + FloatTy::F64 => { + let value = sym_str.parse::<f64>().unwrap(); + + (value.fract() == 0.0, formatter.format(value)) + }, + }; + + 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 { + // If the type suffix is missing the suggestion would be + // incorrectly interpreted as an integer so adding a `.0` + // suffix to prevent that. + if type_suffix.is_none() { + float_str.push_str(".0"); + } + + span_lint_and_sugg( + cx, + LOSSY_FLOAT_LITERAL, + expr.span, + "literal cannot be represented as the underlying type without loss of precision", + "consider changing the type or replacing it with", + numeric_literal::format(&float_str, type_suffix, true), + Applicability::MachineApplicable, + ); + } + } else if digits > max as usize && float_str.len() < sym_str.len() { + span_lint_and_sugg( + cx, + EXCESSIVE_PRECISION, + expr.span, + "float has excessive precision", + "consider changing the type or truncating it to", + numeric_literal::format(&float_str, type_suffix, true), + Applicability::MachineApplicable, + ); + } + } + } + } +} + +#[must_use] +fn max_digits(fty: FloatTy) -> u32 { + match fty { + FloatTy::F32 => f32::DIGITS, + FloatTy::F64 => f64::DIGITS, + } +} + +/// Counts the digits excluding leading zeros +#[must_use] +fn count_digits(s: &str) -> usize { + // Note that s does not contain the f32/64 suffix, and underscores have been stripped + s.chars() + .filter(|c| *c != '-' && *c != '.') + .take_while(|c| *c != 'e' && *c != 'E') + .fold(0, |count, c| { + // leading zeros + if c == '0' && count == 0 { count } else { count + 1 } + }) +} + +enum FloatFormat { + LowerExp, + UpperExp, + Normal, +} +impl FloatFormat { + #[must_use] + fn new(s: &str) -> Self { + s.chars() + .find_map(|x| match x { + 'e' => Some(Self::LowerExp), + 'E' => Some(Self::UpperExp), + _ => None, + }) + .unwrap_or(Self::Normal) + } + fn format<T>(&self, f: T) -> String + where + T: fmt::UpperExp + fmt::LowerExp + fmt::Display, + { + match self { + Self::LowerExp => format!("{:e}", f), + Self::UpperExp => format!("{:E}", f), + Self::Normal => format!("{}", f), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/floating_point_arithmetic.rs b/src/tools/clippy/clippy_lints/src/floating_point_arithmetic.rs new file mode 100644 index 000000000..df9b41d2c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/floating_point_arithmetic.rs @@ -0,0 +1,735 @@ +use clippy_utils::consts::{ + constant, constant_simple, Constant, + Constant::{Int, F32, F64}, +}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::higher; +use clippy_utils::{eq_expr_value, get_parent_expr, in_constant, numeric_literal, peel_blocks, sugg}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, PathSegment, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; + +use rustc_ast::ast; +use std::f32::consts as f32_consts; +use std::f64::consts as f64_consts; +use sugg::Sugg; + +declare_clippy_lint! { + /// ### What it does + /// Looks for floating-point expressions that + /// can be expressed using built-in methods to improve accuracy + /// at the cost of performance. + /// + /// ### Why is this bad? + /// Negatively impacts accuracy. + /// + /// ### Example + /// ```rust + /// let a = 3f32; + /// let _ = a.powf(1.0 / 3.0); + /// let _ = (1.0 + a).ln(); + /// let _ = a.exp() - 1.0; + /// ``` + /// + /// Use instead: + /// ```rust + /// let a = 3f32; + /// let _ = a.cbrt(); + /// let _ = a.ln_1p(); + /// let _ = a.exp_m1(); + /// ``` + #[clippy::version = "1.43.0"] + pub IMPRECISE_FLOPS, + nursery, + "usage of imprecise floating point operations" +} + +declare_clippy_lint! { + /// ### What it does + /// Looks for floating-point expressions that + /// can be expressed using built-in methods to improve both + /// accuracy and performance. + /// + /// ### Why is this bad? + /// Negatively impacts accuracy and performance. + /// + /// ### Example + /// ```rust + /// use std::f32::consts::E; + /// + /// let a = 3f32; + /// let _ = (2f32).powf(a); + /// let _ = E.powf(a); + /// let _ = a.powf(1.0 / 2.0); + /// let _ = a.log(2.0); + /// let _ = a.log(10.0); + /// let _ = a.log(E); + /// let _ = a.powf(2.0); + /// let _ = a * 2.0 + 4.0; + /// let _ = if a < 0.0 { + /// -a + /// } else { + /// a + /// }; + /// let _ = if a < 0.0 { + /// a + /// } else { + /// -a + /// }; + /// ``` + /// + /// is better expressed as + /// + /// ```rust + /// use std::f32::consts::E; + /// + /// let a = 3f32; + /// let _ = a.exp2(); + /// let _ = a.exp(); + /// let _ = a.sqrt(); + /// let _ = a.log2(); + /// let _ = a.log10(); + /// let _ = a.ln(); + /// let _ = a.powi(2); + /// let _ = a.mul_add(2.0, 4.0); + /// let _ = a.abs(); + /// let _ = -a.abs(); + /// ``` + #[clippy::version = "1.43.0"] + pub SUBOPTIMAL_FLOPS, + nursery, + "usage of sub-optimal floating point operations" +} + +declare_lint_pass!(FloatingPointArithmetic => [ + IMPRECISE_FLOPS, + SUBOPTIMAL_FLOPS +]); + +// Returns the specialized log method for a given base if base is constant +// and is one of 2, 10 and e +fn get_specialized_log_method(cx: &LateContext<'_>, base: &Expr<'_>) -> Option<&'static str> { + if let Some((value, _)) = constant(cx, cx.typeck_results(), base) { + if F32(2.0) == value || F64(2.0) == value { + return Some("log2"); + } else if F32(10.0) == value || F64(10.0) == value { + return Some("log10"); + } else if F32(f32_consts::E) == value || F64(f64_consts::E) == value { + return Some("ln"); + } + } + + None +} + +// Adds type suffixes and parenthesis to method receivers if necessary +fn prepare_receiver_sugg<'a>(cx: &LateContext<'_>, mut expr: &'a Expr<'a>) -> Sugg<'a> { + let mut suggestion = Sugg::hir(cx, expr, ".."); + + if let ExprKind::Unary(UnOp::Neg, inner_expr) = &expr.kind { + expr = inner_expr; + } + + if_chain! { + // if the expression is a float literal and it is unsuffixed then + // add a suffix so the suggestion is valid and unambiguous + if let ty::Float(float_ty) = cx.typeck_results().expr_ty(expr).kind(); + if let ExprKind::Lit(lit) = &expr.kind; + if let ast::LitKind::Float(sym, ast::LitFloatType::Unsuffixed) = lit.node; + then { + let op = format!( + "{}{}{}", + suggestion, + // Check for float literals without numbers following the decimal + // separator such as `2.` and adds a trailing zero + if sym.as_str().ends_with('.') { + "0" + } else { + "" + }, + float_ty.name_str() + ).into(); + + suggestion = match suggestion { + Sugg::MaybeParen(_) => Sugg::MaybeParen(op), + _ => Sugg::NonParen(op) + }; + } + } + + suggestion.maybe_par() +} + +fn check_log_base(cx: &LateContext<'_>, expr: &Expr<'_>, args: &[Expr<'_>]) { + if let Some(method) = get_specialized_log_method(cx, &args[1]) { + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + expr.span, + "logarithm for bases 2, 10 and e can be computed more accurately", + "consider using", + format!("{}.{}()", Sugg::hir(cx, &args[0], ".."), method), + Applicability::MachineApplicable, + ); + } +} + +// TODO: Lint expressions of the form `(x + y).ln()` where y > 1 and +// suggest usage of `(x + (y - 1)).ln_1p()` instead +fn check_ln1p(cx: &LateContext<'_>, expr: &Expr<'_>, args: &[Expr<'_>]) { + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Add, .. + }, + lhs, + rhs, + ) = &args[0].kind + { + let recv = match ( + constant(cx, cx.typeck_results(), lhs), + constant(cx, cx.typeck_results(), rhs), + ) { + (Some((value, _)), _) if F32(1.0) == value || F64(1.0) == value => rhs, + (_, Some((value, _))) if F32(1.0) == value || F64(1.0) == value => lhs, + _ => return, + }; + + span_lint_and_sugg( + cx, + IMPRECISE_FLOPS, + expr.span, + "ln(1 + x) can be computed more accurately", + "consider using", + format!("{}.ln_1p()", prepare_receiver_sugg(cx, recv)), + Applicability::MachineApplicable, + ); + } +} + +// Returns an integer if the float constant is a whole number and it can be +// converted to an integer without loss of precision. For now we only check +// 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> { + match value { + F32(num) if num.fract() == 0.0 => { + if (-16_777_215.0..16_777_216.0).contains(num) { + Some(num.round() as i32) + } else { + None + } + }, + F64(num) if num.fract() == 0.0 => { + if (-2_147_483_648.0..2_147_483_648.0).contains(num) { + Some(num.round() as i32) + } else { + None + } + }, + _ => None, + } +} + +fn check_powf(cx: &LateContext<'_>, expr: &Expr<'_>, args: &[Expr<'_>]) { + // Check receiver + if let Some((value, _)) = constant(cx, cx.typeck_results(), &args[0]) { + let method = if F32(f32_consts::E) == value || F64(f64_consts::E) == value { + "exp" + } else if F32(2.0) == value || F64(2.0) == value { + "exp2" + } else { + return; + }; + + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + expr.span, + "exponent for bases 2 and e can be computed more accurately", + "consider using", + format!("{}.{}()", prepare_receiver_sugg(cx, &args[1]), method), + Applicability::MachineApplicable, + ); + } + + // Check argument + if let Some((value, _)) = constant(cx, cx.typeck_results(), &args[1]) { + let (lint, help, suggestion) = if F32(1.0 / 2.0) == value || F64(1.0 / 2.0) == value { + ( + SUBOPTIMAL_FLOPS, + "square-root of a number can be computed more efficiently and accurately", + format!("{}.sqrt()", Sugg::hir(cx, &args[0], "..")), + ) + } else if F32(1.0 / 3.0) == value || F64(1.0 / 3.0) == value { + ( + IMPRECISE_FLOPS, + "cube-root of a number can be computed more accurately", + format!("{}.cbrt()", Sugg::hir(cx, &args[0], "..")), + ) + } else if let Some(exponent) = get_integer_from_float_constant(&value) { + ( + SUBOPTIMAL_FLOPS, + "exponentiation with integer powers can be computed more efficiently", + format!( + "{}.powi({})", + Sugg::hir(cx, &args[0], ".."), + numeric_literal::format(&exponent.to_string(), None, false) + ), + ) + } else { + return; + }; + + span_lint_and_sugg( + cx, + lint, + expr.span, + help, + "consider using", + suggestion, + Applicability::MachineApplicable, + ); + } +} + +fn check_powi(cx: &LateContext<'_>, expr: &Expr<'_>, args: &[Expr<'_>]) { + if let Some((value, _)) = constant(cx, cx.typeck_results(), &args[1]) { + if value == Int(2) { + if let Some(parent) = get_parent_expr(cx, expr) { + if let Some(grandparent) = get_parent_expr(cx, parent) { + if let ExprKind::MethodCall(PathSegment { ident: method_name, .. }, args, _) = grandparent.kind { + if method_name.as_str() == "sqrt" && detect_hypot(cx, args).is_some() { + return; + } + } + } + + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Add, .. + }, + lhs, + rhs, + ) = parent.kind + { + let other_addend = if lhs.hir_id == expr.hir_id { rhs } else { lhs }; + + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + parent.span, + "multiply and add expressions can be calculated more efficiently and accurately", + "consider using", + format!( + "{}.mul_add({}, {})", + Sugg::hir(cx, &args[0], ".."), + Sugg::hir(cx, &args[0], ".."), + Sugg::hir(cx, other_addend, ".."), + ), + Applicability::MachineApplicable, + ); + } + } + } + } +} + +fn detect_hypot(cx: &LateContext<'_>, args: &[Expr<'_>]) -> Option<String> { + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Add, .. + }, + add_lhs, + add_rhs, + ) = args[0].kind + { + // check if expression of the form x * x + y * y + if_chain! { + if let ExprKind::Binary(Spanned { node: BinOpKind::Mul, .. }, lmul_lhs, lmul_rhs) = add_lhs.kind; + if let ExprKind::Binary(Spanned { node: BinOpKind::Mul, .. }, rmul_lhs, rmul_rhs) = add_rhs.kind; + if eq_expr_value(cx, lmul_lhs, lmul_rhs); + if eq_expr_value(cx, rmul_lhs, rmul_rhs); + then { + return Some(format!("{}.hypot({})", Sugg::hir(cx, lmul_lhs, "..").maybe_par(), Sugg::hir(cx, rmul_lhs, ".."))); + } + } + + // check if expression of the form x.powi(2) + y.powi(2) + if_chain! { + if let ExprKind::MethodCall( + PathSegment { ident: lmethod_name, .. }, + [largs_0, largs_1, ..], + _ + ) = &add_lhs.kind; + if let ExprKind::MethodCall( + PathSegment { ident: rmethod_name, .. }, + [rargs_0, rargs_1, ..], + _ + ) = &add_rhs.kind; + if lmethod_name.as_str() == "powi" && rmethod_name.as_str() == "powi"; + if let Some((lvalue, _)) = constant(cx, cx.typeck_results(), largs_1); + if let Some((rvalue, _)) = constant(cx, cx.typeck_results(), rargs_1); + if Int(2) == lvalue && Int(2) == rvalue; + then { + return Some(format!("{}.hypot({})", Sugg::hir(cx, largs_0, "..").maybe_par(), Sugg::hir(cx, rargs_0, ".."))); + } + } + } + + None +} + +fn check_hypot(cx: &LateContext<'_>, expr: &Expr<'_>, args: &[Expr<'_>]) { + if let Some(message) = detect_hypot(cx, args) { + span_lint_and_sugg( + cx, + IMPRECISE_FLOPS, + expr.span, + "hypotenuse can be computed more accurately", + "consider using", + message, + Applicability::MachineApplicable, + ); + } +} + +// TODO: Lint expressions of the form `x.exp() - y` where y > 1 +// and suggest usage of `x.exp_m1() - (y - 1)` instead +fn check_expm1(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::Binary(Spanned { node: BinOpKind::Sub, .. }, lhs, rhs) = expr.kind; + if cx.typeck_results().expr_ty(lhs).is_floating_point(); + if let Some((value, _)) = constant(cx, cx.typeck_results(), rhs); + if F32(1.0) == value || F64(1.0) == value; + if let ExprKind::MethodCall(path, [self_arg, ..], _) = &lhs.kind; + if cx.typeck_results().expr_ty(self_arg).is_floating_point(); + if path.ident.name.as_str() == "exp"; + then { + span_lint_and_sugg( + cx, + IMPRECISE_FLOPS, + expr.span, + "(e.pow(x) - 1) can be computed more accurately", + "consider using", + format!( + "{}.exp_m1()", + Sugg::hir(cx, self_arg, "..") + ), + Applicability::MachineApplicable, + ); + } + } +} + +fn is_float_mul_expr<'a>(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<(&'a Expr<'a>, &'a Expr<'a>)> { + if_chain! { + if let ExprKind::Binary(Spanned { node: BinOpKind::Mul, .. }, lhs, rhs) = &expr.kind; + if cx.typeck_results().expr_ty(lhs).is_floating_point(); + if cx.typeck_results().expr_ty(rhs).is_floating_point(); + then { + return Some((lhs, rhs)); + } + } + + None +} + +// TODO: Fix rust-lang/rust-clippy#4735 +fn check_mul_add(cx: &LateContext<'_>, expr: &Expr<'_>) { + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Add, .. + }, + lhs, + rhs, + ) = &expr.kind + { + if let Some(parent) = get_parent_expr(cx, expr) { + if let ExprKind::MethodCall(PathSegment { ident: method_name, .. }, args, _) = parent.kind { + if method_name.as_str() == "sqrt" && detect_hypot(cx, args).is_some() { + return; + } + } + } + + let (recv, arg1, arg2) = if let Some((inner_lhs, inner_rhs)) = is_float_mul_expr(cx, lhs) { + (inner_lhs, inner_rhs, rhs) + } else if let Some((inner_lhs, inner_rhs)) = is_float_mul_expr(cx, rhs) { + (inner_lhs, inner_rhs, lhs) + } else { + return; + }; + + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + expr.span, + "multiply and add expressions can be calculated more efficiently and accurately", + "consider using", + format!( + "{}.mul_add({}, {})", + prepare_receiver_sugg(cx, recv), + Sugg::hir(cx, arg1, ".."), + Sugg::hir(cx, arg2, ".."), + ), + Applicability::MachineApplicable, + ); + } +} + +/// Returns true iff expr is an expression which tests whether or not +/// test is positive or an expression which tests whether or not test +/// is nonnegative. +/// Used for check-custom-abs function below +fn is_testing_positive(cx: &LateContext<'_>, expr: &Expr<'_>, test: &Expr<'_>) -> bool { + if let ExprKind::Binary(Spanned { node: op, .. }, left, right) = expr.kind { + match op { + BinOpKind::Gt | BinOpKind::Ge => is_zero(cx, right) && eq_expr_value(cx, left, test), + BinOpKind::Lt | BinOpKind::Le => is_zero(cx, left) && eq_expr_value(cx, right, test), + _ => false, + } + } else { + false + } +} + +/// See [`is_testing_positive`] +fn is_testing_negative(cx: &LateContext<'_>, expr: &Expr<'_>, test: &Expr<'_>) -> bool { + if let ExprKind::Binary(Spanned { node: op, .. }, left, right) = expr.kind { + match op { + BinOpKind::Gt | BinOpKind::Ge => is_zero(cx, left) && eq_expr_value(cx, right, test), + BinOpKind::Lt | BinOpKind::Le => is_zero(cx, right) && eq_expr_value(cx, left, test), + _ => false, + } + } else { + false + } +} + +/// Returns true iff expr is some zero literal +fn is_zero(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + match constant_simple(cx, cx.typeck_results(), expr) { + Some(Constant::Int(i)) => i == 0, + Some(Constant::F32(f)) => f == 0.0, + Some(Constant::F64(f)) => f == 0.0, + _ => false, + } +} + +/// If the two expressions are negations of each other, then it returns +/// a tuple, in which the first element is true iff expr1 is the +/// positive expressions, and the second element is the positive +/// one of the two expressions +/// If the two expressions are not negations of each other, then it +/// returns None. +fn are_negated<'a>(cx: &LateContext<'_>, expr1: &'a Expr<'a>, expr2: &'a Expr<'a>) -> Option<(bool, &'a Expr<'a>)> { + if let ExprKind::Unary(UnOp::Neg, expr1_negated) = &expr1.kind { + if eq_expr_value(cx, expr1_negated, expr2) { + return Some((false, expr2)); + } + } + if let ExprKind::Unary(UnOp::Neg, expr2_negated) = &expr2.kind { + if eq_expr_value(cx, expr1, expr2_negated) { + return Some((true, expr1)); + } + } + None +} + +fn check_custom_abs(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let Some(higher::If { cond, then, r#else: Some(r#else) }) = higher::If::hir(expr); + let if_body_expr = peel_blocks(then); + let else_body_expr = peel_blocks(r#else); + if let Some((if_expr_positive, body)) = are_negated(cx, if_body_expr, else_body_expr); + then { + let positive_abs_sugg = ( + "manual implementation of `abs` method", + format!("{}.abs()", Sugg::hir(cx, body, "..")), + ); + let negative_abs_sugg = ( + "manual implementation of negation of `abs` method", + format!("-{}.abs()", Sugg::hir(cx, body, "..")), + ); + let sugg = if is_testing_positive(cx, cond, body) { + if if_expr_positive { + positive_abs_sugg + } else { + negative_abs_sugg + } + } else if is_testing_negative(cx, cond, body) { + if if_expr_positive { + negative_abs_sugg + } else { + positive_abs_sugg + } + } else { + return; + }; + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + expr.span, + sugg.0, + "try", + sugg.1, + Applicability::MachineApplicable, + ); + } + } +} + +fn are_same_base_logs(cx: &LateContext<'_>, expr_a: &Expr<'_>, expr_b: &Expr<'_>) -> bool { + if_chain! { + if let ExprKind::MethodCall(PathSegment { ident: method_name_a, .. }, args_a, _) = expr_a.kind; + if let ExprKind::MethodCall(PathSegment { ident: method_name_b, .. }, args_b, _) = expr_b.kind; + then { + return method_name_a.as_str() == method_name_b.as_str() && + args_a.len() == args_b.len() && + ( + ["ln", "log2", "log10"].contains(&method_name_a.as_str()) || + method_name_a.as_str() == "log" && args_a.len() == 2 && eq_expr_value(cx, &args_a[1], &args_b[1]) + ); + } + } + + false +} + +fn check_log_division(cx: &LateContext<'_>, expr: &Expr<'_>) { + // check if expression of the form x.logN() / y.logN() + if_chain! { + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Div, .. + }, + lhs, + rhs, + ) = &expr.kind; + if are_same_base_logs(cx, lhs, rhs); + if let ExprKind::MethodCall(_, [largs_self, ..], _) = &lhs.kind; + if let ExprKind::MethodCall(_, [rargs_self, ..], _) = &rhs.kind; + then { + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + expr.span, + "log base can be expressed more clearly", + "consider using", + format!("{}.log({})", Sugg::hir(cx, largs_self, ".."), Sugg::hir(cx, rargs_self, ".."),), + Applicability::MachineApplicable, + ); + } + } +} + +fn check_radians(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Div, .. + }, + div_lhs, + div_rhs, + ) = &expr.kind; + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Mul, .. + }, + mul_lhs, + mul_rhs, + ) = &div_lhs.kind; + if let Some((rvalue, _)) = constant(cx, cx.typeck_results(), div_rhs); + if let Some((lvalue, _)) = constant(cx, cx.typeck_results(), mul_rhs); + then { + // TODO: also check for constant values near PI/180 or 180/PI + if (F32(f32_consts::PI) == rvalue || F64(f64_consts::PI) == rvalue) && + (F32(180_f32) == lvalue || F64(180_f64) == lvalue) + { + let mut proposal = format!("{}.to_degrees()", Sugg::hir(cx, mul_lhs, "..")); + if_chain! { + if let ExprKind::Lit(ref literal) = mul_lhs.kind; + if let ast::LitKind::Float(ref value, float_type) = literal.node; + if float_type == ast::LitFloatType::Unsuffixed; + then { + if value.as_str().ends_with('.') { + proposal = format!("{}0_f64.to_degrees()", Sugg::hir(cx, mul_lhs, "..")); + } else { + proposal = format!("{}_f64.to_degrees()", Sugg::hir(cx, mul_lhs, "..")); + } + } + } + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + expr.span, + "conversion to degrees can be done more accurately", + "consider using", + proposal, + Applicability::MachineApplicable, + ); + } else if + (F32(180_f32) == rvalue || F64(180_f64) == rvalue) && + (F32(f32_consts::PI) == lvalue || F64(f64_consts::PI) == lvalue) + { + let mut proposal = format!("{}.to_radians()", Sugg::hir(cx, mul_lhs, "..")); + if_chain! { + if let ExprKind::Lit(ref literal) = mul_lhs.kind; + if let ast::LitKind::Float(ref value, float_type) = literal.node; + if float_type == ast::LitFloatType::Unsuffixed; + then { + if value.as_str().ends_with('.') { + proposal = format!("{}0_f64.to_radians()", Sugg::hir(cx, mul_lhs, "..")); + } else { + proposal = format!("{}_f64.to_radians()", Sugg::hir(cx, mul_lhs, "..")); + } + } + } + span_lint_and_sugg( + cx, + SUBOPTIMAL_FLOPS, + expr.span, + "conversion to radians can be done more accurately", + "consider using", + proposal, + Applicability::MachineApplicable, + ); + } + } + } +} + +impl<'tcx> LateLintPass<'tcx> for FloatingPointArithmetic { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // All of these operations are currently not const. + if in_constant(cx, expr.hir_id) { + return; + } + + if let ExprKind::MethodCall(path, args, _) = &expr.kind { + let recv_ty = cx.typeck_results().expr_ty(&args[0]); + + if recv_ty.is_floating_point() { + match path.ident.name.as_str() { + "ln" => check_ln1p(cx, expr, args), + "log" => check_log_base(cx, expr, args), + "powf" => check_powf(cx, expr, args), + "powi" => check_powi(cx, expr, args), + "sqrt" => check_hypot(cx, expr, args), + _ => {}, + } + } + } else { + check_expm1(cx, expr); + check_mul_add(cx, expr); + check_custom_abs(cx, expr); + check_log_division(cx, expr); + check_radians(cx, expr); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/format.rs b/src/tools/clippy/clippy_lints/src/format.rs new file mode 100644 index 000000000..925a8cb8d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/format.rs @@ -0,0 +1,123 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn}; +use clippy_utils::source::{snippet_opt, snippet_with_applicability}; +use clippy_utils::sugg::Sugg; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::kw; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of `format!("string literal with no + /// argument")` and `format!("{}", foo)` where `foo` is a string. + /// + /// ### Why is this bad? + /// There is no point of doing that. `format!("foo")` can + /// be replaced by `"foo".to_owned()` if you really need a `String`. The even + /// worse `&format!("foo")` is often encountered in the wild. `format!("{}", + /// foo)` can be replaced by `foo.clone()` if `foo: String` or `foo.to_owned()` + /// if `foo: &str`. + /// + /// ### Examples + /// ```rust + /// let foo = "foo"; + /// format!("{}", foo); + /// ``` + /// + /// Use instead: + /// ```rust + /// let foo = "foo"; + /// foo.to_owned(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USELESS_FORMAT, + complexity, + "useless use of `format!`" +} + +declare_lint_pass!(UselessFormat => [USELESS_FORMAT]); + +impl<'tcx> LateLintPass<'tcx> for UselessFormat { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (format_args, call_site) = if_chain! { + if let Some(macro_call) = root_macro_call_first_node(cx, expr); + if cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id); + if let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, macro_call.expn); + then { + (format_args, macro_call.span) + } else { + return + } + }; + + let mut applicability = Applicability::MachineApplicable; + if format_args.value_args.is_empty() { + match *format_args.format_string_parts { + [] => span_useless_format_empty(cx, call_site, "String::new()".to_owned(), applicability), + [_] => { + if let Some(s_src) = snippet_opt(cx, format_args.format_string_span) { + // Simulate macro expansion, converting {{ and }} to { and }. + let s_expand = s_src.replace("{{", "{").replace("}}", "}"); + let sugg = format!("{}.to_string()", s_expand); + span_useless_format(cx, call_site, sugg, applicability); + } + }, + [..] => {}, + } + } else if let [value] = *format_args.value_args { + if_chain! { + if format_args.format_string_parts == [kw::Empty]; + if match cx.typeck_results().expr_ty(value).peel_refs().kind() { + ty::Adt(adt, _) => cx.tcx.is_diagnostic_item(sym::String, adt.did()), + ty::Str => true, + _ => false, + }; + if let Some(args) = format_args.args(); + if args.iter().all(|arg| arg.format_trait == sym::Display && !arg.has_string_formatting()); + then { + let is_new_string = match value.kind { + ExprKind::Binary(..) => true, + ExprKind::MethodCall(path, ..) => path.ident.name == sym::to_string, + _ => false, + }; + let sugg = if is_new_string { + snippet_with_applicability(cx, value.span, "..", &mut applicability).into_owned() + } else { + let sugg = Sugg::hir_with_applicability(cx, value, "<arg>", &mut applicability); + format!("{}.to_string()", sugg.maybe_par()) + }; + span_useless_format(cx, call_site, sugg, applicability); + } + } + }; + } +} + +fn span_useless_format_empty(cx: &LateContext<'_>, span: Span, sugg: String, applicability: Applicability) { + span_lint_and_sugg( + cx, + USELESS_FORMAT, + span, + "useless use of `format!`", + "consider using `String::new()`", + sugg, + applicability, + ); +} + +fn span_useless_format(cx: &LateContext<'_>, span: Span, sugg: String, applicability: Applicability) { + span_lint_and_sugg( + cx, + USELESS_FORMAT, + span, + "useless use of `format!`", + "consider using `.to_string()`", + sugg, + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/format_args.rs b/src/tools/clippy/clippy_lints/src/format_args.rs new file mode 100644 index 000000000..1e6feaac2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/format_args.rs @@ -0,0 +1,199 @@ +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::is_diag_trait_item; +use clippy_utils::macros::{is_format_macro, FormatArgsArg, FormatArgsExpn}; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::implements_trait; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::adjustment::{Adjust, Adjustment}; +use rustc_middle::ty::Ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, ExpnData, ExpnKind, Span, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Detects `format!` within the arguments of another macro that does + /// formatting such as `format!` itself, `write!` or `println!`. Suggests + /// inlining the `format!` call. + /// + /// ### Why is this bad? + /// The recommended code is both shorter and avoids a temporary allocation. + /// + /// ### Example + /// ```rust + /// # use std::panic::Location; + /// println!("error: {}", format!("something failed at {}", Location::caller())); + /// ``` + /// Use instead: + /// ```rust + /// # use std::panic::Location; + /// println!("error: something failed at {}", Location::caller()); + /// ``` + #[clippy::version = "1.58.0"] + pub FORMAT_IN_FORMAT_ARGS, + perf, + "`format!` used in a macro that does formatting" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for [`ToString::to_string`](https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string) + /// applied to a type that implements [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html) + /// in a macro that does formatting. + /// + /// ### Why is this bad? + /// Since the type implements `Display`, the use of `to_string` is + /// unnecessary. + /// + /// ### Example + /// ```rust + /// # use std::panic::Location; + /// println!("error: something failed at {}", Location::caller().to_string()); + /// ``` + /// Use instead: + /// ```rust + /// # use std::panic::Location; + /// println!("error: something failed at {}", Location::caller()); + /// ``` + #[clippy::version = "1.58.0"] + pub TO_STRING_IN_FORMAT_ARGS, + perf, + "`to_string` applied to a type that implements `Display` in format args" +} + +declare_lint_pass!(FormatArgs => [FORMAT_IN_FORMAT_ARGS, TO_STRING_IN_FORMAT_ARGS]); + +impl<'tcx> LateLintPass<'tcx> for FormatArgs { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if_chain! { + if let Some(format_args) = FormatArgsExpn::parse(cx, expr); + let expr_expn_data = expr.span.ctxt().outer_expn_data(); + let outermost_expn_data = outermost_expn_data(expr_expn_data); + if let Some(macro_def_id) = outermost_expn_data.macro_def_id; + if is_format_macro(cx, macro_def_id); + if let ExpnKind::Macro(_, name) = outermost_expn_data.kind; + if let Some(args) = format_args.args(); + then { + for (i, arg) in args.iter().enumerate() { + if arg.format_trait != sym::Display { + continue; + } + if arg.has_string_formatting() { + continue; + } + if is_aliased(&args, i) { + continue; + } + check_format_in_format_args(cx, outermost_expn_data.call_site, name, arg.value); + check_to_string_in_format_args(cx, name, arg.value); + } + } + } + } +} + +fn outermost_expn_data(expn_data: ExpnData) -> ExpnData { + if expn_data.call_site.from_expansion() { + outermost_expn_data(expn_data.call_site.ctxt().outer_expn_data()) + } else { + expn_data + } +} + +fn check_format_in_format_args(cx: &LateContext<'_>, call_site: Span, name: Symbol, arg: &Expr<'_>) { + let expn_data = arg.span.ctxt().outer_expn_data(); + if expn_data.call_site.from_expansion() { + return; + } + let Some(mac_id) = expn_data.macro_def_id else { return }; + if !cx.tcx.is_diagnostic_item(sym::format_macro, mac_id) { + return; + } + span_lint_and_then( + cx, + FORMAT_IN_FORMAT_ARGS, + call_site, + &format!("`format!` in `{}!` args", name), + |diag| { + diag.help(&format!( + "combine the `format!(..)` arguments with the outer `{}!(..)` call", + name + )); + diag.help("or consider changing `format!` to `format_args!`"); + }, + ); +} + +fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Expr<'_>) { + if_chain! { + if !value.span.from_expansion(); + if let ExprKind::MethodCall(_, [receiver], _) = value.kind; + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(value.hir_id); + if is_diag_trait_item(cx, method_def_id, sym::ToString); + let receiver_ty = cx.typeck_results().expr_ty(receiver); + if let Some(display_trait_id) = cx.tcx.get_diagnostic_item(sym::Display); + if let Some(receiver_snippet) = snippet_opt(cx, receiver.span); + then { + let (n_needed_derefs, target) = count_needed_derefs( + receiver_ty, + cx.typeck_results().expr_adjustments(receiver).iter(), + ); + if implements_trait(cx, target, display_trait_id, &[]) { + if n_needed_derefs == 0 { + span_lint_and_sugg( + cx, + TO_STRING_IN_FORMAT_ARGS, + value.span.with_lo(receiver.span.hi()), + &format!("`to_string` applied to a type that implements `Display` in `{}!` args", name), + "remove this", + String::new(), + Applicability::MachineApplicable, + ); + } else { + span_lint_and_sugg( + cx, + TO_STRING_IN_FORMAT_ARGS, + value.span, + &format!("`to_string` applied to a type that implements `Display` in `{}!` args", name), + "use this", + format!("{:*>width$}{}", "", receiver_snippet, width = n_needed_derefs), + Applicability::MachineApplicable, + ); + } + } + } + } +} + +// Returns true if `args[i]` "refers to" or "is referred to by" another argument. +fn is_aliased(args: &[FormatArgsArg<'_>], i: usize) -> bool { + let value = args[i].value; + args.iter() + .enumerate() + .any(|(j, arg)| i != j && std::ptr::eq(value, arg.value)) +} + +fn count_needed_derefs<'tcx, I>(mut ty: Ty<'tcx>, mut iter: I) -> (usize, Ty<'tcx>) +where + I: Iterator<Item = &'tcx Adjustment<'tcx>>, +{ + let mut n_total = 0; + let mut n_needed = 0; + loop { + if let Some(Adjustment { + kind: Adjust::Deref(overloaded_deref), + target, + }) = iter.next() + { + n_total += 1; + if overloaded_deref.is_some() { + n_needed = n_total; + } + ty = *target; + } else { + return (n_needed, ty); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/format_impl.rs b/src/tools/clippy/clippy_lints/src/format_impl.rs new file mode 100644 index 000000000..04b5be6c8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/format_impl.rs @@ -0,0 +1,253 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; +use clippy_utils::macros::{is_format_macro, root_macro_call_first_node, FormatArgsArg, FormatArgsExpn}; +use clippy_utils::{get_parent_as_impl, is_diag_trait_item, path_to_local, peel_ref_operators}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, Impl, ImplItem, ImplItemKind, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{sym, symbol::kw, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for format trait implementations (e.g. `Display`) with a recursive call to itself + /// which uses `self` as a parameter. + /// This is typically done indirectly with the `write!` macro or with `to_string()`. + /// + /// ### Why is this bad? + /// This will lead to infinite recursion and a stack overflow. + /// + /// ### Example + /// + /// ```rust + /// use std::fmt; + /// + /// struct Structure(i32); + /// impl fmt::Display for Structure { + /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// write!(f, "{}", self.to_string()) + /// } + /// } + /// + /// ``` + /// Use instead: + /// ```rust + /// use std::fmt; + /// + /// struct Structure(i32); + /// impl fmt::Display for Structure { + /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// write!(f, "{}", self.0) + /// } + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub RECURSIVE_FORMAT_IMPL, + correctness, + "Format trait method called while implementing the same Format trait" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `println`, `print`, `eprintln` or `eprint` in an + /// implementation of a formatting trait. + /// + /// ### Why is this bad? + /// Using a print macro is likely unintentional since formatting traits + /// should write to the `Formatter`, not stdout/stderr. + /// + /// ### Example + /// ```rust + /// use std::fmt::{Display, Error, Formatter}; + /// + /// struct S; + /// impl Display for S { + /// fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + /// println!("S"); + /// + /// Ok(()) + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// use std::fmt::{Display, Error, Formatter}; + /// + /// struct S; + /// impl Display for S { + /// fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + /// writeln!(f, "S"); + /// + /// Ok(()) + /// } + /// } + /// ``` + #[clippy::version = "1.61.0"] + pub PRINT_IN_FORMAT_IMPL, + suspicious, + "use of a print macro in a formatting trait impl" +} + +#[derive(Clone, Copy)] +struct FormatTrait { + /// e.g. `sym::Display` + name: Symbol, + /// `f` in `fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {}` + formatter_name: Option<Symbol>, +} + +#[derive(Default)] +pub struct FormatImpl { + // Whether we are inside Display or Debug trait impl - None for neither + format_trait_impl: Option<FormatTrait>, +} + +impl FormatImpl { + pub fn new() -> Self { + Self { + format_trait_impl: None, + } + } +} + +impl_lint_pass!(FormatImpl => [RECURSIVE_FORMAT_IMPL, PRINT_IN_FORMAT_IMPL]); + +impl<'tcx> LateLintPass<'tcx> for FormatImpl { + fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) { + self.format_trait_impl = is_format_trait_impl(cx, impl_item); + } + + fn check_impl_item_post(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) { + // Assume no nested Impl of Debug and Display within eachother + if is_format_trait_impl(cx, impl_item).is_some() { + self.format_trait_impl = None; + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let Some(format_trait_impl) = self.format_trait_impl else { return }; + + if format_trait_impl.name == sym::Display { + check_to_string_in_display(cx, expr); + } + + check_self_in_format_args(cx, expr, format_trait_impl); + check_print_in_format_impl(cx, expr, format_trait_impl); + } +} + +fn check_to_string_in_display(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + // Get the hir_id of the object we are calling the method on + if let ExprKind::MethodCall(path, [ref self_arg, ..], _) = expr.kind; + // Is the method to_string() ? + if path.ident.name == sym::to_string; + // Is the method a part of the ToString trait? (i.e. not to_string() implemented + // separately) + if let Some(expr_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if is_diag_trait_item(cx, expr_def_id, sym::ToString); + // Is the method is called on self + if let ExprKind::Path(QPath::Resolved(_, path)) = self_arg.kind; + if let [segment] = path.segments; + if segment.ident.name == kw::SelfLower; + then { + span_lint( + cx, + RECURSIVE_FORMAT_IMPL, + expr.span, + "using `self.to_string` in `fmt::Display` implementation will cause infinite recursion", + ); + } + } +} + +fn check_self_in_format_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, impl_trait: FormatTrait) { + // Check each arg in format calls - do we ever use Display on self (directly or via deref)? + if_chain! { + if let Some(outer_macro) = root_macro_call_first_node(cx, expr); + if let macro_def_id = outer_macro.def_id; + if let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, outer_macro.expn); + if is_format_macro(cx, macro_def_id); + if let Some(args) = format_args.args(); + then { + for arg in args { + if arg.format_trait != impl_trait.name { + continue; + } + check_format_arg_self(cx, expr, &arg, impl_trait); + } + } + } +} + +fn check_format_arg_self(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &FormatArgsArg<'_>, impl_trait: FormatTrait) { + // Handle multiple dereferencing of references e.g. &&self + // Handle dereference of &self -> self that is equivalent (i.e. via *self in fmt() impl) + // Since the argument to fmt is itself a reference: &self + let reference = peel_ref_operators(cx, arg.value); + let map = cx.tcx.hir(); + // Is the reference self? + if path_to_local(reference).map(|x| map.name(x)) == Some(kw::SelfLower) { + let FormatTrait { name, .. } = impl_trait; + span_lint( + cx, + RECURSIVE_FORMAT_IMPL, + expr.span, + &format!("using `self` as `{name}` in `impl {name}` will cause infinite recursion"), + ); + } +} + +fn check_print_in_format_impl(cx: &LateContext<'_>, expr: &Expr<'_>, impl_trait: FormatTrait) { + if_chain! { + if let Some(macro_call) = root_macro_call_first_node(cx, expr); + if let Some(name) = cx.tcx.get_diagnostic_name(macro_call.def_id); + then { + let replacement = match name { + sym::print_macro | sym::eprint_macro => "write", + sym::println_macro | sym::eprintln_macro => "writeln", + _ => return, + }; + + let name = name.as_str().strip_suffix("_macro").unwrap(); + + span_lint_and_sugg( + cx, + PRINT_IN_FORMAT_IMPL, + macro_call.span, + &format!("use of `{}!` in `{}` impl", name, impl_trait.name), + "replace with", + if let Some(formatter_name) = impl_trait.formatter_name { + format!("{}!({}, ..)", replacement, formatter_name) + } else { + format!("{}!(..)", replacement) + }, + Applicability::HasPlaceholders, + ); + } + } +} + +fn is_format_trait_impl(cx: &LateContext<'_>, impl_item: &ImplItem<'_>) -> Option<FormatTrait> { + if_chain! { + if impl_item.ident.name == sym::fmt; + if let ImplItemKind::Fn(_, body_id) = impl_item.kind; + if let Some(Impl { of_trait: Some(trait_ref),..}) = get_parent_as_impl(cx.tcx, impl_item.hir_id()); + if let Some(did) = trait_ref.trait_def_id(); + if let Some(name) = cx.tcx.get_diagnostic_name(did); + if matches!(name, sym::Debug | sym::Display); + then { + let body = cx.tcx.hir().body(body_id); + let formatter_name = body.params.get(1) + .and_then(|param| param.pat.simple_ident()) + .map(|ident| ident.name); + + Some(FormatTrait { + name, + formatter_name, + }) + } else { + None + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/format_push_string.rs b/src/tools/clippy/clippy_lints/src/format_push_string.rs new file mode 100644 index 000000000..ebf5ab086 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/format_push_string.rs @@ -0,0 +1,83 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{match_def_path, paths, peel_hir_expr_refs}; +use rustc_hir::{BinOpKind, 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 + /// Detects cases where the result of a `format!` call is + /// appended to an existing `String`. + /// + /// ### Why is this bad? + /// Introduces an extra, avoidable heap allocation. + /// + /// ### Known problems + /// `format!` returns a `String` but `write!` returns a `Result`. + /// Thus you are forced to ignore the `Err` variant to achieve the same API. + /// + /// While using `write!` in the suggested way should never fail, this isn't necessarily clear to the programmer. + /// + /// ### Example + /// ```rust + /// let mut s = String::new(); + /// s += &format!("0x{:X}", 1024); + /// s.push_str(&format!("0x{:X}", 1024)); + /// ``` + /// Use instead: + /// ```rust + /// use std::fmt::Write as _; // import without risk of name clashing + /// + /// let mut s = String::new(); + /// let _ = write!(s, "0x{:X}", 1024); + /// ``` + #[clippy::version = "1.62.0"] + pub FORMAT_PUSH_STRING, + restriction, + "`format!(..)` appended to existing `String`" +} +declare_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]); + +fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), sym::String) +} +fn is_format(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + if 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 { + false + } +} + +impl<'tcx> LateLintPass<'tcx> for FormatPushString { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let arg = match expr.kind { + ExprKind::MethodCall(_, [_, arg], _) => { + if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) && + match_def_path(cx, fn_def_id, &paths::PUSH_STR) { + arg + } else { + return; + } + } + ExprKind::AssignOp(op, left, arg) + if op.node == BinOpKind::Add && is_string(cx, left) => { + arg + }, + _ => return, + }; + let (arg, _) = peel_hir_expr_refs(arg); + if is_format(cx, arg) { + span_lint_and_help( + cx, + FORMAT_PUSH_STRING, + expr.span, + "`format!(..)` appended to existing `String`", + None, + "consider using `write!` to avoid the extra allocation", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/formatting.rs b/src/tools/clippy/clippy_lints/src/formatting.rs new file mode 100644 index 000000000..db0166da5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/formatting.rs @@ -0,0 +1,341 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_note}; +use clippy_utils::source::snippet_opt; +use if_chain::if_chain; +use rustc_ast::ast::{BinOpKind, Block, Expr, ExprKind, StmtKind, UnOp}; +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::source_map::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of the non-existent `=*`, `=!` and `=-` + /// operators. + /// + /// ### Why is this bad? + /// This is either a typo of `*=`, `!=` or `-=` or + /// confusing. + /// + /// ### Example + /// ```rust,ignore + /// a =- 42; // confusing, should it be `a -= 42` or `a = -42`? + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SUSPICIOUS_ASSIGNMENT_FORMATTING, + suspicious, + "suspicious formatting of `*=`, `-=` or `!=`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks the formatting of a unary operator on the right hand side + /// of a binary operator. It lints if there is no space between the binary and unary operators, + /// but there is a space between the unary and its operand. + /// + /// ### Why is this bad? + /// This is either a typo in the binary operator or confusing. + /// + /// ### Example + /// ```rust + /// # let foo = true; + /// # let bar = false; + /// // &&! looks like a different operator + /// if foo &&! bar {} + /// ``` + /// + /// Use instead: + /// ```rust + /// # let foo = true; + /// # let bar = false; + /// if foo && !bar {} + /// ``` + #[clippy::version = "1.40.0"] + pub SUSPICIOUS_UNARY_OP_FORMATTING, + suspicious, + "suspicious formatting of unary `-` or `!` on the RHS of a BinOp" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for formatting of `else`. It lints if the `else` + /// is followed immediately by a newline or the `else` seems to be missing. + /// + /// ### Why is this bad? + /// This is probably some refactoring remnant, even if the + /// code is correct, it might look confusing. + /// + /// ### Example + /// ```rust,ignore + /// if foo { + /// } { // looks like an `else` is missing here + /// } + /// + /// if foo { + /// } if bar { // looks like an `else` is missing here + /// } + /// + /// if foo { + /// } else + /// + /// { // this is the `else` block of the previous `if`, but should it be? + /// } + /// + /// if foo { + /// } else + /// + /// if bar { // this is the `else` block of the previous `if`, but should it be? + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SUSPICIOUS_ELSE_FORMATTING, + suspicious, + "suspicious formatting of `else`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for possible missing comma in an array. It lints if + /// an array element is a binary operator expression and it lies on two lines. + /// + /// ### Why is this bad? + /// This could lead to unexpected results. + /// + /// ### Example + /// ```rust,ignore + /// let a = &[ + /// -1, -2, -3 // <= no comma here + /// -4, -5, -6 + /// ]; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub POSSIBLE_MISSING_COMMA, + correctness, + "possible missing comma in array" +} + +declare_lint_pass!(Formatting => [ + SUSPICIOUS_ASSIGNMENT_FORMATTING, + SUSPICIOUS_UNARY_OP_FORMATTING, + SUSPICIOUS_ELSE_FORMATTING, + POSSIBLE_MISSING_COMMA +]); + +impl EarlyLintPass for Formatting { + fn check_block(&mut self, cx: &EarlyContext<'_>, block: &Block) { + for w in block.stmts.windows(2) { + if let (StmtKind::Expr(first), StmtKind::Expr(second) | StmtKind::Semi(second)) = (&w[0].kind, &w[1].kind) { + check_missing_else(cx, first, second); + } + } + } + + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + check_assign(cx, expr); + check_unop(cx, expr); + check_else(cx, expr); + check_array(cx, expr); + } +} + +/// Implementation of the `SUSPICIOUS_ASSIGNMENT_FORMATTING` lint. +fn check_assign(cx: &EarlyContext<'_>, expr: &Expr) { + if let ExprKind::Assign(ref lhs, ref rhs, _) = expr.kind { + if !lhs.span.from_expansion() && !rhs.span.from_expansion() { + let eq_span = lhs.span.between(rhs.span); + if let ExprKind::Unary(op, ref sub_rhs) = rhs.kind { + if let Some(eq_snippet) = snippet_opt(cx, eq_span) { + let op = UnOp::to_string(op); + let eqop_span = lhs.span.between(sub_rhs.span); + if eq_snippet.ends_with('=') { + span_lint_and_note( + cx, + SUSPICIOUS_ASSIGNMENT_FORMATTING, + eqop_span, + &format!( + "this looks like you are trying to use `.. {op}= ..`, but you \ + really are doing `.. = ({op} ..)`", + op = op + ), + None, + &format!("to remove this lint, use either `{op}=` or `= {op}`", op = op), + ); + } + } + } + } + } +} + +/// Implementation of the `SUSPICIOUS_UNARY_OP_FORMATTING` lint. +fn check_unop(cx: &EarlyContext<'_>, expr: &Expr) { + if_chain! { + if let ExprKind::Binary(ref binop, ref lhs, ref rhs) = expr.kind; + if !lhs.span.from_expansion() && !rhs.span.from_expansion(); + // span between BinOp LHS and RHS + let binop_span = lhs.span.between(rhs.span); + // if RHS is an UnOp + if let ExprKind::Unary(op, ref un_rhs) = rhs.kind; + // from UnOp operator to UnOp operand + let unop_operand_span = rhs.span.until(un_rhs.span); + if let Some(binop_snippet) = snippet_opt(cx, binop_span); + if let Some(unop_operand_snippet) = snippet_opt(cx, unop_operand_span); + let binop_str = BinOpKind::to_string(&binop.node); + // no space after BinOp operator and space after UnOp operator + if binop_snippet.ends_with(binop_str) && unop_operand_snippet.ends_with(' '); + then { + let unop_str = UnOp::to_string(op); + let eqop_span = lhs.span.between(un_rhs.span); + span_lint_and_help( + cx, + SUSPICIOUS_UNARY_OP_FORMATTING, + eqop_span, + &format!( + "by not having a space between `{binop}` and `{unop}` it looks like \ + `{binop}{unop}` is a single operator", + binop = binop_str, + unop = unop_str + ), + None, + &format!( + "put a space between `{binop}` and `{unop}` and remove the space after `{unop}`", + binop = binop_str, + unop = unop_str + ), + ); + } + } +} + +/// Implementation of the `SUSPICIOUS_ELSE_FORMATTING` lint for weird `else`. +fn check_else(cx: &EarlyContext<'_>, expr: &Expr) { + if_chain! { + if let ExprKind::If(_, then, Some(else_)) = &expr.kind; + if is_block(else_) || is_if(else_); + if !then.span.from_expansion() && !else_.span.from_expansion(); + if !in_external_macro(cx.sess(), expr.span); + + // workaround for rust-lang/rust#43081 + if expr.span.lo().0 != 0 && expr.span.hi().0 != 0; + + // this will be a span from the closing ‘}’ of the “then” block (excluding) to + // the “if” of the “else if” block (excluding) + let else_span = then.span.between(else_.span); + + // the snippet should look like " else \n " with maybe comments anywhere + // it’s bad when there is a ‘\n’ after the “else” + if let Some(else_snippet) = snippet_opt(cx, else_span); + if let Some((pre_else, post_else)) = else_snippet.split_once("else"); + if let Some((_, post_else_post_eol)) = post_else.split_once('\n'); + + then { + // Allow allman style braces `} \n else \n {` + if_chain! { + if is_block(else_); + if let Some((_, pre_else_post_eol)) = pre_else.split_once('\n'); + // Exactly one eol before and after the else + if !pre_else_post_eol.contains('\n'); + if !post_else_post_eol.contains('\n'); + then { + return; + } + } + + let else_desc = if is_if(else_) { "if" } else { "{..}" }; + span_lint_and_note( + cx, + SUSPICIOUS_ELSE_FORMATTING, + else_span, + &format!("this is an `else {}` but the formatting might hide it", else_desc), + None, + &format!( + "to remove this lint, remove the `else` or remove the new line between \ + `else` and `{}`", + else_desc, + ), + ); + } + } +} + +#[must_use] +fn has_unary_equivalent(bin_op: BinOpKind) -> bool { + // &, *, - + bin_op == BinOpKind::And || bin_op == BinOpKind::Mul || bin_op == BinOpKind::Sub +} + +fn indentation(cx: &EarlyContext<'_>, span: Span) -> usize { + cx.sess().source_map().lookup_char_pos(span.lo()).col.0 +} + +/// Implementation of the `POSSIBLE_MISSING_COMMA` lint for array +fn check_array(cx: &EarlyContext<'_>, expr: &Expr) { + if let ExprKind::Array(ref array) = expr.kind { + for element in array { + if_chain! { + if let ExprKind::Binary(ref op, ref lhs, _) = element.kind; + if has_unary_equivalent(op.node) && lhs.span.ctxt() == op.span.ctxt(); + let space_span = lhs.span.between(op.span); + if let Some(space_snippet) = snippet_opt(cx, space_span); + let lint_span = lhs.span.with_lo(lhs.span.hi()); + if space_snippet.contains('\n'); + if indentation(cx, op.span) <= indentation(cx, lhs.span); + then { + span_lint_and_note( + cx, + POSSIBLE_MISSING_COMMA, + lint_span, + "possibly missing a comma here", + None, + "to remove this lint, add a comma or write the expr in a single line", + ); + } + } + } + } +} + +fn check_missing_else(cx: &EarlyContext<'_>, first: &Expr, second: &Expr) { + if_chain! { + if !first.span.from_expansion() && !second.span.from_expansion(); + if let ExprKind::If(cond_expr, ..) = &first.kind; + if is_block(second) || is_if(second); + + // Proc-macros can give weird spans. Make sure this is actually an `if`. + if let Some(if_snip) = snippet_opt(cx, first.span.until(cond_expr.span)); + if if_snip.starts_with("if"); + + // If there is a line break between the two expressions, don't lint. + // If there is a non-whitespace character, this span came from a proc-macro. + let else_span = first.span.between(second.span); + if let Some(else_snippet) = snippet_opt(cx, else_span); + if !else_snippet.chars().any(|c| c == '\n' || !c.is_whitespace()); + then { + let (looks_like, next_thing) = if is_if(second) { + ("an `else if`", "the second `if`") + } else { + ("an `else {..}`", "the next block") + }; + + span_lint_and_note( + cx, + SUSPICIOUS_ELSE_FORMATTING, + else_span, + &format!("this looks like {} but the `else` is missing", looks_like), + None, + &format!( + "to remove this lint, add the missing `else` or add a new line before {}", + next_thing, + ), + ); + } + } +} + +fn is_block(expr: &Expr) -> bool { + matches!(expr.kind, ExprKind::Block(..)) +} + +/// Check if the expression is an `if` or `if let` +fn is_if(expr: &Expr) -> bool { + matches!(expr.kind, ExprKind::If(..)) +} diff --git a/src/tools/clippy/clippy_lints/src/from_over_into.rs b/src/tools/clippy/clippy_lints/src/from_over_into.rs new file mode 100644 index 000000000..5d25c1d06 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/from_over_into.rs @@ -0,0 +1,81 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{meets_msrv, msrvs}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Searches for implementations of the `Into<..>` trait and suggests to implement `From<..>` instead. + /// + /// ### Why is this bad? + /// According the std docs implementing `From<..>` is preferred since it gives you `Into<..>` for free where the reverse isn't true. + /// + /// ### Example + /// ```rust + /// struct StringWrapper(String); + /// + /// impl Into<StringWrapper> for String { + /// fn into(self) -> StringWrapper { + /// StringWrapper(self) + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// struct StringWrapper(String); + /// + /// impl From<String> for StringWrapper { + /// fn from(s: String) -> StringWrapper { + /// StringWrapper(s) + /// } + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub FROM_OVER_INTO, + style, + "Warns on implementations of `Into<..>` to use `From<..>`" +} + +pub struct FromOverInto { + msrv: Option<RustcVersion>, +} + +impl FromOverInto { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + FromOverInto { msrv } + } +} + +impl_lint_pass!(FromOverInto => [FROM_OVER_INTO]); + +impl<'tcx> LateLintPass<'tcx> for FromOverInto { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + if !meets_msrv(self.msrv, msrvs::RE_REBALANCING_COHERENCE) { + return; + } + + if_chain! { + if let hir::ItemKind::Impl{ .. } = &item.kind; + if let Some(impl_trait_ref) = cx.tcx.impl_trait_ref(item.def_id); + if cx.tcx.is_diagnostic_item(sym::Into, impl_trait_ref.def_id); + + then { + span_lint_and_help( + cx, + FROM_OVER_INTO, + cx.tcx.sess.source_map().guess_head_span(item.span), + "an implementation of `From` is preferred since it gives you `Into<_>` for free where the reverse isn't true", + None, + &format!("consider to implement `From<{}>` instead", impl_trait_ref.self_ty()), + ); + } + } + } + + extract_msrv_attr!(LateContext); +} diff --git a/src/tools/clippy/clippy_lints/src/from_str_radix_10.rs b/src/tools/clippy/clippy_lints/src/from_str_radix_10.rs new file mode 100644 index 000000000..57b075132 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/from_str_radix_10.rs @@ -0,0 +1,103 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{def, Expr, ExprKind, PrimTy, QPath, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::Ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// + /// Checks for function invocations of the form `primitive::from_str_radix(s, 10)` + /// + /// ### Why is this bad? + /// + /// This specific common use case can be rewritten as `s.parse::<primitive>()` + /// (and in most cases, the turbofish can be removed), which reduces code length + /// and complexity. + /// + /// ### Known problems + /// + /// This lint may suggest using (&<expression>).parse() instead of <expression>.parse() directly + /// in some cases, which is correct but adds unnecessary complexity to the code. + /// + /// ### Example + /// ```ignore + /// let input: &str = get_input(); + /// let num = u16::from_str_radix(input, 10)?; + /// ``` + /// Use instead: + /// ```ignore + /// let input: &str = get_input(); + /// let num: u16 = input.parse()?; + /// ``` + #[clippy::version = "1.52.0"] + pub FROM_STR_RADIX_10, + style, + "from_str_radix with radix 10" +} + +declare_lint_pass!(FromStrRadix10 => [FROM_STR_RADIX_10]); + +impl<'tcx> LateLintPass<'tcx> for FromStrRadix10 { + fn check_expr(&mut self, cx: &LateContext<'tcx>, exp: &Expr<'tcx>) { + if_chain! { + if let ExprKind::Call(maybe_path, arguments) = &exp.kind; + if let ExprKind::Path(QPath::TypeRelative(ty, pathseg)) = &maybe_path.kind; + + // check if the first part of the path is some integer primitive + if let TyKind::Path(ty_qpath) = &ty.kind; + let ty_res = cx.qpath_res(ty_qpath, ty.hir_id); + if let def::Res::PrimTy(prim_ty) = ty_res; + if matches!(prim_ty, PrimTy::Int(_) | PrimTy::Uint(_)); + + // check if the second part of the path indeed calls the associated + // function `from_str_radix` + if pathseg.ident.name.as_str() == "from_str_radix"; + + // check if the second argument is a primitive `10` + if arguments.len() == 2; + if let ExprKind::Lit(lit) = &arguments[1].kind; + if let rustc_ast::ast::LitKind::Int(10, _) = lit.node; + + then { + let expr = if let ExprKind::AddrOf(_, _, expr) = &arguments[0].kind { + let ty = cx.typeck_results().expr_ty(expr); + if is_ty_stringish(cx, ty) { + expr + } else { + &arguments[0] + } + } else { + &arguments[0] + }; + + let sugg = Sugg::hir_with_applicability( + cx, + expr, + "<string>", + &mut Applicability::MachineApplicable + ).maybe_par(); + + span_lint_and_sugg( + cx, + FROM_STR_RADIX_10, + exp.span, + "this call to `from_str_radix` can be replaced with a call to `str::parse`", + "try", + format!("{}.parse::<{}>()", sugg, prim_ty.name_str()), + Applicability::MaybeIncorrect + ); + } + } + } +} + +/// Checks if a Ty is `String` or `&str` +fn is_ty_stringish(cx: &LateContext<'_>, ty: Ty<'_>) -> bool { + is_type_diagnostic_item(cx, ty, sym::String) || is_type_diagnostic_item(cx, ty, sym::str) +} diff --git a/src/tools/clippy/clippy_lints/src/functions/mod.rs b/src/tools/clippy/clippy_lints/src/functions/mod.rs new file mode 100644 index 000000000..73261fb8a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/functions/mod.rs @@ -0,0 +1,276 @@ +mod must_use; +mod not_unsafe_ptr_arg_deref; +mod result_unit_err; +mod too_many_arguments; +mod too_many_lines; + +use rustc_hir as hir; +use rustc_hir::intravisit; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions with too many parameters. + /// + /// ### Why is this bad? + /// Functions with lots of parameters are considered bad + /// style and reduce readability (“what does the 5th parameter mean?”). Consider + /// grouping some parameters into a new type. + /// + /// ### Example + /// ```rust + /// # struct Color; + /// fn foo(x: u32, y: u32, name: &str, c: Color, w: f32, h: f32, a: f32, b: f32) { + /// // .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TOO_MANY_ARGUMENTS, + complexity, + "functions with too many arguments" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions with a large amount of lines. + /// + /// ### Why is this bad? + /// Functions with a lot of lines are harder to understand + /// due to having to look at a larger amount of code to understand what the + /// function is doing. Consider splitting the body of the function into + /// multiple functions. + /// + /// ### Example + /// ```rust + /// fn im_too_long() { + /// println!(""); + /// // ... 100 more LoC + /// println!(""); + /// } + /// ``` + #[clippy::version = "1.34.0"] + pub TOO_MANY_LINES, + pedantic, + "functions with too many lines" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for public functions that dereference raw pointer + /// arguments but are not marked `unsafe`. + /// + /// ### Why is this bad? + /// The function should probably be marked `unsafe`, since + /// for an arbitrary raw pointer, there is no way of telling for sure if it is + /// valid. + /// + /// ### Known problems + /// * It does not check functions recursively so if the pointer is passed to a + /// private non-`unsafe` function which does the dereferencing, the lint won't + /// trigger. + /// * It only checks for arguments whose type are raw pointers, not raw pointers + /// got from an argument in some other way (`fn foo(bar: &[*const u8])` or + /// `some_argument.get_raw_ptr()`). + /// + /// ### Example + /// ```rust,ignore + /// pub fn foo(x: *const u8) { + /// println!("{}", unsafe { *x }); + /// } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// pub unsafe fn foo(x: *const u8) { + /// println!("{}", unsafe { *x }); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NOT_UNSAFE_PTR_ARG_DEREF, + correctness, + "public functions dereferencing raw pointer arguments but not marked `unsafe`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for a `#[must_use]` attribute on + /// unit-returning functions and methods. + /// + /// ### Why is this bad? + /// Unit values are useless. The attribute is likely + /// a remnant of a refactoring that removed the return type. + /// + /// ### Examples + /// ```rust + /// #[must_use] + /// fn useless() { } + /// ``` + #[clippy::version = "1.40.0"] + pub MUST_USE_UNIT, + style, + "`#[must_use]` attribute on a unit-returning function / method" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for a `#[must_use]` attribute without + /// further information on functions and methods that return a type already + /// marked as `#[must_use]`. + /// + /// ### Why is this bad? + /// The attribute isn't needed. Not using the result + /// will already be reported. Alternatively, one can add some text to the + /// attribute to improve the lint message. + /// + /// ### Examples + /// ```rust + /// #[must_use] + /// fn double_must_use() -> Result<(), ()> { + /// unimplemented!(); + /// } + /// ``` + #[clippy::version = "1.40.0"] + pub DOUBLE_MUST_USE, + style, + "`#[must_use]` attribute on a `#[must_use]`-returning function / method" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for public functions that have no + /// `#[must_use]` attribute, but return something not already marked + /// must-use, have no mutable arg and mutate no statics. + /// + /// ### Why is this bad? + /// Not bad at all, this lint just shows places where + /// you could add the attribute. + /// + /// ### Known problems + /// The lint only checks the arguments for mutable + /// types without looking if they are actually changed. On the other hand, + /// it also ignores a broad range of potentially interesting side effects, + /// because we cannot decide whether the programmer intends the function to + /// be called for the side effect or the result. Expect many false + /// positives. At least we don't lint if the result type is unit or already + /// `#[must_use]`. + /// + /// ### Examples + /// ```rust + /// // this could be annotated with `#[must_use]`. + /// fn id<T>(t: T) -> T { t } + /// ``` + #[clippy::version = "1.40.0"] + pub MUST_USE_CANDIDATE, + pedantic, + "function or method that could take a `#[must_use]` attribute" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for public functions that return a `Result` + /// with an `Err` type of `()`. It suggests using a custom type that + /// implements `std::error::Error`. + /// + /// ### Why is this bad? + /// Unit does not implement `Error` and carries no + /// further information about what went wrong. + /// + /// ### Known problems + /// Of course, this lint assumes that `Result` is used + /// for a fallible operation (which is after all the intended use). However + /// code may opt to (mis)use it as a basic two-variant-enum. In that case, + /// the suggestion is misguided, and the code should use a custom enum + /// instead. + /// + /// ### Examples + /// ```rust + /// pub fn read_u8() -> Result<u8, ()> { Err(()) } + /// ``` + /// should become + /// ```rust,should_panic + /// use std::fmt; + /// + /// #[derive(Debug)] + /// pub struct EndOfStream; + /// + /// impl fmt::Display for EndOfStream { + /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + /// write!(f, "End of Stream") + /// } + /// } + /// + /// impl std::error::Error for EndOfStream { } + /// + /// pub fn read_u8() -> Result<u8, EndOfStream> { Err(EndOfStream) } + ///# fn main() { + ///# read_u8().unwrap(); + ///# } + /// ``` + /// + /// Note that there are crates that simplify creating the error type, e.g. + /// [`thiserror`](https://docs.rs/thiserror). + #[clippy::version = "1.49.0"] + pub RESULT_UNIT_ERR, + style, + "public function returning `Result` with an `Err` type of `()`" +} + +#[derive(Copy, Clone)] +pub struct Functions { + too_many_arguments_threshold: u64, + too_many_lines_threshold: u64, +} + +impl Functions { + pub fn new(too_many_arguments_threshold: u64, too_many_lines_threshold: u64) -> Self { + Self { + too_many_arguments_threshold, + too_many_lines_threshold, + } + } +} + +impl_lint_pass!(Functions => [ + TOO_MANY_ARGUMENTS, + TOO_MANY_LINES, + NOT_UNSAFE_PTR_ARG_DEREF, + MUST_USE_UNIT, + DOUBLE_MUST_USE, + MUST_USE_CANDIDATE, + RESULT_UNIT_ERR, +]); + +impl<'tcx> LateLintPass<'tcx> for Functions { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: intravisit::FnKind<'tcx>, + decl: &'tcx hir::FnDecl<'_>, + body: &'tcx hir::Body<'_>, + span: Span, + hir_id: hir::HirId, + ) { + too_many_arguments::check_fn(cx, kind, decl, span, hir_id, self.too_many_arguments_threshold); + too_many_lines::check_fn(cx, kind, span, body, self.too_many_lines_threshold); + not_unsafe_ptr_arg_deref::check_fn(cx, kind, decl, body, hir_id); + } + + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + must_use::check_item(cx, item); + result_unit_err::check_item(cx, item); + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) { + must_use::check_impl_item(cx, item); + result_unit_err::check_impl_item(cx, item); + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) { + too_many_arguments::check_trait_item(cx, item, self.too_many_arguments_threshold); + not_unsafe_ptr_arg_deref::check_trait_item(cx, item); + must_use::check_trait_item(cx, item); + result_unit_err::check_trait_item(cx, item); + } +} diff --git a/src/tools/clippy/clippy_lints/src/functions/must_use.rs b/src/tools/clippy/clippy_lints/src/functions/must_use.rs new file mode 100644 index 000000000..6672a6cb0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/functions/must_use.rs @@ -0,0 +1,259 @@ +use rustc_ast::ast::Attribute; +use rustc_errors::Applicability; +use rustc_hir::def_id::{DefIdSet, LocalDefId}; +use rustc_hir::{self as hir, def::Res, intravisit, QPath}; +use rustc_lint::{LateContext, LintContext}; +use rustc_middle::{ + lint::in_external_macro, + ty::{self, Ty}, +}; +use rustc_span::{sym, Span}; + +use clippy_utils::attrs::is_proc_macro; +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then}; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::is_must_use_ty; +use clippy_utils::{match_def_path, return_ty, trait_ref_of_method}; + +use super::{DOUBLE_MUST_USE, MUST_USE_CANDIDATE, MUST_USE_UNIT}; + +pub(super) fn check_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + let attr = cx.tcx.get_attr(item.def_id.to_def_id(), sym::must_use); + if let hir::ItemKind::Fn(ref sig, _generics, ref body_id) = item.kind { + let is_public = cx.access_levels.is_exported(item.def_id); + let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); + if let Some(attr) = attr { + check_needless_must_use(cx, sig.decl, item.hir_id(), item.span, fn_header_span, attr); + } else if is_public && !is_proc_macro(cx.sess(), attrs) && !attrs.iter().any(|a| a.has_name(sym::no_mangle)) { + check_must_use_candidate( + cx, + sig.decl, + cx.tcx.hir().body(*body_id), + item.span, + item.def_id, + item.span.with_hi(sig.decl.output.span().hi()), + "this function could have a `#[must_use]` attribute", + ); + } + } +} + +pub(super) fn check_impl_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) { + if let hir::ImplItemKind::Fn(ref sig, ref body_id) = item.kind { + let is_public = cx.access_levels.is_exported(item.def_id); + let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); + let attrs = cx.tcx.hir().attrs(item.hir_id()); + let attr = cx.tcx.get_attr(item.def_id.to_def_id(), sym::must_use); + if let Some(attr) = attr { + check_needless_must_use(cx, sig.decl, item.hir_id(), item.span, fn_header_span, attr); + } else if is_public && !is_proc_macro(cx.sess(), attrs) && trait_ref_of_method(cx, item.def_id).is_none() { + check_must_use_candidate( + cx, + sig.decl, + cx.tcx.hir().body(*body_id), + item.span, + item.def_id, + item.span.with_hi(sig.decl.output.span().hi()), + "this method could have a `#[must_use]` attribute", + ); + } + } +} + +pub(super) fn check_trait_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) { + if let hir::TraitItemKind::Fn(ref sig, ref eid) = item.kind { + let is_public = cx.access_levels.is_exported(item.def_id); + let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); + + let attrs = cx.tcx.hir().attrs(item.hir_id()); + let attr = cx.tcx.get_attr(item.def_id.to_def_id(), sym::must_use); + if let Some(attr) = attr { + check_needless_must_use(cx, sig.decl, item.hir_id(), item.span, fn_header_span, attr); + } else if let hir::TraitFn::Provided(eid) = *eid { + let body = cx.tcx.hir().body(eid); + if attr.is_none() && is_public && !is_proc_macro(cx.sess(), attrs) { + check_must_use_candidate( + cx, + sig.decl, + body, + item.span, + item.def_id, + item.span.with_hi(sig.decl.output.span().hi()), + "this method could have a `#[must_use]` attribute", + ); + } + } + } +} + +fn check_needless_must_use( + cx: &LateContext<'_>, + decl: &hir::FnDecl<'_>, + item_id: hir::HirId, + item_span: Span, + fn_header_span: Span, + attr: &Attribute, +) { + if in_external_macro(cx.sess(), item_span) { + return; + } + if returns_unit(decl) { + span_lint_and_then( + cx, + MUST_USE_UNIT, + fn_header_span, + "this unit-returning function has a `#[must_use]` attribute", + |diag| { + diag.span_suggestion(attr.span, "remove the attribute", "", Applicability::MachineApplicable); + }, + ); + } else if attr.value_str().is_none() && is_must_use_ty(cx, return_ty(cx, item_id)) { + span_lint_and_help( + cx, + DOUBLE_MUST_USE, + fn_header_span, + "this function has an empty `#[must_use]` attribute, but returns a type already marked as `#[must_use]`", + None, + "either add some descriptive text or remove the attribute", + ); + } +} + +fn check_must_use_candidate<'tcx>( + cx: &LateContext<'tcx>, + decl: &'tcx hir::FnDecl<'_>, + body: &'tcx hir::Body<'_>, + item_span: Span, + item_id: LocalDefId, + fn_span: Span, + msg: &str, +) { + if has_mutable_arg(cx, body) + || mutates_static(cx, body) + || in_external_macro(cx.sess(), item_span) + || returns_unit(decl) + || !cx.access_levels.is_exported(item_id) + || is_must_use_ty(cx, return_ty(cx, cx.tcx.hir().local_def_id_to_hir_id(item_id))) + { + return; + } + span_lint_and_then(cx, MUST_USE_CANDIDATE, fn_span, msg, |diag| { + if let Some(snippet) = snippet_opt(cx, fn_span) { + diag.span_suggestion( + fn_span, + "add the attribute", + format!("#[must_use] {}", snippet), + Applicability::MachineApplicable, + ); + } + }); +} + +fn returns_unit(decl: &hir::FnDecl<'_>) -> bool { + match decl.output { + hir::FnRetTy::DefaultReturn(_) => true, + hir::FnRetTy::Return(ty) => match ty.kind { + hir::TyKind::Tup(tys) => tys.is_empty(), + hir::TyKind::Never => true, + _ => false, + }, + } +} + +fn has_mutable_arg(cx: &LateContext<'_>, body: &hir::Body<'_>) -> bool { + let mut tys = DefIdSet::default(); + body.params.iter().any(|param| is_mutable_pat(cx, param.pat, &mut tys)) +} + +fn is_mutable_pat(cx: &LateContext<'_>, pat: &hir::Pat<'_>, tys: &mut DefIdSet) -> bool { + if let hir::PatKind::Wild = pat.kind { + return false; // ignore `_` patterns + } + if cx.tcx.has_typeck_results(pat.hir_id.owner.to_def_id()) { + is_mutable_ty(cx, cx.tcx.typeck(pat.hir_id.owner).pat_ty(pat), pat.span, tys) + } else { + false + } +} + +static KNOWN_WRAPPER_TYS: &[&[&str]] = &[&["alloc", "rc", "Rc"], &["std", "sync", "Arc"]]; + +fn is_mutable_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, span: Span, tys: &mut DefIdSet) -> bool { + match *ty.kind() { + // primitive types are never mutable + ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) | ty::Float(_) | ty::Str => false, + ty::Adt(adt, substs) => { + tys.insert(adt.did()) && !ty.is_freeze(cx.tcx.at(span), cx.param_env) + || KNOWN_WRAPPER_TYS.iter().any(|path| match_def_path(cx, adt.did(), path)) + && substs.types().any(|ty| is_mutable_ty(cx, ty, span, tys)) + }, + ty::Tuple(substs) => substs.iter().any(|ty| is_mutable_ty(cx, ty, span, tys)), + ty::Array(ty, _) | ty::Slice(ty) => is_mutable_ty(cx, ty, span, tys), + ty::RawPtr(ty::TypeAndMut { ty, mutbl }) | ty::Ref(_, ty, mutbl) => { + mutbl == hir::Mutability::Mut || is_mutable_ty(cx, ty, span, tys) + }, + // calling something constitutes a side effect, so return true on all callables + // also never calls need not be used, so return true for them, too + _ => true, + } +} + +struct StaticMutVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + mutates_static: bool, +} + +impl<'a, 'tcx> intravisit::Visitor<'tcx> for StaticMutVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) { + use hir::ExprKind::{AddrOf, Assign, AssignOp, Call, MethodCall}; + + if self.mutates_static { + return; + } + match expr.kind { + Call(_, args) | MethodCall(_, args, _) => { + let mut tys = DefIdSet::default(); + for arg in args { + if self.cx.tcx.has_typeck_results(arg.hir_id.owner.to_def_id()) + && is_mutable_ty( + self.cx, + self.cx.tcx.typeck(arg.hir_id.owner).expr_ty(arg), + arg.span, + &mut tys, + ) + && is_mutated_static(arg) + { + self.mutates_static = true; + return; + } + tys.clear(); + } + }, + Assign(target, ..) | AssignOp(_, target, _) | AddrOf(_, hir::Mutability::Mut, target) => { + self.mutates_static |= is_mutated_static(target); + }, + _ => {}, + } + } +} + +fn is_mutated_static(e: &hir::Expr<'_>) -> bool { + use hir::ExprKind::{Field, Index, Path}; + + match e.kind { + Path(QPath::Resolved(_, path)) => !matches!(path.res, Res::Local(_)), + Path(_) => true, + Field(inner, _) | Index(inner, _) => is_mutated_static(inner), + _ => false, + } +} + +fn mutates_static<'tcx>(cx: &LateContext<'tcx>, body: &'tcx hir::Body<'_>) -> bool { + let mut v = StaticMutVisitor { + cx, + mutates_static: false, + }; + intravisit::walk_expr(&mut v, &body.value); + v.mutates_static +} diff --git a/src/tools/clippy/clippy_lints/src/functions/not_unsafe_ptr_arg_deref.rs b/src/tools/clippy/clippy_lints/src/functions/not_unsafe_ptr_arg_deref.rs new file mode 100644 index 000000000..565a1c871 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/functions/not_unsafe_ptr_arg_deref.rs @@ -0,0 +1,122 @@ +use rustc_hir::{self as hir, intravisit, HirIdSet}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::def_id::LocalDefId; + +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::type_is_unsafe_function; +use clippy_utils::{iter_input_pats, path_to_local}; + +use super::NOT_UNSAFE_PTR_ARG_DEREF; + +pub(super) fn check_fn<'tcx>( + cx: &LateContext<'tcx>, + kind: intravisit::FnKind<'tcx>, + decl: &'tcx hir::FnDecl<'tcx>, + body: &'tcx hir::Body<'tcx>, + hir_id: hir::HirId, +) { + let unsafety = match kind { + intravisit::FnKind::ItemFn(_, _, hir::FnHeader { unsafety, .. }) => unsafety, + intravisit::FnKind::Method(_, sig) => sig.header.unsafety, + intravisit::FnKind::Closure => return, + }; + + check_raw_ptr(cx, unsafety, decl, body, cx.tcx.hir().local_def_id(hir_id)); +} + +pub(super) fn check_trait_item<'tcx>(cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) { + if let hir::TraitItemKind::Fn(ref sig, hir::TraitFn::Provided(eid)) = item.kind { + let body = cx.tcx.hir().body(eid); + check_raw_ptr(cx, sig.header.unsafety, sig.decl, body, item.def_id); + } +} + +fn check_raw_ptr<'tcx>( + cx: &LateContext<'tcx>, + unsafety: hir::Unsafety, + decl: &'tcx hir::FnDecl<'tcx>, + body: &'tcx hir::Body<'tcx>, + def_id: LocalDefId, +) { + let expr = &body.value; + if unsafety == hir::Unsafety::Normal && cx.access_levels.is_exported(def_id) { + let raw_ptrs = iter_input_pats(decl, body) + .filter_map(|arg| raw_ptr_arg(cx, arg)) + .collect::<HirIdSet>(); + + if !raw_ptrs.is_empty() { + let typeck_results = cx.tcx.typeck_body(body.id()); + let mut v = DerefVisitor { + cx, + ptrs: raw_ptrs, + typeck_results, + }; + + intravisit::walk_expr(&mut v, expr); + } + } +} + +fn raw_ptr_arg(cx: &LateContext<'_>, arg: &hir::Param<'_>) -> Option<hir::HirId> { + if let (&hir::PatKind::Binding(_, id, _, _), Some(&ty::RawPtr(_))) = ( + &arg.pat.kind, + cx.maybe_typeck_results() + .map(|typeck_results| typeck_results.pat_ty(arg.pat).kind()), + ) { + Some(id) + } else { + None + } +} + +struct DerefVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + ptrs: HirIdSet, + typeck_results: &'a ty::TypeckResults<'tcx>, +} + +impl<'a, 'tcx> intravisit::Visitor<'tcx> for DerefVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) { + match expr.kind { + hir::ExprKind::Call(f, args) => { + let ty = self.typeck_results.expr_ty(f); + + if type_is_unsafe_function(self.cx, ty) { + for arg in args { + self.check_arg(arg); + } + } + }, + hir::ExprKind::MethodCall(_, args, _) => { + let def_id = self.typeck_results.type_dependent_def_id(expr.hir_id).unwrap(); + let base_type = self.cx.tcx.type_of(def_id); + + if type_is_unsafe_function(self.cx, base_type) { + for arg in args { + self.check_arg(arg); + } + } + }, + hir::ExprKind::Unary(hir::UnOp::Deref, ptr) => self.check_arg(ptr), + _ => (), + } + + intravisit::walk_expr(self, expr); + } +} + +impl<'a, 'tcx> DerefVisitor<'a, 'tcx> { + fn check_arg(&self, ptr: &hir::Expr<'_>) { + if let Some(id) = path_to_local(ptr) { + if self.ptrs.contains(&id) { + span_lint( + self.cx, + NOT_UNSAFE_PTR_ARG_DEREF, + ptr.span, + "this public function might dereference a raw pointer but is not marked `unsafe`", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/functions/result_unit_err.rs b/src/tools/clippy/clippy_lints/src/functions/result_unit_err.rs new file mode 100644 index 000000000..2e63a1f92 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/functions/result_unit_err.rs @@ -0,0 +1,66 @@ +use rustc_hir as hir; +use rustc_lint::{LateContext, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_span::{sym, Span}; +use rustc_typeck::hir_ty_to_ty; + +use if_chain::if_chain; + +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::trait_ref_of_method; +use clippy_utils::ty::is_type_diagnostic_item; + +use super::RESULT_UNIT_ERR; + +pub(super) fn check_item(cx: &LateContext<'_>, item: &hir::Item<'_>) { + if let hir::ItemKind::Fn(ref sig, _generics, _) = item.kind { + let is_public = cx.access_levels.is_exported(item.def_id); + let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); + if is_public { + check_result_unit_err(cx, sig.decl, item.span, fn_header_span); + } + } +} + +pub(super) fn check_impl_item(cx: &LateContext<'_>, item: &hir::ImplItem<'_>) { + if let hir::ImplItemKind::Fn(ref sig, _) = item.kind { + let is_public = cx.access_levels.is_exported(item.def_id); + let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); + if is_public && trait_ref_of_method(cx, item.def_id).is_none() { + check_result_unit_err(cx, sig.decl, item.span, fn_header_span); + } + } +} + +pub(super) fn check_trait_item(cx: &LateContext<'_>, item: &hir::TraitItem<'_>) { + if let hir::TraitItemKind::Fn(ref sig, _) = item.kind { + let is_public = cx.access_levels.is_exported(item.def_id); + let fn_header_span = item.span.with_hi(sig.decl.output.span().hi()); + if is_public { + check_result_unit_err(cx, sig.decl, item.span, fn_header_span); + } + } +} + +fn check_result_unit_err(cx: &LateContext<'_>, decl: &hir::FnDecl<'_>, item_span: Span, fn_header_span: Span) { + if_chain! { + if !in_external_macro(cx.sess(), item_span); + if let hir::FnRetTy::Return(ty) = decl.output; + let ty = hir_ty_to_ty(cx.tcx, ty); + if is_type_diagnostic_item(cx, ty, sym::Result); + if let ty::Adt(_, substs) = ty.kind(); + let err_ty = substs.type_at(1); + if err_ty.is_unit(); + then { + span_lint_and_help( + cx, + RESULT_UNIT_ERR, + fn_header_span, + "this returns a `Result<_, ()>`", + None, + "use a custom `Error` type instead", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/functions/too_many_arguments.rs b/src/tools/clippy/clippy_lints/src/functions/too_many_arguments.rs new file mode 100644 index 000000000..5c8d8b8e7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/functions/too_many_arguments.rs @@ -0,0 +1,68 @@ +use rustc_hir::{self as hir, intravisit}; +use rustc_lint::LateContext; +use rustc_span::Span; +use rustc_target::spec::abi::Abi; + +use clippy_utils::diagnostics::span_lint; +use clippy_utils::is_trait_impl_item; + +use super::TOO_MANY_ARGUMENTS; + +pub(super) fn check_fn( + cx: &LateContext<'_>, + kind: intravisit::FnKind<'_>, + decl: &hir::FnDecl<'_>, + span: Span, + hir_id: hir::HirId, + too_many_arguments_threshold: u64, +) { + // don't warn for implementations, it's not their fault + if !is_trait_impl_item(cx, hir_id) { + // don't lint extern functions decls, it's not their fault either + match kind { + intravisit::FnKind::Method( + _, + &hir::FnSig { + header: hir::FnHeader { abi: Abi::Rust, .. }, + .. + }, + ) + | intravisit::FnKind::ItemFn(_, _, hir::FnHeader { abi: Abi::Rust, .. }) => check_arg_number( + cx, + decl, + span.with_hi(decl.output.span().hi()), + too_many_arguments_threshold, + ), + _ => {}, + } + } +} + +pub(super) fn check_trait_item(cx: &LateContext<'_>, item: &hir::TraitItem<'_>, too_many_arguments_threshold: u64) { + if let hir::TraitItemKind::Fn(ref sig, _) = item.kind { + // don't lint extern functions decls, it's not their fault + if sig.header.abi == Abi::Rust { + check_arg_number( + cx, + sig.decl, + item.span.with_hi(sig.decl.output.span().hi()), + too_many_arguments_threshold, + ); + } + } +} + +fn check_arg_number(cx: &LateContext<'_>, decl: &hir::FnDecl<'_>, fn_span: Span, too_many_arguments_threshold: u64) { + let args = decl.inputs.len() as u64; + if args > too_many_arguments_threshold { + span_lint( + cx, + TOO_MANY_ARGUMENTS, + fn_span, + &format!( + "this function has too many arguments ({}/{})", + args, too_many_arguments_threshold + ), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/functions/too_many_lines.rs b/src/tools/clippy/clippy_lints/src/functions/too_many_lines.rs new file mode 100644 index 000000000..54bdea7ea --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/functions/too_many_lines.rs @@ -0,0 +1,87 @@ +use rustc_hir as hir; +use rustc_hir::intravisit::FnKind; +use rustc_lint::{LateContext, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_span::Span; + +use clippy_utils::diagnostics::span_lint; +use clippy_utils::source::snippet_opt; + +use super::TOO_MANY_LINES; + +pub(super) fn check_fn( + cx: &LateContext<'_>, + kind: FnKind<'_>, + span: Span, + body: &hir::Body<'_>, + too_many_lines_threshold: u64, +) { + // Closures must be contained in a parent body, which will be checked for `too_many_lines`. + // Don't check closures for `too_many_lines` to avoid duplicated lints. + if matches!(kind, FnKind::Closure) || in_external_macro(cx.sess(), span) { + return; + } + + let code_snippet = match snippet_opt(cx, body.value.span) { + Some(s) => s, + _ => return, + }; + let mut line_count: u64 = 0; + let mut in_comment = false; + let mut code_in_line; + + let function_lines = if matches!(body.value.kind, hir::ExprKind::Block(..)) + && code_snippet.as_bytes().first().copied() == Some(b'{') + && code_snippet.as_bytes().last().copied() == Some(b'}') + { + // Removing the braces from the enclosing block + &code_snippet[1..code_snippet.len() - 1] + } else { + &code_snippet + } + .trim() // Remove leading and trailing blank lines + .lines(); + + for mut line in function_lines { + code_in_line = false; + loop { + line = line.trim_start(); + if line.is_empty() { + break; + } + if in_comment { + if let Some(i) = line.find("*/") { + line = &line[i + 2..]; + in_comment = false; + continue; + } + } else { + let multi_idx = line.find("/*").unwrap_or(line.len()); + let single_idx = line.find("//").unwrap_or(line.len()); + code_in_line |= multi_idx > 0 && single_idx > 0; + // Implies multi_idx is below line.len() + if multi_idx < single_idx { + line = &line[multi_idx + 2..]; + in_comment = true; + continue; + } + } + break; + } + if code_in_line { + line_count += 1; + } + } + + if line_count > too_many_lines_threshold { + span_lint( + cx, + TOO_MANY_LINES, + span, + &format!( + "this function has too many lines ({}/{})", + line_count, too_many_lines_threshold + ), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/future_not_send.rs b/src/tools/clippy/clippy_lints/src/future_not_send.rs new file mode 100644 index 000000000..5c46d6c7d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/future_not_send.rs @@ -0,0 +1,112 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::return_ty; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{Body, FnDecl, HirId}; +use rustc_infer::infer::TyCtxtInferExt; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::subst::Subst; +use rustc_middle::ty::{EarlyBinder, Opaque, PredicateKind::Trait}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span}; +use rustc_trait_selection::traits::error_reporting::suggestions::InferCtxtExt; +use rustc_trait_selection::traits::{self, FulfillmentError, TraitEngine}; + +declare_clippy_lint! { + /// ### What it does + /// This lint requires Future implementations returned from + /// functions and methods to implement the `Send` marker trait. It is mostly + /// used by library authors (public and internal) that target an audience where + /// multithreaded executors are likely to be used for running these Futures. + /// + /// ### Why is this bad? + /// A Future implementation captures some state that it + /// needs to eventually produce its final value. When targeting a multithreaded + /// executor (which is the norm on non-embedded devices) this means that this + /// state may need to be transported to other threads, in other words the + /// whole Future needs to implement the `Send` marker trait. If it does not, + /// then the resulting Future cannot be submitted to a thread pool in the + /// end user’s code. + /// + /// Especially for generic functions it can be confusing to leave the + /// discovery of this problem to the end user: the reported error location + /// will be far from its cause and can in many cases not even be fixed without + /// modifying the library where the offending Future implementation is + /// produced. + /// + /// ### Example + /// ```rust + /// async fn not_send(bytes: std::rc::Rc<[u8]>) {} + /// ``` + /// Use instead: + /// ```rust + /// async fn is_send(bytes: std::sync::Arc<[u8]>) {} + /// ``` + #[clippy::version = "1.44.0"] + pub FUTURE_NOT_SEND, + nursery, + "public Futures must be Send" +} + +declare_lint_pass!(FutureNotSend => [FUTURE_NOT_SEND]); + +impl<'tcx> LateLintPass<'tcx> for FutureNotSend { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'tcx>, + _: &'tcx Body<'tcx>, + _: Span, + hir_id: HirId, + ) { + if let FnKind::Closure = kind { + return; + } + let ret_ty = return_ty(cx, hir_id); + if let Opaque(id, subst) = *ret_ty.kind() { + let preds = cx.tcx.explicit_item_bounds(id); + let mut is_future = false; + for &(p, _span) in preds { + let p = EarlyBinder(p).subst(cx.tcx, subst); + if let Some(trait_pred) = p.to_opt_poly_trait_pred() { + if Some(trait_pred.skip_binder().trait_ref.def_id) == cx.tcx.lang_items().future_trait() { + is_future = true; + break; + } + } + } + if is_future { + let send_trait = cx.tcx.get_diagnostic_item(sym::Send).unwrap(); + let span = decl.output.span(); + let send_errors = cx.tcx.infer_ctxt().enter(|infcx| { + let cause = traits::ObligationCause::misc(span, hir_id); + let mut fulfillment_cx = traits::FulfillmentContext::new(); + fulfillment_cx.register_bound(&infcx, cx.param_env, ret_ty, send_trait, cause); + fulfillment_cx.select_all_or_error(&infcx) + }); + if !send_errors.is_empty() { + span_lint_and_then( + cx, + FUTURE_NOT_SEND, + span, + "future cannot be sent between threads safely", + |db| { + cx.tcx.infer_ctxt().enter(|infcx| { + for FulfillmentError { obligation, .. } in send_errors { + infcx.maybe_note_obligation_cause_for_async_await(db, &obligation); + if let Trait(trait_pred) = obligation.predicate.kind().skip_binder() { + db.note(&format!( + "`{}` doesn't implement `{}`", + trait_pred.self_ty(), + trait_pred.trait_ref.print_only_trait_path(), + )); + } + } + }); + }, + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/get_first.rs b/src/tools/clippy/clippy_lints/src/get_first.rs new file mode 100644 index 000000000..529f7baba --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/get_first.rs @@ -0,0 +1,68 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{is_slice_of_primitives, match_def_path, paths}; +use if_chain::if_chain; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; + +declare_clippy_lint! { + /// ### What it does + /// Checks for using `x.get(0)` instead of + /// `x.first()`. + /// + /// ### Why is this bad? + /// Using `x.first()` is easier to read and has the same + /// result. + /// + /// ### Example + /// ```rust + /// let x = vec![2, 3, 5]; + /// let first_element = x.get(0); + /// ``` + /// + /// Use instead: + /// ```rust + /// let x = vec![2, 3, 5]; + /// let first_element = x.first(); + /// ``` + #[clippy::version = "1.63.0"] + pub GET_FIRST, + style, + "Using `x.get(0)` when `x.first()` is simpler" +} +declare_lint_pass!(GetFirst => [GET_FIRST]); + +impl<'tcx> LateLintPass<'tcx> for GetFirst { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if_chain! { + if let hir::ExprKind::MethodCall(_, [struct_calling_on, method_arg], _) = &expr.kind; + if let Some(expr_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if match_def_path(cx, expr_def_id, &paths::SLICE_GET); + + if let Some(_) = is_slice_of_primitives(cx, struct_calling_on); + if let hir::ExprKind::Lit(Spanned { node: LitKind::Int(0, _), .. }) = method_arg.kind; + + then { + let mut applicability = Applicability::MachineApplicable; + let slice_name = snippet_with_applicability( + cx, + struct_calling_on.span, "..", + &mut applicability, + ); + span_lint_and_sugg( + cx, + GET_FIRST, + expr.span, + &format!("accessing first element with `{0}.get(0)`", slice_name), + "try", + format!("{}.first()", slice_name), + applicability, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/if_let_mutex.rs b/src/tools/clippy/clippy_lints/src/if_let_mutex.rs new file mode 100644 index 000000000..e95017007 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/if_let_mutex.rs @@ -0,0 +1,140 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::higher; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::SpanlessEq; +use if_chain::if_chain; +use rustc_hir::intravisit::{self as visit, Visitor}; +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 `Mutex::lock` calls in `if let` expression + /// with lock calls in any of the else blocks. + /// + /// ### Why is this bad? + /// The Mutex lock remains held for the whole + /// `if let ... else` block and deadlocks. + /// + /// ### Example + /// ```rust,ignore + /// if let Ok(thing) = mutex.lock() { + /// do_thing(); + /// } else { + /// mutex.lock(); + /// } + /// ``` + /// Should be written + /// ```rust,ignore + /// let locked = mutex.lock(); + /// if let Ok(thing) = locked { + /// do_thing(thing); + /// } else { + /// use_locked(locked); + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub IF_LET_MUTEX, + correctness, + "locking a `Mutex` in an `if let` block can cause deadlocks" +} + +declare_lint_pass!(IfLetMutex => [IF_LET_MUTEX]); + +impl<'tcx> LateLintPass<'tcx> for IfLetMutex { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + let mut arm_visit = ArmVisitor { + mutex_lock_called: false, + found_mutex: None, + cx, + }; + let mut op_visit = OppVisitor { + mutex_lock_called: false, + found_mutex: None, + cx, + }; + if let Some(higher::IfLet { + let_expr, + if_then, + if_else: Some(if_else), + .. + }) = higher::IfLet::hir(cx, expr) + { + op_visit.visit_expr(let_expr); + if op_visit.mutex_lock_called { + arm_visit.visit_expr(if_then); + arm_visit.visit_expr(if_else); + + if arm_visit.mutex_lock_called && arm_visit.same_mutex(cx, op_visit.found_mutex.unwrap()) { + span_lint_and_help( + cx, + IF_LET_MUTEX, + expr.span, + "calling `Mutex::lock` inside the scope of another `Mutex::lock` causes a deadlock", + None, + "move the lock call outside of the `if let ...` expression", + ); + } + } + } + } +} + +/// Checks if `Mutex::lock` is called in the `if let` expr. +pub struct OppVisitor<'a, 'tcx> { + mutex_lock_called: bool, + found_mutex: Option<&'tcx Expr<'tcx>>, + cx: &'a LateContext<'tcx>, +} + +impl<'tcx> Visitor<'tcx> for OppVisitor<'_, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if let Some(mutex) = is_mutex_lock_call(self.cx, expr) { + self.found_mutex = Some(mutex); + self.mutex_lock_called = true; + return; + } + visit::walk_expr(self, expr); + } +} + +/// Checks if `Mutex::lock` is called in any of the branches. +pub struct ArmVisitor<'a, 'tcx> { + mutex_lock_called: bool, + found_mutex: Option<&'tcx Expr<'tcx>>, + cx: &'a LateContext<'tcx>, +} + +impl<'tcx> Visitor<'tcx> for ArmVisitor<'_, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { + if let Some(mutex) = is_mutex_lock_call(self.cx, expr) { + self.found_mutex = Some(mutex); + self.mutex_lock_called = true; + return; + } + visit::walk_expr(self, expr); + } +} + +impl<'tcx, 'l> ArmVisitor<'tcx, 'l> { + fn same_mutex(&self, cx: &LateContext<'_>, op_mutex: &Expr<'_>) -> bool { + self.found_mutex + .map_or(false, |arm_mutex| SpanlessEq::new(cx).eq_expr(op_mutex, arm_mutex)) + } +} + +fn is_mutex_lock_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { + if_chain! { + if let ExprKind::MethodCall(path, [self_arg, ..], _) = &expr.kind; + if path.ident.as_str() == "lock"; + let ty = cx.typeck_results().expr_ty(self_arg); + if is_type_diagnostic_item(cx, ty, sym::Mutex); + then { + Some(self_arg) + } else { + None + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/if_not_else.rs b/src/tools/clippy/clippy_lints/src/if_not_else.rs new file mode 100644 index 000000000..3d59b7833 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/if_not_else.rs @@ -0,0 +1,90 @@ +//! lint on if branches that could be swapped so no `!` operation is necessary +//! on the condition + +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_else_clause; +use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `!` or `!=` in an if condition with an + /// else branch. + /// + /// ### Why is this bad? + /// Negations reduce the readability of statements. + /// + /// ### Example + /// ```rust + /// # let v: Vec<usize> = vec![]; + /// # fn a() {} + /// # fn b() {} + /// if !v.is_empty() { + /// a() + /// } else { + /// b() + /// } + /// ``` + /// + /// Could be written: + /// + /// ```rust + /// # let v: Vec<usize> = vec![]; + /// # fn a() {} + /// # fn b() {} + /// if v.is_empty() { + /// b() + /// } else { + /// a() + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub IF_NOT_ELSE, + pedantic, + "`if` branches that could be swapped so no negation operation is necessary on the condition" +} + +declare_lint_pass!(IfNotElse => [IF_NOT_ELSE]); + +impl LateLintPass<'_> for IfNotElse { + fn check_expr(&mut self, cx: &LateContext<'_>, item: &Expr<'_>) { + // While loops will be desugared to ExprKind::If. This will cause the lint to fire. + // To fix this, return early if this span comes from a macro or desugaring. + if item.span.from_expansion() { + return; + } + if let ExprKind::If(cond, _, Some(els)) = item.kind { + if let ExprKind::Block(..) = els.kind { + // Disable firing the lint in "else if" expressions. + if is_else_clause(cx.tcx, item) { + return; + } + + match cond.peel_drop_temps().kind { + ExprKind::Unary(UnOp::Not, _) => { + span_lint_and_help( + cx, + IF_NOT_ELSE, + item.span, + "unnecessary boolean `not` operation", + None, + "remove the `!` and swap the blocks of the `if`/`else`", + ); + }, + ExprKind::Binary(ref kind, _, _) if kind.node == BinOpKind::Ne => { + span_lint_and_help( + cx, + IF_NOT_ELSE, + item.span, + "unnecessary `!=` operation", + None, + "change to `==` and swap the blocks of the `if`/`else`", + ); + }, + _ => (), + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/if_then_some_else_none.rs b/src/tools/clippy/clippy_lints/src/if_then_some_else_none.rs new file mode 100644 index 000000000..b8d227855 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/if_then_some_else_none.rs @@ -0,0 +1,122 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet_with_macro_callsite; +use clippy_utils::{contains_return, higher, is_else_clause, is_lang_ctor, meets_msrv, msrvs, peel_blocks}; +use if_chain::if_chain; +use rustc_hir::LangItem::{OptionNone, OptionSome}; +use rustc_hir::{Expr, ExprKind, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for if-else that could be written to `bool::then`. + /// + /// ### Why is this bad? + /// Looks a little redundant. Using `bool::then` helps it have less lines of code. + /// + /// ### Example + /// ```rust + /// # let v = vec![0]; + /// let a = if v.is_empty() { + /// println!("true!"); + /// Some(42) + /// } else { + /// None + /// }; + /// ``` + /// + /// Could be written: + /// + /// ```rust + /// # let v = vec![0]; + /// let a = v.is_empty().then(|| { + /// println!("true!"); + /// 42 + /// }); + /// ``` + #[clippy::version = "1.53.0"] + pub IF_THEN_SOME_ELSE_NONE, + restriction, + "Finds if-else that could be written using `bool::then`" +} + +pub struct IfThenSomeElseNone { + msrv: Option<RustcVersion>, +} + +impl IfThenSomeElseNone { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(IfThenSomeElseNone => [IF_THEN_SOME_ELSE_NONE]); + +impl<'tcx> LateLintPass<'tcx> for IfThenSomeElseNone { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'tcx Expr<'_>) { + if !meets_msrv(self.msrv, msrvs::BOOL_THEN) { + return; + } + + if in_external_macro(cx.sess(), expr.span) { + return; + } + + // We only care about the top-most `if` in the chain + if is_else_clause(cx.tcx, expr) { + return; + } + + if_chain! { + if let Some(higher::If { cond, then, r#else: Some(els) }) = higher::If::hir(expr); + if let ExprKind::Block(then_block, _) = then.kind; + if let Some(then_expr) = then_block.expr; + if let ExprKind::Call(then_call, [then_arg]) = then_expr.kind; + if let ExprKind::Path(ref then_call_qpath) = then_call.kind; + if is_lang_ctor(cx, then_call_qpath, OptionSome); + if let ExprKind::Path(ref qpath) = peel_blocks(els).kind; + if is_lang_ctor(cx, qpath, OptionNone); + if !stmts_contains_early_return(then_block.stmts); + then { + let cond_snip = snippet_with_macro_callsite(cx, cond.span, "[condition]"); + let cond_snip = if matches!(cond.kind, ExprKind::Unary(_, _) | ExprKind::Binary(_, _, _)) { + format!("({})", cond_snip) + } else { + cond_snip.into_owned() + }; + let arg_snip = snippet_with_macro_callsite(cx, then_arg.span, ""); + let closure_body = if then_block.stmts.is_empty() { + arg_snip.into_owned() + } else { + format!("{{ /* snippet */ {} }}", arg_snip) + }; + let help = format!( + "consider using `bool::then` like: `{}.then(|| {})`", + cond_snip, + closure_body, + ); + span_lint_and_help( + cx, + IF_THEN_SOME_ELSE_NONE, + expr.span, + "this could be simplified with `bool::then`", + None, + &help, + ); + } + } + } + + extract_msrv_attr!(LateContext); +} + +fn stmts_contains_early_return(stmts: &[Stmt<'_>]) -> bool { + stmts.iter().any(|stmt| { + let Stmt { kind: StmtKind::Semi(e), .. } = stmt else { return false }; + + contains_return(e) + }) +} diff --git a/src/tools/clippy/clippy_lints/src/implicit_hasher.rs b/src/tools/clippy/clippy_lints/src/implicit_hasher.rs new file mode 100644 index 000000000..4f9680f60 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/implicit_hasher.rs @@ -0,0 +1,388 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; + +use rustc_errors::Diagnostic; +use rustc_hir as hir; +use rustc_hir::intravisit::{walk_body, walk_expr, walk_inf, walk_ty, Visitor}; +use rustc_hir::{Body, Expr, ExprKind, GenericArg, Item, ItemKind, QPath, TyKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::hir::nested_filter; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::{Ty, TypeckResults}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::symbol::sym; +use rustc_typeck::hir_ty_to_ty; + +use if_chain::if_chain; + +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::source::{snippet, snippet_opt}; +use clippy_utils::ty::is_type_diagnostic_item; + +declare_clippy_lint! { + /// ### What it does + /// Checks for public `impl` or `fn` missing generalization + /// over different hashers and implicitly defaulting to the default hashing + /// algorithm (`SipHash`). + /// + /// ### Why is this bad? + /// `HashMap` or `HashSet` with custom hashers cannot be + /// used with them. + /// + /// ### Known problems + /// Suggestions for replacing constructors can contain + /// false-positives. Also applying suggestions can require modification of other + /// pieces of code, possibly including external crates. + /// + /// ### Example + /// ```rust + /// # use std::collections::HashMap; + /// # use std::hash::{Hash, BuildHasher}; + /// # trait Serialize {}; + /// impl<K: Hash + Eq, V> Serialize for HashMap<K, V> { } + /// + /// pub fn foo(map: &mut HashMap<i32, i32>) { } + /// ``` + /// could be rewritten as + /// ```rust + /// # use std::collections::HashMap; + /// # use std::hash::{Hash, BuildHasher}; + /// # trait Serialize {}; + /// impl<K: Hash + Eq, V, S: BuildHasher> Serialize for HashMap<K, V, S> { } + /// + /// pub fn foo<S: BuildHasher>(map: &mut HashMap<i32, i32, S>) { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub IMPLICIT_HASHER, + pedantic, + "missing generalization over different hashers" +} + +declare_lint_pass!(ImplicitHasher => [IMPLICIT_HASHER]); + +impl<'tcx> LateLintPass<'tcx> for ImplicitHasher { + #[expect(clippy::cast_possible_truncation, clippy::too_many_lines)] + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + use rustc_span::BytePos; + + fn suggestion<'tcx>( + cx: &LateContext<'tcx>, + diag: &mut Diagnostic, + generics_span: Span, + generics_suggestion_span: Span, + target: &ImplicitHasherType<'_>, + vis: ImplicitHasherConstructorVisitor<'_, '_, '_>, + ) { + let generics_snip = snippet(cx, generics_span, ""); + // trim `<` `>` + let generics_snip = if generics_snip.is_empty() { + "" + } else { + &generics_snip[1..generics_snip.len() - 1] + }; + + multispan_sugg( + diag, + "consider adding a type parameter", + vec![ + ( + generics_suggestion_span, + format!( + "<{}{}S: ::std::hash::BuildHasher{}>", + generics_snip, + if generics_snip.is_empty() { "" } else { ", " }, + if vis.suggestions.is_empty() { + "" + } else { + // request users to add `Default` bound so that generic constructors can be used + " + Default" + }, + ), + ), + ( + target.span(), + format!("{}<{}, S>", target.type_name(), target.type_arguments(),), + ), + ], + ); + + if !vis.suggestions.is_empty() { + multispan_sugg(diag, "...and use generic constructor", vis.suggestions); + } + } + + if !cx.access_levels.is_exported(item.def_id) { + return; + } + + match item.kind { + ItemKind::Impl(impl_) => { + let mut vis = ImplicitHasherTypeVisitor::new(cx); + vis.visit_ty(impl_.self_ty); + + for target in &vis.found { + if item.span.ctxt() != target.span().ctxt() { + return; + } + + let generics_suggestion_span = impl_.generics.span.substitute_dummy({ + let pos = snippet_opt(cx, item.span.until(target.span())) + .and_then(|snip| Some(item.span.lo() + BytePos(snip.find("impl")? as u32 + 4))); + if let Some(pos) = pos { + Span::new(pos, pos, item.span.ctxt(), item.span.parent()) + } else { + return; + } + }); + + let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target); + for item in impl_.items.iter().map(|item| cx.tcx.hir().impl_item(item.id)) { + ctr_vis.visit_impl_item(item); + } + + span_lint_and_then( + cx, + IMPLICIT_HASHER, + target.span(), + &format!( + "impl for `{}` should be generalized over different hashers", + target.type_name() + ), + move |diag| { + suggestion(cx, diag, impl_.generics.span, generics_suggestion_span, target, ctr_vis); + }, + ); + } + }, + ItemKind::Fn(ref sig, generics, body_id) => { + let body = cx.tcx.hir().body(body_id); + + for ty in sig.decl.inputs { + let mut vis = ImplicitHasherTypeVisitor::new(cx); + vis.visit_ty(ty); + + for target in &vis.found { + if in_external_macro(cx.sess(), generics.span) { + continue; + } + let generics_suggestion_span = generics.span.substitute_dummy({ + let pos = snippet_opt( + cx, + Span::new( + item.span.lo(), + body.params[0].pat.span.lo(), + item.span.ctxt(), + item.span.parent(), + ), + ) + .and_then(|snip| { + let i = snip.find("fn")?; + Some(item.span.lo() + BytePos((i + snip[i..].find('(')?) as u32)) + }) + .expect("failed to create span for type parameters"); + Span::new(pos, pos, item.span.ctxt(), item.span.parent()) + }); + + let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target); + ctr_vis.visit_body(body); + + span_lint_and_then( + cx, + IMPLICIT_HASHER, + target.span(), + &format!( + "parameter of type `{}` should be generalized over different hashers", + target.type_name() + ), + move |diag| { + suggestion(cx, diag, generics.span, generics_suggestion_span, target, ctr_vis); + }, + ); + } + } + }, + _ => {}, + } + } +} + +enum ImplicitHasherType<'tcx> { + HashMap(Span, Ty<'tcx>, Cow<'static, str>, Cow<'static, str>), + HashSet(Span, Ty<'tcx>, Cow<'static, str>), +} + +impl<'tcx> ImplicitHasherType<'tcx> { + /// Checks that `ty` is a target type without a `BuildHasher`. + fn new(cx: &LateContext<'tcx>, hir_ty: &hir::Ty<'_>) -> Option<Self> { + if let TyKind::Path(QPath::Resolved(None, path)) = hir_ty.kind { + let params: Vec<_> = path + .segments + .last() + .as_ref()? + .args + .as_ref()? + .args + .iter() + .filter_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }) + .collect(); + let params_len = params.len(); + + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + + if is_type_diagnostic_item(cx, ty, sym::HashMap) && params_len == 2 { + Some(ImplicitHasherType::HashMap( + hir_ty.span, + ty, + snippet(cx, params[0].span, "K"), + snippet(cx, params[1].span, "V"), + )) + } else if is_type_diagnostic_item(cx, ty, sym::HashSet) && params_len == 1 { + Some(ImplicitHasherType::HashSet( + hir_ty.span, + ty, + snippet(cx, params[0].span, "T"), + )) + } else { + None + } + } else { + None + } + } + + fn type_name(&self) -> &'static str { + match *self { + ImplicitHasherType::HashMap(..) => "HashMap", + ImplicitHasherType::HashSet(..) => "HashSet", + } + } + + fn type_arguments(&self) -> String { + match *self { + ImplicitHasherType::HashMap(.., ref k, ref v) => format!("{}, {}", k, v), + ImplicitHasherType::HashSet(.., ref t) => format!("{}", t), + } + } + + fn ty(&self) -> Ty<'tcx> { + match *self { + ImplicitHasherType::HashMap(_, ty, ..) | ImplicitHasherType::HashSet(_, ty, ..) => ty, + } + } + + fn span(&self) -> Span { + match *self { + ImplicitHasherType::HashMap(span, ..) | ImplicitHasherType::HashSet(span, ..) => span, + } + } +} + +struct ImplicitHasherTypeVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + found: Vec<ImplicitHasherType<'tcx>>, +} + +impl<'a, 'tcx> ImplicitHasherTypeVisitor<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> Self { + Self { cx, found: vec![] } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for ImplicitHasherTypeVisitor<'a, 'tcx> { + fn visit_ty(&mut self, t: &'tcx hir::Ty<'_>) { + if let Some(target) = ImplicitHasherType::new(self.cx, t) { + self.found.push(target); + } + + walk_ty(self, t); + } + + fn visit_infer(&mut self, inf: &'tcx hir::InferArg) { + if let Some(target) = ImplicitHasherType::new(self.cx, &inf.to_ty()) { + self.found.push(target); + } + + walk_inf(self, inf); + } +} + +/// Looks for default-hasher-dependent constructors like `HashMap::new`. +struct ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> { + cx: &'a LateContext<'tcx>, + maybe_typeck_results: Option<&'tcx TypeckResults<'tcx>>, + target: &'b ImplicitHasherType<'tcx>, + suggestions: BTreeMap<Span, String>, +} + +impl<'a, 'b, 'tcx> ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> { + fn new(cx: &'a LateContext<'tcx>, target: &'b ImplicitHasherType<'tcx>) -> Self { + Self { + cx, + maybe_typeck_results: cx.maybe_typeck_results(), + target, + suggestions: BTreeMap::new(), + } + } +} + +impl<'a, 'b, 'tcx> Visitor<'tcx> for ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_body(&mut self, body: &'tcx Body<'_>) { + let old_maybe_typeck_results = self.maybe_typeck_results.replace(self.cx.tcx.typeck_body(body.id())); + walk_body(self, body); + self.maybe_typeck_results = old_maybe_typeck_results; + } + + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::Call(fun, args) = e.kind; + if let ExprKind::Path(QPath::TypeRelative(ty, method)) = fun.kind; + if let TyKind::Path(QPath::Resolved(None, ty_path)) = ty.kind; + if let Some(ty_did) = ty_path.res.opt_def_id(); + then { + if self.target.ty() != self.maybe_typeck_results.unwrap().expr_ty(e) { + return; + } + + if self.cx.tcx.is_diagnostic_item(sym::HashMap, ty_did) { + if method.ident.name == sym::new { + self.suggestions + .insert(e.span, "HashMap::default()".to_string()); + } else if method.ident.name == sym!(with_capacity) { + self.suggestions.insert( + e.span, + format!( + "HashMap::with_capacity_and_hasher({}, Default::default())", + snippet(self.cx, args[0].span, "capacity"), + ), + ); + } + } else if self.cx.tcx.is_diagnostic_item(sym::HashSet, ty_did) { + if method.ident.name == sym::new { + self.suggestions + .insert(e.span, "HashSet::default()".to_string()); + } else if method.ident.name == sym!(with_capacity) { + self.suggestions.insert( + e.span, + format!( + "HashSet::with_capacity_and_hasher({}, Default::default())", + snippet(self.cx, args[0].span, "capacity"), + ), + ); + } + } + } + } + + walk_expr(self, e); + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} diff --git a/src/tools/clippy/clippy_lints/src/implicit_return.rs b/src/tools/clippy/clippy_lints/src/implicit_return.rs new file mode 100644 index 000000000..a6610ade3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/implicit_return.rs @@ -0,0 +1,250 @@ +use clippy_utils::{ + diagnostics::span_lint_hir_and_then, + get_async_fn_body, is_async_fn, + source::{snippet_with_applicability, snippet_with_context, walk_span_to_context}, + visitors::expr_visitor_no_bodies, +}; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{FnKind, Visitor}; +use rustc_hir::{Block, Body, Expr, ExprKind, FnDecl, FnRetTy, HirId}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{Span, SyntaxContext}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for missing return statements at the end of a block. + /// + /// ### Why is this bad? + /// Actually omitting the return keyword is idiomatic Rust code. Programmers + /// coming from other languages might prefer the expressiveness of `return`. It's possible to miss + /// the last returning statement because the only difference is a missing `;`. Especially in bigger + /// code with multiple return paths having a `return` keyword makes it easier to find the + /// corresponding statements. + /// + /// ### Example + /// ```rust + /// fn foo(x: usize) -> usize { + /// x + /// } + /// ``` + /// add return + /// ```rust + /// fn foo(x: usize) -> usize { + /// return x; + /// } + /// ``` + #[clippy::version = "1.33.0"] + pub IMPLICIT_RETURN, + restriction, + "use a return statement like `return expr` instead of an expression" +} + +declare_lint_pass!(ImplicitReturn => [IMPLICIT_RETURN]); + +fn lint_return(cx: &LateContext<'_>, emission_place: HirId, span: Span) { + let mut app = Applicability::MachineApplicable; + let snip = snippet_with_applicability(cx, span, "..", &mut app); + span_lint_hir_and_then( + cx, + IMPLICIT_RETURN, + emission_place, + span, + "missing `return` statement", + |diag| { + diag.span_suggestion(span, "add `return` as shown", format!("return {}", snip), app); + }, + ); +} + +fn lint_break(cx: &LateContext<'_>, emission_place: HirId, break_span: Span, expr_span: Span) { + let mut app = Applicability::MachineApplicable; + let snip = snippet_with_context(cx, expr_span, break_span.ctxt(), "..", &mut app).0; + span_lint_hir_and_then( + cx, + IMPLICIT_RETURN, + emission_place, + break_span, + "missing `return` statement", + |diag| { + diag.span_suggestion( + break_span, + "change `break` to `return` as shown", + format!("return {}", snip), + app, + ); + }, + ); +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum LintLocation { + /// The lint was applied to a parent expression. + Parent, + /// The lint was applied to this expression, a child, or not applied. + Inner, +} +impl LintLocation { + fn still_parent(self, b: bool) -> Self { + if b { self } else { Self::Inner } + } + + fn is_parent(self) -> bool { + self == Self::Parent + } +} + +// Gets the call site if the span is in a child context. Otherwise returns `None`. +fn get_call_site(span: Span, ctxt: SyntaxContext) -> Option<Span> { + (span.ctxt() != ctxt).then(|| walk_span_to_context(span, ctxt).unwrap_or(span)) +} + +fn lint_implicit_returns( + cx: &LateContext<'_>, + expr: &Expr<'_>, + // The context of the function body. + ctxt: SyntaxContext, + // Whether the expression is from a macro expansion. + call_site_span: Option<Span>, +) -> LintLocation { + match expr.kind { + ExprKind::Block( + Block { + expr: Some(block_expr), .. + }, + _, + ) => lint_implicit_returns( + cx, + block_expr, + ctxt, + call_site_span.or_else(|| get_call_site(block_expr.span, ctxt)), + ) + .still_parent(call_site_span.is_some()), + + ExprKind::If(_, then_expr, Some(else_expr)) => { + // Both `then_expr` or `else_expr` are required to be blocks in the same context as the `if`. Don't + // bother checking. + let res = lint_implicit_returns(cx, then_expr, ctxt, call_site_span).still_parent(call_site_span.is_some()); + if res.is_parent() { + // The return was added as a parent of this if expression. + return res; + } + lint_implicit_returns(cx, else_expr, ctxt, call_site_span).still_parent(call_site_span.is_some()) + }, + + ExprKind::Match(_, arms, _) => { + for arm in arms { + let res = lint_implicit_returns( + cx, + arm.body, + ctxt, + call_site_span.or_else(|| get_call_site(arm.body.span, ctxt)), + ) + .still_parent(call_site_span.is_some()); + if res.is_parent() { + // The return was added as a parent of this match expression. + return res; + } + } + LintLocation::Inner + }, + + ExprKind::Loop(block, ..) => { + let mut add_return = false; + expr_visitor_no_bodies(|e| { + if let ExprKind::Break(dest, sub_expr) = e.kind { + if dest.target_id.ok() == Some(expr.hir_id) { + if call_site_span.is_none() && e.span.ctxt() == ctxt { + // At this point sub_expr can be `None` in async functions which either diverge, or return + // the unit type. + if let Some(sub_expr) = sub_expr { + lint_break(cx, e.hir_id, e.span, sub_expr.span); + } + } else { + // the break expression is from a macro call, add a return to the loop + add_return = true; + } + } + } + true + }) + .visit_block(block); + if add_return { + #[expect(clippy::option_if_let_else)] + if let Some(span) = call_site_span { + lint_return(cx, expr.hir_id, span); + LintLocation::Parent + } else { + lint_return(cx, expr.hir_id, expr.span); + LintLocation::Inner + } + } else { + LintLocation::Inner + } + }, + + // If expressions without an else clause, and blocks without a final expression can only be the final expression + // if they are divergent, or return the unit type. + ExprKind::If(_, _, None) | ExprKind::Block(Block { expr: None, .. }, _) | ExprKind::Ret(_) => { + LintLocation::Inner + }, + + // Any divergent expression doesn't need a return statement. + ExprKind::MethodCall(..) + | ExprKind::Call(..) + | ExprKind::Binary(..) + | ExprKind::Unary(..) + | ExprKind::Index(..) + if cx.typeck_results().expr_ty(expr).is_never() => + { + LintLocation::Inner + }, + + _ => + { + #[expect(clippy::option_if_let_else)] + if let Some(span) = call_site_span { + lint_return(cx, expr.hir_id, span); + LintLocation::Parent + } else { + lint_return(cx, expr.hir_id, expr.span); + LintLocation::Inner + } + }, + } +} + +impl<'tcx> LateLintPass<'tcx> for ImplicitReturn { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + span: Span, + _: HirId, + ) { + if (!matches!(kind, FnKind::Closure) && matches!(decl.output, FnRetTy::DefaultReturn(_))) + || span.ctxt() != body.value.span.ctxt() + || in_external_macro(cx.sess(), span) + { + return; + } + + let res_ty = cx.typeck_results().expr_ty(&body.value); + if res_ty.is_unit() || res_ty.is_never() { + return; + } + + let expr = if is_async_fn(kind) { + match get_async_fn_body(cx.tcx, body) { + Some(e) => e, + None => return, + } + } else { + &body.value + }; + lint_implicit_returns(cx, expr, expr.span.ctxt(), None); + } +} diff --git a/src/tools/clippy/clippy_lints/src/implicit_saturating_sub.rs b/src/tools/clippy/clippy_lints/src/implicit_saturating_sub.rs new file mode 100644 index 000000000..46654bc61 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/implicit_saturating_sub.rs @@ -0,0 +1,176 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::{higher, peel_blocks_with_stmt, SpanlessEq}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for implicit saturating subtraction. + /// + /// ### Why is this bad? + /// Simplicity and readability. Instead we can easily use an builtin function. + /// + /// ### Example + /// ```rust + /// # let end: u32 = 10; + /// # let start: u32 = 5; + /// let mut i: u32 = end - start; + /// + /// if i != 0 { + /// i -= 1; + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let end: u32 = 10; + /// # let start: u32 = 5; + /// let mut i: u32 = end - start; + /// + /// i = i.saturating_sub(1); + /// ``` + #[clippy::version = "1.44.0"] + pub IMPLICIT_SATURATING_SUB, + pedantic, + "Perform saturating subtraction instead of implicitly checking lower bound of data type" +} + +declare_lint_pass!(ImplicitSaturatingSub => [IMPLICIT_SATURATING_SUB]); + +impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if expr.span.from_expansion() { + return; + } + if_chain! { + if let Some(higher::If { cond, then, r#else: None }) = higher::If::hir(expr); + + // Check if the conditional expression is a binary operation + if let ExprKind::Binary(ref cond_op, cond_left, cond_right) = cond.kind; + + // Ensure that the binary operator is >, !=, or < + if BinOpKind::Ne == cond_op.node || BinOpKind::Gt == cond_op.node || BinOpKind::Lt == cond_op.node; + + // Check if assign operation is done + if let Some(target) = subtracts_one(cx, then); + + // Extracting out the variable name + if let ExprKind::Path(QPath::Resolved(_, ares_path)) = target.kind; + + then { + // Handle symmetric conditions in the if statement + let (cond_var, cond_num_val) = if SpanlessEq::new(cx).eq_expr(cond_left, target) { + if BinOpKind::Gt == cond_op.node || BinOpKind::Ne == cond_op.node { + (cond_left, cond_right) + } else { + return; + } + } else if SpanlessEq::new(cx).eq_expr(cond_right, target) { + if BinOpKind::Lt == cond_op.node || BinOpKind::Ne == cond_op.node { + (cond_right, cond_left) + } else { + return; + } + } else { + return; + }; + + // Check if the variable in the condition statement is an integer + if !cx.typeck_results().expr_ty(cond_var).is_integral() { + return; + } + + // Get the variable name + let var_name = ares_path.segments[0].ident.name.as_str(); + match cond_num_val.kind { + ExprKind::Lit(ref cond_lit) => { + // Check if the constant is zero + if let LitKind::Int(0, _) = cond_lit.node { + if cx.typeck_results().expr_ty(cond_left).is_signed() { + } else { + print_lint_and_sugg(cx, var_name, expr); + }; + } + }, + ExprKind::Path(QPath::TypeRelative(_, name)) => { + if_chain! { + if name.ident.as_str() == "MIN"; + if let Some(const_id) = cx.typeck_results().type_dependent_def_id(cond_num_val.hir_id); + if let Some(impl_id) = cx.tcx.impl_of_method(const_id); + if let None = cx.tcx.impl_trait_ref(impl_id); // An inherent impl + if cx.tcx.type_of(impl_id).is_integral(); + then { + print_lint_and_sugg(cx, var_name, expr) + } + } + }, + ExprKind::Call(func, []) => { + if_chain! { + if let ExprKind::Path(QPath::TypeRelative(_, name)) = func.kind; + if name.ident.as_str() == "min_value"; + if let Some(func_id) = cx.typeck_results().type_dependent_def_id(func.hir_id); + if let Some(impl_id) = cx.tcx.impl_of_method(func_id); + if let None = cx.tcx.impl_trait_ref(impl_id); // An inherent impl + if cx.tcx.type_of(impl_id).is_integral(); + then { + print_lint_and_sugg(cx, var_name, expr) + } + } + }, + _ => (), + } + } + } + } +} + +fn subtracts_one<'a>(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<&'a Expr<'a>> { + match peel_blocks_with_stmt(expr).kind { + ExprKind::AssignOp(ref op1, target, value) => { + if_chain! { + if BinOpKind::Sub == op1.node; + // Check if literal being subtracted is one + if let ExprKind::Lit(ref lit1) = value.kind; + if let LitKind::Int(1, _) = lit1.node; + then { + Some(target) + } else { + None + } + } + }, + ExprKind::Assign(target, value, _) => { + if_chain! { + if let ExprKind::Binary(ref op1, left1, right1) = value.kind; + if BinOpKind::Sub == op1.node; + + if SpanlessEq::new(cx).eq_expr(left1, target); + + if let ExprKind::Lit(ref lit1) = right1.kind; + if let LitKind::Int(1, _) = lit1.node; + then { + Some(target) + } else { + None + } + } + }, + _ => None, + } +} + +fn print_lint_and_sugg(cx: &LateContext<'_>, var_name: &str, expr: &Expr<'_>) { + span_lint_and_sugg( + cx, + IMPLICIT_SATURATING_SUB, + expr.span, + "implicitly performing saturating subtraction", + "try", + format!("{} = {}.saturating_sub({});", var_name, var_name, '1'), + Applicability::MachineApplicable, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/inconsistent_struct_constructor.rs b/src/tools/clippy/clippy_lints/src/inconsistent_struct_constructor.rs new file mode 100644 index 000000000..14b22d2b5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/inconsistent_struct_constructor.rs @@ -0,0 +1,136 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use if_chain::if_chain; +use rustc_data_structures::fx::FxHashMap; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::Symbol; +use std::fmt::Write as _; + +declare_clippy_lint! { + /// ### What it does + /// Checks for struct constructors where all fields are shorthand and + /// the order of the field init shorthand in the constructor is inconsistent + /// with the order in the struct definition. + /// + /// ### Why is this bad? + /// Since the order of fields in a constructor doesn't affect the + /// resulted instance as the below example indicates, + /// + /// ```rust + /// #[derive(Debug, PartialEq, Eq)] + /// struct Foo { + /// x: i32, + /// y: i32, + /// } + /// let x = 1; + /// let y = 2; + /// + /// // This assertion never fails: + /// assert_eq!(Foo { x, y }, Foo { y, x }); + /// ``` + /// + /// inconsistent order can be confusing and decreases readability and consistency. + /// + /// ### Example + /// ```rust + /// struct Foo { + /// x: i32, + /// y: i32, + /// } + /// let x = 1; + /// let y = 2; + /// + /// Foo { y, x }; + /// ``` + /// + /// Use instead: + /// ```rust + /// # struct Foo { + /// # x: i32, + /// # y: i32, + /// # } + /// # let x = 1; + /// # let y = 2; + /// Foo { x, y }; + /// ``` + #[clippy::version = "1.52.0"] + pub INCONSISTENT_STRUCT_CONSTRUCTOR, + pedantic, + "the order of the field init shorthand is inconsistent with the order in the struct definition" +} + +declare_lint_pass!(InconsistentStructConstructor => [INCONSISTENT_STRUCT_CONSTRUCTOR]); + +impl<'tcx> LateLintPass<'tcx> for InconsistentStructConstructor { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if_chain! { + if !expr.span.from_expansion(); + if let ExprKind::Struct(qpath, fields, base) = expr.kind; + let ty = cx.typeck_results().expr_ty(expr); + if let Some(adt_def) = ty.ty_adt_def(); + if adt_def.is_struct(); + if let Some(variant) = adt_def.variants().iter().next(); + if fields.iter().all(|f| f.is_shorthand); + then { + let mut def_order_map = FxHashMap::default(); + for (idx, field) in variant.fields.iter().enumerate() { + def_order_map.insert(field.name, idx); + } + + if is_consistent_order(fields, &def_order_map) { + return; + } + + let mut ordered_fields: Vec<_> = fields.iter().map(|f| f.ident.name).collect(); + ordered_fields.sort_unstable_by_key(|id| def_order_map[id]); + + let mut fields_snippet = String::new(); + let (last_ident, idents) = ordered_fields.split_last().unwrap(); + for ident in idents { + let _ = write!(fields_snippet, "{}, ", ident); + } + fields_snippet.push_str(&last_ident.to_string()); + + let base_snippet = if let Some(base) = base { + format!(", ..{}", snippet(cx, base.span, "..")) + } else { + String::new() + }; + + let sugg = format!("{} {{ {}{} }}", + snippet(cx, qpath.span(), ".."), + fields_snippet, + base_snippet, + ); + + span_lint_and_sugg( + cx, + INCONSISTENT_STRUCT_CONSTRUCTOR, + expr.span, + "struct constructor field order is inconsistent with struct definition field order", + "try", + sugg, + Applicability::MachineApplicable, + ) + } + } + } +} + +// Check whether the order of the fields in the constructor is consistent with the order in the +// definition. +fn is_consistent_order<'tcx>(fields: &'tcx [hir::ExprField<'tcx>], def_order_map: &FxHashMap<Symbol, usize>) -> bool { + let mut cur_idx = usize::MIN; + for f in fields { + let next_idx = def_order_map[&f.ident.name]; + if cur_idx > next_idx { + return false; + } + cur_idx = next_idx; + } + + true +} diff --git a/src/tools/clippy/clippy_lints/src/index_refutable_slice.rs b/src/tools/clippy/clippy_lints/src/index_refutable_slice.rs new file mode 100644 index 000000000..d0c6495e3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/index_refutable_slice.rs @@ -0,0 +1,275 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::higher::IfLet; +use clippy_utils::ty::is_copy; +use clippy_utils::{is_expn_of, is_lint_allowed, meets_msrv, msrvs, path_to_local}; +use if_chain::if_chain; +use rustc_data_structures::fx::{FxHashSet, FxIndexMap}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::intravisit::{self, Visitor}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter; +use rustc_middle::ty; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{symbol::Ident, Span}; + +declare_clippy_lint! { + /// ### What it does + /// The lint checks for slice bindings in patterns that are only used to + /// access individual slice values. + /// + /// ### Why is this bad? + /// Accessing slice values using indices can lead to panics. Using refutable + /// patterns can avoid these. Binding to individual values also improves the + /// readability as they can be named. + /// + /// ### Limitations + /// This lint currently only checks for immutable access inside `if let` + /// patterns. + /// + /// ### Example + /// ```rust + /// let slice: Option<&[u32]> = Some(&[1, 2, 3]); + /// + /// if let Some(slice) = slice { + /// println!("{}", slice[0]); + /// } + /// ``` + /// Use instead: + /// ```rust + /// let slice: Option<&[u32]> = Some(&[1, 2, 3]); + /// + /// if let Some(&[first, ..]) = slice { + /// println!("{}", first); + /// } + /// ``` + #[clippy::version = "1.59.0"] + pub INDEX_REFUTABLE_SLICE, + nursery, + "avoid indexing on slices which could be destructed" +} + +#[derive(Copy, Clone)] +pub struct IndexRefutableSlice { + max_suggested_slice: u64, + msrv: Option<RustcVersion>, +} + +impl IndexRefutableSlice { + pub fn new(max_suggested_slice_pattern_length: u64, msrv: Option<RustcVersion>) -> Self { + Self { + max_suggested_slice: max_suggested_slice_pattern_length, + msrv, + } + } +} + +impl_lint_pass!(IndexRefutableSlice => [INDEX_REFUTABLE_SLICE]); + +impl<'tcx> LateLintPass<'tcx> for IndexRefutableSlice { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if_chain! { + if !expr.span.from_expansion() || is_expn_of(expr.span, "if_chain").is_some(); + if let Some(IfLet {let_pat, if_then, ..}) = IfLet::hir(cx, expr); + if !is_lint_allowed(cx, INDEX_REFUTABLE_SLICE, expr.hir_id); + if meets_msrv(self.msrv, msrvs::SLICE_PATTERNS); + + let found_slices = find_slice_values(cx, let_pat); + if !found_slices.is_empty(); + let filtered_slices = filter_lintable_slices(cx, found_slices, self.max_suggested_slice, if_then); + if !filtered_slices.is_empty(); + then { + for slice in filtered_slices.values() { + lint_slice(cx, slice); + } + } + } + } + + extract_msrv_attr!(LateContext); +} + +fn find_slice_values(cx: &LateContext<'_>, pat: &hir::Pat<'_>) -> FxIndexMap<hir::HirId, SliceLintInformation> { + let mut removed_pat: FxHashSet<hir::HirId> = FxHashSet::default(); + let mut slices: FxIndexMap<hir::HirId, SliceLintInformation> = FxIndexMap::default(); + pat.walk_always(|pat| { + if let hir::PatKind::Binding(binding, value_hir_id, ident, sub_pat) = pat.kind { + // We'll just ignore mut and ref mut for simplicity sake right now + if let hir::BindingAnnotation::Mutable | hir::BindingAnnotation::RefMut = binding { + return; + } + + // This block catches bindings with sub patterns. It would be hard to build a correct suggestion + // for them and it's likely that the user knows what they are doing in such a case. + if removed_pat.contains(&value_hir_id) { + return; + } + if sub_pat.is_some() { + removed_pat.insert(value_hir_id); + slices.remove(&value_hir_id); + return; + } + + let bound_ty = cx.typeck_results().node_type(pat.hir_id); + if let ty::Slice(inner_ty) | ty::Array(inner_ty, _) = bound_ty.peel_refs().kind() { + // The values need to use the `ref` keyword if they can't be copied. + // This will need to be adjusted if the lint want to support mutable access in the future + let src_is_ref = bound_ty.is_ref() && binding != hir::BindingAnnotation::Ref; + let needs_ref = !(src_is_ref || is_copy(cx, *inner_ty)); + + let slice_info = slices + .entry(value_hir_id) + .or_insert_with(|| SliceLintInformation::new(ident, needs_ref)); + slice_info.pattern_spans.push(pat.span); + } + } + }); + + slices +} + +fn lint_slice(cx: &LateContext<'_>, slice: &SliceLintInformation) { + let used_indices = slice + .index_use + .iter() + .map(|(index, _)| *index) + .collect::<FxHashSet<_>>(); + + let value_name = |index| format!("{}_{}", slice.ident.name, index); + + if let Some(max_index) = used_indices.iter().max() { + let opt_ref = if slice.needs_ref { "ref " } else { "" }; + let pat_sugg_idents = (0..=*max_index) + .map(|index| { + if used_indices.contains(&index) { + format!("{}{}", opt_ref, value_name(index)) + } else { + "_".to_string() + } + }) + .collect::<Vec<_>>(); + let pat_sugg = format!("[{}, ..]", pat_sugg_idents.join(", ")); + + span_lint_and_then( + cx, + INDEX_REFUTABLE_SLICE, + slice.ident.span, + "this binding can be a slice pattern to avoid indexing", + |diag| { + diag.multipart_suggestion( + "try using a slice pattern here", + slice + .pattern_spans + .iter() + .map(|span| (*span, pat_sugg.clone())) + .collect(), + Applicability::MaybeIncorrect, + ); + + diag.multipart_suggestion( + "and replace the index expressions here", + slice + .index_use + .iter() + .map(|(index, span)| (*span, value_name(*index))) + .collect(), + Applicability::MaybeIncorrect, + ); + + // The lint message doesn't contain a warning about the removed index expression, + // since `filter_lintable_slices` will only return slices where all access indices + // are known at compile time. Therefore, they can be removed without side effects. + }, + ); + } +} + +#[derive(Debug)] +struct SliceLintInformation { + ident: Ident, + needs_ref: bool, + pattern_spans: Vec<Span>, + index_use: Vec<(u64, Span)>, +} + +impl SliceLintInformation { + fn new(ident: Ident, needs_ref: bool) -> Self { + Self { + ident, + needs_ref, + pattern_spans: Vec::new(), + index_use: Vec::new(), + } + } +} + +fn filter_lintable_slices<'a, 'tcx>( + cx: &'a LateContext<'tcx>, + slice_lint_info: FxIndexMap<hir::HirId, SliceLintInformation>, + max_suggested_slice: u64, + scope: &'tcx hir::Expr<'tcx>, +) -> FxIndexMap<hir::HirId, SliceLintInformation> { + let mut visitor = SliceIndexLintingVisitor { + cx, + slice_lint_info, + max_suggested_slice, + }; + + intravisit::walk_expr(&mut visitor, scope); + + visitor.slice_lint_info +} + +struct SliceIndexLintingVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + slice_lint_info: FxIndexMap<hir::HirId, SliceLintInformation>, + max_suggested_slice: u64, +} + +impl<'a, 'tcx> Visitor<'tcx> for SliceIndexLintingVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) { + if let Some(local_id) = path_to_local(expr) { + let Self { + cx, + ref mut slice_lint_info, + max_suggested_slice, + } = *self; + + if_chain! { + // Check if this is even a local we're interested in + if let Some(use_info) = slice_lint_info.get_mut(&local_id); + + let map = cx.tcx.hir(); + + // Checking for slice indexing + let parent_id = map.get_parent_node(expr.hir_id); + if let Some(hir::Node::Expr(parent_expr)) = map.find(parent_id); + if let hir::ExprKind::Index(_, index_expr) = parent_expr.kind; + if let Some((Constant::Int(index_value), _)) = constant(cx, cx.typeck_results(), index_expr); + if let Ok(index_value) = index_value.try_into(); + if index_value < max_suggested_slice; + + // Make sure that this slice index is read only + let maybe_addrof_id = map.get_parent_node(parent_id); + if let Some(hir::Node::Expr(maybe_addrof_expr)) = map.find(maybe_addrof_id); + if let hir::ExprKind::AddrOf(_kind, hir::Mutability::Not, _inner_expr) = maybe_addrof_expr.kind; + then { + use_info.index_use.push((index_value, map.span(parent_expr.hir_id))); + return; + } + } + + // The slice was used for something other than indexing + self.slice_lint_info.remove(&local_id); + } + intravisit::walk_expr(self, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/indexing_slicing.rs b/src/tools/clippy/clippy_lints/src/indexing_slicing.rs new file mode 100644 index 000000000..4a375752e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/indexing_slicing.rs @@ -0,0 +1,205 @@ +//! lint on indexing and slicing operations + +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +use clippy_utils::higher; +use rustc_ast::ast::RangeLimits; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for out of bounds array indexing with a constant + /// index. + /// + /// ### Why is this bad? + /// This will always panic at runtime. + /// + /// ### Example + /// ```rust,no_run + /// # #![allow(const_err)] + /// let x = [1, 2, 3, 4]; + /// + /// x[9]; + /// &x[2..9]; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = [1, 2, 3, 4]; + /// // Index within bounds + /// + /// x[0]; + /// x[3]; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OUT_OF_BOUNDS_INDEXING, + correctness, + "out of bounds constant indexing" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of indexing or slicing. Arrays are special cases, this lint + /// does report on arrays if we can tell that slicing operations are in bounds and does not + /// lint on constant `usize` indexing on arrays because that is handled by rustc's `const_err` lint. + /// + /// ### Why is this bad? + /// Indexing and slicing can panic at runtime and there are + /// safe alternatives. + /// + /// ### Example + /// ```rust,no_run + /// // Vector + /// let x = vec![0; 5]; + /// + /// x[2]; + /// &x[2..100]; + /// + /// // Array + /// let y = [0, 1, 2, 3]; + /// + /// &y[10..100]; + /// &y[10..]; + /// ``` + /// + /// Use instead: + /// ```rust + /// # #![allow(unused)] + /// + /// # let x = vec![0; 5]; + /// # let y = [0, 1, 2, 3]; + /// x.get(2); + /// x.get(2..100); + /// + /// y.get(10); + /// y.get(10..100); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INDEXING_SLICING, + restriction, + "indexing/slicing usage" +} + +declare_lint_pass!(IndexingSlicing => [INDEXING_SLICING, OUT_OF_BOUNDS_INDEXING]); + +impl<'tcx> LateLintPass<'tcx> for IndexingSlicing { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if cx.tcx.hir().is_inside_const_context(expr.hir_id) { + return; + } + + if let ExprKind::Index(array, index) = &expr.kind { + let ty = cx.typeck_results().expr_ty(array).peel_refs(); + if let Some(range) = higher::Range::hir(index) { + // Ranged indexes, i.e., &x[n..m], &x[n..], &x[..n] and &x[..] + if let ty::Array(_, s) = ty.kind() { + let size: u128 = if let Some(size) = s.try_eval_usize(cx.tcx, cx.param_env) { + size.into() + } else { + return; + }; + + let const_range = to_const_range(cx, range, size); + + if let (Some(start), _) = const_range { + if start > size { + span_lint( + cx, + OUT_OF_BOUNDS_INDEXING, + range.start.map_or(expr.span, |start| start.span), + "range is out of bounds", + ); + return; + } + } + + if let (_, Some(end)) = const_range { + if end > size { + span_lint( + cx, + OUT_OF_BOUNDS_INDEXING, + range.end.map_or(expr.span, |end| end.span), + "range is out of bounds", + ); + return; + } + } + + if let (Some(_), Some(_)) = const_range { + // early return because both start and end are constants + // and we have proven above that they are in bounds + return; + } + } + + let help_msg = match (range.start, range.end) { + (None, Some(_)) => "consider using `.get(..n)`or `.get_mut(..n)` instead", + (Some(_), None) => "consider using `.get(n..)` or .get_mut(n..)` instead", + (Some(_), Some(_)) => "consider using `.get(n..m)` or `.get_mut(n..m)` instead", + (None, None) => return, // [..] is ok. + }; + + span_lint_and_help(cx, INDEXING_SLICING, expr.span, "slicing may panic", None, help_msg); + } else { + // Catchall non-range index, i.e., [n] or [n << m] + if let ty::Array(..) = ty.kind() { + // Index is a const block. + if let ExprKind::ConstBlock(..) = index.kind { + return; + } + // Index is a constant uint. + if let Some(..) = constant(cx, cx.typeck_results(), index) { + // Let rustc's `const_err` lint handle constant `usize` indexing on arrays. + return; + } + } + + span_lint_and_help( + cx, + INDEXING_SLICING, + expr.span, + "indexing may panic", + None, + "consider using `.get(n)` or `.get_mut(n)` instead", + ); + } + } + } +} + +/// Returns a tuple of options with the start and end (exclusive) values of +/// the range. If the start or end is not constant, None is returned. +fn to_const_range<'tcx>( + cx: &LateContext<'tcx>, + range: higher::Range<'_>, + array_size: u128, +) -> (Option<u128>, Option<u128>) { + let s = range + .start + .map(|expr| constant(cx, cx.typeck_results(), expr).map(|(c, _)| c)); + let start = match s { + Some(Some(Constant::Int(x))) => Some(x), + Some(_) => None, + None => Some(0), + }; + + let e = range + .end + .map(|expr| constant(cx, cx.typeck_results(), expr).map(|(c, _)| c)); + let end = match e { + Some(Some(Constant::Int(x))) => { + if range.limits == RangeLimits::Closed { + Some(x + 1) + } else { + Some(x) + } + }, + Some(_) => None, + None => Some(array_size), + }; + + (start, end) +} diff --git a/src/tools/clippy/clippy_lints/src/infinite_iter.rs b/src/tools/clippy/clippy_lints/src/infinite_iter.rs new file mode 100644 index 000000000..01c7eef4e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/infinite_iter.rs @@ -0,0 +1,260 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use clippy_utils::{higher, match_def_path, path_def_id, paths}; +use rustc_hir::{BorrowKind, Closure, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::{sym, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for iteration that is guaranteed to be infinite. + /// + /// ### Why is this bad? + /// While there may be places where this is acceptable + /// (e.g., in event streams), in most cases this is simply an error. + /// + /// ### Example + /// ```no_run + /// use std::iter; + /// + /// iter::repeat(1_u8).collect::<Vec<_>>(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INFINITE_ITER, + correctness, + "infinite iteration" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for iteration that may be infinite. + /// + /// ### Why is this bad? + /// While there may be places where this is acceptable + /// (e.g., in event streams), in most cases this is simply an error. + /// + /// ### Known problems + /// The code may have a condition to stop iteration, but + /// this lint is not clever enough to analyze it. + /// + /// ### Example + /// ```rust + /// let infinite_iter = 0..; + /// # #[allow(unused)] + /// [0..].iter().zip(infinite_iter.take_while(|x| *x > 5)); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MAYBE_INFINITE_ITER, + pedantic, + "possible infinite iteration" +} + +declare_lint_pass!(InfiniteIter => [INFINITE_ITER, MAYBE_INFINITE_ITER]); + +impl<'tcx> LateLintPass<'tcx> for InfiniteIter { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (lint, msg) = match complete_infinite_iter(cx, expr) { + Infinite => (INFINITE_ITER, "infinite iteration detected"), + MaybeInfinite => (MAYBE_INFINITE_ITER, "possible infinite iteration detected"), + Finite => { + return; + }, + }; + span_lint(cx, lint, expr.span, msg); + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Finiteness { + Infinite, + MaybeInfinite, + Finite, +} + +use self::Finiteness::{Finite, Infinite, MaybeInfinite}; + +impl Finiteness { + #[must_use] + fn and(self, b: Self) -> Self { + match (self, b) { + (Finite, _) | (_, Finite) => Finite, + (MaybeInfinite, _) | (_, MaybeInfinite) => MaybeInfinite, + _ => Infinite, + } + } + + #[must_use] + fn or(self, b: Self) -> Self { + match (self, b) { + (Infinite, _) | (_, Infinite) => Infinite, + (MaybeInfinite, _) | (_, MaybeInfinite) => MaybeInfinite, + _ => Finite, + } + } +} + +impl From<bool> for Finiteness { + #[must_use] + fn from(b: bool) -> Self { + if b { Infinite } else { Finite } + } +} + +/// This tells us what to look for to know if the iterator returned by +/// this method is infinite +#[derive(Copy, Clone)] +enum Heuristic { + /// infinite no matter what + Always, + /// infinite if the first argument is + First, + /// infinite if any of the supplied arguments is + Any, + /// infinite if all of the supplied arguments are + All, +} + +use self::Heuristic::{All, Always, Any, First}; + +/// a slice of (method name, number of args, heuristic, bounds) tuples +/// that will be used to determine whether the method in question +/// returns an infinite or possibly infinite iterator. The finiteness +/// is an upper bound, e.g., some methods can return a possibly +/// infinite iterator at worst, e.g., `take_while`. +const HEURISTICS: [(&str, usize, Heuristic, Finiteness); 19] = [ + ("zip", 2, All, Infinite), + ("chain", 2, Any, Infinite), + ("cycle", 1, Always, Infinite), + ("map", 2, First, Infinite), + ("by_ref", 1, First, Infinite), + ("cloned", 1, First, Infinite), + ("rev", 1, First, Infinite), + ("inspect", 1, First, Infinite), + ("enumerate", 1, First, Infinite), + ("peekable", 2, First, Infinite), + ("fuse", 1, First, Infinite), + ("skip", 2, First, Infinite), + ("skip_while", 1, First, Infinite), + ("filter", 2, First, Infinite), + ("filter_map", 2, First, Infinite), + ("flat_map", 2, First, Infinite), + ("unzip", 1, First, Infinite), + ("take_while", 2, First, MaybeInfinite), + ("scan", 3, First, MaybeInfinite), +]; + +fn is_infinite(cx: &LateContext<'_>, expr: &Expr<'_>) -> Finiteness { + match expr.kind { + ExprKind::MethodCall(method, args, _) => { + for &(name, len, heuristic, cap) in &HEURISTICS { + if method.ident.name.as_str() == name && args.len() == len { + return (match heuristic { + Always => Infinite, + First => is_infinite(cx, &args[0]), + Any => is_infinite(cx, &args[0]).or(is_infinite(cx, &args[1])), + All => is_infinite(cx, &args[0]).and(is_infinite(cx, &args[1])), + }) + .and(cap); + } + } + if method.ident.name == sym!(flat_map) && args.len() == 2 { + if let ExprKind::Closure(&Closure { body, .. }) = args[1].kind { + let body = cx.tcx.hir().body(body); + return is_infinite(cx, &body.value); + } + } + Finite + }, + ExprKind::Block(block, _) => block.expr.as_ref().map_or(Finite, |e| is_infinite(cx, e)), + ExprKind::Box(e) | ExprKind::AddrOf(BorrowKind::Ref, _, e) => is_infinite(cx, e), + ExprKind::Call(path, _) => path_def_id(cx, path) + .map_or(false, |id| match_def_path(cx, id, &paths::ITER_REPEAT)) + .into(), + ExprKind::Struct(..) => higher::Range::hir(expr).map_or(false, |r| r.end.is_none()).into(), + _ => Finite, + } +} + +/// the names and argument lengths of methods that *may* exhaust their +/// iterators +const POSSIBLY_COMPLETING_METHODS: [(&str, usize); 6] = [ + ("find", 2), + ("rfind", 2), + ("position", 2), + ("rposition", 2), + ("any", 2), + ("all", 2), +]; + +/// the names and argument lengths of methods that *always* exhaust +/// their iterators +const COMPLETING_METHODS: [(&str, usize); 12] = [ + ("count", 1), + ("fold", 3), + ("for_each", 2), + ("partition", 2), + ("max", 1), + ("max_by", 2), + ("max_by_key", 2), + ("min", 1), + ("min_by", 2), + ("min_by_key", 2), + ("sum", 1), + ("product", 1), +]; + +/// the paths of types that are known to be infinitely allocating +const INFINITE_COLLECTORS: &[Symbol] = &[ + sym::BinaryHeap, + sym::BTreeMap, + sym::BTreeSet, + sym::HashMap, + sym::HashSet, + sym::LinkedList, + sym::Vec, + sym::VecDeque, +]; + +fn complete_infinite_iter(cx: &LateContext<'_>, expr: &Expr<'_>) -> Finiteness { + match expr.kind { + ExprKind::MethodCall(method, args, _) => { + for &(name, len) in &COMPLETING_METHODS { + if method.ident.name.as_str() == name && args.len() == len { + return is_infinite(cx, &args[0]); + } + } + for &(name, len) in &POSSIBLY_COMPLETING_METHODS { + if method.ident.name.as_str() == name && args.len() == len { + return MaybeInfinite.and(is_infinite(cx, &args[0])); + } + } + if method.ident.name == sym!(last) && args.len() == 1 { + let not_double_ended = cx + .tcx + .get_diagnostic_item(sym::DoubleEndedIterator) + .map_or(false, |id| { + !implements_trait(cx, cx.typeck_results().expr_ty(&args[0]), id, &[]) + }); + if not_double_ended { + return is_infinite(cx, &args[0]); + } + } else if method.ident.name == sym!(collect) { + let ty = cx.typeck_results().expr_ty(expr); + if INFINITE_COLLECTORS + .iter() + .any(|diag_item| is_type_diagnostic_item(cx, ty, *diag_item)) + { + return is_infinite(cx, &args[0]); + } + } + }, + ExprKind::Binary(op, l, r) => { + if op.node.is_comparison() { + return is_infinite(cx, l).and(is_infinite(cx, r)).and(MaybeInfinite); + } + }, // TODO: ExprKind::Loop + Match + _ => (), + } + Finite +} diff --git a/src/tools/clippy/clippy_lints/src/inherent_impl.rs b/src/tools/clippy/clippy_lints/src/inherent_impl.rs new file mode 100644 index 000000000..c5abcc462 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/inherent_impl.rs @@ -0,0 +1,139 @@ +//! lint on inherent implementations + +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::is_lint_allowed; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::{def_id::LocalDefId, Item, ItemKind, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; +use std::collections::hash_map::Entry; + +declare_clippy_lint! { + /// ### What it does + /// Checks for multiple inherent implementations of a struct + /// + /// ### Why is this bad? + /// Splitting the implementation of a type makes the code harder to navigate. + /// + /// ### Example + /// ```rust + /// struct X; + /// impl X { + /// fn one() {} + /// } + /// impl X { + /// fn other() {} + /// } + /// ``` + /// + /// Could be written: + /// + /// ```rust + /// struct X; + /// impl X { + /// fn one() {} + /// fn other() {} + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MULTIPLE_INHERENT_IMPL, + restriction, + "Multiple inherent impl that could be grouped" +} + +declare_lint_pass!(MultipleInherentImpl => [MULTIPLE_INHERENT_IMPL]); + +impl<'tcx> LateLintPass<'tcx> for MultipleInherentImpl { + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + // Map from a type to it's first impl block. Needed to distinguish generic arguments. + // e.g. `Foo<Bar>` and `Foo<Baz>` + let mut type_map = FxHashMap::default(); + // List of spans to lint. (lint_span, first_span) + let mut lint_spans = Vec::new(); + + for (_, impl_ids) in cx + .tcx + .crate_inherent_impls(()) + .inherent_impls + .iter() + .filter(|(&id, impls)| { + impls.len() > 1 + // Check for `#[allow]` on the type definition + && !is_lint_allowed( + cx, + MULTIPLE_INHERENT_IMPL, + cx.tcx.hir().local_def_id_to_hir_id(id), + ) + }) + { + for impl_id in impl_ids.iter().map(|id| id.expect_local()) { + match type_map.entry(cx.tcx.type_of(impl_id)) { + Entry::Vacant(e) => { + // Store the id for the first impl block of this type. The span is retrieved lazily. + e.insert(IdOrSpan::Id(impl_id)); + }, + Entry::Occupied(mut e) => { + if let Some(span) = get_impl_span(cx, impl_id) { + let first_span = match *e.get() { + IdOrSpan::Span(s) => s, + IdOrSpan::Id(id) => { + if let Some(s) = get_impl_span(cx, id) { + // Remember the span of the first block. + *e.get_mut() = IdOrSpan::Span(s); + s + } else { + // The first impl block isn't considered by the lint. Replace it with the + // current one. + *e.get_mut() = IdOrSpan::Span(span); + continue; + } + }, + }; + lint_spans.push((span, first_span)); + } + }, + } + } + + // Switching to the next type definition, no need to keep the current entries around. + type_map.clear(); + } + + // `TyCtxt::crate_inherent_impls` doesn't have a defined order. Sort the lint output first. + lint_spans.sort_by_key(|x| x.0.lo()); + for (span, first_span) in lint_spans { + span_lint_and_note( + cx, + MULTIPLE_INHERENT_IMPL, + span, + "multiple implementations of this structure", + Some(first_span), + "first implementation here", + ); + } + } +} + +/// Gets the span for the given impl block unless it's not being considered by the lint. +fn get_impl_span(cx: &LateContext<'_>, id: LocalDefId) -> Option<Span> { + let id = cx.tcx.hir().local_def_id_to_hir_id(id); + if let Node::Item(&Item { + kind: ItemKind::Impl(impl_item), + span, + .. + }) = cx.tcx.hir().get(id) + { + (!span.from_expansion() + && impl_item.generics.params.is_empty() + && !is_lint_allowed(cx, MULTIPLE_INHERENT_IMPL, id)) + .then_some(span) + } else { + None + } +} + +enum IdOrSpan { + Id(LocalDefId), + Span(Span), +} diff --git a/src/tools/clippy/clippy_lints/src/inherent_to_string.rs b/src/tools/clippy/clippy_lints/src/inherent_to_string.rs new file mode 100644 index 000000000..17d867aac --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/inherent_to_string.rs @@ -0,0 +1,153 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use clippy_utils::{get_trait_def_id, paths, return_ty, trait_ref_of_method}; +use if_chain::if_chain; +use rustc_hir::{GenericParamKind, ImplItem, ImplItemKind}; +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 the definition of inherent methods with a signature of `to_string(&self) -> String`. + /// + /// ### Why is this bad? + /// This method is also implicitly defined if a type implements the `Display` trait. As the functionality of `Display` is much more versatile, it should be preferred. + /// + /// ### Example + /// ```rust + /// pub struct A; + /// + /// impl A { + /// pub fn to_string(&self) -> String { + /// "I am A".to_string() + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// use std::fmt; + /// + /// pub struct A; + /// + /// impl fmt::Display for A { + /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// write!(f, "I am A") + /// } + /// } + /// ``` + #[clippy::version = "1.38.0"] + pub INHERENT_TO_STRING, + style, + "type implements inherent method `to_string()`, but should instead implement the `Display` trait" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the definition of inherent methods with a signature of `to_string(&self) -> String` and if the type implementing this method also implements the `Display` trait. + /// + /// ### Why is this bad? + /// This method is also implicitly defined if a type implements the `Display` trait. The less versatile inherent method will then shadow the implementation introduced by `Display`. + /// + /// ### Example + /// ```rust + /// use std::fmt; + /// + /// pub struct A; + /// + /// impl A { + /// pub fn to_string(&self) -> String { + /// "I am A".to_string() + /// } + /// } + /// + /// impl fmt::Display for A { + /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// write!(f, "I am A, too") + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// use std::fmt; + /// + /// pub struct A; + /// + /// impl fmt::Display for A { + /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// write!(f, "I am A") + /// } + /// } + /// ``` + #[clippy::version = "1.38.0"] + pub INHERENT_TO_STRING_SHADOW_DISPLAY, + correctness, + "type implements inherent method `to_string()`, which gets shadowed by the implementation of the `Display` trait" +} + +declare_lint_pass!(InherentToString => [INHERENT_TO_STRING, INHERENT_TO_STRING_SHADOW_DISPLAY]); + +impl<'tcx> LateLintPass<'tcx> for InherentToString { + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) { + if impl_item.span.from_expansion() { + return; + } + + if_chain! { + // Check if item is a method, called to_string and has a parameter 'self' + if let ImplItemKind::Fn(ref signature, _) = impl_item.kind; + if impl_item.ident.name == sym::to_string; + let decl = &signature.decl; + if decl.implicit_self.has_implicit_self(); + if decl.inputs.len() == 1; + if impl_item.generics.params.iter().all(|p| matches!(p.kind, GenericParamKind::Lifetime { .. })); + + // Check if return type is String + if is_type_diagnostic_item(cx, return_ty(cx, impl_item.hir_id()), sym::String); + + // Filters instances of to_string which are required by a trait + if trait_ref_of_method(cx, impl_item.def_id).is_none(); + + then { + show_lint(cx, impl_item); + } + } + } +} + +fn show_lint(cx: &LateContext<'_>, item: &ImplItem<'_>) { + let display_trait_id = get_trait_def_id(cx, &paths::DISPLAY_TRAIT).expect("Failed to get trait ID of `Display`!"); + + // Get the real type of 'self' + let self_type = cx.tcx.fn_sig(item.def_id).input(0); + let self_type = self_type.skip_binder().peel_refs(); + + // Emit either a warning or an error + if implements_trait(cx, self_type, display_trait_id, &[]) { + span_lint_and_help( + cx, + INHERENT_TO_STRING_SHADOW_DISPLAY, + item.span, + &format!( + "type `{}` implements inherent method `to_string(&self) -> String` which shadows the implementation of `Display`", + self_type + ), + None, + &format!("remove the inherent method from type `{}`", self_type), + ); + } else { + span_lint_and_help( + cx, + INHERENT_TO_STRING, + item.span, + &format!( + "implementation of inherent method `to_string(&self) -> String` for type `{}`", + self_type + ), + None, + &format!("implement trait `Display` for type `{}` instead", self_type), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/init_numbered_fields.rs b/src/tools/clippy/clippy_lints/src/init_numbered_fields.rs new file mode 100644 index 000000000..7e1548531 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/init_numbered_fields.rs @@ -0,0 +1,81 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use std::borrow::Cow; +use std::cmp::Reverse; +use std::collections::BinaryHeap; + +declare_clippy_lint! { + /// ### What it does + /// Checks for tuple structs initialized with field syntax. + /// It will however not lint if a base initializer is present. + /// The lint will also ignore code in macros. + /// + /// ### Why is this bad? + /// This may be confusing to the uninitiated and adds no + /// benefit as opposed to tuple initializers + /// + /// ### Example + /// ```rust + /// struct TupleStruct(u8, u16); + /// + /// let _ = TupleStruct { + /// 0: 1, + /// 1: 23, + /// }; + /// + /// // should be written as + /// let base = TupleStruct(1, 23); + /// + /// // This is OK however + /// let _ = TupleStruct { 0: 42, ..base }; + /// ``` + #[clippy::version = "1.59.0"] + pub INIT_NUMBERED_FIELDS, + style, + "numbered fields in tuple struct initializer" +} + +declare_lint_pass!(NumberedFields => [INIT_NUMBERED_FIELDS]); + +impl<'tcx> LateLintPass<'tcx> for NumberedFields { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if let ExprKind::Struct(path, fields, None) = e.kind { + if !fields.is_empty() + && !e.span.from_expansion() + && fields + .iter() + .all(|f| f.ident.as_str().as_bytes().iter().all(u8::is_ascii_digit)) + && !matches!(cx.qpath_res(path, e.hir_id), Res::Def(DefKind::TyAlias, ..)) + { + let expr_spans = fields + .iter() + .map(|f| (Reverse(f.ident.as_str().parse::<usize>().unwrap()), f.expr.span)) + .collect::<BinaryHeap<_>>(); + let mut appl = Applicability::MachineApplicable; + let snippet = format!( + "{}({})", + snippet_with_applicability(cx, path.span(), "..", &mut appl), + expr_spans + .into_iter_sorted() + .map(|(_, span)| snippet_with_applicability(cx, span, "..", &mut appl)) + .intersperse(Cow::Borrowed(", ")) + .collect::<String>() + ); + span_lint_and_sugg( + cx, + INIT_NUMBERED_FIELDS, + e.span, + "used a field initializer for a tuple struct", + "try this instead", + snippet, + appl, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/inline_fn_without_body.rs b/src/tools/clippy/clippy_lints/src/inline_fn_without_body.rs new file mode 100644 index 000000000..dd7177e01 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/inline_fn_without_body.rs @@ -0,0 +1,60 @@ +//! checks for `#[inline]` on trait methods without bodies + +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg::DiagnosticExt; +use rustc_ast::ast::Attribute; +use rustc_errors::Applicability; +use rustc_hir::{TraitFn, TraitItem, TraitItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `#[inline]` on trait methods without bodies + /// + /// ### Why is this bad? + /// Only implementations of trait methods may be inlined. + /// The inline attribute is ignored for trait methods without bodies. + /// + /// ### Example + /// ```rust + /// trait Animal { + /// #[inline] + /// fn name(&self) -> &'static str; + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INLINE_FN_WITHOUT_BODY, + correctness, + "use of `#[inline]` on trait methods without bodies" +} + +declare_lint_pass!(InlineFnWithoutBody => [INLINE_FN_WITHOUT_BODY]); + +impl<'tcx> LateLintPass<'tcx> for InlineFnWithoutBody { + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) { + if let TraitItemKind::Fn(_, TraitFn::Required(_)) = item.kind { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + check_attrs(cx, item.ident.name, attrs); + } + } +} + +fn check_attrs(cx: &LateContext<'_>, name: Symbol, attrs: &[Attribute]) { + for attr in attrs { + if !attr.has_name(sym::inline) { + continue; + } + + span_lint_and_then( + cx, + INLINE_FN_WITHOUT_BODY, + attr.span, + &format!("use of `#[inline]` on trait method `{}` which has no body", name), + |diag| { + diag.suggest_remove_item(cx, attr.span, "remove", Applicability::MachineApplicable); + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/int_plus_one.rs b/src/tools/clippy/clippy_lints/src/int_plus_one.rs new file mode 100644 index 000000000..9a944def3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/int_plus_one.rs @@ -0,0 +1,171 @@ +//! lint on blocks unnecessarily using >= with a + 1 or - 1 + +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_opt; +use rustc_ast::ast::{BinOpKind, Expr, ExprKind, Lit, LitKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `x >= y + 1` or `x - 1 >= y` (and `<=`) in a block + /// + /// ### Why is this bad? + /// Readability -- better to use `> y` instead of `>= y + 1`. + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// # let y = 1; + /// if x >= y + 1 {} + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = 1; + /// # let y = 1; + /// if x > y {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INT_PLUS_ONE, + complexity, + "instead of using `x >= y + 1`, use `x > y`" +} + +declare_lint_pass!(IntPlusOne => [INT_PLUS_ONE]); + +// cases: +// BinOpKind::Ge +// x >= y + 1 +// x - 1 >= y +// +// BinOpKind::Le +// x + 1 <= y +// x <= y - 1 + +#[derive(Copy, Clone)] +enum Side { + Lhs, + Rhs, +} + +impl IntPlusOne { + #[expect(clippy::cast_sign_loss)] + fn check_lit(lit: &Lit, target_value: i128) -> bool { + if let LitKind::Int(value, ..) = lit.kind { + return value == (target_value as u128); + } + false + } + + fn check_binop(cx: &EarlyContext<'_>, binop: BinOpKind, lhs: &Expr, rhs: &Expr) -> Option<String> { + match (binop, &lhs.kind, &rhs.kind) { + // case where `x - 1 >= ...` or `-1 + x >= ...` + (BinOpKind::Ge, &ExprKind::Binary(ref lhskind, ref lhslhs, ref lhsrhs), _) => { + match (lhskind.node, &lhslhs.kind, &lhsrhs.kind) { + // `-1 + x` + (BinOpKind::Add, &ExprKind::Lit(ref lit), _) if Self::check_lit(lit, -1) => { + Self::generate_recommendation(cx, binop, lhsrhs, rhs, Side::Lhs) + }, + // `x - 1` + (BinOpKind::Sub, _, &ExprKind::Lit(ref lit)) if Self::check_lit(lit, 1) => { + Self::generate_recommendation(cx, binop, lhslhs, rhs, Side::Lhs) + }, + _ => None, + } + }, + // case where `... >= y + 1` or `... >= 1 + y` + (BinOpKind::Ge, _, &ExprKind::Binary(ref rhskind, ref rhslhs, ref rhsrhs)) + if rhskind.node == BinOpKind::Add => + { + match (&rhslhs.kind, &rhsrhs.kind) { + // `y + 1` and `1 + y` + (&ExprKind::Lit(ref lit), _) if Self::check_lit(lit, 1) => { + Self::generate_recommendation(cx, binop, rhsrhs, lhs, Side::Rhs) + }, + (_, &ExprKind::Lit(ref lit)) if Self::check_lit(lit, 1) => { + Self::generate_recommendation(cx, binop, rhslhs, lhs, Side::Rhs) + }, + _ => None, + } + }, + // case where `x + 1 <= ...` or `1 + x <= ...` + (BinOpKind::Le, &ExprKind::Binary(ref lhskind, ref lhslhs, ref lhsrhs), _) + if lhskind.node == BinOpKind::Add => + { + match (&lhslhs.kind, &lhsrhs.kind) { + // `1 + x` and `x + 1` + (&ExprKind::Lit(ref lit), _) if Self::check_lit(lit, 1) => { + Self::generate_recommendation(cx, binop, lhsrhs, rhs, Side::Lhs) + }, + (_, &ExprKind::Lit(ref lit)) if Self::check_lit(lit, 1) => { + Self::generate_recommendation(cx, binop, lhslhs, rhs, Side::Lhs) + }, + _ => None, + } + }, + // case where `... >= y - 1` or `... >= -1 + y` + (BinOpKind::Le, _, &ExprKind::Binary(ref rhskind, ref rhslhs, ref rhsrhs)) => { + match (rhskind.node, &rhslhs.kind, &rhsrhs.kind) { + // `-1 + y` + (BinOpKind::Add, &ExprKind::Lit(ref lit), _) if Self::check_lit(lit, -1) => { + Self::generate_recommendation(cx, binop, rhsrhs, lhs, Side::Rhs) + }, + // `y - 1` + (BinOpKind::Sub, _, &ExprKind::Lit(ref lit)) if Self::check_lit(lit, 1) => { + Self::generate_recommendation(cx, binop, rhslhs, lhs, Side::Rhs) + }, + _ => None, + } + }, + _ => None, + } + } + + fn generate_recommendation( + cx: &EarlyContext<'_>, + binop: BinOpKind, + node: &Expr, + other_side: &Expr, + side: Side, + ) -> Option<String> { + let binop_string = match binop { + BinOpKind::Ge => ">", + BinOpKind::Le => "<", + _ => return None, + }; + if let Some(snippet) = snippet_opt(cx, node.span) { + if let Some(other_side_snippet) = snippet_opt(cx, other_side.span) { + let rec = match side { + Side::Lhs => Some(format!("{} {} {}", snippet, binop_string, other_side_snippet)), + Side::Rhs => Some(format!("{} {} {}", other_side_snippet, binop_string, snippet)), + }; + return rec; + } + } + None + } + + fn emit_warning(cx: &EarlyContext<'_>, block: &Expr, recommendation: String) { + span_lint_and_sugg( + cx, + INT_PLUS_ONE, + block.span, + "unnecessary `>= y + 1` or `x - 1 >=`", + "change it to", + recommendation, + Applicability::MachineApplicable, // snippet + ); + } +} + +impl EarlyLintPass for IntPlusOne { + fn check_expr(&mut self, cx: &EarlyContext<'_>, item: &Expr) { + if let ExprKind::Binary(ref kind, ref lhs, ref rhs) = item.kind { + if let Some(rec) = Self::check_binop(cx, kind.node, lhs, rhs) { + Self::emit_warning(cx, item, rec); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/invalid_upcast_comparisons.rs b/src/tools/clippy/clippy_lints/src/invalid_upcast_comparisons.rs new file mode 100644 index 000000000..36e03e50a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/invalid_upcast_comparisons.rs @@ -0,0 +1,161 @@ +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::{self, IntTy, UintTy}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +use clippy_utils::comparisons; +use clippy_utils::comparisons::Rel; +use clippy_utils::consts::{constant_full_int, FullInt}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::source::snippet; + +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons where the relation is always either + /// true or false, but where one side has been upcast so that the comparison is + /// necessary. Only integer types are checked. + /// + /// ### Why is this bad? + /// An expression like `let x : u8 = ...; (x as u32) > 300` + /// will mistakenly imply that it is possible for `x` to be outside the range of + /// `u8`. + /// + /// ### Known problems + /// https://github.com/rust-lang/rust-clippy/issues/886 + /// + /// ### Example + /// ```rust + /// let x: u8 = 1; + /// (x as u32) > 300; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INVALID_UPCAST_COMPARISONS, + pedantic, + "a comparison involving an upcast which is always true or false" +} + +declare_lint_pass!(InvalidUpcastComparisons => [INVALID_UPCAST_COMPARISONS]); + +fn numeric_cast_precast_bounds<'a>(cx: &LateContext<'_>, expr: &'a Expr<'_>) -> Option<(FullInt, FullInt)> { + if let ExprKind::Cast(cast_exp, _) = expr.kind { + let pre_cast_ty = cx.typeck_results().expr_ty(cast_exp); + let cast_ty = cx.typeck_results().expr_ty(expr); + // if it's a cast from i32 to u32 wrapping will invalidate all these checks + if cx.layout_of(pre_cast_ty).ok().map(|l| l.size) == cx.layout_of(cast_ty).ok().map(|l| l.size) { + return None; + } + match pre_cast_ty.kind() { + ty::Int(int_ty) => Some(match int_ty { + IntTy::I8 => (FullInt::S(i128::from(i8::MIN)), FullInt::S(i128::from(i8::MAX))), + IntTy::I16 => (FullInt::S(i128::from(i16::MIN)), FullInt::S(i128::from(i16::MAX))), + IntTy::I32 => (FullInt::S(i128::from(i32::MIN)), FullInt::S(i128::from(i32::MAX))), + IntTy::I64 => (FullInt::S(i128::from(i64::MIN)), FullInt::S(i128::from(i64::MAX))), + IntTy::I128 => (FullInt::S(i128::MIN), FullInt::S(i128::MAX)), + IntTy::Isize => (FullInt::S(isize::MIN as i128), FullInt::S(isize::MAX as i128)), + }), + ty::Uint(uint_ty) => Some(match uint_ty { + UintTy::U8 => (FullInt::U(u128::from(u8::MIN)), FullInt::U(u128::from(u8::MAX))), + UintTy::U16 => (FullInt::U(u128::from(u16::MIN)), FullInt::U(u128::from(u16::MAX))), + UintTy::U32 => (FullInt::U(u128::from(u32::MIN)), FullInt::U(u128::from(u32::MAX))), + UintTy::U64 => (FullInt::U(u128::from(u64::MIN)), FullInt::U(u128::from(u64::MAX))), + UintTy::U128 => (FullInt::U(u128::MIN), FullInt::U(u128::MAX)), + UintTy::Usize => (FullInt::U(usize::MIN as u128), FullInt::U(usize::MAX as u128)), + }), + _ => None, + } + } else { + None + } +} + +fn err_upcast_comparison(cx: &LateContext<'_>, span: Span, expr: &Expr<'_>, always: bool) { + if let ExprKind::Cast(cast_val, _) = expr.kind { + span_lint( + cx, + INVALID_UPCAST_COMPARISONS, + span, + &format!( + "because of the numeric bounds on `{}` prior to casting, this expression is always {}", + snippet(cx, cast_val.span, "the expression"), + if always { "true" } else { "false" }, + ), + ); + } +} + +fn upcast_comparison_bounds_err<'tcx>( + cx: &LateContext<'tcx>, + span: Span, + rel: comparisons::Rel, + lhs_bounds: Option<(FullInt, FullInt)>, + lhs: &'tcx Expr<'_>, + rhs: &'tcx Expr<'_>, + invert: bool, +) { + if let Some((lb, ub)) = lhs_bounds { + if let Some(norm_rhs_val) = constant_full_int(cx, cx.typeck_results(), rhs) { + if rel == Rel::Eq || rel == Rel::Ne { + if norm_rhs_val < lb || norm_rhs_val > ub { + err_upcast_comparison(cx, span, lhs, rel == Rel::Ne); + } + } else if match rel { + Rel::Lt => { + if invert { + norm_rhs_val < lb + } else { + ub < norm_rhs_val + } + }, + Rel::Le => { + if invert { + norm_rhs_val <= lb + } else { + ub <= norm_rhs_val + } + }, + Rel::Eq | Rel::Ne => unreachable!(), + } { + err_upcast_comparison(cx, span, lhs, true); + } else if match rel { + Rel::Lt => { + if invert { + norm_rhs_val >= ub + } else { + lb >= norm_rhs_val + } + }, + Rel::Le => { + if invert { + norm_rhs_val > ub + } else { + lb > norm_rhs_val + } + }, + Rel::Eq | Rel::Ne => unreachable!(), + } { + err_upcast_comparison(cx, span, lhs, false); + } + } + } +} + +impl<'tcx> LateLintPass<'tcx> for InvalidUpcastComparisons { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Binary(ref cmp, lhs, rhs) = expr.kind { + let normalized = comparisons::normalize_comparison(cmp.node, lhs, rhs); + let (rel, normalized_lhs, normalized_rhs) = if let Some(val) = normalized { + val + } else { + return; + }; + + let lhs_bounds = numeric_cast_precast_bounds(cx, normalized_lhs); + let rhs_bounds = numeric_cast_precast_bounds(cx, normalized_rhs); + + upcast_comparison_bounds_err(cx, expr.span, rel, lhs_bounds, normalized_lhs, normalized_rhs, false); + upcast_comparison_bounds_err(cx, expr.span, rel, rhs_bounds, normalized_rhs, normalized_lhs, true); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/invalid_utf8_in_unchecked.rs b/src/tools/clippy/clippy_lints/src/invalid_utf8_in_unchecked.rs new file mode 100644 index 000000000..e0a607f9a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/invalid_utf8_in_unchecked.rs @@ -0,0 +1,74 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{match_function_call, paths}; +use rustc_ast::{BorrowKind, LitKind}; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `std::str::from_utf8_unchecked` with an invalid UTF-8 literal + /// + /// ### Why is this bad? + /// Creating such a `str` would result in undefined behavior + /// + /// ### Example + /// ```rust + /// # #[allow(unused)] + /// unsafe { + /// std::str::from_utf8_unchecked(b"cl\x82ippy"); + /// } + /// ``` + #[clippy::version = "1.64.0"] + pub INVALID_UTF8_IN_UNCHECKED, + correctness, + "using a non UTF-8 literal in `std::std::from_utf8_unchecked`" +} +declare_lint_pass!(InvalidUtf8InUnchecked => [INVALID_UTF8_IN_UNCHECKED]); + +impl<'tcx> LateLintPass<'tcx> for InvalidUtf8InUnchecked { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if let Some([arg]) = match_function_call(cx, expr, &paths::STR_FROM_UTF8_UNCHECKED) { + match &arg.kind { + ExprKind::Lit(Spanned { node: lit, .. }) => { + if let LitKind::ByteStr(bytes) = &lit + && std::str::from_utf8(bytes).is_err() + { + lint(cx, expr.span); + } + }, + ExprKind::AddrOf(BorrowKind::Ref, _, Expr { kind: ExprKind::Array(args), .. }) => { + let elements = args.iter().map(|e|{ + match &e.kind { + ExprKind::Lit(Spanned { node: lit, .. }) => match lit { + LitKind::Byte(b) => Some(*b), + #[allow(clippy::cast_possible_truncation)] + LitKind::Int(b, _) => Some(*b as u8), + _ => None + } + _ => None + } + }).collect::<Option<Vec<_>>>(); + + if let Some(elements) = elements + && std::str::from_utf8(&elements).is_err() + { + lint(cx, expr.span); + } + } + _ => {} + } + } + } +} + +fn lint(cx: &LateContext<'_>, span: Span) { + span_lint( + cx, + INVALID_UTF8_IN_UNCHECKED, + span, + "non UTF-8 literal in `std::str::from_utf8_unchecked`", + ); +} diff --git a/src/tools/clippy/clippy_lints/src/items_after_statements.rs b/src/tools/clippy/clippy_lints/src/items_after_statements.rs new file mode 100644 index 000000000..46d439b44 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/items_after_statements.rs @@ -0,0 +1,88 @@ +//! lint when items are used after statements + +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast::{Block, ItemKind, StmtKind}; +use rustc_lint::{EarlyContext, EarlyLintPass, 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 items declared after some statement in a block. + /// + /// ### Why is this bad? + /// Items live for the entire scope they are declared + /// in. But statements are processed in order. This might cause confusion as + /// it's hard to figure out which item is meant in a statement. + /// + /// ### Example + /// ```rust + /// fn foo() { + /// println!("cake"); + /// } + /// + /// fn main() { + /// foo(); // prints "foo" + /// fn foo() { + /// println!("foo"); + /// } + /// foo(); // prints "foo" + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// fn foo() { + /// println!("cake"); + /// } + /// + /// fn main() { + /// fn foo() { + /// println!("foo"); + /// } + /// foo(); // prints "foo" + /// foo(); // prints "foo" + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ITEMS_AFTER_STATEMENTS, + pedantic, + "blocks where an item comes after a statement" +} + +declare_lint_pass!(ItemsAfterStatements => [ITEMS_AFTER_STATEMENTS]); + +impl EarlyLintPass for ItemsAfterStatements { + fn check_block(&mut self, cx: &EarlyContext<'_>, item: &Block) { + if in_external_macro(cx.sess(), item.span) { + return; + } + + // skip initial items and trailing semicolons + let stmts = item + .stmts + .iter() + .map(|stmt| &stmt.kind) + .skip_while(|s| matches!(**s, StmtKind::Item(..) | StmtKind::Empty)); + + // lint on all further items + for stmt in stmts { + if let StmtKind::Item(ref it) = *stmt { + if in_external_macro(cx.sess(), it.span) { + return; + } + if let ItemKind::MacroDef(..) = it.kind { + // do not lint `macro_rules`, but continue processing further statements + continue; + } + span_lint( + cx, + ITEMS_AFTER_STATEMENTS, + it.span, + "adding items after statements is confusing, since items exist from the \ + start of the scope", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/iter_not_returning_iterator.rs b/src/tools/clippy/clippy_lints/src/iter_not_returning_iterator.rs new file mode 100644 index 000000000..b56d87c53 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/iter_not_returning_iterator.rs @@ -0,0 +1,90 @@ +use clippy_utils::{diagnostics::span_lint, get_parent_node, ty::implements_trait}; +use rustc_hir::{def_id::LocalDefId, FnSig, ImplItem, ImplItemKind, Item, ItemKind, Node, TraitItem, TraitItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Detects methods named `iter` or `iter_mut` that do not have a return type that implements `Iterator`. + /// + /// ### Why is this bad? + /// Methods named `iter` or `iter_mut` conventionally return an `Iterator`. + /// + /// ### Example + /// ```rust + /// // `String` does not implement `Iterator` + /// struct Data {} + /// impl Data { + /// fn iter(&self) -> String { + /// todo!() + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// use std::str::Chars; + /// struct Data {} + /// impl Data { + /// fn iter(&self) -> Chars<'static> { + /// todo!() + /// } + /// } + /// ``` + #[clippy::version = "1.57.0"] + pub ITER_NOT_RETURNING_ITERATOR, + pedantic, + "methods named `iter` or `iter_mut` that do not return an `Iterator`" +} + +declare_lint_pass!(IterNotReturningIterator => [ITER_NOT_RETURNING_ITERATOR]); + +impl<'tcx> LateLintPass<'tcx> for IterNotReturningIterator { + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) { + let name = item.ident.name.as_str(); + if matches!(name, "iter" | "iter_mut") { + if let TraitItemKind::Fn(fn_sig, _) = &item.kind { + check_sig(cx, name, fn_sig, item.def_id); + } + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'tcx>) { + let name = item.ident.name.as_str(); + if matches!(name, "iter" | "iter_mut") + && !matches!( + get_parent_node(cx.tcx, item.hir_id()), + Some(Node::Item(Item { kind: ItemKind::Impl(i), .. })) if i.of_trait.is_some() + ) + { + if let ImplItemKind::Fn(fn_sig, _) = &item.kind { + check_sig(cx, name, fn_sig, item.def_id); + } + } + } +} + +fn check_sig(cx: &LateContext<'_>, name: &str, sig: &FnSig<'_>, fn_id: LocalDefId) { + if sig.decl.implicit_self.has_implicit_self() { + let ret_ty = cx.tcx.erase_late_bound_regions(cx.tcx.fn_sig(fn_id).output()); + let ret_ty = cx + .tcx + .try_normalize_erasing_regions(cx.param_env, ret_ty) + .unwrap_or(ret_ty); + if cx + .tcx + .get_diagnostic_item(sym::Iterator) + .map_or(false, |iter_id| !implements_trait(cx, ret_ty, iter_id, &[])) + { + span_lint( + cx, + ITER_NOT_RETURNING_ITERATOR, + sig.span, + &format!( + "this method is named `{}` but its return type does not implement `Iterator`", + name + ), + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/large_const_arrays.rs b/src/tools/clippy/clippy_lints/src/large_const_arrays.rs new file mode 100644 index 000000000..984c5cd4e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/large_const_arrays.rs @@ -0,0 +1,86 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::{self, ConstKind}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{BytePos, Pos, Span}; +use rustc_typeck::hir_ty_to_ty; + +declare_clippy_lint! { + /// ### What it does + /// Checks for large `const` arrays that should + /// be defined as `static` instead. + /// + /// ### Why is this bad? + /// Performance: const variables are inlined upon use. + /// Static items result in only one instance and has a fixed location in memory. + /// + /// ### Example + /// ```rust,ignore + /// pub const a = [0u32; 1_000_000]; + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// pub static a = [0u32; 1_000_000]; + /// ``` + #[clippy::version = "1.44.0"] + pub LARGE_CONST_ARRAYS, + perf, + "large non-scalar const array may cause performance overhead" +} + +pub struct LargeConstArrays { + maximum_allowed_size: u64, +} + +impl LargeConstArrays { + #[must_use] + pub fn new(maximum_allowed_size: u64) -> Self { + Self { maximum_allowed_size } + } +} + +impl_lint_pass!(LargeConstArrays => [LARGE_CONST_ARRAYS]); + +impl<'tcx> LateLintPass<'tcx> for LargeConstArrays { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if_chain! { + if !item.span.from_expansion(); + if let ItemKind::Const(hir_ty, _) = &item.kind; + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + if let ty::Array(element_type, cst) = ty.kind(); + if let ConstKind::Value(ty::ValTree::Leaf(element_count)) = cst.kind(); + if let Ok(element_count) = element_count.try_to_machine_usize(cx.tcx); + if let Ok(element_size) = cx.layout_of(*element_type).map(|l| l.size.bytes()); + if self.maximum_allowed_size < element_count * element_size; + + then { + let hi_pos = item.ident.span.lo() - BytePos::from_usize(1); + let sugg_span = Span::new( + hi_pos - BytePos::from_usize("const".len()), + hi_pos, + item.span.ctxt(), + item.span.parent(), + ); + span_lint_and_then( + cx, + LARGE_CONST_ARRAYS, + item.span, + "large array defined as const", + |diag| { + diag.span_suggestion( + sugg_span, + "make this a static item", + "static", + Applicability::MachineApplicable, + ); + } + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/large_enum_variant.rs b/src/tools/clippy/clippy_lints/src/large_enum_variant.rs new file mode 100644 index 000000000..c58df126d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/large_enum_variant.rs @@ -0,0 +1,200 @@ +//! lint when there is a large size difference between variants on an enum + +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{diagnostics::span_lint_and_then, ty::is_copy}; +use rustc_errors::Applicability; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::{Adt, Ty}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for large size differences between variants on + /// `enum`s. + /// + /// ### Why is this bad? + /// Enum size is bounded by the largest variant. Having a + /// large variant can penalize the memory layout of that enum. + /// + /// ### Known problems + /// This lint obviously cannot take the distribution of + /// variants in your running program into account. It is possible that the + /// smaller variants make up less than 1% of all instances, in which case + /// the overhead is negligible and the boxing is counter-productive. Always + /// measure the change this lint suggests. + /// + /// For types that implement `Copy`, the suggestion to `Box` a variant's + /// data would require removing the trait impl. The types can of course + /// still be `Clone`, but that is worse ergonomically. Depending on the + /// use case it may be possible to store the large data in an auxiliary + /// structure (e.g. Arena or ECS). + /// + /// The lint will ignore generic types if the layout depends on the + /// generics, even if the size difference will be large anyway. + /// + /// ### Example + /// ```rust + /// enum Test { + /// A(i32), + /// B([i32; 8000]), + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// // Possibly better + /// enum Test2 { + /// A(i32), + /// B(Box<[i32; 8000]>), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LARGE_ENUM_VARIANT, + perf, + "large size difference between variants on an enum" +} + +#[derive(Copy, Clone)] +pub struct LargeEnumVariant { + maximum_size_difference_allowed: u64, +} + +impl LargeEnumVariant { + #[must_use] + pub fn new(maximum_size_difference_allowed: u64) -> Self { + Self { + maximum_size_difference_allowed, + } + } +} + +struct FieldInfo { + ind: usize, + size: u64, +} + +struct VariantInfo { + ind: usize, + size: u64, + fields_size: Vec<FieldInfo>, +} + +impl_lint_pass!(LargeEnumVariant => [LARGE_ENUM_VARIANT]); + +impl<'tcx> LateLintPass<'tcx> for LargeEnumVariant { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &Item<'tcx>) { + if in_external_macro(cx.tcx.sess, item.span) { + return; + } + if let ItemKind::Enum(ref def, _) = item.kind { + let ty = cx.tcx.type_of(item.def_id); + let adt = ty.ty_adt_def().expect("already checked whether this is an enum"); + if adt.variants().len() <= 1 { + return; + } + let mut variants_size: Vec<VariantInfo> = Vec::new(); + for (i, variant) in adt.variants().iter().enumerate() { + let mut fields_size = Vec::new(); + for (i, f) in variant.fields.iter().enumerate() { + let ty = cx.tcx.type_of(f.did); + // don't lint variants which have a field of generic type. + match cx.layout_of(ty) { + Ok(l) => { + let fsize = l.size.bytes(); + fields_size.push(FieldInfo { ind: i, size: fsize }); + }, + Err(_) => { + return; + }, + } + } + let size: u64 = fields_size.iter().map(|info| info.size).sum(); + + variants_size.push(VariantInfo { + ind: i, + size, + fields_size, + }); + } + + variants_size.sort_by(|a, b| (b.size.cmp(&a.size))); + + let mut difference = variants_size[0].size - variants_size[1].size; + if difference > self.maximum_size_difference_allowed { + let help_text = "consider boxing the large fields to reduce the total size of the enum"; + span_lint_and_then( + cx, + LARGE_ENUM_VARIANT, + def.variants[variants_size[0].ind].span, + "large size difference between variants", + |diag| { + diag.span_label( + def.variants[variants_size[0].ind].span, + &format!("this variant is {} bytes", variants_size[0].size), + ); + diag.span_note( + def.variants[variants_size[1].ind].span, + &format!("and the second-largest variant is {} bytes:", variants_size[1].size), + ); + + let fields = def.variants[variants_size[0].ind].data.fields(); + variants_size[0].fields_size.sort_by(|a, b| (a.size.cmp(&b.size))); + let mut applicability = Applicability::MaybeIncorrect; + if is_copy(cx, ty) || maybe_copy(cx, ty) { + diag.span_note( + item.ident.span, + "boxing a variant would require the type no longer be `Copy`", + ); + } else { + let sugg: Vec<(Span, String)> = variants_size[0] + .fields_size + .iter() + .rev() + .map_while(|val| { + if difference > self.maximum_size_difference_allowed { + difference = difference.saturating_sub(val.size); + Some(( + fields[val.ind].ty.span, + format!( + "Box<{}>", + snippet_with_applicability( + cx, + fields[val.ind].ty.span, + "..", + &mut applicability + ) + .into_owned() + ), + )) + } else { + None + } + }) + .collect(); + + if !sugg.is_empty() { + diag.multipart_suggestion(help_text, sugg, Applicability::MaybeIncorrect); + return; + } + } + diag.span_help(def.variants[variants_size[0].ind].span, help_text); + }, + ); + } + } + } +} + +fn maybe_copy<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + if let Adt(_def, substs) = ty.kind() + && substs.types().next().is_some() + && let Some(copy_trait) = cx.tcx.lang_items().copy_trait() + { + return cx.tcx.non_blanket_impls_for_ty(copy_trait, ty).next().is_some(); + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/large_include_file.rs b/src/tools/clippy/clippy_lints/src/large_include_file.rs new file mode 100644 index 000000000..84dd61a1e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/large_include_file.rs @@ -0,0 +1,87 @@ +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::is_lint_allowed; +use clippy_utils::macros::root_macro_call_first_node; +use rustc_ast::LitKind; +use rustc_hir::Expr; +use rustc_hir::ExprKind; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for the inclusion of large files via `include_bytes!()` + /// and `include_str!()` + /// + /// ### Why is this bad? + /// Including large files can increase the size of the binary + /// + /// ### Example + /// ```rust,ignore + /// let included_str = include_str!("very_large_file.txt"); + /// let included_bytes = include_bytes!("very_large_file.txt"); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// use std::fs; + /// + /// // You can load the file at runtime + /// let string = fs::read_to_string("very_large_file.txt")?; + /// let bytes = fs::read("very_large_file.txt")?; + /// ``` + #[clippy::version = "1.62.0"] + pub LARGE_INCLUDE_FILE, + restriction, + "including a large file" +} + +pub struct LargeIncludeFile { + max_file_size: u64, +} + +impl LargeIncludeFile { + #[must_use] + pub fn new(max_file_size: u64) -> Self { + Self { max_file_size } + } +} + +impl_lint_pass!(LargeIncludeFile => [LARGE_INCLUDE_FILE]); + +impl LateLintPass<'_> for LargeIncludeFile { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) { + if_chain! { + if let Some(macro_call) = root_macro_call_first_node(cx, expr); + if !is_lint_allowed(cx, LARGE_INCLUDE_FILE, expr.hir_id); + if cx.tcx.is_diagnostic_item(sym::include_bytes_macro, macro_call.def_id) + || cx.tcx.is_diagnostic_item(sym::include_str_macro, macro_call.def_id); + if let ExprKind::Lit(lit) = &expr.kind; + then { + let len = match &lit.node { + // include_bytes + LitKind::ByteStr(bstr) => bstr.len(), + // include_str + LitKind::Str(sym, _) => sym.as_str().len(), + _ => return, + }; + + if len as u64 <= self.max_file_size { + return; + } + + span_lint_and_note( + cx, + LARGE_INCLUDE_FILE, + expr.span, + "attempted to include a large file", + None, + &format!( + "the configuration allows a maximum size of {} bytes", + self.max_file_size + ), + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/large_stack_arrays.rs b/src/tools/clippy/clippy_lints/src/large_stack_arrays.rs new file mode 100644 index 000000000..0acbd81ae --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/large_stack_arrays.rs @@ -0,0 +1,67 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::{self, ConstKind}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for local arrays that may be too large. + /// + /// ### Why is this bad? + /// Large local arrays may cause stack overflow. + /// + /// ### Example + /// ```rust,ignore + /// let a = [0u32; 1_000_000]; + /// ``` + #[clippy::version = "1.41.0"] + pub LARGE_STACK_ARRAYS, + pedantic, + "allocating large arrays on stack may cause stack overflow" +} + +pub struct LargeStackArrays { + maximum_allowed_size: u64, +} + +impl LargeStackArrays { + #[must_use] + pub fn new(maximum_allowed_size: u64) -> Self { + Self { maximum_allowed_size } + } +} + +impl_lint_pass!(LargeStackArrays => [LARGE_STACK_ARRAYS]); + +impl<'tcx> LateLintPass<'tcx> for LargeStackArrays { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::Repeat(_, _) = expr.kind; + if let ty::Array(element_type, cst) = cx.typeck_results().expr_ty(expr).kind(); + if let ConstKind::Value(ty::ValTree::Leaf(element_count)) = cst.kind(); + if let Ok(element_count) = element_count.try_to_machine_usize(cx.tcx); + if let Ok(element_size) = cx.layout_of(*element_type).map(|l| l.size.bytes()); + if self.maximum_allowed_size < element_count * element_size; + then { + span_lint_and_help( + cx, + LARGE_STACK_ARRAYS, + expr.span, + &format!( + "allocating a local array larger than {} bytes", + self.maximum_allowed_size + ), + None, + &format!( + "consider allocating on the heap with `vec!{}.into_boxed_slice()`", + snippet(cx, expr.span, "[...]") + ), + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/len_zero.rs b/src/tools/clippy/clippy_lints/src/len_zero.rs new file mode 100644 index 000000000..246f5aad8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/len_zero.rs @@ -0,0 +1,495 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{get_item_name, get_parent_as_impl, is_lint_allowed}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::def_id::DefIdSet; +use rustc_hir::{ + def_id::DefId, AssocItemKind, BinOpKind, Expr, ExprKind, FnRetTy, ImplItem, ImplItemKind, ImplicitSelfKind, Item, + ItemKind, Mutability, Node, TraitItemRef, TyKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, AssocKind, FnSig, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{ + source_map::{Span, Spanned, Symbol}, + symbol::sym, +}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for getting the length of something via `.len()` + /// just to compare to zero, and suggests using `.is_empty()` where applicable. + /// + /// ### Why is this bad? + /// Some structures can answer `.is_empty()` much faster + /// than calculating their length. So it is good to get into the habit of using + /// `.is_empty()`, and having it is cheap. + /// Besides, it makes the intent clearer than a manual comparison in some contexts. + /// + /// ### Example + /// ```ignore + /// if x.len() == 0 { + /// .. + /// } + /// if y.len() != 0 { + /// .. + /// } + /// ``` + /// instead use + /// ```ignore + /// if x.is_empty() { + /// .. + /// } + /// if !y.is_empty() { + /// .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LEN_ZERO, + style, + "checking `.len() == 0` or `.len() > 0` (or similar) when `.is_empty()` could be used instead" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for items that implement `.len()` but not + /// `.is_empty()`. + /// + /// ### Why is this bad? + /// It is good custom to have both methods, because for + /// some data structures, asking about the length will be a costly operation, + /// whereas `.is_empty()` can usually answer in constant time. Also it used to + /// lead to false positives on the [`len_zero`](#len_zero) lint – currently that + /// lint will ignore such entities. + /// + /// ### Example + /// ```ignore + /// impl X { + /// pub fn len(&self) -> usize { + /// .. + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LEN_WITHOUT_IS_EMPTY, + style, + "traits or impls with a public `len` method but no corresponding `is_empty` method" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for comparing to an empty slice such as `""` or `[]`, + /// and suggests using `.is_empty()` where applicable. + /// + /// ### Why is this bad? + /// Some structures can answer `.is_empty()` much faster + /// than checking for equality. So it is good to get into the habit of using + /// `.is_empty()`, and having it is cheap. + /// Besides, it makes the intent clearer than a manual comparison in some contexts. + /// + /// ### Example + /// + /// ```ignore + /// if s == "" { + /// .. + /// } + /// + /// if arr == [] { + /// .. + /// } + /// ``` + /// Use instead: + /// ```ignore + /// if s.is_empty() { + /// .. + /// } + /// + /// if arr.is_empty() { + /// .. + /// } + /// ``` + #[clippy::version = "1.49.0"] + pub COMPARISON_TO_EMPTY, + style, + "checking `x == \"\"` or `x == []` (or similar) when `.is_empty()` could be used instead" +} + +declare_lint_pass!(LenZero => [LEN_ZERO, LEN_WITHOUT_IS_EMPTY, COMPARISON_TO_EMPTY]); + +impl<'tcx> LateLintPass<'tcx> for LenZero { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if item.span.from_expansion() { + return; + } + + if let ItemKind::Trait(_, _, _, _, trait_items) = item.kind { + check_trait_items(cx, item, trait_items); + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) { + if_chain! { + if item.ident.name == sym::len; + if let ImplItemKind::Fn(sig, _) = &item.kind; + if sig.decl.implicit_self.has_implicit_self(); + if cx.access_levels.is_exported(item.def_id); + if matches!(sig.decl.output, FnRetTy::Return(_)); + if let Some(imp) = get_parent_as_impl(cx.tcx, item.hir_id()); + if imp.of_trait.is_none(); + if let TyKind::Path(ty_path) = &imp.self_ty.kind; + if let Some(ty_id) = cx.qpath_res(ty_path, imp.self_ty.hir_id).opt_def_id(); + if let Some(local_id) = ty_id.as_local(); + let ty_hir_id = cx.tcx.hir().local_def_id_to_hir_id(local_id); + if !is_lint_allowed(cx, LEN_WITHOUT_IS_EMPTY, ty_hir_id); + if let Some(output) = parse_len_output(cx, cx.tcx.fn_sig(item.def_id).skip_binder()); + then { + let (name, kind) = match cx.tcx.hir().find(ty_hir_id) { + Some(Node::ForeignItem(x)) => (x.ident.name, "extern type"), + Some(Node::Item(x)) => match x.kind { + ItemKind::Struct(..) => (x.ident.name, "struct"), + ItemKind::Enum(..) => (x.ident.name, "enum"), + ItemKind::Union(..) => (x.ident.name, "union"), + _ => (x.ident.name, "type"), + } + _ => return, + }; + check_for_is_empty(cx, sig.span, sig.decl.implicit_self, output, ty_id, name, kind) + } + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if expr.span.from_expansion() { + return; + } + + if let ExprKind::Binary(Spanned { node: cmp, .. }, left, right) = expr.kind { + match cmp { + BinOpKind::Eq => { + check_cmp(cx, expr.span, left, right, "", 0); // len == 0 + check_cmp(cx, expr.span, right, left, "", 0); // 0 == len + }, + BinOpKind::Ne => { + check_cmp(cx, expr.span, left, right, "!", 0); // len != 0 + check_cmp(cx, expr.span, right, left, "!", 0); // 0 != len + }, + BinOpKind::Gt => { + check_cmp(cx, expr.span, left, right, "!", 0); // len > 0 + check_cmp(cx, expr.span, right, left, "", 1); // 1 > len + }, + BinOpKind::Lt => { + check_cmp(cx, expr.span, left, right, "", 1); // len < 1 + check_cmp(cx, expr.span, right, left, "!", 0); // 0 < len + }, + BinOpKind::Ge => check_cmp(cx, expr.span, left, right, "!", 1), // len >= 1 + BinOpKind::Le => check_cmp(cx, expr.span, right, left, "!", 1), // 1 <= len + _ => (), + } + } + } +} + +fn check_trait_items(cx: &LateContext<'_>, visited_trait: &Item<'_>, trait_items: &[TraitItemRef]) { + fn is_named_self(cx: &LateContext<'_>, item: &TraitItemRef, name: Symbol) -> bool { + item.ident.name == name + && if let AssocItemKind::Fn { has_self } = item.kind { + has_self && { cx.tcx.fn_sig(item.id.def_id).inputs().skip_binder().len() == 1 } + } else { + false + } + } + + // fill the set with current and super traits + fn fill_trait_set(traitt: DefId, set: &mut DefIdSet, cx: &LateContext<'_>) { + if set.insert(traitt) { + for supertrait in rustc_trait_selection::traits::supertrait_def_ids(cx.tcx, traitt) { + fill_trait_set(supertrait, set, cx); + } + } + } + + if cx.access_levels.is_exported(visited_trait.def_id) && trait_items.iter().any(|i| is_named_self(cx, i, sym::len)) + { + let mut current_and_super_traits = DefIdSet::default(); + fill_trait_set(visited_trait.def_id.to_def_id(), &mut current_and_super_traits, cx); + let is_empty = sym!(is_empty); + + let is_empty_method_found = current_and_super_traits + .iter() + .flat_map(|&i| cx.tcx.associated_items(i).filter_by_name_unhygienic(is_empty)) + .any(|i| { + i.kind == ty::AssocKind::Fn + && i.fn_has_self_parameter + && cx.tcx.fn_sig(i.def_id).inputs().skip_binder().len() == 1 + }); + + if !is_empty_method_found { + span_lint( + cx, + LEN_WITHOUT_IS_EMPTY, + visited_trait.span, + &format!( + "trait `{}` has a `len` method but no (possibly inherited) `is_empty` method", + visited_trait.ident.name + ), + ); + } + } +} + +#[derive(Debug, Clone, Copy)] +enum LenOutput<'tcx> { + Integral, + Option(DefId), + Result(DefId, Ty<'tcx>), +} +fn parse_len_output<'tcx>(cx: &LateContext<'_>, sig: FnSig<'tcx>) -> Option<LenOutput<'tcx>> { + match *sig.output().kind() { + ty::Int(_) | ty::Uint(_) => Some(LenOutput::Integral), + ty::Adt(adt, subs) if cx.tcx.is_diagnostic_item(sym::Option, adt.did()) => { + subs.type_at(0).is_integral().then(|| LenOutput::Option(adt.did())) + }, + ty::Adt(adt, subs) if cx.tcx.is_diagnostic_item(sym::Result, adt.did()) => subs + .type_at(0) + .is_integral() + .then(|| LenOutput::Result(adt.did(), subs.type_at(1))), + _ => None, + } +} + +impl<'tcx> LenOutput<'tcx> { + fn matches_is_empty_output(self, ty: Ty<'tcx>) -> bool { + match (self, ty.kind()) { + (_, &ty::Bool) => true, + (Self::Option(id), &ty::Adt(adt, subs)) if id == adt.did() => subs.type_at(0).is_bool(), + (Self::Result(id, err_ty), &ty::Adt(adt, subs)) if id == adt.did() => { + subs.type_at(0).is_bool() && subs.type_at(1) == err_ty + }, + _ => false, + } + } + + fn expected_sig(self, self_kind: ImplicitSelfKind) -> String { + let self_ref = match self_kind { + ImplicitSelfKind::ImmRef => "&", + ImplicitSelfKind::MutRef => "&mut ", + _ => "", + }; + match self { + Self::Integral => format!("expected signature: `({}self) -> bool`", self_ref), + Self::Option(_) => format!( + "expected signature: `({}self) -> bool` or `({}self) -> Option<bool>", + self_ref, self_ref + ), + Self::Result(..) => format!( + "expected signature: `({}self) -> bool` or `({}self) -> Result<bool>", + self_ref, self_ref + ), + } + } +} + +/// Checks if the given signature matches the expectations for `is_empty` +fn check_is_empty_sig<'tcx>(sig: FnSig<'tcx>, self_kind: ImplicitSelfKind, len_output: LenOutput<'tcx>) -> bool { + match &**sig.inputs_and_output { + [arg, res] if len_output.matches_is_empty_output(*res) => { + matches!( + (arg.kind(), self_kind), + (ty::Ref(_, _, Mutability::Not), ImplicitSelfKind::ImmRef) + | (ty::Ref(_, _, Mutability::Mut), ImplicitSelfKind::MutRef) + ) || (!arg.is_ref() && matches!(self_kind, ImplicitSelfKind::Imm | ImplicitSelfKind::Mut)) + }, + _ => false, + } +} + +/// Checks if the given type has an `is_empty` method with the appropriate signature. +fn check_for_is_empty<'tcx>( + cx: &LateContext<'tcx>, + span: Span, + self_kind: ImplicitSelfKind, + output: LenOutput<'tcx>, + impl_ty: DefId, + item_name: Symbol, + item_kind: &str, +) { + let is_empty = Symbol::intern("is_empty"); + let is_empty = cx + .tcx + .inherent_impls(impl_ty) + .iter() + .flat_map(|&id| cx.tcx.associated_items(id).filter_by_name_unhygienic(is_empty)) + .find(|item| item.kind == AssocKind::Fn); + + let (msg, is_empty_span, self_kind) = match is_empty { + None => ( + format!( + "{} `{}` has a public `len` method, but no `is_empty` method", + item_kind, + item_name.as_str(), + ), + None, + None, + ), + Some(is_empty) if !cx.access_levels.is_exported(is_empty.def_id.expect_local()) => ( + format!( + "{} `{}` has a public `len` method, but a private `is_empty` method", + item_kind, + item_name.as_str(), + ), + Some(cx.tcx.def_span(is_empty.def_id)), + None, + ), + Some(is_empty) + if !(is_empty.fn_has_self_parameter + && check_is_empty_sig(cx.tcx.fn_sig(is_empty.def_id).skip_binder(), self_kind, output)) => + { + ( + format!( + "{} `{}` has a public `len` method, but the `is_empty` method has an unexpected signature", + item_kind, + item_name.as_str(), + ), + Some(cx.tcx.def_span(is_empty.def_id)), + Some(self_kind), + ) + }, + Some(_) => return, + }; + + span_lint_and_then(cx, LEN_WITHOUT_IS_EMPTY, span, &msg, |db| { + if let Some(span) = is_empty_span { + db.span_note(span, "`is_empty` defined here"); + } + if let Some(self_kind) = self_kind { + db.note(&output.expected_sig(self_kind)); + } + }); +} + +fn check_cmp(cx: &LateContext<'_>, span: Span, method: &Expr<'_>, lit: &Expr<'_>, op: &str, compare_to: u32) { + if let (&ExprKind::MethodCall(method_path, args, _), &ExprKind::Lit(ref lit)) = (&method.kind, &lit.kind) { + // check if we are in an is_empty() method + if let Some(name) = get_item_name(cx, method) { + if name.as_str() == "is_empty" { + return; + } + } + + check_len(cx, span, method_path.ident.name, args, &lit.node, op, compare_to); + } else { + check_empty_expr(cx, span, method, lit, op); + } +} + +fn check_len( + cx: &LateContext<'_>, + span: Span, + method_name: Symbol, + args: &[Expr<'_>], + lit: &LitKind, + op: &str, + compare_to: u32, +) { + if let LitKind::Int(lit, _) = *lit { + // check if length is compared to the specified number + if lit != u128::from(compare_to) { + return; + } + + if method_name == sym::len && args.len() == 1 && has_is_empty(cx, &args[0]) { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + LEN_ZERO, + span, + &format!("length comparison to {}", if compare_to == 0 { "zero" } else { "one" }), + &format!("using `{}is_empty` is clearer and more explicit", op), + format!( + "{}{}.is_empty()", + op, + snippet_with_applicability(cx, args[0].span, "_", &mut applicability) + ), + applicability, + ); + } + } +} + +fn check_empty_expr(cx: &LateContext<'_>, span: Span, lit1: &Expr<'_>, lit2: &Expr<'_>, op: &str) { + if (is_empty_array(lit2) || is_empty_string(lit2)) && has_is_empty(cx, lit1) { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + COMPARISON_TO_EMPTY, + span, + "comparison to empty slice", + &format!("using `{}is_empty` is clearer and more explicit", op), + format!( + "{}{}.is_empty()", + op, + snippet_with_applicability(cx, lit1.span, "_", &mut applicability) + ), + applicability, + ); + } +} + +fn is_empty_string(expr: &Expr<'_>) -> bool { + if let ExprKind::Lit(ref lit) = expr.kind { + if let LitKind::Str(lit, _) = lit.node { + let lit = lit.as_str(); + return lit.is_empty(); + } + } + false +} + +fn is_empty_array(expr: &Expr<'_>) -> bool { + if let ExprKind::Array(arr) = expr.kind { + return arr.is_empty(); + } + false +} + +/// Checks if this type has an `is_empty` method. +fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + /// Gets an `AssocItem` and return true if it matches `is_empty(self)`. + fn is_is_empty(cx: &LateContext<'_>, item: &ty::AssocItem) -> bool { + if item.kind == ty::AssocKind::Fn { + let sig = cx.tcx.fn_sig(item.def_id); + let ty = sig.skip_binder(); + ty.inputs().len() == 1 + } else { + false + } + } + + /// Checks the inherent impl's items for an `is_empty(self)` method. + fn has_is_empty_impl(cx: &LateContext<'_>, id: DefId) -> bool { + let is_empty = sym!(is_empty); + cx.tcx.inherent_impls(id).iter().any(|imp| { + cx.tcx + .associated_items(*imp) + .filter_by_name_unhygienic(is_empty) + .any(|item| is_is_empty(cx, item)) + }) + } + + let ty = &cx.typeck_results().expr_ty(expr).peel_refs(); + match ty.kind() { + ty::Dynamic(tt, ..) => tt.principal().map_or(false, |principal| { + let is_empty = sym!(is_empty); + cx.tcx + .associated_items(principal.def_id()) + .filter_by_name_unhygienic(is_empty) + .any(|item| is_is_empty(cx, item)) + }), + ty::Projection(ref proj) => has_is_empty_impl(cx, proj.item_def_id), + ty::Adt(id, _) => has_is_empty_impl(cx, id.did()), + ty::Array(..) | ty::Slice(..) | ty::Str => true, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/let_if_seq.rs b/src/tools/clippy/clippy_lints/src/let_if_seq.rs new file mode 100644 index 000000000..56bbbbbc8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/let_if_seq.rs @@ -0,0 +1,160 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::{path_to_local_id, visitors::is_local_used}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::BindingAnnotation; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for variable declarations immediately followed by a + /// conditional affectation. + /// + /// ### Why is this bad? + /// This is not idiomatic Rust. + /// + /// ### Example + /// ```rust,ignore + /// let foo; + /// + /// if bar() { + /// foo = 42; + /// } else { + /// foo = 0; + /// } + /// + /// let mut baz = None; + /// + /// if bar() { + /// baz = Some(42); + /// } + /// ``` + /// + /// should be written + /// + /// ```rust,ignore + /// let foo = if bar() { + /// 42 + /// } else { + /// 0 + /// }; + /// + /// let baz = if bar() { + /// Some(42) + /// } else { + /// None + /// }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USELESS_LET_IF_SEQ, + nursery, + "unidiomatic `let mut` declaration followed by initialization in `if`" +} + +declare_lint_pass!(LetIfSeq => [USELESS_LET_IF_SEQ]); + +impl<'tcx> LateLintPass<'tcx> for LetIfSeq { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) { + let mut it = block.stmts.iter().peekable(); + while let Some(stmt) = it.next() { + if_chain! { + if let Some(expr) = it.peek(); + if let hir::StmtKind::Local(local) = stmt.kind; + if let hir::PatKind::Binding(mode, canonical_id, ident, None) = local.pat.kind; + if let hir::StmtKind::Expr(if_) = expr.kind; + if let hir::ExprKind::If(hir::Expr { kind: hir::ExprKind::DropTemps(cond), ..}, then, else_) = if_.kind; + if !is_local_used(cx, *cond, canonical_id); + if let hir::ExprKind::Block(then, _) = then.kind; + if let Some(value) = check_assign(cx, canonical_id, then); + if !is_local_used(cx, value, canonical_id); + then { + let span = stmt.span.to(if_.span); + + let has_interior_mutability = !cx.typeck_results().node_type(canonical_id).is_freeze( + cx.tcx.at(span), + cx.param_env, + ); + if has_interior_mutability { return; } + + let (default_multi_stmts, default) = if let Some(else_) = else_ { + if let hir::ExprKind::Block(else_, _) = else_.kind { + if let Some(default) = check_assign(cx, canonical_id, else_) { + (else_.stmts.len() > 1, default) + } else if let Some(default) = local.init { + (true, default) + } else { + continue; + } + } else { + continue; + } + } else if let Some(default) = local.init { + (false, default) + } else { + continue; + }; + + let mutability = match mode { + BindingAnnotation::RefMut | BindingAnnotation::Mutable => "<mut> ", + _ => "", + }; + + // FIXME: this should not suggest `mut` if we can detect that the variable is not + // use mutably after the `if` + + let sug = format!( + "let {mut}{name} = if {cond} {{{then} {value} }} else {{{else} {default} }};", + mut=mutability, + name=ident.name, + cond=snippet(cx, cond.span, "_"), + then=if then.stmts.len() > 1 { " ..;" } else { "" }, + else=if default_multi_stmts { " ..;" } else { "" }, + value=snippet(cx, value.span, "<value>"), + default=snippet(cx, default.span, "<default>"), + ); + span_lint_and_then(cx, + USELESS_LET_IF_SEQ, + span, + "`if _ { .. } else { .. }` is an expression", + |diag| { + diag.span_suggestion( + span, + "it is more idiomatic to write", + sug, + Applicability::HasPlaceholders, + ); + if !mutability.is_empty() { + diag.note("you might not need `mut` at all"); + } + }); + } + } + } + } +} + +fn check_assign<'tcx>( + cx: &LateContext<'tcx>, + decl: hir::HirId, + block: &'tcx hir::Block<'_>, +) -> Option<&'tcx hir::Expr<'tcx>> { + if_chain! { + if block.expr.is_none(); + if let Some(expr) = block.stmts.iter().last(); + if let hir::StmtKind::Semi(expr) = expr.kind; + if let hir::ExprKind::Assign(var, value, _) = expr.kind; + if path_to_local_id(var, decl); + then { + if block.stmts.iter().take(block.stmts.len()-1).any(|stmt| is_local_used(cx, stmt, decl)) { + None + } else { + Some(value) + } + } else { + None + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/let_underscore.rs b/src/tools/clippy/clippy_lints/src/let_underscore.rs new file mode 100644 index 000000000..176787497 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/let_underscore.rs @@ -0,0 +1,171 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::{is_must_use_ty, match_type}; +use clippy_utils::{is_must_use_func_call, paths}; +use if_chain::if_chain; +use rustc_hir::{Local, PatKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::subst::GenericArgKind; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `let _ = <expr>` where expr is `#[must_use]` + /// + /// ### Why is this bad? + /// It's better to explicitly handle the value of a `#[must_use]` + /// expr + /// + /// ### Example + /// ```rust + /// fn f() -> Result<u32, u32> { + /// Ok(0) + /// } + /// + /// let _ = f(); + /// // is_ok() is marked #[must_use] + /// let _ = f().is_ok(); + /// ``` + #[clippy::version = "1.42.0"] + pub LET_UNDERSCORE_MUST_USE, + restriction, + "non-binding let on a `#[must_use]` expression" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `let _ = sync_lock`. + /// This supports `mutex` and `rwlock` in `std::sync` and `parking_lot`. + /// + /// ### Why is this bad? + /// This statement immediately drops the lock instead of + /// extending its lifetime to the end of the scope, which is often not intended. + /// To extend lock lifetime to the end of the scope, use an underscore-prefixed + /// name instead (i.e. _lock). If you want to explicitly drop the lock, + /// `std::mem::drop` conveys your intention better and is less error-prone. + /// + /// ### Example + /// ```rust,ignore + /// let _ = mutex.lock(); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// let _lock = mutex.lock(); + /// ``` + #[clippy::version = "1.43.0"] + pub LET_UNDERSCORE_LOCK, + correctness, + "non-binding let on a synchronization lock" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `let _ = <expr>` + /// where expr has a type that implements `Drop` + /// + /// ### Why is this bad? + /// This statement immediately drops the initializer + /// expression instead of extending its lifetime to the end of the scope, which + /// is often not intended. To extend the expression's lifetime to the end of the + /// scope, use an underscore-prefixed name instead (i.e. _var). If you want to + /// explicitly drop the expression, `std::mem::drop` conveys your intention + /// better and is less error-prone. + /// + /// ### Example + /// ```rust + /// # struct DroppableItem; + /// { + /// let _ = DroppableItem; + /// // ^ dropped here + /// /* more code */ + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # struct DroppableItem; + /// { + /// let _droppable = DroppableItem; + /// /* more code */ + /// // dropped at end of scope + /// } + /// ``` + #[clippy::version = "1.50.0"] + pub LET_UNDERSCORE_DROP, + pedantic, + "non-binding let on a type that implements `Drop`" +} + +declare_lint_pass!(LetUnderscore => [LET_UNDERSCORE_MUST_USE, LET_UNDERSCORE_LOCK, LET_UNDERSCORE_DROP]); + +const SYNC_GUARD_PATHS: [&[&str]; 6] = [ + &paths::MUTEX_GUARD, + &paths::RWLOCK_READ_GUARD, + &paths::RWLOCK_WRITE_GUARD, + &paths::PARKING_LOT_MUTEX_GUARD, + &paths::PARKING_LOT_RWLOCK_READ_GUARD, + &paths::PARKING_LOT_RWLOCK_WRITE_GUARD, +]; + +impl<'tcx> LateLintPass<'tcx> for LetUnderscore { + fn check_local(&mut self, cx: &LateContext<'_>, local: &Local<'_>) { + if in_external_macro(cx.tcx.sess, local.span) { + return; + } + + if_chain! { + if let PatKind::Wild = local.pat.kind; + if let Some(init) = local.init; + then { + let init_ty = cx.typeck_results().expr_ty(init); + let contains_sync_guard = init_ty.walk().any(|inner| match inner.unpack() { + GenericArgKind::Type(inner_ty) => { + SYNC_GUARD_PATHS.iter().any(|path| match_type(cx, inner_ty, path)) + }, + + GenericArgKind::Lifetime(_) | GenericArgKind::Const(_) => false, + }); + if contains_sync_guard { + span_lint_and_help( + cx, + LET_UNDERSCORE_LOCK, + local.span, + "non-binding let on a synchronization lock", + None, + "consider using an underscore-prefixed named \ + binding or dropping explicitly with `std::mem::drop`" + ); + } else if init_ty.needs_drop(cx.tcx, cx.param_env) { + span_lint_and_help( + cx, + LET_UNDERSCORE_DROP, + local.span, + "non-binding `let` on a type that implements `Drop`", + None, + "consider using an underscore-prefixed named \ + binding or dropping explicitly with `std::mem::drop`" + ); + } else if is_must_use_ty(cx, cx.typeck_results().expr_ty(init)) { + span_lint_and_help( + cx, + LET_UNDERSCORE_MUST_USE, + local.span, + "non-binding let on an expression with `#[must_use]` type", + None, + "consider explicitly using expression value" + ); + } else if is_must_use_func_call(cx, init) { + span_lint_and_help( + cx, + LET_UNDERSCORE_MUST_USE, + local.span, + "non-binding let on a result of a `#[must_use]` function", + None, + "consider explicitly using function result" + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/lib.deprecated.rs b/src/tools/clippy/clippy_lints/src/lib.deprecated.rs new file mode 100644 index 000000000..80bde1b11 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.deprecated.rs @@ -0,0 +1,70 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +{ + store.register_removed( + "clippy::should_assert_eq", + "`assert!()` will be more flexible with RFC 2011", + ); + store.register_removed( + "clippy::extend_from_slice", + "`.extend_from_slice(_)` is a faster way to extend a Vec by a slice", + ); + store.register_removed( + "clippy::range_step_by_zero", + "`iterator.step_by(0)` panics nowadays", + ); + store.register_removed( + "clippy::unstable_as_slice", + "`Vec::as_slice` has been stabilized in 1.7", + ); + store.register_removed( + "clippy::unstable_as_mut_slice", + "`Vec::as_mut_slice` has been stabilized in 1.7", + ); + store.register_removed( + "clippy::misaligned_transmute", + "this lint has been split into cast_ptr_alignment and transmute_ptr_to_ptr", + ); + store.register_removed( + "clippy::assign_ops", + "using compound assignment operators (e.g., `+=`) is harmless", + ); + store.register_removed( + "clippy::if_let_redundant_pattern_matching", + "this lint has been changed to redundant_pattern_matching", + ); + store.register_removed( + "clippy::unsafe_vector_initialization", + "the replacement suggested by this lint had substantially different behavior", + ); + store.register_removed( + "clippy::unused_collect", + "`collect` has been marked as #[must_use] in rustc and that covers all cases of this lint", + ); + store.register_removed( + "clippy::replace_consts", + "associated-constants `MIN`/`MAX` of integers are preferred to `{min,max}_value()` and module constants", + ); + store.register_removed( + "clippy::regex_macro", + "the regex! macro has been removed from the regex crate in 2018", + ); + store.register_removed( + "clippy::find_map", + "this lint has been replaced by `manual_find_map`, a more specific lint", + ); + store.register_removed( + "clippy::filter_map", + "this lint has been replaced by `manual_filter_map`, a more specific lint", + ); + store.register_removed( + "clippy::pub_enum_variant_names", + "set the `avoid-breaking-exported-api` config option to `false` to enable the `enum_variant_names` lint for public items", + ); + store.register_removed( + "clippy::wrong_pub_self_convention", + "set the `avoid-breaking-exported-api` config option to `false` to enable the `wrong_self_convention` lint for public items", + ); +} diff --git a/src/tools/clippy/clippy_lints/src/lib.register_all.rs b/src/tools/clippy/clippy_lints/src/lib.register_all.rs new file mode 100644 index 000000000..763dd2a40 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_all.rs @@ -0,0 +1,352 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::all", Some("clippy_all"), vec![ + LintId::of(almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE), + LintId::of(approx_const::APPROX_CONSTANT), + LintId::of(assertions_on_constants::ASSERTIONS_ON_CONSTANTS), + LintId::of(async_yields_async::ASYNC_YIELDS_ASYNC), + LintId::of(attrs::BLANKET_CLIPPY_RESTRICTION_LINTS), + LintId::of(attrs::DEPRECATED_CFG_ATTR), + LintId::of(attrs::DEPRECATED_SEMVER), + LintId::of(attrs::MISMATCHED_TARGET_OS), + LintId::of(attrs::USELESS_ATTRIBUTE), + LintId::of(await_holding_invalid::AWAIT_HOLDING_INVALID_TYPE), + LintId::of(await_holding_invalid::AWAIT_HOLDING_LOCK), + LintId::of(await_holding_invalid::AWAIT_HOLDING_REFCELL_REF), + LintId::of(blacklisted_name::BLACKLISTED_NAME), + LintId::of(blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS), + LintId::of(bool_assert_comparison::BOOL_ASSERT_COMPARISON), + LintId::of(booleans::LOGIC_BUG), + LintId::of(booleans::NONMINIMAL_BOOL), + LintId::of(borrow_deref_ref::BORROW_DEREF_REF), + LintId::of(bytes_count_to_len::BYTES_COUNT_TO_LEN), + LintId::of(casts::CAST_ABS_TO_UNSIGNED), + LintId::of(casts::CAST_ENUM_CONSTRUCTOR), + LintId::of(casts::CAST_ENUM_TRUNCATION), + LintId::of(casts::CAST_REF_TO_MUT), + LintId::of(casts::CAST_SLICE_DIFFERENT_SIZES), + LintId::of(casts::CHAR_LIT_AS_U8), + LintId::of(casts::FN_TO_NUMERIC_CAST), + LintId::of(casts::FN_TO_NUMERIC_CAST_WITH_TRUNCATION), + LintId::of(casts::UNNECESSARY_CAST), + LintId::of(collapsible_if::COLLAPSIBLE_ELSE_IF), + LintId::of(collapsible_if::COLLAPSIBLE_IF), + LintId::of(comparison_chain::COMPARISON_CHAIN), + LintId::of(copies::IFS_SAME_COND), + LintId::of(copies::IF_SAME_THEN_ELSE), + LintId::of(crate_in_macro_def::CRATE_IN_MACRO_DEF), + LintId::of(default::FIELD_REASSIGN_WITH_DEFAULT), + LintId::of(default_instead_of_iter_empty::DEFAULT_INSTEAD_OF_ITER_EMPTY), + LintId::of(dereference::NEEDLESS_BORROW), + LintId::of(derivable_impls::DERIVABLE_IMPLS), + LintId::of(derive::DERIVE_HASH_XOR_EQ), + LintId::of(derive::DERIVE_ORD_XOR_PARTIAL_ORD), + LintId::of(derive::DERIVE_PARTIAL_EQ_WITHOUT_EQ), + LintId::of(disallowed_methods::DISALLOWED_METHODS), + LintId::of(disallowed_types::DISALLOWED_TYPES), + LintId::of(doc::MISSING_SAFETY_DOC), + LintId::of(doc::NEEDLESS_DOCTEST_MAIN), + LintId::of(double_parens::DOUBLE_PARENS), + LintId::of(drop_forget_ref::DROP_COPY), + LintId::of(drop_forget_ref::DROP_NON_DROP), + LintId::of(drop_forget_ref::DROP_REF), + LintId::of(drop_forget_ref::FORGET_COPY), + LintId::of(drop_forget_ref::FORGET_NON_DROP), + LintId::of(drop_forget_ref::FORGET_REF), + LintId::of(drop_forget_ref::UNDROPPED_MANUALLY_DROPS), + LintId::of(duplicate_mod::DUPLICATE_MOD), + LintId::of(entry::MAP_ENTRY), + LintId::of(enum_clike::ENUM_CLIKE_UNPORTABLE_VARIANT), + LintId::of(enum_variants::ENUM_VARIANT_NAMES), + LintId::of(enum_variants::MODULE_INCEPTION), + LintId::of(escape::BOXED_LOCAL), + LintId::of(eta_reduction::REDUNDANT_CLOSURE), + LintId::of(explicit_write::EXPLICIT_WRITE), + LintId::of(float_literal::EXCESSIVE_PRECISION), + LintId::of(format::USELESS_FORMAT), + LintId::of(format_args::FORMAT_IN_FORMAT_ARGS), + LintId::of(format_args::TO_STRING_IN_FORMAT_ARGS), + LintId::of(format_impl::PRINT_IN_FORMAT_IMPL), + LintId::of(format_impl::RECURSIVE_FORMAT_IMPL), + LintId::of(formatting::POSSIBLE_MISSING_COMMA), + LintId::of(formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING), + LintId::of(formatting::SUSPICIOUS_ELSE_FORMATTING), + LintId::of(formatting::SUSPICIOUS_UNARY_OP_FORMATTING), + LintId::of(from_over_into::FROM_OVER_INTO), + LintId::of(from_str_radix_10::FROM_STR_RADIX_10), + LintId::of(functions::DOUBLE_MUST_USE), + LintId::of(functions::MUST_USE_UNIT), + LintId::of(functions::NOT_UNSAFE_PTR_ARG_DEREF), + LintId::of(functions::RESULT_UNIT_ERR), + LintId::of(functions::TOO_MANY_ARGUMENTS), + LintId::of(get_first::GET_FIRST), + LintId::of(if_let_mutex::IF_LET_MUTEX), + LintId::of(indexing_slicing::OUT_OF_BOUNDS_INDEXING), + LintId::of(infinite_iter::INFINITE_ITER), + LintId::of(inherent_to_string::INHERENT_TO_STRING), + LintId::of(inherent_to_string::INHERENT_TO_STRING_SHADOW_DISPLAY), + LintId::of(init_numbered_fields::INIT_NUMBERED_FIELDS), + LintId::of(inline_fn_without_body::INLINE_FN_WITHOUT_BODY), + LintId::of(int_plus_one::INT_PLUS_ONE), + LintId::of(invalid_utf8_in_unchecked::INVALID_UTF8_IN_UNCHECKED), + LintId::of(large_const_arrays::LARGE_CONST_ARRAYS), + LintId::of(large_enum_variant::LARGE_ENUM_VARIANT), + LintId::of(len_zero::COMPARISON_TO_EMPTY), + LintId::of(len_zero::LEN_WITHOUT_IS_EMPTY), + LintId::of(len_zero::LEN_ZERO), + LintId::of(let_underscore::LET_UNDERSCORE_LOCK), + LintId::of(lifetimes::EXTRA_UNUSED_LIFETIMES), + LintId::of(lifetimes::NEEDLESS_LIFETIMES), + LintId::of(literal_representation::INCONSISTENT_DIGIT_GROUPING), + LintId::of(literal_representation::MISTYPED_LITERAL_SUFFIXES), + LintId::of(literal_representation::UNUSUAL_BYTE_GROUPINGS), + LintId::of(loops::EMPTY_LOOP), + LintId::of(loops::EXPLICIT_COUNTER_LOOP), + LintId::of(loops::FOR_KV_MAP), + LintId::of(loops::FOR_LOOPS_OVER_FALLIBLES), + LintId::of(loops::ITER_NEXT_LOOP), + LintId::of(loops::MANUAL_FIND), + LintId::of(loops::MANUAL_FLATTEN), + LintId::of(loops::MANUAL_MEMCPY), + LintId::of(loops::MISSING_SPIN_LOOP), + LintId::of(loops::MUT_RANGE_BOUND), + LintId::of(loops::NEEDLESS_COLLECT), + LintId::of(loops::NEEDLESS_RANGE_LOOP), + LintId::of(loops::NEVER_LOOP), + LintId::of(loops::SAME_ITEM_PUSH), + LintId::of(loops::SINGLE_ELEMENT_LOOP), + LintId::of(loops::WHILE_IMMUTABLE_CONDITION), + LintId::of(loops::WHILE_LET_LOOP), + LintId::of(loops::WHILE_LET_ON_ITERATOR), + LintId::of(main_recursion::MAIN_RECURSION), + LintId::of(manual_async_fn::MANUAL_ASYNC_FN), + LintId::of(manual_bits::MANUAL_BITS), + LintId::of(manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE), + LintId::of(manual_rem_euclid::MANUAL_REM_EUCLID), + LintId::of(manual_retain::MANUAL_RETAIN), + LintId::of(manual_strip::MANUAL_STRIP), + LintId::of(map_clone::MAP_CLONE), + LintId::of(map_unit_fn::OPTION_MAP_UNIT_FN), + LintId::of(map_unit_fn::RESULT_MAP_UNIT_FN), + LintId::of(match_result_ok::MATCH_RESULT_OK), + LintId::of(matches::COLLAPSIBLE_MATCH), + LintId::of(matches::INFALLIBLE_DESTRUCTURING_MATCH), + LintId::of(matches::MANUAL_MAP), + LintId::of(matches::MANUAL_UNWRAP_OR), + LintId::of(matches::MATCH_AS_REF), + LintId::of(matches::MATCH_LIKE_MATCHES_MACRO), + LintId::of(matches::MATCH_OVERLAPPING_ARM), + LintId::of(matches::MATCH_REF_PATS), + LintId::of(matches::MATCH_SINGLE_BINDING), + LintId::of(matches::MATCH_STR_CASE_MISMATCH), + LintId::of(matches::NEEDLESS_MATCH), + LintId::of(matches::REDUNDANT_PATTERN_MATCHING), + LintId::of(matches::SINGLE_MATCH), + LintId::of(matches::WILDCARD_IN_OR_PATTERNS), + LintId::of(mem_replace::MEM_REPLACE_OPTION_WITH_NONE), + LintId::of(mem_replace::MEM_REPLACE_WITH_DEFAULT), + LintId::of(mem_replace::MEM_REPLACE_WITH_UNINIT), + LintId::of(methods::BIND_INSTEAD_OF_MAP), + LintId::of(methods::BYTES_NTH), + LintId::of(methods::CHARS_LAST_CMP), + LintId::of(methods::CHARS_NEXT_CMP), + LintId::of(methods::CLONE_DOUBLE_REF), + LintId::of(methods::CLONE_ON_COPY), + LintId::of(methods::ERR_EXPECT), + LintId::of(methods::EXPECT_FUN_CALL), + LintId::of(methods::EXTEND_WITH_DRAIN), + LintId::of(methods::FILTER_MAP_IDENTITY), + LintId::of(methods::FILTER_NEXT), + LintId::of(methods::FLAT_MAP_IDENTITY), + LintId::of(methods::GET_LAST_WITH_LEN), + LintId::of(methods::INSPECT_FOR_EACH), + LintId::of(methods::INTO_ITER_ON_REF), + LintId::of(methods::IS_DIGIT_ASCII_RADIX), + LintId::of(methods::ITERATOR_STEP_BY_ZERO), + LintId::of(methods::ITER_CLONED_COLLECT), + LintId::of(methods::ITER_COUNT), + LintId::of(methods::ITER_NEXT_SLICE), + LintId::of(methods::ITER_NTH), + LintId::of(methods::ITER_NTH_ZERO), + LintId::of(methods::ITER_OVEREAGER_CLONED), + LintId::of(methods::ITER_SKIP_NEXT), + LintId::of(methods::MANUAL_FILTER_MAP), + LintId::of(methods::MANUAL_FIND_MAP), + LintId::of(methods::MANUAL_SATURATING_ARITHMETIC), + LintId::of(methods::MANUAL_SPLIT_ONCE), + LintId::of(methods::MANUAL_STR_REPEAT), + LintId::of(methods::MAP_COLLECT_RESULT_UNIT), + LintId::of(methods::MAP_FLATTEN), + LintId::of(methods::MAP_IDENTITY), + LintId::of(methods::NEEDLESS_OPTION_AS_DEREF), + LintId::of(methods::NEEDLESS_OPTION_TAKE), + LintId::of(methods::NEEDLESS_SPLITN), + LintId::of(methods::NEW_RET_NO_SELF), + LintId::of(methods::NO_EFFECT_REPLACE), + LintId::of(methods::OBFUSCATED_IF_ELSE), + LintId::of(methods::OK_EXPECT), + LintId::of(methods::OPTION_AS_REF_DEREF), + LintId::of(methods::OPTION_FILTER_MAP), + LintId::of(methods::OPTION_MAP_OR_NONE), + LintId::of(methods::OR_FUN_CALL), + LintId::of(methods::OR_THEN_UNWRAP), + LintId::of(methods::RESULT_MAP_OR_INTO_OPTION), + LintId::of(methods::SEARCH_IS_SOME), + LintId::of(methods::SHOULD_IMPLEMENT_TRAIT), + LintId::of(methods::SINGLE_CHAR_ADD_STR), + LintId::of(methods::SINGLE_CHAR_PATTERN), + LintId::of(methods::SKIP_WHILE_NEXT), + LintId::of(methods::STRING_EXTEND_CHARS), + LintId::of(methods::SUSPICIOUS_MAP), + LintId::of(methods::SUSPICIOUS_SPLITN), + LintId::of(methods::UNINIT_ASSUMED_INIT), + LintId::of(methods::UNNECESSARY_FILTER_MAP), + LintId::of(methods::UNNECESSARY_FIND_MAP), + LintId::of(methods::UNNECESSARY_FOLD), + LintId::of(methods::UNNECESSARY_LAZY_EVALUATIONS), + LintId::of(methods::UNNECESSARY_TO_OWNED), + LintId::of(methods::UNWRAP_OR_ELSE_DEFAULT), + LintId::of(methods::USELESS_ASREF), + LintId::of(methods::WRONG_SELF_CONVENTION), + LintId::of(methods::ZST_OFFSET), + LintId::of(minmax::MIN_MAX), + LintId::of(misc::SHORT_CIRCUIT_STATEMENT), + LintId::of(misc::TOPLEVEL_REF_ARG), + LintId::of(misc::ZERO_PTR), + LintId::of(misc_early::BUILTIN_TYPE_SHADOW), + LintId::of(misc_early::DOUBLE_NEG), + LintId::of(misc_early::DUPLICATE_UNDERSCORE_ARGUMENT), + LintId::of(misc_early::MIXED_CASE_HEX_LITERALS), + LintId::of(misc_early::REDUNDANT_PATTERN), + LintId::of(misc_early::UNNEEDED_WILDCARD_PATTERN), + LintId::of(misc_early::ZERO_PREFIXED_LITERAL), + LintId::of(mixed_read_write_in_expression::DIVERGING_SUB_EXPRESSION), + LintId::of(mut_key::MUTABLE_KEY_TYPE), + LintId::of(mut_mutex_lock::MUT_MUTEX_LOCK), + LintId::of(mut_reference::UNNECESSARY_MUT_PASSED), + LintId::of(needless_arbitrary_self_type::NEEDLESS_ARBITRARY_SELF_TYPE), + LintId::of(needless_bool::BOOL_COMPARISON), + LintId::of(needless_bool::NEEDLESS_BOOL), + LintId::of(needless_borrowed_ref::NEEDLESS_BORROWED_REFERENCE), + LintId::of(needless_late_init::NEEDLESS_LATE_INIT), + LintId::of(needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS), + LintId::of(needless_question_mark::NEEDLESS_QUESTION_MARK), + LintId::of(needless_update::NEEDLESS_UPDATE), + LintId::of(neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD), + LintId::of(neg_multiply::NEG_MULTIPLY), + LintId::of(new_without_default::NEW_WITHOUT_DEFAULT), + LintId::of(no_effect::NO_EFFECT), + LintId::of(no_effect::UNNECESSARY_OPERATION), + LintId::of(non_copy_const::BORROW_INTERIOR_MUTABLE_CONST), + LintId::of(non_copy_const::DECLARE_INTERIOR_MUTABLE_CONST), + LintId::of(non_expressive_names::JUST_UNDERSCORES_AND_DIGITS), + LintId::of(non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS), + LintId::of(octal_escapes::OCTAL_ESCAPES), + LintId::of(open_options::NONSENSICAL_OPEN_OPTIONS), + LintId::of(operators::ABSURD_EXTREME_COMPARISONS), + LintId::of(operators::ASSIGN_OP_PATTERN), + LintId::of(operators::BAD_BIT_MASK), + LintId::of(operators::CMP_NAN), + LintId::of(operators::CMP_OWNED), + LintId::of(operators::DOUBLE_COMPARISONS), + LintId::of(operators::DURATION_SUBSEC), + LintId::of(operators::EQ_OP), + LintId::of(operators::ERASING_OP), + LintId::of(operators::FLOAT_EQUALITY_WITHOUT_ABS), + LintId::of(operators::IDENTITY_OP), + LintId::of(operators::INEFFECTIVE_BIT_MASK), + LintId::of(operators::MISREFACTORED_ASSIGN_OP), + LintId::of(operators::MODULO_ONE), + LintId::of(operators::OP_REF), + LintId::of(operators::PTR_EQ), + LintId::of(operators::SELF_ASSIGNMENT), + LintId::of(option_env_unwrap::OPTION_ENV_UNWRAP), + LintId::of(overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL), + LintId::of(partialeq_ne_impl::PARTIALEQ_NE_IMPL), + LintId::of(precedence::PRECEDENCE), + LintId::of(ptr::CMP_NULL), + LintId::of(ptr::INVALID_NULL_PTR_USAGE), + LintId::of(ptr::MUT_FROM_REF), + LintId::of(ptr::PTR_ARG), + LintId::of(ptr_offset_with_cast::PTR_OFFSET_WITH_CAST), + LintId::of(question_mark::QUESTION_MARK), + LintId::of(ranges::MANUAL_RANGE_CONTAINS), + LintId::of(ranges::RANGE_ZIP_WITH_LEN), + LintId::of(ranges::REVERSED_EMPTY_RANGES), + LintId::of(rc_clone_in_vec_init::RC_CLONE_IN_VEC_INIT), + LintId::of(read_zero_byte_vec::READ_ZERO_BYTE_VEC), + LintId::of(redundant_clone::REDUNDANT_CLONE), + LintId::of(redundant_closure_call::REDUNDANT_CLOSURE_CALL), + LintId::of(redundant_field_names::REDUNDANT_FIELD_NAMES), + LintId::of(redundant_slicing::REDUNDANT_SLICING), + LintId::of(redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES), + LintId::of(reference::DEREF_ADDROF), + LintId::of(regex::INVALID_REGEX), + LintId::of(repeat_once::REPEAT_ONCE), + LintId::of(returns::LET_AND_RETURN), + LintId::of(returns::NEEDLESS_RETURN), + LintId::of(self_named_constructors::SELF_NAMED_CONSTRUCTORS), + LintId::of(serde_api::SERDE_API_MISUSE), + LintId::of(single_component_path_imports::SINGLE_COMPONENT_PATH_IMPORTS), + LintId::of(size_of_in_element_count::SIZE_OF_IN_ELEMENT_COUNT), + LintId::of(slow_vector_initialization::SLOW_VECTOR_INITIALIZATION), + LintId::of(strings::STRING_FROM_UTF8_AS_BYTES), + LintId::of(strings::TRIM_SPLIT_WHITESPACE), + LintId::of(strlen_on_c_strings::STRLEN_ON_C_STRINGS), + LintId::of(suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL), + LintId::of(suspicious_trait_impl::SUSPICIOUS_OP_ASSIGN_IMPL), + LintId::of(swap::ALMOST_SWAPPED), + LintId::of(swap::MANUAL_SWAP), + LintId::of(swap_ptr_to_ref::SWAP_PTR_TO_REF), + LintId::of(tabs_in_doc_comments::TABS_IN_DOC_COMMENTS), + LintId::of(temporary_assignment::TEMPORARY_ASSIGNMENT), + LintId::of(to_digit_is_some::TO_DIGIT_IS_SOME), + LintId::of(transmute::CROSSPOINTER_TRANSMUTE), + LintId::of(transmute::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS), + LintId::of(transmute::TRANSMUTE_BYTES_TO_STR), + LintId::of(transmute::TRANSMUTE_FLOAT_TO_INT), + LintId::of(transmute::TRANSMUTE_INT_TO_BOOL), + LintId::of(transmute::TRANSMUTE_INT_TO_CHAR), + LintId::of(transmute::TRANSMUTE_INT_TO_FLOAT), + LintId::of(transmute::TRANSMUTE_NUM_TO_BYTES), + LintId::of(transmute::TRANSMUTE_PTR_TO_REF), + LintId::of(transmute::UNSOUND_COLLECTION_TRANSMUTE), + LintId::of(transmute::USELESS_TRANSMUTE), + LintId::of(transmute::WRONG_TRANSMUTE), + LintId::of(transmuting_null::TRANSMUTING_NULL), + LintId::of(types::BORROWED_BOX), + LintId::of(types::BOX_COLLECTION), + LintId::of(types::REDUNDANT_ALLOCATION), + LintId::of(types::TYPE_COMPLEXITY), + LintId::of(types::VEC_BOX), + LintId::of(unicode::INVISIBLE_CHARACTERS), + LintId::of(uninit_vec::UNINIT_VEC), + LintId::of(unit_hash::UNIT_HASH), + LintId::of(unit_return_expecting_ord::UNIT_RETURN_EXPECTING_ORD), + LintId::of(unit_types::LET_UNIT_VALUE), + LintId::of(unit_types::UNIT_ARG), + LintId::of(unit_types::UNIT_CMP), + LintId::of(unnamed_address::FN_ADDRESS_COMPARISONS), + LintId::of(unnamed_address::VTABLE_ADDRESS_COMPARISONS), + LintId::of(unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS), + LintId::of(unnecessary_sort_by::UNNECESSARY_SORT_BY), + LintId::of(unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME), + LintId::of(unused_io_amount::UNUSED_IO_AMOUNT), + LintId::of(unused_unit::UNUSED_UNIT), + LintId::of(unwrap::PANICKING_UNWRAP), + LintId::of(unwrap::UNNECESSARY_UNWRAP), + LintId::of(upper_case_acronyms::UPPER_CASE_ACRONYMS), + LintId::of(useless_conversion::USELESS_CONVERSION), + LintId::of(vec::USELESS_VEC), + LintId::of(vec_init_then_push::VEC_INIT_THEN_PUSH), + LintId::of(vec_resize_to_zero::VEC_RESIZE_TO_ZERO), + LintId::of(write::PRINTLN_EMPTY_STRING), + LintId::of(write::PRINT_LITERAL), + LintId::of(write::PRINT_WITH_NEWLINE), + LintId::of(write::WRITELN_EMPTY_STRING), + LintId::of(write::WRITE_LITERAL), + LintId::of(write::WRITE_WITH_NEWLINE), + LintId::of(zero_div_zero::ZERO_DIVIDED_BY_ZERO), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_cargo.rs b/src/tools/clippy/clippy_lints/src/lib.register_cargo.rs new file mode 100644 index 000000000..c890523fe --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_cargo.rs @@ -0,0 +1,11 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::cargo", Some("clippy_cargo"), vec![ + LintId::of(cargo::CARGO_COMMON_METADATA), + LintId::of(cargo::MULTIPLE_CRATE_VERSIONS), + LintId::of(cargo::NEGATIVE_FEATURE_NAMES), + LintId::of(cargo::REDUNDANT_FEATURE_NAMES), + LintId::of(cargo::WILDCARD_DEPENDENCIES), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_complexity.rs b/src/tools/clippy/clippy_lints/src/lib.register_complexity.rs new file mode 100644 index 000000000..ed5446f58 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_complexity.rs @@ -0,0 +1,105 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::complexity", Some("clippy_complexity"), vec![ + LintId::of(attrs::DEPRECATED_CFG_ATTR), + LintId::of(booleans::NONMINIMAL_BOOL), + LintId::of(borrow_deref_ref::BORROW_DEREF_REF), + LintId::of(bytes_count_to_len::BYTES_COUNT_TO_LEN), + LintId::of(casts::CHAR_LIT_AS_U8), + LintId::of(casts::UNNECESSARY_CAST), + LintId::of(derivable_impls::DERIVABLE_IMPLS), + LintId::of(double_parens::DOUBLE_PARENS), + LintId::of(explicit_write::EXPLICIT_WRITE), + LintId::of(format::USELESS_FORMAT), + LintId::of(functions::TOO_MANY_ARGUMENTS), + LintId::of(int_plus_one::INT_PLUS_ONE), + LintId::of(lifetimes::EXTRA_UNUSED_LIFETIMES), + LintId::of(lifetimes::NEEDLESS_LIFETIMES), + LintId::of(loops::EXPLICIT_COUNTER_LOOP), + LintId::of(loops::MANUAL_FIND), + LintId::of(loops::MANUAL_FLATTEN), + LintId::of(loops::SINGLE_ELEMENT_LOOP), + LintId::of(loops::WHILE_LET_LOOP), + LintId::of(manual_rem_euclid::MANUAL_REM_EUCLID), + LintId::of(manual_strip::MANUAL_STRIP), + LintId::of(map_unit_fn::OPTION_MAP_UNIT_FN), + LintId::of(map_unit_fn::RESULT_MAP_UNIT_FN), + LintId::of(matches::MANUAL_UNWRAP_OR), + LintId::of(matches::MATCH_AS_REF), + LintId::of(matches::MATCH_SINGLE_BINDING), + LintId::of(matches::NEEDLESS_MATCH), + LintId::of(matches::WILDCARD_IN_OR_PATTERNS), + LintId::of(methods::BIND_INSTEAD_OF_MAP), + LintId::of(methods::CLONE_ON_COPY), + LintId::of(methods::FILTER_MAP_IDENTITY), + LintId::of(methods::FILTER_NEXT), + LintId::of(methods::FLAT_MAP_IDENTITY), + LintId::of(methods::GET_LAST_WITH_LEN), + LintId::of(methods::INSPECT_FOR_EACH), + LintId::of(methods::ITER_COUNT), + LintId::of(methods::MANUAL_FILTER_MAP), + LintId::of(methods::MANUAL_FIND_MAP), + LintId::of(methods::MANUAL_SPLIT_ONCE), + LintId::of(methods::MAP_FLATTEN), + LintId::of(methods::MAP_IDENTITY), + LintId::of(methods::NEEDLESS_OPTION_AS_DEREF), + LintId::of(methods::NEEDLESS_OPTION_TAKE), + LintId::of(methods::NEEDLESS_SPLITN), + LintId::of(methods::OPTION_AS_REF_DEREF), + LintId::of(methods::OPTION_FILTER_MAP), + LintId::of(methods::OR_THEN_UNWRAP), + LintId::of(methods::SEARCH_IS_SOME), + LintId::of(methods::SKIP_WHILE_NEXT), + LintId::of(methods::UNNECESSARY_FILTER_MAP), + LintId::of(methods::UNNECESSARY_FIND_MAP), + LintId::of(methods::USELESS_ASREF), + LintId::of(misc::SHORT_CIRCUIT_STATEMENT), + LintId::of(misc_early::UNNEEDED_WILDCARD_PATTERN), + LintId::of(misc_early::ZERO_PREFIXED_LITERAL), + LintId::of(mixed_read_write_in_expression::DIVERGING_SUB_EXPRESSION), + LintId::of(needless_arbitrary_self_type::NEEDLESS_ARBITRARY_SELF_TYPE), + LintId::of(needless_bool::BOOL_COMPARISON), + LintId::of(needless_bool::NEEDLESS_BOOL), + LintId::of(needless_borrowed_ref::NEEDLESS_BORROWED_REFERENCE), + LintId::of(needless_question_mark::NEEDLESS_QUESTION_MARK), + LintId::of(needless_update::NEEDLESS_UPDATE), + LintId::of(neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD), + LintId::of(no_effect::NO_EFFECT), + LintId::of(no_effect::UNNECESSARY_OPERATION), + LintId::of(operators::DOUBLE_COMPARISONS), + LintId::of(operators::DURATION_SUBSEC), + LintId::of(operators::IDENTITY_OP), + LintId::of(overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL), + LintId::of(partialeq_ne_impl::PARTIALEQ_NE_IMPL), + LintId::of(precedence::PRECEDENCE), + LintId::of(ptr_offset_with_cast::PTR_OFFSET_WITH_CAST), + LintId::of(ranges::RANGE_ZIP_WITH_LEN), + LintId::of(redundant_closure_call::REDUNDANT_CLOSURE_CALL), + LintId::of(redundant_slicing::REDUNDANT_SLICING), + LintId::of(reference::DEREF_ADDROF), + LintId::of(repeat_once::REPEAT_ONCE), + LintId::of(strings::STRING_FROM_UTF8_AS_BYTES), + LintId::of(strlen_on_c_strings::STRLEN_ON_C_STRINGS), + LintId::of(swap::MANUAL_SWAP), + LintId::of(temporary_assignment::TEMPORARY_ASSIGNMENT), + LintId::of(transmute::CROSSPOINTER_TRANSMUTE), + LintId::of(transmute::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS), + LintId::of(transmute::TRANSMUTE_BYTES_TO_STR), + LintId::of(transmute::TRANSMUTE_FLOAT_TO_INT), + LintId::of(transmute::TRANSMUTE_INT_TO_BOOL), + LintId::of(transmute::TRANSMUTE_INT_TO_CHAR), + LintId::of(transmute::TRANSMUTE_INT_TO_FLOAT), + LintId::of(transmute::TRANSMUTE_NUM_TO_BYTES), + LintId::of(transmute::TRANSMUTE_PTR_TO_REF), + LintId::of(transmute::USELESS_TRANSMUTE), + LintId::of(types::BORROWED_BOX), + LintId::of(types::TYPE_COMPLEXITY), + LintId::of(types::VEC_BOX), + LintId::of(unit_types::UNIT_ARG), + LintId::of(unnecessary_sort_by::UNNECESSARY_SORT_BY), + LintId::of(unwrap::UNNECESSARY_UNWRAP), + LintId::of(useless_conversion::USELESS_CONVERSION), + LintId::of(zero_div_zero::ZERO_DIVIDED_BY_ZERO), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_correctness.rs b/src/tools/clippy/clippy_lints/src/lib.register_correctness.rs new file mode 100644 index 000000000..9975859c5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_correctness.rs @@ -0,0 +1,78 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::correctness", Some("clippy_correctness"), vec![ + LintId::of(approx_const::APPROX_CONSTANT), + LintId::of(async_yields_async::ASYNC_YIELDS_ASYNC), + LintId::of(attrs::DEPRECATED_SEMVER), + LintId::of(attrs::MISMATCHED_TARGET_OS), + LintId::of(attrs::USELESS_ATTRIBUTE), + LintId::of(booleans::LOGIC_BUG), + LintId::of(casts::CAST_REF_TO_MUT), + LintId::of(casts::CAST_SLICE_DIFFERENT_SIZES), + LintId::of(copies::IFS_SAME_COND), + LintId::of(copies::IF_SAME_THEN_ELSE), + LintId::of(derive::DERIVE_HASH_XOR_EQ), + LintId::of(derive::DERIVE_ORD_XOR_PARTIAL_ORD), + LintId::of(drop_forget_ref::DROP_COPY), + LintId::of(drop_forget_ref::DROP_REF), + LintId::of(drop_forget_ref::FORGET_COPY), + LintId::of(drop_forget_ref::FORGET_REF), + LintId::of(drop_forget_ref::UNDROPPED_MANUALLY_DROPS), + LintId::of(enum_clike::ENUM_CLIKE_UNPORTABLE_VARIANT), + LintId::of(format_impl::RECURSIVE_FORMAT_IMPL), + LintId::of(formatting::POSSIBLE_MISSING_COMMA), + LintId::of(functions::NOT_UNSAFE_PTR_ARG_DEREF), + LintId::of(if_let_mutex::IF_LET_MUTEX), + LintId::of(indexing_slicing::OUT_OF_BOUNDS_INDEXING), + LintId::of(infinite_iter::INFINITE_ITER), + LintId::of(inherent_to_string::INHERENT_TO_STRING_SHADOW_DISPLAY), + LintId::of(inline_fn_without_body::INLINE_FN_WITHOUT_BODY), + LintId::of(invalid_utf8_in_unchecked::INVALID_UTF8_IN_UNCHECKED), + LintId::of(let_underscore::LET_UNDERSCORE_LOCK), + LintId::of(literal_representation::MISTYPED_LITERAL_SUFFIXES), + LintId::of(loops::ITER_NEXT_LOOP), + LintId::of(loops::NEVER_LOOP), + LintId::of(loops::WHILE_IMMUTABLE_CONDITION), + LintId::of(matches::MATCH_STR_CASE_MISMATCH), + LintId::of(mem_replace::MEM_REPLACE_WITH_UNINIT), + LintId::of(methods::CLONE_DOUBLE_REF), + LintId::of(methods::ITERATOR_STEP_BY_ZERO), + LintId::of(methods::SUSPICIOUS_SPLITN), + LintId::of(methods::UNINIT_ASSUMED_INIT), + LintId::of(methods::ZST_OFFSET), + LintId::of(minmax::MIN_MAX), + LintId::of(non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS), + LintId::of(open_options::NONSENSICAL_OPEN_OPTIONS), + LintId::of(operators::ABSURD_EXTREME_COMPARISONS), + LintId::of(operators::BAD_BIT_MASK), + LintId::of(operators::CMP_NAN), + LintId::of(operators::EQ_OP), + LintId::of(operators::ERASING_OP), + LintId::of(operators::INEFFECTIVE_BIT_MASK), + LintId::of(operators::MODULO_ONE), + LintId::of(operators::SELF_ASSIGNMENT), + LintId::of(option_env_unwrap::OPTION_ENV_UNWRAP), + LintId::of(ptr::INVALID_NULL_PTR_USAGE), + LintId::of(ptr::MUT_FROM_REF), + LintId::of(ranges::REVERSED_EMPTY_RANGES), + LintId::of(read_zero_byte_vec::READ_ZERO_BYTE_VEC), + LintId::of(regex::INVALID_REGEX), + LintId::of(serde_api::SERDE_API_MISUSE), + LintId::of(size_of_in_element_count::SIZE_OF_IN_ELEMENT_COUNT), + LintId::of(swap::ALMOST_SWAPPED), + LintId::of(transmute::UNSOUND_COLLECTION_TRANSMUTE), + LintId::of(transmute::WRONG_TRANSMUTE), + LintId::of(transmuting_null::TRANSMUTING_NULL), + LintId::of(unicode::INVISIBLE_CHARACTERS), + LintId::of(uninit_vec::UNINIT_VEC), + LintId::of(unit_hash::UNIT_HASH), + LintId::of(unit_return_expecting_ord::UNIT_RETURN_EXPECTING_ORD), + LintId::of(unit_types::UNIT_CMP), + LintId::of(unnamed_address::FN_ADDRESS_COMPARISONS), + LintId::of(unnamed_address::VTABLE_ADDRESS_COMPARISONS), + LintId::of(unused_io_amount::UNUSED_IO_AMOUNT), + LintId::of(unwrap::PANICKING_UNWRAP), + LintId::of(vec_resize_to_zero::VEC_RESIZE_TO_ZERO), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_internal.rs b/src/tools/clippy/clippy_lints/src/lib.register_internal.rs new file mode 100644 index 000000000..be63646a1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_internal.rs @@ -0,0 +1,22 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::internal", Some("clippy_internal"), vec![ + LintId::of(utils::internal_lints::CLIPPY_LINTS_INTERNAL), + LintId::of(utils::internal_lints::COLLAPSIBLE_SPAN_LINT_CALLS), + LintId::of(utils::internal_lints::COMPILER_LINT_FUNCTIONS), + LintId::of(utils::internal_lints::DEFAULT_DEPRECATION_REASON), + LintId::of(utils::internal_lints::DEFAULT_LINT), + LintId::of(utils::internal_lints::IF_CHAIN_STYLE), + LintId::of(utils::internal_lints::INTERNING_DEFINED_SYMBOL), + LintId::of(utils::internal_lints::INVALID_CLIPPY_VERSION_ATTRIBUTE), + LintId::of(utils::internal_lints::INVALID_PATHS), + LintId::of(utils::internal_lints::LINT_WITHOUT_LINT_PASS), + LintId::of(utils::internal_lints::MATCH_TYPE_ON_DIAGNOSTIC_ITEM), + LintId::of(utils::internal_lints::MISSING_CLIPPY_VERSION_ATTRIBUTE), + LintId::of(utils::internal_lints::MISSING_MSRV_ATTR_IMPL), + LintId::of(utils::internal_lints::OUTER_EXPN_EXPN_DATA), + LintId::of(utils::internal_lints::PRODUCE_ICE), + LintId::of(utils::internal_lints::UNNECESSARY_SYMBOL_STR), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_lints.rs b/src/tools/clippy/clippy_lints/src/lib.register_lints.rs new file mode 100644 index 000000000..99bde35cf --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_lints.rs @@ -0,0 +1,597 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_lints(&[ + #[cfg(feature = "internal")] + utils::internal_lints::CLIPPY_LINTS_INTERNAL, + #[cfg(feature = "internal")] + utils::internal_lints::COLLAPSIBLE_SPAN_LINT_CALLS, + #[cfg(feature = "internal")] + utils::internal_lints::COMPILER_LINT_FUNCTIONS, + #[cfg(feature = "internal")] + utils::internal_lints::DEFAULT_DEPRECATION_REASON, + #[cfg(feature = "internal")] + utils::internal_lints::DEFAULT_LINT, + #[cfg(feature = "internal")] + utils::internal_lints::IF_CHAIN_STYLE, + #[cfg(feature = "internal")] + utils::internal_lints::INTERNING_DEFINED_SYMBOL, + #[cfg(feature = "internal")] + utils::internal_lints::INVALID_CLIPPY_VERSION_ATTRIBUTE, + #[cfg(feature = "internal")] + utils::internal_lints::INVALID_PATHS, + #[cfg(feature = "internal")] + utils::internal_lints::LINT_WITHOUT_LINT_PASS, + #[cfg(feature = "internal")] + utils::internal_lints::MATCH_TYPE_ON_DIAGNOSTIC_ITEM, + #[cfg(feature = "internal")] + utils::internal_lints::MISSING_CLIPPY_VERSION_ATTRIBUTE, + #[cfg(feature = "internal")] + utils::internal_lints::MISSING_MSRV_ATTR_IMPL, + #[cfg(feature = "internal")] + utils::internal_lints::OUTER_EXPN_EXPN_DATA, + #[cfg(feature = "internal")] + utils::internal_lints::PRODUCE_ICE, + #[cfg(feature = "internal")] + utils::internal_lints::UNNECESSARY_SYMBOL_STR, + almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE, + approx_const::APPROX_CONSTANT, + as_conversions::AS_CONVERSIONS, + as_underscore::AS_UNDERSCORE, + asm_syntax::INLINE_ASM_X86_ATT_SYNTAX, + asm_syntax::INLINE_ASM_X86_INTEL_SYNTAX, + assertions_on_constants::ASSERTIONS_ON_CONSTANTS, + assertions_on_result_states::ASSERTIONS_ON_RESULT_STATES, + async_yields_async::ASYNC_YIELDS_ASYNC, + attrs::ALLOW_ATTRIBUTES_WITHOUT_REASON, + attrs::BLANKET_CLIPPY_RESTRICTION_LINTS, + attrs::DEPRECATED_CFG_ATTR, + attrs::DEPRECATED_SEMVER, + attrs::EMPTY_LINE_AFTER_OUTER_ATTR, + attrs::INLINE_ALWAYS, + attrs::MISMATCHED_TARGET_OS, + attrs::USELESS_ATTRIBUTE, + await_holding_invalid::AWAIT_HOLDING_INVALID_TYPE, + await_holding_invalid::AWAIT_HOLDING_LOCK, + await_holding_invalid::AWAIT_HOLDING_REFCELL_REF, + blacklisted_name::BLACKLISTED_NAME, + blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS, + bool_assert_comparison::BOOL_ASSERT_COMPARISON, + booleans::LOGIC_BUG, + booleans::NONMINIMAL_BOOL, + borrow_as_ptr::BORROW_AS_PTR, + borrow_deref_ref::BORROW_DEREF_REF, + bytecount::NAIVE_BYTECOUNT, + bytes_count_to_len::BYTES_COUNT_TO_LEN, + cargo::CARGO_COMMON_METADATA, + cargo::MULTIPLE_CRATE_VERSIONS, + cargo::NEGATIVE_FEATURE_NAMES, + cargo::REDUNDANT_FEATURE_NAMES, + cargo::WILDCARD_DEPENDENCIES, + case_sensitive_file_extension_comparisons::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS, + casts::CAST_ABS_TO_UNSIGNED, + casts::CAST_ENUM_CONSTRUCTOR, + casts::CAST_ENUM_TRUNCATION, + casts::CAST_LOSSLESS, + casts::CAST_POSSIBLE_TRUNCATION, + casts::CAST_POSSIBLE_WRAP, + casts::CAST_PRECISION_LOSS, + casts::CAST_PTR_ALIGNMENT, + casts::CAST_REF_TO_MUT, + casts::CAST_SIGN_LOSS, + casts::CAST_SLICE_DIFFERENT_SIZES, + casts::CHAR_LIT_AS_U8, + casts::FN_TO_NUMERIC_CAST, + casts::FN_TO_NUMERIC_CAST_ANY, + casts::FN_TO_NUMERIC_CAST_WITH_TRUNCATION, + casts::PTR_AS_PTR, + casts::UNNECESSARY_CAST, + checked_conversions::CHECKED_CONVERSIONS, + cognitive_complexity::COGNITIVE_COMPLEXITY, + collapsible_if::COLLAPSIBLE_ELSE_IF, + collapsible_if::COLLAPSIBLE_IF, + comparison_chain::COMPARISON_CHAIN, + copies::BRANCHES_SHARING_CODE, + copies::IFS_SAME_COND, + copies::IF_SAME_THEN_ELSE, + copies::SAME_FUNCTIONS_IN_IF_CONDITION, + copy_iterator::COPY_ITERATOR, + crate_in_macro_def::CRATE_IN_MACRO_DEF, + create_dir::CREATE_DIR, + dbg_macro::DBG_MACRO, + default::DEFAULT_TRAIT_ACCESS, + default::FIELD_REASSIGN_WITH_DEFAULT, + default_instead_of_iter_empty::DEFAULT_INSTEAD_OF_ITER_EMPTY, + default_numeric_fallback::DEFAULT_NUMERIC_FALLBACK, + default_union_representation::DEFAULT_UNION_REPRESENTATION, + dereference::EXPLICIT_AUTO_DEREF, + dereference::EXPLICIT_DEREF_METHODS, + dereference::NEEDLESS_BORROW, + dereference::REF_BINDING_TO_REFERENCE, + derivable_impls::DERIVABLE_IMPLS, + derive::DERIVE_HASH_XOR_EQ, + derive::DERIVE_ORD_XOR_PARTIAL_ORD, + derive::DERIVE_PARTIAL_EQ_WITHOUT_EQ, + derive::EXPL_IMPL_CLONE_ON_COPY, + derive::UNSAFE_DERIVE_DESERIALIZE, + disallowed_methods::DISALLOWED_METHODS, + disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS, + disallowed_types::DISALLOWED_TYPES, + doc::DOC_MARKDOWN, + doc::MISSING_ERRORS_DOC, + doc::MISSING_PANICS_DOC, + doc::MISSING_SAFETY_DOC, + doc::NEEDLESS_DOCTEST_MAIN, + doc_link_with_quotes::DOC_LINK_WITH_QUOTES, + double_parens::DOUBLE_PARENS, + drop_forget_ref::DROP_COPY, + drop_forget_ref::DROP_NON_DROP, + drop_forget_ref::DROP_REF, + drop_forget_ref::FORGET_COPY, + drop_forget_ref::FORGET_NON_DROP, + drop_forget_ref::FORGET_REF, + drop_forget_ref::UNDROPPED_MANUALLY_DROPS, + duplicate_mod::DUPLICATE_MOD, + else_if_without_else::ELSE_IF_WITHOUT_ELSE, + empty_drop::EMPTY_DROP, + empty_enum::EMPTY_ENUM, + empty_structs_with_brackets::EMPTY_STRUCTS_WITH_BRACKETS, + entry::MAP_ENTRY, + enum_clike::ENUM_CLIKE_UNPORTABLE_VARIANT, + enum_variants::ENUM_VARIANT_NAMES, + enum_variants::MODULE_INCEPTION, + enum_variants::MODULE_NAME_REPETITIONS, + equatable_if_let::EQUATABLE_IF_LET, + escape::BOXED_LOCAL, + eta_reduction::REDUNDANT_CLOSURE, + eta_reduction::REDUNDANT_CLOSURE_FOR_METHOD_CALLS, + excessive_bools::FN_PARAMS_EXCESSIVE_BOOLS, + excessive_bools::STRUCT_EXCESSIVE_BOOLS, + exhaustive_items::EXHAUSTIVE_ENUMS, + exhaustive_items::EXHAUSTIVE_STRUCTS, + exit::EXIT, + explicit_write::EXPLICIT_WRITE, + fallible_impl_from::FALLIBLE_IMPL_FROM, + float_literal::EXCESSIVE_PRECISION, + float_literal::LOSSY_FLOAT_LITERAL, + floating_point_arithmetic::IMPRECISE_FLOPS, + floating_point_arithmetic::SUBOPTIMAL_FLOPS, + format::USELESS_FORMAT, + format_args::FORMAT_IN_FORMAT_ARGS, + format_args::TO_STRING_IN_FORMAT_ARGS, + format_impl::PRINT_IN_FORMAT_IMPL, + format_impl::RECURSIVE_FORMAT_IMPL, + format_push_string::FORMAT_PUSH_STRING, + formatting::POSSIBLE_MISSING_COMMA, + formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING, + formatting::SUSPICIOUS_ELSE_FORMATTING, + formatting::SUSPICIOUS_UNARY_OP_FORMATTING, + from_over_into::FROM_OVER_INTO, + from_str_radix_10::FROM_STR_RADIX_10, + functions::DOUBLE_MUST_USE, + functions::MUST_USE_CANDIDATE, + functions::MUST_USE_UNIT, + functions::NOT_UNSAFE_PTR_ARG_DEREF, + functions::RESULT_UNIT_ERR, + functions::TOO_MANY_ARGUMENTS, + functions::TOO_MANY_LINES, + future_not_send::FUTURE_NOT_SEND, + get_first::GET_FIRST, + if_let_mutex::IF_LET_MUTEX, + if_not_else::IF_NOT_ELSE, + if_then_some_else_none::IF_THEN_SOME_ELSE_NONE, + implicit_hasher::IMPLICIT_HASHER, + implicit_return::IMPLICIT_RETURN, + implicit_saturating_sub::IMPLICIT_SATURATING_SUB, + inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR, + index_refutable_slice::INDEX_REFUTABLE_SLICE, + indexing_slicing::INDEXING_SLICING, + indexing_slicing::OUT_OF_BOUNDS_INDEXING, + infinite_iter::INFINITE_ITER, + infinite_iter::MAYBE_INFINITE_ITER, + inherent_impl::MULTIPLE_INHERENT_IMPL, + inherent_to_string::INHERENT_TO_STRING, + inherent_to_string::INHERENT_TO_STRING_SHADOW_DISPLAY, + init_numbered_fields::INIT_NUMBERED_FIELDS, + inline_fn_without_body::INLINE_FN_WITHOUT_BODY, + int_plus_one::INT_PLUS_ONE, + invalid_upcast_comparisons::INVALID_UPCAST_COMPARISONS, + invalid_utf8_in_unchecked::INVALID_UTF8_IN_UNCHECKED, + items_after_statements::ITEMS_AFTER_STATEMENTS, + iter_not_returning_iterator::ITER_NOT_RETURNING_ITERATOR, + large_const_arrays::LARGE_CONST_ARRAYS, + large_enum_variant::LARGE_ENUM_VARIANT, + large_include_file::LARGE_INCLUDE_FILE, + large_stack_arrays::LARGE_STACK_ARRAYS, + len_zero::COMPARISON_TO_EMPTY, + len_zero::LEN_WITHOUT_IS_EMPTY, + len_zero::LEN_ZERO, + let_if_seq::USELESS_LET_IF_SEQ, + let_underscore::LET_UNDERSCORE_DROP, + let_underscore::LET_UNDERSCORE_LOCK, + let_underscore::LET_UNDERSCORE_MUST_USE, + lifetimes::EXTRA_UNUSED_LIFETIMES, + lifetimes::NEEDLESS_LIFETIMES, + literal_representation::DECIMAL_LITERAL_REPRESENTATION, + literal_representation::INCONSISTENT_DIGIT_GROUPING, + literal_representation::LARGE_DIGIT_GROUPS, + literal_representation::MISTYPED_LITERAL_SUFFIXES, + literal_representation::UNREADABLE_LITERAL, + literal_representation::UNUSUAL_BYTE_GROUPINGS, + loops::EMPTY_LOOP, + loops::EXPLICIT_COUNTER_LOOP, + loops::EXPLICIT_INTO_ITER_LOOP, + loops::EXPLICIT_ITER_LOOP, + loops::FOR_KV_MAP, + loops::FOR_LOOPS_OVER_FALLIBLES, + loops::ITER_NEXT_LOOP, + loops::MANUAL_FIND, + loops::MANUAL_FLATTEN, + loops::MANUAL_MEMCPY, + loops::MISSING_SPIN_LOOP, + loops::MUT_RANGE_BOUND, + loops::NEEDLESS_COLLECT, + loops::NEEDLESS_RANGE_LOOP, + loops::NEVER_LOOP, + loops::SAME_ITEM_PUSH, + loops::SINGLE_ELEMENT_LOOP, + loops::WHILE_IMMUTABLE_CONDITION, + loops::WHILE_LET_LOOP, + loops::WHILE_LET_ON_ITERATOR, + macro_use::MACRO_USE_IMPORTS, + main_recursion::MAIN_RECURSION, + manual_assert::MANUAL_ASSERT, + manual_async_fn::MANUAL_ASYNC_FN, + manual_bits::MANUAL_BITS, + manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE, + manual_ok_or::MANUAL_OK_OR, + manual_rem_euclid::MANUAL_REM_EUCLID, + manual_retain::MANUAL_RETAIN, + manual_strip::MANUAL_STRIP, + map_clone::MAP_CLONE, + map_err_ignore::MAP_ERR_IGNORE, + map_unit_fn::OPTION_MAP_UNIT_FN, + map_unit_fn::RESULT_MAP_UNIT_FN, + match_result_ok::MATCH_RESULT_OK, + matches::COLLAPSIBLE_MATCH, + matches::INFALLIBLE_DESTRUCTURING_MATCH, + matches::MANUAL_MAP, + matches::MANUAL_UNWRAP_OR, + matches::MATCH_AS_REF, + matches::MATCH_BOOL, + matches::MATCH_LIKE_MATCHES_MACRO, + matches::MATCH_ON_VEC_ITEMS, + matches::MATCH_OVERLAPPING_ARM, + matches::MATCH_REF_PATS, + matches::MATCH_SAME_ARMS, + matches::MATCH_SINGLE_BINDING, + matches::MATCH_STR_CASE_MISMATCH, + matches::MATCH_WILDCARD_FOR_SINGLE_VARIANTS, + matches::MATCH_WILD_ERR_ARM, + matches::NEEDLESS_MATCH, + matches::REDUNDANT_PATTERN_MATCHING, + matches::REST_PAT_IN_FULLY_BOUND_STRUCTS, + matches::SIGNIFICANT_DROP_IN_SCRUTINEE, + matches::SINGLE_MATCH, + matches::SINGLE_MATCH_ELSE, + matches::TRY_ERR, + matches::WILDCARD_ENUM_MATCH_ARM, + matches::WILDCARD_IN_OR_PATTERNS, + mem_forget::MEM_FORGET, + mem_replace::MEM_REPLACE_OPTION_WITH_NONE, + mem_replace::MEM_REPLACE_WITH_DEFAULT, + mem_replace::MEM_REPLACE_WITH_UNINIT, + methods::BIND_INSTEAD_OF_MAP, + methods::BYTES_NTH, + methods::CHARS_LAST_CMP, + methods::CHARS_NEXT_CMP, + methods::CLONED_INSTEAD_OF_COPIED, + methods::CLONE_DOUBLE_REF, + methods::CLONE_ON_COPY, + methods::CLONE_ON_REF_PTR, + methods::ERR_EXPECT, + methods::EXPECT_FUN_CALL, + methods::EXPECT_USED, + methods::EXTEND_WITH_DRAIN, + methods::FILETYPE_IS_FILE, + methods::FILTER_MAP_IDENTITY, + methods::FILTER_MAP_NEXT, + methods::FILTER_NEXT, + methods::FLAT_MAP_IDENTITY, + methods::FLAT_MAP_OPTION, + methods::FROM_ITER_INSTEAD_OF_COLLECT, + methods::GET_LAST_WITH_LEN, + methods::GET_UNWRAP, + methods::IMPLICIT_CLONE, + methods::INEFFICIENT_TO_STRING, + methods::INSPECT_FOR_EACH, + methods::INTO_ITER_ON_REF, + methods::IS_DIGIT_ASCII_RADIX, + methods::ITERATOR_STEP_BY_ZERO, + methods::ITER_CLONED_COLLECT, + methods::ITER_COUNT, + methods::ITER_NEXT_SLICE, + methods::ITER_NTH, + methods::ITER_NTH_ZERO, + methods::ITER_OVEREAGER_CLONED, + methods::ITER_SKIP_NEXT, + methods::ITER_WITH_DRAIN, + methods::MANUAL_FILTER_MAP, + methods::MANUAL_FIND_MAP, + methods::MANUAL_SATURATING_ARITHMETIC, + methods::MANUAL_SPLIT_ONCE, + methods::MANUAL_STR_REPEAT, + methods::MAP_COLLECT_RESULT_UNIT, + methods::MAP_FLATTEN, + methods::MAP_IDENTITY, + methods::MAP_UNWRAP_OR, + methods::NEEDLESS_OPTION_AS_DEREF, + methods::NEEDLESS_OPTION_TAKE, + methods::NEEDLESS_SPLITN, + methods::NEW_RET_NO_SELF, + methods::NO_EFFECT_REPLACE, + methods::OBFUSCATED_IF_ELSE, + methods::OK_EXPECT, + methods::OPTION_AS_REF_DEREF, + methods::OPTION_FILTER_MAP, + methods::OPTION_MAP_OR_NONE, + methods::OR_FUN_CALL, + methods::OR_THEN_UNWRAP, + methods::RESULT_MAP_OR_INTO_OPTION, + methods::SEARCH_IS_SOME, + methods::SHOULD_IMPLEMENT_TRAIT, + methods::SINGLE_CHAR_ADD_STR, + methods::SINGLE_CHAR_PATTERN, + methods::SKIP_WHILE_NEXT, + methods::STRING_EXTEND_CHARS, + methods::SUSPICIOUS_MAP, + methods::SUSPICIOUS_SPLITN, + methods::UNINIT_ASSUMED_INIT, + methods::UNNECESSARY_FILTER_MAP, + methods::UNNECESSARY_FIND_MAP, + methods::UNNECESSARY_FOLD, + methods::UNNECESSARY_JOIN, + methods::UNNECESSARY_LAZY_EVALUATIONS, + methods::UNNECESSARY_TO_OWNED, + methods::UNWRAP_OR_ELSE_DEFAULT, + methods::UNWRAP_USED, + methods::USELESS_ASREF, + methods::WRONG_SELF_CONVENTION, + methods::ZST_OFFSET, + minmax::MIN_MAX, + misc::SHORT_CIRCUIT_STATEMENT, + misc::TOPLEVEL_REF_ARG, + misc::USED_UNDERSCORE_BINDING, + misc::ZERO_PTR, + misc_early::BUILTIN_TYPE_SHADOW, + misc_early::DOUBLE_NEG, + misc_early::DUPLICATE_UNDERSCORE_ARGUMENT, + misc_early::MIXED_CASE_HEX_LITERALS, + misc_early::REDUNDANT_PATTERN, + misc_early::SEPARATED_LITERAL_SUFFIX, + misc_early::UNNEEDED_FIELD_PATTERN, + misc_early::UNNEEDED_WILDCARD_PATTERN, + misc_early::UNSEPARATED_LITERAL_SUFFIX, + misc_early::ZERO_PREFIXED_LITERAL, + mismatching_type_param_order::MISMATCHING_TYPE_PARAM_ORDER, + missing_const_for_fn::MISSING_CONST_FOR_FN, + missing_doc::MISSING_DOCS_IN_PRIVATE_ITEMS, + missing_enforced_import_rename::MISSING_ENFORCED_IMPORT_RENAMES, + missing_inline::MISSING_INLINE_IN_PUBLIC_ITEMS, + mixed_read_write_in_expression::DIVERGING_SUB_EXPRESSION, + mixed_read_write_in_expression::MIXED_READ_WRITE_IN_EXPRESSION, + module_style::MOD_MODULE_FILES, + module_style::SELF_NAMED_MODULE_FILES, + mut_key::MUTABLE_KEY_TYPE, + mut_mut::MUT_MUT, + mut_mutex_lock::MUT_MUTEX_LOCK, + mut_reference::UNNECESSARY_MUT_PASSED, + mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL, + mutex_atomic::MUTEX_ATOMIC, + mutex_atomic::MUTEX_INTEGER, + needless_arbitrary_self_type::NEEDLESS_ARBITRARY_SELF_TYPE, + needless_bool::BOOL_COMPARISON, + needless_bool::NEEDLESS_BOOL, + needless_borrowed_ref::NEEDLESS_BORROWED_REFERENCE, + needless_continue::NEEDLESS_CONTINUE, + needless_for_each::NEEDLESS_FOR_EACH, + needless_late_init::NEEDLESS_LATE_INIT, + needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS, + needless_pass_by_value::NEEDLESS_PASS_BY_VALUE, + needless_question_mark::NEEDLESS_QUESTION_MARK, + needless_update::NEEDLESS_UPDATE, + neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD, + neg_multiply::NEG_MULTIPLY, + new_without_default::NEW_WITHOUT_DEFAULT, + no_effect::NO_EFFECT, + no_effect::NO_EFFECT_UNDERSCORE_BINDING, + no_effect::UNNECESSARY_OPERATION, + non_copy_const::BORROW_INTERIOR_MUTABLE_CONST, + non_copy_const::DECLARE_INTERIOR_MUTABLE_CONST, + non_expressive_names::JUST_UNDERSCORES_AND_DIGITS, + non_expressive_names::MANY_SINGLE_CHAR_NAMES, + non_expressive_names::SIMILAR_NAMES, + non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS, + non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY, + nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES, + octal_escapes::OCTAL_ESCAPES, + only_used_in_recursion::ONLY_USED_IN_RECURSION, + open_options::NONSENSICAL_OPEN_OPTIONS, + operators::ABSURD_EXTREME_COMPARISONS, + operators::ARITHMETIC, + operators::ASSIGN_OP_PATTERN, + operators::BAD_BIT_MASK, + operators::CMP_NAN, + operators::CMP_OWNED, + operators::DOUBLE_COMPARISONS, + operators::DURATION_SUBSEC, + operators::EQ_OP, + operators::ERASING_OP, + operators::FLOAT_ARITHMETIC, + operators::FLOAT_CMP, + operators::FLOAT_CMP_CONST, + operators::FLOAT_EQUALITY_WITHOUT_ABS, + operators::IDENTITY_OP, + operators::INEFFECTIVE_BIT_MASK, + operators::INTEGER_ARITHMETIC, + operators::INTEGER_DIVISION, + operators::MISREFACTORED_ASSIGN_OP, + operators::MODULO_ARITHMETIC, + operators::MODULO_ONE, + operators::NEEDLESS_BITWISE_BOOL, + operators::OP_REF, + operators::PTR_EQ, + operators::SELF_ASSIGNMENT, + operators::VERBOSE_BIT_MASK, + option_env_unwrap::OPTION_ENV_UNWRAP, + option_if_let_else::OPTION_IF_LET_ELSE, + overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL, + panic_in_result_fn::PANIC_IN_RESULT_FN, + panic_unimplemented::PANIC, + panic_unimplemented::TODO, + panic_unimplemented::UNIMPLEMENTED, + panic_unimplemented::UNREACHABLE, + partialeq_ne_impl::PARTIALEQ_NE_IMPL, + pass_by_ref_or_value::LARGE_TYPES_PASSED_BY_VALUE, + pass_by_ref_or_value::TRIVIALLY_COPY_PASS_BY_REF, + path_buf_push_overwrite::PATH_BUF_PUSH_OVERWRITE, + pattern_type_mismatch::PATTERN_TYPE_MISMATCH, + precedence::PRECEDENCE, + ptr::CMP_NULL, + ptr::INVALID_NULL_PTR_USAGE, + ptr::MUT_FROM_REF, + ptr::PTR_ARG, + ptr_offset_with_cast::PTR_OFFSET_WITH_CAST, + pub_use::PUB_USE, + question_mark::QUESTION_MARK, + ranges::MANUAL_RANGE_CONTAINS, + ranges::RANGE_MINUS_ONE, + ranges::RANGE_PLUS_ONE, + ranges::RANGE_ZIP_WITH_LEN, + ranges::REVERSED_EMPTY_RANGES, + rc_clone_in_vec_init::RC_CLONE_IN_VEC_INIT, + read_zero_byte_vec::READ_ZERO_BYTE_VEC, + redundant_clone::REDUNDANT_CLONE, + redundant_closure_call::REDUNDANT_CLOSURE_CALL, + redundant_else::REDUNDANT_ELSE, + redundant_field_names::REDUNDANT_FIELD_NAMES, + redundant_pub_crate::REDUNDANT_PUB_CRATE, + redundant_slicing::DEREF_BY_SLICING, + redundant_slicing::REDUNDANT_SLICING, + redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES, + ref_option_ref::REF_OPTION_REF, + reference::DEREF_ADDROF, + regex::INVALID_REGEX, + regex::TRIVIAL_REGEX, + repeat_once::REPEAT_ONCE, + return_self_not_must_use::RETURN_SELF_NOT_MUST_USE, + returns::LET_AND_RETURN, + returns::NEEDLESS_RETURN, + same_name_method::SAME_NAME_METHOD, + self_named_constructors::SELF_NAMED_CONSTRUCTORS, + semicolon_if_nothing_returned::SEMICOLON_IF_NOTHING_RETURNED, + serde_api::SERDE_API_MISUSE, + shadow::SHADOW_REUSE, + shadow::SHADOW_SAME, + shadow::SHADOW_UNRELATED, + single_char_lifetime_names::SINGLE_CHAR_LIFETIME_NAMES, + single_component_path_imports::SINGLE_COMPONENT_PATH_IMPORTS, + size_of_in_element_count::SIZE_OF_IN_ELEMENT_COUNT, + slow_vector_initialization::SLOW_VECTOR_INITIALIZATION, + stable_sort_primitive::STABLE_SORT_PRIMITIVE, + std_instead_of_core::ALLOC_INSTEAD_OF_CORE, + std_instead_of_core::STD_INSTEAD_OF_ALLOC, + std_instead_of_core::STD_INSTEAD_OF_CORE, + strings::STRING_ADD, + strings::STRING_ADD_ASSIGN, + strings::STRING_FROM_UTF8_AS_BYTES, + strings::STRING_LIT_AS_BYTES, + strings::STRING_SLICE, + strings::STRING_TO_STRING, + strings::STR_TO_STRING, + strings::TRIM_SPLIT_WHITESPACE, + strlen_on_c_strings::STRLEN_ON_C_STRINGS, + suspicious_operation_groupings::SUSPICIOUS_OPERATION_GROUPINGS, + suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL, + suspicious_trait_impl::SUSPICIOUS_OP_ASSIGN_IMPL, + swap::ALMOST_SWAPPED, + swap::MANUAL_SWAP, + swap_ptr_to_ref::SWAP_PTR_TO_REF, + tabs_in_doc_comments::TABS_IN_DOC_COMMENTS, + temporary_assignment::TEMPORARY_ASSIGNMENT, + to_digit_is_some::TO_DIGIT_IS_SOME, + trailing_empty_array::TRAILING_EMPTY_ARRAY, + trait_bounds::TRAIT_DUPLICATION_IN_BOUNDS, + trait_bounds::TYPE_REPETITION_IN_BOUNDS, + transmute::CROSSPOINTER_TRANSMUTE, + transmute::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS, + transmute::TRANSMUTE_BYTES_TO_STR, + transmute::TRANSMUTE_FLOAT_TO_INT, + transmute::TRANSMUTE_INT_TO_BOOL, + transmute::TRANSMUTE_INT_TO_CHAR, + transmute::TRANSMUTE_INT_TO_FLOAT, + transmute::TRANSMUTE_NUM_TO_BYTES, + transmute::TRANSMUTE_PTR_TO_PTR, + transmute::TRANSMUTE_PTR_TO_REF, + transmute::TRANSMUTE_UNDEFINED_REPR, + transmute::UNSOUND_COLLECTION_TRANSMUTE, + transmute::USELESS_TRANSMUTE, + transmute::WRONG_TRANSMUTE, + transmuting_null::TRANSMUTING_NULL, + types::BORROWED_BOX, + types::BOX_COLLECTION, + types::LINKEDLIST, + types::OPTION_OPTION, + types::RC_BUFFER, + types::RC_MUTEX, + types::REDUNDANT_ALLOCATION, + types::TYPE_COMPLEXITY, + types::VEC_BOX, + undocumented_unsafe_blocks::UNDOCUMENTED_UNSAFE_BLOCKS, + unicode::INVISIBLE_CHARACTERS, + unicode::NON_ASCII_LITERAL, + unicode::UNICODE_NOT_NFC, + uninit_vec::UNINIT_VEC, + unit_hash::UNIT_HASH, + unit_return_expecting_ord::UNIT_RETURN_EXPECTING_ORD, + unit_types::LET_UNIT_VALUE, + unit_types::UNIT_ARG, + unit_types::UNIT_CMP, + unnamed_address::FN_ADDRESS_COMPARISONS, + unnamed_address::VTABLE_ADDRESS_COMPARISONS, + unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS, + unnecessary_self_imports::UNNECESSARY_SELF_IMPORTS, + unnecessary_sort_by::UNNECESSARY_SORT_BY, + unnecessary_wraps::UNNECESSARY_WRAPS, + unnested_or_patterns::UNNESTED_OR_PATTERNS, + unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME, + unused_async::UNUSED_ASYNC, + unused_io_amount::UNUSED_IO_AMOUNT, + unused_rounding::UNUSED_ROUNDING, + unused_self::UNUSED_SELF, + unused_unit::UNUSED_UNIT, + unwrap::PANICKING_UNWRAP, + unwrap::UNNECESSARY_UNWRAP, + unwrap_in_result::UNWRAP_IN_RESULT, + upper_case_acronyms::UPPER_CASE_ACRONYMS, + use_self::USE_SELF, + useless_conversion::USELESS_CONVERSION, + vec::USELESS_VEC, + vec_init_then_push::VEC_INIT_THEN_PUSH, + vec_resize_to_zero::VEC_RESIZE_TO_ZERO, + verbose_file_reads::VERBOSE_FILE_READS, + wildcard_imports::ENUM_GLOB_USE, + wildcard_imports::WILDCARD_IMPORTS, + write::PRINTLN_EMPTY_STRING, + write::PRINT_LITERAL, + write::PRINT_STDERR, + write::PRINT_STDOUT, + write::PRINT_WITH_NEWLINE, + write::USE_DEBUG, + write::WRITELN_EMPTY_STRING, + write::WRITE_LITERAL, + write::WRITE_WITH_NEWLINE, + zero_div_zero::ZERO_DIVIDED_BY_ZERO, + zero_sized_map_values::ZERO_SIZED_MAP_VALUES, +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_nursery.rs b/src/tools/clippy/clippy_lints/src/lib.register_nursery.rs new file mode 100644 index 000000000..973191eb1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_nursery.rs @@ -0,0 +1,36 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::nursery", Some("clippy_nursery"), vec![ + LintId::of(attrs::EMPTY_LINE_AFTER_OUTER_ATTR), + LintId::of(cognitive_complexity::COGNITIVE_COMPLEXITY), + LintId::of(copies::BRANCHES_SHARING_CODE), + LintId::of(dereference::EXPLICIT_AUTO_DEREF), + LintId::of(equatable_if_let::EQUATABLE_IF_LET), + LintId::of(fallible_impl_from::FALLIBLE_IMPL_FROM), + LintId::of(floating_point_arithmetic::IMPRECISE_FLOPS), + LintId::of(floating_point_arithmetic::SUBOPTIMAL_FLOPS), + LintId::of(future_not_send::FUTURE_NOT_SEND), + LintId::of(index_refutable_slice::INDEX_REFUTABLE_SLICE), + LintId::of(let_if_seq::USELESS_LET_IF_SEQ), + LintId::of(matches::SIGNIFICANT_DROP_IN_SCRUTINEE), + LintId::of(methods::ITER_WITH_DRAIN), + LintId::of(missing_const_for_fn::MISSING_CONST_FOR_FN), + LintId::of(mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL), + LintId::of(mutex_atomic::MUTEX_ATOMIC), + LintId::of(mutex_atomic::MUTEX_INTEGER), + LintId::of(non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY), + LintId::of(nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES), + LintId::of(only_used_in_recursion::ONLY_USED_IN_RECURSION), + LintId::of(option_if_let_else::OPTION_IF_LET_ELSE), + LintId::of(path_buf_push_overwrite::PATH_BUF_PUSH_OVERWRITE), + LintId::of(redundant_pub_crate::REDUNDANT_PUB_CRATE), + LintId::of(regex::TRIVIAL_REGEX), + LintId::of(strings::STRING_LIT_AS_BYTES), + LintId::of(suspicious_operation_groupings::SUSPICIOUS_OPERATION_GROUPINGS), + LintId::of(trailing_empty_array::TRAILING_EMPTY_ARRAY), + LintId::of(transmute::TRANSMUTE_UNDEFINED_REPR), + LintId::of(unused_rounding::UNUSED_ROUNDING), + LintId::of(use_self::USE_SELF), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_pedantic.rs b/src/tools/clippy/clippy_lints/src/lib.register_pedantic.rs new file mode 100644 index 000000000..a1b546658 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_pedantic.rs @@ -0,0 +1,102 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), vec![ + LintId::of(attrs::INLINE_ALWAYS), + LintId::of(borrow_as_ptr::BORROW_AS_PTR), + LintId::of(bytecount::NAIVE_BYTECOUNT), + LintId::of(case_sensitive_file_extension_comparisons::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS), + LintId::of(casts::CAST_LOSSLESS), + LintId::of(casts::CAST_POSSIBLE_TRUNCATION), + LintId::of(casts::CAST_POSSIBLE_WRAP), + LintId::of(casts::CAST_PRECISION_LOSS), + LintId::of(casts::CAST_PTR_ALIGNMENT), + LintId::of(casts::CAST_SIGN_LOSS), + LintId::of(casts::PTR_AS_PTR), + LintId::of(checked_conversions::CHECKED_CONVERSIONS), + LintId::of(copies::SAME_FUNCTIONS_IN_IF_CONDITION), + LintId::of(copy_iterator::COPY_ITERATOR), + LintId::of(default::DEFAULT_TRAIT_ACCESS), + LintId::of(dereference::EXPLICIT_DEREF_METHODS), + LintId::of(dereference::REF_BINDING_TO_REFERENCE), + LintId::of(derive::EXPL_IMPL_CLONE_ON_COPY), + LintId::of(derive::UNSAFE_DERIVE_DESERIALIZE), + LintId::of(doc::DOC_MARKDOWN), + LintId::of(doc::MISSING_ERRORS_DOC), + LintId::of(doc::MISSING_PANICS_DOC), + LintId::of(doc_link_with_quotes::DOC_LINK_WITH_QUOTES), + LintId::of(empty_enum::EMPTY_ENUM), + LintId::of(enum_variants::MODULE_NAME_REPETITIONS), + LintId::of(eta_reduction::REDUNDANT_CLOSURE_FOR_METHOD_CALLS), + LintId::of(excessive_bools::FN_PARAMS_EXCESSIVE_BOOLS), + LintId::of(excessive_bools::STRUCT_EXCESSIVE_BOOLS), + LintId::of(functions::MUST_USE_CANDIDATE), + LintId::of(functions::TOO_MANY_LINES), + LintId::of(if_not_else::IF_NOT_ELSE), + LintId::of(implicit_hasher::IMPLICIT_HASHER), + LintId::of(implicit_saturating_sub::IMPLICIT_SATURATING_SUB), + LintId::of(inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR), + LintId::of(infinite_iter::MAYBE_INFINITE_ITER), + LintId::of(invalid_upcast_comparisons::INVALID_UPCAST_COMPARISONS), + LintId::of(items_after_statements::ITEMS_AFTER_STATEMENTS), + LintId::of(iter_not_returning_iterator::ITER_NOT_RETURNING_ITERATOR), + LintId::of(large_stack_arrays::LARGE_STACK_ARRAYS), + LintId::of(let_underscore::LET_UNDERSCORE_DROP), + LintId::of(literal_representation::LARGE_DIGIT_GROUPS), + LintId::of(literal_representation::UNREADABLE_LITERAL), + LintId::of(loops::EXPLICIT_INTO_ITER_LOOP), + LintId::of(loops::EXPLICIT_ITER_LOOP), + LintId::of(macro_use::MACRO_USE_IMPORTS), + LintId::of(manual_assert::MANUAL_ASSERT), + LintId::of(manual_ok_or::MANUAL_OK_OR), + LintId::of(matches::MATCH_BOOL), + LintId::of(matches::MATCH_ON_VEC_ITEMS), + LintId::of(matches::MATCH_SAME_ARMS), + LintId::of(matches::MATCH_WILDCARD_FOR_SINGLE_VARIANTS), + LintId::of(matches::MATCH_WILD_ERR_ARM), + LintId::of(matches::SINGLE_MATCH_ELSE), + LintId::of(methods::CLONED_INSTEAD_OF_COPIED), + LintId::of(methods::FILTER_MAP_NEXT), + LintId::of(methods::FLAT_MAP_OPTION), + LintId::of(methods::FROM_ITER_INSTEAD_OF_COLLECT), + LintId::of(methods::IMPLICIT_CLONE), + LintId::of(methods::INEFFICIENT_TO_STRING), + LintId::of(methods::MAP_UNWRAP_OR), + LintId::of(methods::UNNECESSARY_JOIN), + LintId::of(misc::USED_UNDERSCORE_BINDING), + LintId::of(mismatching_type_param_order::MISMATCHING_TYPE_PARAM_ORDER), + LintId::of(mut_mut::MUT_MUT), + LintId::of(needless_continue::NEEDLESS_CONTINUE), + LintId::of(needless_for_each::NEEDLESS_FOR_EACH), + LintId::of(needless_pass_by_value::NEEDLESS_PASS_BY_VALUE), + LintId::of(no_effect::NO_EFFECT_UNDERSCORE_BINDING), + LintId::of(non_expressive_names::MANY_SINGLE_CHAR_NAMES), + LintId::of(non_expressive_names::SIMILAR_NAMES), + LintId::of(operators::FLOAT_CMP), + LintId::of(operators::NEEDLESS_BITWISE_BOOL), + LintId::of(operators::VERBOSE_BIT_MASK), + LintId::of(pass_by_ref_or_value::LARGE_TYPES_PASSED_BY_VALUE), + LintId::of(pass_by_ref_or_value::TRIVIALLY_COPY_PASS_BY_REF), + LintId::of(ranges::RANGE_MINUS_ONE), + LintId::of(ranges::RANGE_PLUS_ONE), + LintId::of(redundant_else::REDUNDANT_ELSE), + LintId::of(ref_option_ref::REF_OPTION_REF), + LintId::of(return_self_not_must_use::RETURN_SELF_NOT_MUST_USE), + LintId::of(semicolon_if_nothing_returned::SEMICOLON_IF_NOTHING_RETURNED), + LintId::of(stable_sort_primitive::STABLE_SORT_PRIMITIVE), + LintId::of(strings::STRING_ADD_ASSIGN), + LintId::of(trait_bounds::TRAIT_DUPLICATION_IN_BOUNDS), + LintId::of(trait_bounds::TYPE_REPETITION_IN_BOUNDS), + LintId::of(transmute::TRANSMUTE_PTR_TO_PTR), + LintId::of(types::LINKEDLIST), + LintId::of(types::OPTION_OPTION), + LintId::of(unicode::UNICODE_NOT_NFC), + LintId::of(unnecessary_wraps::UNNECESSARY_WRAPS), + LintId::of(unnested_or_patterns::UNNESTED_OR_PATTERNS), + LintId::of(unused_async::UNUSED_ASYNC), + LintId::of(unused_self::UNUSED_SELF), + LintId::of(wildcard_imports::ENUM_GLOB_USE), + LintId::of(wildcard_imports::WILDCARD_IMPORTS), + LintId::of(zero_sized_map_values::ZERO_SIZED_MAP_VALUES), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_perf.rs b/src/tools/clippy/clippy_lints/src/lib.register_perf.rs new file mode 100644 index 000000000..e1b90acb9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_perf.rs @@ -0,0 +1,31 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::perf", Some("clippy_perf"), vec![ + LintId::of(entry::MAP_ENTRY), + LintId::of(escape::BOXED_LOCAL), + LintId::of(format_args::FORMAT_IN_FORMAT_ARGS), + LintId::of(format_args::TO_STRING_IN_FORMAT_ARGS), + LintId::of(large_const_arrays::LARGE_CONST_ARRAYS), + LintId::of(large_enum_variant::LARGE_ENUM_VARIANT), + LintId::of(loops::MANUAL_MEMCPY), + LintId::of(loops::MISSING_SPIN_LOOP), + LintId::of(loops::NEEDLESS_COLLECT), + LintId::of(manual_retain::MANUAL_RETAIN), + LintId::of(methods::EXPECT_FUN_CALL), + LintId::of(methods::EXTEND_WITH_DRAIN), + LintId::of(methods::ITER_NTH), + LintId::of(methods::ITER_OVEREAGER_CLONED), + LintId::of(methods::MANUAL_STR_REPEAT), + LintId::of(methods::OR_FUN_CALL), + LintId::of(methods::SINGLE_CHAR_PATTERN), + LintId::of(methods::UNNECESSARY_TO_OWNED), + LintId::of(operators::CMP_OWNED), + LintId::of(redundant_clone::REDUNDANT_CLONE), + LintId::of(slow_vector_initialization::SLOW_VECTOR_INITIALIZATION), + LintId::of(types::BOX_COLLECTION), + LintId::of(types::REDUNDANT_ALLOCATION), + LintId::of(vec::USELESS_VEC), + LintId::of(vec_init_then_push::VEC_INIT_THEN_PUSH), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_restriction.rs b/src/tools/clippy/clippy_lints/src/lib.register_restriction.rs new file mode 100644 index 000000000..a7339ef27 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_restriction.rs @@ -0,0 +1,88 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![ + LintId::of(as_conversions::AS_CONVERSIONS), + LintId::of(as_underscore::AS_UNDERSCORE), + LintId::of(asm_syntax::INLINE_ASM_X86_ATT_SYNTAX), + LintId::of(asm_syntax::INLINE_ASM_X86_INTEL_SYNTAX), + LintId::of(assertions_on_result_states::ASSERTIONS_ON_RESULT_STATES), + LintId::of(attrs::ALLOW_ATTRIBUTES_WITHOUT_REASON), + LintId::of(casts::FN_TO_NUMERIC_CAST_ANY), + LintId::of(create_dir::CREATE_DIR), + LintId::of(dbg_macro::DBG_MACRO), + LintId::of(default_numeric_fallback::DEFAULT_NUMERIC_FALLBACK), + LintId::of(default_union_representation::DEFAULT_UNION_REPRESENTATION), + LintId::of(disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS), + LintId::of(else_if_without_else::ELSE_IF_WITHOUT_ELSE), + LintId::of(empty_drop::EMPTY_DROP), + LintId::of(empty_structs_with_brackets::EMPTY_STRUCTS_WITH_BRACKETS), + LintId::of(exhaustive_items::EXHAUSTIVE_ENUMS), + LintId::of(exhaustive_items::EXHAUSTIVE_STRUCTS), + LintId::of(exit::EXIT), + LintId::of(float_literal::LOSSY_FLOAT_LITERAL), + LintId::of(format_push_string::FORMAT_PUSH_STRING), + LintId::of(if_then_some_else_none::IF_THEN_SOME_ELSE_NONE), + LintId::of(implicit_return::IMPLICIT_RETURN), + LintId::of(indexing_slicing::INDEXING_SLICING), + LintId::of(inherent_impl::MULTIPLE_INHERENT_IMPL), + LintId::of(large_include_file::LARGE_INCLUDE_FILE), + LintId::of(let_underscore::LET_UNDERSCORE_MUST_USE), + LintId::of(literal_representation::DECIMAL_LITERAL_REPRESENTATION), + LintId::of(map_err_ignore::MAP_ERR_IGNORE), + LintId::of(matches::REST_PAT_IN_FULLY_BOUND_STRUCTS), + LintId::of(matches::TRY_ERR), + LintId::of(matches::WILDCARD_ENUM_MATCH_ARM), + LintId::of(mem_forget::MEM_FORGET), + LintId::of(methods::CLONE_ON_REF_PTR), + LintId::of(methods::EXPECT_USED), + LintId::of(methods::FILETYPE_IS_FILE), + LintId::of(methods::GET_UNWRAP), + LintId::of(methods::UNWRAP_USED), + LintId::of(misc_early::SEPARATED_LITERAL_SUFFIX), + LintId::of(misc_early::UNNEEDED_FIELD_PATTERN), + LintId::of(misc_early::UNSEPARATED_LITERAL_SUFFIX), + LintId::of(missing_doc::MISSING_DOCS_IN_PRIVATE_ITEMS), + LintId::of(missing_enforced_import_rename::MISSING_ENFORCED_IMPORT_RENAMES), + LintId::of(missing_inline::MISSING_INLINE_IN_PUBLIC_ITEMS), + LintId::of(mixed_read_write_in_expression::MIXED_READ_WRITE_IN_EXPRESSION), + LintId::of(module_style::MOD_MODULE_FILES), + LintId::of(module_style::SELF_NAMED_MODULE_FILES), + LintId::of(operators::ARITHMETIC), + LintId::of(operators::FLOAT_ARITHMETIC), + LintId::of(operators::FLOAT_CMP_CONST), + LintId::of(operators::INTEGER_ARITHMETIC), + LintId::of(operators::INTEGER_DIVISION), + LintId::of(operators::MODULO_ARITHMETIC), + LintId::of(panic_in_result_fn::PANIC_IN_RESULT_FN), + LintId::of(panic_unimplemented::PANIC), + LintId::of(panic_unimplemented::TODO), + LintId::of(panic_unimplemented::UNIMPLEMENTED), + LintId::of(panic_unimplemented::UNREACHABLE), + LintId::of(pattern_type_mismatch::PATTERN_TYPE_MISMATCH), + LintId::of(pub_use::PUB_USE), + LintId::of(redundant_slicing::DEREF_BY_SLICING), + LintId::of(same_name_method::SAME_NAME_METHOD), + LintId::of(shadow::SHADOW_REUSE), + LintId::of(shadow::SHADOW_SAME), + LintId::of(shadow::SHADOW_UNRELATED), + LintId::of(single_char_lifetime_names::SINGLE_CHAR_LIFETIME_NAMES), + LintId::of(std_instead_of_core::ALLOC_INSTEAD_OF_CORE), + LintId::of(std_instead_of_core::STD_INSTEAD_OF_ALLOC), + LintId::of(std_instead_of_core::STD_INSTEAD_OF_CORE), + LintId::of(strings::STRING_ADD), + LintId::of(strings::STRING_SLICE), + LintId::of(strings::STRING_TO_STRING), + LintId::of(strings::STR_TO_STRING), + LintId::of(types::RC_BUFFER), + LintId::of(types::RC_MUTEX), + LintId::of(undocumented_unsafe_blocks::UNDOCUMENTED_UNSAFE_BLOCKS), + LintId::of(unicode::NON_ASCII_LITERAL), + LintId::of(unnecessary_self_imports::UNNECESSARY_SELF_IMPORTS), + LintId::of(unwrap_in_result::UNWRAP_IN_RESULT), + LintId::of(verbose_file_reads::VERBOSE_FILE_READS), + LintId::of(write::PRINT_STDERR), + LintId::of(write::PRINT_STDOUT), + LintId::of(write::USE_DEBUG), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_style.rs b/src/tools/clippy/clippy_lints/src/lib.register_style.rs new file mode 100644 index 000000000..e95bab1d0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_style.rs @@ -0,0 +1,127 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::style", Some("clippy_style"), vec![ + LintId::of(assertions_on_constants::ASSERTIONS_ON_CONSTANTS), + LintId::of(blacklisted_name::BLACKLISTED_NAME), + LintId::of(blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS), + LintId::of(bool_assert_comparison::BOOL_ASSERT_COMPARISON), + LintId::of(casts::FN_TO_NUMERIC_CAST), + LintId::of(casts::FN_TO_NUMERIC_CAST_WITH_TRUNCATION), + LintId::of(collapsible_if::COLLAPSIBLE_ELSE_IF), + LintId::of(collapsible_if::COLLAPSIBLE_IF), + LintId::of(comparison_chain::COMPARISON_CHAIN), + LintId::of(default::FIELD_REASSIGN_WITH_DEFAULT), + LintId::of(default_instead_of_iter_empty::DEFAULT_INSTEAD_OF_ITER_EMPTY), + LintId::of(dereference::NEEDLESS_BORROW), + LintId::of(derive::DERIVE_PARTIAL_EQ_WITHOUT_EQ), + LintId::of(disallowed_methods::DISALLOWED_METHODS), + LintId::of(disallowed_types::DISALLOWED_TYPES), + LintId::of(doc::MISSING_SAFETY_DOC), + LintId::of(doc::NEEDLESS_DOCTEST_MAIN), + LintId::of(enum_variants::ENUM_VARIANT_NAMES), + LintId::of(enum_variants::MODULE_INCEPTION), + LintId::of(eta_reduction::REDUNDANT_CLOSURE), + LintId::of(float_literal::EXCESSIVE_PRECISION), + LintId::of(from_over_into::FROM_OVER_INTO), + LintId::of(from_str_radix_10::FROM_STR_RADIX_10), + LintId::of(functions::DOUBLE_MUST_USE), + LintId::of(functions::MUST_USE_UNIT), + LintId::of(functions::RESULT_UNIT_ERR), + LintId::of(get_first::GET_FIRST), + LintId::of(inherent_to_string::INHERENT_TO_STRING), + LintId::of(init_numbered_fields::INIT_NUMBERED_FIELDS), + LintId::of(len_zero::COMPARISON_TO_EMPTY), + LintId::of(len_zero::LEN_WITHOUT_IS_EMPTY), + LintId::of(len_zero::LEN_ZERO), + LintId::of(literal_representation::INCONSISTENT_DIGIT_GROUPING), + LintId::of(literal_representation::UNUSUAL_BYTE_GROUPINGS), + LintId::of(loops::FOR_KV_MAP), + LintId::of(loops::NEEDLESS_RANGE_LOOP), + LintId::of(loops::SAME_ITEM_PUSH), + LintId::of(loops::WHILE_LET_ON_ITERATOR), + LintId::of(main_recursion::MAIN_RECURSION), + LintId::of(manual_async_fn::MANUAL_ASYNC_FN), + LintId::of(manual_bits::MANUAL_BITS), + LintId::of(manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE), + LintId::of(map_clone::MAP_CLONE), + LintId::of(match_result_ok::MATCH_RESULT_OK), + LintId::of(matches::COLLAPSIBLE_MATCH), + LintId::of(matches::INFALLIBLE_DESTRUCTURING_MATCH), + LintId::of(matches::MANUAL_MAP), + LintId::of(matches::MATCH_LIKE_MATCHES_MACRO), + LintId::of(matches::MATCH_OVERLAPPING_ARM), + LintId::of(matches::MATCH_REF_PATS), + LintId::of(matches::REDUNDANT_PATTERN_MATCHING), + LintId::of(matches::SINGLE_MATCH), + LintId::of(mem_replace::MEM_REPLACE_OPTION_WITH_NONE), + LintId::of(mem_replace::MEM_REPLACE_WITH_DEFAULT), + LintId::of(methods::BYTES_NTH), + LintId::of(methods::CHARS_LAST_CMP), + LintId::of(methods::CHARS_NEXT_CMP), + LintId::of(methods::ERR_EXPECT), + LintId::of(methods::INTO_ITER_ON_REF), + LintId::of(methods::IS_DIGIT_ASCII_RADIX), + LintId::of(methods::ITER_CLONED_COLLECT), + LintId::of(methods::ITER_NEXT_SLICE), + LintId::of(methods::ITER_NTH_ZERO), + LintId::of(methods::ITER_SKIP_NEXT), + LintId::of(methods::MANUAL_SATURATING_ARITHMETIC), + LintId::of(methods::MAP_COLLECT_RESULT_UNIT), + LintId::of(methods::NEW_RET_NO_SELF), + LintId::of(methods::OBFUSCATED_IF_ELSE), + LintId::of(methods::OK_EXPECT), + LintId::of(methods::OPTION_MAP_OR_NONE), + LintId::of(methods::RESULT_MAP_OR_INTO_OPTION), + LintId::of(methods::SHOULD_IMPLEMENT_TRAIT), + LintId::of(methods::SINGLE_CHAR_ADD_STR), + LintId::of(methods::STRING_EXTEND_CHARS), + LintId::of(methods::UNNECESSARY_FOLD), + LintId::of(methods::UNNECESSARY_LAZY_EVALUATIONS), + LintId::of(methods::UNWRAP_OR_ELSE_DEFAULT), + LintId::of(methods::WRONG_SELF_CONVENTION), + LintId::of(misc::TOPLEVEL_REF_ARG), + LintId::of(misc::ZERO_PTR), + LintId::of(misc_early::BUILTIN_TYPE_SHADOW), + LintId::of(misc_early::DOUBLE_NEG), + LintId::of(misc_early::DUPLICATE_UNDERSCORE_ARGUMENT), + LintId::of(misc_early::MIXED_CASE_HEX_LITERALS), + LintId::of(misc_early::REDUNDANT_PATTERN), + LintId::of(mut_mutex_lock::MUT_MUTEX_LOCK), + LintId::of(mut_reference::UNNECESSARY_MUT_PASSED), + LintId::of(needless_late_init::NEEDLESS_LATE_INIT), + LintId::of(needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS), + LintId::of(neg_multiply::NEG_MULTIPLY), + LintId::of(new_without_default::NEW_WITHOUT_DEFAULT), + LintId::of(non_copy_const::BORROW_INTERIOR_MUTABLE_CONST), + LintId::of(non_copy_const::DECLARE_INTERIOR_MUTABLE_CONST), + LintId::of(non_expressive_names::JUST_UNDERSCORES_AND_DIGITS), + LintId::of(operators::ASSIGN_OP_PATTERN), + LintId::of(operators::OP_REF), + LintId::of(operators::PTR_EQ), + LintId::of(ptr::CMP_NULL), + LintId::of(ptr::PTR_ARG), + LintId::of(question_mark::QUESTION_MARK), + LintId::of(ranges::MANUAL_RANGE_CONTAINS), + LintId::of(redundant_field_names::REDUNDANT_FIELD_NAMES), + LintId::of(redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES), + LintId::of(returns::LET_AND_RETURN), + LintId::of(returns::NEEDLESS_RETURN), + LintId::of(self_named_constructors::SELF_NAMED_CONSTRUCTORS), + LintId::of(single_component_path_imports::SINGLE_COMPONENT_PATH_IMPORTS), + LintId::of(strings::TRIM_SPLIT_WHITESPACE), + LintId::of(tabs_in_doc_comments::TABS_IN_DOC_COMMENTS), + LintId::of(to_digit_is_some::TO_DIGIT_IS_SOME), + LintId::of(unit_types::LET_UNIT_VALUE), + LintId::of(unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS), + LintId::of(unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME), + LintId::of(unused_unit::UNUSED_UNIT), + LintId::of(upper_case_acronyms::UPPER_CASE_ACRONYMS), + LintId::of(write::PRINTLN_EMPTY_STRING), + LintId::of(write::PRINT_LITERAL), + LintId::of(write::PRINT_WITH_NEWLINE), + LintId::of(write::WRITELN_EMPTY_STRING), + LintId::of(write::WRITE_LITERAL), + LintId::of(write::WRITE_WITH_NEWLINE), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.register_suspicious.rs b/src/tools/clippy/clippy_lints/src/lib.register_suspicious.rs new file mode 100644 index 000000000..964992bd9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.register_suspicious.rs @@ -0,0 +1,35 @@ +// This file was generated by `cargo dev update_lints`. +// Use that command to update this file and do not edit by hand. +// Manual edits will be overwritten. + +store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec![ + LintId::of(almost_complete_letter_range::ALMOST_COMPLETE_LETTER_RANGE), + LintId::of(attrs::BLANKET_CLIPPY_RESTRICTION_LINTS), + LintId::of(await_holding_invalid::AWAIT_HOLDING_INVALID_TYPE), + LintId::of(await_holding_invalid::AWAIT_HOLDING_LOCK), + LintId::of(await_holding_invalid::AWAIT_HOLDING_REFCELL_REF), + LintId::of(casts::CAST_ABS_TO_UNSIGNED), + LintId::of(casts::CAST_ENUM_CONSTRUCTOR), + LintId::of(casts::CAST_ENUM_TRUNCATION), + LintId::of(crate_in_macro_def::CRATE_IN_MACRO_DEF), + LintId::of(drop_forget_ref::DROP_NON_DROP), + LintId::of(drop_forget_ref::FORGET_NON_DROP), + LintId::of(duplicate_mod::DUPLICATE_MOD), + LintId::of(format_impl::PRINT_IN_FORMAT_IMPL), + LintId::of(formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING), + LintId::of(formatting::SUSPICIOUS_ELSE_FORMATTING), + LintId::of(formatting::SUSPICIOUS_UNARY_OP_FORMATTING), + LintId::of(loops::EMPTY_LOOP), + LintId::of(loops::FOR_LOOPS_OVER_FALLIBLES), + LintId::of(loops::MUT_RANGE_BOUND), + LintId::of(methods::NO_EFFECT_REPLACE), + LintId::of(methods::SUSPICIOUS_MAP), + LintId::of(mut_key::MUTABLE_KEY_TYPE), + LintId::of(octal_escapes::OCTAL_ESCAPES), + LintId::of(operators::FLOAT_EQUALITY_WITHOUT_ABS), + LintId::of(operators::MISREFACTORED_ASSIGN_OP), + LintId::of(rc_clone_in_vec_init::RC_CLONE_IN_VEC_INIT), + LintId::of(suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL), + LintId::of(suspicious_trait_impl::SUSPICIOUS_OP_ASSIGN_IMPL), + LintId::of(swap_ptr_to_ref::SWAP_PTR_TO_REF), +]) diff --git a/src/tools/clippy/clippy_lints/src/lib.rs b/src/tools/clippy/clippy_lints/src/lib.rs new file mode 100644 index 000000000..5a3111632 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lib.rs @@ -0,0 +1,985 @@ +#![feature(array_windows)] +#![feature(binary_heap_into_iter_sorted)] +#![feature(box_patterns)] +#![feature(control_flow_enum)] +#![feature(drain_filter)] +#![feature(iter_intersperse)] +#![feature(let_chains)] +#![feature(let_else)] +#![feature(lint_reasons)] +#![feature(never_type)] +#![feature(once_cell)] +#![feature(rustc_private)] +#![feature(stmt_expr_attributes)] +#![recursion_limit = "512"] +#![cfg_attr(feature = "deny-warnings", deny(warnings))] +#![allow(clippy::missing_docs_in_private_items, clippy::must_use_candidate)] +#![warn(trivial_casts, trivial_numeric_casts)] +// warn on lints, that are included in `rust-lang/rust`s bootstrap +#![warn(rust_2018_idioms, unused_lifetimes)] +// warn on rustc internal lints +#![warn(rustc::internal)] +// Disable this rustc lint for now, as it was also done in rustc +#![allow(rustc::potential_query_instability)] + +// FIXME: switch to something more ergonomic here, once available. +// (Currently there is no way to opt into sysroot crates without `extern crate`.) +extern crate rustc_arena; +extern crate rustc_ast; +extern crate rustc_ast_pretty; +extern crate rustc_attr; +extern crate rustc_data_structures; +extern crate rustc_driver; +extern crate rustc_errors; +extern crate rustc_hir; +extern crate rustc_hir_pretty; +extern crate rustc_index; +extern crate rustc_infer; +extern crate rustc_lexer; +extern crate rustc_lint; +extern crate rustc_middle; +extern crate rustc_mir_dataflow; +extern crate rustc_parse; +extern crate rustc_parse_format; +extern crate rustc_session; +extern crate rustc_span; +extern crate rustc_target; +extern crate rustc_trait_selection; +extern crate rustc_typeck; + +#[macro_use] +extern crate clippy_utils; + +use clippy_utils::parse_msrv; +use rustc_data_structures::fx::FxHashSet; +use rustc_lint::LintId; +use rustc_semver::RustcVersion; +use rustc_session::Session; + +/// Macro used to declare a Clippy lint. +/// +/// Every lint declaration consists of 4 parts: +/// +/// 1. The documentation, which is used for the website +/// 2. The `LINT_NAME`. See [lint naming][lint_naming] on lint naming conventions. +/// 3. The `lint_level`, which is a mapping from *one* of our lint groups to `Allow`, `Warn` or +/// `Deny`. The lint level here has nothing to do with what lint groups the lint is a part of. +/// 4. The `description` that contains a short explanation on what's wrong with code where the +/// lint is triggered. +/// +/// Currently the categories `style`, `correctness`, `suspicious`, `complexity` and `perf` are +/// enabled by default. As said in the README.md of this repository, if the lint level mapping +/// changes, please update README.md. +/// +/// # Example +/// +/// ``` +/// #![feature(rustc_private)] +/// extern crate rustc_session; +/// use rustc_session::declare_tool_lint; +/// use clippy_lints::declare_clippy_lint; +/// +/// declare_clippy_lint! { +/// /// ### What it does +/// /// Checks for ... (describe what the lint matches). +/// /// +/// /// ### Why is this bad? +/// /// Supply the reason for linting the code. +/// /// +/// /// ### Example +/// /// ```rust +/// /// Insert a short example of code that triggers the lint +/// /// ``` +/// /// +/// /// Use instead: +/// /// ```rust +/// /// Insert a short example of improved code that doesn't trigger the lint +/// /// ``` +/// pub LINT_NAME, +/// pedantic, +/// "description" +/// } +/// ``` +/// [lint_naming]: https://rust-lang.github.io/rfcs/0344-conventions-galore.html#lints +#[macro_export] +macro_rules! declare_clippy_lint { + { $(#[$attr:meta])* pub $name:tt, style, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Warn, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, correctness, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Deny, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, suspicious, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Warn, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, complexity, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Warn, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, perf, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Warn, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, pedantic, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Allow, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, restriction, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Allow, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, cargo, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Allow, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, nursery, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Allow, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, internal, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Allow, $description, report_in_external_macro: true + } + }; + { $(#[$attr:meta])* pub $name:tt, internal_warn, $description:tt } => { + declare_tool_lint! { + $(#[$attr])* pub clippy::$name, Warn, $description, report_in_external_macro: true + } + }; +} + +#[cfg(feature = "internal")] +pub mod deprecated_lints; +#[cfg_attr(feature = "internal", allow(clippy::missing_clippy_version_attribute))] +mod utils; + +mod renamed_lints; + +// begin lints modules, do not remove this comment, it’s used in `update_lints` +mod almost_complete_letter_range; +mod approx_const; +mod as_conversions; +mod as_underscore; +mod asm_syntax; +mod assertions_on_constants; +mod assertions_on_result_states; +mod async_yields_async; +mod attrs; +mod await_holding_invalid; +mod blacklisted_name; +mod blocks_in_if_conditions; +mod bool_assert_comparison; +mod booleans; +mod borrow_as_ptr; +mod borrow_deref_ref; +mod bytecount; +mod bytes_count_to_len; +mod cargo; +mod case_sensitive_file_extension_comparisons; +mod casts; +mod checked_conversions; +mod cognitive_complexity; +mod collapsible_if; +mod comparison_chain; +mod copies; +mod copy_iterator; +mod crate_in_macro_def; +mod create_dir; +mod dbg_macro; +mod default; +mod default_instead_of_iter_empty; +mod default_numeric_fallback; +mod default_union_representation; +mod dereference; +mod derivable_impls; +mod derive; +mod disallowed_methods; +mod disallowed_script_idents; +mod disallowed_types; +mod doc; +mod doc_link_with_quotes; +mod double_parens; +mod drop_forget_ref; +mod duplicate_mod; +mod else_if_without_else; +mod empty_drop; +mod empty_enum; +mod empty_structs_with_brackets; +mod entry; +mod enum_clike; +mod enum_variants; +mod equatable_if_let; +mod escape; +mod eta_reduction; +mod excessive_bools; +mod exhaustive_items; +mod exit; +mod explicit_write; +mod fallible_impl_from; +mod float_literal; +mod floating_point_arithmetic; +mod format; +mod format_args; +mod format_impl; +mod format_push_string; +mod formatting; +mod from_over_into; +mod from_str_radix_10; +mod functions; +mod future_not_send; +mod get_first; +mod if_let_mutex; +mod if_not_else; +mod if_then_some_else_none; +mod implicit_hasher; +mod implicit_return; +mod implicit_saturating_sub; +mod inconsistent_struct_constructor; +mod index_refutable_slice; +mod indexing_slicing; +mod infinite_iter; +mod inherent_impl; +mod inherent_to_string; +mod init_numbered_fields; +mod inline_fn_without_body; +mod int_plus_one; +mod invalid_upcast_comparisons; +mod invalid_utf8_in_unchecked; +mod items_after_statements; +mod iter_not_returning_iterator; +mod large_const_arrays; +mod large_enum_variant; +mod large_include_file; +mod large_stack_arrays; +mod len_zero; +mod let_if_seq; +mod let_underscore; +mod lifetimes; +mod literal_representation; +mod loops; +mod macro_use; +mod main_recursion; +mod manual_assert; +mod manual_async_fn; +mod manual_bits; +mod manual_non_exhaustive; +mod manual_ok_or; +mod manual_rem_euclid; +mod manual_retain; +mod manual_strip; +mod map_clone; +mod map_err_ignore; +mod map_unit_fn; +mod match_result_ok; +mod matches; +mod mem_forget; +mod mem_replace; +mod methods; +mod minmax; +mod misc; +mod misc_early; +mod mismatching_type_param_order; +mod missing_const_for_fn; +mod missing_doc; +mod missing_enforced_import_rename; +mod missing_inline; +mod mixed_read_write_in_expression; +mod module_style; +mod mut_key; +mod mut_mut; +mod mut_mutex_lock; +mod mut_reference; +mod mutable_debug_assertion; +mod mutex_atomic; +mod needless_arbitrary_self_type; +mod needless_bool; +mod needless_borrowed_ref; +mod needless_continue; +mod needless_for_each; +mod needless_late_init; +mod needless_parens_on_range_literals; +mod needless_pass_by_value; +mod needless_question_mark; +mod needless_update; +mod neg_cmp_op_on_partial_ord; +mod neg_multiply; +mod new_without_default; +mod no_effect; +mod non_copy_const; +mod non_expressive_names; +mod non_octal_unix_permissions; +mod non_send_fields_in_send_ty; +mod nonstandard_macro_braces; +mod octal_escapes; +mod only_used_in_recursion; +mod open_options; +mod operators; +mod option_env_unwrap; +mod option_if_let_else; +mod overflow_check_conditional; +mod panic_in_result_fn; +mod panic_unimplemented; +mod partialeq_ne_impl; +mod pass_by_ref_or_value; +mod path_buf_push_overwrite; +mod pattern_type_mismatch; +mod precedence; +mod ptr; +mod ptr_offset_with_cast; +mod pub_use; +mod question_mark; +mod ranges; +mod rc_clone_in_vec_init; +mod read_zero_byte_vec; +mod redundant_clone; +mod redundant_closure_call; +mod redundant_else; +mod redundant_field_names; +mod redundant_pub_crate; +mod redundant_slicing; +mod redundant_static_lifetimes; +mod ref_option_ref; +mod reference; +mod regex; +mod repeat_once; +mod return_self_not_must_use; +mod returns; +mod same_name_method; +mod self_named_constructors; +mod semicolon_if_nothing_returned; +mod serde_api; +mod shadow; +mod single_char_lifetime_names; +mod single_component_path_imports; +mod size_of_in_element_count; +mod slow_vector_initialization; +mod stable_sort_primitive; +mod std_instead_of_core; +mod strings; +mod strlen_on_c_strings; +mod suspicious_operation_groupings; +mod suspicious_trait_impl; +mod swap; +mod swap_ptr_to_ref; +mod tabs_in_doc_comments; +mod temporary_assignment; +mod to_digit_is_some; +mod trailing_empty_array; +mod trait_bounds; +mod transmute; +mod transmuting_null; +mod types; +mod undocumented_unsafe_blocks; +mod unicode; +mod uninit_vec; +mod unit_hash; +mod unit_return_expecting_ord; +mod unit_types; +mod unnamed_address; +mod unnecessary_owned_empty_strings; +mod unnecessary_self_imports; +mod unnecessary_sort_by; +mod unnecessary_wraps; +mod unnested_or_patterns; +mod unsafe_removed_from_name; +mod unused_async; +mod unused_io_amount; +mod unused_rounding; +mod unused_self; +mod unused_unit; +mod unwrap; +mod unwrap_in_result; +mod upper_case_acronyms; +mod use_self; +mod useless_conversion; +mod vec; +mod vec_init_then_push; +mod vec_resize_to_zero; +mod verbose_file_reads; +mod wildcard_imports; +mod write; +mod zero_div_zero; +mod zero_sized_map_values; +// end lints modules, do not remove this comment, it’s used in `update_lints` + +pub use crate::utils::conf::Conf; +use crate::utils::conf::{format_error, TryConf}; + +/// Register all pre expansion lints +/// +/// Pre-expansion lints run before any macro expansion has happened. +/// +/// Note that due to the architecture of the compiler, currently `cfg_attr` attributes on crate +/// level (i.e `#![cfg_attr(...)]`) will still be expanded even when using a pre-expansion pass. +/// +/// Used in `./src/driver.rs`. +pub fn register_pre_expansion_lints(store: &mut rustc_lint::LintStore, sess: &Session, conf: &Conf) { + // NOTE: Do not add any more pre-expansion passes. These should be removed eventually. + + let msrv = conf.msrv.as_ref().and_then(|s| { + parse_msrv(s, None, None).or_else(|| { + sess.err(&format!( + "error reading Clippy's configuration file. `{}` is not a valid Rust version", + s + )); + None + }) + }); + + store.register_pre_expansion_pass(|| Box::new(write::Write::default())); + store.register_pre_expansion_pass(move || Box::new(attrs::EarlyAttributes { msrv })); +} + +fn read_msrv(conf: &Conf, sess: &Session) -> Option<RustcVersion> { + let cargo_msrv = std::env::var("CARGO_PKG_RUST_VERSION") + .ok() + .and_then(|v| parse_msrv(&v, None, None)); + let clippy_msrv = conf.msrv.as_ref().and_then(|s| { + parse_msrv(s, None, None).or_else(|| { + sess.err(&format!( + "error reading Clippy's configuration file. `{}` is not a valid Rust version", + s + )); + None + }) + }); + + if let Some(cargo_msrv) = cargo_msrv { + if let Some(clippy_msrv) = clippy_msrv { + // if both files have an msrv, let's compare them and emit a warning if they differ + if clippy_msrv != cargo_msrv { + sess.warn(&format!( + "the MSRV in `clippy.toml` and `Cargo.toml` differ; using `{}` from `clippy.toml`", + clippy_msrv + )); + } + + Some(clippy_msrv) + } else { + Some(cargo_msrv) + } + } else { + clippy_msrv + } +} + +#[doc(hidden)] +pub fn read_conf(sess: &Session) -> Conf { + let file_name = match utils::conf::lookup_conf_file() { + Ok(Some(path)) => path, + Ok(None) => return Conf::default(), + Err(error) => { + sess.struct_err(&format!("error finding Clippy's configuration file: {}", error)) + .emit(); + return Conf::default(); + }, + }; + + let TryConf { conf, errors } = utils::conf::read(&file_name); + // all conf errors are non-fatal, we just use the default conf in case of error + for error in errors { + sess.err(&format!( + "error reading Clippy's configuration file `{}`: {}", + file_name.display(), + format_error(error) + )); + } + + conf +} + +/// Register all lints and lint groups with the rustc plugin registry +/// +/// Used in `./src/driver.rs`. +#[expect(clippy::too_many_lines)] +pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: &Conf) { + register_removed_non_tool_lints(store); + + include!("lib.deprecated.rs"); + + include!("lib.register_lints.rs"); + include!("lib.register_restriction.rs"); + include!("lib.register_pedantic.rs"); + + #[cfg(feature = "internal")] + include!("lib.register_internal.rs"); + + include!("lib.register_all.rs"); + include!("lib.register_style.rs"); + include!("lib.register_complexity.rs"); + include!("lib.register_correctness.rs"); + include!("lib.register_suspicious.rs"); + include!("lib.register_perf.rs"); + include!("lib.register_cargo.rs"); + include!("lib.register_nursery.rs"); + + #[cfg(feature = "internal")] + { + if std::env::var("ENABLE_METADATA_COLLECTION").eq(&Ok("1".to_string())) { + store.register_late_pass(|| Box::new(utils::internal_lints::metadata_collector::MetadataCollector::new())); + return; + } + } + + // all the internal lints + #[cfg(feature = "internal")] + { + store.register_early_pass(|| Box::new(utils::internal_lints::ClippyLintsInternal)); + store.register_early_pass(|| Box::new(utils::internal_lints::ProduceIce)); + store.register_late_pass(|| Box::new(utils::internal_lints::CollapsibleCalls)); + store.register_late_pass(|| Box::new(utils::internal_lints::CompilerLintFunctions::new())); + store.register_late_pass(|| Box::new(utils::internal_lints::IfChainStyle)); + store.register_late_pass(|| Box::new(utils::internal_lints::InvalidPaths)); + store.register_late_pass(|| Box::new(utils::internal_lints::InterningDefinedSymbol::default())); + store.register_late_pass(|| Box::new(utils::internal_lints::LintWithoutLintPass::default())); + store.register_late_pass(|| Box::new(utils::internal_lints::MatchTypeOnDiagItem)); + store.register_late_pass(|| Box::new(utils::internal_lints::OuterExpnDataPass)); + store.register_late_pass(|| Box::new(utils::internal_lints::MsrvAttrImpl)); + } + + let arithmetic_allowed = conf.arithmetic_allowed.clone(); + store.register_late_pass(move || Box::new(operators::arithmetic::Arithmetic::new(arithmetic_allowed.clone()))); + store.register_late_pass(|| Box::new(utils::dump_hir::DumpHir)); + store.register_late_pass(|| Box::new(utils::author::Author)); + let await_holding_invalid_types = conf.await_holding_invalid_types.clone(); + store.register_late_pass(move || { + Box::new(await_holding_invalid::AwaitHolding::new( + await_holding_invalid_types.clone(), + )) + }); + store.register_late_pass(|| Box::new(serde_api::SerdeApi)); + let vec_box_size_threshold = conf.vec_box_size_threshold; + let type_complexity_threshold = conf.type_complexity_threshold; + let avoid_breaking_exported_api = conf.avoid_breaking_exported_api; + store.register_late_pass(move || { + Box::new(types::Types::new( + vec_box_size_threshold, + type_complexity_threshold, + avoid_breaking_exported_api, + )) + }); + store.register_late_pass(|| Box::new(booleans::NonminimalBool)); + store.register_late_pass(|| Box::new(enum_clike::UnportableVariant)); + store.register_late_pass(|| Box::new(float_literal::FloatLiteral)); + store.register_late_pass(|| Box::new(ptr::Ptr)); + store.register_late_pass(|| Box::new(needless_bool::NeedlessBool)); + store.register_late_pass(|| Box::new(needless_bool::BoolComparison)); + store.register_late_pass(|| Box::new(needless_for_each::NeedlessForEach)); + store.register_late_pass(|| Box::new(misc::MiscLints)); + store.register_late_pass(|| Box::new(eta_reduction::EtaReduction)); + store.register_late_pass(|| Box::new(mut_mut::MutMut)); + store.register_late_pass(|| Box::new(mut_reference::UnnecessaryMutPassed)); + store.register_late_pass(|| Box::new(len_zero::LenZero)); + store.register_late_pass(|| Box::new(attrs::Attributes)); + store.register_late_pass(|| Box::new(blocks_in_if_conditions::BlocksInIfConditions)); + store.register_late_pass(|| Box::new(unicode::Unicode)); + store.register_late_pass(|| Box::new(uninit_vec::UninitVec)); + store.register_late_pass(|| Box::new(unit_hash::UnitHash)); + store.register_late_pass(|| Box::new(unit_return_expecting_ord::UnitReturnExpectingOrd)); + store.register_late_pass(|| Box::new(strings::StringAdd)); + store.register_late_pass(|| Box::new(implicit_return::ImplicitReturn)); + store.register_late_pass(|| Box::new(implicit_saturating_sub::ImplicitSaturatingSub)); + store.register_late_pass(|| Box::new(default_numeric_fallback::DefaultNumericFallback)); + store.register_late_pass(|| Box::new(inconsistent_struct_constructor::InconsistentStructConstructor)); + store.register_late_pass(|| Box::new(non_octal_unix_permissions::NonOctalUnixPermissions)); + store.register_early_pass(|| Box::new(unnecessary_self_imports::UnnecessarySelfImports)); + + let msrv = read_msrv(conf, sess); + let avoid_breaking_exported_api = conf.avoid_breaking_exported_api; + let allow_expect_in_tests = conf.allow_expect_in_tests; + let allow_unwrap_in_tests = conf.allow_unwrap_in_tests; + store.register_late_pass(move || Box::new(approx_const::ApproxConstant::new(msrv))); + store.register_late_pass(move || { + Box::new(methods::Methods::new( + avoid_breaking_exported_api, + msrv, + allow_expect_in_tests, + allow_unwrap_in_tests, + )) + }); + store.register_late_pass(move || Box::new(matches::Matches::new(msrv))); + store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveStruct::new(msrv))); + store.register_late_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveEnum::new(msrv))); + store.register_late_pass(move || Box::new(manual_strip::ManualStrip::new(msrv))); + store.register_early_pass(move || Box::new(redundant_static_lifetimes::RedundantStaticLifetimes::new(msrv))); + store.register_early_pass(move || Box::new(redundant_field_names::RedundantFieldNames::new(msrv))); + store.register_late_pass(move || Box::new(checked_conversions::CheckedConversions::new(msrv))); + store.register_late_pass(move || Box::new(mem_replace::MemReplace::new(msrv))); + store.register_late_pass(move || Box::new(ranges::Ranges::new(msrv))); + store.register_late_pass(move || Box::new(from_over_into::FromOverInto::new(msrv))); + store.register_late_pass(move || Box::new(use_self::UseSelf::new(msrv))); + store.register_late_pass(move || Box::new(missing_const_for_fn::MissingConstForFn::new(msrv))); + store.register_late_pass(move || Box::new(needless_question_mark::NeedlessQuestionMark)); + store.register_late_pass(move || Box::new(casts::Casts::new(msrv))); + store.register_early_pass(move || Box::new(unnested_or_patterns::UnnestedOrPatterns::new(msrv))); + store.register_late_pass(move || Box::new(map_clone::MapClone::new(msrv))); + + store.register_late_pass(|| Box::new(size_of_in_element_count::SizeOfInElementCount)); + store.register_late_pass(|| Box::new(same_name_method::SameNameMethod)); + let max_suggested_slice_pattern_length = conf.max_suggested_slice_pattern_length; + store.register_late_pass(move || { + Box::new(index_refutable_slice::IndexRefutableSlice::new( + max_suggested_slice_pattern_length, + msrv, + )) + }); + store.register_late_pass(|| Box::new(map_err_ignore::MapErrIgnore)); + store.register_late_pass(|| Box::new(shadow::Shadow::default())); + store.register_late_pass(|| Box::new(unit_types::UnitTypes)); + store.register_late_pass(|| Box::new(loops::Loops)); + store.register_late_pass(|| Box::new(main_recursion::MainRecursion::default())); + store.register_late_pass(|| Box::new(lifetimes::Lifetimes)); + store.register_late_pass(|| Box::new(entry::HashMapPass)); + store.register_late_pass(|| Box::new(minmax::MinMaxPass)); + store.register_late_pass(|| Box::new(open_options::OpenOptions)); + store.register_late_pass(|| Box::new(zero_div_zero::ZeroDiv)); + store.register_late_pass(|| Box::new(mutex_atomic::Mutex)); + store.register_late_pass(|| Box::new(needless_update::NeedlessUpdate)); + store.register_late_pass(|| Box::new(needless_borrowed_ref::NeedlessBorrowedRef)); + store.register_late_pass(|| Box::new(borrow_deref_ref::BorrowDerefRef)); + store.register_late_pass(|| Box::new(no_effect::NoEffect)); + store.register_late_pass(|| Box::new(temporary_assignment::TemporaryAssignment)); + store.register_late_pass(move || Box::new(transmute::Transmute::new(msrv))); + let cognitive_complexity_threshold = conf.cognitive_complexity_threshold; + store.register_late_pass(move || { + Box::new(cognitive_complexity::CognitiveComplexity::new( + cognitive_complexity_threshold, + )) + }); + 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(|| Box::new(panic_unimplemented::PanicUnimplemented)); + store.register_late_pass(|| Box::new(strings::StringLitAsBytes)); + store.register_late_pass(|| Box::new(derive::Derive)); + store.register_late_pass(|| Box::new(derivable_impls::DerivableImpls)); + store.register_late_pass(|| Box::new(drop_forget_ref::DropForgetRef)); + store.register_late_pass(|| Box::new(empty_enum::EmptyEnum)); + store.register_late_pass(|| Box::new(invalid_upcast_comparisons::InvalidUpcastComparisons)); + store.register_late_pass(|| Box::new(regex::Regex)); + store.register_late_pass(|| Box::new(copies::CopyAndPaste)); + store.register_late_pass(|| Box::new(copy_iterator::CopyIterator)); + store.register_late_pass(|| Box::new(format::UselessFormat)); + store.register_late_pass(|| Box::new(swap::Swap)); + store.register_late_pass(|| Box::new(overflow_check_conditional::OverflowCheckConditional)); + store.register_late_pass(|| Box::new(new_without_default::NewWithoutDefault::default())); + let blacklisted_names = conf.blacklisted_names.iter().cloned().collect::<FxHashSet<_>>(); + store.register_late_pass(move || Box::new(blacklisted_name::BlacklistedName::new(blacklisted_names.clone()))); + let too_many_arguments_threshold = conf.too_many_arguments_threshold; + let too_many_lines_threshold = conf.too_many_lines_threshold; + store.register_late_pass(move || { + Box::new(functions::Functions::new( + too_many_arguments_threshold, + too_many_lines_threshold, + )) + }); + let doc_valid_idents = conf.doc_valid_idents.iter().cloned().collect::<FxHashSet<_>>(); + 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(|| Box::new(missing_doc::MissingDoc::new())); + store.register_late_pass(|| Box::new(missing_inline::MissingInline)); + store.register_late_pass(move || Box::new(exhaustive_items::ExhaustiveItems)); + store.register_late_pass(|| Box::new(match_result_ok::MatchResultOk)); + store.register_late_pass(|| Box::new(partialeq_ne_impl::PartialEqNeImpl)); + store.register_late_pass(|| Box::new(unused_io_amount::UnusedIoAmount)); + let enum_variant_size_threshold = conf.enum_variant_size_threshold; + store.register_late_pass(move || Box::new(large_enum_variant::LargeEnumVariant::new(enum_variant_size_threshold))); + store.register_late_pass(|| Box::new(explicit_write::ExplicitWrite)); + store.register_late_pass(|| Box::new(needless_pass_by_value::NeedlessPassByValue)); + let pass_by_ref_or_value = pass_by_ref_or_value::PassByRefOrValue::new( + conf.trivial_copy_size_limit, + conf.pass_by_value_size_limit, + conf.avoid_breaking_exported_api, + &sess.target, + ); + store.register_late_pass(move || Box::new(pass_by_ref_or_value)); + store.register_late_pass(|| Box::new(ref_option_ref::RefOptionRef)); + store.register_late_pass(|| Box::new(bytecount::ByteCount)); + store.register_late_pass(|| Box::new(infinite_iter::InfiniteIter)); + store.register_late_pass(|| Box::new(inline_fn_without_body::InlineFnWithoutBody)); + store.register_late_pass(|| Box::new(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_early_pass(|| Box::new(suspicious_operation_groupings::SuspiciousOperationGroupings)); + store.register_late_pass(|| Box::new(suspicious_trait_impl::SuspiciousImpl)); + store.register_late_pass(|| Box::new(map_unit_fn::MapUnit)); + store.register_late_pass(|| Box::new(inherent_impl::MultipleInherentImpl)); + store.register_late_pass(|| Box::new(neg_cmp_op_on_partial_ord::NoNegCompOpForPartialOrd)); + store.register_late_pass(|| Box::new(unwrap::Unwrap)); + store.register_late_pass(|| Box::new(indexing_slicing::IndexingSlicing)); + store.register_late_pass(|| Box::new(non_copy_const::NonCopyConst)); + store.register_late_pass(|| Box::new(ptr_offset_with_cast::PtrOffsetWithCast)); + store.register_late_pass(|| Box::new(redundant_clone::RedundantClone)); + store.register_late_pass(|| Box::new(slow_vector_initialization::SlowVectorInit)); + store.register_late_pass(|| Box::new(unnecessary_sort_by::UnnecessarySortBy)); + store.register_late_pass(move || Box::new(unnecessary_wraps::UnnecessaryWraps::new(avoid_breaking_exported_api))); + store.register_late_pass(|| Box::new(assertions_on_constants::AssertionsOnConstants)); + store.register_late_pass(|| Box::new(assertions_on_result_states::AssertionsOnResultStates)); + store.register_late_pass(|| Box::new(transmuting_null::TransmutingNull)); + store.register_late_pass(|| Box::new(path_buf_push_overwrite::PathBufPushOverwrite)); + 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(|| Box::new(comparison_chain::ComparisonChain)); + store.register_late_pass(|| Box::new(mut_key::MutableKeyType)); + store.register_early_pass(|| Box::new(reference::DerefAddrOf)); + store.register_early_pass(|| Box::new(double_parens::DoubleParens)); + store.register_late_pass(|| Box::new(format_impl::FormatImpl::new())); + store.register_early_pass(|| Box::new(unsafe_removed_from_name::UnsafeNameRemoval)); + store.register_early_pass(|| Box::new(else_if_without_else::ElseIfWithoutElse)); + 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)); + store.register_early_pass(|| Box::new(collapsible_if::CollapsibleIf)); + store.register_early_pass(|| Box::new(items_after_statements::ItemsAfterStatements)); + store.register_early_pass(|| Box::new(precedence::Precedence)); + store.register_late_pass(|| Box::new(needless_parens_on_range_literals::NeedlessParensOnRangeLiterals)); + store.register_early_pass(|| Box::new(needless_continue::NeedlessContinue)); + store.register_early_pass(|| Box::new(redundant_else::RedundantElse)); + store.register_late_pass(|| Box::new(create_dir::CreateDir)); + store.register_early_pass(|| Box::new(needless_arbitrary_self_type::NeedlessArbitrarySelfType)); + let literal_representation_lint_fraction_readability = conf.unreadable_literal_lint_fractions; + store.register_early_pass(move || { + Box::new(literal_representation::LiteralDigitGrouping::new( + literal_representation_lint_fraction_readability, + )) + }); + let literal_representation_threshold = conf.literal_representation_threshold; + store.register_early_pass(move || { + Box::new(literal_representation::DecimalLiteralRepresentation::new( + literal_representation_threshold, + )) + }); + let enum_variant_name_threshold = conf.enum_variant_name_threshold; + store.register_late_pass(move || { + Box::new(enum_variants::EnumVariantNames::new( + enum_variant_name_threshold, + avoid_breaking_exported_api, + )) + }); + store.register_early_pass(|| Box::new(tabs_in_doc_comments::TabsInDocComments)); + let upper_case_acronyms_aggressive = conf.upper_case_acronyms_aggressive; + store.register_late_pass(move || { + Box::new(upper_case_acronyms::UpperCaseAcronyms::new( + avoid_breaking_exported_api, + upper_case_acronyms_aggressive, + )) + }); + store.register_late_pass(|| Box::new(default::Default::default())); + store.register_late_pass(move || Box::new(unused_self::UnusedSelf::new(avoid_breaking_exported_api))); + store.register_late_pass(|| Box::new(mutable_debug_assertion::DebugAssertWithMutCall)); + store.register_late_pass(|| Box::new(exit::Exit)); + store.register_late_pass(|| Box::new(to_digit_is_some::ToDigitIsSome)); + let array_size_threshold = conf.array_size_threshold; + 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(let_underscore::LetUnderscore)); + store.register_early_pass(|| Box::new(single_component_path_imports::SingleComponentPathImports)); + let max_fn_params_bools = conf.max_fn_params_bools; + let max_struct_bools = conf.max_struct_bools; + store.register_early_pass(move || { + Box::new(excessive_bools::ExcessiveBools::new( + max_struct_bools, + max_fn_params_bools, + )) + }); + store.register_early_pass(|| Box::new(option_env_unwrap::OptionEnvUnwrap)); + let warn_on_all_wildcard_imports = conf.warn_on_all_wildcard_imports; + store.register_late_pass(move || Box::new(wildcard_imports::WildcardImports::new(warn_on_all_wildcard_imports))); + store.register_late_pass(|| Box::new(verbose_file_reads::VerboseFileReads)); + store.register_late_pass(|| Box::new(redundant_pub_crate::RedundantPubCrate::default())); + store.register_late_pass(|| Box::new(unnamed_address::UnnamedAddress)); + store.register_late_pass(|| Box::new(dereference::Dereferencing::default())); + store.register_late_pass(|| Box::new(option_if_let_else::OptionIfLetElse)); + store.register_late_pass(|| Box::new(future_not_send::FutureNotSend)); + store.register_late_pass(|| Box::new(if_let_mutex::IfLetMutex)); + store.register_late_pass(|| Box::new(if_not_else::IfNotElse)); + store.register_late_pass(|| Box::new(equatable_if_let::PatternEquality)); + store.register_late_pass(|| Box::new(mut_mutex_lock::MutMutexLock)); + store.register_late_pass(|| Box::new(manual_async_fn::ManualAsyncFn)); + store.register_late_pass(|| Box::new(vec_resize_to_zero::VecResizeToZero)); + store.register_late_pass(|| Box::new(panic_in_result_fn::PanicInResultFn)); + let single_char_binding_names_threshold = conf.single_char_binding_names_threshold; + store.register_early_pass(move || { + Box::new(non_expressive_names::NonExpressiveNames { + single_char_binding_names_threshold, + }) + }); + let macro_matcher = conf.standard_macro_braces.iter().cloned().collect::<FxHashSet<_>>(); + store.register_early_pass(move || Box::new(nonstandard_macro_braces::MacroBraces::new(¯o_matcher))); + store.register_late_pass(|| Box::new(macro_use::MacroUseImports::default())); + store.register_late_pass(|| Box::new(pattern_type_mismatch::PatternTypeMismatch)); + store.register_late_pass(|| Box::new(stable_sort_primitive::StableSortPrimitive)); + store.register_late_pass(|| Box::new(repeat_once::RepeatOnce)); + store.register_late_pass(|| Box::new(unwrap_in_result::UnwrapInResult)); + store.register_late_pass(|| Box::new(manual_ok_or::ManualOkOr)); + store.register_late_pass(|| Box::new(semicolon_if_nothing_returned::SemicolonIfNothingReturned)); + store.register_late_pass(|| Box::new(async_yields_async::AsyncYieldsAsync)); + let disallowed_methods = conf.disallowed_methods.clone(); + store.register_late_pass(move || Box::new(disallowed_methods::DisallowedMethods::new(disallowed_methods.clone()))); + store.register_early_pass(|| Box::new(asm_syntax::InlineAsmX86AttSyntax)); + store.register_early_pass(|| Box::new(asm_syntax::InlineAsmX86IntelSyntax)); + store.register_late_pass(|| Box::new(empty_drop::EmptyDrop)); + store.register_late_pass(|| Box::new(strings::StrToString)); + store.register_late_pass(|| Box::new(strings::StringToString)); + store.register_late_pass(|| Box::new(zero_sized_map_values::ZeroSizedMapValues)); + store.register_late_pass(|| Box::new(vec_init_then_push::VecInitThenPush::default())); + store.register_late_pass(|| { + Box::new(case_sensitive_file_extension_comparisons::CaseSensitiveFileExtensionComparisons) + }); + store.register_late_pass(|| Box::new(redundant_slicing::RedundantSlicing)); + store.register_late_pass(|| Box::new(from_str_radix_10::FromStrRadix10)); + store.register_late_pass(move || Box::new(if_then_some_else_none::IfThenSomeElseNone::new(msrv))); + store.register_late_pass(|| Box::new(bool_assert_comparison::BoolAssertComparison)); + store.register_early_pass(move || Box::new(module_style::ModStyle)); + store.register_late_pass(|| Box::new(unused_async::UnusedAsync)); + let disallowed_types = conf.disallowed_types.clone(); + store.register_late_pass(move || Box::new(disallowed_types::DisallowedTypes::new(disallowed_types.clone()))); + let import_renames = conf.enforced_import_renames.clone(); + store.register_late_pass(move || { + Box::new(missing_enforced_import_rename::ImportRename::new( + import_renames.clone(), + )) + }); + let scripts = conf.allowed_scripts.clone(); + store.register_early_pass(move || Box::new(disallowed_script_idents::DisallowedScriptIdents::new(&scripts))); + store.register_late_pass(|| Box::new(strlen_on_c_strings::StrlenOnCStrings)); + store.register_late_pass(move || Box::new(self_named_constructors::SelfNamedConstructors)); + store.register_late_pass(move || Box::new(iter_not_returning_iterator::IterNotReturningIterator)); + store.register_late_pass(move || Box::new(manual_assert::ManualAssert)); + let enable_raw_pointer_heuristic_for_send = conf.enable_raw_pointer_heuristic_for_send; + store.register_late_pass(move || { + Box::new(non_send_fields_in_send_ty::NonSendFieldInSendTy::new( + enable_raw_pointer_heuristic_for_send, + )) + }); + store.register_late_pass(move || Box::new(undocumented_unsafe_blocks::UndocumentedUnsafeBlocks)); + store.register_late_pass(move || Box::new(format_args::FormatArgs)); + store.register_late_pass(|| Box::new(trailing_empty_array::TrailingEmptyArray)); + store.register_early_pass(|| Box::new(octal_escapes::OctalEscapes)); + store.register_late_pass(|| Box::new(needless_late_init::NeedlessLateInit)); + store.register_late_pass(|| Box::new(return_self_not_must_use::ReturnSelfNotMustUse)); + store.register_late_pass(|| Box::new(init_numbered_fields::NumberedFields)); + store.register_early_pass(|| Box::new(single_char_lifetime_names::SingleCharLifetimeNames)); + store.register_late_pass(move || Box::new(borrow_as_ptr::BorrowAsPtr::new(msrv))); + store.register_late_pass(move || Box::new(manual_bits::ManualBits::new(msrv))); + store.register_late_pass(|| Box::new(default_union_representation::DefaultUnionRepresentation)); + store.register_early_pass(|| Box::new(doc_link_with_quotes::DocLinkWithQuotes)); + store.register_late_pass(|| Box::new(only_used_in_recursion::OnlyUsedInRecursion)); + let allow_dbg_in_tests = conf.allow_dbg_in_tests; + store.register_late_pass(move || Box::new(dbg_macro::DbgMacro::new(allow_dbg_in_tests))); + let cargo_ignore_publish = conf.cargo_ignore_publish; + store.register_late_pass(move || { + Box::new(cargo::Cargo { + ignore_publish: cargo_ignore_publish, + }) + }); + store.register_early_pass(|| Box::new(crate_in_macro_def::CrateInMacroDef)); + store.register_early_pass(|| Box::new(empty_structs_with_brackets::EmptyStructsWithBrackets)); + store.register_late_pass(|| Box::new(unnecessary_owned_empty_strings::UnnecessaryOwnedEmptyStrings)); + store.register_early_pass(|| Box::new(pub_use::PubUse)); + store.register_late_pass(|| Box::new(format_push_string::FormatPushString)); + store.register_late_pass(|| Box::new(bytes_count_to_len::BytesCountToLen)); + let max_include_file_size = conf.max_include_file_size; + store.register_late_pass(move || Box::new(large_include_file::LargeIncludeFile::new(max_include_file_size))); + store.register_late_pass(|| Box::new(strings::TrimSplitWhitespace)); + store.register_late_pass(|| Box::new(rc_clone_in_vec_init::RcCloneInVecInit)); + store.register_early_pass(|| Box::new(duplicate_mod::DuplicateMod::default())); + store.register_late_pass(|| Box::new(get_first::GetFirst)); + store.register_early_pass(|| Box::new(unused_rounding::UnusedRounding)); + store.register_early_pass(move || Box::new(almost_complete_letter_range::AlmostCompleteLetterRange::new(msrv))); + store.register_late_pass(|| Box::new(swap_ptr_to_ref::SwapPtrToRef)); + store.register_late_pass(|| Box::new(mismatching_type_param_order::TypeParamMismatch)); + store.register_late_pass(|| Box::new(as_underscore::AsUnderscore)); + store.register_late_pass(|| Box::new(read_zero_byte_vec::ReadZeroByteVec)); + store.register_late_pass(|| Box::new(default_instead_of_iter_empty::DefaultIterEmpty)); + store.register_late_pass(move || Box::new(manual_rem_euclid::ManualRemEuclid::new(msrv))); + store.register_late_pass(move || Box::new(manual_retain::ManualRetain::new(msrv))); + let verbose_bit_mask_threshold = conf.verbose_bit_mask_threshold; + store.register_late_pass(move || Box::new(operators::Operators::new(verbose_bit_mask_threshold))); + store.register_late_pass(|| Box::new(invalid_utf8_in_unchecked::InvalidUtf8InUnchecked)); + store.register_late_pass(|| Box::new(std_instead_of_core::StdReexports::default())); + // add lints here, do not remove this comment, it's used in `new_lint` +} + +#[rustfmt::skip] +fn register_removed_non_tool_lints(store: &mut rustc_lint::LintStore) { + store.register_removed( + "should_assert_eq", + "`assert!()` will be more flexible with RFC 2011", + ); + store.register_removed( + "extend_from_slice", + "`.extend_from_slice(_)` is a faster way to extend a Vec by a slice", + ); + store.register_removed( + "range_step_by_zero", + "`iterator.step_by(0)` panics nowadays", + ); + store.register_removed( + "unstable_as_slice", + "`Vec::as_slice` has been stabilized in 1.7", + ); + store.register_removed( + "unstable_as_mut_slice", + "`Vec::as_mut_slice` has been stabilized in 1.7", + ); + store.register_removed( + "misaligned_transmute", + "this lint has been split into cast_ptr_alignment and transmute_ptr_to_ptr", + ); + store.register_removed( + "assign_ops", + "using compound assignment operators (e.g., `+=`) is harmless", + ); + store.register_removed( + "if_let_redundant_pattern_matching", + "this lint has been changed to redundant_pattern_matching", + ); + store.register_removed( + "unsafe_vector_initialization", + "the replacement suggested by this lint had substantially different behavior", + ); + store.register_removed( + "reverse_range_loop", + "this lint is now included in reversed_empty_ranges", + ); +} + +/// Register renamed lints. +/// +/// Used in `./src/driver.rs`. +pub fn register_renamed(ls: &mut rustc_lint::LintStore) { + for (old_name, new_name) in renamed_lints::RENAMED_LINTS { + ls.register_renamed(old_name, new_name); + } +} + +// only exists to let the dogfood integration test works. +// Don't run clippy as an executable directly +#[allow(dead_code)] +fn main() { + panic!("Please use the cargo-clippy executable"); +} diff --git a/src/tools/clippy/clippy_lints/src/lifetimes.rs b/src/tools/clippy/clippy_lints/src/lifetimes.rs new file mode 100644 index 000000000..573a7c016 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/lifetimes.rs @@ -0,0 +1,620 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::trait_ref_of_method; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_hir::intravisit::nested_filter::{self as hir_nested_filter, NestedFilter}; +use rustc_hir::intravisit::{ + walk_fn_decl, walk_generic_param, walk_generics, walk_impl_item_ref, walk_item, walk_param_bound, + walk_poly_trait_ref, walk_trait_ref, walk_ty, Visitor, +}; +use rustc_hir::FnRetTy::Return; +use rustc_hir::{ + BareFnTy, BodyId, FnDecl, GenericArg, GenericBound, GenericParam, GenericParamKind, Generics, Impl, ImplItem, + ImplItemKind, Item, ItemKind, LangItem, Lifetime, LifetimeName, ParamName, PolyTraitRef, PredicateOrigin, + TraitBoundModifier, TraitFn, TraitItem, TraitItemKind, Ty, TyKind, WherePredicate, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter as middle_nested_filter; +use rustc_middle::ty::TyCtxt; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::def_id::LocalDefId; +use rustc_span::source_map::Span; +use rustc_span::symbol::{kw, Ident, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for lifetime annotations which can be removed by + /// relying on lifetime elision. + /// + /// ### Why is this bad? + /// The additional lifetimes make the code look more + /// complicated, while there is nothing out of the ordinary going on. Removing + /// them leads to more readable code. + /// + /// ### Known problems + /// - We bail out if the function has a `where` clause where lifetimes + /// are mentioned due to potential false positives. + /// - Lifetime bounds such as `impl Foo + 'a` and `T: 'a` must be elided with the + /// placeholder notation `'_` because the fully elided notation leaves the type bound to `'static`. + /// + /// ### Example + /// ```rust + /// // Unnecessary lifetime annotations + /// fn in_and_out<'a>(x: &'a u8, y: u8) -> &'a u8 { + /// x + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// fn elided(x: &u8, y: u8) -> &u8 { + /// x + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_LIFETIMES, + complexity, + "using explicit lifetimes for references in function arguments when elision rules \ + would allow omitting them" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for lifetimes in generics that are never used + /// anywhere else. + /// + /// ### Why is this bad? + /// The additional lifetimes make the code look more + /// complicated, while there is nothing out of the ordinary going on. Removing + /// them leads to more readable code. + /// + /// ### Example + /// ```rust + /// // unnecessary lifetimes + /// fn unused_lifetime<'a>(x: u8) { + /// // .. + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// fn no_lifetime(x: u8) { + /// // ... + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXTRA_UNUSED_LIFETIMES, + complexity, + "unused lifetimes in function definitions" +} + +declare_lint_pass!(Lifetimes => [NEEDLESS_LIFETIMES, EXTRA_UNUSED_LIFETIMES]); + +impl<'tcx> LateLintPass<'tcx> for Lifetimes { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if let ItemKind::Fn(ref sig, generics, id) = item.kind { + check_fn_inner(cx, sig.decl, Some(id), None, generics, item.span, true); + } else if let ItemKind::Impl(impl_) = item.kind { + if !item.span.from_expansion() { + report_extra_impl_lifetimes(cx, impl_); + } + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) { + if let ImplItemKind::Fn(ref sig, id) = item.kind { + let report_extra_lifetimes = trait_ref_of_method(cx, item.def_id).is_none(); + check_fn_inner( + cx, + sig.decl, + Some(id), + None, + item.generics, + item.span, + report_extra_lifetimes, + ); + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) { + if let TraitItemKind::Fn(ref sig, ref body) = item.kind { + let (body, trait_sig) = match *body { + TraitFn::Required(sig) => (None, Some(sig)), + TraitFn::Provided(id) => (Some(id), None), + }; + check_fn_inner(cx, sig.decl, body, trait_sig, item.generics, item.span, true); + } + } +} + +/// The lifetime of a &-reference. +#[derive(PartialEq, Eq, Hash, Debug, Clone)] +enum RefLt { + Unnamed, + Static, + Named(LocalDefId), +} + +fn check_fn_inner<'tcx>( + cx: &LateContext<'tcx>, + decl: &'tcx FnDecl<'_>, + body: Option<BodyId>, + trait_sig: Option<&[Ident]>, + generics: &'tcx Generics<'_>, + span: Span, + report_extra_lifetimes: bool, +) { + if span.from_expansion() || has_where_lifetimes(cx, generics) { + return; + } + + let types = generics + .params + .iter() + .filter(|param| matches!(param.kind, GenericParamKind::Type { .. })); + for typ in types { + for pred in generics.bounds_for_param(cx.tcx.hir().local_def_id(typ.hir_id)) { + if pred.origin == PredicateOrigin::WhereClause { + // has_where_lifetimes checked that this predicate contains no lifetime. + continue; + } + + for bound in pred.bounds { + let mut visitor = RefVisitor::new(cx); + walk_param_bound(&mut visitor, bound); + if visitor.lts.iter().any(|lt| matches!(lt, RefLt::Named(_))) { + return; + } + if let GenericBound::Trait(ref trait_ref, _) = *bound { + let params = &trait_ref + .trait_ref + .path + .segments + .last() + .expect("a path must have at least one segment") + .args; + if let Some(params) = *params { + let lifetimes = params.args.iter().filter_map(|arg| match arg { + GenericArg::Lifetime(lt) => Some(lt), + _ => None, + }); + for bound in lifetimes { + if bound.name != LifetimeName::Static && !bound.is_elided() { + return; + } + } + } + } + } + } + } + if could_use_elision(cx, decl, body, trait_sig, generics.params) { + span_lint( + cx, + NEEDLESS_LIFETIMES, + span.with_hi(decl.output.span().hi()), + "explicit lifetimes given in parameter types where they could be elided \ + (or replaced with `'_` if needed by type declaration)", + ); + } + if report_extra_lifetimes { + self::report_extra_lifetimes(cx, decl, generics); + } +} + +// elision doesn't work for explicit self types, see rust-lang/rust#69064 +fn explicit_self_type<'tcx>(cx: &LateContext<'tcx>, func: &FnDecl<'tcx>, ident: Option<Ident>) -> bool { + if_chain! { + if let Some(ident) = ident; + if ident.name == kw::SelfLower; + if !func.implicit_self.has_implicit_self(); + + if let Some(self_ty) = func.inputs.first(); + then { + let mut visitor = RefVisitor::new(cx); + visitor.visit_ty(self_ty); + + !visitor.all_lts().is_empty() + } else { + false + } + } +} + +fn could_use_elision<'tcx>( + cx: &LateContext<'tcx>, + func: &'tcx FnDecl<'_>, + body: Option<BodyId>, + trait_sig: Option<&[Ident]>, + named_generics: &'tcx [GenericParam<'_>], +) -> bool { + // There are two scenarios where elision works: + // * no output references, all input references have different LT + // * output references, exactly one input reference with same LT + // All lifetimes must be unnamed, 'static or defined without bounds on the + // level of the current item. + + // check named LTs + let allowed_lts = allowed_lts_from(cx.tcx, named_generics); + + // these will collect all the lifetimes for references in arg/return types + let mut input_visitor = RefVisitor::new(cx); + let mut output_visitor = RefVisitor::new(cx); + + // extract lifetimes in input argument types + for arg in func.inputs { + input_visitor.visit_ty(arg); + } + // extract lifetimes in output type + if let Return(ty) = func.output { + output_visitor.visit_ty(ty); + } + for lt in named_generics { + input_visitor.visit_generic_param(lt); + } + + if input_visitor.abort() || output_visitor.abort() { + return false; + } + + let input_lts = input_visitor.lts; + let output_lts = output_visitor.lts; + + if let Some(trait_sig) = trait_sig { + if explicit_self_type(cx, func, trait_sig.first().copied()) { + return false; + } + } + + if let Some(body_id) = body { + let body = cx.tcx.hir().body(body_id); + + let first_ident = body.params.first().and_then(|param| param.pat.simple_ident()); + if explicit_self_type(cx, func, first_ident) { + return false; + } + + let mut checker = BodyLifetimeChecker { + lifetimes_used_in_body: false, + }; + checker.visit_expr(&body.value); + if checker.lifetimes_used_in_body { + return false; + } + } + + // check for lifetimes from higher scopes + for lt in input_lts.iter().chain(output_lts.iter()) { + if !allowed_lts.contains(lt) { + return false; + } + } + + // check for higher-ranked trait bounds + if !input_visitor.nested_elision_site_lts.is_empty() || !output_visitor.nested_elision_site_lts.is_empty() { + let allowed_lts: FxHashSet<_> = allowed_lts + .iter() + .filter_map(|lt| match lt { + RefLt::Named(def_id) => Some(cx.tcx.item_name(def_id.to_def_id())), + _ => None, + }) + .collect(); + for lt in input_visitor.nested_elision_site_lts { + if let RefLt::Named(def_id) = lt { + if allowed_lts.contains(&cx.tcx.item_name(def_id.to_def_id())) { + return false; + } + } + } + for lt in output_visitor.nested_elision_site_lts { + if let RefLt::Named(def_id) = lt { + if allowed_lts.contains(&cx.tcx.item_name(def_id.to_def_id())) { + return false; + } + } + } + } + + // no input lifetimes? easy case! + if input_lts.is_empty() { + false + } else if output_lts.is_empty() { + // no output lifetimes, check distinctness of input lifetimes + + // only unnamed and static, ok + let unnamed_and_static = input_lts.iter().all(|lt| *lt == RefLt::Unnamed || *lt == RefLt::Static); + if unnamed_and_static { + return false; + } + // we have no output reference, so we only need all distinct lifetimes + input_lts.len() == unique_lifetimes(&input_lts) + } else { + // we have output references, so we need one input reference, + // and all output lifetimes must be the same + if unique_lifetimes(&output_lts) > 1 { + return false; + } + if input_lts.len() == 1 { + match (&input_lts[0], &output_lts[0]) { + (&RefLt::Named(n1), &RefLt::Named(n2)) if n1 == n2 => true, + (&RefLt::Named(_), &RefLt::Unnamed) => true, + _ => false, /* already elided, different named lifetimes + * or something static going on */ + } + } else { + false + } + } +} + +fn allowed_lts_from(tcx: TyCtxt<'_>, named_generics: &[GenericParam<'_>]) -> FxHashSet<RefLt> { + let mut allowed_lts = FxHashSet::default(); + for par in named_generics.iter() { + if let GenericParamKind::Lifetime { .. } = par.kind { + allowed_lts.insert(RefLt::Named(tcx.hir().local_def_id(par.hir_id))); + } + } + allowed_lts.insert(RefLt::Unnamed); + allowed_lts.insert(RefLt::Static); + allowed_lts +} + +/// Number of unique lifetimes in the given vector. +#[must_use] +fn unique_lifetimes(lts: &[RefLt]) -> usize { + lts.iter().collect::<FxHashSet<_>>().len() +} + +const CLOSURE_TRAIT_BOUNDS: [LangItem; 3] = [LangItem::Fn, LangItem::FnMut, LangItem::FnOnce]; + +/// A visitor usable for `rustc_front::visit::walk_ty()`. +struct RefVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + lts: Vec<RefLt>, + nested_elision_site_lts: Vec<RefLt>, + unelided_trait_object_lifetime: bool, +} + +impl<'a, 'tcx> RefVisitor<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> Self { + Self { + cx, + lts: Vec::new(), + nested_elision_site_lts: Vec::new(), + unelided_trait_object_lifetime: false, + } + } + + fn record(&mut self, lifetime: &Option<Lifetime>) { + if let Some(ref lt) = *lifetime { + if lt.name == LifetimeName::Static { + self.lts.push(RefLt::Static); + } else if let LifetimeName::Param(_, ParamName::Fresh) = lt.name { + // Fresh lifetimes generated should be ignored. + self.lts.push(RefLt::Unnamed); + } else if lt.is_elided() { + self.lts.push(RefLt::Unnamed); + } else if let LifetimeName::Param(def_id, _) = lt.name { + self.lts.push(RefLt::Named(def_id)); + } else { + self.lts.push(RefLt::Unnamed); + } + } else { + self.lts.push(RefLt::Unnamed); + } + } + + fn all_lts(&self) -> Vec<RefLt> { + self.lts + .iter() + .chain(self.nested_elision_site_lts.iter()) + .cloned() + .collect::<Vec<_>>() + } + + fn abort(&self) -> bool { + self.unelided_trait_object_lifetime + } +} + +impl<'a, 'tcx> Visitor<'tcx> for RefVisitor<'a, 'tcx> { + // for lifetimes as parameters of generics + fn visit_lifetime(&mut self, lifetime: &'tcx Lifetime) { + self.record(&Some(*lifetime)); + } + + fn visit_poly_trait_ref(&mut self, poly_tref: &'tcx PolyTraitRef<'tcx>, tbm: TraitBoundModifier) { + let trait_ref = &poly_tref.trait_ref; + if CLOSURE_TRAIT_BOUNDS.iter().any(|&item| { + self.cx + .tcx + .lang_items() + .require(item) + .map_or(false, |id| Some(id) == trait_ref.trait_def_id()) + }) { + let mut sub_visitor = RefVisitor::new(self.cx); + sub_visitor.visit_trait_ref(trait_ref); + self.nested_elision_site_lts.append(&mut sub_visitor.all_lts()); + } else { + walk_poly_trait_ref(self, poly_tref, tbm); + } + } + + fn visit_ty(&mut self, ty: &'tcx Ty<'_>) { + match ty.kind { + TyKind::OpaqueDef(item, bounds) => { + let map = self.cx.tcx.hir(); + let item = map.item(item); + let len = self.lts.len(); + walk_item(self, item); + self.lts.truncate(len); + self.lts.extend(bounds.iter().filter_map(|bound| match bound { + GenericArg::Lifetime(l) => Some(if let LifetimeName::Param(def_id, _) = l.name { + RefLt::Named(def_id) + } else { + RefLt::Unnamed + }), + _ => None, + })); + }, + TyKind::BareFn(&BareFnTy { decl, .. }) => { + let mut sub_visitor = RefVisitor::new(self.cx); + sub_visitor.visit_fn_decl(decl); + self.nested_elision_site_lts.append(&mut sub_visitor.all_lts()); + }, + TyKind::TraitObject(bounds, ref lt, _) => { + if !lt.is_elided() { + self.unelided_trait_object_lifetime = true; + } + for bound in bounds { + self.visit_poly_trait_ref(bound, TraitBoundModifier::None); + } + }, + _ => walk_ty(self, ty), + } + } +} + +/// Are any lifetimes mentioned in the `where` clause? If so, we don't try to +/// reason about elision. +fn has_where_lifetimes<'tcx>(cx: &LateContext<'tcx>, generics: &'tcx Generics<'_>) -> bool { + for predicate in generics.predicates { + match *predicate { + WherePredicate::RegionPredicate(..) => return true, + WherePredicate::BoundPredicate(ref pred) => { + // a predicate like F: Trait or F: for<'a> Trait<'a> + let mut visitor = RefVisitor::new(cx); + // walk the type F, it may not contain LT refs + walk_ty(&mut visitor, pred.bounded_ty); + if !visitor.all_lts().is_empty() { + return true; + } + // if the bounds define new lifetimes, they are fine to occur + let allowed_lts = allowed_lts_from(cx.tcx, pred.bound_generic_params); + // now walk the bounds + for bound in pred.bounds.iter() { + walk_param_bound(&mut visitor, bound); + } + // and check that all lifetimes are allowed + if visitor.all_lts().iter().any(|it| !allowed_lts.contains(it)) { + return true; + } + }, + WherePredicate::EqPredicate(ref pred) => { + let mut visitor = RefVisitor::new(cx); + walk_ty(&mut visitor, pred.lhs_ty); + walk_ty(&mut visitor, pred.rhs_ty); + if !visitor.lts.is_empty() { + return true; + } + }, + } + } + false +} + +struct LifetimeChecker<'cx, 'tcx, F> { + cx: &'cx LateContext<'tcx>, + map: FxHashMap<Symbol, Span>, + phantom: std::marker::PhantomData<F>, +} + +impl<'cx, 'tcx, F> LifetimeChecker<'cx, 'tcx, F> { + fn new(cx: &'cx LateContext<'tcx>, map: FxHashMap<Symbol, Span>) -> LifetimeChecker<'cx, 'tcx, F> { + Self { + cx, + map, + phantom: std::marker::PhantomData, + } + } +} + +impl<'cx, 'tcx, F> Visitor<'tcx> for LifetimeChecker<'cx, 'tcx, F> +where + F: NestedFilter<'tcx>, +{ + type Map = rustc_middle::hir::map::Map<'tcx>; + type NestedFilter = F; + + // for lifetimes as parameters of generics + fn visit_lifetime(&mut self, lifetime: &'tcx Lifetime) { + self.map.remove(&lifetime.name.ident().name); + } + + fn visit_generic_param(&mut self, param: &'tcx GenericParam<'_>) { + // don't actually visit `<'a>` or `<'a: 'b>` + // we've already visited the `'a` declarations and + // don't want to spuriously remove them + // `'b` in `'a: 'b` is useless unless used elsewhere in + // a non-lifetime bound + if let GenericParamKind::Type { .. } = param.kind { + walk_generic_param(self, param); + } + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +fn report_extra_lifetimes<'tcx>(cx: &LateContext<'tcx>, func: &'tcx FnDecl<'_>, generics: &'tcx Generics<'_>) { + let hs = generics + .params + .iter() + .filter_map(|par| match par.kind { + GenericParamKind::Lifetime { .. } => Some((par.name.ident().name, par.span)), + _ => None, + }) + .collect(); + let mut checker = LifetimeChecker::<hir_nested_filter::None>::new(cx, hs); + + walk_generics(&mut checker, generics); + walk_fn_decl(&mut checker, func); + + for &v in checker.map.values() { + span_lint( + cx, + EXTRA_UNUSED_LIFETIMES, + v, + "this lifetime isn't used in the function definition", + ); + } +} + +fn report_extra_impl_lifetimes<'tcx>(cx: &LateContext<'tcx>, impl_: &'tcx Impl<'_>) { + let hs = impl_ + .generics + .params + .iter() + .filter_map(|par| match par.kind { + GenericParamKind::Lifetime { .. } => Some((par.name.ident().name, par.span)), + _ => None, + }) + .collect(); + let mut checker = LifetimeChecker::<middle_nested_filter::All>::new(cx, hs); + + walk_generics(&mut checker, impl_.generics); + if let Some(ref trait_ref) = impl_.of_trait { + walk_trait_ref(&mut checker, trait_ref); + } + walk_ty(&mut checker, impl_.self_ty); + for item in impl_.items { + walk_impl_item_ref(&mut checker, item); + } + + for &v in checker.map.values() { + span_lint(cx, EXTRA_UNUSED_LIFETIMES, v, "this lifetime isn't used in the impl"); + } +} + +struct BodyLifetimeChecker { + lifetimes_used_in_body: bool, +} + +impl<'tcx> Visitor<'tcx> for BodyLifetimeChecker { + // for lifetimes as parameters of generics + fn visit_lifetime(&mut self, lifetime: &'tcx Lifetime) { + if lifetime.name.ident().name != kw::UnderscoreLifetime && lifetime.name.ident().name != kw::StaticLifetime { + self.lifetimes_used_in_body = true; + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/literal_representation.rs b/src/tools/clippy/clippy_lints/src/literal_representation.rs new file mode 100644 index 000000000..fb2104861 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/literal_representation.rs @@ -0,0 +1,534 @@ +//! Lints concerned with the grouping of digits with underscores in integral or +//! floating-point literal expressions. + +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::numeric_literal::{NumericLiteral, Radix}; +use clippy_utils::source::snippet_opt; +use if_chain::if_chain; +use rustc_ast::ast::{Expr, ExprKind, Lit, 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}; +use std::iter; + +declare_clippy_lint! { + /// ### What it does + /// Warns if a long integral or floating-point constant does + /// not contain underscores. + /// + /// ### Why is this bad? + /// Reading long numbers is difficult without separators. + /// + /// ### Example + /// ```rust + /// # let _: u64 = + /// 61864918973511 + /// # ; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let _: u64 = + /// 61_864_918_973_511 + /// # ; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNREADABLE_LITERAL, + pedantic, + "long literal without underscores" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns for mistyped suffix in literals + /// + /// ### Why is this bad? + /// This is most probably a typo + /// + /// ### Known problems + /// - Does not match on integers too large to fit in the corresponding unsigned type + /// - Does not match on `_127` since that is a valid grouping for decimal and octal numbers + /// + /// ### Example + /// ```ignore + /// `2_32` => `2_i32` + /// `250_8 => `250_u8` + /// ``` + #[clippy::version = "1.30.0"] + pub MISTYPED_LITERAL_SUFFIXES, + correctness, + "mistyped literal suffix" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if an integral or floating-point constant is + /// grouped inconsistently with underscores. + /// + /// ### Why is this bad? + /// Readers may incorrectly interpret inconsistently + /// grouped digits. + /// + /// ### Example + /// ```rust + /// # let _: u64 = + /// 618_64_9189_73_511 + /// # ; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let _: u64 = + /// 61_864_918_973_511 + /// # ; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INCONSISTENT_DIGIT_GROUPING, + style, + "integer literals with digits grouped inconsistently" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if hexadecimal or binary literals are not grouped + /// by nibble or byte. + /// + /// ### Why is this bad? + /// Negatively impacts readability. + /// + /// ### Example + /// ```rust + /// let x: u32 = 0xFFF_FFF; + /// let y: u8 = 0b01_011_101; + /// ``` + #[clippy::version = "1.49.0"] + pub UNUSUAL_BYTE_GROUPINGS, + style, + "binary or hex literals that aren't grouped by four" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if the digits of an integral or floating-point + /// constant are grouped into groups that + /// are too large. + /// + /// ### Why is this bad? + /// Negatively impacts readability. + /// + /// ### Example + /// ```rust + /// let x: u64 = 6186491_8973511; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LARGE_DIGIT_GROUPS, + pedantic, + "grouping digits into groups that are too large" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if there is a better representation for a numeric literal. + /// + /// ### Why is this bad? + /// Especially for big powers of 2 a hexadecimal representation is more + /// readable than a decimal representation. + /// + /// ### Example + /// ```text + /// `255` => `0xFF` + /// `65_535` => `0xFFFF` + /// `4_042_322_160` => `0xF0F0_F0F0` + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DECIMAL_LITERAL_REPRESENTATION, + restriction, + "using decimal representation when hexadecimal would be better" +} + +enum WarningType { + UnreadableLiteral, + InconsistentDigitGrouping, + LargeDigitGroups, + DecimalRepresentation, + MistypedLiteralSuffix, + UnusualByteGroupings, +} + +impl WarningType { + fn display(&self, suggested_format: String, cx: &EarlyContext<'_>, span: rustc_span::Span) { + match self { + Self::MistypedLiteralSuffix => span_lint_and_sugg( + cx, + MISTYPED_LITERAL_SUFFIXES, + span, + "mistyped literal suffix", + "did you mean to write", + suggested_format, + Applicability::MaybeIncorrect, + ), + Self::UnreadableLiteral => span_lint_and_sugg( + cx, + UNREADABLE_LITERAL, + span, + "long literal lacking separators", + "consider", + suggested_format, + Applicability::MachineApplicable, + ), + Self::LargeDigitGroups => span_lint_and_sugg( + cx, + LARGE_DIGIT_GROUPS, + span, + "digit groups should be smaller", + "consider", + suggested_format, + Applicability::MachineApplicable, + ), + Self::InconsistentDigitGrouping => span_lint_and_sugg( + cx, + INCONSISTENT_DIGIT_GROUPING, + span, + "digits grouped inconsistently by underscores", + "consider", + suggested_format, + Applicability::MachineApplicable, + ), + Self::DecimalRepresentation => span_lint_and_sugg( + cx, + DECIMAL_LITERAL_REPRESENTATION, + span, + "integer literal has a better hexadecimal representation", + "consider", + suggested_format, + Applicability::MachineApplicable, + ), + Self::UnusualByteGroupings => span_lint_and_sugg( + cx, + UNUSUAL_BYTE_GROUPINGS, + span, + "digits of hex or binary literal not grouped by four", + "consider", + suggested_format, + Applicability::MachineApplicable, + ), + }; + } +} + +#[derive(Copy, Clone)] +pub struct LiteralDigitGrouping { + lint_fraction_readability: bool, +} + +impl_lint_pass!(LiteralDigitGrouping => [ + UNREADABLE_LITERAL, + INCONSISTENT_DIGIT_GROUPING, + LARGE_DIGIT_GROUPS, + MISTYPED_LITERAL_SUFFIXES, + UNUSUAL_BYTE_GROUPINGS, +]); + +impl EarlyLintPass for LiteralDigitGrouping { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if let ExprKind::Lit(ref lit) = expr.kind { + self.check_lit(cx, lit); + } + } +} + +// Length of each UUID hyphenated group in hex digits. +const UUID_GROUP_LENS: [usize; 5] = [8, 4, 4, 4, 12]; + +impl LiteralDigitGrouping { + pub fn new(lint_fraction_readability: bool) -> Self { + Self { + lint_fraction_readability, + } + } + + fn check_lit(self, cx: &EarlyContext<'_>, lit: &Lit) { + if_chain! { + if let Some(src) = snippet_opt(cx, lit.span); + if let Some(mut num_lit) = NumericLiteral::from_lit(&src, lit); + then { + if !Self::check_for_mistyped_suffix(cx, lit.span, &mut num_lit) { + return; + } + + if Self::is_literal_uuid_formatted(&mut num_lit) { + return; + } + + let result = (|| { + + let integral_group_size = Self::get_group_size(num_lit.integer.split('_'), num_lit.radix, true)?; + if let Some(fraction) = num_lit.fraction { + let fractional_group_size = Self::get_group_size( + fraction.rsplit('_'), + num_lit.radix, + self.lint_fraction_readability)?; + + let consistent = Self::parts_consistent(integral_group_size, + fractional_group_size, + num_lit.integer.len(), + fraction.len()); + if !consistent { + return Err(WarningType::InconsistentDigitGrouping); + }; + } + + Ok(()) + })(); + + + if let Err(warning_type) = result { + let should_warn = match warning_type { + | WarningType::UnreadableLiteral + | WarningType::InconsistentDigitGrouping + | WarningType::UnusualByteGroupings + | WarningType::LargeDigitGroups => { + !lit.span.from_expansion() + } + WarningType::DecimalRepresentation | WarningType::MistypedLiteralSuffix => { + true + } + }; + if should_warn { + warning_type.display(num_lit.format(), cx, lit.span); + } + } + } + } + } + + // Returns `false` if the check fails + fn check_for_mistyped_suffix( + cx: &EarlyContext<'_>, + span: rustc_span::Span, + num_lit: &mut NumericLiteral<'_>, + ) -> bool { + if num_lit.suffix.is_some() { + return true; + } + + let (part, mistyped_suffixes, is_float) = if let Some((_, exponent)) = &mut num_lit.exponent { + (exponent, &["32", "64"][..], true) + } else if num_lit.fraction.is_some() { + return true; + } else { + (&mut num_lit.integer, &["8", "16", "32", "64"][..], false) + }; + + let mut split = part.rsplit('_'); + let last_group = split.next().expect("At least one group"); + if split.next().is_some() && mistyped_suffixes.contains(&last_group) { + let main_part = &part[..part.len() - last_group.len()]; + let missing_char; + if is_float { + missing_char = 'f'; + } else { + let radix = match num_lit.radix { + Radix::Binary => 2, + Radix::Octal => 8, + Radix::Decimal => 10, + Radix::Hexadecimal => 16, + }; + if let Ok(int) = u64::from_str_radix(&main_part.replace('_', ""), radix) { + missing_char = match (last_group, int) { + ("8", i) if i8::try_from(i).is_ok() => 'i', + ("16", i) if i16::try_from(i).is_ok() => 'i', + ("32", i) if i32::try_from(i).is_ok() => 'i', + ("64", i) if i64::try_from(i).is_ok() => 'i', + ("8", u) if u8::try_from(u).is_ok() => 'u', + ("16", u) if u16::try_from(u).is_ok() => 'u', + ("32", u) if u32::try_from(u).is_ok() => 'u', + ("64", _) => 'u', + _ => { + return true; + }, + } + } else { + return true; + } + } + *part = main_part; + let mut sugg = num_lit.format(); + sugg.push('_'); + sugg.push(missing_char); + sugg.push_str(last_group); + WarningType::MistypedLiteralSuffix.display(sugg, cx, span); + false + } else { + true + } + } + + /// Checks whether the numeric literal matches the formatting of a UUID. + /// + /// Returns `true` if the radix is hexadecimal, and the groups match the + /// UUID format of 8-4-4-4-12. + fn is_literal_uuid_formatted(num_lit: &mut NumericLiteral<'_>) -> bool { + if num_lit.radix != Radix::Hexadecimal { + return false; + } + + // UUIDs should not have a fraction + if num_lit.fraction.is_some() { + return false; + } + + let group_sizes: Vec<usize> = num_lit.integer.split('_').map(str::len).collect(); + if UUID_GROUP_LENS.len() == group_sizes.len() { + iter::zip(&UUID_GROUP_LENS, &group_sizes).all(|(&a, &b)| a == b) + } else { + false + } + } + + /// Given the sizes of the digit groups of both integral and fractional + /// parts, and the length + /// of both parts, determine if the digits have been grouped consistently. + #[must_use] + fn parts_consistent( + int_group_size: Option<usize>, + frac_group_size: Option<usize>, + int_size: usize, + frac_size: usize, + ) -> bool { + match (int_group_size, frac_group_size) { + // No groups on either side of decimal point - trivially consistent. + (None, None) => true, + // Integral part has grouped digits, fractional part does not. + (Some(int_group_size), None) => frac_size <= int_group_size, + // Fractional part has grouped digits, integral part does not. + (None, Some(frac_group_size)) => int_size <= frac_group_size, + // Both parts have grouped digits. Groups should be the same size. + (Some(int_group_size), Some(frac_group_size)) => int_group_size == frac_group_size, + } + } + + /// Returns the size of the digit groups (or None if ungrouped) if successful, + /// otherwise returns a `WarningType` for linting. + fn get_group_size<'a>( + groups: impl Iterator<Item = &'a str>, + radix: Radix, + lint_unreadable: bool, + ) -> Result<Option<usize>, WarningType> { + let mut groups = groups.map(str::len); + + let first = groups.next().expect("At least one group"); + + if (radix == Radix::Binary || radix == Radix::Hexadecimal) && groups.any(|i| i != 4 && i != 2) { + return Err(WarningType::UnusualByteGroupings); + } + + if let Some(second) = groups.next() { + if !groups.all(|x| x == second) || first > second { + Err(WarningType::InconsistentDigitGrouping) + } else if second > 4 { + Err(WarningType::LargeDigitGroups) + } else { + Ok(Some(second)) + } + } else if first > 5 && lint_unreadable { + Err(WarningType::UnreadableLiteral) + } else { + Ok(None) + } + } +} + +#[expect(clippy::module_name_repetitions)] +#[derive(Copy, Clone)] +pub struct DecimalLiteralRepresentation { + threshold: u64, +} + +impl_lint_pass!(DecimalLiteralRepresentation => [DECIMAL_LITERAL_REPRESENTATION]); + +impl EarlyLintPass for DecimalLiteralRepresentation { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if let ExprKind::Lit(ref lit) = expr.kind { + self.check_lit(cx, lit); + } + } +} + +impl DecimalLiteralRepresentation { + #[must_use] + pub fn new(threshold: u64) -> Self { + Self { threshold } + } + fn check_lit(self, cx: &EarlyContext<'_>, lit: &Lit) { + // Lint integral literals. + if_chain! { + if let LitKind::Int(val, _) = lit.kind; + if let Some(src) = snippet_opt(cx, lit.span); + if let Some(num_lit) = NumericLiteral::from_lit(&src, lit); + if num_lit.radix == Radix::Decimal; + if val >= u128::from(self.threshold); + then { + let hex = format!("{:#X}", val); + let num_lit = NumericLiteral::new(&hex, num_lit.suffix, false); + let _ = Self::do_lint(num_lit.integer).map_err(|warning_type| { + warning_type.display(num_lit.format(), cx, lit.span); + }); + } + } + } + + fn do_lint(digits: &str) -> Result<(), WarningType> { + if digits.len() == 1 { + // Lint for 1 digit literals, if someone really sets the threshold that low + if digits == "1" + || digits == "2" + || digits == "4" + || digits == "8" + || digits == "3" + || digits == "7" + || digits == "F" + { + return Err(WarningType::DecimalRepresentation); + } + } else if digits.len() < 4 { + // Lint for Literals with a hex-representation of 2 or 3 digits + let f = &digits[0..1]; // first digit + let s = &digits[1..]; // suffix + + // Powers of 2 + if ((f.eq("1") || f.eq("2") || f.eq("4") || f.eq("8")) && s.chars().all(|c| c == '0')) + // Powers of 2 minus 1 + || ((f.eq("1") || f.eq("3") || f.eq("7") || f.eq("F")) && s.chars().all(|c| c == 'F')) + { + return Err(WarningType::DecimalRepresentation); + } + } else { + // Lint for Literals with a hex-representation of 4 digits or more + let f = &digits[0..1]; // first digit + let m = &digits[1..digits.len() - 1]; // middle digits, except last + let s = &digits[1..]; // suffix + + // Powers of 2 with a margin of +15/-16 + if ((f.eq("1") || f.eq("2") || f.eq("4") || f.eq("8")) && m.chars().all(|c| c == '0')) + || ((f.eq("1") || f.eq("3") || f.eq("7") || f.eq("F")) && m.chars().all(|c| c == 'F')) + // Lint for representations with only 0s and Fs, while allowing 7 as the first + // digit + || ((f.eq("7") || f.eq("F")) && s.chars().all(|c| c == '0' || c == 'F')) + { + return Err(WarningType::DecimalRepresentation); + } + } + + Ok(()) + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/empty_loop.rs b/src/tools/clippy/clippy_lints/src/loops/empty_loop.rs new file mode 100644 index 000000000..823cf0f43 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/empty_loop.rs @@ -0,0 +1,18 @@ +use super::EMPTY_LOOP; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{is_in_panic_handler, is_no_std_crate}; + +use rustc_hir::{Block, Expr}; +use rustc_lint::LateContext; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, loop_block: &Block<'_>) { + if loop_block.stmts.is_empty() && loop_block.expr.is_none() && !is_in_panic_handler(cx, expr) { + let msg = "empty `loop {}` wastes CPU cycles"; + let help = if is_no_std_crate(cx) { + "you should either use `panic!()` or add a call pausing or sleeping the thread to the loop body" + } else { + "you should either use `panic!()` or add `std::thread::sleep(..);` to the loop body" + }; + span_lint_and_help(cx, EMPTY_LOOP, expr.span, msg, None, help); + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/explicit_counter_loop.rs b/src/tools/clippy/clippy_lints/src/loops/explicit_counter_loop.rs new file mode 100644 index 000000000..8e3ab26a9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/explicit_counter_loop.rs @@ -0,0 +1,93 @@ +use super::{make_iterator_snippet, IncrementVisitor, InitializeVisitor, EXPLICIT_COUNTER_LOOP}; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{get_enclosing_block, is_integer_const}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_block, walk_expr}; +use rustc_hir::{Expr, Pat}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty, UintTy}; + +// To trigger the EXPLICIT_COUNTER_LOOP lint, a variable must be +// incremented exactly once in the loop body, and initialized to zero +// at the start of the loop. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + arg: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + expr: &'tcx Expr<'_>, +) { + // Look for variables that are incremented once per loop iteration. + let mut increment_visitor = IncrementVisitor::new(cx); + walk_expr(&mut increment_visitor, body); + + // For each candidate, check the parent block to see if + // it's initialized to zero at the start of the loop. + if let Some(block) = get_enclosing_block(cx, expr.hir_id) { + for id in increment_visitor.into_results() { + let mut initialize_visitor = InitializeVisitor::new(cx, expr, id); + walk_block(&mut initialize_visitor, block); + + if_chain! { + if let Some((name, ty, initializer)) = initialize_visitor.get_result(); + if is_integer_const(cx, initializer, 0); + then { + let mut applicability = Applicability::MaybeIncorrect; + let span = expr.span.with_hi(arg.span.hi()); + + let int_name = match ty.map(Ty::kind) { + // usize or inferred + Some(ty::Uint(UintTy::Usize)) | None => { + span_lint_and_sugg( + cx, + EXPLICIT_COUNTER_LOOP, + span, + &format!("the variable `{}` is used as a loop counter", name), + "consider using", + format!( + "for ({}, {}) in {}.enumerate()", + name, + snippet_with_applicability(cx, pat.span, "item", &mut applicability), + make_iterator_snippet(cx, arg, &mut applicability), + ), + applicability, + ); + return; + } + Some(ty::Int(int_ty)) => int_ty.name_str(), + Some(ty::Uint(uint_ty)) => uint_ty.name_str(), + _ => return, + }; + + span_lint_and_then( + cx, + EXPLICIT_COUNTER_LOOP, + span, + &format!("the variable `{}` is used as a loop counter", name), + |diag| { + diag.span_suggestion( + span, + "consider using", + format!( + "for ({}, {}) in (0_{}..).zip({})", + name, + snippet_with_applicability(cx, pat.span, "item", &mut applicability), + int_name, + make_iterator_snippet(cx, arg, &mut applicability), + ), + applicability, + ); + + diag.note(&format!( + "`{}` is of type `{}`, making it ineligible for `Iterator::enumerate`", + name, int_name + )); + }, + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/explicit_into_iter_loop.rs b/src/tools/clippy/clippy_lints/src/loops/explicit_into_iter_loop.rs new file mode 100644 index 000000000..175e2b382 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/explicit_into_iter_loop.rs @@ -0,0 +1,29 @@ +use super::EXPLICIT_INTO_ITER_LOOP; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_trait_method; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +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)) { + return; + } + + let mut applicability = Applicability::MachineApplicable; + let object = snippet_with_applicability(cx, self_arg.span, "_", &mut applicability); + span_lint_and_sugg( + cx, + EXPLICIT_INTO_ITER_LOOP, + call_expr.span, + "it is more concise to loop over containers instead of using explicit \ + iteration methods", + "to write this more concisely, try", + object.to_string(), + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/loops/explicit_iter_loop.rs b/src/tools/clippy/clippy_lints/src/loops/explicit_iter_loop.rs new file mode 100644 index 000000000..5f5beccd0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/explicit_iter_loop.rs @@ -0,0 +1,75 @@ +use super::EXPLICIT_ITER_LOOP; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_trait_method; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::{Expr, Mutability}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +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 { + 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, + "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), + 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) +} + +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_usize(cx.tcx, cx.param_env) + .map_or(false, |val| (0..=32).contains(&val)), + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/for_kv_map.rs b/src/tools/clippy/clippy_lints/src/loops/for_kv_map.rs new file mode 100644 index 000000000..bee0e1d76 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/for_kv_map.rs @@ -0,0 +1,66 @@ +use super::FOR_KV_MAP; +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::source::snippet; +use clippy_utils::sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::visitors::is_local_used; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, Pat, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::sym; + +/// Checks for the `FOR_KV_MAP` lint. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>, arg: &'tcx Expr<'_>, body: &'tcx Expr<'_>) { + let pat_span = pat.span; + + if let PatKind::Tuple(pat, _) = pat.kind { + if pat.len() == 2 { + let arg_span = arg.span; + let (new_pat_span, kind, ty, mutbl) = match *cx.typeck_results().expr_ty(arg).kind() { + ty::Ref(_, ty, mutbl) => match (&pat[0].kind, &pat[1].kind) { + (key, _) if pat_is_wild(cx, key, body) => (pat[1].span, "value", ty, mutbl), + (_, value) if pat_is_wild(cx, value, body) => (pat[0].span, "key", ty, Mutability::Not), + _ => return, + }, + _ => return, + }; + let mutbl = match mutbl { + Mutability::Not => "", + Mutability::Mut => "_mut", + }; + let arg = match arg.kind { + ExprKind::AddrOf(BorrowKind::Ref, _, expr) => expr, + _ => arg, + }; + + if is_type_diagnostic_item(cx, ty, sym::HashMap) || is_type_diagnostic_item(cx, ty, sym::BTreeMap) { + span_lint_and_then( + cx, + FOR_KV_MAP, + arg_span, + &format!("you seem to want to iterate on a map's {}s", kind), + |diag| { + let map = sugg::Sugg::hir(cx, arg, "map"); + multispan_sugg( + diag, + "use the corresponding method", + vec![ + (pat_span, snippet(cx, new_pat_span, kind).into_owned()), + (arg_span, format!("{}.{}s{}()", map.maybe_par(), kind, mutbl)), + ], + ); + }, + ); + } + } + } +} + +/// Returns `true` if the pattern is a `PatWild` or an ident prefixed with `_`. +fn pat_is_wild<'tcx>(cx: &LateContext<'tcx>, pat: &'tcx PatKind<'_>, body: &'tcx Expr<'_>) -> bool { + match *pat { + PatKind::Wild => true, + PatKind::Binding(_, id, ident, None) if ident.as_str().starts_with('_') => !is_local_used(cx, body, id), + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/for_loops_over_fallibles.rs b/src/tools/clippy/clippy_lints/src/loops/for_loops_over_fallibles.rs new file mode 100644 index 000000000..77de90fd7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/for_loops_over_fallibles.rs @@ -0,0 +1,65 @@ +use super::FOR_LOOPS_OVER_FALLIBLES; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_hir::{Expr, Pat}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +/// Checks for `for` loops over `Option`s and `Result`s. +pub(super) fn check(cx: &LateContext<'_>, pat: &Pat<'_>, arg: &Expr<'_>, method_name: Option<&str>) { + let ty = cx.typeck_results().expr_ty(arg); + if is_type_diagnostic_item(cx, ty, sym::Option) { + let help_string = if let Some(method_name) = method_name { + format!( + "consider replacing `for {0} in {1}.{method_name}()` with `if let Some({0}) = {1}`", + snippet(cx, pat.span, "_"), + snippet(cx, arg.span, "_") + ) + } else { + format!( + "consider replacing `for {0} in {1}` with `if let Some({0}) = {1}`", + snippet(cx, pat.span, "_"), + snippet(cx, arg.span, "_") + ) + }; + span_lint_and_help( + cx, + FOR_LOOPS_OVER_FALLIBLES, + arg.span, + &format!( + "for loop over `{0}`, which is an `Option`. This is more readably written as an \ + `if let` statement", + snippet(cx, arg.span, "_") + ), + None, + &help_string, + ); + } else if is_type_diagnostic_item(cx, ty, sym::Result) { + let help_string = if let Some(method_name) = method_name { + format!( + "consider replacing `for {0} in {1}.{method_name}()` with `if let Ok({0}) = {1}`", + snippet(cx, pat.span, "_"), + snippet(cx, arg.span, "_") + ) + } else { + format!( + "consider replacing `for {0} in {1}` with `if let Ok({0}) = {1}`", + snippet(cx, pat.span, "_"), + snippet(cx, arg.span, "_") + ) + }; + span_lint_and_help( + cx, + FOR_LOOPS_OVER_FALLIBLES, + arg.span, + &format!( + "for loop over `{0}`, which is a `Result`. This is more readably written as an \ + `if let` statement", + snippet(cx, arg.span, "_") + ), + None, + &help_string, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/iter_next_loop.rs b/src/tools/clippy/clippy_lints/src/loops/iter_next_loop.rs new file mode 100644 index 000000000..e640c62eb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/iter_next_loop.rs @@ -0,0 +1,21 @@ +use super::ITER_NEXT_LOOP; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::is_trait_method; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::sym; + +pub(super) fn check(cx: &LateContext<'_>, arg: &Expr<'_>) -> bool { + if is_trait_method(cx, arg, sym::Iterator) { + span_lint( + cx, + ITER_NEXT_LOOP, + arg.span, + "you are iterating over `Iterator::next()` which is an Option; this will compile but is \ + probably not what you want", + ); + true + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/manual_find.rs b/src/tools/clippy/clippy_lints/src/loops/manual_find.rs new file mode 100644 index 000000000..215c83a7e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/manual_find.rs @@ -0,0 +1,158 @@ +use super::utils::make_iterator_snippet; +use super::MANUAL_FIND; +use clippy_utils::{ + diagnostics::span_lint_and_then, higher, is_lang_ctor, path_res, peel_blocks_with_stmt, + source::snippet_with_applicability, ty::implements_trait, +}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{ + def::Res, lang_items::LangItem, BindingAnnotation, Block, Expr, ExprKind, HirId, Node, Pat, PatKind, Stmt, StmtKind, +}; +use rustc_lint::LateContext; +use rustc_span::source_map::Span; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + arg: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + span: Span, + expr: &'tcx Expr<'_>, +) { + let inner_expr = peel_blocks_with_stmt(body); + // Check for the specific case that the result is returned and optimize suggestion for that (more + // cases can be added later) + if_chain! { + if let Some(higher::If { cond, then, r#else: None, }) = higher::If::hir(inner_expr); + if let Some(binding_id) = get_binding(pat); + if let ExprKind::Block(block, _) = then.kind; + if let [stmt] = block.stmts; + if let StmtKind::Semi(semi) = stmt.kind; + if let ExprKind::Ret(Some(ret_value)) = semi.kind; + if let ExprKind::Call(Expr { kind: ExprKind::Path(ctor), .. }, [inner_ret]) = ret_value.kind; + if is_lang_ctor(cx, ctor, LangItem::OptionSome); + if path_res(cx, inner_ret) == Res::Local(binding_id); + if let Some((last_stmt, last_ret)) = last_stmt_and_ret(cx, expr); + then { + let mut applicability = Applicability::MachineApplicable; + let mut snippet = make_iterator_snippet(cx, arg, &mut applicability); + // Checks if `pat` is a single reference to a binding (`&x`) + let is_ref_to_binding = + matches!(pat.kind, PatKind::Ref(inner, _) if matches!(inner.kind, PatKind::Binding(..))); + // If `pat` is not a binding or a reference to a binding (`x` or `&x`) + // we need to map it to the binding returned by the function (i.e. `.map(|(x, _)| x)`) + if !(matches!(pat.kind, PatKind::Binding(..)) || is_ref_to_binding) { + snippet.push_str( + &format!( + ".map(|{}| {})", + snippet_with_applicability(cx, pat.span, "..", &mut applicability), + snippet_with_applicability(cx, inner_ret.span, "..", &mut applicability), + )[..], + ); + } + let ty = cx.typeck_results().expr_ty(inner_ret); + if cx.tcx.lang_items().copy_trait().map_or(false, |id| implements_trait(cx, ty, id, &[])) { + snippet.push_str( + &format!( + ".find(|{}{}| {})", + "&".repeat(1 + usize::from(is_ref_to_binding)), + snippet_with_applicability(cx, inner_ret.span, "..", &mut applicability), + snippet_with_applicability(cx, cond.span, "..", &mut applicability), + )[..], + ); + if is_ref_to_binding { + snippet.push_str(".copied()"); + } + } else { + applicability = Applicability::MaybeIncorrect; + snippet.push_str( + &format!( + ".find(|{}| {})", + snippet_with_applicability(cx, inner_ret.span, "..", &mut applicability), + snippet_with_applicability(cx, cond.span, "..", &mut applicability), + )[..], + ); + } + // Extends to `last_stmt` to include semicolon in case of `return None;` + let lint_span = span.to(last_stmt.span).to(last_ret.span); + span_lint_and_then( + cx, + MANUAL_FIND, + lint_span, + "manual implementation of `Iterator::find`", + |diag| { + if applicability == Applicability::MaybeIncorrect { + diag.note("you may need to dereference some variables"); + } + diag.span_suggestion( + lint_span, + "replace with an iterator", + snippet, + applicability, + ); + }, + ); + } + } +} + +fn get_binding(pat: &Pat<'_>) -> Option<HirId> { + let mut hir_id = None; + let mut count = 0; + pat.each_binding(|annotation, id, _, _| { + count += 1; + if count > 1 { + hir_id = None; + return; + } + if let BindingAnnotation::Unannotated = annotation { + hir_id = Some(id); + } + }); + hir_id +} + +// Returns the last statement and last return if function fits format for lint +fn last_stmt_and_ret<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, +) -> Option<(&'tcx Stmt<'tcx>, &'tcx Expr<'tcx>)> { + // Returns last non-return statement and the last return + fn extract<'tcx>(block: &Block<'tcx>) -> Option<(&'tcx Stmt<'tcx>, &'tcx Expr<'tcx>)> { + if let [.., last_stmt] = block.stmts { + if let Some(ret) = block.expr { + return Some((last_stmt, ret)); + } + if_chain! { + if let [.., snd_last, _] = block.stmts; + if let StmtKind::Semi(last_expr) = last_stmt.kind; + if let ExprKind::Ret(Some(ret)) = last_expr.kind; + then { + return Some((snd_last, ret)); + } + } + } + None + } + let mut parent_iter = cx.tcx.hir().parent_iter(expr.hir_id); + if_chain! { + // This should be the loop + if let Some((node_hir, Node::Stmt(..))) = parent_iter.next(); + // This should be the function body + if let Some((_, Node::Block(block))) = parent_iter.next(); + if let Some((last_stmt, last_ret)) = extract(block); + if last_stmt.hir_id == node_hir; + if let ExprKind::Path(path) = &last_ret.kind; + if is_lang_ctor(cx, path, LangItem::OptionNone); + if let Some((_, Node::Expr(_block))) = parent_iter.next(); + // This includes the function header + if let Some((_, func)) = parent_iter.next(); + if func.fn_kind().is_some(); + then { + Some((block.stmts.last().unwrap(), last_ret)) + } else { + None + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/manual_flatten.rs b/src/tools/clippy/clippy_lints/src/loops/manual_flatten.rs new file mode 100644 index 000000000..1d6ddf4b9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/manual_flatten.rs @@ -0,0 +1,85 @@ +use super::utils::make_iterator_snippet; +use super::MANUAL_FLATTEN; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::higher; +use clippy_utils::visitors::is_local_used; +use clippy_utils::{is_lang_ctor, path_to_local_id, peel_blocks_with_stmt}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{OptionSome, ResultOk}; +use rustc_hir::{Expr, Pat, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::source_map::Span; + +/// Check for unnecessary `if let` usage in a for loop where only the `Some` or `Ok` variant of the +/// iterator element is used. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + arg: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + span: Span, +) { + let inner_expr = peel_blocks_with_stmt(body); + if_chain! { + if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else: None }) + = higher::IfLet::hir(cx, inner_expr); + // Ensure match_expr in `if let` statement is the same as the pat from the for-loop + if let PatKind::Binding(_, pat_hir_id, _, _) = pat.kind; + if path_to_local_id(let_expr, pat_hir_id); + // Ensure the `if let` statement is for the `Some` variant of `Option` or the `Ok` variant of `Result` + if let PatKind::TupleStruct(ref qpath, _, _) = let_pat.kind; + let some_ctor = is_lang_ctor(cx, qpath, OptionSome); + let ok_ctor = is_lang_ctor(cx, qpath, ResultOk); + if some_ctor || ok_ctor; + // Ensure expr in `if let` is not used afterwards + if !is_local_used(cx, if_then, pat_hir_id); + then { + let if_let_type = if some_ctor { "Some" } else { "Ok" }; + // Prepare the error message + let msg = format!("unnecessary `if let` since only the `{}` variant of the iterator element is used", if_let_type); + + // Prepare the help message + let mut applicability = Applicability::MaybeIncorrect; + let arg_snippet = make_iterator_snippet(cx, arg, &mut applicability); + let copied = match cx.typeck_results().expr_ty(let_expr).kind() { + ty::Ref(_, inner, _) => match inner.kind() { + ty::Ref(..) => ".copied()", + _ => "" + } + _ => "" + }; + + let sugg = format!("{arg_snippet}{copied}.flatten()"); + + // If suggestion is not a one-liner, it won't be shown inline within the error message. In that case, + // it will be shown in the extra `help` message at the end, which is why the first `help_msg` needs + // to refer to the correct relative position of the suggestion. + let help_msg = if sugg.contains('\n') { + "remove the `if let` statement in the for loop and then..." + } else { + "...and remove the `if let` statement in the for loop" + }; + + span_lint_and_then( + cx, + MANUAL_FLATTEN, + span, + &msg, + |diag| { + diag.span_suggestion( + arg.span, + "try", + sugg, + applicability, + ); + diag.span_help( + inner_expr.span, + help_msg, + ); + } + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/manual_memcpy.rs b/src/tools/clippy/clippy_lints/src/loops/manual_memcpy.rs new file mode 100644 index 000000000..b31015d19 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/manual_memcpy.rs @@ -0,0 +1,461 @@ +use super::{IncrementVisitor, InitializeVisitor, MANUAL_MEMCPY}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::is_copy; +use clippy_utils::{get_enclosing_block, higher, path_to_local, sugg}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_hir::intravisit::walk_block; +use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, Pat, PatKind, StmtKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::symbol::sym; +use std::fmt::Display; +use std::iter::Iterator; + +/// Checks for for loops that sequentially copy items from one slice-like +/// object to another. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + arg: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + expr: &'tcx Expr<'_>, +) -> bool { + if let Some(higher::Range { + start: Some(start), + end: Some(end), + limits, + }) = higher::Range::hir(arg) + { + // the var must be a single name + if let PatKind::Binding(_, canonical_id, _, _) = pat.kind { + let mut starts = vec![Start { + id: canonical_id, + kind: StartKind::Range, + }]; + + // This is one of few ways to return different iterators + // derived from: https://stackoverflow.com/questions/29760668/conditionally-iterate-over-one-of-several-possible-iterators/52064434#52064434 + let mut iter_a = None; + let mut iter_b = None; + + if let ExprKind::Block(block, _) = body.kind { + if let Some(loop_counters) = get_loop_counters(cx, block, expr) { + starts.extend(loop_counters); + } + iter_a = Some(get_assignments(block, &starts)); + } else { + iter_b = Some(get_assignment(body)); + } + + let assignments = iter_a.into_iter().flatten().chain(iter_b.into_iter()); + + let big_sugg = assignments + // The only statements in the for loops can be indexed assignments from + // indexed retrievals (except increments of loop counters). + .map(|o| { + o.and_then(|(lhs, rhs)| { + let rhs = fetch_cloned_expr(rhs); + if_chain! { + if let ExprKind::Index(base_left, idx_left) = lhs.kind; + if let ExprKind::Index(base_right, idx_right) = rhs.kind; + if let Some(ty) = get_slice_like_element_ty(cx, cx.typeck_results().expr_ty(base_left)); + if get_slice_like_element_ty(cx, cx.typeck_results().expr_ty(base_right)).is_some(); + if let Some((start_left, offset_left)) = get_details_from_idx(cx, idx_left, &starts); + if let Some((start_right, offset_right)) = get_details_from_idx(cx, idx_right, &starts); + + // Source and destination must be different + if path_to_local(base_left) != path_to_local(base_right); + then { + Some((ty, IndexExpr { base: base_left, idx: start_left, idx_offset: offset_left }, + IndexExpr { base: base_right, idx: start_right, idx_offset: offset_right })) + } else { + None + } + } + }) + }) + .map(|o| o.map(|(ty, dst, src)| build_manual_memcpy_suggestion(cx, start, end, limits, ty, &dst, &src))) + .collect::<Option<Vec<_>>>() + .filter(|v| !v.is_empty()) + .map(|v| v.join("\n ")); + + if let Some(big_sugg) = big_sugg { + span_lint_and_sugg( + cx, + MANUAL_MEMCPY, + expr.span, + "it looks like you're manually copying between slices", + "try replacing the loop by", + big_sugg, + Applicability::Unspecified, + ); + return true; + } + } + } + false +} + +fn build_manual_memcpy_suggestion<'tcx>( + cx: &LateContext<'tcx>, + start: &Expr<'_>, + end: &Expr<'_>, + limits: ast::RangeLimits, + elem_ty: Ty<'tcx>, + dst: &IndexExpr<'_>, + src: &IndexExpr<'_>, +) -> String { + fn print_offset(offset: MinifyingSugg<'static>) -> MinifyingSugg<'static> { + if offset.to_string() == "0" { + sugg::EMPTY.into() + } else { + offset + } + } + + let print_limit = |end: &Expr<'_>, end_str: &str, base: &Expr<'_>, sugg: MinifyingSugg<'static>| { + if_chain! { + if let ExprKind::MethodCall(method, len_args, _) = end.kind; + if method.ident.name == sym::len; + if len_args.len() == 1; + if let Some(arg) = len_args.get(0); + if path_to_local(arg) == path_to_local(base); + then { + if sugg.to_string() == end_str { + sugg::EMPTY.into() + } else { + sugg + } + } else { + match limits { + ast::RangeLimits::Closed => { + sugg + &sugg::ONE.into() + }, + ast::RangeLimits::HalfOpen => sugg, + } + } + } + }; + + let start_str = Sugg::hir(cx, start, "").into(); + let end_str: MinifyingSugg<'_> = Sugg::hir(cx, end, "").into(); + + let print_offset_and_limit = |idx_expr: &IndexExpr<'_>| match idx_expr.idx { + StartKind::Range => ( + print_offset(apply_offset(&start_str, &idx_expr.idx_offset)).into_sugg(), + print_limit( + end, + end_str.to_string().as_str(), + idx_expr.base, + apply_offset(&end_str, &idx_expr.idx_offset), + ) + .into_sugg(), + ), + StartKind::Counter { initializer } => { + let counter_start = Sugg::hir(cx, initializer, "").into(); + ( + print_offset(apply_offset(&counter_start, &idx_expr.idx_offset)).into_sugg(), + print_limit( + end, + end_str.to_string().as_str(), + idx_expr.base, + apply_offset(&end_str, &idx_expr.idx_offset) + &counter_start - &start_str, + ) + .into_sugg(), + ) + }, + }; + + let (dst_offset, dst_limit) = print_offset_and_limit(dst); + let (src_offset, src_limit) = print_offset_and_limit(src); + + let dst_base_str = snippet(cx, dst.base.span, "???"); + let src_base_str = snippet(cx, src.base.span, "???"); + + let dst = if dst_offset == sugg::EMPTY && dst_limit == sugg::EMPTY { + dst_base_str + } else { + format!( + "{}[{}..{}]", + dst_base_str, + dst_offset.maybe_par(), + dst_limit.maybe_par() + ) + .into() + }; + + let method_str = if is_copy(cx, elem_ty) { + "copy_from_slice" + } else { + "clone_from_slice" + }; + + format!( + "{}.{}(&{}[{}..{}]);", + dst, + method_str, + src_base_str, + src_offset.maybe_par(), + src_limit.maybe_par() + ) +} + +/// a wrapper of `Sugg`. Besides what `Sugg` do, this removes unnecessary `0`; +/// and also, it avoids subtracting a variable from the same one by replacing it with `0`. +/// it exists for the convenience of the overloaded operators while normal functions can do the +/// same. +#[derive(Clone)] +struct MinifyingSugg<'a>(Sugg<'a>); + +impl<'a> Display for MinifyingSugg<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl<'a> MinifyingSugg<'a> { + fn into_sugg(self) -> Sugg<'a> { + self.0 + } +} + +impl<'a> From<Sugg<'a>> for MinifyingSugg<'a> { + fn from(sugg: Sugg<'a>) -> Self { + Self(sugg) + } +} + +impl std::ops::Add for &MinifyingSugg<'static> { + type Output = MinifyingSugg<'static>; + fn add(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> { + match (self.to_string().as_str(), rhs.to_string().as_str()) { + ("0", _) => rhs.clone(), + (_, "0") => self.clone(), + (_, _) => (&self.0 + &rhs.0).into(), + } + } +} + +impl std::ops::Sub for &MinifyingSugg<'static> { + type Output = MinifyingSugg<'static>; + fn sub(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> { + match (self.to_string().as_str(), rhs.to_string().as_str()) { + (_, "0") => self.clone(), + ("0", _) => (-rhs.0.clone()).into(), + (x, y) if x == y => sugg::ZERO.into(), + (_, _) => (&self.0 - &rhs.0).into(), + } + } +} + +impl std::ops::Add<&MinifyingSugg<'static>> for MinifyingSugg<'static> { + type Output = MinifyingSugg<'static>; + fn add(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> { + match (self.to_string().as_str(), rhs.to_string().as_str()) { + ("0", _) => rhs.clone(), + (_, "0") => self, + (_, _) => (self.0 + &rhs.0).into(), + } + } +} + +impl std::ops::Sub<&MinifyingSugg<'static>> for MinifyingSugg<'static> { + type Output = MinifyingSugg<'static>; + fn sub(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> { + match (self.to_string().as_str(), rhs.to_string().as_str()) { + (_, "0") => self, + ("0", _) => (-rhs.0.clone()).into(), + (x, y) if x == y => sugg::ZERO.into(), + (_, _) => (self.0 - &rhs.0).into(), + } + } +} + +/// a wrapper around `MinifyingSugg`, which carries an operator like currying +/// so that the suggested code become more efficient (e.g. `foo + -bar` `foo - bar`). +struct Offset { + value: MinifyingSugg<'static>, + sign: OffsetSign, +} + +#[derive(Clone, Copy)] +enum OffsetSign { + Positive, + Negative, +} + +impl Offset { + fn negative(value: Sugg<'static>) -> Self { + Self { + value: value.into(), + sign: OffsetSign::Negative, + } + } + + fn positive(value: Sugg<'static>) -> Self { + Self { + value: value.into(), + sign: OffsetSign::Positive, + } + } + + fn empty() -> Self { + Self::positive(sugg::ZERO) + } +} + +fn apply_offset(lhs: &MinifyingSugg<'static>, rhs: &Offset) -> MinifyingSugg<'static> { + match rhs.sign { + OffsetSign::Positive => lhs + &rhs.value, + OffsetSign::Negative => lhs - &rhs.value, + } +} + +#[derive(Debug, Clone, Copy)] +enum StartKind<'hir> { + Range, + Counter { initializer: &'hir Expr<'hir> }, +} + +struct IndexExpr<'hir> { + base: &'hir Expr<'hir>, + idx: StartKind<'hir>, + idx_offset: Offset, +} + +struct Start<'hir> { + id: HirId, + kind: StartKind<'hir>, +} + +fn get_slice_like_element_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> { + match ty.kind() { + ty::Adt(adt, subs) if cx.tcx.is_diagnostic_item(sym::Vec, adt.did()) => Some(subs.type_at(0)), + ty::Ref(_, subty, _) => get_slice_like_element_ty(cx, *subty), + ty::Slice(ty) | ty::Array(ty, _) => Some(*ty), + _ => None, + } +} + +fn fetch_cloned_expr<'tcx>(expr: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> { + if_chain! { + if let ExprKind::MethodCall(method, args, _) = expr.kind; + if method.ident.name == sym::clone; + if args.len() == 1; + if let Some(arg) = args.get(0); + then { arg } else { expr } + } +} + +fn get_details_from_idx<'tcx>( + cx: &LateContext<'tcx>, + idx: &Expr<'_>, + starts: &[Start<'tcx>], +) -> Option<(StartKind<'tcx>, Offset)> { + fn get_start<'tcx>(e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<StartKind<'tcx>> { + let id = path_to_local(e)?; + starts.iter().find(|start| start.id == id).map(|start| start.kind) + } + + fn get_offset<'tcx>(cx: &LateContext<'tcx>, e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<Sugg<'static>> { + match &e.kind { + ExprKind::Lit(l) => match l.node { + ast::LitKind::Int(x, _ty) => Some(Sugg::NonParen(x.to_string().into())), + _ => None, + }, + ExprKind::Path(..) if get_start(e, starts).is_none() => Some(Sugg::hir(cx, e, "???")), + _ => None, + } + } + + match idx.kind { + ExprKind::Binary(op, lhs, rhs) => match op.node { + BinOpKind::Add => { + let offset_opt = get_start(lhs, starts) + .and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, o))) + .or_else(|| get_start(rhs, starts).and_then(|s| get_offset(cx, lhs, starts).map(|o| (s, o)))); + + offset_opt.map(|(s, o)| (s, Offset::positive(o))) + }, + BinOpKind::Sub => { + get_start(lhs, starts).and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, Offset::negative(o)))) + }, + _ => None, + }, + ExprKind::Path(..) => get_start(idx, starts).map(|s| (s, Offset::empty())), + _ => None, + } +} + +fn get_assignment<'tcx>(e: &'tcx Expr<'tcx>) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> { + if let ExprKind::Assign(lhs, rhs, _) = e.kind { + Some((lhs, rhs)) + } else { + None + } +} + +/// Get assignments from the given block. +/// The returned iterator yields `None` if no assignment expressions are there, +/// filtering out the increments of the given whitelisted loop counters; +/// because its job is to make sure there's nothing other than assignments and the increments. +fn get_assignments<'a, 'tcx>( + Block { stmts, expr, .. }: &'tcx Block<'tcx>, + loop_counters: &'a [Start<'tcx>], +) -> impl Iterator<Item = Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)>> + 'a { + // As the `filter` and `map` below do different things, I think putting together + // just increases complexity. (cc #3188 and #4193) + stmts + .iter() + .filter_map(move |stmt| match stmt.kind { + StmtKind::Local(..) | StmtKind::Item(..) => None, + StmtKind::Expr(e) | StmtKind::Semi(e) => Some(e), + }) + .chain((*expr).into_iter()) + .filter(move |e| { + if let ExprKind::AssignOp(_, place, _) = e.kind { + path_to_local(place).map_or(false, |id| { + !loop_counters + .iter() + // skip the first item which should be `StartKind::Range` + // this makes it possible to use the slice with `StartKind::Range` in the same iterator loop. + .skip(1) + .any(|counter| counter.id == id) + }) + } else { + true + } + }) + .map(get_assignment) +} + +fn get_loop_counters<'a, 'tcx>( + cx: &'a LateContext<'tcx>, + body: &'tcx Block<'tcx>, + expr: &'tcx Expr<'_>, +) -> Option<impl Iterator<Item = Start<'tcx>> + 'a> { + // Look for variables that are incremented once per loop iteration. + let mut increment_visitor = IncrementVisitor::new(cx); + walk_block(&mut increment_visitor, body); + + // For each candidate, check the parent block to see if + // it's initialized to zero at the start of the loop. + get_enclosing_block(cx, expr.hir_id).and_then(|block| { + increment_visitor + .into_results() + .filter_map(move |var_id| { + let mut initialize_visitor = InitializeVisitor::new(cx, expr, var_id); + walk_block(&mut initialize_visitor, block); + + initialize_visitor.get_result().map(|(_, _, initializer)| Start { + id: var_id, + kind: StartKind::Counter { initializer }, + }) + }) + .into() + }) +} diff --git a/src/tools/clippy/clippy_lints/src/loops/missing_spin_loop.rs b/src/tools/clippy/clippy_lints/src/loops/missing_spin_loop.rs new file mode 100644 index 000000000..0696afa39 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/missing_spin_loop.rs @@ -0,0 +1,56 @@ +use super::MISSING_SPIN_LOOP; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_no_std_crate; +use rustc_errors::Applicability; +use rustc_hir::{Block, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::sym; + +fn unpack_cond<'tcx>(cond: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> { + match &cond.kind { + ExprKind::Block( + Block { + stmts: [], + expr: Some(e), + .. + }, + _, + ) + | ExprKind::Unary(_, e) => unpack_cond(e), + ExprKind::Binary(_, l, r) => { + let l = unpack_cond(l); + if let ExprKind::MethodCall(..) = l.kind { + l + } else { + unpack_cond(r) + } + }, + _ => cond, + } +} + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, cond: &'tcx Expr<'_>, body: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::Block(Block { stmts: [], expr: None, ..}, _) = body.kind; + if let ExprKind::MethodCall(method, [callee, ..], _) = unpack_cond(cond).kind; + if [sym::load, sym::compare_exchange, sym::compare_exchange_weak].contains(&method.ident.name); + if let ty::Adt(def, _substs) = cx.typeck_results().expr_ty(callee).kind(); + if cx.tcx.is_diagnostic_item(sym::AtomicBool, def.did()); + then { + span_lint_and_sugg( + cx, + MISSING_SPIN_LOOP, + body.span, + "busy-waiting loop should at least have a spin loop hint", + "try this", + (if is_no_std_crate(cx) { + "{ core::hint::spin_loop() }" + } else { + "{ std::hint::spin_loop() }" + }).into(), + Applicability::MachineApplicable + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/mod.rs b/src/tools/clippy/clippy_lints/src/loops/mod.rs new file mode 100644 index 000000000..ed270bd49 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/mod.rs @@ -0,0 +1,768 @@ +mod empty_loop; +mod explicit_counter_loop; +mod explicit_into_iter_loop; +mod explicit_iter_loop; +mod for_kv_map; +mod for_loops_over_fallibles; +mod iter_next_loop; +mod manual_find; +mod manual_flatten; +mod manual_memcpy; +mod missing_spin_loop; +mod mut_range_bound; +mod needless_collect; +mod needless_range_loop; +mod never_loop; +mod same_item_push; +mod single_element_loop; +mod utils; +mod while_immutable_condition; +mod while_let_loop; +mod while_let_on_iterator; + +use clippy_utils::higher; +use rustc_hir::{Expr, ExprKind, LoopSource, Pat}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use utils::{make_iterator_snippet, IncrementVisitor, InitializeVisitor}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for for-loops that manually copy items between + /// slices that could be optimized by having a memcpy. + /// + /// ### Why is this bad? + /// It is not as fast as a memcpy. + /// + /// ### Example + /// ```rust + /// # let src = vec![1]; + /// # let mut dst = vec![0; 65]; + /// for i in 0..src.len() { + /// dst[i + 64] = src[i]; + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let src = vec![1]; + /// # let mut dst = vec![0; 65]; + /// dst[64..(src.len() + 64)].clone_from_slice(&src[..]); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MANUAL_MEMCPY, + perf, + "manually copying items between slices" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for looping over the range of `0..len` of some + /// collection just to get the values by index. + /// + /// ### Why is this bad? + /// Just iterating the collection itself makes the intent + /// more clear and is probably faster. + /// + /// ### Example + /// ```rust + /// let vec = vec!['a', 'b', 'c']; + /// for i in 0..vec.len() { + /// println!("{}", vec[i]); + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// let vec = vec!['a', 'b', 'c']; + /// for i in vec { + /// println!("{}", i); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_RANGE_LOOP, + style, + "for-looping over a range of indices where an iterator over items would do" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for loops on `x.iter()` where `&x` will do, and + /// suggests the latter. + /// + /// ### Why is this bad? + /// Readability. + /// + /// ### Known problems + /// False negatives. We currently only warn on some known + /// types. + /// + /// ### Example + /// ```rust + /// // with `y` a `Vec` or slice: + /// # let y = vec![1]; + /// for x in y.iter() { + /// // .. + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let y = vec![1]; + /// for x in &y { + /// // .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXPLICIT_ITER_LOOP, + pedantic, + "for-looping over `_.iter()` or `_.iter_mut()` when `&_` or `&mut _` would do" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for loops on `y.into_iter()` where `y` will do, and + /// suggests the latter. + /// + /// ### Why is this bad? + /// Readability. + /// + /// ### Example + /// ```rust + /// # let y = vec![1]; + /// // with `y` a `Vec` or slice: + /// for x in y.into_iter() { + /// // .. + /// } + /// ``` + /// can be rewritten to + /// ```rust + /// # let y = vec![1]; + /// for x in y { + /// // .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXPLICIT_INTO_ITER_LOOP, + pedantic, + "for-looping over `_.into_iter()` when `_` would do" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for loops on `x.next()`. + /// + /// ### Why is this bad? + /// `next()` returns either `Some(value)` if there was a + /// value, or `None` otherwise. The insidious thing is that `Option<_>` + /// implements `IntoIterator`, so that possibly one value will be iterated, + /// leading to some hard to find bugs. No one will want to write such code + /// [except to win an Underhanded Rust + /// Contest](https://www.reddit.com/r/rust/comments/3hb0wm/underhanded_rust_contest/cu5yuhr). + /// + /// ### Example + /// ```ignore + /// for x in y.next() { + /// .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ITER_NEXT_LOOP, + correctness, + "for-looping over `_.next()` which is probably not intended" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `for` loops over `Option` or `Result` values. + /// + /// ### Why is this bad? + /// Readability. This is more clearly expressed as an `if + /// let`. + /// + /// ### Example + /// ```rust + /// # let opt = Some(1); + /// # let res: Result<i32, std::io::Error> = Ok(1); + /// for x in opt { + /// // .. + /// } + /// + /// for x in &res { + /// // .. + /// } + /// + /// for x in res.iter() { + /// // .. + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let opt = Some(1); + /// # let res: Result<i32, std::io::Error> = Ok(1); + /// if let Some(x) = opt { + /// // .. + /// } + /// + /// if let Ok(x) = res { + /// // .. + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub FOR_LOOPS_OVER_FALLIBLES, + suspicious, + "for-looping over an `Option` or a `Result`, which is more clearly expressed as an `if let`" +} + +declare_clippy_lint! { + /// ### What it does + /// Detects `loop + match` combinations that are easier + /// written as a `while let` loop. + /// + /// ### Why is this bad? + /// The `while let` loop is usually shorter and more + /// readable. + /// + /// ### Known problems + /// Sometimes the wrong binding is displayed ([#383](https://github.com/rust-lang/rust-clippy/issues/383)). + /// + /// ### Example + /// ```rust,no_run + /// # let y = Some(1); + /// loop { + /// let x = match y { + /// Some(x) => x, + /// None => break, + /// }; + /// // .. do something with x + /// } + /// // is easier written as + /// while let Some(x) = y { + /// // .. do something with x + /// }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WHILE_LET_LOOP, + complexity, + "`loop { if let { ... } else break }`, which can be written as a `while let` loop" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions collecting an iterator when collect + /// is not needed. + /// + /// ### Why is this bad? + /// `collect` causes the allocation of a new data structure, + /// when this allocation may not be needed. + /// + /// ### Example + /// ```rust + /// # let iterator = vec![1].into_iter(); + /// let len = iterator.clone().collect::<Vec<_>>().len(); + /// // should be + /// let len = iterator.count(); + /// ``` + #[clippy::version = "1.30.0"] + pub NEEDLESS_COLLECT, + perf, + "collecting an iterator when collect is not needed" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks `for` loops over slices with an explicit counter + /// and suggests the use of `.enumerate()`. + /// + /// ### Why is this bad? + /// Using `.enumerate()` makes the intent more clear, + /// declutters the code and may be faster in some instances. + /// + /// ### Example + /// ```rust + /// # let v = vec![1]; + /// # fn bar(bar: usize, baz: usize) {} + /// let mut i = 0; + /// for item in &v { + /// bar(i, *item); + /// i += 1; + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let v = vec![1]; + /// # fn bar(bar: usize, baz: usize) {} + /// for (i, item) in v.iter().enumerate() { bar(i, *item); } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXPLICIT_COUNTER_LOOP, + complexity, + "for-looping with an explicit counter when `_.enumerate()` would do" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for empty `loop` expressions. + /// + /// ### Why is this bad? + /// These busy loops burn CPU cycles without doing + /// anything. It is _almost always_ a better idea to `panic!` than to have + /// a busy loop. + /// + /// If panicking isn't possible, think of the environment and either: + /// - block on something + /// - sleep the thread for some microseconds + /// - yield or pause the thread + /// + /// For `std` targets, this can be done with + /// [`std::thread::sleep`](https://doc.rust-lang.org/std/thread/fn.sleep.html) + /// or [`std::thread::yield_now`](https://doc.rust-lang.org/std/thread/fn.yield_now.html). + /// + /// For `no_std` targets, doing this is more complicated, especially because + /// `#[panic_handler]`s can't panic. To stop/pause the thread, you will + /// probably need to invoke some target-specific intrinsic. Examples include: + /// - [`x86_64::instructions::hlt`](https://docs.rs/x86_64/0.12.2/x86_64/instructions/fn.hlt.html) + /// - [`cortex_m::asm::wfi`](https://docs.rs/cortex-m/0.6.3/cortex_m/asm/fn.wfi.html) + /// + /// ### Example + /// ```no_run + /// loop {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EMPTY_LOOP, + suspicious, + "empty `loop {}`, which should block or sleep" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `while let` expressions on iterators. + /// + /// ### Why is this bad? + /// Readability. A simple `for` loop is shorter and conveys + /// the intent better. + /// + /// ### Example + /// ```ignore + /// while let Some(val) = iter.next() { + /// .. + /// } + /// ``` + /// + /// Use instead: + /// ```ignore + /// for val in &mut iter { + /// .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WHILE_LET_ON_ITERATOR, + style, + "using a `while let` loop instead of a for loop on an iterator" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for iterating a map (`HashMap` or `BTreeMap`) and + /// ignoring either the keys or values. + /// + /// ### Why is this bad? + /// Readability. There are `keys` and `values` methods that + /// can be used to express that don't need the values or keys. + /// + /// ### Example + /// ```ignore + /// for (k, _) in &map { + /// .. + /// } + /// ``` + /// + /// could be replaced by + /// + /// ```ignore + /// for k in map.keys() { + /// .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FOR_KV_MAP, + style, + "looping on a map using `iter` when `keys` or `values` would do" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for loops that will always `break`, `return` or + /// `continue` an outer loop. + /// + /// ### Why is this bad? + /// This loop never loops, all it does is obfuscating the + /// code. + /// + /// ### Example + /// ```rust + /// loop { + /// ..; + /// break; + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEVER_LOOP, + correctness, + "any loop that will always `break` or `return`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for loops which have a range bound that is a mutable variable + /// + /// ### Why is this bad? + /// One might think that modifying the mutable variable changes the loop bounds + /// + /// ### Known problems + /// False positive when mutation is followed by a `break`, but the `break` is not immediately + /// after the mutation: + /// + /// ```rust + /// let mut x = 5; + /// for _ in 0..x { + /// x += 1; // x is a range bound that is mutated + /// ..; // some other expression + /// break; // leaves the loop, so mutation is not an issue + /// } + /// ``` + /// + /// False positive on nested loops ([#6072](https://github.com/rust-lang/rust-clippy/issues/6072)) + /// + /// ### Example + /// ```rust + /// let mut foo = 42; + /// for i in 0..foo { + /// foo -= 1; + /// println!("{}", i); // prints numbers from 0 to 42, not 0 to 21 + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MUT_RANGE_BOUND, + suspicious, + "for loop over a range where one of the bounds is a mutable variable" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks whether variables used within while loop condition + /// can be (and are) mutated in the body. + /// + /// ### Why is this bad? + /// If the condition is unchanged, entering the body of the loop + /// will lead to an infinite loop. + /// + /// ### Known problems + /// If the `while`-loop is in a closure, the check for mutation of the + /// condition variables in the body can cause false negatives. For example when only `Upvar` `a` is + /// in the condition and only `Upvar` `b` gets mutated in the body, the lint will not trigger. + /// + /// ### Example + /// ```rust + /// let i = 0; + /// while i > 10 { + /// println!("let me loop forever!"); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WHILE_IMMUTABLE_CONDITION, + correctness, + "variables used within while expression are not mutated in the body" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks whether a for loop is being used to push a constant + /// value into a Vec. + /// + /// ### Why is this bad? + /// This kind of operation can be expressed more succinctly with + /// `vec![item; SIZE]` or `vec.resize(NEW_SIZE, item)` and using these alternatives may also + /// have better performance. + /// + /// ### Example + /// ```rust + /// let item1 = 2; + /// let item2 = 3; + /// let mut vec: Vec<u8> = Vec::new(); + /// for _ in 0..20 { + /// vec.push(item1); + /// } + /// for _ in 0..30 { + /// vec.push(item2); + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// let item1 = 2; + /// let item2 = 3; + /// let mut vec: Vec<u8> = vec![item1; 20]; + /// vec.resize(20 + 30, item2); + /// ``` + #[clippy::version = "1.47.0"] + pub SAME_ITEM_PUSH, + style, + "the same item is pushed inside of a for loop" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks whether a for loop has a single element. + /// + /// ### Why is this bad? + /// There is no reason to have a loop of a + /// single element. + /// + /// ### Example + /// ```rust + /// let item1 = 2; + /// for item in &[item1] { + /// println!("{}", item); + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// let item1 = 2; + /// let item = &item1; + /// println!("{}", item); + /// ``` + #[clippy::version = "1.49.0"] + pub SINGLE_ELEMENT_LOOP, + complexity, + "there is no reason to have a single element loop" +} + +declare_clippy_lint! { + /// ### What it does + /// Check 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? + /// It is verbose and can be simplified + /// by first calling the `flatten` method on the `Iterator`. + /// + /// ### Example + /// + /// ```rust + /// let x = vec![Some(1), Some(2), Some(3)]; + /// for n in x { + /// if let Some(n) = n { + /// println!("{}", n); + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// let x = vec![Some(1), Some(2), Some(3)]; + /// for n in x.into_iter().flatten() { + /// println!("{}", n); + /// } + /// ``` + #[clippy::version = "1.52.0"] + pub MANUAL_FLATTEN, + complexity, + "for loops over `Option`s or `Result`s with a single expression can be simplified" +} + +declare_clippy_lint! { + /// ### What it does + /// Check for empty spin loops + /// + /// ### Why is this bad? + /// The loop body should have something like `thread::park()` or at least + /// `std::hint::spin_loop()` to avoid needlessly burning cycles and conserve + /// energy. Perhaps even better use an actual lock, if possible. + /// + /// ### Known problems + /// This lint doesn't currently trigger on `while let` or + /// `loop { match .. { .. } }` loops, which would be considered idiomatic in + /// combination with e.g. `AtomicBool::compare_exchange_weak`. + /// + /// ### Example + /// + /// ```ignore + /// use core::sync::atomic::{AtomicBool, Ordering}; + /// let b = AtomicBool::new(true); + /// // give a ref to `b` to another thread,wait for it to become false + /// while b.load(Ordering::Acquire) {}; + /// ``` + /// Use instead: + /// ```rust,no_run + ///# use core::sync::atomic::{AtomicBool, Ordering}; + ///# let b = AtomicBool::new(true); + /// while b.load(Ordering::Acquire) { + /// std::hint::spin_loop() + /// } + /// ``` + #[clippy::version = "1.61.0"] + pub MISSING_SPIN_LOOP, + perf, + "An empty busy waiting loop" +} + +declare_clippy_lint! { + /// ### What it does + /// Check for manual implementations of Iterator::find + /// + /// ### Why is this bad? + /// It doesn't affect performance, but using `find` is shorter and easier to read. + /// + /// ### Example + /// + /// ```rust + /// fn example(arr: Vec<i32>) -> Option<i32> { + /// for el in arr { + /// if el == 1 { + /// return Some(el); + /// } + /// } + /// None + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn example(arr: Vec<i32>) -> Option<i32> { + /// arr.into_iter().find(|&el| el == 1) + /// } + /// ``` + #[clippy::version = "1.61.0"] + pub MANUAL_FIND, + complexity, + "manual implementation of `Iterator::find`" +} + +declare_lint_pass!(Loops => [ + MANUAL_MEMCPY, + MANUAL_FLATTEN, + NEEDLESS_RANGE_LOOP, + EXPLICIT_ITER_LOOP, + EXPLICIT_INTO_ITER_LOOP, + ITER_NEXT_LOOP, + FOR_LOOPS_OVER_FALLIBLES, + WHILE_LET_LOOP, + NEEDLESS_COLLECT, + EXPLICIT_COUNTER_LOOP, + EMPTY_LOOP, + WHILE_LET_ON_ITERATOR, + FOR_KV_MAP, + NEVER_LOOP, + MUT_RANGE_BOUND, + WHILE_IMMUTABLE_CONDITION, + SAME_ITEM_PUSH, + SINGLE_ELEMENT_LOOP, + MISSING_SPIN_LOOP, + MANUAL_FIND, +]); + +impl<'tcx> LateLintPass<'tcx> for Loops { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let for_loop = higher::ForLoop::hir(expr); + if let Some(higher::ForLoop { + pat, + arg, + body, + loop_id, + span, + }) = for_loop + { + // we don't want to check expanded macros + // this check is not at the top of the function + // since higher::for_loop expressions are marked as expansions + if body.span.from_expansion() { + return; + } + 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()); + } + } + + // we don't want to check expanded macros + if expr.span.from_expansion() { + return; + } + + // check for never_loop + if let ExprKind::Loop(block, ..) = expr.kind { + never_loop::check(cx, block, expr.hir_id, expr.span, None); + } + + // check for `loop { if let {} else break }` that could be `while let` + // (also matches an explicit "match" instead of "if let") + // (even if the "match" or "if let" is used for declaration) + if let ExprKind::Loop(block, _, LoopSource::Loop, _) = expr.kind { + // also check for empty `loop {}` statements, skipping those in #[panic_handler] + empty_loop::check(cx, expr, block); + while_let_loop::check(cx, expr, block); + } + + while_let_on_iterator::check(cx, expr); + + if let Some(higher::While { condition, body }) = higher::While::hir(expr) { + while_immutable_condition::check(cx, condition, body); + missing_spin_loop::check(cx, condition, body); + } + + needless_collect::check(expr, cx); + } +} + +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); + } + 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: &Pat<'_>, arg: &Expr<'_>) { + let mut next_loop_linted = false; // whether or not ITER_NEXT_LOOP lint was used + + 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); + for_loops_over_fallibles::check(cx, pat, self_arg, Some(method_name)); + }, + "into_iter" => { + explicit_iter_loop::check(cx, self_arg, arg, method_name); + explicit_into_iter_loop::check(cx, self_arg, arg); + for_loops_over_fallibles::check(cx, pat, self_arg, Some(method_name)); + }, + "next" => { + next_loop_linted = iter_next_loop::check(cx, arg); + }, + _ => {}, + } + } + + if !next_loop_linted { + for_loops_over_fallibles::check(cx, pat, arg, None); + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/mut_range_bound.rs b/src/tools/clippy/clippy_lints/src/loops/mut_range_bound.rs new file mode 100644 index 000000000..aedf3810b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/mut_range_bound.rs @@ -0,0 +1,167 @@ +use super::MUT_RANGE_BOUND; +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::{get_enclosing_block, higher, path_to_local}; +use if_chain::if_chain; +use rustc_hir::intravisit::{self, Visitor}; +use rustc_hir::{BindingAnnotation, Expr, ExprKind, HirId, Node, PatKind}; +use rustc_infer::infer::TyCtxtInferExt; +use rustc_lint::LateContext; +use rustc_middle::{mir::FakeReadCause, ty}; +use rustc_span::source_map::Span; +use rustc_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, PlaceWithHirId}; + +pub(super) fn check(cx: &LateContext<'_>, arg: &Expr<'_>, body: &Expr<'_>) { + if_chain! { + if let Some(higher::Range { + start: Some(start), + end: Some(end), + .. + }) = higher::Range::hir(arg); + let (mut_id_start, mut_id_end) = (check_for_mutability(cx, start), check_for_mutability(cx, end)); + if mut_id_start.is_some() || mut_id_end.is_some(); + then { + let (span_low, span_high) = check_for_mutation(cx, body, mut_id_start, mut_id_end); + mut_warn_with_span(cx, span_low); + mut_warn_with_span(cx, span_high); + } + } +} + +fn mut_warn_with_span(cx: &LateContext<'_>, span: Option<Span>) { + if let Some(sp) = span { + span_lint_and_note( + cx, + MUT_RANGE_BOUND, + sp, + "attempt to mutate range bound within loop", + None, + "the range of the loop is unchanged", + ); + } +} + +fn check_for_mutability(cx: &LateContext<'_>, bound: &Expr<'_>) -> Option<HirId> { + if_chain! { + if let Some(hir_id) = path_to_local(bound); + if let Node::Pat(pat) = cx.tcx.hir().get(hir_id); + if let PatKind::Binding(BindingAnnotation::Mutable, ..) = pat.kind; + then { + return Some(hir_id); + } + } + None +} + +fn check_for_mutation<'tcx>( + cx: &LateContext<'tcx>, + body: &Expr<'_>, + bound_id_start: Option<HirId>, + bound_id_end: Option<HirId>, +) -> (Option<Span>, Option<Span>) { + let mut delegate = MutatePairDelegate { + cx, + hir_id_low: bound_id_start, + hir_id_high: bound_id_end, + span_low: None, + span_high: None, + }; + cx.tcx.infer_ctxt().enter(|infcx| { + ExprUseVisitor::new( + &mut delegate, + &infcx, + body.hir_id.owner, + cx.param_env, + cx.typeck_results(), + ) + .walk_expr(body); + }); + + delegate.mutation_span() +} + +struct MutatePairDelegate<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + hir_id_low: Option<HirId>, + hir_id_high: Option<HirId>, + span_low: Option<Span>, + span_high: Option<Span>, +} + +impl<'tcx> Delegate<'tcx> for MutatePairDelegate<'_, 'tcx> { + fn consume(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId) {} + + fn borrow(&mut self, cmt: &PlaceWithHirId<'tcx>, diag_expr_id: HirId, bk: ty::BorrowKind) { + if bk == ty::BorrowKind::MutBorrow { + if let PlaceBase::Local(id) = cmt.place.base { + if Some(id) == self.hir_id_low && !BreakAfterExprVisitor::is_found(self.cx, diag_expr_id) { + self.span_low = Some(self.cx.tcx.hir().span(diag_expr_id)); + } + if Some(id) == self.hir_id_high && !BreakAfterExprVisitor::is_found(self.cx, diag_expr_id) { + self.span_high = Some(self.cx.tcx.hir().span(diag_expr_id)); + } + } + } + } + + fn mutate(&mut self, cmt: &PlaceWithHirId<'tcx>, diag_expr_id: HirId) { + if let PlaceBase::Local(id) = cmt.place.base { + if Some(id) == self.hir_id_low && !BreakAfterExprVisitor::is_found(self.cx, diag_expr_id) { + self.span_low = Some(self.cx.tcx.hir().span(diag_expr_id)); + } + if Some(id) == self.hir_id_high && !BreakAfterExprVisitor::is_found(self.cx, diag_expr_id) { + self.span_high = Some(self.cx.tcx.hir().span(diag_expr_id)); + } + } + } + + fn fake_read(&mut self, _: &rustc_typeck::expr_use_visitor::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {} +} + +impl MutatePairDelegate<'_, '_> { + fn mutation_span(&self) -> (Option<Span>, Option<Span>) { + (self.span_low, self.span_high) + } +} + +struct BreakAfterExprVisitor { + hir_id: HirId, + past_expr: bool, + past_candidate: bool, + break_after_expr: bool, +} + +impl BreakAfterExprVisitor { + pub fn is_found(cx: &LateContext<'_>, hir_id: HirId) -> bool { + let mut visitor = BreakAfterExprVisitor { + hir_id, + past_expr: false, + past_candidate: false, + break_after_expr: false, + }; + + get_enclosing_block(cx, hir_id).map_or(false, |block| { + visitor.visit_block(block); + visitor.break_after_expr + }) + } +} + +impl<'tcx> intravisit::Visitor<'tcx> for BreakAfterExprVisitor { + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { + if self.past_candidate { + return; + } + + if expr.hir_id == self.hir_id { + self.past_expr = true; + } else if self.past_expr { + if matches!(&expr.kind, ExprKind::Break(..)) { + self.break_after_expr = true; + } + + self.past_candidate = true; + } else { + intravisit::walk_expr(self, expr); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/needless_collect.rs b/src/tools/clippy/clippy_lints/src/loops/needless_collect.rs new file mode 100644 index 000000000..ddaffc751 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/needless_collect.rs @@ -0,0 +1,369 @@ +use super::NEEDLESS_COLLECT; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then}; +use clippy_utils::source::{snippet, snippet_with_applicability}; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{can_move_expr_to_closure, is_trait_method, path_to_local, path_to_local_id, CaptureKind}; +use if_chain::if_chain; +use rustc_data_structures::fx::FxHashMap; +use rustc_errors::{Applicability, MultiSpan}; +use rustc_hir::intravisit::{walk_block, walk_expr, Visitor}; +use rustc_hir::{Block, Expr, ExprKind, HirId, HirIdSet, Local, Mutability, Node, PatKind, Stmt, StmtKind}; +use rustc_lint::LateContext; +use rustc_middle::hir::nested_filter; +use rustc_middle::ty::subst::GenericArgKind; +use rustc_middle::ty::{self, Ty}; +use rustc_span::sym; +use rustc_span::Span; + +const NEEDLESS_COLLECT_MSG: &str = "avoid using `collect()` when not needed"; + +pub(super) fn check<'tcx>(expr: &'tcx Expr<'_>, cx: &LateContext<'tcx>) { + check_needless_collect_direct_usage(expr, cx); + check_needless_collect_indirect_usage(expr, cx); +} +fn check_needless_collect_direct_usage<'tcx>(expr: &'tcx Expr<'_>, cx: &LateContext<'tcx>) { + if_chain! { + if let ExprKind::MethodCall(method, args, _) = expr.kind; + if let ExprKind::MethodCall(chain_method, _, _) = args[0].kind; + if chain_method.ident.name == sym!(collect) && is_trait_method(cx, &args[0], sym::Iterator); + then { + let ty = cx.typeck_results().expr_ty(&args[0]); + let mut applicability = Applicability::MaybeIncorrect; + let is_empty_sugg = "next().is_none()".to_string(); + let method_name = method.ident.name.as_str(); + let sugg = if is_type_diagnostic_item(cx, ty, sym::Vec) || + is_type_diagnostic_item(cx, ty, sym::VecDeque) || + is_type_diagnostic_item(cx, ty, sym::LinkedList) || + is_type_diagnostic_item(cx, ty, sym::BinaryHeap) { + match method_name { + "len" => "count()".to_string(), + "is_empty" => is_empty_sugg, + "contains" => { + let contains_arg = snippet_with_applicability(cx, args[1].span, "??", &mut applicability); + let (arg, pred) = contains_arg + .strip_prefix('&') + .map_or(("&x", &*contains_arg), |s| ("x", s)); + format!("any(|{}| x == {})", arg, pred) + } + _ => return, + } + } + else if is_type_diagnostic_item(cx, ty, sym::BTreeMap) || + is_type_diagnostic_item(cx, ty, sym::HashMap) { + match method_name { + "is_empty" => is_empty_sugg, + _ => return, + } + } + else { + return; + }; + span_lint_and_sugg( + cx, + NEEDLESS_COLLECT, + chain_method.ident.span.with_hi(expr.span.hi()), + NEEDLESS_COLLECT_MSG, + "replace with", + sugg, + applicability, + ); + } + } +} + +fn check_needless_collect_indirect_usage<'tcx>(expr: &'tcx Expr<'_>, cx: &LateContext<'tcx>) { + if let ExprKind::Block(block, _) = expr.kind { + for stmt in block.stmts { + if_chain! { + if let StmtKind::Local(local) = stmt.kind; + if let PatKind::Binding(_, id, ..) = local.pat.kind; + if let Some(init_expr) = local.init; + if let ExprKind::MethodCall(method_name, &[ref iter_source], ..) = init_expr.kind; + if method_name.ident.name == sym!(collect) && is_trait_method(cx, init_expr, sym::Iterator); + let ty = cx.typeck_results().expr_ty(init_expr); + if is_type_diagnostic_item(cx, ty, sym::Vec) || + is_type_diagnostic_item(cx, ty, sym::VecDeque) || + is_type_diagnostic_item(cx, ty, sym::BinaryHeap) || + is_type_diagnostic_item(cx, ty, sym::LinkedList); + let iter_ty = cx.typeck_results().expr_ty(iter_source); + if let Some(iter_calls) = detect_iter_and_into_iters(block, id, cx, get_captured_ids(cx, iter_ty)); + if let [iter_call] = &*iter_calls; + then { + let mut used_count_visitor = UsedCountVisitor { + cx, + id, + count: 0, + }; + walk_block(&mut used_count_visitor, block); + if used_count_visitor.count > 1 { + return; + } + + // Suggest replacing iter_call with iter_replacement, and removing stmt + let mut span = MultiSpan::from_span(method_name.ident.span); + span.push_span_label(iter_call.span, "the iterator could be used here instead"); + span_lint_hir_and_then( + cx, + super::NEEDLESS_COLLECT, + init_expr.hir_id, + span, + NEEDLESS_COLLECT_MSG, + |diag| { + let iter_replacement = format!("{}{}", Sugg::hir(cx, iter_source, ".."), iter_call.get_iter_method(cx)); + diag.multipart_suggestion( + iter_call.get_suggestion_text(), + vec![ + (stmt.span, String::new()), + (iter_call.span, iter_replacement) + ], + Applicability::MaybeIncorrect, + ); + }, + ); + } + } + } + } +} + +struct IterFunction { + func: IterFunctionKind, + span: Span, +} +impl IterFunction { + fn get_iter_method(&self, cx: &LateContext<'_>) -> String { + match &self.func { + IterFunctionKind::IntoIter => String::new(), + IterFunctionKind::Len => String::from(".count()"), + IterFunctionKind::IsEmpty => String::from(".next().is_none()"), + IterFunctionKind::Contains(span) => { + let s = snippet(cx, *span, ".."); + if let Some(stripped) = s.strip_prefix('&') { + format!(".any(|x| x == {})", stripped) + } else { + format!(".any(|x| x == *{})", s) + } + }, + } + } + fn get_suggestion_text(&self) -> &'static str { + match &self.func { + IterFunctionKind::IntoIter => { + "use the original Iterator instead of collecting it and then producing a new one" + }, + IterFunctionKind::Len => { + "take the original Iterator's count instead of collecting it and finding the length" + }, + IterFunctionKind::IsEmpty => { + "check if the original Iterator has anything instead of collecting it and seeing if it's empty" + }, + IterFunctionKind::Contains(_) => { + "check if the original Iterator contains an element instead of collecting then checking" + }, + } + } +} +enum IterFunctionKind { + IntoIter, + Len, + IsEmpty, + Contains(Span), +} + +struct IterFunctionVisitor<'a, 'tcx> { + illegal_mutable_capture_ids: HirIdSet, + current_mutably_captured_ids: HirIdSet, + cx: &'a LateContext<'tcx>, + uses: Vec<Option<IterFunction>>, + hir_id_uses_map: FxHashMap<HirId, usize>, + current_statement_hir_id: Option<HirId>, + seen_other: bool, + target: HirId, +} +impl<'tcx> Visitor<'tcx> for IterFunctionVisitor<'_, 'tcx> { + fn visit_block(&mut self, block: &'tcx Block<'tcx>) { + for (expr, hir_id) in block.stmts.iter().filter_map(get_expr_and_hir_id_from_stmt) { + self.visit_block_expr(expr, hir_id); + } + if let Some(expr) = block.expr { + self.visit_block_expr(expr, None); + } + } + + 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 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); + return; + } + + if path_to_local_id(recv, self.target) { + if self + .illegal_mutable_capture_ids + .intersection(&self.current_mutably_captured_ids) + .next() + .is_none() + { + if let Some(hir_id) = self.current_statement_hir_id { + self.hir_id_uses_map.insert(hir_id, self.uses.len()); + } + match method_name.ident.name.as_str() { + "into_iter" => self.uses.push(Some(IterFunction { + func: IterFunctionKind::IntoIter, + span: expr.span, + })), + "len" => self.uses.push(Some(IterFunction { + func: IterFunctionKind::Len, + span: expr.span, + })), + "is_empty" => self.uses.push(Some(IterFunction { + func: IterFunctionKind::IsEmpty, + span: expr.span, + })), + "contains" => self.uses.push(Some(IterFunction { + func: IterFunctionKind::Contains(args[0].span), + span: expr.span, + })), + _ => { + self.seen_other = true; + if let Some(hir_id) = self.current_statement_hir_id { + self.hir_id_uses_map.remove(&hir_id); + } + }, + } + } + return; + } + + if let Some(hir_id) = path_to_local(recv) { + if let Some(index) = self.hir_id_uses_map.remove(&hir_id) { + if self + .illegal_mutable_capture_ids + .intersection(&self.current_mutably_captured_ids) + .next() + .is_none() + { + if let Some(hir_id) = self.current_statement_hir_id { + self.hir_id_uses_map.insert(hir_id, index); + } + } else { + self.uses[index] = None; + } + } + } + } + // Check if the collection is used for anything else + if path_to_local_id(expr, self.target) { + self.seen_other = true; + } else { + walk_expr(self, expr); + } + } +} + +impl<'tcx> IterFunctionVisitor<'_, 'tcx> { + fn visit_block_expr(&mut self, expr: &'tcx Expr<'tcx>, hir_id: Option<HirId>) { + self.current_statement_hir_id = hir_id; + self.current_mutably_captured_ids = get_captured_ids(self.cx, self.cx.typeck_results().expr_ty(expr)); + self.visit_expr(expr); + } +} + +fn get_expr_and_hir_id_from_stmt<'v>(stmt: &'v Stmt<'v>) -> Option<(&'v Expr<'v>, Option<HirId>)> { + match stmt.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => Some((expr, None)), + StmtKind::Item(..) => None, + StmtKind::Local(Local { init, pat, .. }) => { + if let PatKind::Binding(_, hir_id, ..) = pat.kind { + init.map(|init_expr| (init_expr, Some(hir_id))) + } else { + init.map(|init_expr| (init_expr, None)) + } + }, + } +} + +struct UsedCountVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + id: HirId, + count: usize, +} + +impl<'a, 'tcx> Visitor<'tcx> for UsedCountVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if path_to_local_id(expr, self.id) { + self.count += 1; + } else { + walk_expr(self, expr); + } + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +/// Detect the occurrences of calls to `iter` or `into_iter` for the +/// given identifier +fn detect_iter_and_into_iters<'tcx: 'a, 'a>( + block: &'tcx Block<'tcx>, + id: HirId, + cx: &'a LateContext<'tcx>, + captured_ids: HirIdSet, +) -> Option<Vec<IterFunction>> { + let mut visitor = IterFunctionVisitor { + uses: Vec::new(), + target: id, + seen_other: false, + cx, + current_mutably_captured_ids: HirIdSet::default(), + illegal_mutable_capture_ids: captured_ids, + hir_id_uses_map: FxHashMap::default(), + current_statement_hir_id: None, + }; + visitor.visit_block(block); + if visitor.seen_other { + None + } else { + Some(visitor.uses.into_iter().flatten().collect()) + } +} + +fn get_captured_ids(cx: &LateContext<'_>, ty: Ty<'_>) -> HirIdSet { + fn get_captured_ids_recursive(cx: &LateContext<'_>, ty: Ty<'_>, set: &mut HirIdSet) { + match ty.kind() { + ty::Adt(_, generics) => { + for generic in *generics { + if let GenericArgKind::Type(ty) = generic.unpack() { + get_captured_ids_recursive(cx, ty, set); + } + } + }, + ty::Closure(def_id, _) => { + let closure_hir_node = cx.tcx.hir().get_if_local(*def_id).unwrap(); + if let Node::Expr(closure_expr) = closure_hir_node { + can_move_expr_to_closure(cx, closure_expr) + .unwrap() + .into_iter() + .for_each(|(hir_id, capture_kind)| { + if matches!(capture_kind, CaptureKind::Ref(Mutability::Mut)) { + set.insert(hir_id); + } + }); + } + }, + _ => (), + } + } + + let mut set = HirIdSet::default(); + + get_captured_ids_recursive(cx, ty, &mut set); + + set +} diff --git a/src/tools/clippy/clippy_lints/src/loops/needless_range_loop.rs b/src/tools/clippy/clippy_lints/src/loops/needless_range_loop.rs new file mode 100644 index 000000000..a7ef562b2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/needless_range_loop.rs @@ -0,0 +1,380 @@ +use super::NEEDLESS_RANGE_LOOP; +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::source::snippet; +use clippy_utils::ty::has_iter_method; +use clippy_utils::visitors::is_local_used; +use clippy_utils::{contains_name, higher, is_integer_const, match_trait_method, paths, sugg, SpanlessEq}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{BinOpKind, BorrowKind, Closure, Expr, ExprKind, HirId, Mutability, Pat, PatKind, QPath}; +use rustc_lint::LateContext; +use rustc_middle::middle::region; +use rustc_middle::ty::{self, Ty}; +use rustc_span::symbol::{sym, Symbol}; +use std::iter::{self, Iterator}; +use std::mem; + +/// Checks for looping over a range and then indexing a sequence with it. +/// The iteratee must be a range literal. +#[expect(clippy::too_many_lines)] +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + arg: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + expr: &'tcx Expr<'_>, +) { + if let Some(higher::Range { + start: Some(start), + ref end, + limits, + }) = higher::Range::hir(arg) + { + // the var must be a single name + if let PatKind::Binding(_, canonical_id, ident, _) = pat.kind { + let mut visitor = VarVisitor { + cx, + var: canonical_id, + indexed_mut: FxHashSet::default(), + indexed_indirectly: FxHashMap::default(), + indexed_directly: FxHashMap::default(), + referenced: FxHashSet::default(), + nonindex: false, + prefer_mutable: false, + }; + walk_expr(&mut visitor, body); + + // linting condition: we only indexed one variable, and indexed it directly + if visitor.indexed_indirectly.is_empty() && visitor.indexed_directly.len() == 1 { + let (indexed, (indexed_extent, indexed_ty)) = visitor + .indexed_directly + .into_iter() + .next() + .expect("already checked that we have exactly 1 element"); + + // ensure that the indexed variable was declared before the loop, see #601 + if let Some(indexed_extent) = indexed_extent { + let parent_def_id = cx.tcx.hir().get_parent_item(expr.hir_id); + let region_scope_tree = cx.tcx.region_scope_tree(parent_def_id); + let pat_extent = region_scope_tree.var_scope(pat.hir_id.local_id).unwrap(); + if region_scope_tree.is_subscope_of(indexed_extent, pat_extent) { + return; + } + } + + // don't lint if the container that is indexed does not have .iter() method + let has_iter = has_iter_method(cx, indexed_ty); + if has_iter.is_none() { + return; + } + + // don't lint if the container that is indexed into is also used without + // indexing + if visitor.referenced.contains(&indexed) { + return; + } + + let starts_at_zero = is_integer_const(cx, start, 0); + + let skip = if starts_at_zero { + String::new() + } else if visitor.indexed_mut.contains(&indexed) && contains_name(indexed, start) { + return; + } else { + format!(".skip({})", snippet(cx, start.span, "..")) + }; + + let mut end_is_start_plus_val = false; + + let take = if let Some(end) = *end { + let mut take_expr = end; + + if let ExprKind::Binary(ref op, left, right) = end.kind { + if op.node == BinOpKind::Add { + let start_equal_left = SpanlessEq::new(cx).eq_expr(start, left); + let start_equal_right = SpanlessEq::new(cx).eq_expr(start, right); + + if start_equal_left { + take_expr = right; + } else if start_equal_right { + take_expr = left; + } + + end_is_start_plus_val = start_equal_left | start_equal_right; + } + } + + if is_len_call(end, indexed) || is_end_eq_array_len(cx, end, limits, indexed_ty) { + String::new() + } else if visitor.indexed_mut.contains(&indexed) && contains_name(indexed, take_expr) { + return; + } else { + match limits { + ast::RangeLimits::Closed => { + let take_expr = sugg::Sugg::hir(cx, take_expr, "<count>"); + format!(".take({})", take_expr + sugg::ONE) + }, + ast::RangeLimits::HalfOpen => { + format!(".take({})", snippet(cx, take_expr.span, "..")) + }, + } + } + } else { + String::new() + }; + + let (ref_mut, method) = if visitor.indexed_mut.contains(&indexed) { + ("mut ", "iter_mut") + } else { + ("", "iter") + }; + + let take_is_empty = take.is_empty(); + let mut method_1 = take; + let mut method_2 = skip; + + if end_is_start_plus_val { + mem::swap(&mut method_1, &mut method_2); + } + + if visitor.nonindex { + span_lint_and_then( + cx, + NEEDLESS_RANGE_LOOP, + arg.span, + &format!("the loop variable `{}` is used to index `{}`", ident.name, indexed), + |diag| { + multispan_sugg( + diag, + "consider using an iterator", + vec![ + (pat.span, format!("({}, <item>)", ident.name)), + ( + arg.span, + format!("{}.{}().enumerate(){}{}", indexed, method, method_1, method_2), + ), + ], + ); + }, + ); + } else { + let repl = if starts_at_zero && take_is_empty { + format!("&{}{}", ref_mut, indexed) + } else { + format!("{}.{}(){}{}", indexed, method, method_1, method_2) + }; + + span_lint_and_then( + cx, + NEEDLESS_RANGE_LOOP, + arg.span, + &format!("the loop variable `{}` is only used to index `{}`", ident.name, indexed), + |diag| { + multispan_sugg( + diag, + "consider using an iterator", + vec![(pat.span, "<item>".to_string()), (arg.span, repl)], + ); + }, + ); + } + } + } + } +} + +fn is_len_call(expr: &Expr<'_>, var: Symbol) -> bool { + if_chain! { + if let ExprKind::MethodCall(method, len_args, _) = expr.kind; + if len_args.len() == 1; + if method.ident.name == sym::len; + if let ExprKind::Path(QPath::Resolved(_, path)) = len_args[0].kind; + if path.segments.len() == 1; + if path.segments[0].ident.name == var; + then { + return true; + } + } + + false +} + +fn is_end_eq_array_len<'tcx>( + cx: &LateContext<'tcx>, + end: &Expr<'_>, + limits: ast::RangeLimits, + indexed_ty: Ty<'tcx>, +) -> bool { + if_chain! { + if let ExprKind::Lit(ref lit) = end.kind; + if let ast::LitKind::Int(end_int, _) = lit.node; + if let ty::Array(_, arr_len_const) = indexed_ty.kind(); + if let Some(arr_len) = arr_len_const.try_eval_usize(cx.tcx, cx.param_env); + then { + return match limits { + ast::RangeLimits::Closed => end_int + 1 >= arr_len.into(), + ast::RangeLimits::HalfOpen => end_int >= arr_len.into(), + }; + } + } + + false +} + +struct VarVisitor<'a, 'tcx> { + /// context reference + cx: &'a LateContext<'tcx>, + /// var name to look for as index + var: HirId, + /// indexed variables that are used mutably + indexed_mut: FxHashSet<Symbol>, + /// indirectly indexed variables (`v[(i + 4) % N]`), the extend is `None` for global + indexed_indirectly: FxHashMap<Symbol, Option<region::Scope>>, + /// subset of `indexed` of vars that are indexed directly: `v[i]` + /// this will not contain cases like `v[calc_index(i)]` or `v[(i + 4) % N]` + indexed_directly: FxHashMap<Symbol, (Option<region::Scope>, Ty<'tcx>)>, + /// Any names that are used outside an index operation. + /// Used to detect things like `&mut vec` used together with `vec[i]` + referenced: FxHashSet<Symbol>, + /// has the loop variable been used in expressions other than the index of + /// an index op? + nonindex: bool, + /// Whether we are inside the `$` in `&mut $` or `$ = foo` or `$.bar`, where bar + /// takes `&mut self` + prefer_mutable: bool, +} + +impl<'a, 'tcx> VarVisitor<'a, 'tcx> { + fn check(&mut self, idx: &'tcx Expr<'_>, seqexpr: &'tcx Expr<'_>, expr: &'tcx Expr<'_>) -> bool { + if_chain! { + // the indexed container is referenced by a name + if let ExprKind::Path(ref seqpath) = seqexpr.kind; + if let QPath::Resolved(None, seqvar) = *seqpath; + if seqvar.segments.len() == 1; + if is_local_used(self.cx, idx, self.var); + then { + if self.prefer_mutable { + self.indexed_mut.insert(seqvar.segments[0].ident.name); + } + let index_used_directly = matches!(idx.kind, ExprKind::Path(_)); + let res = self.cx.qpath_res(seqpath, seqexpr.hir_id); + match res { + Res::Local(hir_id) => { + let parent_def_id = self.cx.tcx.hir().get_parent_item(expr.hir_id); + let extent = self.cx + .tcx + .region_scope_tree(parent_def_id) + .var_scope(hir_id.local_id) + .unwrap(); + if index_used_directly { + self.indexed_directly.insert( + seqvar.segments[0].ident.name, + (Some(extent), self.cx.typeck_results().node_type(seqexpr.hir_id)), + ); + } else { + self.indexed_indirectly.insert(seqvar.segments[0].ident.name, Some(extent)); + } + return false; // no need to walk further *on the variable* + } + Res::Def(DefKind::Static (_)| DefKind::Const, ..) => { + if index_used_directly { + self.indexed_directly.insert( + seqvar.segments[0].ident.name, + (None, self.cx.typeck_results().node_type(seqexpr.hir_id)), + ); + } else { + self.indexed_indirectly.insert(seqvar.segments[0].ident.name, None); + } + return false; // no need to walk further *on the variable* + } + _ => (), + } + } + } + true + } +} + +impl<'a, 'tcx> Visitor<'tcx> for VarVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if_chain! { + // a range index op + if let ExprKind::MethodCall(meth, [args_0, args_1, ..], _) = &expr.kind; + if (meth.ident.name == sym::index && match_trait_method(self.cx, expr, &paths::INDEX)) + || (meth.ident.name == sym::index_mut && match_trait_method(self.cx, expr, &paths::INDEX_MUT)); + if !self.check(args_1, args_0, expr); + then { return } + } + + if_chain! { + // an index op + if let ExprKind::Index(seqexpr, idx) = expr.kind; + if !self.check(idx, seqexpr, expr); + then { return } + } + + if_chain! { + // directly using a variable + if let ExprKind::Path(QPath::Resolved(None, path)) = expr.kind; + if let Res::Local(local_id) = path.res; + then { + if local_id == self.var { + self.nonindex = true; + } else { + // not the correct variable, but still a variable + self.referenced.insert(path.segments[0].ident.name); + } + } + } + + let old = self.prefer_mutable; + match expr.kind { + ExprKind::AssignOp(_, lhs, rhs) | ExprKind::Assign(lhs, rhs, _) => { + self.prefer_mutable = true; + self.visit_expr(lhs); + self.prefer_mutable = false; + self.visit_expr(rhs); + }, + ExprKind::AddrOf(BorrowKind::Ref, mutbl, expr) => { + if mutbl == Mutability::Mut { + self.prefer_mutable = true; + } + self.visit_expr(expr); + }, + ExprKind::Call(f, args) => { + self.visit_expr(f); + for expr in args { + let ty = self.cx.typeck_results().expr_ty_adjusted(expr); + self.prefer_mutable = false; + if let ty::Ref(_, _, mutbl) = *ty.kind() { + if mutbl == Mutability::Mut { + self.prefer_mutable = true; + } + } + self.visit_expr(expr); + } + }, + ExprKind::MethodCall(_, args, _) => { + let def_id = self.cx.typeck_results().type_dependent_def_id(expr.hir_id).unwrap(); + for (ty, expr) in iter::zip(self.cx.tcx.fn_sig(def_id).inputs().skip_binder(), args) { + self.prefer_mutable = false; + if let ty::Ref(_, _, mutbl) = *ty.kind() { + if mutbl == Mutability::Mut { + self.prefer_mutable = true; + } + } + self.visit_expr(expr); + } + }, + ExprKind::Closure(&Closure { body, .. }) => { + let body = self.cx.tcx.hir().body(body); + self.visit_expr(&body.value); + }, + _ => walk_expr(self, expr), + } + self.prefer_mutable = old; + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/never_loop.rs b/src/tools/clippy/clippy_lints/src/loops/never_loop.rs new file mode 100644 index 000000000..32de20f65 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/never_loop.rs @@ -0,0 +1,218 @@ +use super::utils::make_iterator_snippet; +use super::NEVER_LOOP; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::higher::ForLoop; +use clippy_utils::source::snippet; +use rustc_errors::Applicability; +use rustc_hir::{Block, 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<'_>, + loop_id: HirId, + span: Span, + for_loop: Option<&ForLoop<'_>>, +) { + match never_loop_block(block, loop_id) { + NeverLoopResult::AlwaysBreak => { + span_lint_and_then(cx, NEVER_LOOP, span, "this loop never actually loops", |diag| { + if let Some(ForLoop { + arg: iterator, + pat, + span: for_span, + .. + }) = for_loop + { + // Suggests using an `if let` instead. This is `Unspecified` because the + // loop may (probably) contain `break` statements which would be invalid + // in an `if let`. + diag.span_suggestion_verbose( + for_span.with_hi(iterator.span.hi()), + "if you need the first element of the iterator, try writing", + for_to_if_let_sugg(cx, iterator, pat), + Applicability::Unspecified, + ); + } + }); + }, + NeverLoopResult::MayContinueMainLoop | NeverLoopResult::Otherwise => (), + } +} + +enum NeverLoopResult { + // A break/return always get triggered but not necessarily for the main loop. + AlwaysBreak, + // A continue may occur for the main loop. + MayContinueMainLoop, + Otherwise, +} + +#[must_use] +fn absorb_break(arg: &NeverLoopResult) -> NeverLoopResult { + match *arg { + NeverLoopResult::AlwaysBreak | NeverLoopResult::Otherwise => NeverLoopResult::Otherwise, + NeverLoopResult::MayContinueMainLoop => NeverLoopResult::MayContinueMainLoop, + } +} + +// Combine two results for parts that are called in order. +#[must_use] +fn combine_seq(first: NeverLoopResult, second: NeverLoopResult) -> NeverLoopResult { + match first { + NeverLoopResult::AlwaysBreak | NeverLoopResult::MayContinueMainLoop => first, + NeverLoopResult::Otherwise => second, + } +} + +// Combine two results where both parts are called but not necessarily in order. +#[must_use] +fn combine_both(left: NeverLoopResult, right: NeverLoopResult) -> NeverLoopResult { + match (left, right) { + (NeverLoopResult::MayContinueMainLoop, _) | (_, NeverLoopResult::MayContinueMainLoop) => { + NeverLoopResult::MayContinueMainLoop + }, + (NeverLoopResult::AlwaysBreak, _) | (_, NeverLoopResult::AlwaysBreak) => NeverLoopResult::AlwaysBreak, + (NeverLoopResult::Otherwise, NeverLoopResult::Otherwise) => NeverLoopResult::Otherwise, + } +} + +// Combine two results where only one of the part may have been executed. +#[must_use] +fn combine_branches(b1: NeverLoopResult, b2: NeverLoopResult) -> NeverLoopResult { + match (b1, b2) { + (NeverLoopResult::AlwaysBreak, NeverLoopResult::AlwaysBreak) => NeverLoopResult::AlwaysBreak, + (NeverLoopResult::MayContinueMainLoop, _) | (_, NeverLoopResult::MayContinueMainLoop) => { + NeverLoopResult::MayContinueMainLoop + }, + (NeverLoopResult::Otherwise, _) | (_, NeverLoopResult::Otherwise) => NeverLoopResult::Otherwise, + } +} + +fn never_loop_block(block: &Block<'_>, main_loop_id: HirId) -> NeverLoopResult { + let mut iter = block.stmts.iter().filter_map(stmt_to_expr).chain(block.expr); + never_loop_expr_seq(&mut iter, main_loop_id) +} + +fn never_loop_expr_seq<'a, T: Iterator<Item = &'a Expr<'a>>>(es: &mut T, main_loop_id: HirId) -> NeverLoopResult { + es.map(|e| never_loop_expr(e, main_loop_id)) + .fold(NeverLoopResult::Otherwise, combine_seq) +} + +fn stmt_to_expr<'tcx>(stmt: &Stmt<'tcx>) -> Option<&'tcx Expr<'tcx>> { + match stmt.kind { + StmtKind::Semi(e, ..) | StmtKind::Expr(e, ..) => Some(e), + StmtKind::Local(local) => local.init, + StmtKind::Item(..) => None, + } +} + +fn never_loop_expr(expr: &Expr<'_>, main_loop_id: HirId) -> NeverLoopResult { + match expr.kind { + ExprKind::Box(e) + | ExprKind::Unary(_, e) + | ExprKind::Cast(e, _) + | ExprKind::Type(e, _) + | ExprKind::Field(e, _) + | ExprKind::AddrOf(_, _, e) + | ExprKind::Repeat(e, _) + | ExprKind::DropTemps(e) => never_loop_expr(e, main_loop_id), + ExprKind::Let(let_expr) => never_loop_expr(let_expr.init, main_loop_id), + ExprKind::Array(es) | ExprKind::MethodCall(_, es, _) | ExprKind::Tup(es) => { + never_loop_expr_all(&mut es.iter(), main_loop_id) + }, + ExprKind::Struct(_, fields, base) => { + let fields = never_loop_expr_all(&mut fields.iter().map(|f| f.expr), main_loop_id); + if let Some(base) = base { + combine_both(fields, never_loop_expr(base, main_loop_id)) + } else { + fields + } + }, + ExprKind::Call(e, es) => never_loop_expr_all(&mut once(e).chain(es.iter()), 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(), main_loop_id), + ExprKind::Loop(b, _, _, _) => { + // Break can come from the inner loop so remove them. + absorb_break(&never_loop_block(b, main_loop_id)) + }, + ExprKind::If(e, e2, e3) => { + let e1 = never_loop_expr(e, main_loop_id); + let e2 = never_loop_expr(e2, main_loop_id); + let e3 = e3 + .as_ref() + .map_or(NeverLoopResult::Otherwise, |e| never_loop_expr(e, main_loop_id)); + combine_seq(e1, combine_branches(e2, e3)) + }, + ExprKind::Match(e, arms, _) => { + let e = never_loop_expr(e, main_loop_id); + if arms.is_empty() { + e + } else { + let arms = never_loop_expr_branch(&mut arms.iter().map(|a| a.body), main_loop_id); + combine_seq(e, arms) + } + }, + ExprKind::Block(b, _) => never_loop_block(b, main_loop_id), + ExprKind::Continue(d) => { + let id = d + .target_id + .expect("target ID can only be missing in the presence of compilation errors"); + if id == main_loop_id { + NeverLoopResult::MayContinueMainLoop + } else { + NeverLoopResult::AlwaysBreak + } + }, + ExprKind::Break(_, e) | ExprKind::Ret(e) => e.as_ref().map_or(NeverLoopResult::AlwaysBreak, |e| { + combine_seq(never_loop_expr(e, 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, main_loop_id) + }, + InlineAsmOperand::Out { expr, .. } => never_loop_expr_all(&mut expr.iter(), main_loop_id), + InlineAsmOperand::SplitInOut { in_expr, out_expr, .. } => { + never_loop_expr_all(&mut once(in_expr).chain(out_expr.iter()), main_loop_id) + }, + InlineAsmOperand::Const { .. } + | InlineAsmOperand::SymFn { .. } + | InlineAsmOperand::SymStatic { .. } => NeverLoopResult::Otherwise, + }) + .fold(NeverLoopResult::Otherwise, combine_both), + ExprKind::Yield(_, _) + | ExprKind::Closure { .. } + | ExprKind::Path(_) + | ExprKind::ConstBlock(_) + | ExprKind::Lit(_) + | ExprKind::Err => NeverLoopResult::Otherwise, + } +} + +fn never_loop_expr_all<'a, T: Iterator<Item = &'a Expr<'a>>>(es: &mut T, main_loop_id: HirId) -> NeverLoopResult { + es.map(|e| never_loop_expr(e, main_loop_id)) + .fold(NeverLoopResult::Otherwise, combine_both) +} + +fn never_loop_expr_branch<'a, T: Iterator<Item = &'a Expr<'a>>>(e: &mut T, main_loop_id: HirId) -> NeverLoopResult { + e.map(|e| never_loop_expr(e, main_loop_id)) + .fold(NeverLoopResult::AlwaysBreak, combine_branches) +} + +fn for_to_if_let_sugg(cx: &LateContext<'_>, iterator: &Expr<'_>, pat: &Pat<'_>) -> String { + let pat_snippet = snippet(cx, pat.span, "_"); + let iter_snippet = make_iterator_snippet(cx, iterator, &mut Applicability::Unspecified); + + format!( + "if let Some({pat}) = {iter}.next()", + pat = pat_snippet, + iter = iter_snippet + ) +} diff --git a/src/tools/clippy/clippy_lints/src/loops/same_item_push.rs b/src/tools/clippy/clippy_lints/src/loops/same_item_push.rs new file mode 100644 index 000000000..1439f1f4c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/same_item_push.rs @@ -0,0 +1,195 @@ +use super::SAME_ITEM_PUSH; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::path_to_local; +use clippy_utils::source::snippet_with_macro_callsite; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use if_chain::if_chain; +use rustc_data_structures::fx::FxHashSet; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{BindingAnnotation, Block, Expr, ExprKind, HirId, Node, Pat, PatKind, Stmt, StmtKind}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; +use std::iter::Iterator; + +/// Detects for loop pushing the same item into a Vec +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + _: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + _: &'tcx Expr<'_>, +) { + fn emit_lint(cx: &LateContext<'_>, vec: &Expr<'_>, pushed_item: &Expr<'_>) { + let vec_str = snippet_with_macro_callsite(cx, vec.span, ""); + let item_str = snippet_with_macro_callsite(cx, pushed_item.span, ""); + + span_lint_and_help( + cx, + SAME_ITEM_PUSH, + vec.span, + "it looks like the same item is being pushed into this Vec", + None, + &format!( + "try using vec![{};SIZE] or {}.resize(NEW_SIZE, {})", + item_str, vec_str, item_str + ), + ); + } + + if !matches!(pat.kind, PatKind::Wild) { + return; + } + + // Determine whether it is safe to lint the body + let mut same_item_push_visitor = SameItemPushVisitor::new(cx); + walk_expr(&mut same_item_push_visitor, body); + if_chain! { + if same_item_push_visitor.should_lint(); + if let Some((vec, pushed_item)) = same_item_push_visitor.vec_push; + let vec_ty = cx.typeck_results().expr_ty(vec); + let ty = vec_ty.walk().nth(1).unwrap().expect_ty(); + if cx + .tcx + .lang_items() + .clone_trait() + .map_or(false, |id| implements_trait(cx, ty, id, &[])); + then { + // Make sure that the push does not involve possibly mutating values + match pushed_item.kind { + ExprKind::Path(ref qpath) => { + match cx.qpath_res(qpath, pushed_item.hir_id) { + // immutable bindings that are initialized with literal or constant + Res::Local(hir_id) => { + let node = cx.tcx.hir().get(hir_id); + if_chain! { + if let Node::Pat(pat) = node; + if let PatKind::Binding(bind_ann, ..) = pat.kind; + if !matches!(bind_ann, BindingAnnotation::RefMut | BindingAnnotation::Mutable); + let parent_node = cx.tcx.hir().get_parent_node(hir_id); + if let Some(Node::Local(parent_let_expr)) = cx.tcx.hir().find(parent_node); + if let Some(init) = parent_let_expr.init; + then { + match init.kind { + // immutable bindings that are initialized with literal + ExprKind::Lit(..) => emit_lint(cx, vec, pushed_item), + // immutable bindings that are initialized with constant + ExprKind::Path(ref path) => { + if let Res::Def(DefKind::Const, ..) = cx.qpath_res(path, init.hir_id) { + emit_lint(cx, vec, pushed_item); + } + } + _ => {}, + } + } + } + }, + // constant + Res::Def(DefKind::Const, ..) => emit_lint(cx, vec, pushed_item), + _ => {}, + } + }, + ExprKind::Lit(..) => emit_lint(cx, vec, pushed_item), + _ => {}, + } + } + } +} + +// Scans the body of the for loop and determines whether lint should be given +struct SameItemPushVisitor<'a, 'tcx> { + non_deterministic_expr: bool, + multiple_pushes: bool, + // this field holds the last vec push operation visited, which should be the only push seen + vec_push: Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)>, + cx: &'a LateContext<'tcx>, + used_locals: FxHashSet<HirId>, +} + +impl<'a, 'tcx> SameItemPushVisitor<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> Self { + Self { + non_deterministic_expr: false, + multiple_pushes: false, + vec_push: None, + cx, + used_locals: FxHashSet::default(), + } + } + + fn should_lint(&self) -> bool { + if_chain! { + if !self.non_deterministic_expr; + if !self.multiple_pushes; + if let Some((vec, _)) = self.vec_push; + if let Some(hir_id) = path_to_local(vec); + then { + !self.used_locals.contains(&hir_id) + } else { + false + } + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for SameItemPushVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + match &expr.kind { + // Non-determinism may occur ... don't give a lint + ExprKind::Loop(..) | ExprKind::Match(..) | ExprKind::If(..) => self.non_deterministic_expr = true, + ExprKind::Block(block, _) => self.visit_block(block), + _ => { + if let Some(hir_id) = path_to_local(expr) { + self.used_locals.insert(hir_id); + } + walk_expr(self, expr); + }, + } + } + + fn visit_block(&mut self, b: &'tcx Block<'_>) { + for stmt in b.stmts.iter() { + self.visit_stmt(stmt); + } + } + + fn visit_stmt(&mut self, s: &'tcx Stmt<'_>) { + let vec_push_option = get_vec_push(self.cx, s); + if vec_push_option.is_none() { + // Current statement is not a push so visit inside + match &s.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => self.visit_expr(expr), + _ => {}, + } + } else { + // Current statement is a push ...check whether another + // push had been previously done + if self.vec_push.is_none() { + self.vec_push = vec_push_option; + } else { + // There are multiple pushes ... don't lint + self.multiple_pushes = true; + } + } + } +} + +// Given some statement, determine if that statement is a push on a Vec. If it is, return +// the Vec being pushed into and the item being pushed +fn get_vec_push<'tcx>(cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> { + if_chain! { + // Extract method being called + if let StmtKind::Semi(semi_stmt) = &stmt.kind; + if let ExprKind::MethodCall(path, args, _) = &semi_stmt.kind; + // Figure out the parameters for the method call + if let Some(self_expr) = args.get(0); + if let Some(pushed_item) = args.get(1); + // Check that the method being called is push() on a Vec + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(self_expr), sym::Vec); + if path.ident.name.as_str() == "push"; + then { + return Some((self_expr, pushed_item)) + } + } + None +} diff --git a/src/tools/clippy/clippy_lints/src/loops/single_element_loop.rs b/src/tools/clippy/clippy_lints/src/loops/single_element_loop.rs new file mode 100644 index 000000000..a0bd7ad0a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/single_element_loop.rs @@ -0,0 +1,101 @@ +use super::SINGLE_ELEMENT_LOOP; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{indent_of, snippet_with_applicability}; +use if_chain::if_chain; +use rustc_ast::util::parser::PREC_PREFIX; +use rustc_ast::Mutability; +use rustc_errors::Applicability; +use rustc_hir::{is_range_literal, BorrowKind, Expr, ExprKind, Pat}; +use rustc_lint::LateContext; +use rustc_span::edition::Edition; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + arg: &'tcx Expr<'_>, + body: &'tcx Expr<'_>, + expr: &'tcx Expr<'_>, +) { + let (arg_expression, prefix) = match arg.kind { + ExprKind::AddrOf( + BorrowKind::Ref, + Mutability::Not, + Expr { + kind: ExprKind::Array([arg]), + .. + }, + ) => (arg, "&"), + ExprKind::AddrOf( + BorrowKind::Ref, + Mutability::Mut, + Expr { + kind: ExprKind::Array([arg]), + .. + }, + ) => (arg, "&mut "), + ExprKind::MethodCall( + method, + [ + Expr { + kind: ExprKind::Array([arg]), + .. + }, + ], + _, + ) if method.ident.name == rustc_span::sym::iter => (arg, "&"), + ExprKind::MethodCall( + method, + [ + Expr { + kind: ExprKind::Array([arg]), + .. + }, + ], + _, + ) if method.ident.name.as_str() == "iter_mut" => (arg, "&mut "), + ExprKind::MethodCall( + method, + [ + Expr { + kind: ExprKind::Array([arg]), + .. + }, + ], + _, + ) if method.ident.name == rustc_span::sym::into_iter => (arg, ""), + // Only check for arrays edition 2021 or later, as this case will trigger a compiler error otherwise. + ExprKind::Array([arg]) if cx.tcx.sess.edition() >= Edition::Edition2021 => (arg, ""), + _ => return, + }; + if_chain! { + if let ExprKind::Block(block, _) = body.kind; + if !block.stmts.is_empty(); + then { + let mut applicability = Applicability::MachineApplicable; + let pat_snip = snippet_with_applicability(cx, pat.span, "..", &mut applicability); + let mut arg_snip = snippet_with_applicability(cx, arg_expression.span, "..", &mut applicability); + let mut block_str = snippet_with_applicability(cx, block.span, "..", &mut applicability).into_owned(); + block_str.remove(0); + block_str.pop(); + let indent = " ".repeat(indent_of(cx, block.stmts[0].span).unwrap_or(0)); + + // Reference iterator from `&(mut) []` or `[].iter(_mut)()`. + if !prefix.is_empty() && ( + // Precedence of internal expression is less than or equal to precedence of `&expr`. + arg_expression.precedence().order() <= PREC_PREFIX || is_range_literal(arg_expression) + ) { + arg_snip = format!("({arg_snip})").into(); + } + + span_lint_and_sugg( + cx, + SINGLE_ELEMENT_LOOP, + expr.span, + "for loop over a single element", + "try", + format!("{{\n{indent}let {pat_snip} = {prefix}{arg_snip};{block_str}}}"), + applicability, + ) + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/utils.rs b/src/tools/clippy/clippy_lints/src/loops/utils.rs new file mode 100644 index 000000000..4801a84eb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/utils.rs @@ -0,0 +1,358 @@ +use clippy_utils::ty::{has_iter_method, implements_trait}; +use clippy_utils::{get_parent_expr, is_integer_const, path_to_local, path_to_local_id, sugg}; +use if_chain::if_chain; +use rustc_ast::ast::{LitIntType, LitKind}; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_expr, walk_local, walk_pat, walk_stmt, Visitor}; +use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, HirId, HirIdMap, Local, Mutability, Pat, PatKind, Stmt}; +use rustc_lint::LateContext; +use rustc_middle::hir::nested_filter; +use rustc_middle::ty::{self, Ty}; +use rustc_span::source_map::Spanned; +use rustc_span::symbol::{sym, Symbol}; +use rustc_typeck::hir_ty_to_ty; +use std::iter::Iterator; + +#[derive(Debug, PartialEq, Eq)] +enum IncrementVisitorVarState { + Initial, // Not examined yet + IncrOnce, // Incremented exactly once, may be a loop counter + DontWarn, +} + +/// Scan a for loop for variables that are incremented exactly once and not used after that. +pub(super) struct IncrementVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, // context reference + states: HirIdMap<IncrementVisitorVarState>, // incremented variables + depth: u32, // depth of conditional expressions + done: bool, +} + +impl<'a, 'tcx> IncrementVisitor<'a, 'tcx> { + pub(super) fn new(cx: &'a LateContext<'tcx>) -> Self { + Self { + cx, + states: HirIdMap::default(), + depth: 0, + done: false, + } + } + + pub(super) fn into_results(self) -> impl Iterator<Item = HirId> { + self.states.into_iter().filter_map(|(id, state)| { + if state == IncrementVisitorVarState::IncrOnce { + Some(id) + } else { + None + } + }) + } +} + +impl<'a, 'tcx> Visitor<'tcx> for IncrementVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if self.done { + return; + } + + // If node is a variable + if let Some(def_id) = path_to_local(expr) { + if let Some(parent) = get_parent_expr(self.cx, expr) { + let state = self.states.entry(def_id).or_insert(IncrementVisitorVarState::Initial); + if *state == IncrementVisitorVarState::IncrOnce { + *state = IncrementVisitorVarState::DontWarn; + return; + } + + match parent.kind { + ExprKind::AssignOp(op, lhs, rhs) => { + if lhs.hir_id == expr.hir_id { + *state = if op.node == BinOpKind::Add + && is_integer_const(self.cx, rhs, 1) + && *state == IncrementVisitorVarState::Initial + && self.depth == 0 + { + IncrementVisitorVarState::IncrOnce + } else { + // Assigned some other value or assigned multiple times + IncrementVisitorVarState::DontWarn + }; + } + }, + ExprKind::Assign(lhs, _, _) if lhs.hir_id == expr.hir_id => { + *state = IncrementVisitorVarState::DontWarn; + }, + ExprKind::AddrOf(BorrowKind::Ref, mutability, _) if mutability == Mutability::Mut => { + *state = IncrementVisitorVarState::DontWarn; + }, + _ => (), + } + } + + walk_expr(self, expr); + } else if is_loop(expr) || is_conditional(expr) { + self.depth += 1; + walk_expr(self, expr); + self.depth -= 1; + } else if let ExprKind::Continue(_) = expr.kind { + self.done = true; + } else { + walk_expr(self, expr); + } + } +} + +enum InitializeVisitorState<'hir> { + Initial, // Not examined yet + Declared(Symbol, Option<Ty<'hir>>), // Declared but not (yet) initialized + Initialized { + name: Symbol, + ty: Option<Ty<'hir>>, + initializer: &'hir Expr<'hir>, + }, + DontWarn, +} + +/// Checks whether a variable is initialized at the start of a loop and not modified +/// and used after the loop. +pub(super) struct InitializeVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, // context reference + end_expr: &'tcx Expr<'tcx>, // the for loop. Stop scanning here. + var_id: HirId, + state: InitializeVisitorState<'tcx>, + depth: u32, // depth of conditional expressions + past_loop: bool, +} + +impl<'a, 'tcx> InitializeVisitor<'a, 'tcx> { + pub(super) fn new(cx: &'a LateContext<'tcx>, end_expr: &'tcx Expr<'tcx>, var_id: HirId) -> Self { + Self { + cx, + end_expr, + var_id, + state: InitializeVisitorState::Initial, + depth: 0, + past_loop: false, + } + } + + pub(super) fn get_result(&self) -> Option<(Symbol, Option<Ty<'tcx>>, &'tcx Expr<'tcx>)> { + if let InitializeVisitorState::Initialized { name, ty, initializer } = self.state { + Some((name, ty, initializer)) + } else { + None + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for InitializeVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_local(&mut self, l: &'tcx Local<'_>) { + // Look for declarations of the variable + if_chain! { + if l.pat.hir_id == self.var_id; + if let PatKind::Binding(.., ident, _) = l.pat.kind; + then { + let ty = l.ty.map(|ty| hir_ty_to_ty(self.cx.tcx, ty)); + + self.state = l.init.map_or(InitializeVisitorState::Declared(ident.name, ty), |init| { + InitializeVisitorState::Initialized { + initializer: init, + ty, + name: ident.name, + } + }) + } + } + + walk_local(self, l); + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if matches!(self.state, InitializeVisitorState::DontWarn) { + return; + } + if expr.hir_id == self.end_expr.hir_id { + self.past_loop = true; + return; + } + // No need to visit expressions before the variable is + // declared + if matches!(self.state, InitializeVisitorState::Initial) { + return; + } + + // If node is the desired variable, see how it's used + if path_to_local_id(expr, self.var_id) { + if self.past_loop { + self.state = InitializeVisitorState::DontWarn; + return; + } + + if let Some(parent) = get_parent_expr(self.cx, expr) { + match parent.kind { + ExprKind::AssignOp(_, lhs, _) if lhs.hir_id == expr.hir_id => { + self.state = InitializeVisitorState::DontWarn; + }, + ExprKind::Assign(lhs, rhs, _) if lhs.hir_id == expr.hir_id => { + self.state = if self.depth == 0 { + match self.state { + InitializeVisitorState::Declared(name, mut ty) => { + if ty.is_none() { + if let ExprKind::Lit(Spanned { + node: LitKind::Int(_, LitIntType::Unsuffixed), + .. + }) = rhs.kind + { + ty = None; + } else { + ty = self.cx.typeck_results().expr_ty_opt(rhs); + } + } + + InitializeVisitorState::Initialized { + initializer: rhs, + ty, + name, + } + }, + InitializeVisitorState::Initialized { ty, name, .. } => { + InitializeVisitorState::Initialized { + initializer: rhs, + ty, + name, + } + }, + _ => InitializeVisitorState::DontWarn, + } + } else { + InitializeVisitorState::DontWarn + } + }, + ExprKind::AddrOf(BorrowKind::Ref, mutability, _) if mutability == Mutability::Mut => { + self.state = InitializeVisitorState::DontWarn; + }, + _ => (), + } + } + + walk_expr(self, expr); + } else if !self.past_loop && is_loop(expr) { + self.state = InitializeVisitorState::DontWarn; + } else if is_conditional(expr) { + self.depth += 1; + walk_expr(self, expr); + self.depth -= 1; + } else { + walk_expr(self, expr); + } + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +fn is_loop(expr: &Expr<'_>) -> bool { + matches!(expr.kind, ExprKind::Loop(..)) +} + +fn is_conditional(expr: &Expr<'_>) -> bool { + matches!(expr.kind, ExprKind::If(..) | ExprKind::Match(..)) +} + +#[derive(PartialEq, Eq)] +pub(super) enum Nesting { + Unknown, // no nesting detected yet + RuledOut, // the iterator is initialized or assigned within scope + LookFurther, // no nesting detected, no further walk required +} + +use self::Nesting::{LookFurther, RuledOut, Unknown}; + +pub(super) struct LoopNestVisitor { + pub(super) hir_id: HirId, + pub(super) iterator: HirId, + pub(super) nesting: Nesting, +} + +impl<'tcx> Visitor<'tcx> for LoopNestVisitor { + fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) { + if stmt.hir_id == self.hir_id { + self.nesting = LookFurther; + } else if self.nesting == Unknown { + walk_stmt(self, stmt); + } + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if self.nesting != Unknown { + return; + } + if expr.hir_id == self.hir_id { + self.nesting = LookFurther; + return; + } + match expr.kind { + ExprKind::Assign(path, _, _) | ExprKind::AssignOp(_, path, _) => { + if path_to_local_id(path, self.iterator) { + self.nesting = RuledOut; + } + }, + _ => walk_expr(self, expr), + } + } + + fn visit_pat(&mut self, pat: &'tcx Pat<'_>) { + if self.nesting != Unknown { + return; + } + if let PatKind::Binding(_, id, ..) = pat.kind { + if id == self.iterator { + self.nesting = RuledOut; + return; + } + } + walk_pat(self, pat); + } +} + +/// If `arg` was the argument to a `for` loop, return the "cleanest" way of writing the +/// actual `Iterator` that the loop uses. +pub(super) fn make_iterator_snippet(cx: &LateContext<'_>, arg: &Expr<'_>, applic_ref: &mut Applicability) -> String { + let impls_iterator = cx.tcx.get_diagnostic_item(sym::Iterator).map_or(false, |id| { + implements_trait(cx, cx.typeck_results().expr_ty(arg), id, &[]) + }); + if impls_iterator { + format!( + "{}", + sugg::Sugg::hir_with_applicability(cx, arg, "_", applic_ref).maybe_par() + ) + } else { + // (&x).into_iter() ==> x.iter() + // (&mut x).into_iter() ==> x.iter_mut() + let arg_ty = cx.typeck_results().expr_ty_adjusted(arg); + match &arg_ty.kind() { + ty::Ref(_, inner_ty, mutbl) if has_iter_method(cx, *inner_ty).is_some() => { + let method_name = match mutbl { + Mutability::Mut => "iter_mut", + Mutability::Not => "iter", + }; + let caller = match &arg.kind { + ExprKind::AddrOf(BorrowKind::Ref, _, arg_inner) => arg_inner, + _ => arg, + }; + format!( + "{}.{}()", + sugg::Sugg::hir_with_applicability(cx, caller, "_", applic_ref).maybe_par(), + method_name, + ) + }, + _ => format!( + "{}.into_iter()", + sugg::Sugg::hir_with_applicability(cx, arg, "_", applic_ref).maybe_par() + ), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/while_immutable_condition.rs b/src/tools/clippy/clippy_lints/src/loops/while_immutable_condition.rs new file mode 100644 index 000000000..a63422d2a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/while_immutable_condition.rs @@ -0,0 +1,128 @@ +use super::WHILE_IMMUTABLE_CONDITION; +use clippy_utils::consts::constant; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::usage::mutated_variables; +use if_chain::if_chain; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::def_id::DefIdMap; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::HirIdSet; +use rustc_hir::{Expr, ExprKind, QPath}; +use rustc_lint::LateContext; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, cond: &'tcx Expr<'_>, expr: &'tcx Expr<'_>) { + if constant(cx, cx.typeck_results(), cond).is_some() { + // A pure constant condition (e.g., `while false`) is not linted. + return; + } + + let mut var_visitor = VarCollectorVisitor { + cx, + ids: HirIdSet::default(), + def_ids: DefIdMap::default(), + skip: false, + }; + var_visitor.visit_expr(cond); + if var_visitor.skip { + return; + } + let used_in_condition = &var_visitor.ids; + let mutated_in_body = mutated_variables(expr, cx); + let mutated_in_condition = mutated_variables(cond, cx); + let no_cond_variable_mutated = + if let (Some(used_mutably_body), Some(used_mutably_cond)) = (mutated_in_body, mutated_in_condition) { + used_in_condition.is_disjoint(&used_mutably_body) && used_in_condition.is_disjoint(&used_mutably_cond) + } else { + return; + }; + let mutable_static_in_cond = var_visitor.def_ids.iter().any(|(_, v)| *v); + + let mut has_break_or_return_visitor = HasBreakOrReturnVisitor { + has_break_or_return: false, + }; + has_break_or_return_visitor.visit_expr(expr); + let has_break_or_return = has_break_or_return_visitor.has_break_or_return; + + if no_cond_variable_mutated && !mutable_static_in_cond { + span_lint_and_then( + cx, + WHILE_IMMUTABLE_CONDITION, + cond.span, + "variables in the condition are not mutated in the loop body", + |diag| { + diag.note("this may lead to an infinite or to a never running loop"); + + if has_break_or_return { + diag.note("this loop contains `return`s or `break`s"); + diag.help("rewrite it as `if cond { loop { } }`"); + } + }, + ); + } +} + +struct HasBreakOrReturnVisitor { + has_break_or_return: bool, +} + +impl<'tcx> Visitor<'tcx> for HasBreakOrReturnVisitor { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if self.has_break_or_return { + return; + } + + match expr.kind { + ExprKind::Ret(_) | ExprKind::Break(_, _) => { + self.has_break_or_return = true; + return; + }, + _ => {}, + } + + walk_expr(self, expr); + } +} + +/// Collects the set of variables in an expression +/// Stops analysis if a function call is found +/// Note: In some cases such as `self`, there are no mutable annotation, +/// All variables definition IDs are collected +struct VarCollectorVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + ids: HirIdSet, + def_ids: DefIdMap<bool>, + skip: bool, +} + +impl<'a, 'tcx> VarCollectorVisitor<'a, 'tcx> { + fn insert_def_id(&mut self, ex: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::Path(ref qpath) = ex.kind; + if let QPath::Resolved(None, _) = *qpath; + then { + match self.cx.qpath_res(qpath, ex.hir_id) { + Res::Local(hir_id) => { + self.ids.insert(hir_id); + }, + Res::Def(DefKind::Static(_), def_id) => { + let mutable = self.cx.tcx.is_mutable_static(def_id); + self.def_ids.insert(def_id, mutable); + }, + _ => {}, + } + } + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for VarCollectorVisitor<'a, 'tcx> { + fn visit_expr(&mut self, ex: &'tcx Expr<'_>) { + match ex.kind { + ExprKind::Path(_) => self.insert_def_id(ex), + // If there is any function/method call… we just stop analysis + ExprKind::Call(..) | ExprKind::MethodCall(..) => self.skip = true, + + _ => walk_expr(self, ex), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/loops/while_let_loop.rs b/src/tools/clippy/clippy_lints/src/loops/while_let_loop.rs new file mode 100644 index 000000000..ca617859d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/while_let_loop.rs @@ -0,0 +1,96 @@ +use super::WHILE_LET_LOOP; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::higher; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::needs_ordered_drop; +use clippy_utils::visitors::any_temporaries_need_ordered_drop; +use rustc_errors::Applicability; +use rustc_hir::{Block, Expr, ExprKind, Local, MatchSource, Pat, StmtKind}; +use rustc_lint::LateContext; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_block: &'tcx Block<'_>) { + let (init, has_trailing_exprs) = match (loop_block.stmts, loop_block.expr) { + ([stmt, stmts @ ..], expr) => { + if let StmtKind::Local(&Local { init: Some(e), els: None, .. }) | StmtKind::Semi(e) | StmtKind::Expr(e) = stmt.kind { + (e, !stmts.is_empty() || expr.is_some()) + } else { + return; + } + }, + ([], Some(e)) => (e, false), + _ => return, + }; + + if let Some(if_let) = higher::IfLet::hir(cx, init) + && let Some(else_expr) = if_let.if_else + && is_simple_break_expr(else_expr) + { + could_be_while_let(cx, expr, if_let.let_pat, if_let.let_expr, has_trailing_exprs); + } else if let ExprKind::Match(scrutinee, [arm1, arm2], MatchSource::Normal) = init.kind + && arm1.guard.is_none() + && arm2.guard.is_none() + && is_simple_break_expr(arm2.body) + { + could_be_while_let(cx, expr, arm1.pat, scrutinee, has_trailing_exprs); + } +} + +/// Returns `true` if expr contains a single break expression without a label or eub-expression. +fn is_simple_break_expr(e: &Expr<'_>) -> bool { + matches!(peel_blocks(e).kind, ExprKind::Break(dest, None) if dest.label.is_none()) +} + +/// Removes any blocks containing only a single expression. +fn peel_blocks<'tcx>(e: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> { + if let ExprKind::Block(b, _) = e.kind { + match (b.stmts, b.expr) { + ([s], None) => { + if let StmtKind::Expr(e) | StmtKind::Semi(e) = s.kind { + peel_blocks(e) + } else { + e + } + }, + ([], Some(e)) => peel_blocks(e), + _ => e, + } + } else { + e + } +} + +fn could_be_while_let<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + let_pat: &'tcx Pat<'_>, + let_expr: &'tcx Expr<'_>, + has_trailing_exprs: bool, +) { + if has_trailing_exprs + && (needs_ordered_drop(cx, cx.typeck_results().expr_ty(let_expr)) + || any_temporaries_need_ordered_drop(cx, let_expr)) + { + // Switching to a `while let` loop will extend the lifetime of some values. + return; + } + + // NOTE: we used to build a body here instead of using + // ellipsis, this was removed because: + // 1) it was ugly with big bodies; + // 2) it was not indented properly; + // 3) it wasn’t very smart (see #675). + let mut applicability = Applicability::HasPlaceholders; + span_lint_and_sugg( + cx, + WHILE_LET_LOOP, + expr.span, + "this loop could be written as a `while let` loop", + "try", + format!( + "while let {} = {} {{ .. }}", + snippet_with_applicability(cx, let_pat.span, "..", &mut applicability), + snippet_with_applicability(cx, let_expr.span, "..", &mut applicability), + ), + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/loops/while_let_on_iterator.rs b/src/tools/clippy/clippy_lints/src/loops/while_let_on_iterator.rs new file mode 100644 index 000000000..e9e215e66 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/loops/while_let_on_iterator.rs @@ -0,0 +1,362 @@ +use super::WHILE_LET_ON_ITERATOR; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::higher; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{ + get_enclosing_loop_or_multi_call_closure, is_refutable, is_trait_method, match_def_path, paths, + visitors::is_res_used, +}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{def::Res, Closure, Expr, ExprKind, HirId, Local, Mutability, PatKind, QPath, UnOp}; +use rustc_lint::LateContext; +use rustc_middle::hir::nested_filter::OnlyBodies; +use rustc_middle::ty::adjustment::Adjust; +use rustc_span::{symbol::sym, Symbol}; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (scrutinee_expr, iter_expr_struct, iter_expr, some_pat, loop_expr) = if_chain! { + if let Some(higher::WhileLet { if_then, let_pat, let_expr }) = higher::WhileLet::hir(expr); + // check for `Some(..)` pattern + if let PatKind::TupleStruct(QPath::Resolved(None, pat_path), some_pat, _) = let_pat.kind; + if let Res::Def(_, pat_did) = pat_path.res; + if match_def_path(cx, pat_did, &paths::OPTION_SOME); + // check for call to `Iterator::next` + if let ExprKind::MethodCall(method_name, [iter_expr], _) = let_expr.kind; + if method_name.ident.name == sym::next; + if is_trait_method(cx, let_expr, sym::Iterator); + if let Some(iter_expr_struct) = try_parse_iter_expr(cx, iter_expr); + // get the loop containing the match expression + if !uses_iter(cx, &iter_expr_struct, if_then); + then { + (let_expr, iter_expr_struct, iter_expr, some_pat, expr) + } else { + return; + } + }; + + let mut applicability = Applicability::MachineApplicable; + let loop_var = if let Some(some_pat) = some_pat.first() { + if is_refutable(cx, some_pat) { + // Refutable patterns don't work with for loops. + return; + } + snippet_with_applicability(cx, some_pat.span, "..", &mut applicability) + } else { + "_".into() + }; + + // If the iterator is a field or the iterator is accessed after the loop is complete it needs to be + // borrowed mutably. TODO: If the struct can be partially moved from and the struct isn't used + // afterwards a mutable borrow of a field isn't necessary. + let by_ref = if cx.typeck_results().expr_ty(iter_expr).ref_mutability() == Some(Mutability::Mut) + || !iter_expr_struct.can_move + || !iter_expr_struct.fields.is_empty() + || needs_mutable_borrow(cx, &iter_expr_struct, loop_expr) + { + ".by_ref()" + } else { + "" + }; + + let iterator = snippet_with_applicability(cx, iter_expr.span, "_", &mut applicability); + span_lint_and_sugg( + cx, + WHILE_LET_ON_ITERATOR, + expr.span.with_hi(scrutinee_expr.span.hi()), + "this loop could be written as a `for` loop", + "try", + format!("for {} in {}{}", loop_var, iterator, by_ref), + applicability, + ); +} + +#[derive(Debug)] +struct IterExpr { + /// The fields used, in order of child to parent. + fields: Vec<Symbol>, + /// The path being used. + path: Res, + /// Whether or not the iterator can be moved. + can_move: bool, +} + +/// Parses any expression to find out which field of which variable is used. Will return `None` if +/// the expression might have side effects. +fn try_parse_iter_expr(cx: &LateContext<'_>, mut e: &Expr<'_>) -> Option<IterExpr> { + let mut fields = Vec::new(); + let mut can_move = true; + loop { + if cx + .typeck_results() + .expr_adjustments(e) + .iter() + .any(|a| matches!(a.kind, Adjust::Deref(Some(..)))) + { + // Custom deref impls need to borrow the whole value as it's captured by reference + can_move = false; + fields.clear(); + } + match e.kind { + ExprKind::Path(ref path) => { + break Some(IterExpr { + fields, + path: cx.qpath_res(path, e.hir_id), + can_move, + }); + }, + ExprKind::Field(base, name) => { + fields.push(name.name); + e = base; + }, + // Dereferencing a pointer has no side effects and doesn't affect which field is being used. + ExprKind::Unary(UnOp::Deref, base) if cx.typeck_results().expr_ty(base).is_ref() => e = base, + + // Shouldn't have side effects, but there's no way to trace which field is used. So forget which fields have + // already been seen. + ExprKind::Index(base, idx) if !idx.can_have_side_effects() => { + can_move = false; + fields.clear(); + e = base; + }, + ExprKind::Unary(UnOp::Deref, base) => { + can_move = false; + fields.clear(); + e = base; + }, + + // No effect and doesn't affect which field is being used. + ExprKind::DropTemps(base) | ExprKind::AddrOf(_, _, base) | ExprKind::Type(base, _) => e = base, + _ => break None, + } + } +} + +fn is_expr_same_field(cx: &LateContext<'_>, mut e: &Expr<'_>, mut fields: &[Symbol], path_res: Res) -> bool { + loop { + match (&e.kind, fields) { + (&ExprKind::Field(base, name), [head_field, tail_fields @ ..]) if name.name == *head_field => { + e = base; + fields = tail_fields; + }, + (ExprKind::Path(path), []) => { + break cx.qpath_res(path, e.hir_id) == path_res; + }, + (&(ExprKind::DropTemps(base) | ExprKind::AddrOf(_, _, base) | ExprKind::Type(base, _)), _) => e = base, + _ => break false, + } + } +} + +/// Checks if the given expression is the same field as, is a child of, or is the parent of the +/// given field. Used to check if the expression can be used while the given field is borrowed +/// mutably. e.g. if checking for `x.y`, then `x.y`, `x.y.z`, and `x` will all return true, but +/// `x.z`, and `y` will return false. +fn is_expr_same_child_or_parent_field(cx: &LateContext<'_>, expr: &Expr<'_>, fields: &[Symbol], path_res: Res) -> bool { + match expr.kind { + ExprKind::Field(base, name) => { + if let Some((head_field, tail_fields)) = fields.split_first() { + if name.name == *head_field && is_expr_same_field(cx, base, tail_fields, path_res) { + return true; + } + // Check if the expression is a parent field + let mut fields_iter = tail_fields.iter(); + while let Some(field) = fields_iter.next() { + if *field == name.name && is_expr_same_field(cx, base, fields_iter.as_slice(), path_res) { + return true; + } + } + } + + // Check if the expression is a child field. + let mut e = base; + loop { + match e.kind { + ExprKind::Field(..) if is_expr_same_field(cx, e, fields, path_res) => break true, + ExprKind::Field(base, _) | ExprKind::DropTemps(base) | ExprKind::Type(base, _) => e = base, + ExprKind::Path(ref path) if fields.is_empty() => { + break cx.qpath_res(path, e.hir_id) == path_res; + }, + _ => break false, + } + } + }, + // If the path matches, this is either an exact match, or the expression is a parent of the field. + ExprKind::Path(ref path) => cx.qpath_res(path, expr.hir_id) == path_res, + ExprKind::DropTemps(base) | ExprKind::Type(base, _) | ExprKind::AddrOf(_, _, base) => { + is_expr_same_child_or_parent_field(cx, base, fields, path_res) + }, + _ => false, + } +} + +/// Strips off all field and path expressions. This will return true if a field or path has been +/// skipped. Used to skip them after failing to check for equality. +fn skip_fields_and_path<'tcx>(expr: &'tcx Expr<'_>) -> (Option<&'tcx Expr<'tcx>>, bool) { + let mut e = expr; + let e = loop { + match e.kind { + ExprKind::Field(base, _) | ExprKind::DropTemps(base) | ExprKind::Type(base, _) => e = base, + ExprKind::Path(_) => return (None, true), + _ => break e, + } + }; + (Some(e), e.hir_id != expr.hir_id) +} + +/// Checks if the given expression uses the iterator. +fn uses_iter<'tcx>(cx: &LateContext<'tcx>, iter_expr: &IterExpr, container: &'tcx Expr<'_>) -> bool { + struct V<'a, 'b, 'tcx> { + cx: &'a LateContext<'tcx>, + iter_expr: &'b IterExpr, + uses_iter: bool, + } + impl<'tcx> Visitor<'tcx> for V<'_, '_, 'tcx> { + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + if self.uses_iter { + // return + } else if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) { + self.uses_iter = true; + } else if let (e, true) = skip_fields_and_path(e) { + if let Some(e) = e { + self.visit_expr(e); + } + } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind { + if is_res_used(self.cx, self.iter_expr.path, id) { + self.uses_iter = true; + } + } else { + walk_expr(self, e); + } + } + } + + let mut v = V { + cx, + iter_expr, + uses_iter: false, + }; + v.visit_expr(container); + v.uses_iter +} + +#[expect(clippy::too_many_lines)] +fn needs_mutable_borrow(cx: &LateContext<'_>, iter_expr: &IterExpr, loop_expr: &Expr<'_>) -> bool { + struct AfterLoopVisitor<'a, 'b, 'tcx> { + cx: &'a LateContext<'tcx>, + iter_expr: &'b IterExpr, + loop_id: HirId, + after_loop: bool, + used_iter: bool, + } + impl<'tcx> Visitor<'tcx> for AfterLoopVisitor<'_, '_, 'tcx> { + type NestedFilter = OnlyBodies; + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + if self.used_iter { + return; + } + if self.after_loop { + if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) { + self.used_iter = true; + } else if let (e, true) = skip_fields_and_path(e) { + if let Some(e) = e { + self.visit_expr(e); + } + } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind { + self.used_iter = is_res_used(self.cx, self.iter_expr.path, id); + } else { + walk_expr(self, e); + } + } else if self.loop_id == e.hir_id { + self.after_loop = true; + } else { + walk_expr(self, e); + } + } + } + + struct NestedLoopVisitor<'a, 'b, 'tcx> { + cx: &'a LateContext<'tcx>, + iter_expr: &'b IterExpr, + local_id: HirId, + loop_id: HirId, + after_loop: bool, + found_local: bool, + used_after: bool, + } + impl<'a, 'b, 'tcx> Visitor<'tcx> for NestedLoopVisitor<'a, 'b, 'tcx> { + type NestedFilter = OnlyBodies; + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_local(&mut self, l: &'tcx Local<'_>) { + if !self.after_loop { + l.pat.each_binding_or_first(&mut |_, id, _, _| { + if id == self.local_id { + self.found_local = true; + } + }); + } + if let Some(e) = l.init { + self.visit_expr(e); + } + } + + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + if self.used_after { + return; + } + if self.after_loop { + if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) { + self.used_after = true; + } else if let (e, true) = skip_fields_and_path(e) { + if let Some(e) = e { + self.visit_expr(e); + } + } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind { + self.used_after = is_res_used(self.cx, self.iter_expr.path, id); + } else { + walk_expr(self, e); + } + } else if e.hir_id == self.loop_id { + self.after_loop = true; + } else { + walk_expr(self, e); + } + } + } + + if let Some(e) = get_enclosing_loop_or_multi_call_closure(cx, loop_expr) { + let local_id = match iter_expr.path { + Res::Local(id) => id, + _ => return true, + }; + let mut v = NestedLoopVisitor { + cx, + iter_expr, + local_id, + loop_id: loop_expr.hir_id, + after_loop: false, + found_local: false, + used_after: false, + }; + v.visit_expr(e); + v.used_after || !v.found_local + } else { + let mut v = AfterLoopVisitor { + cx, + iter_expr, + loop_id: loop_expr.hir_id, + after_loop: false, + used_iter: false, + }; + v.visit_expr(&cx.tcx.hir().body(cx.enclosing_body.unwrap()).value); + v.used_iter + } +} diff --git a/src/tools/clippy/clippy_lints/src/macro_use.rs b/src/tools/clippy/clippy_lints/src/macro_use.rs new file mode 100644 index 000000000..d573a1b4f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/macro_use.rs @@ -0,0 +1,221 @@ +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::source::snippet; +use hir::def::{DefKind, Res}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{edition::Edition, sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `#[macro_use] use...`. + /// + /// ### Why is this bad? + /// Since the Rust 2018 edition you can import + /// macro's directly, this is considered idiomatic. + /// + /// ### Example + /// ```rust,ignore + /// #[macro_use] + /// use some_macro; + /// ``` + #[clippy::version = "1.44.0"] + pub MACRO_USE_IMPORTS, + pedantic, + "#[macro_use] is no longer needed" +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct PathAndSpan { + path: String, + span: Span, +} + +/// `MacroRefData` includes the name of the macro. +#[derive(Debug, Clone)] +pub struct MacroRefData { + name: String, +} + +impl MacroRefData { + pub fn new(name: String) -> Self { + Self { name } + } +} + +#[derive(Default)] +#[expect(clippy::module_name_repetitions)] +pub struct MacroUseImports { + /// the actual import path used and the span of the attribute above it. The value is + /// the location, where the lint should be emitted. + imports: Vec<(String, Span, hir::HirId)>, + /// the span of the macro reference, kept to ensure only one reference is used per macro call. + collected: FxHashSet<Span>, + mac_refs: Vec<MacroRefData>, +} + +impl_lint_pass!(MacroUseImports => [MACRO_USE_IMPORTS]); + +impl MacroUseImports { + fn push_unique_macro(&mut self, cx: &LateContext<'_>, span: Span) { + let call_site = span.source_callsite(); + let name = snippet(cx, cx.sess().source_map().span_until_char(call_site, '!'), "_"); + if span.source_callee().is_some() && !self.collected.contains(&call_site) { + let name = if name.contains("::") { + name.split("::").last().unwrap().to_string() + } else { + name.to_string() + }; + + self.mac_refs.push(MacroRefData::new(name)); + self.collected.insert(call_site); + } + } + + fn push_unique_macro_pat_ty(&mut self, cx: &LateContext<'_>, span: Span) { + let call_site = span.source_callsite(); + let name = snippet(cx, cx.sess().source_map().span_until_char(call_site, '!'), "_"); + if span.source_callee().is_some() && !self.collected.contains(&call_site) { + self.mac_refs.push(MacroRefData::new(name.to_string())); + self.collected.insert(call_site); + } + } +} + +impl<'tcx> LateLintPass<'tcx> for MacroUseImports { + fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) { + if_chain! { + if cx.sess().opts.edition >= Edition::Edition2018; + if let hir::ItemKind::Use(path, _kind) = &item.kind; + let hir_id = item.hir_id(); + let attrs = cx.tcx.hir().attrs(hir_id); + if let Some(mac_attr) = attrs.iter().find(|attr| attr.has_name(sym::macro_use)); + if let Res::Def(DefKind::Mod, id) = path.res; + if !id.is_local(); + then { + for kid in cx.tcx.module_children(id).iter() { + 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); + self.imports.push((def_path, span, hir_id)); + } + } + } else { + if item.span.from_expansion() { + self.push_unique_macro_pat_ty(cx, item.span); + } + } + } + } + fn check_attribute(&mut self, cx: &LateContext<'_>, attr: &ast::Attribute) { + if attr.span.from_expansion() { + self.push_unique_macro(cx, attr.span); + } + } + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) { + if expr.span.from_expansion() { + self.push_unique_macro(cx, expr.span); + } + } + fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &hir::Stmt<'_>) { + if stmt.span.from_expansion() { + self.push_unique_macro(cx, stmt.span); + } + } + fn check_pat(&mut self, cx: &LateContext<'_>, pat: &hir::Pat<'_>) { + if pat.span.from_expansion() { + self.push_unique_macro_pat_ty(cx, pat.span); + } + } + fn check_ty(&mut self, cx: &LateContext<'_>, ty: &hir::Ty<'_>) { + if ty.span.from_expansion() { + self.push_unique_macro_pat_ty(cx, ty.span); + } + } + fn check_crate_post(&mut self, cx: &LateContext<'_>) { + let mut used = FxHashMap::default(); + let mut check_dup = vec![]; + for (import, span, hir_id) in &self.imports { + let found_idx = self.mac_refs.iter().position(|mac| import.ends_with(&mac.name)); + + if let Some(idx) = found_idx { + self.mac_refs.remove(idx); + let seg = import.split("::").collect::<Vec<_>>(); + + match seg.as_slice() { + // an empty path is impossible + // a path should always consist of 2 or more segments + [] | [_] => return, + [root, item] => { + if !check_dup.contains(&(*item).to_string()) { + used.entry(((*root).to_string(), span, hir_id)) + .or_insert_with(Vec::new) + .push((*item).to_string()); + check_dup.push((*item).to_string()); + } + }, + [root, rest @ ..] => { + if rest.iter().all(|item| !check_dup.contains(&(*item).to_string())) { + let filtered = rest + .iter() + .filter_map(|item| { + if check_dup.contains(&(*item).to_string()) { + None + } else { + Some((*item).to_string()) + } + }) + .collect::<Vec<_>>(); + used.entry(((*root).to_string(), span, hir_id)) + .or_insert_with(Vec::new) + .push(filtered.join("::")); + check_dup.extend(filtered); + } else { + let rest = rest.to_vec(); + used.entry(((*root).to_string(), span, hir_id)) + .or_insert_with(Vec::new) + .push(rest.join("::")); + check_dup.extend(rest.iter().map(ToString::to_string)); + } + }, + } + } + } + + let mut suggestions = vec![]; + for ((root, span, hir_id), path) in used { + if path.len() == 1 { + suggestions.push((span, format!("{}::{}", root, path[0]), hir_id)); + } else { + suggestions.push((span, format!("{}::{{{}}}", root, path.join(", ")), hir_id)); + } + } + + // If mac_refs is not empty we have encountered an import we could not handle + // such as `std::prelude::v1::foo` or some other macro that expands to an import. + if self.mac_refs.is_empty() { + for (span, import, hir_id) in suggestions { + let help = format!("use {};", import); + span_lint_hir_and_then( + cx, + MACRO_USE_IMPORTS, + *hir_id, + *span, + "`macro_use` attributes are no longer needed in the Rust 2018 edition", + |diag| { + diag.span_suggestion( + *span, + "remove the attribute and import the macro directly, try", + help, + Applicability::MaybeIncorrect, + ); + }, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/main_recursion.rs b/src/tools/clippy/clippy_lints/src/main_recursion.rs new file mode 100644 index 000000000..20333c150 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/main_recursion.rs @@ -0,0 +1,63 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet; +use clippy_utils::{is_entrypoint_fn, is_no_std_crate}; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for recursion using the entrypoint. + /// + /// ### Why is this bad? + /// Apart from special setups (which we could detect following attributes like #![no_std]), + /// recursing into main() seems like an unintuitive anti-pattern we should be able to detect. + /// + /// ### Example + /// ```no_run + /// fn main() { + /// main(); + /// } + /// ``` + #[clippy::version = "1.38.0"] + pub MAIN_RECURSION, + style, + "recursion using the entrypoint" +} + +#[derive(Default)] +pub struct MainRecursion { + has_no_std_attr: bool, +} + +impl_lint_pass!(MainRecursion => [MAIN_RECURSION]); + +impl LateLintPass<'_> for MainRecursion { + fn check_crate(&mut self, cx: &LateContext<'_>) { + self.has_no_std_attr = is_no_std_crate(cx); + } + + fn check_expr_post(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if self.has_no_std_attr { + return; + } + + if_chain! { + if let ExprKind::Call(func, _) = &expr.kind; + if let ExprKind::Path(QPath::Resolved(_, path)) = &func.kind; + if let Some(def_id) = path.res.opt_def_id(); + if is_entrypoint_fn(cx, def_id); + then { + span_lint_and_help( + cx, + MAIN_RECURSION, + func.span, + &format!("recursing into entrypoint `{}`", snippet(cx, func.span, "main")), + None, + "consider using another function for this recursion" + ) + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/manual_assert.rs b/src/tools/clippy/clippy_lints/src/manual_assert.rs new file mode 100644 index 000000000..26b53ab5d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_assert.rs @@ -0,0 +1,71 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::{root_macro_call, FormatArgsExpn}; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{peel_blocks_with_stmt, sugg}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Detects `if`-then-`panic!` that can be replaced with `assert!`. + /// + /// ### Why is this bad? + /// `assert!` is simpler than `if`-then-`panic!`. + /// + /// ### Example + /// ```rust + /// let sad_people: Vec<&str> = vec![]; + /// if !sad_people.is_empty() { + /// panic!("there are sad people: {:?}", sad_people); + /// } + /// ``` + /// Use instead: + /// ```rust + /// let sad_people: Vec<&str> = vec![]; + /// assert!(sad_people.is_empty(), "there are sad people: {:?}", sad_people); + /// ``` + #[clippy::version = "1.57.0"] + pub MANUAL_ASSERT, + pedantic, + "`panic!` and only a `panic!` in `if`-then statement" +} + +declare_lint_pass!(ManualAssert => [MANUAL_ASSERT]); + +impl<'tcx> LateLintPass<'tcx> for ManualAssert { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + if_chain! { + if let ExprKind::If(cond, then, None) = expr.kind; + if !matches!(cond.kind, ExprKind::Let(_)); + if !expr.span.from_expansion(); + let then = peel_blocks_with_stmt(then); + if let Some(macro_call) = root_macro_call(then.span); + if cx.tcx.item_name(macro_call.def_id) == sym::panic; + if !cx.tcx.sess.source_map().is_multiline(cond.span); + if let Some(format_args) = FormatArgsExpn::find_nested(cx, then, macro_call.expn); + then { + let mut applicability = Applicability::MachineApplicable; + let format_args_snip = snippet_with_applicability(cx, format_args.inputs_span(), "..", &mut applicability); + let cond = cond.peel_drop_temps(); + let (cond, not) = match cond.kind { + ExprKind::Unary(UnOp::Not, e) => (e, ""), + _ => (cond, "!"), + }; + let cond_sugg = sugg::Sugg::hir_with_applicability(cx, cond, "..", &mut applicability).maybe_par(); + let sugg = format!("assert!({not}{cond_sugg}, {format_args_snip});"); + span_lint_and_sugg( + cx, + MANUAL_ASSERT, + expr.span, + "only a `panic!` in `if`-then statement", + "try", + sugg, + Applicability::MachineApplicable, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/manual_async_fn.rs b/src/tools/clippy/clippy_lints/src/manual_async_fn.rs new file mode 100644 index 000000000..a0ca7e6ff --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_async_fn.rs @@ -0,0 +1,202 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::match_function_call; +use clippy_utils::paths::FUTURE_FROM_GENERATOR; +use clippy_utils::source::{position_before_rarrow, snippet_block, snippet_opt}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{ + AsyncGeneratorKind, Block, Body, Closure, Expr, ExprKind, FnDecl, FnRetTy, GeneratorKind, GenericArg, GenericBound, + HirId, IsAsync, ItemKind, LifetimeName, Term, TraitRef, Ty, TyKind, TypeBindingKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// It checks for manual implementations of `async` functions. + /// + /// ### Why is this bad? + /// It's more idiomatic to use the dedicated syntax. + /// + /// ### Example + /// ```rust + /// use std::future::Future; + /// + /// fn foo() -> impl Future<Output = i32> { async { 42 } } + /// ``` + /// Use instead: + /// ```rust + /// async fn foo() -> i32 { 42 } + /// ``` + #[clippy::version = "1.45.0"] + pub MANUAL_ASYNC_FN, + style, + "manual implementations of `async` functions can be simplified using the dedicated syntax" +} + +declare_lint_pass!(ManualAsyncFn => [MANUAL_ASYNC_FN]); + +impl<'tcx> LateLintPass<'tcx> for ManualAsyncFn { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + span: Span, + _: HirId, + ) { + if_chain! { + if let Some(header) = kind.header(); + if header.asyncness == IsAsync::NotAsync; + // Check that this function returns `impl Future` + if let FnRetTy::Return(ret_ty) = decl.output; + if let Some((trait_ref, output_lifetimes)) = future_trait_ref(cx, ret_ty); + if let Some(output) = future_output_ty(trait_ref); + if captures_all_lifetimes(decl.inputs, &output_lifetimes); + // Check that the body of the function consists of one async block + if let ExprKind::Block(block, _) = body.value.kind; + if block.stmts.is_empty(); + if let Some(closure_body) = desugared_async_block(cx, block); + then { + let header_span = span.with_hi(ret_ty.span.hi()); + + span_lint_and_then( + cx, + MANUAL_ASYNC_FN, + header_span, + "this function can be simplified using the `async fn` syntax", + |diag| { + if_chain! { + if let Some(header_snip) = snippet_opt(cx, header_span); + if let Some(ret_pos) = position_before_rarrow(&header_snip); + if let Some((ret_sugg, ret_snip)) = suggested_ret(cx, output); + then { + let help = format!("make the function `async` and {}", ret_sugg); + diag.span_suggestion( + header_span, + &help, + format!("async {}{}", &header_snip[..ret_pos], ret_snip), + Applicability::MachineApplicable + ); + + let body_snip = snippet_block(cx, closure_body.value.span, "..", Some(block.span)); + diag.span_suggestion( + block.span, + "move the body of the async block to the enclosing function", + body_snip, + Applicability::MachineApplicable + ); + } + } + }, + ); + } + } + } +} + +fn future_trait_ref<'tcx>( + cx: &LateContext<'tcx>, + ty: &'tcx Ty<'tcx>, +) -> Option<(&'tcx TraitRef<'tcx>, Vec<LifetimeName>)> { + if_chain! { + if let TyKind::OpaqueDef(item_id, bounds) = ty.kind; + let item = cx.tcx.hir().item(item_id); + if let ItemKind::OpaqueTy(opaque) = &item.kind; + if let Some(trait_ref) = opaque.bounds.iter().find_map(|bound| { + if let GenericBound::Trait(poly, _) = bound { + Some(&poly.trait_ref) + } else { + None + } + }); + if trait_ref.trait_def_id() == cx.tcx.lang_items().future_trait(); + then { + let output_lifetimes = bounds + .iter() + .filter_map(|bound| { + if let GenericArg::Lifetime(lt) = bound { + Some(lt.name) + } else { + None + } + }) + .collect(); + + return Some((trait_ref, output_lifetimes)); + } + } + + None +} + +fn future_output_ty<'tcx>(trait_ref: &'tcx TraitRef<'tcx>) -> Option<&'tcx Ty<'tcx>> { + if_chain! { + if let Some(segment) = trait_ref.path.segments.last(); + if let Some(args) = segment.args; + if args.bindings.len() == 1; + let binding = &args.bindings[0]; + if binding.ident.name == sym::Output; + if let TypeBindingKind::Equality{term: Term::Ty(output)} = binding.kind; + then { + return Some(output) + } + } + + None +} + +fn captures_all_lifetimes(inputs: &[Ty<'_>], output_lifetimes: &[LifetimeName]) -> bool { + let input_lifetimes: Vec<LifetimeName> = inputs + .iter() + .filter_map(|ty| { + if let TyKind::Rptr(lt, _) = ty.kind { + Some(lt.name) + } else { + None + } + }) + .collect(); + + // The lint should trigger in one of these cases: + // - There are no input lifetimes + // - There's only one output lifetime bound using `+ '_` + // - All input lifetimes are explicitly bound to the output + input_lifetimes.is_empty() + || (output_lifetimes.len() == 1 && matches!(output_lifetimes[0], LifetimeName::Infer)) + || input_lifetimes + .iter() + .all(|in_lt| output_lifetimes.iter().any(|out_lt| in_lt == out_lt)) +} + +fn desugared_async_block<'tcx>(cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) -> Option<&'tcx Body<'tcx>> { + if_chain! { + if let Some(block_expr) = block.expr; + if let Some(args) = match_function_call(cx, block_expr, &FUTURE_FROM_GENERATOR); + if args.len() == 1; + if let Expr{kind: ExprKind::Closure(&Closure { body, .. }), ..} = args[0]; + let closure_body = cx.tcx.hir().body(body); + if closure_body.generator_kind == Some(GeneratorKind::Async(AsyncGeneratorKind::Block)); + then { + return Some(closure_body); + } + } + + None +} + +fn suggested_ret(cx: &LateContext<'_>, output: &Ty<'_>) -> Option<(&'static str, String)> { + match output.kind { + TyKind::Tup(tys) if tys.is_empty() => { + let sugg = "remove the return type"; + Some((sugg, "".into())) + }, + _ => { + let sugg = "return the output of the future directly"; + snippet_opt(cx, output.span).map(|snip| (sugg, format!(" -> {}", snip))) + }, + } +} diff --git a/src/tools/clippy/clippy_lints/src/manual_bits.rs b/src/tools/clippy/clippy_lints/src/manual_bits.rs new file mode 100644 index 000000000..60bbcde4f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_bits.rs @@ -0,0 +1,146 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{get_parent_expr, meets_msrv, msrvs}; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, GenericArg, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, Ty}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for uses of `std::mem::size_of::<T>() * 8` when + /// `T::BITS` is available. + /// + /// ### Why is this bad? + /// Can be written as the shorter `T::BITS`. + /// + /// ### Example + /// ```rust + /// std::mem::size_of::<usize>() * 8; + /// ``` + /// Use instead: + /// ```rust + /// usize::BITS as usize; + /// ``` + #[clippy::version = "1.60.0"] + pub MANUAL_BITS, + style, + "manual implementation of `size_of::<T>() * 8` can be simplified with `T::BITS`" +} + +#[derive(Clone)] +pub struct ManualBits { + msrv: Option<RustcVersion>, +} + +impl ManualBits { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(ManualBits => [MANUAL_BITS]); + +impl<'tcx> LateLintPass<'tcx> for ManualBits { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if !meets_msrv(self.msrv, msrvs::MANUAL_BITS) { + return; + } + + if_chain! { + if let ExprKind::Binary(bin_op, left_expr, right_expr) = expr.kind; + if let BinOpKind::Mul = &bin_op.node; + if let Some((real_ty, resolved_ty, other_expr)) = get_one_size_of_ty(cx, left_expr, right_expr); + if matches!(resolved_ty.kind(), ty::Int(_) | ty::Uint(_)); + if let ExprKind::Lit(lit) = &other_expr.kind; + if let LitKind::Int(8, _) = lit.node; + then { + let mut app = Applicability::MachineApplicable; + let ty_snip = snippet_with_applicability(cx, real_ty.span, "..", &mut app); + let sugg = create_sugg(cx, expr, format!("{ty_snip}::BITS")); + + span_lint_and_sugg( + cx, + MANUAL_BITS, + expr.span, + "usage of `mem::size_of::<T>()` to obtain the size of `T` in bits", + "consider using", + sugg, + app, + ); + } + } + } + + extract_msrv_attr!(LateContext); +} + +fn get_one_size_of_ty<'tcx>( + cx: &LateContext<'tcx>, + expr1: &'tcx Expr<'_>, + expr2: &'tcx Expr<'_>, +) -> Option<(&'tcx rustc_hir::Ty<'tcx>, Ty<'tcx>, &'tcx Expr<'tcx>)> { + match (get_size_of_ty(cx, expr1), get_size_of_ty(cx, expr2)) { + (Some((real_ty, resolved_ty)), None) => Some((real_ty, resolved_ty, expr2)), + (None, Some((real_ty, resolved_ty))) => Some((real_ty, resolved_ty, expr1)), + _ => None, + } +} + +fn get_size_of_ty<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<(&'tcx rustc_hir::Ty<'tcx>, Ty<'tcx>)> { + if_chain! { + if let ExprKind::Call(count_func, _func_args) = expr.kind; + if let ExprKind::Path(ref count_func_qpath) = count_func.kind; + + if let QPath::Resolved(_, count_func_path) = count_func_qpath; + if let Some(segment_zero) = count_func_path.segments.get(0); + if let Some(args) = segment_zero.args; + if let Some(GenericArg::Type(real_ty)) = args.args.get(0); + + if let Some(def_id) = cx.qpath_res(count_func_qpath, count_func.hir_id).opt_def_id(); + if cx.tcx.is_diagnostic_item(sym::mem_size_of, def_id); + then { + cx.typeck_results().node_substs(count_func.hir_id).types().next().map(|resolved_ty| (real_ty, resolved_ty)) + } else { + None + } + } +} + +fn create_sugg(cx: &LateContext<'_>, expr: &Expr<'_>, base_sugg: String) -> String { + if let Some(parent_expr) = get_parent_expr(cx, expr) { + if is_ty_conversion(parent_expr) { + return base_sugg; + } + + // These expressions have precedence over casts, the suggestion therefore + // needs to be wrapped into parentheses + match parent_expr.kind { + ExprKind::Unary(..) | ExprKind::AddrOf(..) | ExprKind::MethodCall(..) => { + return format!("({base_sugg} as usize)"); + }, + _ => {}, + } + } + + format!("{base_sugg} as usize") +} + +fn is_ty_conversion(expr: &Expr<'_>) -> bool { + if let ExprKind::Cast(..) = expr.kind { + true + } else if let ExprKind::MethodCall(path, [_], _) = expr.kind + && path.ident.name == rustc_span::sym::try_into + { + // This is only called for `usize` which implements `TryInto`. Therefore, + // we don't have to check here if `self` implements the `TryInto` trait. + true + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/manual_non_exhaustive.rs b/src/tools/clippy/clippy_lints/src/manual_non_exhaustive.rs new file mode 100644 index 000000000..2b04475c7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_non_exhaustive.rs @@ -0,0 +1,221 @@ +use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then}; +use clippy_utils::source::snippet_opt; +use clippy_utils::{is_doc_hidden, meets_msrv, msrvs}; +use rustc_ast::ast::{self, VisibilityKind}; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res}; +use rustc_hir::{self as hir, Expr, ExprKind, QPath}; +use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext}; +use rustc_middle::ty::DefIdTree; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::def_id::{DefId, LocalDefId}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for manual implementations of the non-exhaustive pattern. + /// + /// ### Why is this bad? + /// Using the #[non_exhaustive] attribute expresses better the intent + /// and allows possible optimizations when applied to enums. + /// + /// ### Example + /// ```rust + /// struct S { + /// pub a: i32, + /// pub b: i32, + /// _c: (), + /// } + /// + /// enum E { + /// A, + /// B, + /// #[doc(hidden)] + /// _C, + /// } + /// + /// struct T(pub i32, pub i32, ()); + /// ``` + /// Use instead: + /// ```rust + /// #[non_exhaustive] + /// struct S { + /// pub a: i32, + /// pub b: i32, + /// } + /// + /// #[non_exhaustive] + /// enum E { + /// A, + /// B, + /// } + /// + /// #[non_exhaustive] + /// struct T(pub i32, pub i32); + /// ``` + #[clippy::version = "1.45.0"] + pub MANUAL_NON_EXHAUSTIVE, + style, + "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]" +} + +#[expect(clippy::module_name_repetitions)] +pub struct ManualNonExhaustiveStruct { + msrv: Option<RustcVersion>, +} + +impl ManualNonExhaustiveStruct { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(ManualNonExhaustiveStruct => [MANUAL_NON_EXHAUSTIVE]); + +#[expect(clippy::module_name_repetitions)] +pub struct ManualNonExhaustiveEnum { + msrv: Option<RustcVersion>, + constructed_enum_variants: FxHashSet<(DefId, DefId)>, + potential_enums: Vec<(LocalDefId, LocalDefId, Span, Span)>, +} + +impl ManualNonExhaustiveEnum { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { + msrv, + constructed_enum_variants: FxHashSet::default(), + potential_enums: Vec::new(), + } + } +} + +impl_lint_pass!(ManualNonExhaustiveEnum => [MANUAL_NON_EXHAUSTIVE]); + +impl EarlyLintPass for ManualNonExhaustiveStruct { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { + if !meets_msrv(self.msrv, msrvs::NON_EXHAUSTIVE) { + return; + } + + if let ast::ItemKind::Struct(variant_data, _) = &item.kind { + let (fields, delimiter) = match variant_data { + ast::VariantData::Struct(fields, _) => (&**fields, '{'), + ast::VariantData::Tuple(fields, _) => (&**fields, '('), + ast::VariantData::Unit(_) => return, + }; + if fields.len() <= 1 { + return; + } + let mut iter = fields.iter().filter_map(|f| match f.vis.kind { + VisibilityKind::Public => None, + VisibilityKind::Inherited => Some(Ok(f)), + VisibilityKind::Restricted { .. } => Some(Err(())), + }); + if let Some(Ok(field)) = iter.next() + && iter.next().is_none() + && field.ty.kind.is_unit() + && field.ident.map_or(true, |name| name.as_str().starts_with('_')) + { + span_lint_and_then( + cx, + MANUAL_NON_EXHAUSTIVE, + item.span, + "this seems like a manual implementation of the non-exhaustive pattern", + |diag| { + if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive)) + && let header_span = cx.sess().source_map().span_until_char(item.span, delimiter) + && let Some(snippet) = snippet_opt(cx, header_span) + { + diag.span_suggestion( + header_span, + "add the attribute", + format!("#[non_exhaustive] {}", snippet), + Applicability::Unspecified, + ); + } + diag.span_help(field.span, "remove this field"); + } + ); + } + } + } + + extract_msrv_attr!(EarlyContext); +} + +impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustiveEnum { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + if !meets_msrv(self.msrv, msrvs::NON_EXHAUSTIVE) { + return; + } + + if let hir::ItemKind::Enum(def, _) = &item.kind + && def.variants.len() > 1 + { + let mut iter = def.variants.iter().filter_map(|v| { + let id = cx.tcx.hir().local_def_id(v.id); + (matches!(v.data, hir::VariantData::Unit(_)) + && v.ident.as_str().starts_with('_') + && is_doc_hidden(cx.tcx.hir().attrs(v.id))) + .then_some((id, v.span)) + }); + if let Some((id, span)) = iter.next() + && iter.next().is_none() + { + self.potential_enums.push((item.def_id, id, item.span, span)); + } + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if let ExprKind::Path(QPath::Resolved(None, p)) = &e.kind + && let [.., name] = p.segments + && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = p.res + && name.ident.as_str().starts_with('_') + { + let variant_id = cx.tcx.parent(id); + let enum_id = cx.tcx.parent(variant_id); + + self.constructed_enum_variants.insert((enum_id, variant_id)); + } + } + + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + for &(enum_id, _, enum_span, variant_span) in + self.potential_enums.iter().filter(|&&(enum_id, variant_id, _, _)| { + !self + .constructed_enum_variants + .contains(&(enum_id.to_def_id(), variant_id.to_def_id())) + }) + { + let hir_id = cx.tcx.hir().local_def_id_to_hir_id(enum_id); + span_lint_hir_and_then( + cx, + MANUAL_NON_EXHAUSTIVE, + hir_id, + enum_span, + "this seems like a manual implementation of the non-exhaustive pattern", + |diag| { + if !cx.tcx.adt_def(enum_id).is_variant_list_non_exhaustive() + && let header_span = cx.sess().source_map().span_until_char(enum_span, '{') + && let Some(snippet) = snippet_opt(cx, header_span) + { + diag.span_suggestion( + header_span, + "add the attribute", + format!("#[non_exhaustive] {}", snippet), + Applicability::Unspecified, + ); + } + diag.span_help(variant_span, "remove this variant"); + }, + ); + } + } + + extract_msrv_attr!(LateContext); +} diff --git a/src/tools/clippy/clippy_lints/src/manual_ok_or.rs b/src/tools/clippy/clippy_lints/src/manual_ok_or.rs new file mode 100644 index 000000000..9abf2507b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_ok_or.rs @@ -0,0 +1,98 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{indent_of, reindent_multiline, snippet_opt}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{is_lang_ctor, path_to_local_id}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{ResultErr, ResultOk}; +use rustc_hir::{Closure, Expr, ExprKind, PatKind}; +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}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// + /// Finds patterns that reimplement `Option::ok_or`. + /// + /// ### Why is this bad? + /// + /// Concise code helps focusing on behavior instead of boilerplate. + /// + /// ### Examples + /// ```rust + /// let foo: Option<i32> = None; + /// foo.map_or(Err("error"), |v| Ok(v)); + /// ``` + /// + /// Use instead: + /// ```rust + /// let foo: Option<i32> = None; + /// foo.ok_or("error"); + /// ``` + #[clippy::version = "1.49.0"] + pub MANUAL_OK_OR, + pedantic, + "finds patterns that can be encoded more concisely with `Option::ok_or`" +} + +declare_lint_pass!(ManualOkOr => [MANUAL_OK_OR]); + +impl<'tcx> LateLintPass<'tcx> for ManualOkOr { + fn check_expr(&mut self, cx: &LateContext<'tcx>, scrutinee: &'tcx Expr<'tcx>) { + if in_external_macro(cx.sess(), scrutinee.span) { + return; + } + + if_chain! { + if let ExprKind::MethodCall(method_segment, args, _) = scrutinee.kind; + if method_segment.ident.name == sym!(map_or); + if args.len() == 3; + let method_receiver = &args[0]; + let ty = cx.typeck_results().expr_ty(method_receiver); + if is_type_diagnostic_item(cx, ty, sym::Option); + let or_expr = &args[1]; + if is_ok_wrapping(cx, &args[2]); + if let ExprKind::Call(Expr { kind: ExprKind::Path(err_path), .. }, &[ref err_arg]) = or_expr.kind; + if is_lang_ctor(cx, err_path, ResultErr); + if let Some(method_receiver_snippet) = snippet_opt(cx, method_receiver.span); + if let Some(err_arg_snippet) = snippet_opt(cx, err_arg.span); + if let Some(indent) = indent_of(cx, scrutinee.span); + then { + let reindented_err_arg_snippet = + reindent_multiline(err_arg_snippet.into(), true, Some(indent + 4)); + span_lint_and_sugg( + cx, + MANUAL_OK_OR, + scrutinee.span, + "this pattern reimplements `Option::ok_or`", + "replace with", + format!( + "{}.ok_or({})", + method_receiver_snippet, + reindented_err_arg_snippet + ), + Applicability::MachineApplicable, + ); + } + } + } +} + +fn is_ok_wrapping(cx: &LateContext<'_>, map_expr: &Expr<'_>) -> bool { + if let ExprKind::Path(ref qpath) = map_expr.kind { + if is_lang_ctor(cx, qpath, ResultOk) { + return true; + } + } + if_chain! { + if let ExprKind::Closure(&Closure { body, .. }) = map_expr.kind; + let body = cx.tcx.hir().body(body); + if let PatKind::Binding(_, param_id, ..) = body.params[0].pat.kind; + if let ExprKind::Call(Expr { kind: ExprKind::Path(ok_path), .. }, &[ref ok_arg]) = body.value.kind; + if is_lang_ctor(cx, ok_path, ResultOk); + then { path_to_local_id(ok_arg, param_id) } else { false } + } +} diff --git a/src/tools/clippy/clippy_lints/src/manual_rem_euclid.rs b/src/tools/clippy/clippy_lints/src/manual_rem_euclid.rs new file mode 100644 index 000000000..95cc6bdbd --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_rem_euclid.rs @@ -0,0 +1,123 @@ +use clippy_utils::consts::{constant_full_int, FullInt}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{in_constant, meets_msrv, msrvs, path_to_local}; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, Node, TyKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for an expression like `((x % 4) + 4) % 4` which is a common manual reimplementation + /// of `x.rem_euclid(4)`. + /// + /// ### Why is this bad? + /// It's simpler and more readable. + /// + /// ### Example + /// ```rust + /// let x: i32 = 24; + /// let rem = ((x % 4) + 4) % 4; + /// ``` + /// Use instead: + /// ```rust + /// let x: i32 = 24; + /// let rem = x.rem_euclid(4); + /// ``` + #[clippy::version = "1.63.0"] + pub MANUAL_REM_EUCLID, + complexity, + "manually reimplementing `rem_euclid`" +} + +pub struct ManualRemEuclid { + msrv: Option<RustcVersion>, +} + +impl ManualRemEuclid { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(ManualRemEuclid => [MANUAL_REM_EUCLID]); + +impl<'tcx> LateLintPass<'tcx> for ManualRemEuclid { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if !meets_msrv(self.msrv, msrvs::REM_EUCLID) { + return; + } + + if in_constant(cx, expr.hir_id) && !meets_msrv(self.msrv, msrvs::REM_EUCLID_CONST) { + return; + } + + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if let ExprKind::Binary(op1, expr1, right) = expr.kind + && op1.node == BinOpKind::Rem + && let Some(const1) = check_for_unsigned_int_constant(cx, right) + && let ExprKind::Binary(op2, left, right) = expr1.kind + && op2.node == BinOpKind::Add + && let Some((const2, expr2)) = check_for_either_unsigned_int_constant(cx, left, right) + && let ExprKind::Binary(op3, expr3, right) = expr2.kind + && op3.node == BinOpKind::Rem + && let Some(const3) = check_for_unsigned_int_constant(cx, right) + // Also ensures the const is nonzero since zero can't be a divisor + && const1 == const2 && const2 == const3 + && let Some(hir_id) = path_to_local(expr3) + && let Some(Node::Pat(_)) = cx.tcx.hir().find(hir_id) { + // Apply only to params or locals with annotated types + match cx.tcx.hir().find(cx.tcx.hir().get_parent_node(hir_id)) { + Some(Node::Param(..)) => (), + Some(Node::Local(local)) => { + let Some(ty) = local.ty else { return }; + if matches!(ty.kind, TyKind::Infer) { + return; + } + } + _ => return, + }; + + let mut app = Applicability::MachineApplicable; + let rem_of = snippet_with_applicability(cx, expr3.span, "_", &mut app); + span_lint_and_sugg( + cx, + MANUAL_REM_EUCLID, + expr.span, + "manual `rem_euclid` implementation", + "consider using", + format!("{rem_of}.rem_euclid({const1})"), + app, + ); + } + } + + extract_msrv_attr!(LateContext); +} + +// Checks if either the left or right expressions can be an unsigned int constant and returns that +// constant along with the other expression unchanged if so +fn check_for_either_unsigned_int_constant<'a>( + cx: &'a LateContext<'_>, + left: &'a Expr<'_>, + right: &'a Expr<'_>, +) -> Option<(u128, &'a Expr<'a>)> { + check_for_unsigned_int_constant(cx, left) + .map(|int_const| (int_const, right)) + .or_else(|| check_for_unsigned_int_constant(cx, right).map(|int_const| (int_const, left))) +} + +fn check_for_unsigned_int_constant<'a>(cx: &'a LateContext<'_>, expr: &'a Expr<'_>) -> Option<u128> { + let Some(int_const) = constant_full_int(cx, cx.typeck_results(), expr) else { return None }; + match int_const { + FullInt::S(s) => s.try_into().ok(), + FullInt::U(u) => Some(u), + } +} diff --git a/src/tools/clippy/clippy_lints/src/manual_retain.rs b/src/tools/clippy/clippy_lints/src/manual_retain.rs new file mode 100644 index 000000000..42d2577cc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_retain.rs @@ -0,0 +1,228 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{get_parent_expr, match_def_path, paths, SpanlessEq}; +use clippy_utils::{meets_msrv, msrvs}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::def_id::DefId; +use rustc_hir::ExprKind::Assign; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::sym; + +const ACCEPTABLE_METHODS: [&[&str]; 4] = [ + &paths::HASHSET_ITER, + &paths::BTREESET_ITER, + &paths::SLICE_INTO, + &paths::VEC_DEQUE_ITER, +]; +const ACCEPTABLE_TYPES: [(rustc_span::Symbol, Option<RustcVersion>); 6] = [ + (sym::BTreeSet, Some(msrvs::BTREE_SET_RETAIN)), + (sym::BTreeMap, Some(msrvs::BTREE_MAP_RETAIN)), + (sym::HashSet, Some(msrvs::HASH_SET_RETAIN)), + (sym::HashMap, Some(msrvs::HASH_MAP_RETAIN)), + (sym::Vec, None), + (sym::VecDeque, None), +]; + +declare_clippy_lint! { + /// ### What it does + /// Checks for code to be replaced by `.retain()`. + /// ### Why is this bad? + /// `.retain()` is simpler and avoids needless allocation. + /// ### Example + /// ```rust + /// let mut vec = vec![0, 1, 2]; + /// vec = vec.iter().filter(|&x| x % 2 == 0).copied().collect(); + /// vec = vec.into_iter().filter(|x| x % 2 == 0).collect(); + /// ``` + /// Use instead: + /// ```rust + /// let mut vec = vec![0, 1, 2]; + /// vec.retain(|x| x % 2 == 0); + /// ``` + #[clippy::version = "1.63.0"] + pub MANUAL_RETAIN, + perf, + "`retain()` is simpler and the same functionalitys" +} + +pub struct ManualRetain { + msrv: Option<RustcVersion>, +} + +impl ManualRetain { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(ManualRetain => [MANUAL_RETAIN]); + +impl<'tcx> LateLintPass<'tcx> for ManualRetain { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if let Some(parent_expr) = get_parent_expr(cx, expr) + && let Assign(left_expr, collect_expr, _) = &parent_expr.kind + && let hir::ExprKind::MethodCall(seg, _, _) = &collect_expr.kind + && seg.args.is_none() + && let hir::ExprKind::MethodCall(_, [target_expr], _) = &collect_expr.kind + && let Some(collect_def_id) = cx.typeck_results().type_dependent_def_id(collect_expr.hir_id) + && match_def_path(cx, collect_def_id, &paths::CORE_ITER_COLLECT) { + check_into_iter(cx, parent_expr, left_expr, target_expr, self.msrv); + check_iter(cx, parent_expr, left_expr, target_expr, self.msrv); + check_to_owned(cx, parent_expr, left_expr, target_expr, self.msrv); + } + } + + extract_msrv_attr!(LateContext); +} + +fn check_into_iter( + cx: &LateContext<'_>, + parent_expr: &hir::Expr<'_>, + left_expr: &hir::Expr<'_>, + target_expr: &hir::Expr<'_>, + msrv: Option<RustcVersion>, +) { + if let hir::ExprKind::MethodCall(_, [into_iter_expr, _], _) = &target_expr.kind + && let Some(filter_def_id) = cx.typeck_results().type_dependent_def_id(target_expr.hir_id) + && match_def_path(cx, filter_def_id, &paths::CORE_ITER_FILTER) + && let hir::ExprKind::MethodCall(_, [struct_expr], _) = &into_iter_expr.kind + && let Some(into_iter_def_id) = cx.typeck_results().type_dependent_def_id(into_iter_expr.hir_id) + && match_def_path(cx, into_iter_def_id, &paths::CORE_ITER_INTO_ITER) + && match_acceptable_type(cx, left_expr, msrv) + && SpanlessEq::new(cx).eq_expr(left_expr, struct_expr) { + suggest(cx, parent_expr, left_expr, target_expr); + } +} + +fn check_iter( + cx: &LateContext<'_>, + parent_expr: &hir::Expr<'_>, + left_expr: &hir::Expr<'_>, + target_expr: &hir::Expr<'_>, + msrv: Option<RustcVersion>, +) { + if let hir::ExprKind::MethodCall(_, [filter_expr], _) = &target_expr.kind + && let Some(copied_def_id) = cx.typeck_results().type_dependent_def_id(target_expr.hir_id) + && (match_def_path(cx, copied_def_id, &paths::CORE_ITER_COPIED) + || match_def_path(cx, copied_def_id, &paths::CORE_ITER_CLONED)) + && let hir::ExprKind::MethodCall(_, [iter_expr, _], _) = &filter_expr.kind + && let Some(filter_def_id) = cx.typeck_results().type_dependent_def_id(filter_expr.hir_id) + && match_def_path(cx, filter_def_id, &paths::CORE_ITER_FILTER) + && let hir::ExprKind::MethodCall(_, [struct_expr], _) = &iter_expr.kind + && let Some(iter_expr_def_id) = cx.typeck_results().type_dependent_def_id(iter_expr.hir_id) + && match_acceptable_def_path(cx, iter_expr_def_id) + && match_acceptable_type(cx, left_expr, msrv) + && SpanlessEq::new(cx).eq_expr(left_expr, struct_expr) { + suggest(cx, parent_expr, left_expr, filter_expr); + } +} + +fn check_to_owned( + cx: &LateContext<'_>, + parent_expr: &hir::Expr<'_>, + left_expr: &hir::Expr<'_>, + target_expr: &hir::Expr<'_>, + msrv: Option<RustcVersion>, +) { + if meets_msrv(msrv, msrvs::STRING_RETAIN) + && let hir::ExprKind::MethodCall(_, [filter_expr], _) = &target_expr.kind + && let Some(to_owned_def_id) = cx.typeck_results().type_dependent_def_id(target_expr.hir_id) + && match_def_path(cx, to_owned_def_id, &paths::TO_OWNED_METHOD) + && let hir::ExprKind::MethodCall(_, [chars_expr, _], _) = &filter_expr.kind + && let Some(filter_def_id) = cx.typeck_results().type_dependent_def_id(filter_expr.hir_id) + && match_def_path(cx, filter_def_id, &paths::CORE_ITER_FILTER) + && let hir::ExprKind::MethodCall(_, [str_expr], _) = &chars_expr.kind + && let Some(chars_expr_def_id) = cx.typeck_results().type_dependent_def_id(chars_expr.hir_id) + && match_def_path(cx, chars_expr_def_id, &paths::STR_CHARS) + && let ty = cx.typeck_results().expr_ty(str_expr).peel_refs() + && is_type_diagnostic_item(cx, ty, sym::String) + && SpanlessEq::new(cx).eq_expr(left_expr, str_expr) { + suggest(cx, parent_expr, left_expr, filter_expr); + } +} + +fn suggest(cx: &LateContext<'_>, parent_expr: &hir::Expr<'_>, left_expr: &hir::Expr<'_>, filter_expr: &hir::Expr<'_>) { + if let hir::ExprKind::MethodCall(_, [_, closure], _) = filter_expr.kind + && let hir::ExprKind::Closure(&hir::Closure { body, ..}) = closure.kind + && let filter_body = cx.tcx.hir().body(body) + && let [filter_params] = filter_body.params + && let Some(sugg) = match filter_params.pat.kind { + hir::PatKind::Binding(_, _, filter_param_ident, None) => { + Some(format!("{}.retain(|{}| {})", snippet(cx, left_expr.span, ".."), filter_param_ident, snippet(cx, filter_body.value.span, ".."))) + }, + hir::PatKind::Tuple([key_pat, value_pat], _) => { + make_sugg(cx, key_pat, value_pat, left_expr, filter_body) + }, + hir::PatKind::Ref(pat, _) => { + match pat.kind { + hir::PatKind::Binding(_, _, filter_param_ident, None) => { + Some(format!("{}.retain(|{}| {})", snippet(cx, left_expr.span, ".."), filter_param_ident, snippet(cx, filter_body.value.span, ".."))) + }, + _ => None + } + }, + _ => None + } { + span_lint_and_sugg( + cx, + MANUAL_RETAIN, + parent_expr.span, + "this expression can be written more simply using `.retain()`", + "consider calling `.retain()` instead", + sugg, + Applicability::MachineApplicable + ); + } +} + +fn make_sugg( + cx: &LateContext<'_>, + key_pat: &rustc_hir::Pat<'_>, + value_pat: &rustc_hir::Pat<'_>, + left_expr: &hir::Expr<'_>, + filter_body: &hir::Body<'_>, +) -> Option<String> { + match (&key_pat.kind, &value_pat.kind) { + (hir::PatKind::Binding(_, _, key_param_ident, None), hir::PatKind::Binding(_, _, value_param_ident, None)) => { + Some(format!( + "{}.retain(|{}, &mut {}| {})", + snippet(cx, left_expr.span, ".."), + key_param_ident, + value_param_ident, + snippet(cx, filter_body.value.span, "..") + )) + }, + (hir::PatKind::Binding(_, _, key_param_ident, None), hir::PatKind::Wild) => Some(format!( + "{}.retain(|{}, _| {})", + snippet(cx, left_expr.span, ".."), + key_param_ident, + snippet(cx, filter_body.value.span, "..") + )), + (hir::PatKind::Wild, hir::PatKind::Binding(_, _, value_param_ident, None)) => Some(format!( + "{}.retain(|_, &mut {}| {})", + snippet(cx, left_expr.span, ".."), + value_param_ident, + snippet(cx, filter_body.value.span, "..") + )), + _ => None, + } +} + +fn match_acceptable_def_path(cx: &LateContext<'_>, collect_def_id: DefId) -> bool { + ACCEPTABLE_METHODS + .iter() + .any(|&method| match_def_path(cx, collect_def_id, method)) +} + +fn match_acceptable_type(cx: &LateContext<'_>, expr: &hir::Expr<'_>, msrv: Option<RustcVersion>) -> bool { + let expr_ty = cx.typeck_results().expr_ty(expr).peel_refs(); + ACCEPTABLE_TYPES.iter().any(|(ty, acceptable_msrv)| { + is_type_diagnostic_item(cx, expr_ty, *ty) + && acceptable_msrv.map_or(true, |acceptable_msrv| meets_msrv(msrv, acceptable_msrv)) + }) +} diff --git a/src/tools/clippy/clippy_lints/src/manual_strip.rs b/src/tools/clippy/clippy_lints/src/manual_strip.rs new file mode 100644 index 000000000..dfb3efc4e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/manual_strip.rs @@ -0,0 +1,252 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::source::snippet; +use clippy_utils::usage::mutated_variables; +use clippy_utils::{eq_expr_value, higher, match_def_path, meets_msrv, msrvs, paths}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_hir::def::Res; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::BinOpKind; +use rustc_hir::{BorrowKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Spanned; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing using + /// the pattern's length. + /// + /// ### Why is this bad? + /// Using `str:strip_{prefix,suffix}` is safer and may have better performance as there is no + /// slicing which may panic and the compiler does not need to insert this panic code. It is + /// also sometimes more readable as it removes the need for duplicating or storing the pattern + /// used by `str::{starts,ends}_with` and in the slicing. + /// + /// ### Example + /// ```rust + /// let s = "hello, world!"; + /// if s.starts_with("hello, ") { + /// assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!"); + /// } + /// ``` + /// Use instead: + /// ```rust + /// let s = "hello, world!"; + /// if let Some(end) = s.strip_prefix("hello, ") { + /// assert_eq!(end.to_uppercase(), "WORLD!"); + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub MANUAL_STRIP, + complexity, + "suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing" +} + +pub struct ManualStrip { + msrv: Option<RustcVersion>, +} + +impl ManualStrip { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(ManualStrip => [MANUAL_STRIP]); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum StripKind { + Prefix, + Suffix, +} + +impl<'tcx> LateLintPass<'tcx> for ManualStrip { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if !meets_msrv(self.msrv, msrvs::STR_STRIP_PREFIX) { + return; + } + + if_chain! { + if let Some(higher::If { cond, then, .. }) = higher::If::hir(expr); + if let ExprKind::MethodCall(_, [target_arg, pattern], _) = cond.kind; + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(cond.hir_id); + if let ExprKind::Path(target_path) = &target_arg.kind; + then { + let strip_kind = if match_def_path(cx, method_def_id, &paths::STR_STARTS_WITH) { + StripKind::Prefix + } else if match_def_path(cx, method_def_id, &paths::STR_ENDS_WITH) { + StripKind::Suffix + } else { + return; + }; + let target_res = cx.qpath_res(target_path, target_arg.hir_id); + if target_res == Res::Err { + return; + }; + + if_chain! { + if let Res::Local(hir_id) = target_res; + if let Some(used_mutably) = mutated_variables(then, cx); + if used_mutably.contains(&hir_id); + then { + return; + } + } + + let strippings = find_stripping(cx, strip_kind, target_res, pattern, then); + if !strippings.is_empty() { + + let kind_word = match strip_kind { + StripKind::Prefix => "prefix", + StripKind::Suffix => "suffix", + }; + + let test_span = expr.span.until(then.span); + span_lint_and_then(cx, MANUAL_STRIP, strippings[0], &format!("stripping a {} manually", kind_word), |diag| { + diag.span_note(test_span, &format!("the {} was tested here", kind_word)); + multispan_sugg( + diag, + &format!("try using the `strip_{}` method", kind_word), + vec![(test_span, + format!("if let Some(<stripped>) = {}.strip_{}({}) ", + snippet(cx, target_arg.span, ".."), + kind_word, + snippet(cx, pattern.span, "..")))] + .into_iter().chain(strippings.into_iter().map(|span| (span, "<stripped>".into()))), + ); + }); + } + } + } + } + + extract_msrv_attr!(LateContext); +} + +// Returns `Some(arg)` if `expr` matches `arg.len()` and `None` otherwise. +fn len_arg<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { + if_chain! { + if let ExprKind::MethodCall(_, [arg], _) = expr.kind; + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if match_def_path(cx, method_def_id, &paths::STR_LEN); + then { + Some(arg) + } else { + None + } + } +} + +// Returns the length of the `expr` if it's a constant string or char. +fn constant_length(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> { + let (value, _) = constant(cx, cx.typeck_results(), expr)?; + match value { + Constant::Str(value) => Some(value.len() as u128), + Constant::Char(value) => Some(value.len_utf8() as u128), + _ => None, + } +} + +// Tests if `expr` equals the length of the pattern. +fn eq_pattern_length<'tcx>(cx: &LateContext<'tcx>, pattern: &Expr<'_>, expr: &'tcx Expr<'_>) -> bool { + if let ExprKind::Lit(Spanned { + node: LitKind::Int(n, _), + .. + }) = expr.kind + { + constant_length(cx, pattern).map_or(false, |length| length == n) + } else { + len_arg(cx, expr).map_or(false, |arg| eq_expr_value(cx, pattern, arg)) + } +} + +// Tests if `expr` is a `&str`. +fn is_ref_str(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + match cx.typeck_results().expr_ty_adjusted(expr).kind() { + ty::Ref(_, ty, _) => ty.is_str(), + _ => false, + } +} + +// Removes the outer `AddrOf` expression if needed. +fn peel_ref<'a>(expr: &'a Expr<'_>) -> &'a Expr<'a> { + if let ExprKind::AddrOf(BorrowKind::Ref, _, unref) = &expr.kind { + unref + } else { + expr + } +} + +// Find expressions where `target` is stripped using the length of `pattern`. +// We'll suggest replacing these expressions with the result of the `strip_{prefix,suffix}` +// method. +fn find_stripping<'tcx>( + cx: &LateContext<'tcx>, + strip_kind: StripKind, + target: Res, + pattern: &'tcx Expr<'_>, + expr: &'tcx Expr<'_>, +) -> Vec<Span> { + struct StrippingFinder<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + strip_kind: StripKind, + target: Res, + pattern: &'tcx Expr<'tcx>, + results: Vec<Span>, + } + + impl<'a, 'tcx> Visitor<'tcx> for StrippingFinder<'a, 'tcx> { + fn visit_expr(&mut self, ex: &'tcx Expr<'_>) { + if_chain! { + if is_ref_str(self.cx, ex); + let unref = peel_ref(ex); + if let ExprKind::Index(indexed, index) = &unref.kind; + if let Some(higher::Range { start, end, .. }) = higher::Range::hir(index); + if let ExprKind::Path(path) = &indexed.kind; + if self.cx.qpath_res(path, ex.hir_id) == self.target; + then { + match (self.strip_kind, start, end) { + (StripKind::Prefix, Some(start), None) => { + if eq_pattern_length(self.cx, self.pattern, start) { + self.results.push(ex.span); + return; + } + }, + (StripKind::Suffix, None, Some(end)) => { + if_chain! { + if let ExprKind::Binary(Spanned { node: BinOpKind::Sub, .. }, left, right) = end.kind; + if let Some(left_arg) = len_arg(self.cx, left); + if let ExprKind::Path(left_path) = &left_arg.kind; + if self.cx.qpath_res(left_path, left_arg.hir_id) == self.target; + if eq_pattern_length(self.cx, self.pattern, right); + then { + self.results.push(ex.span); + return; + } + } + }, + _ => {} + } + } + } + + walk_expr(self, ex); + } + } + + let mut finder = StrippingFinder { + cx, + strip_kind, + target, + pattern, + results: vec![], + }; + walk_expr(&mut finder, expr); + finder.results +} diff --git a/src/tools/clippy/clippy_lints/src/map_clone.rs b/src/tools/clippy/clippy_lints/src/map_clone.rs new file mode 100644 index 000000000..95c312f1f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/map_clone.rs @@ -0,0 +1,167 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::{is_copy, is_type_diagnostic_item}; +use clippy_utils::{is_trait_method, meets_msrv, msrvs, peel_blocks}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::mir::Mutability; +use rustc_middle::ty; +use rustc_middle::ty::adjustment::Adjust; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::Ident; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `map(|x| x.clone())` or + /// dereferencing closures for `Copy` types, on `Iterator` or `Option`, + /// and suggests `cloned()` or `copied()` instead + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely + /// + /// ### Example + /// ```rust + /// let x = vec![42, 43]; + /// let y = x.iter(); + /// let z = y.map(|i| *i); + /// ``` + /// + /// The correct use would be: + /// + /// ```rust + /// let x = vec![42, 43]; + /// let y = x.iter(); + /// let z = y.cloned(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MAP_CLONE, + style, + "using `iterator.map(|x| x.clone())`, or dereferencing closures for `Copy` types" +} + +pub struct MapClone { + msrv: Option<RustcVersion>, +} + +impl_lint_pass!(MapClone => [MAP_CLONE]); + +impl MapClone { + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl<'tcx> LateLintPass<'tcx> for MapClone { + fn check_expr(&mut self, cx: &LateContext<'_>, e: &hir::Expr<'_>) { + if e.span.from_expansion() { + return; + } + + if_chain! { + if let hir::ExprKind::MethodCall(method, args, _) = e.kind; + if args.len() == 2; + if method.ident.name == sym::map; + let ty = cx.typeck_results().expr_ty(&args[0]); + if is_type_diagnostic_item(cx, ty, sym::Option) || is_trait_method(cx, e, sym::Iterator); + if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = args[1].kind; + then { + let closure_body = cx.tcx.hir().body(body); + let closure_expr = peel_blocks(&closure_body.value); + match closure_body.params[0].pat.kind { + hir::PatKind::Ref(inner, hir::Mutability::Not) => if let hir::PatKind::Binding( + hir::BindingAnnotation::Unannotated, .., name, None + ) = inner.kind { + if ident_eq(name, closure_expr) { + self.lint_explicit_closure(cx, e.span, args[0].span, true); + } + }, + hir::PatKind::Binding(hir::BindingAnnotation::Unannotated, .., name, None) => { + match closure_expr.kind { + hir::ExprKind::Unary(hir::UnOp::Deref, inner) => { + if ident_eq(name, inner) { + if let ty::Ref(.., Mutability::Not) = cx.typeck_results().expr_ty(inner).kind() { + self.lint_explicit_closure(cx, e.span, args[0].span, true); + } + } + }, + hir::ExprKind::MethodCall(method, [obj], _) => if_chain! { + if ident_eq(name, obj) && method.ident.name == sym::clone; + if let Some(fn_id) = cx.typeck_results().type_dependent_def_id(closure_expr.hir_id); + if let Some(trait_id) = cx.tcx.trait_of_item(fn_id); + if cx.tcx.lang_items().clone_trait().map_or(false, |id| id == trait_id); + // no autoderefs + if !cx.typeck_results().expr_adjustments(obj).iter() + .any(|a| matches!(a.kind, Adjust::Deref(Some(..)))); + then { + let obj_ty = cx.typeck_results().expr_ty(obj); + if let ty::Ref(_, ty, mutability) = obj_ty.kind() { + if matches!(mutability, Mutability::Not) { + let copy = is_copy(cx, *ty); + self.lint_explicit_closure(cx, e.span, args[0].span, copy); + } + } else { + lint_needless_cloning(cx, e.span, args[0].span); + } + } + }, + _ => {}, + } + }, + _ => {}, + } + } + } + } + + extract_msrv_attr!(LateContext); +} + +fn ident_eq(name: Ident, path: &hir::Expr<'_>) -> bool { + if let hir::ExprKind::Path(hir::QPath::Resolved(None, path)) = path.kind { + path.segments.len() == 1 && path.segments[0].ident == name + } else { + false + } +} + +fn lint_needless_cloning(cx: &LateContext<'_>, root: Span, receiver: Span) { + span_lint_and_sugg( + cx, + MAP_CLONE, + root.trim_start(receiver).unwrap(), + "you are needlessly cloning iterator elements", + "remove the `map` call", + String::new(), + Applicability::MachineApplicable, + ); +} + +impl MapClone { + fn lint_explicit_closure(&self, cx: &LateContext<'_>, replace: Span, root: Span, is_copy: bool) { + let mut applicability = Applicability::MachineApplicable; + + let (message, sugg_method) = if is_copy && meets_msrv(self.msrv, msrvs::ITERATOR_COPIED) { + ("you are using an explicit closure for copying elements", "copied") + } else { + ("you are using an explicit closure for cloning elements", "cloned") + }; + + span_lint_and_sugg( + cx, + MAP_CLONE, + replace, + message, + &format!("consider calling the dedicated `{}` method", sugg_method), + format!( + "{}.{}()", + snippet_with_applicability(cx, root, "..", &mut applicability), + sugg_method, + ), + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/map_err_ignore.rs b/src/tools/clippy/clippy_lints/src/map_err_ignore.rs new file mode 100644 index 000000000..21d0e19eb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/map_err_ignore.rs @@ -0,0 +1,154 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{CaptureBy, Closure, Expr, ExprKind, PatKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for instances of `map_err(|_| Some::Enum)` + /// + /// ### Why is this bad? + /// This `map_err` throws away the original error rather than allowing the enum to contain and report the cause of the error + /// + /// ### Example + /// Before: + /// ```rust + /// use std::fmt; + /// + /// #[derive(Debug)] + /// enum Error { + /// Indivisible, + /// Remainder(u8), + /// } + /// + /// impl fmt::Display for Error { + /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + /// match self { + /// Error::Indivisible => write!(f, "could not divide input by three"), + /// Error::Remainder(remainder) => write!( + /// f, + /// "input is not divisible by three, remainder = {}", + /// remainder + /// ), + /// } + /// } + /// } + /// + /// impl std::error::Error for Error {} + /// + /// fn divisible_by_3(input: &str) -> Result<(), Error> { + /// input + /// .parse::<i32>() + /// .map_err(|_| Error::Indivisible) + /// .map(|v| v % 3) + /// .and_then(|remainder| { + /// if remainder == 0 { + /// Ok(()) + /// } else { + /// Err(Error::Remainder(remainder as u8)) + /// } + /// }) + /// } + /// ``` + /// + /// After: + /// ```rust + /// use std::{fmt, num::ParseIntError}; + /// + /// #[derive(Debug)] + /// enum Error { + /// Indivisible(ParseIntError), + /// Remainder(u8), + /// } + /// + /// impl fmt::Display for Error { + /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + /// match self { + /// Error::Indivisible(_) => write!(f, "could not divide input by three"), + /// Error::Remainder(remainder) => write!( + /// f, + /// "input is not divisible by three, remainder = {}", + /// remainder + /// ), + /// } + /// } + /// } + /// + /// impl std::error::Error for Error { + /// fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + /// match self { + /// Error::Indivisible(source) => Some(source), + /// _ => None, + /// } + /// } + /// } + /// + /// fn divisible_by_3(input: &str) -> Result<(), Error> { + /// input + /// .parse::<i32>() + /// .map_err(Error::Indivisible) + /// .map(|v| v % 3) + /// .and_then(|remainder| { + /// if remainder == 0 { + /// Ok(()) + /// } else { + /// Err(Error::Remainder(remainder as u8)) + /// } + /// }) + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub MAP_ERR_IGNORE, + restriction, + "`map_err` should not ignore the original error" +} + +declare_lint_pass!(MapErrIgnore => [MAP_ERR_IGNORE]); + +impl<'tcx> LateLintPass<'tcx> for MapErrIgnore { + // do not try to lint if this is from a macro or desugaring + fn check_expr(&mut self, cx: &LateContext<'_>, e: &Expr<'_>) { + if e.span.from_expansion() { + return; + } + + // check if this is a method call (e.g. x.foo()) + if let ExprKind::MethodCall(method, args, _) = e.kind { + // only work if the method name is `map_err` and there are only 2 arguments (e.g. x.map_err(|_|[1] + // Enum::Variant[2])) + if method.ident.as_str() == "map_err" && args.len() == 2 { + // make sure the first argument is a closure, and grab the CaptureRef, BodyId, and fn_decl_span + // fields + if let ExprKind::Closure(&Closure { + capture_clause, + body, + fn_decl_span, + .. + }) = args[1].kind + { + // check if this is by Reference (meaning there's no move statement) + if capture_clause == CaptureBy::Ref { + // Get the closure body to check the parameters and values + let closure_body = cx.tcx.hir().body(body); + // make sure there's only one parameter (`|_|`) + if closure_body.params.len() == 1 { + // make sure that parameter is the wild token (`_`) + if let PatKind::Wild = closure_body.params[0].pat.kind { + // span the area of the closure capture and warn that the + // original error will be thrown away + span_lint_and_help( + cx, + MAP_ERR_IGNORE, + fn_decl_span, + "`map_err(|_|...` wildcard pattern discards the original error", + None, + "consider storing the original error as a source in the new error, or silence this warning using an ignored identifier (`.map_err(|_foo| ...`)", + ); + } + } + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/map_unit_fn.rs b/src/tools/clippy/clippy_lints/src/map_unit_fn.rs new file mode 100644 index 000000000..af9d948af --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/map_unit_fn.rs @@ -0,0 +1,272 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::{snippet, snippet_with_applicability, snippet_with_context}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{iter_input_pats, method_chain_args}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `option.map(f)` where f is a function + /// or closure that returns the unit type `()`. + /// + /// ### Why is this bad? + /// Readability, this can be written more clearly with + /// an if let statement + /// + /// ### Example + /// ```rust + /// # fn do_stuff() -> Option<String> { Some(String::new()) } + /// # fn log_err_msg(foo: String) -> Option<String> { Some(foo) } + /// # fn format_msg(foo: String) -> String { String::new() } + /// let x: Option<String> = do_stuff(); + /// x.map(log_err_msg); + /// # let x: Option<String> = do_stuff(); + /// x.map(|msg| log_err_msg(format_msg(msg))); + /// ``` + /// + /// The correct use would be: + /// + /// ```rust + /// # fn do_stuff() -> Option<String> { Some(String::new()) } + /// # fn log_err_msg(foo: String) -> Option<String> { Some(foo) } + /// # fn format_msg(foo: String) -> String { String::new() } + /// let x: Option<String> = do_stuff(); + /// if let Some(msg) = x { + /// log_err_msg(msg); + /// } + /// + /// # let x: Option<String> = do_stuff(); + /// if let Some(msg) = x { + /// log_err_msg(format_msg(msg)); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OPTION_MAP_UNIT_FN, + complexity, + "using `option.map(f)`, where `f` is a function or closure that returns `()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `result.map(f)` where f is a function + /// or closure that returns the unit type `()`. + /// + /// ### Why is this bad? + /// Readability, this can be written more clearly with + /// an if let statement + /// + /// ### Example + /// ```rust + /// # fn do_stuff() -> Result<String, String> { Ok(String::new()) } + /// # fn log_err_msg(foo: String) -> Result<String, String> { Ok(foo) } + /// # fn format_msg(foo: String) -> String { String::new() } + /// let x: Result<String, String> = do_stuff(); + /// x.map(log_err_msg); + /// # let x: Result<String, String> = do_stuff(); + /// x.map(|msg| log_err_msg(format_msg(msg))); + /// ``` + /// + /// The correct use would be: + /// + /// ```rust + /// # fn do_stuff() -> Result<String, String> { Ok(String::new()) } + /// # fn log_err_msg(foo: String) -> Result<String, String> { Ok(foo) } + /// # fn format_msg(foo: String) -> String { String::new() } + /// let x: Result<String, String> = do_stuff(); + /// if let Ok(msg) = x { + /// log_err_msg(msg); + /// }; + /// # let x: Result<String, String> = do_stuff(); + /// if let Ok(msg) = x { + /// log_err_msg(format_msg(msg)); + /// }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub RESULT_MAP_UNIT_FN, + complexity, + "using `result.map(f)`, where `f` is a function or closure that returns `()`" +} + +declare_lint_pass!(MapUnit => [OPTION_MAP_UNIT_FN, RESULT_MAP_UNIT_FN]); + +fn is_unit_type(ty: Ty<'_>) -> bool { + match ty.kind() { + ty::Tuple(slice) => slice.is_empty(), + ty::Never => true, + _ => false, + } +} + +fn is_unit_function(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool { + let ty = cx.typeck_results().expr_ty(expr); + + if let ty::FnDef(id, _) = *ty.kind() { + if let Some(fn_type) = cx.tcx.fn_sig(id).no_bound_vars() { + return is_unit_type(fn_type.output()); + } + } + false +} + +fn is_unit_expression(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool { + is_unit_type(cx.typeck_results().expr_ty(expr)) +} + +/// The expression inside a closure may or may not have surrounding braces and +/// semicolons, which causes problems when generating a suggestion. Given an +/// expression that evaluates to '()' or '!', recursively remove useless braces +/// and semi-colons until is suitable for including in the suggestion template +fn reduce_unit_expression<'a>(cx: &LateContext<'_>, expr: &'a hir::Expr<'_>) -> Option<Span> { + if !is_unit_expression(cx, expr) { + return None; + } + + match expr.kind { + hir::ExprKind::Call(_, _) | hir::ExprKind::MethodCall(..) => { + // Calls can't be reduced any more + Some(expr.span) + }, + hir::ExprKind::Block(block, _) => { + match (block.stmts, block.expr.as_ref()) { + (&[], Some(inner_expr)) => { + // If block only contains an expression, + // reduce `{ X }` to `X` + reduce_unit_expression(cx, inner_expr) + }, + (&[ref inner_stmt], None) => { + // If block only contains statements, + // reduce `{ X; }` to `X` or `X;` + match inner_stmt.kind { + hir::StmtKind::Local(local) => Some(local.span), + hir::StmtKind::Expr(e) => Some(e.span), + hir::StmtKind::Semi(..) => Some(inner_stmt.span), + hir::StmtKind::Item(..) => None, + } + }, + _ => { + // For closures that contain multiple statements + // it's difficult to get a correct suggestion span + // for all cases (multi-line closures specifically) + // + // We do not attempt to build a suggestion for those right now. + None + }, + } + }, + _ => None, + } +} + +fn unit_closure<'tcx>( + cx: &LateContext<'tcx>, + expr: &hir::Expr<'_>, +) -> Option<(&'tcx hir::Param<'tcx>, &'tcx hir::Expr<'tcx>)> { + if_chain! { + if let hir::ExprKind::Closure(&hir::Closure { fn_decl, body, .. }) = expr.kind; + let body = cx.tcx.hir().body(body); + let body_expr = &body.value; + if fn_decl.inputs.len() == 1; + if is_unit_expression(cx, body_expr); + if let Some(binding) = iter_input_pats(fn_decl, body).next(); + then { + return Some((binding, body_expr)); + } + } + None +} + +/// Builds a name for the let binding variable (`var_arg`) +/// +/// `x.field` => `x_field` +/// `y` => `_y` +/// +/// Anything else will return `a`. +fn let_binding_name(cx: &LateContext<'_>, var_arg: &hir::Expr<'_>) -> String { + match &var_arg.kind { + hir::ExprKind::Field(_, _) => snippet(cx, var_arg.span, "_").replace('.', "_"), + hir::ExprKind::Path(_) => format!("_{}", snippet(cx, var_arg.span, "")), + _ => "a".to_string(), + } +} + +#[must_use] +fn suggestion_msg(function_type: &str, map_type: &str) -> String { + format!( + "called `map(f)` on an `{0}` value where `f` is a {1} that returns the unit type `()`", + map_type, function_type + ) +} + +fn lint_map_unit_fn(cx: &LateContext<'_>, stmt: &hir::Stmt<'_>, expr: &hir::Expr<'_>, map_args: &[hir::Expr<'_>]) { + let var_arg = &map_args[0]; + + let (map_type, variant, lint) = if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(var_arg), sym::Option) { + ("Option", "Some", OPTION_MAP_UNIT_FN) + } else if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(var_arg), sym::Result) { + ("Result", "Ok", RESULT_MAP_UNIT_FN) + } else { + return; + }; + let fn_arg = &map_args[1]; + + if is_unit_function(cx, fn_arg) { + let mut applicability = Applicability::MachineApplicable; + let msg = suggestion_msg("function", map_type); + let suggestion = format!( + "if let {0}({binding}) = {1} {{ {2}({binding}) }}", + variant, + snippet_with_applicability(cx, var_arg.span, "_", &mut applicability), + snippet_with_applicability(cx, fn_arg.span, "_", &mut applicability), + binding = let_binding_name(cx, var_arg) + ); + + span_lint_and_then(cx, lint, expr.span, &msg, |diag| { + diag.span_suggestion(stmt.span, "try this", suggestion, applicability); + }); + } else if let Some((binding, closure_expr)) = unit_closure(cx, fn_arg) { + let msg = suggestion_msg("closure", map_type); + + span_lint_and_then(cx, lint, expr.span, &msg, |diag| { + if let Some(reduced_expr_span) = reduce_unit_expression(cx, closure_expr) { + let mut applicability = Applicability::MachineApplicable; + let suggestion = format!( + "if let {0}({1}) = {2} {{ {3} }}", + variant, + snippet_with_applicability(cx, binding.pat.span, "_", &mut applicability), + snippet_with_applicability(cx, var_arg.span, "_", &mut applicability), + snippet_with_context(cx, reduced_expr_span, var_arg.span.ctxt(), "_", &mut applicability).0, + ); + diag.span_suggestion(stmt.span, "try this", suggestion, applicability); + } else { + let suggestion = format!( + "if let {0}({1}) = {2} {{ ... }}", + variant, + snippet(cx, binding.pat.span, "_"), + snippet(cx, var_arg.span, "_"), + ); + diag.span_suggestion(stmt.span, "try this", suggestion, Applicability::HasPlaceholders); + } + }); + } +} + +impl<'tcx> LateLintPass<'tcx> for MapUnit { + fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &hir::Stmt<'_>) { + if stmt.span.from_expansion() { + return; + } + + if let hir::StmtKind::Semi(expr) = stmt.kind { + if let Some(arglists) = method_chain_args(expr, &["map"]) { + lint_map_unit_fn(cx, stmt, expr, arglists[0]); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/match_result_ok.rs b/src/tools/clippy/clippy_lints/src/match_result_ok.rs new file mode 100644 index 000000000..3349b85f1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/match_result_ok.rs @@ -0,0 +1,90 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::higher; +use clippy_utils::method_chain_args; +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::{Expr, ExprKind, PatKind, QPath}; +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 unnecessary `ok()` in `while let`. + /// + /// ### Why is this bad? + /// Calling `ok()` in `while let` is unnecessary, instead match + /// on `Ok(pat)` + /// + /// ### Example + /// ```ignore + /// while let Some(value) = iter.next().ok() { + /// vec.push(value) + /// } + /// + /// if let Some(value) = iter.next().ok() { + /// vec.push(value) + /// } + /// ``` + /// Use instead: + /// ```ignore + /// while let Ok(value) = iter.next() { + /// vec.push(value) + /// } + /// + /// if let Ok(value) = iter.next() { + /// vec.push(value) + /// } + /// ``` + #[clippy::version = "1.57.0"] + pub MATCH_RESULT_OK, + style, + "usage of `ok()` in `let Some(pat)` statements is unnecessary, match on `Ok(pat)` instead" +} + +declare_lint_pass!(MatchResultOk => [MATCH_RESULT_OK]); + +impl<'tcx> LateLintPass<'tcx> for MatchResultOk { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (let_pat, let_expr, ifwhile) = + if let Some(higher::IfLet { let_pat, let_expr, .. }) = higher::IfLet::hir(cx, expr) { + (let_pat, let_expr, "if") + } else if let Some(higher::WhileLet { let_pat, let_expr, .. }) = higher::WhileLet::hir(expr) { + (let_pat, let_expr, "while") + } else { + return; + }; + + if_chain! { + if let ExprKind::MethodCall(ok_path, [ref result_types_0, ..], _) = let_expr.kind; //check is expr.ok() has type Result<T,E>.ok(, _) + if let PatKind::TupleStruct(QPath::Resolved(_, x), y, _) = let_pat.kind; //get operation + if method_chain_args(let_expr, &["ok"]).is_some(); //test to see if using ok() method use std::marker::Sized; + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(result_types_0), sym::Result); + if rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_path(x, false)) == "Some"; + + then { + + let mut applicability = Applicability::MachineApplicable; + let some_expr_string = snippet_with_applicability(cx, y[0].span, "", &mut applicability); + let trimmed_ok = snippet_with_applicability(cx, let_expr.span.until(ok_path.ident.span), "", &mut applicability); + let sugg = format!( + "{} let Ok({}) = {}", + ifwhile, + some_expr_string, + trimmed_ok.trim().trim_end_matches('.'), + ); + span_lint_and_sugg( + cx, + MATCH_RESULT_OK, + expr.span.with_hi(let_expr.span.hi()), + "matching on `Some` with `ok()` is redundant", + &format!("consider matching on `Ok({})` and removing the call to `ok` instead", some_expr_string), + sugg, + applicability, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/collapsible_match.rs b/src/tools/clippy/clippy_lints/src/matches/collapsible_match.rs new file mode 100644 index 000000000..07021f1bc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/collapsible_match.rs @@ -0,0 +1,143 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::higher::IfLetOrMatch; +use clippy_utils::visitors::is_local_used; +use clippy_utils::{is_lang_ctor, is_unit_expr, path_to_local, peel_blocks_with_stmt, peel_ref_operators, SpanlessEq}; +use if_chain::if_chain; +use rustc_errors::MultiSpan; +use rustc_hir::LangItem::OptionNone; +use rustc_hir::{Arm, Expr, Guard, HirId, Let, Pat, PatKind}; +use rustc_lint::LateContext; +use rustc_span::Span; + +use super::COLLAPSIBLE_MATCH; + +pub(super) fn check_match<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) { + if let Some(els_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) { + for arm in arms { + check_arm(cx, true, arm.pat, arm.body, arm.guard.as_ref(), Some(els_arm.body)); + } + } +} + +pub(super) fn check_if_let<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + body: &'tcx Expr<'_>, + else_expr: Option<&'tcx Expr<'_>>, +) { + check_arm(cx, false, pat, body, None, else_expr); +} + +fn check_arm<'tcx>( + cx: &LateContext<'tcx>, + outer_is_match: bool, + outer_pat: &'tcx Pat<'tcx>, + outer_then_body: &'tcx Expr<'tcx>, + outer_guard: Option<&'tcx Guard<'tcx>>, + outer_else_body: Option<&'tcx Expr<'tcx>>, +) { + let inner_expr = peel_blocks_with_stmt(outer_then_body); + if_chain! { + if let Some(inner) = IfLetOrMatch::parse(cx, inner_expr); + if let Some((inner_scrutinee, inner_then_pat, inner_else_body)) = match inner { + IfLetOrMatch::IfLet(scrutinee, pat, _, els) => Some((scrutinee, pat, els)), + IfLetOrMatch::Match(scrutinee, arms, ..) => if_chain! { + // if there are more than two arms, collapsing would be non-trivial + if arms.len() == 2 && arms.iter().all(|a| a.guard.is_none()); + // one of the arms must be "wild-like" + if let Some(wild_idx) = arms.iter().rposition(|a| arm_is_wild_like(cx, a)); + then { + let (then, els) = (&arms[1 - wild_idx], &arms[wild_idx]); + Some((scrutinee, then.pat, Some(els.body))) + } else { + None + } + }, + }; + if outer_pat.span.ctxt() == inner_scrutinee.span.ctxt(); + // match expression must be a local binding + // match <local> { .. } + if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_scrutinee)); + if !pat_contains_or(inner_then_pat); + // the binding must come from the pattern of the containing match arm + // ..<local>.. => match <local> { .. } + if let Some(binding_span) = find_pat_binding(outer_pat, binding_id); + // the "else" branches must be equal + if match (outer_else_body, inner_else_body) { + (None, None) => true, + (None, Some(e)) | (Some(e), None) => is_unit_expr(e), + (Some(a), Some(b)) => SpanlessEq::new(cx).eq_expr(a, b), + }; + // the binding must not be used in the if guard + if outer_guard.map_or( + true, + |(Guard::If(e) | Guard::IfLet(Let { init: e, .. }))| !is_local_used(cx, *e, binding_id) + ); + // ...or anywhere in the inner expression + if match inner { + IfLetOrMatch::IfLet(_, _, body, els) => { + !is_local_used(cx, body, binding_id) && els.map_or(true, |e| !is_local_used(cx, e, binding_id)) + }, + IfLetOrMatch::Match(_, arms, ..) => !arms.iter().any(|arm| is_local_used(cx, arm, binding_id)), + }; + then { + let msg = format!( + "this `{}` can be collapsed into the outer `{}`", + if matches!(inner, IfLetOrMatch::Match(..)) { "match" } else { "if let" }, + if outer_is_match { "match" } else { "if let" }, + ); + span_lint_and_then( + cx, + COLLAPSIBLE_MATCH, + inner_expr.span, + &msg, + |diag| { + let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_then_pat.span]); + help_span.push_span_label(binding_span, "replace this binding"); + help_span.push_span_label(inner_then_pat.span, "with this pattern"); + diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern"); + }, + ); + } + } +} + +/// A "wild-like" arm has a wild (`_`) or `None` pattern and no guard. Such arms can be "collapsed" +/// into a single wild arm without any significant loss in semantics or readability. +fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool { + if arm.guard.is_some() { + return false; + } + match arm.pat.kind { + PatKind::Binding(..) | PatKind::Wild => true, + PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone), + _ => false, + } +} + +fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> { + let mut span = None; + pat.walk_short(|p| match &p.kind { + // ignore OR patterns + PatKind::Or(_) => false, + PatKind::Binding(_bm, _, _ident, _) => { + let found = p.hir_id == hir_id; + if found { + span = Some(p.span); + } + !found + }, + _ => true, + }); + span +} + +fn pat_contains_or(pat: &Pat<'_>) -> bool { + let mut result = false; + pat.walk(|p| { + let is_or = matches!(p.kind, PatKind::Or(_)); + result |= is_or; + !is_or + }); + result +} diff --git a/src/tools/clippy/clippy_lints/src/matches/infallible_destructuring_match.rs b/src/tools/clippy/clippy_lints/src/matches/infallible_destructuring_match.rs new file mode 100644 index 000000000..2472acb6f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/infallible_destructuring_match.rs @@ -0,0 +1,44 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{path_to_local_id, peel_blocks, strip_pat_refs}; +use rustc_errors::Applicability; +use rustc_hir::{ExprKind, Local, MatchSource, PatKind, QPath}; +use rustc_lint::LateContext; + +use super::INFALLIBLE_DESTRUCTURING_MATCH; + +pub(crate) fn check(cx: &LateContext<'_>, local: &Local<'_>) -> bool { + if_chain! { + if !local.span.from_expansion(); + if let Some(expr) = local.init; + if let ExprKind::Match(target, arms, MatchSource::Normal) = expr.kind; + if arms.len() == 1 && arms[0].guard.is_none(); + if let PatKind::TupleStruct( + QPath::Resolved(None, variant_name), args, _) = arms[0].pat.kind; + if args.len() == 1; + if let PatKind::Binding(_, arg, ..) = strip_pat_refs(&args[0]).kind; + let body = peel_blocks(arms[0].body); + if path_to_local_id(body, arg); + + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + INFALLIBLE_DESTRUCTURING_MATCH, + local.span, + "you seem to be trying to use `match` to destructure a single infallible pattern. \ + Consider using `let`", + "try this", + format!( + "let {}({}) = {};", + snippet_with_applicability(cx, variant_name.span, "..", &mut applicability), + snippet_with_applicability(cx, local.pat.span, "..", &mut applicability), + snippet_with_applicability(cx, target.span, "..", &mut applicability), + ), + applicability, + ); + return true; + } + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/matches/manual_map.rs b/src/tools/clippy/clippy_lints/src/matches/manual_map.rs new file mode 100644 index 000000000..8f98b43b9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/manual_map.rs @@ -0,0 +1,306 @@ +use crate::{map_unit_fn::OPTION_MAP_UNIT_FN, matches::MATCH_AS_REF}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{snippet_with_applicability, snippet_with_context}; +use clippy_utils::ty::{is_type_diagnostic_item, peel_mid_ty_refs_is_mutable, type_is_unsafe_function}; +use clippy_utils::{ + can_move_expr_to_closure, is_else_clause, is_lang_ctor, is_lint_allowed, path_to_local_id, peel_blocks, + peel_hir_expr_refs, peel_hir_expr_while, CaptureKind, +}; +use rustc_ast::util::parser::PREC_POSTFIX; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{OptionNone, OptionSome}; +use rustc_hir::{ + def::Res, Arm, BindingAnnotation, Block, BlockCheckMode, Expr, ExprKind, HirId, Mutability, Pat, PatKind, Path, + QPath, UnsafeSource, +}; +use rustc_lint::LateContext; +use rustc_span::{sym, SyntaxContext}; + +use super::MANUAL_MAP; + +pub(super) fn check_match<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + scrutinee: &'tcx Expr<'_>, + arms: &'tcx [Arm<'_>], +) { + if let [arm1, arm2] = arms + && arm1.guard.is_none() + && arm2.guard.is_none() + { + check(cx, expr, scrutinee, arm1.pat, arm1.body, Some(arm2.pat), arm2.body); + } +} + +pub(super) fn check_if_let<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + let_pat: &'tcx Pat<'_>, + let_expr: &'tcx Expr<'_>, + then_expr: &'tcx Expr<'_>, + else_expr: &'tcx Expr<'_>, +) { + check(cx, expr, let_expr, let_pat, then_expr, None, else_expr); +} + +#[expect(clippy::too_many_lines)] +fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + scrutinee: &'tcx Expr<'_>, + then_pat: &'tcx Pat<'_>, + then_body: &'tcx Expr<'_>, + else_pat: Option<&'tcx Pat<'_>>, + else_body: &'tcx Expr<'_>, +) { + let (scrutinee_ty, ty_ref_count, ty_mutability) = + peel_mid_ty_refs_is_mutable(cx.typeck_results().expr_ty(scrutinee)); + if !(is_type_diagnostic_item(cx, scrutinee_ty, sym::Option) + && is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(expr), sym::Option)) + { + return; + } + + let expr_ctxt = expr.span.ctxt(); + let (some_expr, some_pat, pat_ref_count, is_wild_none) = match ( + try_parse_pattern(cx, then_pat, expr_ctxt), + else_pat.map_or(Some(OptionPat::Wild), |p| try_parse_pattern(cx, p, expr_ctxt)), + ) { + (Some(OptionPat::Wild), Some(OptionPat::Some { pattern, ref_count })) if is_none_expr(cx, then_body) => { + (else_body, pattern, ref_count, true) + }, + (Some(OptionPat::None), Some(OptionPat::Some { pattern, ref_count })) if is_none_expr(cx, then_body) => { + (else_body, pattern, ref_count, false) + }, + (Some(OptionPat::Some { pattern, ref_count }), Some(OptionPat::Wild)) if is_none_expr(cx, else_body) => { + (then_body, pattern, ref_count, true) + }, + (Some(OptionPat::Some { pattern, ref_count }), Some(OptionPat::None)) if is_none_expr(cx, else_body) => { + (then_body, pattern, ref_count, false) + }, + _ => return, + }; + + // Top level or patterns aren't allowed in closures. + if matches!(some_pat.kind, PatKind::Or(_)) { + return; + } + + let some_expr = match get_some_expr(cx, some_expr, false, expr_ctxt) { + Some(expr) => expr, + None => return, + }; + + // These two lints will go back and forth with each other. + if cx.typeck_results().expr_ty(some_expr.expr) == cx.tcx.types.unit + && !is_lint_allowed(cx, OPTION_MAP_UNIT_FN, expr.hir_id) + { + return; + } + + // `map` won't perform any adjustments. + if !cx.typeck_results().expr_adjustments(some_expr.expr).is_empty() { + return; + } + + // Determine which binding mode to use. + let explicit_ref = some_pat.contains_explicit_ref_binding(); + let binding_ref = explicit_ref.or_else(|| (ty_ref_count != pat_ref_count).then_some(ty_mutability)); + + let as_ref_str = match binding_ref { + Some(Mutability::Mut) => ".as_mut()", + Some(Mutability::Not) => ".as_ref()", + None => "", + }; + + match can_move_expr_to_closure(cx, some_expr.expr) { + Some(captures) => { + // Check if captures the closure will need conflict with borrows made in the scrutinee. + // TODO: check all the references made in the scrutinee expression. This will require interacting + // with the borrow checker. Currently only `<local>[.<field>]*` is checked for. + if let Some(binding_ref_mutability) = binding_ref { + let e = peel_hir_expr_while(scrutinee, |e| match e.kind { + ExprKind::Field(e, _) | ExprKind::AddrOf(_, _, e) => Some(e), + _ => None, + }); + if let ExprKind::Path(QPath::Resolved(None, Path { res: Res::Local(l), .. })) = e.kind { + match captures.get(l) { + Some(CaptureKind::Value | CaptureKind::Ref(Mutability::Mut)) => return, + Some(CaptureKind::Ref(Mutability::Not)) if binding_ref_mutability == Mutability::Mut => { + return; + }, + Some(CaptureKind::Ref(Mutability::Not)) | None => (), + } + } + } + }, + None => return, + }; + + let mut app = Applicability::MachineApplicable; + + // Remove address-of expressions from the scrutinee. Either `as_ref` will be called, or + // it's being passed by value. + let scrutinee = peel_hir_expr_refs(scrutinee).0; + let (scrutinee_str, _) = snippet_with_context(cx, scrutinee.span, expr_ctxt, "..", &mut app); + let scrutinee_str = if scrutinee.span.ctxt() == expr.span.ctxt() && scrutinee.precedence().order() < PREC_POSTFIX { + format!("({})", scrutinee_str) + } else { + scrutinee_str.into() + }; + + let body_str = if let PatKind::Binding(annotation, id, some_binding, None) = some_pat.kind { + if_chain! { + if !some_expr.needs_unsafe_block; + if let Some(func) = can_pass_as_func(cx, id, some_expr.expr); + if func.span.ctxt() == some_expr.expr.span.ctxt(); + then { + snippet_with_applicability(cx, func.span, "..", &mut app).into_owned() + } else { + if path_to_local_id(some_expr.expr, id) + && !is_lint_allowed(cx, MATCH_AS_REF, expr.hir_id) + && binding_ref.is_some() + { + return; + } + + // `ref` and `ref mut` annotations were handled earlier. + let annotation = if matches!(annotation, BindingAnnotation::Mutable) { + "mut " + } else { + "" + }; + let expr_snip = snippet_with_context(cx, some_expr.expr.span, expr_ctxt, "..", &mut app).0; + if some_expr.needs_unsafe_block { + format!("|{}{}| unsafe {{ {} }}", annotation, some_binding, expr_snip) + } else { + format!("|{}{}| {}", annotation, some_binding, expr_snip) + } + } + } + } else if !is_wild_none && explicit_ref.is_none() { + // TODO: handle explicit reference annotations. + let pat_snip = snippet_with_context(cx, some_pat.span, expr_ctxt, "..", &mut app).0; + let expr_snip = snippet_with_context(cx, some_expr.expr.span, expr_ctxt, "..", &mut app).0; + if some_expr.needs_unsafe_block { + format!("|{}| unsafe {{ {} }}", pat_snip, expr_snip) + } else { + format!("|{}| {}", pat_snip, expr_snip) + } + } else { + // Refutable bindings and mixed reference annotations can't be handled by `map`. + return; + }; + + span_lint_and_sugg( + cx, + MANUAL_MAP, + expr.span, + "manual implementation of `Option::map`", + "try this", + if else_pat.is_none() && is_else_clause(cx.tcx, expr) { + format!("{{ {}{}.map({}) }}", scrutinee_str, as_ref_str, body_str) + } else { + format!("{}{}.map({})", scrutinee_str, as_ref_str, body_str) + }, + app, + ); +} + +// Checks whether the expression could be passed as a function, or whether a closure is needed. +// Returns the function to be passed to `map` if it exists. +fn can_pass_as_func<'tcx>(cx: &LateContext<'tcx>, binding: HirId, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { + match expr.kind { + ExprKind::Call(func, [arg]) + if path_to_local_id(arg, binding) + && cx.typeck_results().expr_adjustments(arg).is_empty() + && !type_is_unsafe_function(cx, cx.typeck_results().expr_ty(func).peel_refs()) => + { + Some(func) + }, + _ => None, + } +} + +enum OptionPat<'a> { + Wild, + None, + Some { + // The pattern contained in the `Some` tuple. + pattern: &'a Pat<'a>, + // The number of references before the `Some` tuple. + // e.g. `&&Some(_)` has a ref count of 2. + ref_count: usize, + }, +} + +struct SomeExpr<'tcx> { + expr: &'tcx Expr<'tcx>, + needs_unsafe_block: bool, +} + +// Try to parse into a recognized `Option` pattern. +// i.e. `_`, `None`, `Some(..)`, or a reference to any of those. +fn try_parse_pattern<'tcx>(cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>, ctxt: SyntaxContext) -> Option<OptionPat<'tcx>> { + fn f<'tcx>( + cx: &LateContext<'tcx>, + pat: &'tcx Pat<'_>, + ref_count: usize, + ctxt: SyntaxContext, + ) -> Option<OptionPat<'tcx>> { + match pat.kind { + PatKind::Wild => Some(OptionPat::Wild), + PatKind::Ref(pat, _) => f(cx, pat, ref_count + 1, ctxt), + PatKind::Path(ref qpath) if is_lang_ctor(cx, qpath, OptionNone) => Some(OptionPat::None), + PatKind::TupleStruct(ref qpath, [pattern], _) + if is_lang_ctor(cx, qpath, OptionSome) && pat.span.ctxt() == ctxt => + { + Some(OptionPat::Some { pattern, ref_count }) + }, + _ => None, + } + } + f(cx, pat, 0, ctxt) +} + +// Checks for an expression wrapped by the `Some` constructor. Returns the contained expression. +fn get_some_expr<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + needs_unsafe_block: bool, + ctxt: SyntaxContext, +) -> Option<SomeExpr<'tcx>> { + // TODO: Allow more complex expressions. + match expr.kind { + ExprKind::Call( + Expr { + kind: ExprKind::Path(ref qpath), + .. + }, + [arg], + ) if ctxt == expr.span.ctxt() && is_lang_ctor(cx, qpath, OptionSome) => Some(SomeExpr { + expr: arg, + needs_unsafe_block, + }), + ExprKind::Block( + Block { + stmts: [], + expr: Some(expr), + rules, + .. + }, + _, + ) => get_some_expr( + cx, + expr, + needs_unsafe_block || *rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided), + ctxt, + ), + _ => None, + } +} + +// Checks for the `None` value. +fn is_none_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + matches!(peel_blocks(expr).kind, ExprKind::Path(ref qpath) if is_lang_ctor(cx, qpath, OptionNone)) +} diff --git a/src/tools/clippy/clippy_lints/src/matches/manual_unwrap_or.rs b/src/tools/clippy/clippy_lints/src/matches/manual_unwrap_or.rs new file mode 100644 index 000000000..e1111c80f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/manual_unwrap_or.rs @@ -0,0 +1,83 @@ +use clippy_utils::consts::constant_simple; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{indent_of, reindent_multiline, snippet_opt}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::usage::contains_return_break_continue_macro; +use clippy_utils::{is_lang_ctor, path_to_local_id, sugg}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk}; +use rustc_hir::{Arm, Expr, PatKind}; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::MANUAL_UNWRAP_OR; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>, scrutinee: &'tcx Expr<'_>, arms: &'tcx [Arm<'_>]) { + let ty = cx.typeck_results().expr_ty(scrutinee); + if_chain! { + if let Some(ty_name) = if is_type_diagnostic_item(cx, ty, sym::Option) { + Some("Option") + } else if is_type_diagnostic_item(cx, ty, sym::Result) { + Some("Result") + } else { + None + }; + if let Some(or_arm) = applicable_or_arm(cx, arms); + if let Some(or_body_snippet) = snippet_opt(cx, or_arm.body.span); + if let Some(indent) = indent_of(cx, expr.span); + if constant_simple(cx, cx.typeck_results(), or_arm.body).is_some(); + then { + let reindented_or_body = + reindent_multiline(or_body_snippet.into(), true, Some(indent)); + + let suggestion = if scrutinee.span.from_expansion() { + // we don't want parentheses around macro, e.g. `(some_macro!()).unwrap_or(0)` + sugg::Sugg::hir_with_macro_callsite(cx, scrutinee, "..") + } + else { + sugg::Sugg::hir(cx, scrutinee, "..").maybe_par() + }; + + span_lint_and_sugg( + cx, + MANUAL_UNWRAP_OR, expr.span, + &format!("this pattern reimplements `{}::unwrap_or`", ty_name), + "replace with", + format!( + "{}.unwrap_or({})", + suggestion, + reindented_or_body, + ), + Applicability::MachineApplicable, + ); + } + } +} + +fn applicable_or_arm<'a>(cx: &LateContext<'_>, arms: &'a [Arm<'a>]) -> Option<&'a Arm<'a>> { + if_chain! { + if arms.len() == 2; + if arms.iter().all(|arm| arm.guard.is_none()); + if let Some((idx, or_arm)) = arms.iter().enumerate().find(|(_, arm)| { + match arm.pat.kind { + PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone), + PatKind::TupleStruct(ref qpath, [pat], _) => + matches!(pat.kind, PatKind::Wild) && is_lang_ctor(cx, qpath, ResultErr), + _ => false, + } + }); + let unwrap_arm = &arms[1 - idx]; + if let PatKind::TupleStruct(ref qpath, [unwrap_pat], _) = unwrap_arm.pat.kind; + if is_lang_ctor(cx, qpath, OptionSome) || is_lang_ctor(cx, qpath, ResultOk); + if let PatKind::Binding(_, binding_hir_id, ..) = unwrap_pat.kind; + if path_to_local_id(unwrap_arm.body, binding_hir_id); + if cx.typeck_results().expr_adjustments(unwrap_arm.body).is_empty(); + if !contains_return_break_continue_macro(or_arm.body); + then { + Some(or_arm) + } else { + None + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_as_ref.rs b/src/tools/clippy/clippy_lints/src/matches/match_as_ref.rs new file mode 100644 index 000000000..d914eba01 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_as_ref.rs @@ -0,0 +1,85 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{is_lang_ctor, peel_blocks}; +use rustc_errors::Applicability; +use rustc_hir::{Arm, BindingAnnotation, Expr, ExprKind, LangItem, PatKind, QPath}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::MATCH_AS_REF; + +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() { + let arm_ref: Option<BindingAnnotation> = if is_none_arm(cx, &arms[0]) { + is_ref_some_arm(cx, &arms[1]) + } else if is_none_arm(cx, &arms[1]) { + is_ref_some_arm(cx, &arms[0]) + } else { + None + }; + if let Some(rb) = arm_ref { + let suggestion = if rb == BindingAnnotation::Ref { + "as_ref" + } else { + "as_mut" + }; + + let output_ty = cx.typeck_results().expr_ty(expr); + let input_ty = cx.typeck_results().expr_ty(ex); + + let cast = if_chain! { + if let ty::Adt(_, substs) = input_ty.kind(); + let input_ty = substs.type_at(0); + if let ty::Adt(_, substs) = output_ty.kind(); + let output_ty = substs.type_at(0); + if let ty::Ref(_, output_ty, _) = *output_ty.kind(); + if input_ty != output_ty; + then { + ".map(|x| x as _)" + } else { + "" + } + }; + + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + MATCH_AS_REF, + expr.span, + &format!("use `{}()` instead", suggestion), + "try this", + format!( + "{}.{}(){}", + snippet_with_applicability(cx, ex.span, "_", &mut applicability), + suggestion, + cast, + ), + applicability, + ); + } + } +} + +// Checks if arm has the form `None => None` +fn is_none_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool { + matches!(arm.pat.kind, PatKind::Path(ref qpath) if is_lang_ctor(cx, qpath, LangItem::OptionNone)) +} + +// Checks if arm has the form `Some(ref v) => Some(v)` (checks for `ref` and `ref mut`) +fn is_ref_some_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> Option<BindingAnnotation> { + if_chain! { + if let PatKind::TupleStruct(ref qpath, [first_pat, ..], _) = arm.pat.kind; + if is_lang_ctor(cx, qpath, LangItem::OptionSome); + if let PatKind::Binding(rb, .., ident, _) = first_pat.kind; + if rb == BindingAnnotation::Ref || rb == BindingAnnotation::RefMut; + if let ExprKind::Call(e, args) = peel_blocks(arm.body).kind; + if let ExprKind::Path(ref some_path) = e.kind; + if is_lang_ctor(cx, some_path, LangItem::OptionSome) && args.len() == 1; + if let ExprKind::Path(QPath::Resolved(_, path2)) = args[0].kind; + if path2.segments.len() == 1 && ident.name == path2.segments[0].ident.name; + then { + return Some(rb) + } + } + None +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_bool.rs b/src/tools/clippy/clippy_lints/src/matches/match_bool.rs new file mode 100644 index 000000000..1c216e135 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_bool.rs @@ -0,0 +1,75 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::is_unit_expr; +use clippy_utils::source::{expr_block, snippet}; +use clippy_utils::sugg::Sugg; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Arm, Expr, ExprKind, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::MATCH_BOOL; + +pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { + // Type of expression is `bool`. + if *cx.typeck_results().expr_ty(ex).kind() == ty::Bool { + span_lint_and_then( + cx, + MATCH_BOOL, + expr.span, + "you seem to be trying to match on a boolean expression", + move |diag| { + if arms.len() == 2 { + // no guards + let exprs = if let PatKind::Lit(arm_bool) = arms[0].pat.kind { + if let ExprKind::Lit(ref lit) = arm_bool.kind { + match lit.node { + LitKind::Bool(true) => Some((arms[0].body, arms[1].body)), + LitKind::Bool(false) => Some((arms[1].body, arms[0].body)), + _ => None, + } + } else { + None + } + } else { + None + }; + + if let Some((true_expr, false_expr)) = exprs { + let sugg = match (is_unit_expr(true_expr), is_unit_expr(false_expr)) { + (false, false) => Some(format!( + "if {} {} else {}", + snippet(cx, ex.span, "b"), + expr_block(cx, true_expr, None, "..", Some(expr.span)), + expr_block(cx, false_expr, None, "..", Some(expr.span)) + )), + (false, true) => Some(format!( + "if {} {}", + snippet(cx, ex.span, "b"), + expr_block(cx, true_expr, None, "..", Some(expr.span)) + )), + (true, false) => { + let test = Sugg::hir(cx, ex, ".."); + Some(format!( + "if {} {}", + !test, + expr_block(cx, false_expr, None, "..", Some(expr.span)) + )) + }, + (true, true) => None, + }; + + if let Some(sugg) = sugg { + diag.span_suggestion( + expr.span, + "consider using an `if`/`else` expression", + sugg, + Applicability::HasPlaceholders, + ); + } + } + } + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_like_matches.rs b/src/tools/clippy/clippy_lints/src/matches/match_like_matches.rs new file mode 100644 index 000000000..0da4833f1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_like_matches.rs @@ -0,0 +1,171 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_wild; +use clippy_utils::source::snippet_with_applicability; +use rustc_ast::{Attribute, LitKind}; +use rustc_errors::Applicability; +use rustc_hir::{Arm, BorrowKind, Expr, ExprKind, Guard, Pat}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::source_map::Spanned; + +use super::MATCH_LIKE_MATCHES_MACRO; + +/// Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!` +pub(crate) fn check_if_let<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + let_pat: &'tcx Pat<'_>, + let_expr: &'tcx Expr<'_>, + then_expr: &'tcx Expr<'_>, + else_expr: &'tcx Expr<'_>, +) { + find_matches_sugg( + cx, + let_expr, + IntoIterator::into_iter([ + (&[][..], Some(let_pat), then_expr, None), + (&[][..], None, else_expr, None), + ]), + expr, + true, + ); +} + +pub(super) fn check_match<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + scrutinee: &'tcx Expr<'_>, + arms: &'tcx [Arm<'tcx>], +) -> bool { + find_matches_sugg( + cx, + scrutinee, + arms.iter().map(|arm| { + ( + cx.tcx.hir().attrs(arm.hir_id), + Some(arm.pat), + arm.body, + arm.guard.as_ref(), + ) + }), + e, + false, + ) +} + +/// Lint a `match` or `if let` for replacement by `matches!` +fn find_matches_sugg<'a, 'b, I>( + cx: &LateContext<'_>, + ex: &Expr<'_>, + mut iter: I, + expr: &Expr<'_>, + is_if_let: bool, +) -> bool +where + 'b: 'a, + I: Clone + + DoubleEndedIterator + + ExactSizeIterator + + Iterator< + Item = ( + &'a [Attribute], + Option<&'a Pat<'b>>, + &'a Expr<'b>, + Option<&'a Guard<'b>>, + ), + >, +{ + if_chain! { + if iter.len() >= 2; + if cx.typeck_results().expr_ty(expr).is_bool(); + if let Some((_, last_pat_opt, last_expr, _)) = iter.next_back(); + let iter_without_last = iter.clone(); + if let Some((first_attrs, _, first_expr, first_guard)) = iter.next(); + if let Some(b0) = find_bool_lit(&first_expr.kind); + if let Some(b1) = find_bool_lit(&last_expr.kind); + if b0 != b1; + if first_guard.is_none() || iter.len() == 0; + if first_attrs.is_empty(); + if iter + .all(|arm| { + find_bool_lit(&arm.2.kind).map_or(false, |b| b == b0) && arm.3.is_none() && arm.0.is_empty() + }); + then { + if let Some(last_pat) = last_pat_opt { + if !is_wild(last_pat) { + return false; + } + } + + // The suggestion may be incorrect, because some arms can have `cfg` attributes + // evaluated into `false` and so such arms will be stripped before. + let mut applicability = Applicability::MaybeIncorrect; + let pat = { + use itertools::Itertools as _; + iter_without_last + .filter_map(|arm| { + let pat_span = arm.1?.span; + Some(snippet_with_applicability(cx, pat_span, "..", &mut applicability)) + }) + .join(" | ") + }; + let pat_and_guard = if let Some(Guard::If(g)) = first_guard { + format!("{} if {}", pat, snippet_with_applicability(cx, g.span, "..", &mut applicability)) + } else { + pat + }; + + // strip potential borrows (#6503), but only if the type is a reference + let mut ex_new = ex; + if let ExprKind::AddrOf(BorrowKind::Ref, .., ex_inner) = ex.kind { + if let ty::Ref(..) = cx.typeck_results().expr_ty(ex_inner).kind() { + ex_new = ex_inner; + } + }; + span_lint_and_sugg( + cx, + MATCH_LIKE_MATCHES_MACRO, + expr.span, + &format!("{} expression looks like `matches!` macro", if is_if_let { "if let .. else" } else { "match" }), + "try this", + format!( + "{}matches!({}, {})", + if b0 { "" } else { "!" }, + snippet_with_applicability(cx, ex_new.span, "..", &mut applicability), + pat_and_guard, + ), + applicability, + ); + true + } else { + false + } + } +} + +/// Extract a `bool` or `{ bool }` +fn find_bool_lit(ex: &ExprKind<'_>) -> Option<bool> { + match ex { + ExprKind::Lit(Spanned { + node: LitKind::Bool(b), .. + }) => Some(*b), + ExprKind::Block( + rustc_hir::Block { + stmts: &[], + expr: Some(exp), + .. + }, + _, + ) => { + if let ExprKind::Lit(Spanned { + node: LitKind::Bool(b), .. + }) = exp.kind + { + Some(b) + } else { + None + } + }, + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_on_vec_items.rs b/src/tools/clippy/clippy_lints/src/matches/match_on_vec_items.rs new file mode 100644 index 000000000..2917f85c4 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_on_vec_items.rs @@ -0,0 +1,61 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, LangItem}; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::MATCH_ON_VEC_ITEMS; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, scrutinee: &'tcx Expr<'_>) { + if_chain! { + if let Some(idx_expr) = is_vec_indexing(cx, scrutinee); + if let ExprKind::Index(vec, idx) = idx_expr.kind; + + then { + // FIXME: could be improved to suggest surrounding every pattern with Some(_), + // but only when `or_patterns` are stabilized. + span_lint_and_sugg( + cx, + MATCH_ON_VEC_ITEMS, + scrutinee.span, + "indexing into a vector may panic", + "try this", + format!( + "{}.get({})", + snippet(cx, vec.span, ".."), + snippet(cx, idx.span, "..") + ), + Applicability::MaybeIncorrect + ); + } + } +} + +fn is_vec_indexing<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { + if_chain! { + if let ExprKind::Index(array, index) = expr.kind; + if is_vector(cx, array); + if !is_full_range(cx, index); + + then { + return Some(expr); + } + } + + None +} + +fn is_vector(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let ty = cx.typeck_results().expr_ty(expr); + let ty = ty.peel_refs(); + is_type_diagnostic_item(cx, ty, sym::Vec) +} + +fn is_full_range(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let ty = cx.typeck_results().expr_ty(expr); + let ty = ty.peel_refs(); + is_type_lang_item(cx, ty, LangItem::RangeFull) +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_ref_pats.rs b/src/tools/clippy/clippy_lints/src/matches/match_ref_pats.rs new file mode 100644 index 000000000..80f964ba1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_ref_pats.rs @@ -0,0 +1,66 @@ +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::source::snippet; +use clippy_utils::sugg::Sugg; +use core::iter::once; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, Pat, PatKind}; +use rustc_lint::LateContext; + +use super::MATCH_REF_PATS; + +pub(crate) fn check<'a, 'b, I>(cx: &LateContext<'_>, ex: &Expr<'_>, pats: I, expr: &Expr<'_>) +where + 'b: 'a, + I: Clone + Iterator<Item = &'a Pat<'b>>, +{ + if !has_multiple_ref_pats(pats.clone()) { + return; + } + + let (first_sugg, msg, title); + let span = ex.span.source_callsite(); + if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, inner) = ex.kind { + first_sugg = once((span, Sugg::hir_with_macro_callsite(cx, inner, "..").to_string())); + msg = "try"; + title = "you don't need to add `&` to both the expression and the patterns"; + } else { + first_sugg = once((span, Sugg::hir_with_macro_callsite(cx, ex, "..").deref().to_string())); + msg = "instead of prefixing all patterns with `&`, you can dereference the expression"; + title = "you don't need to add `&` to all patterns"; + } + + let remaining_suggs = pats.filter_map(|pat| { + if let PatKind::Ref(refp, _) = pat.kind { + Some((pat.span, snippet(cx, refp.span, "..").to_string())) + } else { + None + } + }); + + span_lint_and_then(cx, MATCH_REF_PATS, expr.span, title, |diag| { + if !expr.span.from_expansion() { + multispan_sugg(diag, msg, first_sugg.chain(remaining_suggs)); + } + }); +} + +fn has_multiple_ref_pats<'a, 'b, I>(pats: I) -> bool +where + 'b: 'a, + I: Iterator<Item = &'a Pat<'b>>, +{ + let mut ref_count = 0; + for opt in pats.map(|pat| match pat.kind { + PatKind::Ref(..) => Some(true), // &-patterns + PatKind::Wild => Some(false), // an "anything" wildcard is also fine + _ => None, // any other pattern is not fine + }) { + if let Some(inner) = opt { + if inner { + ref_count += 1; + } + } else { + return false; + } + } + ref_count > 1 +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_same_arms.rs b/src/tools/clippy/clippy_lints/src/matches/match_same_arms.rs new file mode 100644 index 000000000..582782f24 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_same_arms.rs @@ -0,0 +1,414 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::{path_to_local, search_same, SpanlessEq, SpanlessHash}; +use core::cmp::Ordering; +use core::iter; +use core::slice; +use rustc_arena::DroplessArena; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::def_id::DefId; +use rustc_hir::{Arm, Expr, ExprKind, HirId, HirIdMap, HirIdSet, Pat, PatKind, RangeEnd}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::Symbol; +use std::collections::hash_map::Entry; + +use super::MATCH_SAME_ARMS; + +#[expect(clippy::too_many_lines)] +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) { + let hash = |&(_, arm): &(usize, &Arm<'_>)| -> u64 { + let mut h = SpanlessHash::new(cx); + h.hash_expr(arm.body); + h.finish() + }; + + let arena = DroplessArena::default(); + let normalized_pats: Vec<_> = arms + .iter() + .map(|a| NormalizedPat::from_pat(cx, &arena, a.pat)) + .collect(); + + // The furthest forwards a pattern can move without semantic changes + let forwards_blocking_idxs: Vec<_> = normalized_pats + .iter() + .enumerate() + .map(|(i, pat)| { + normalized_pats[i + 1..] + .iter() + .enumerate() + .find_map(|(j, other)| pat.has_overlapping_values(other).then_some(i + 1 + j)) + .unwrap_or(normalized_pats.len()) + }) + .collect(); + + // The furthest backwards a pattern can move without semantic changes + let backwards_blocking_idxs: Vec<_> = normalized_pats + .iter() + .enumerate() + .map(|(i, pat)| { + normalized_pats[..i] + .iter() + .enumerate() + .rev() + .zip(forwards_blocking_idxs[..i].iter().copied().rev()) + .skip_while(|&(_, forward_block)| forward_block > i) + .find_map(|((j, other), forward_block)| { + (forward_block == i || pat.has_overlapping_values(other)).then_some(j) + }) + .unwrap_or(0) + }) + .collect(); + + let eq = |&(lindex, lhs): &(usize, &Arm<'_>), &(rindex, rhs): &(usize, &Arm<'_>)| -> bool { + let min_index = usize::min(lindex, rindex); + let max_index = usize::max(lindex, rindex); + + let mut local_map: HirIdMap<HirId> = HirIdMap::default(); + let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| { + if_chain! { + if let Some(a_id) = path_to_local(a); + if let Some(b_id) = path_to_local(b); + let entry = match local_map.entry(a_id) { + Entry::Vacant(entry) => entry, + // check if using the same bindings as before + Entry::Occupied(entry) => return *entry.get() == b_id, + }; + // the names technically don't have to match; this makes the lint more conservative + if cx.tcx.hir().name(a_id) == cx.tcx.hir().name(b_id); + if cx.typeck_results().expr_ty(a) == cx.typeck_results().expr_ty(b); + if pat_contains_local(lhs.pat, a_id); + if pat_contains_local(rhs.pat, b_id); + then { + entry.insert(b_id); + true + } else { + false + } + } + }; + // Arms with a guard are ignored, those can’t always be merged together + // If both arms overlap with an arm in between then these can't be merged either. + !(backwards_blocking_idxs[max_index] > min_index && forwards_blocking_idxs[min_index] < max_index) + && lhs.guard.is_none() + && rhs.guard.is_none() + && SpanlessEq::new(cx) + .expr_fallback(eq_fallback) + .eq_expr(lhs.body, rhs.body) + // these checks could be removed to allow unused bindings + && bindings_eq(lhs.pat, local_map.keys().copied().collect()) + && bindings_eq(rhs.pat, local_map.values().copied().collect()) + }; + + 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"); + }, + ); + } 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) { + (arm1, arm2) + } else { + (arm2, arm1) + }; + + span_lint_and_then( + cx, + MATCH_SAME_ARMS, + keep_arm.span, + "this match arm has an identical body to another arm", + |diag| { + let move_pat_snip = snippet(cx, move_arm.pat.span, "<pat2>"); + let keep_pat_snip = snippet(cx, keep_arm.pat.span, "<pat1>"); + + diag.span_suggestion( + keep_arm.pat.span, + "try merging the arm patterns", + format!("{} | {}", keep_pat_snip, move_pat_snip), + Applicability::MaybeIncorrect, + ) + .help("or try changing either arm body") + .span_note(move_arm.span, "other arm here"); + }, + ); + } + } +} + +#[derive(Clone, Copy)] +enum NormalizedPat<'a> { + Wild, + Struct(Option<DefId>, &'a [(Symbol, Self)]), + Tuple(Option<DefId>, &'a [Self]), + Or(&'a [Self]), + Path(Option<DefId>), + LitStr(Symbol), + LitBytes(&'a [u8]), + LitInt(u128), + LitBool(bool), + Range(PatRange), + /// A slice pattern. If the second value is `None`, then this matches an exact size. Otherwise + /// the first value contains everything before the `..` wildcard pattern, and the second value + /// contains everything afterwards. Note that either side, or both sides, may contain zero + /// patterns. + Slice(&'a [Self], Option<&'a [Self]>), +} + +#[derive(Clone, Copy)] +struct PatRange { + start: u128, + end: u128, + bounds: RangeEnd, +} +impl PatRange { + fn contains(&self, x: u128) -> bool { + x >= self.start + && match self.bounds { + RangeEnd::Included => x <= self.end, + RangeEnd::Excluded => x < self.end, + } + } + + fn overlaps(&self, other: &Self) -> bool { + // Note: Empty ranges are impossible, so this is correct even though it would return true if an + // empty exclusive range were to reside within an inclusive range. + (match self.bounds { + RangeEnd::Included => self.end >= other.start, + RangeEnd::Excluded => self.end > other.start, + } && match other.bounds { + RangeEnd::Included => self.start <= other.end, + RangeEnd::Excluded => self.start < other.end, + }) + } +} + +/// Iterates over the pairs of fields with matching names. +fn iter_matching_struct_fields<'a>( + left: &'a [(Symbol, NormalizedPat<'a>)], + right: &'a [(Symbol, NormalizedPat<'a>)], +) -> impl Iterator<Item = (&'a NormalizedPat<'a>, &'a NormalizedPat<'a>)> + 'a { + struct Iter<'a>( + slice::Iter<'a, (Symbol, NormalizedPat<'a>)>, + slice::Iter<'a, (Symbol, NormalizedPat<'a>)>, + ); + impl<'a> Iterator for Iter<'a> { + type Item = (&'a NormalizedPat<'a>, &'a NormalizedPat<'a>); + fn next(&mut self) -> Option<Self::Item> { + // Note: all the fields in each slice are sorted by symbol value. + let mut left = self.0.next()?; + let mut right = self.1.next()?; + loop { + match left.0.cmp(&right.0) { + Ordering::Equal => return Some((&left.1, &right.1)), + Ordering::Less => left = self.0.next()?, + Ordering::Greater => right = self.1.next()?, + } + } + } + } + Iter(left.iter(), right.iter()) +} + +#[expect(clippy::similar_names)] +impl<'a> NormalizedPat<'a> { + #[expect(clippy::too_many_lines)] + fn from_pat(cx: &LateContext<'_>, arena: &'a DroplessArena, pat: &'a Pat<'_>) -> Self { + match pat.kind { + PatKind::Wild | PatKind::Binding(.., None) => Self::Wild, + PatKind::Binding(.., Some(pat)) | PatKind::Box(pat) | PatKind::Ref(pat, _) => { + Self::from_pat(cx, arena, pat) + }, + PatKind::Struct(ref path, fields, _) => { + let fields = + arena.alloc_from_iter(fields.iter().map(|f| (f.ident.name, Self::from_pat(cx, arena, f.pat)))); + fields.sort_by_key(|&(name, _)| name); + Self::Struct(cx.qpath_res(path, pat.hir_id).opt_def_id(), fields) + }, + PatKind::TupleStruct(ref path, pats, wild_idx) => { + let adt = match cx.typeck_results().pat_ty(pat).ty_adt_def() { + Some(x) => x, + None => return Self::Wild, + }; + let (var_id, variant) = if adt.is_enum() { + match cx.qpath_res(path, pat.hir_id).opt_def_id() { + Some(x) => (Some(x), adt.variant_with_ctor_id(x)), + None => return Self::Wild, + } + } else { + (None, adt.non_enum_variant()) + }; + let (front, back) = match wild_idx { + Some(i) => pats.split_at(i), + None => (pats, [].as_slice()), + }; + let pats = arena.alloc_from_iter( + front + .iter() + .map(|pat| Self::from_pat(cx, arena, pat)) + .chain(iter::repeat_with(|| Self::Wild).take(variant.fields.len() - pats.len())) + .chain(back.iter().map(|pat| Self::from_pat(cx, arena, pat))), + ); + Self::Tuple(var_id, pats) + }, + PatKind::Or(pats) => Self::Or(arena.alloc_from_iter(pats.iter().map(|pat| Self::from_pat(cx, arena, pat)))), + PatKind::Path(ref path) => Self::Path(cx.qpath_res(path, pat.hir_id).opt_def_id()), + PatKind::Tuple(pats, wild_idx) => { + let field_count = match cx.typeck_results().pat_ty(pat).kind() { + ty::Tuple(subs) => subs.len(), + _ => return Self::Wild, + }; + let (front, back) = match wild_idx { + Some(i) => pats.split_at(i), + None => (pats, [].as_slice()), + }; + let pats = arena.alloc_from_iter( + front + .iter() + .map(|pat| Self::from_pat(cx, arena, pat)) + .chain(iter::repeat_with(|| Self::Wild).take(field_count - pats.len())) + .chain(back.iter().map(|pat| Self::from_pat(cx, arena, pat))), + ); + Self::Tuple(None, pats) + }, + PatKind::Lit(e) => match &e.kind { + // TODO: Handle negative integers. They're currently treated as a wild match. + ExprKind::Lit(lit) => match lit.node { + LitKind::Str(sym, _) => Self::LitStr(sym), + LitKind::ByteStr(ref bytes) => Self::LitBytes(bytes), + LitKind::Byte(val) => Self::LitInt(val.into()), + LitKind::Char(val) => Self::LitInt(val.into()), + LitKind::Int(val, _) => Self::LitInt(val), + LitKind::Bool(val) => Self::LitBool(val), + LitKind::Float(..) | LitKind::Err(_) => Self::Wild, + }, + _ => Self::Wild, + }, + PatKind::Range(start, end, bounds) => { + // TODO: Handle negative integers. They're currently treated as a wild match. + let start = match start { + None => 0, + Some(e) => match &e.kind { + ExprKind::Lit(lit) => match lit.node { + LitKind::Int(val, _) => val, + LitKind::Char(val) => val.into(), + LitKind::Byte(val) => val.into(), + _ => return Self::Wild, + }, + _ => return Self::Wild, + }, + }; + let (end, bounds) = match end { + None => (u128::MAX, RangeEnd::Included), + Some(e) => match &e.kind { + ExprKind::Lit(lit) => match lit.node { + LitKind::Int(val, _) => (val, bounds), + LitKind::Char(val) => (val.into(), bounds), + LitKind::Byte(val) => (val.into(), bounds), + _ => return Self::Wild, + }, + _ => return Self::Wild, + }, + }; + Self::Range(PatRange { start, end, bounds }) + }, + PatKind::Slice(front, wild_pat, back) => Self::Slice( + arena.alloc_from_iter(front.iter().map(|pat| Self::from_pat(cx, arena, pat))), + wild_pat.map(|_| &*arena.alloc_from_iter(back.iter().map(|pat| Self::from_pat(cx, arena, pat)))), + ), + } + } + + /// Checks if two patterns overlap in the values they can match assuming they are for the same + /// type. + fn has_overlapping_values(&self, other: &Self) -> bool { + match (*self, *other) { + (Self::Wild, _) | (_, Self::Wild) => true, + (Self::Or(pats), ref other) | (ref other, Self::Or(pats)) => { + pats.iter().any(|pat| pat.has_overlapping_values(other)) + }, + (Self::Struct(lpath, lfields), Self::Struct(rpath, rfields)) => { + if lpath != rpath { + return false; + } + iter_matching_struct_fields(lfields, rfields).all(|(lpat, rpat)| lpat.has_overlapping_values(rpat)) + }, + (Self::Tuple(lpath, lpats), Self::Tuple(rpath, rpats)) => { + if lpath != rpath { + return false; + } + lpats + .iter() + .zip(rpats.iter()) + .all(|(lpat, rpat)| lpat.has_overlapping_values(rpat)) + }, + (Self::Path(x), Self::Path(y)) => x == y, + (Self::LitStr(x), Self::LitStr(y)) => x == y, + (Self::LitBytes(x), Self::LitBytes(y)) => x == y, + (Self::LitInt(x), Self::LitInt(y)) => x == y, + (Self::LitBool(x), Self::LitBool(y)) => x == y, + (Self::Range(ref x), Self::Range(ref y)) => x.overlaps(y), + (Self::Range(ref range), Self::LitInt(x)) | (Self::LitInt(x), Self::Range(ref range)) => range.contains(x), + (Self::Slice(lpats, None), Self::Slice(rpats, None)) => { + lpats.len() == rpats.len() && lpats.iter().zip(rpats.iter()).all(|(x, y)| x.has_overlapping_values(y)) + }, + (Self::Slice(pats, None), Self::Slice(front, Some(back))) + | (Self::Slice(front, Some(back)), Self::Slice(pats, None)) => { + // Here `pats` is an exact size match. If the combined lengths of `front` and `back` are greater + // then the minimum length required will be greater than the length of `pats`. + if pats.len() < front.len() + back.len() { + return false; + } + pats[..front.len()] + .iter() + .zip(front.iter()) + .chain(pats[pats.len() - back.len()..].iter().zip(back.iter())) + .all(|(x, y)| x.has_overlapping_values(y)) + }, + (Self::Slice(lfront, Some(lback)), Self::Slice(rfront, Some(rback))) => lfront + .iter() + .zip(rfront.iter()) + .chain(lback.iter().rev().zip(rback.iter().rev())) + .all(|(x, y)| x.has_overlapping_values(y)), + + // Enums can mix unit variants with tuple/struct variants. These can never overlap. + (Self::Path(_), Self::Tuple(..) | Self::Struct(..)) + | (Self::Tuple(..) | Self::Struct(..), Self::Path(_)) => false, + + // Tuples can be matched like a struct. + (Self::Tuple(x, _), Self::Struct(y, _)) | (Self::Struct(x, _), Self::Tuple(y, _)) => { + // TODO: check fields here. + x == y + }, + + // TODO: Lit* with Path, Range with Path, LitBytes with Slice + _ => true, + } + } +} + +fn pat_contains_local(pat: &Pat<'_>, id: HirId) -> bool { + let mut result = false; + pat.walk_short(|p| { + result |= matches!(p.kind, PatKind::Binding(_, binding_id, ..) if binding_id == id); + !result + }); + result +} + +/// Returns true if all the bindings in the `Pat` are in `ids` and vice versa +fn bindings_eq(pat: &Pat<'_>, mut ids: HirIdSet) -> bool { + let mut result = true; + pat.each_binding_or_first(&mut |_, id, _, _| result &= ids.remove(&id)); + result && ids.is_empty() +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_single_binding.rs b/src/tools/clippy/clippy_lints/src/matches/match_single_binding.rs new file mode 100644 index 000000000..5ae4a65ac --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_single_binding.rs @@ -0,0 +1,216 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::HirNode; +use clippy_utils::source::{indent_of, snippet, snippet_block, snippet_with_applicability}; +use clippy_utils::sugg::Sugg; +use clippy_utils::{get_parent_expr, is_refutable, peel_blocks}; +use rustc_errors::Applicability; +use rustc_hir::{Arm, Expr, ExprKind, Node, PatKind}; +use rustc_lint::LateContext; +use rustc_span::Span; + +use super::MATCH_SINGLE_BINDING; + +enum AssignmentExpr { + Assign { span: Span, match_span: Span }, + Local { span: Span, pat_span: Span }, +} + +#[expect(clippy::too_many_lines)] +pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], expr: &Expr<'a>) { + if expr.span.from_expansion() || arms.len() != 1 || is_refutable(cx, arms[0].pat) { + return; + } + + let matched_vars = ex.span; + let bind_names = arms[0].pat.span; + let match_body = peel_blocks(arms[0].body); + let mut snippet_body = if match_body.span.from_expansion() { + Sugg::hir_with_macro_callsite(cx, match_body, "..").to_string() + } else { + snippet_block(cx, match_body.span, "..", Some(expr.span)).to_string() + }; + + // Do we need to add ';' to suggestion ? + match match_body.kind { + ExprKind::Block(block, _) => { + // macro + expr_ty(body) == () + if block.span.from_expansion() && cx.typeck_results().expr_ty(match_body).is_unit() { + snippet_body.push(';'); + } + }, + _ => { + // expr_ty(body) == () + if cx.typeck_results().expr_ty(match_body).is_unit() { + snippet_body.push(';'); + } + }, + } + + let mut applicability = Applicability::MaybeIncorrect; + match arms[0].pat.kind { + PatKind::Binding(..) | PatKind::Tuple(_, _) | PatKind::Struct(..) => { + let (target_span, sugg) = match opt_parent_assign_span(cx, ex) { + Some(AssignmentExpr::Assign { span, match_span }) => { + let sugg = sugg_with_curlies( + cx, + (ex, expr), + (bind_names, matched_vars), + &snippet_body, + &mut applicability, + Some(span), + ); + + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + span.to(match_span), + "this assignment could be simplified", + "consider removing the `match` expression", + sugg, + applicability, + ); + + return; + }, + Some(AssignmentExpr::Local { span, pat_span }) => ( + span, + format!( + "let {} = {};\n{}let {} = {};", + snippet_with_applicability(cx, bind_names, "..", &mut applicability), + snippet_with_applicability(cx, matched_vars, "..", &mut applicability), + " ".repeat(indent_of(cx, expr.span).unwrap_or(0)), + snippet_with_applicability(cx, pat_span, "..", &mut applicability), + snippet_body + ), + ), + None => { + let sugg = sugg_with_curlies( + cx, + (ex, expr), + (bind_names, matched_vars), + &snippet_body, + &mut applicability, + None, + ); + (expr.span, sugg) + }, + }; + + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + target_span, + "this match could be written as a `let` statement", + "consider using a `let` statement", + sugg, + applicability, + ); + }, + PatKind::Wild => { + if ex.can_have_side_effects() { + let indent = " ".repeat(indent_of(cx, expr.span).unwrap_or(0)); + let sugg = format!( + "{};\n{}{}", + snippet_with_applicability(cx, ex.span, "..", &mut applicability), + indent, + snippet_body + ); + + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + expr.span, + "this match could be replaced by its scrutinee and body", + "consider using the scrutinee and body instead", + sugg, + applicability, + ); + } else { + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + expr.span, + "this match could be replaced by its body itself", + "consider using the match body instead", + snippet_body, + Applicability::MachineApplicable, + ); + } + }, + _ => (), + } +} + +/// Returns true if the `ex` match expression is in a local (`let`) or assign expression +fn opt_parent_assign_span<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<AssignmentExpr> { + let map = &cx.tcx.hir(); + + if let Some(Node::Expr(parent_arm_expr)) = map.find(map.get_parent_node(ex.hir_id)) { + return match map.find(map.get_parent_node(parent_arm_expr.hir_id)) { + Some(Node::Local(parent_let_expr)) => Some(AssignmentExpr::Local { + span: parent_let_expr.span, + pat_span: parent_let_expr.pat.span(), + }), + Some(Node::Expr(Expr { + kind: ExprKind::Assign(parent_assign_expr, match_expr, _), + .. + })) => Some(AssignmentExpr::Assign { + span: parent_assign_expr.span, + match_span: match_expr.span, + }), + _ => None, + }; + } + + None +} + +fn sugg_with_curlies<'a>( + cx: &LateContext<'a>, + (ex, match_expr): (&Expr<'a>, &Expr<'a>), + (bind_names, matched_vars): (Span, Span), + snippet_body: &str, + applicability: &mut Applicability, + assignment: Option<Span>, +) -> String { + let mut indent = " ".repeat(indent_of(cx, ex.span).unwrap_or(0)); + + let (mut cbrace_start, mut cbrace_end) = (String::new(), String::new()); + if let Some(parent_expr) = get_parent_expr(cx, match_expr) { + if let ExprKind::Closure { .. } = parent_expr.kind { + cbrace_end = format!("\n{}}}", indent); + // Fix body indent due to the closure + indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); + cbrace_start = format!("{{\n{}", indent); + } + } + + // If the parent is already an arm, and the body is another match statement, + // we need curly braces around suggestion + let parent_node_id = cx.tcx.hir().get_parent_node(match_expr.hir_id); + if let Node::Arm(arm) = &cx.tcx.hir().get(parent_node_id) { + if let ExprKind::Match(..) = arm.body.kind { + cbrace_end = format!("\n{}}}", indent); + // Fix body indent due to the match + indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); + cbrace_start = format!("{{\n{}", indent); + } + } + + let assignment_str = assignment.map_or_else(String::new, |span| { + let mut s = snippet(cx, span, "..").to_string(); + s.push_str(" = "); + s + }); + + format!( + "{}let {} = {};\n{}{}{}{}", + cbrace_start, + snippet_with_applicability(cx, bind_names, "..", applicability), + snippet_with_applicability(cx, matched_vars, "..", applicability), + indent, + assignment_str, + snippet_body, + cbrace_end + ) +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_str_case_mismatch.rs b/src/tools/clippy/clippy_lints/src/matches/match_str_case_mismatch.rs new file mode 100644 index 000000000..fa3b8d1fc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_str_case_mismatch.rs @@ -0,0 +1,125 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{Arm, Expr, ExprKind, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::symbol::Symbol; +use rustc_span::{sym, Span}; + +use super::MATCH_STR_CASE_MISMATCH; + +#[derive(Debug)] +enum CaseMethod { + LowerCase, + AsciiLowerCase, + UpperCase, + AsciiUppercase, +} + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, scrutinee: &'tcx Expr<'_>, arms: &'tcx [Arm<'_>]) { + if_chain! { + if let ty::Ref(_, ty, _) = cx.typeck_results().expr_ty(scrutinee).kind(); + if let ty::Str = ty.kind(); + then { + let mut visitor = MatchExprVisitor { + cx, + case_method: None, + }; + + visitor.visit_expr(scrutinee); + + if let Some(case_method) = visitor.case_method { + if let Some((bad_case_span, bad_case_sym)) = verify_case(&case_method, arms) { + lint(cx, &case_method, bad_case_span, bad_case_sym.as_str()); + } + } + } + } +} + +struct MatchExprVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + case_method: Option<CaseMethod>, +} + +impl<'a, 'tcx> Visitor<'tcx> for MatchExprVisitor<'a, 'tcx> { + fn visit_expr(&mut self, ex: &'tcx Expr<'_>) { + match ex.kind { + ExprKind::MethodCall(segment, [receiver], _) if self.case_altered(segment.ident.as_str(), receiver) => {}, + _ => walk_expr(self, ex), + } + } +} + +impl<'a, 'tcx> MatchExprVisitor<'a, 'tcx> { + fn case_altered(&mut self, segment_ident: &str, receiver: &Expr<'_>) -> bool { + if let Some(case_method) = get_case_method(segment_ident) { + let ty = self.cx.typeck_results().expr_ty(receiver).peel_refs(); + + if is_type_diagnostic_item(self.cx, ty, sym::String) || ty.kind() == &ty::Str { + self.case_method = Some(case_method); + return true; + } + } + + false + } +} + +fn get_case_method(segment_ident_str: &str) -> Option<CaseMethod> { + match segment_ident_str { + "to_lowercase" => Some(CaseMethod::LowerCase), + "to_ascii_lowercase" => Some(CaseMethod::AsciiLowerCase), + "to_uppercase" => Some(CaseMethod::UpperCase), + "to_ascii_uppercase" => Some(CaseMethod::AsciiUppercase), + _ => None, + } +} + +fn verify_case<'a>(case_method: &'a CaseMethod, arms: &'a [Arm<'_>]) -> Option<(Span, Symbol)> { + let case_check = match case_method { + CaseMethod::LowerCase => |input: &str| -> bool { input.chars().all(|c| c.to_lowercase().next() == Some(c)) }, + CaseMethod::AsciiLowerCase => |input: &str| -> bool { !input.chars().any(|c| c.is_ascii_uppercase()) }, + CaseMethod::UpperCase => |input: &str| -> bool { input.chars().all(|c| c.to_uppercase().next() == Some(c)) }, + CaseMethod::AsciiUppercase => |input: &str| -> bool { !input.chars().any(|c| c.is_ascii_lowercase()) }, + }; + + for arm in arms { + if_chain! { + if let PatKind::Lit(Expr { + kind: ExprKind::Lit(lit), + .. + }) = arm.pat.kind; + if let LitKind::Str(symbol, _) = lit.node; + let input = symbol.as_str(); + if !case_check(input); + then { + return Some((lit.span, symbol)); + } + } + } + + None +} + +fn lint(cx: &LateContext<'_>, case_method: &CaseMethod, bad_case_span: Span, bad_case_str: &str) { + let (method_str, suggestion) = match case_method { + CaseMethod::LowerCase => ("to_lowercase", bad_case_str.to_lowercase()), + CaseMethod::AsciiLowerCase => ("to_ascii_lowercase", bad_case_str.to_ascii_lowercase()), + CaseMethod::UpperCase => ("to_uppercase", bad_case_str.to_uppercase()), + CaseMethod::AsciiUppercase => ("to_ascii_uppercase", bad_case_str.to_ascii_uppercase()), + }; + + span_lint_and_sugg( + cx, + MATCH_STR_CASE_MISMATCH, + bad_case_span, + "this `match` arm has a differing case than its expression", + &format!("consider changing the case of this arm to respect `{}`", method_str), + format!("\"{}\"", suggestion), + Applicability::MachineApplicable, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs b/src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs new file mode 100644 index 000000000..6f8d766ae --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs @@ -0,0 +1,196 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{is_refutable, peel_hir_pat_refs, recurse_or_patterns}; +use rustc_errors::Applicability; +use rustc_hir::def::{CtorKind, DefKind, Res}; +use rustc_hir::{Arm, Expr, PatKind, PathSegment, QPath, Ty, TyKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, VariantDef}; +use rustc_span::sym; + +use super::{MATCH_WILDCARD_FOR_SINGLE_VARIANTS, WILDCARD_ENUM_MATCH_ARM}; + +#[expect(clippy::too_many_lines)] +pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) { + let ty = cx.typeck_results().expr_ty(ex).peel_refs(); + let adt_def = match ty.kind() { + ty::Adt(adt_def, _) + if adt_def.is_enum() + && !(is_type_diagnostic_item(cx, ty, sym::Option) || is_type_diagnostic_item(cx, ty, sym::Result)) => + { + adt_def + }, + _ => return, + }; + + // First pass - check for violation, but don't do much book-keeping because this is hopefully + // the uncommon case, and the book-keeping is slightly expensive. + let mut wildcard_span = None; + let mut wildcard_ident = None; + let mut has_non_wild = false; + for arm in arms { + match peel_hir_pat_refs(arm.pat).0.kind { + PatKind::Wild => wildcard_span = Some(arm.pat.span), + PatKind::Binding(_, _, ident, None) => { + wildcard_span = Some(arm.pat.span); + wildcard_ident = Some(ident); + }, + _ => has_non_wild = true, + } + } + let wildcard_span = match wildcard_span { + Some(x) if has_non_wild => x, + _ => return, + }; + + // Accumulate the variants which should be put in place of the wildcard because they're not + // already covered. + let has_hidden = adt_def.variants().iter().any(|x| is_hidden(cx, x)); + let mut missing_variants: Vec<_> = adt_def.variants().iter().filter(|x| !is_hidden(cx, x)).collect(); + + let mut path_prefix = CommonPrefixSearcher::None; + for arm in arms { + // Guards mean that this case probably isn't exhaustively covered. Technically + // this is incorrect, as we should really check whether each variant is exhaustively + // covered by the set of guards that cover it, but that's really hard to do. + recurse_or_patterns(arm.pat, |pat| { + let path = match &peel_hir_pat_refs(pat).0.kind { + PatKind::Path(path) => { + let id = match cx.qpath_res(path, pat.hir_id) { + Res::Def( + DefKind::Const | DefKind::ConstParam | DefKind::AnonConst | DefKind::InlineConst, + _, + ) => return, + Res::Def(_, id) => id, + _ => return, + }; + if arm.guard.is_none() { + missing_variants.retain(|e| e.ctor_def_id != Some(id)); + } + path + }, + PatKind::TupleStruct(path, patterns, ..) => { + if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() { + if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p)) { + missing_variants.retain(|e| e.ctor_def_id != Some(id)); + } + } + path + }, + PatKind::Struct(path, patterns, ..) => { + if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() { + if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p.pat)) { + missing_variants.retain(|e| e.def_id != id); + } + } + path + }, + _ => return, + }; + match path { + QPath::Resolved(_, path) => path_prefix.with_path(path.segments), + QPath::TypeRelative( + Ty { + kind: TyKind::Path(QPath::Resolved(_, path)), + .. + }, + _, + ) => path_prefix.with_prefix(path.segments), + _ => (), + } + }); + } + + let format_suggestion = |variant: &VariantDef| { + format!( + "{}{}{}{}", + if let Some(ident) = wildcard_ident { + format!("{} @ ", ident.name) + } else { + String::new() + }, + if let CommonPrefixSearcher::Path(path_prefix) = path_prefix { + let mut s = String::new(); + for seg in path_prefix { + s.push_str(seg.ident.as_str()); + s.push_str("::"); + } + s + } else { + let mut s = cx.tcx.def_path_str(adt_def.did()); + s.push_str("::"); + s + }, + variant.name, + match variant.ctor_kind { + CtorKind::Fn if variant.fields.len() == 1 => "(_)", + CtorKind::Fn => "(..)", + CtorKind::Const => "", + CtorKind::Fictive => "{ .. }", + } + ) + }; + + match missing_variants.as_slice() { + [] => (), + [x] if !adt_def.is_variant_list_non_exhaustive() && !has_hidden => span_lint_and_sugg( + cx, + MATCH_WILDCARD_FOR_SINGLE_VARIANTS, + wildcard_span, + "wildcard matches only a single variant and will also match any future added variants", + "try this", + format_suggestion(x), + Applicability::MaybeIncorrect, + ), + variants => { + let mut suggestions: Vec<_> = variants.iter().copied().map(format_suggestion).collect(); + let message = if adt_def.is_variant_list_non_exhaustive() || has_hidden { + suggestions.push("_".into()); + "wildcard matches known variants and will also match future added variants" + } else { + "wildcard match will also match any future added variants" + }; + + span_lint_and_sugg( + cx, + WILDCARD_ENUM_MATCH_ARM, + wildcard_span, + message, + "try this", + suggestions.join(" | "), + Applicability::MaybeIncorrect, + ); + }, + }; +} + +enum CommonPrefixSearcher<'a> { + None, + Path(&'a [PathSegment<'a>]), + Mixed, +} +impl<'a> CommonPrefixSearcher<'a> { + fn with_path(&mut self, path: &'a [PathSegment<'a>]) { + match path { + [path @ .., _] => self.with_prefix(path), + [] => (), + } + } + + fn with_prefix(&mut self, path: &'a [PathSegment<'a>]) { + match self { + Self::None => *self = Self::Path(path), + Self::Path(self_path) + if path + .iter() + .map(|p| p.ident.name) + .eq(self_path.iter().map(|p| p.ident.name)) => {}, + Self::Path(_) => *self = Self::Mixed, + Self::Mixed => (), + } + } +} + +fn is_hidden(cx: &LateContext<'_>, variant_def: &VariantDef) -> bool { + cx.tcx.is_doc_hidden(variant_def.def_id) || cx.tcx.has_attr(variant_def.def_id, sym::unstable) +} diff --git a/src/tools/clippy/clippy_lints/src/matches/match_wild_err_arm.rs b/src/tools/clippy/clippy_lints/src/matches/match_wild_err_arm.rs new file mode 100644 index 000000000..bc16f17b6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/match_wild_err_arm.rs @@ -0,0 +1,51 @@ +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::macros::{is_panic, root_macro_call}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::visitors::is_local_used; +use clippy_utils::{is_wild, peel_blocks_with_stmt}; +use rustc_hir::{Arm, Expr, PatKind}; +use rustc_lint::LateContext; +use rustc_span::symbol::{kw, sym}; + +use super::MATCH_WILD_ERR_ARM; + +pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, ex: &Expr<'tcx>, arms: &[Arm<'tcx>]) { + let ex_ty = cx.typeck_results().expr_ty(ex).peel_refs(); + if is_type_diagnostic_item(cx, ex_ty, sym::Result) { + for arm in arms { + if let PatKind::TupleStruct(ref path, inner, _) = arm.pat.kind { + let path_str = rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false)); + if path_str == "Err" { + let mut matching_wild = inner.iter().any(is_wild); + let mut ident_bind_name = kw::Underscore; + if !matching_wild { + // Looking for unused bindings (i.e.: `_e`) + for pat in inner.iter() { + 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; + matching_wild = true; + } + } + } + } + if_chain! { + if matching_wild; + if let Some(macro_call) = root_macro_call(peel_blocks_with_stmt(arm.body).span); + if is_panic(cx, macro_call.def_id); + then { + // `Err(_)` or `Err(_e)` arm with `panic!` found + span_lint_and_note(cx, + MATCH_WILD_ERR_ARM, + arm.pat.span, + &format!("`Err({})` matches all errors", ident_bind_name), + None, + "match each error separately or use the error output, or use `.except(msg)` if the error case is unreachable", + ); + } + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/mod.rs b/src/tools/clippy/clippy_lints/src/matches/mod.rs new file mode 100644 index 000000000..eba230e5a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/mod.rs @@ -0,0 +1,1134 @@ +mod collapsible_match; +mod infallible_destructuring_match; +mod manual_map; +mod manual_unwrap_or; +mod match_as_ref; +mod match_bool; +mod match_like_matches; +mod match_on_vec_items; +mod match_ref_pats; +mod match_same_arms; +mod match_single_binding; +mod match_str_case_mismatch; +mod match_wild_enum; +mod match_wild_err_arm; +mod needless_match; +mod overlapping_arms; +mod redundant_pattern_match; +mod rest_pat_in_fully_bound_struct; +mod significant_drop_in_scrutinee; +mod single_match; +mod try_err; +mod wild_in_or_pats; + +use clippy_utils::source::{snippet_opt, span_starts_with, walk_span_to_context}; +use clippy_utils::{higher, in_constant, meets_msrv, msrvs}; +use rustc_hir::{Arm, Expr, ExprKind, Local, MatchSource, Pat}; +use rustc_lexer::{tokenize, TokenKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{Span, SpanData, SyntaxContext}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches with a single arm where an `if let` + /// will usually suffice. + /// + /// ### Why is this bad? + /// Just readability – `if let` nests less than a `match`. + /// + /// ### Example + /// ```rust + /// # fn bar(stool: &str) {} + /// # let x = Some("abc"); + /// match x { + /// Some(ref foo) => bar(foo), + /// _ => (), + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # fn bar(stool: &str) {} + /// # let x = Some("abc"); + /// if let Some(ref foo) = x { + /// bar(foo); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SINGLE_MATCH, + style, + "a `match` statement with a single nontrivial arm (i.e., where the other arm is `_ => {}`) instead of `if let`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches with two arms where an `if let else` will + /// usually suffice. + /// + /// ### Why is this bad? + /// Just readability – `if let` nests less than a `match`. + /// + /// ### Known problems + /// Personal style preferences may differ. + /// + /// ### Example + /// Using `match`: + /// + /// ```rust + /// # fn bar(foo: &usize) {} + /// # let other_ref: usize = 1; + /// # let x: Option<&usize> = Some(&1); + /// match x { + /// Some(ref foo) => bar(foo), + /// _ => bar(&other_ref), + /// } + /// ``` + /// + /// Using `if let` with `else`: + /// + /// ```rust + /// # fn bar(foo: &usize) {} + /// # let other_ref: usize = 1; + /// # let x: Option<&usize> = Some(&1); + /// if let Some(ref foo) = x { + /// bar(foo); + /// } else { + /// bar(&other_ref); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SINGLE_MATCH_ELSE, + pedantic, + "a `match` statement with two arms where the second arm's pattern is a placeholder instead of a specific match pattern" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches where all arms match a reference, + /// suggesting to remove the reference and deref the matched expression + /// instead. It also checks for `if let &foo = bar` blocks. + /// + /// ### Why is this bad? + /// It just makes the code less readable. That reference + /// destructuring adds nothing to the code. + /// + /// ### Example + /// ```rust,ignore + /// match x { + /// &A(ref y) => foo(y), + /// &B => bar(), + /// _ => frob(&x), + /// } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// match *x { + /// A(ref y) => foo(y), + /// B => bar(), + /// _ => frob(x), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_REF_PATS, + style, + "a `match` or `if let` with all arms prefixed with `&` instead of deref-ing the match expression" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches where match expression is a `bool`. It + /// suggests to replace the expression with an `if...else` block. + /// + /// ### Why is this bad? + /// It makes the code less readable. + /// + /// ### Example + /// ```rust + /// # fn foo() {} + /// # fn bar() {} + /// let condition: bool = true; + /// match condition { + /// true => foo(), + /// false => bar(), + /// } + /// ``` + /// Use if/else instead: + /// ```rust + /// # fn foo() {} + /// # fn bar() {} + /// let condition: bool = true; + /// if condition { + /// foo(); + /// } else { + /// bar(); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_BOOL, + pedantic, + "a `match` on a boolean expression instead of an `if..else` block" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for overlapping match arms. + /// + /// ### Why is this bad? + /// It is likely to be an error and if not, makes the code + /// less obvious. + /// + /// ### Example + /// ```rust + /// let x = 5; + /// match x { + /// 1..=10 => println!("1 ... 10"), + /// 5..=15 => println!("5 ... 15"), + /// _ => (), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_OVERLAPPING_ARM, + style, + "a `match` with overlapping arms" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for arm which matches all errors with `Err(_)` + /// and take drastic actions like `panic!`. + /// + /// ### Why is this bad? + /// It is generally a bad practice, similar to + /// catching all exceptions in java with `catch(Exception)` + /// + /// ### Example + /// ```rust + /// let x: Result<i32, &str> = Ok(3); + /// match x { + /// Ok(_) => println!("ok"), + /// Err(_) => panic!("err"), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_WILD_ERR_ARM, + pedantic, + "a `match` with `Err(_)` arm and take drastic actions" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for match which is used to add a reference to an + /// `Option` value. + /// + /// ### Why is this bad? + /// Using `as_ref()` or `as_mut()` instead is shorter. + /// + /// ### Example + /// ```rust + /// let x: Option<()> = None; + /// + /// let r: Option<&()> = match x { + /// None => None, + /// Some(ref v) => Some(v), + /// }; + /// ``` + /// + /// Use instead: + /// ```rust + /// let x: Option<()> = None; + /// + /// let r: Option<&()> = x.as_ref(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_AS_REF, + complexity, + "a `match` on an Option value instead of using `as_ref()` or `as_mut`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard enum matches using `_`. + /// + /// ### Why is this bad? + /// New enum variants added by library updates can be missed. + /// + /// ### Known problems + /// Suggested replacements may be incorrect if guards exhaustively cover some + /// variants, and also may not use correct path to enum if it's not present in the current scope. + /// + /// ### Example + /// ```rust + /// # enum Foo { A(usize), B(usize) } + /// # let x = Foo::B(1); + /// match x { + /// Foo::A(_) => {}, + /// _ => {}, + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # enum Foo { A(usize), B(usize) } + /// # let x = Foo::B(1); + /// match x { + /// Foo::A(_) => {}, + /// Foo::B(_) => {}, + /// } + /// ``` + #[clippy::version = "1.34.0"] + pub WILDCARD_ENUM_MATCH_ARM, + restriction, + "a wildcard enum match arm using `_`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard enum matches for a single variant. + /// + /// ### Why is this bad? + /// New enum variants added by library updates can be missed. + /// + /// ### Known problems + /// Suggested replacements may not use correct path to enum + /// if it's not present in the current scope. + /// + /// ### Example + /// ```rust + /// # enum Foo { A, B, C } + /// # let x = Foo::B; + /// match x { + /// Foo::A => {}, + /// Foo::B => {}, + /// _ => {}, + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # enum Foo { A, B, C } + /// # let x = Foo::B; + /// match x { + /// Foo::A => {}, + /// Foo::B => {}, + /// Foo::C => {}, + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub MATCH_WILDCARD_FOR_SINGLE_VARIANTS, + pedantic, + "a wildcard enum match for a single variant" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard pattern used with others patterns in same match arm. + /// + /// ### Why is this bad? + /// Wildcard pattern already covers any other pattern as it will match anyway. + /// It makes the code less readable, especially to spot wildcard pattern use in match arm. + /// + /// ### Example + /// ```rust + /// # let s = "foo"; + /// match s { + /// "a" => {}, + /// "bar" | _ => {}, + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let s = "foo"; + /// match s { + /// "a" => {}, + /// _ => {}, + /// } + /// ``` + #[clippy::version = "1.42.0"] + pub WILDCARD_IN_OR_PATTERNS, + complexity, + "a wildcard pattern used with others patterns in same match arm" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches being used to destructure a single-variant enum + /// or tuple struct where a `let` will suffice. + /// + /// ### Why is this bad? + /// Just readability – `let` doesn't nest, whereas a `match` does. + /// + /// ### Example + /// ```rust + /// enum Wrapper { + /// Data(i32), + /// } + /// + /// let wrapper = Wrapper::Data(42); + /// + /// let data = match wrapper { + /// Wrapper::Data(i) => i, + /// }; + /// ``` + /// + /// The correct use would be: + /// ```rust + /// enum Wrapper { + /// Data(i32), + /// } + /// + /// let wrapper = Wrapper::Data(42); + /// let Wrapper::Data(data) = wrapper; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INFALLIBLE_DESTRUCTURING_MATCH, + style, + "a `match` statement with a single infallible arm instead of a `let`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for useless match that binds to only one value. + /// + /// ### Why is this bad? + /// Readability and needless complexity. + /// + /// ### Known problems + /// Suggested replacements may be incorrect when `match` + /// is actually binding temporary value, bringing a 'dropped while borrowed' error. + /// + /// ### Example + /// ```rust + /// # let a = 1; + /// # let b = 2; + /// match (a, b) { + /// (c, d) => { + /// // useless match + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let a = 1; + /// # let b = 2; + /// let (c, d) = (a, b); + /// ``` + #[clippy::version = "1.43.0"] + pub MATCH_SINGLE_BINDING, + complexity, + "a match with a single binding instead of using `let` statement" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary '..' pattern binding on struct when all fields are explicitly matched. + /// + /// ### Why is this bad? + /// Correctness and readability. It's like having a wildcard pattern after + /// matching all enum variants explicitly. + /// + /// ### Example + /// ```rust + /// # struct A { a: i32 } + /// let a = A { a: 5 }; + /// + /// match a { + /// A { a: 5, .. } => {}, + /// _ => {}, + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # struct A { a: i32 } + /// # let a = A { a: 5 }; + /// match a { + /// A { a: 5 } => {}, + /// _ => {}, + /// } + /// ``` + #[clippy::version = "1.43.0"] + pub REST_PAT_IN_FULLY_BOUND_STRUCTS, + restriction, + "a match on a struct that binds all fields but still uses the wildcard pattern" +} + +declare_clippy_lint! { + /// ### What it does + /// Lint for redundant pattern matching over `Result`, `Option`, + /// `std::task::Poll` or `std::net::IpAddr` + /// + /// ### Why is this bad? + /// It's more concise and clear to just use the proper + /// utility function + /// + /// ### Known problems + /// This will change the drop order for the matched type. Both `if let` and + /// `while let` will drop the value at the end of the block, both `if` and `while` will drop the + /// value before entering the block. For most types this change will not matter, but for a few + /// types this will not be an acceptable change (e.g. locks). See the + /// [reference](https://doc.rust-lang.org/reference/destructors.html#drop-scopes) for more about + /// drop order. + /// + /// ### Example + /// ```rust + /// # use std::task::Poll; + /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + /// if let Ok(_) = Ok::<i32, i32>(42) {} + /// if let Err(_) = Err::<i32, i32>(42) {} + /// if let None = None::<()> {} + /// if let Some(_) = Some(42) {} + /// if let Poll::Pending = Poll::Pending::<()> {} + /// if let Poll::Ready(_) = Poll::Ready(42) {} + /// if let IpAddr::V4(_) = IpAddr::V4(Ipv4Addr::LOCALHOST) {} + /// if let IpAddr::V6(_) = IpAddr::V6(Ipv6Addr::LOCALHOST) {} + /// match Ok::<i32, i32>(42) { + /// Ok(_) => true, + /// Err(_) => false, + /// }; + /// ``` + /// + /// The more idiomatic use would be: + /// + /// ```rust + /// # use std::task::Poll; + /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + /// if Ok::<i32, i32>(42).is_ok() {} + /// if Err::<i32, i32>(42).is_err() {} + /// if None::<()>.is_none() {} + /// if Some(42).is_some() {} + /// if Poll::Pending::<()>.is_pending() {} + /// if Poll::Ready(42).is_ready() {} + /// if IpAddr::V4(Ipv4Addr::LOCALHOST).is_ipv4() {} + /// if IpAddr::V6(Ipv6Addr::LOCALHOST).is_ipv6() {} + /// Ok::<i32, i32>(42).is_ok(); + /// ``` + #[clippy::version = "1.31.0"] + pub REDUNDANT_PATTERN_MATCHING, + style, + "use the proper utility function avoiding an `if let`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `match` or `if let` expressions producing a + /// `bool` that could be written using `matches!` + /// + /// ### Why is this bad? + /// Readability and needless complexity. + /// + /// ### Known problems + /// This lint falsely triggers, if there are arms with + /// `cfg` attributes that remove an arm evaluating to `false`. + /// + /// ### Example + /// ```rust + /// let x = Some(5); + /// + /// let a = match x { + /// Some(0) => true, + /// _ => false, + /// }; + /// + /// let a = if let Some(0) = x { + /// true + /// } else { + /// false + /// }; + /// ``` + /// + /// Use instead: + /// ```rust + /// let x = Some(5); + /// let a = matches!(x, Some(0)); + /// ``` + #[clippy::version = "1.47.0"] + pub MATCH_LIKE_MATCHES_MACRO, + style, + "a match that could be written with the matches! macro" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `match` with identical arm bodies. + /// + /// ### Why is this bad? + /// This is probably a copy & paste error. If arm bodies + /// are the same on purpose, you can factor them + /// [using `|`](https://doc.rust-lang.org/book/patterns.html#multiple-patterns). + /// + /// ### Known problems + /// False positive possible with order dependent `match` + /// (see issue + /// [#860](https://github.com/rust-lang/rust-clippy/issues/860)). + /// + /// ### Example + /// ```rust,ignore + /// match foo { + /// Bar => bar(), + /// Quz => quz(), + /// Baz => bar(), // <= oops + /// } + /// ``` + /// + /// This should probably be + /// ```rust,ignore + /// match foo { + /// Bar => bar(), + /// Quz => quz(), + /// Baz => baz(), // <= fixed + /// } + /// ``` + /// + /// or if the original code was not a typo: + /// ```rust,ignore + /// match foo { + /// Bar | Baz => bar(), // <= shows the intent better + /// Quz => quz(), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_SAME_ARMS, + pedantic, + "`match` with identical arm bodies" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary `match` or match-like `if let` returns for `Option` and `Result` + /// when function signatures are the same. + /// + /// ### Why is this bad? + /// This `match` block does nothing and might not be what the coder intended. + /// + /// ### Example + /// ```rust,ignore + /// fn foo() -> Result<(), i32> { + /// match result { + /// Ok(val) => Ok(val), + /// Err(err) => Err(err), + /// } + /// } + /// + /// fn bar() -> Option<i32> { + /// if let Some(val) = option { + /// Some(val) + /// } else { + /// None + /// } + /// } + /// ``` + /// + /// Could be replaced as + /// + /// ```rust,ignore + /// fn foo() -> Result<(), i32> { + /// result + /// } + /// + /// fn bar() -> Option<i32> { + /// option + /// } + /// ``` + #[clippy::version = "1.61.0"] + pub NEEDLESS_MATCH, + complexity, + "`match` or match-like `if let` that are unnecessary" +} + +declare_clippy_lint! { + /// ### What it does + /// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together + /// without adding any branches. + /// + /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only + /// cases where merging would most likely make the code more readable. + /// + /// ### Why is this bad? + /// It is unnecessarily verbose and complex. + /// + /// ### Example + /// ```rust + /// fn func(opt: Option<Result<u64, String>>) { + /// let n = match opt { + /// Some(n) => match n { + /// Ok(n) => n, + /// _ => return, + /// } + /// None => return, + /// }; + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn func(opt: Option<Result<u64, String>>) { + /// let n = match opt { + /// Some(Ok(n)) => n, + /// _ => return, + /// }; + /// } + /// ``` + #[clippy::version = "1.50.0"] + pub COLLAPSIBLE_MATCH, + style, + "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together." +} + +declare_clippy_lint! { + /// ### What it does + /// Finds patterns that reimplement `Option::unwrap_or` or `Result::unwrap_or`. + /// + /// ### Why is this bad? + /// Concise code helps focusing on behavior instead of boilerplate. + /// + /// ### Example + /// ```rust + /// let foo: Option<i32> = None; + /// match foo { + /// Some(v) => v, + /// None => 1, + /// }; + /// ``` + /// + /// Use instead: + /// ```rust + /// let foo: Option<i32> = None; + /// foo.unwrap_or(1); + /// ``` + #[clippy::version = "1.49.0"] + pub MANUAL_UNWRAP_OR, + complexity, + "finds patterns that can be encoded more concisely with `Option::unwrap_or` or `Result::unwrap_or`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `match vec[idx]` or `match vec[n..m]`. + /// + /// ### Why is this bad? + /// This can panic at runtime. + /// + /// ### Example + /// ```rust, no_run + /// let arr = vec![0, 1, 2, 3]; + /// let idx = 1; + /// + /// match arr[idx] { + /// 0 => println!("{}", 0), + /// 1 => println!("{}", 3), + /// _ => {}, + /// } + /// ``` + /// + /// Use instead: + /// ```rust, no_run + /// let arr = vec![0, 1, 2, 3]; + /// let idx = 1; + /// + /// match arr.get(idx) { + /// Some(0) => println!("{}", 0), + /// Some(1) => println!("{}", 3), + /// _ => {}, + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub MATCH_ON_VEC_ITEMS, + pedantic, + "matching on vector elements can panic" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `match` expressions modifying the case of a string with non-compliant arms + /// + /// ### Why is this bad? + /// The arm is unreachable, which is likely a mistake + /// + /// ### Example + /// ```rust + /// # let text = "Foo"; + /// match &*text.to_ascii_lowercase() { + /// "foo" => {}, + /// "Bar" => {}, + /// _ => {}, + /// } + /// ``` + /// Use instead: + /// ```rust + /// # let text = "Foo"; + /// match &*text.to_ascii_lowercase() { + /// "foo" => {}, + /// "bar" => {}, + /// _ => {}, + /// } + /// ``` + #[clippy::version = "1.58.0"] + pub MATCH_STR_CASE_MISMATCH, + correctness, + "creation of a case altering match expression with non-compliant arms" +} + +declare_clippy_lint! { + /// ### What it does + /// Check for temporaries returned from function calls in a match scrutinee that have the + /// `clippy::has_significant_drop` attribute. + /// + /// ### Why is this bad? + /// The `clippy::has_significant_drop` attribute can be added to types whose Drop impls have + /// an important side-effect, such as unlocking a mutex, making it important for users to be + /// able to accurately understand their lifetimes. When a temporary is returned in a function + /// call in a match scrutinee, its lifetime lasts until the end of the match block, which may + /// be surprising. + /// + /// For `Mutex`es this can lead to a deadlock. This happens when the match scrutinee uses a + /// function call that returns a `MutexGuard` and then tries to lock again in one of the match + /// arms. In that case the `MutexGuard` in the scrutinee will not be dropped until the end of + /// the match block and thus will not unlock. + /// + /// ### Example + /// ```rust,ignore + /// # use std::sync::Mutex; + /// # struct State {} + /// # impl State { + /// # fn foo(&self) -> bool { + /// # true + /// # } + /// # fn bar(&self) {} + /// # } + /// let mutex = Mutex::new(State {}); + /// + /// match mutex.lock().unwrap().foo() { + /// true => { + /// mutex.lock().unwrap().bar(); // Deadlock! + /// } + /// false => {} + /// }; + /// + /// println!("All done!"); + /// ``` + /// Use instead: + /// ```rust + /// # use std::sync::Mutex; + /// # struct State {} + /// # impl State { + /// # fn foo(&self) -> bool { + /// # true + /// # } + /// # fn bar(&self) {} + /// # } + /// let mutex = Mutex::new(State {}); + /// + /// let is_foo = mutex.lock().unwrap().foo(); + /// match is_foo { + /// true => { + /// mutex.lock().unwrap().bar(); + /// } + /// false => {} + /// }; + /// + /// println!("All done!"); + /// ``` + #[clippy::version = "1.60.0"] + pub SIGNIFICANT_DROP_IN_SCRUTINEE, + nursery, + "warns when a temporary of a type with a drop with a significant side-effect might have a surprising lifetime" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `Err(x)?`. + /// + /// ### Why is this bad? + /// The `?` operator is designed to allow calls that + /// can fail to be easily chained. For example, `foo()?.bar()` or + /// `foo(bar()?)`. Because `Err(x)?` can't be used that way (it will + /// always return), it is more clear to write `return Err(x)`. + /// + /// ### Example + /// ```rust + /// fn foo(fail: bool) -> Result<i32, String> { + /// if fail { + /// Err("failed")?; + /// } + /// Ok(0) + /// } + /// ``` + /// Could be written: + /// + /// ```rust + /// fn foo(fail: bool) -> Result<i32, String> { + /// if fail { + /// return Err("failed".into()); + /// } + /// Ok(0) + /// } + /// ``` + #[clippy::version = "1.38.0"] + pub TRY_ERR, + restriction, + "return errors explicitly rather than hiding them behind a `?`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `match` which could be implemented using `map` + /// + /// ### Why is this bad? + /// Using the `map` method is clearer and more concise. + /// + /// ### Example + /// ```rust + /// match Some(0) { + /// Some(x) => Some(x + 1), + /// None => None, + /// }; + /// ``` + /// Use instead: + /// ```rust + /// Some(0).map(|x| x + 1); + /// ``` + #[clippy::version = "1.52.0"] + pub MANUAL_MAP, + style, + "reimplementation of `map`" +} + +#[derive(Default)] +pub struct Matches { + msrv: Option<RustcVersion>, + infallible_destructuring_match_linted: bool, +} + +impl Matches { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { + msrv, + ..Matches::default() + } + } +} + +impl_lint_pass!(Matches => [ + SINGLE_MATCH, + MATCH_REF_PATS, + MATCH_BOOL, + SINGLE_MATCH_ELSE, + MATCH_OVERLAPPING_ARM, + MATCH_WILD_ERR_ARM, + MATCH_AS_REF, + WILDCARD_ENUM_MATCH_ARM, + MATCH_WILDCARD_FOR_SINGLE_VARIANTS, + WILDCARD_IN_OR_PATTERNS, + MATCH_SINGLE_BINDING, + INFALLIBLE_DESTRUCTURING_MATCH, + REST_PAT_IN_FULLY_BOUND_STRUCTS, + REDUNDANT_PATTERN_MATCHING, + MATCH_LIKE_MATCHES_MACRO, + MATCH_SAME_ARMS, + NEEDLESS_MATCH, + COLLAPSIBLE_MATCH, + MANUAL_UNWRAP_OR, + MATCH_ON_VEC_ITEMS, + MATCH_STR_CASE_MISMATCH, + SIGNIFICANT_DROP_IN_SCRUTINEE, + TRY_ERR, + MANUAL_MAP, +]); + +impl<'tcx> LateLintPass<'tcx> for Matches { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + let from_expansion = expr.span.from_expansion(); + + if let ExprKind::Match(ex, arms, source) = expr.kind { + if source == MatchSource::Normal && !span_starts_with(cx, expr.span, "match") { + return; + } + if matches!(source, MatchSource::Normal | MatchSource::ForLoopDesugar) { + significant_drop_in_scrutinee::check(cx, expr, ex, arms, source); + } + + collapsible_match::check_match(cx, arms); + if !from_expansion { + // These don't depend on a relationship between multiple arms + match_wild_err_arm::check(cx, ex, arms); + wild_in_or_pats::check(cx, arms); + } + + if source == MatchSource::TryDesugar { + try_err::check(cx, expr, ex); + } + + if !from_expansion && !contains_cfg_arm(cx, expr, ex, arms) { + if source == MatchSource::Normal { + if !(meets_msrv(self.msrv, msrvs::MATCHES_MACRO) + && match_like_matches::check_match(cx, expr, ex, arms)) + { + match_same_arms::check(cx, arms); + } + + redundant_pattern_match::check_match(cx, expr, ex, arms); + single_match::check(cx, ex, arms, expr); + match_bool::check(cx, ex, arms, expr); + overlapping_arms::check(cx, ex, arms); + match_wild_enum::check(cx, ex, arms); + match_as_ref::check(cx, ex, arms, expr); + needless_match::check_match(cx, ex, arms, expr); + match_on_vec_items::check(cx, ex); + match_str_case_mismatch::check(cx, ex, arms); + + if !in_constant(cx, expr.hir_id) { + manual_unwrap_or::check(cx, expr, ex, arms); + manual_map::check_match(cx, expr, ex, arms); + } + + if self.infallible_destructuring_match_linted { + self.infallible_destructuring_match_linted = false; + } else { + match_single_binding::check(cx, ex, arms, expr); + } + } + match_ref_pats::check(cx, ex, arms.iter().map(|el| el.pat), expr); + } + } else if let Some(if_let) = higher::IfLet::hir(cx, expr) { + collapsible_match::check_if_let(cx, if_let.let_pat, if_let.if_then, if_let.if_else); + if !from_expansion { + if let Some(else_expr) = if_let.if_else { + if meets_msrv(self.msrv, msrvs::MATCHES_MACRO) { + match_like_matches::check_if_let( + cx, + expr, + if_let.let_pat, + if_let.let_expr, + if_let.if_then, + else_expr, + ); + } + if !in_constant(cx, expr.hir_id) { + manual_map::check_if_let(cx, expr, if_let.let_pat, if_let.let_expr, if_let.if_then, else_expr); + } + } + redundant_pattern_match::check_if_let( + cx, + expr, + if_let.let_pat, + if_let.let_expr, + if_let.if_else.is_some(), + ); + needless_match::check_if_let(cx, expr, &if_let); + } + } else if !from_expansion { + redundant_pattern_match::check(cx, expr); + } + } + + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'_>) { + self.infallible_destructuring_match_linted |= + local.els.is_none() && infallible_destructuring_match::check(cx, local); + } + + fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) { + rest_pat_in_fully_bound_struct::check(cx, pat); + } + + extract_msrv_attr!(LateContext); +} + +/// Checks if there are any arms with a `#[cfg(..)]` attribute. +fn contains_cfg_arm(cx: &LateContext<'_>, e: &Expr<'_>, scrutinee: &Expr<'_>, arms: &[Arm<'_>]) -> bool { + let Some(scrutinee_span) = walk_span_to_context(scrutinee.span, SyntaxContext::root()) else { + // Shouldn't happen, but treat this as though a `cfg` attribute were found + return true; + }; + + let start = scrutinee_span.hi(); + let mut arm_spans = arms.iter().map(|arm| { + let data = arm.span.data(); + (data.ctxt == SyntaxContext::root()).then_some((data.lo, data.hi)) + }); + let end = e.span.hi(); + + // Walk through all the non-code space before each match arm. The space trailing the final arm is + // handled after the `try_fold` e.g. + // + // match foo { + // _________^- everything between the scrutinee and arm1 + //| arm1 => (), + //|---^___________^ everything before arm2 + //| #[cfg(feature = "enabled")] + //| arm2 => some_code(), + //|---^____________________^ everything before arm3 + //| // some comment about arm3 + //| arm3 => some_code(), + //|---^____________________^ everything after arm3 + //| #[cfg(feature = "disabled")] + //| arm4 = some_code(), + //|}; + //|^ + let found = arm_spans.try_fold(start, |start, range| { + let Some((end, next_start)) = range else { + // Shouldn't happen as macros can't expand to match arms, but treat this as though a `cfg` attribute were + // found. + return Err(()); + }; + let span = SpanData { + lo: start, + hi: end, + ctxt: SyntaxContext::root(), + parent: None, + } + .span(); + (!span_contains_cfg(cx, span)).then_some(next_start).ok_or(()) + }); + match found { + Ok(start) => { + let span = SpanData { + lo: start, + hi: end, + ctxt: SyntaxContext::root(), + parent: None, + } + .span(); + span_contains_cfg(cx, span) + }, + Err(()) => true, + } +} + +/// Checks if the given span contains a `#[cfg(..)]` attribute +fn span_contains_cfg(cx: &LateContext<'_>, s: Span) -> bool { + let Some(snip) = snippet_opt(cx, s) else { + // Assume true. This would require either an invalid span, or one which crosses file boundaries. + return true; + }; + let mut pos = 0usize; + let mut iter = tokenize(&snip).map(|t| { + let start = pos; + pos += t.len as usize; + (t.kind, start..pos) + }); + + // Search for the token sequence [`#`, `[`, `cfg`] + while iter.any(|(t, _)| matches!(t, TokenKind::Pound)) { + let mut iter = iter.by_ref().skip_while(|(t, _)| { + matches!( + t, + TokenKind::Whitespace | TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } + ) + }); + if matches!(iter.next(), Some((TokenKind::OpenBracket, _))) + && matches!(iter.next(), Some((TokenKind::Ident, range)) if &snip[range.clone()] == "cfg") + { + return true; + } + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/matches/needless_match.rs b/src/tools/clippy/clippy_lints/src/matches/needless_match.rs new file mode 100644 index 000000000..fa19cddd3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/needless_match.rs @@ -0,0 +1,207 @@ +use super::NEEDLESS_MATCH; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::{is_type_diagnostic_item, same_type_and_consts}; +use clippy_utils::{ + eq_expr_value, get_parent_expr_for_hir, get_parent_node, higher, is_else_clause, is_lang_ctor, over, + peel_blocks_with_stmt, +}; +use rustc_errors::Applicability; +use rustc_hir::LangItem::OptionNone; +use rustc_hir::{Arm, BindingAnnotation, Expr, ExprKind, FnRetTy, Node, Pat, PatKind, Path, QPath}; +use rustc_lint::LateContext; +use rustc_span::sym; +use rustc_typeck::hir_ty_to_ty; + +pub(crate) fn check_match(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { + if arms.len() > 1 && expr_ty_matches_p_ty(cx, ex, expr) && check_all_arms(cx, ex, arms) { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + NEEDLESS_MATCH, + expr.span, + "this match expression is unnecessary", + "replace it with", + snippet_with_applicability(cx, ex.span, "..", &mut applicability).to_string(), + applicability, + ); + } +} + +/// Check for nop `if let` expression that assembled as unnecessary match +/// +/// ```rust,ignore +/// if let Some(a) = option { +/// Some(a) +/// } else { +/// None +/// } +/// ``` +/// OR +/// ```rust,ignore +/// if let SomeEnum::A = some_enum { +/// SomeEnum::A +/// } else if let SomeEnum::B = some_enum { +/// SomeEnum::B +/// } else { +/// some_enum +/// } +/// ``` +pub(crate) fn check_if_let<'tcx>(cx: &LateContext<'tcx>, ex: &Expr<'_>, if_let: &higher::IfLet<'tcx>) { + if !is_else_clause(cx.tcx, ex) && expr_ty_matches_p_ty(cx, if_let.let_expr, ex) && check_if_let_inner(cx, if_let) { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + NEEDLESS_MATCH, + ex.span, + "this if-let expression is unnecessary", + "replace it with", + snippet_with_applicability(cx, if_let.let_expr.span, "..", &mut applicability).to_string(), + applicability, + ); + } +} + +fn check_all_arms(cx: &LateContext<'_>, match_expr: &Expr<'_>, arms: &[Arm<'_>]) -> bool { + for arm in arms { + let arm_expr = peel_blocks_with_stmt(arm.body); + if let PatKind::Wild = arm.pat.kind { + return eq_expr_value(cx, match_expr, strip_return(arm_expr)); + } else if !pat_same_as_expr(arm.pat, arm_expr) { + return false; + } + } + + true +} + +fn check_if_let_inner(cx: &LateContext<'_>, if_let: &higher::IfLet<'_>) -> bool { + if let Some(if_else) = if_let.if_else { + if !pat_same_as_expr(if_let.let_pat, peel_blocks_with_stmt(if_let.if_then)) { + return false; + } + + // Recursively check for each `else if let` phrase, + if let Some(ref nested_if_let) = higher::IfLet::hir(cx, if_else) { + return check_if_let_inner(cx, nested_if_let); + } + + if matches!(if_else.kind, ExprKind::Block(..)) { + let else_expr = peel_blocks_with_stmt(if_else); + if matches!(else_expr.kind, ExprKind::Block(..)) { + return false; + } + let ret = strip_return(else_expr); + let let_expr_ty = cx.typeck_results().expr_ty(if_let.let_expr); + if is_type_diagnostic_item(cx, let_expr_ty, sym::Option) { + if let ExprKind::Path(ref qpath) = ret.kind { + return is_lang_ctor(cx, qpath, OptionNone) || eq_expr_value(cx, if_let.let_expr, ret); + } + return false; + } + return eq_expr_value(cx, if_let.let_expr, ret); + } + } + + false +} + +/// Strip `return` keyword if the expression type is `ExprKind::Ret`. +fn strip_return<'hir>(expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> { + if let ExprKind::Ret(Some(ret)) = expr.kind { + ret + } else { + expr + } +} + +/// Manually check for coercion casting by checking if the type of the match operand or let expr +/// differs with the assigned local variable or the function return type. +fn expr_ty_matches_p_ty(cx: &LateContext<'_>, expr: &Expr<'_>, p_expr: &Expr<'_>) -> bool { + if let Some(p_node) = get_parent_node(cx.tcx, p_expr.hir_id) { + match p_node { + // Compare match_expr ty with local in `let local = match match_expr {..}` + Node::Local(local) => { + let results = cx.typeck_results(); + return same_type_and_consts(results.node_type(local.hir_id), results.expr_ty(expr)); + }, + // compare match_expr ty with RetTy in `fn foo() -> RetTy` + Node::Item(..) => { + if let Some(fn_decl) = p_node.fn_decl() { + if let FnRetTy::Return(ret_ty) = fn_decl.output { + return same_type_and_consts(hir_ty_to_ty(cx.tcx, ret_ty), cx.typeck_results().expr_ty(expr)); + } + } + }, + // check the parent expr for this whole block `{ match match_expr {..} }` + Node::Block(block) => { + if let Some(block_parent_expr) = get_parent_expr_for_hir(cx, block.hir_id) { + return expr_ty_matches_p_ty(cx, expr, block_parent_expr); + } + }, + // recursively call on `if xxx {..}` etc. + Node::Expr(p_expr) => { + return expr_ty_matches_p_ty(cx, expr, p_expr); + }, + _ => {}, + } + } + false +} + +fn pat_same_as_expr(pat: &Pat<'_>, expr: &Expr<'_>) -> bool { + let expr = strip_return(expr); + match (&pat.kind, &expr.kind) { + // Example: `Some(val) => Some(val)` + (PatKind::TupleStruct(QPath::Resolved(_, path), tuple_params, _), ExprKind::Call(call_expr, call_params)) => { + if let ExprKind::Path(QPath::Resolved(_, call_path)) = call_expr.kind { + return over(path.segments, call_path.segments, |pat_seg, call_seg| { + pat_seg.ident.name == call_seg.ident.name + }) && same_non_ref_symbols(tuple_params, call_params); + } + }, + // Example: `val => val` + ( + PatKind::Binding(annot, _, pat_ident, _), + ExprKind::Path(QPath::Resolved( + _, + Path { + segments: [first_seg, ..], + .. + }, + )), + ) => { + return !matches!(annot, BindingAnnotation::Ref | BindingAnnotation::RefMut) + && pat_ident.name == first_seg.ident.name; + }, + // Example: `Custom::TypeA => Custom::TypeB`, or `None => None` + (PatKind::Path(QPath::Resolved(_, p_path)), ExprKind::Path(QPath::Resolved(_, e_path))) => { + return over(p_path.segments, e_path.segments, |p_seg, e_seg| { + p_seg.ident.name == e_seg.ident.name + }); + }, + // Example: `5 => 5` + (PatKind::Lit(pat_lit_expr), ExprKind::Lit(expr_spanned)) => { + if let ExprKind::Lit(pat_spanned) = &pat_lit_expr.kind { + return pat_spanned.node == expr_spanned.node; + } + }, + _ => {}, + } + + false +} + +fn same_non_ref_symbols(pats: &[Pat<'_>], exprs: &[Expr<'_>]) -> bool { + if pats.len() != exprs.len() { + return false; + } + + for i in 0..pats.len() { + if !pat_same_as_expr(&pats[i], &exprs[i]) { + return false; + } + } + + true +} diff --git a/src/tools/clippy/clippy_lints/src/matches/overlapping_arms.rs b/src/tools/clippy/clippy_lints/src/matches/overlapping_arms.rs new file mode 100644 index 000000000..ae69ca8a3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/overlapping_arms.rs @@ -0,0 +1,194 @@ +use clippy_utils::consts::{constant, constant_full_int, miri_to_const, FullInt}; +use clippy_utils::diagnostics::span_lint_and_note; +use core::cmp::Ordering; +use rustc_hir::{Arm, Expr, PatKind, RangeEnd}; +use rustc_lint::LateContext; +use rustc_middle::mir; +use rustc_middle::ty::Ty; +use rustc_span::Span; + +use super::MATCH_OVERLAPPING_ARM; + +pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, ex: &'tcx Expr<'_>, arms: &'tcx [Arm<'_>]) { + if arms.len() >= 2 && cx.typeck_results().expr_ty(ex).is_integral() { + let ranges = all_ranges(cx, arms, cx.typeck_results().expr_ty(ex)); + if !ranges.is_empty() { + if let Some((start, end)) = overlapping(&ranges) { + span_lint_and_note( + cx, + MATCH_OVERLAPPING_ARM, + start.span, + "some ranges overlap", + Some(end.span), + "overlaps with this", + ); + } + } + } +} + +/// Gets the ranges for each range pattern arm. Applies `ty` bounds for open ranges. +fn all_ranges<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>], ty: Ty<'tcx>) -> Vec<SpannedRange<FullInt>> { + arms.iter() + .filter_map(|arm| { + if let Arm { pat, guard: None, .. } = *arm { + if let PatKind::Range(ref lhs, ref rhs, range_end) = pat.kind { + let lhs_const = match lhs { + Some(lhs) => constant(cx, cx.typeck_results(), lhs)?.0, + None => { + let min_val_const = ty.numeric_min_val(cx.tcx)?; + let min_constant = mir::ConstantKind::from_value( + cx.tcx.valtree_to_const_val((ty, min_val_const.to_valtree())), + ty, + ); + miri_to_const(cx.tcx, min_constant)? + }, + }; + let rhs_const = match rhs { + Some(rhs) => constant(cx, cx.typeck_results(), rhs)?.0, + None => { + let max_val_const = ty.numeric_max_val(cx.tcx)?; + let max_constant = mir::ConstantKind::from_value( + cx.tcx.valtree_to_const_val((ty, max_val_const.to_valtree())), + ty, + ); + miri_to_const(cx.tcx, max_constant)? + }, + }; + let lhs_val = lhs_const.int_value(cx, ty)?; + let rhs_val = rhs_const.int_value(cx, ty)?; + let rhs_bound = match range_end { + RangeEnd::Included => EndBound::Included(rhs_val), + RangeEnd::Excluded => EndBound::Excluded(rhs_val), + }; + return Some(SpannedRange { + span: pat.span, + node: (lhs_val, rhs_bound), + }); + } + + if let PatKind::Lit(value) = pat.kind { + let value = constant_full_int(cx, cx.typeck_results(), value)?; + return Some(SpannedRange { + span: pat.span, + node: (value, EndBound::Included(value)), + }); + } + } + None + }) + .collect() +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EndBound<T> { + Included(T), + Excluded(T), +} + +#[derive(Debug, Eq, PartialEq)] +struct SpannedRange<T> { + pub span: Span, + pub node: (T, EndBound<T>), +} + +fn overlapping<T>(ranges: &[SpannedRange<T>]) -> Option<(&SpannedRange<T>, &SpannedRange<T>)> +where + T: Copy + Ord, +{ + #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] + enum BoundKind { + EndExcluded, + Start, + EndIncluded, + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + struct RangeBound<'a, T>(T, BoundKind, &'a SpannedRange<T>); + + impl<'a, T: Copy + Ord> PartialOrd for RangeBound<'a, T> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } + } + + impl<'a, T: Copy + Ord> Ord for RangeBound<'a, T> { + fn cmp(&self, RangeBound(other_value, other_kind, _): &Self) -> Ordering { + let RangeBound(self_value, self_kind, _) = *self; + (self_value, self_kind).cmp(&(*other_value, *other_kind)) + } + } + + let mut values = Vec::with_capacity(2 * ranges.len()); + + for r @ SpannedRange { node: (start, end), .. } in ranges { + values.push(RangeBound(*start, BoundKind::Start, r)); + values.push(match end { + EndBound::Excluded(val) => RangeBound(*val, BoundKind::EndExcluded, r), + EndBound::Included(val) => RangeBound(*val, BoundKind::EndIncluded, r), + }); + } + + values.sort(); + + let mut started = vec![]; + + for RangeBound(_, kind, range) in values { + match kind { + BoundKind::Start => started.push(range), + BoundKind::EndExcluded | BoundKind::EndIncluded => { + let mut overlap = None; + + while let Some(last_started) = started.pop() { + if last_started == range { + break; + } + overlap = Some(last_started); + } + + if let Some(first_overlapping) = overlap { + return Some((range, first_overlapping)); + } + }, + } + } + + None +} + +#[test] +fn test_overlapping() { + use rustc_span::source_map::DUMMY_SP; + + let sp = |s, e| SpannedRange { + span: DUMMY_SP, + node: (s, e), + }; + + assert_eq!(None, overlapping::<u8>(&[])); + assert_eq!(None, overlapping(&[sp(1, EndBound::Included(4))])); + assert_eq!( + None, + overlapping(&[sp(1, EndBound::Included(4)), sp(5, EndBound::Included(6))]) + ); + assert_eq!( + None, + overlapping(&[ + sp(1, EndBound::Included(4)), + sp(5, EndBound::Included(6)), + sp(10, EndBound::Included(11)) + ],) + ); + assert_eq!( + Some((&sp(1, EndBound::Included(4)), &sp(3, EndBound::Included(6)))), + overlapping(&[sp(1, EndBound::Included(4)), sp(3, EndBound::Included(6))]) + ); + assert_eq!( + Some((&sp(5, EndBound::Included(6)), &sp(6, EndBound::Included(11)))), + overlapping(&[ + sp(1, EndBound::Included(4)), + sp(5, EndBound::Included(6)), + sp(6, EndBound::Included(11)) + ],) + ); +} diff --git a/src/tools/clippy/clippy_lints/src/matches/redundant_pattern_match.rs b/src/tools/clippy/clippy_lints/src/matches/redundant_pattern_match.rs new file mode 100644 index 000000000..8499e050a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/redundant_pattern_match.rs @@ -0,0 +1,304 @@ +use super::REDUNDANT_PATTERN_MATCHING; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::needs_ordered_drop; +use clippy_utils::visitors::any_temporaries_need_ordered_drop; +use clippy_utils::{higher, is_lang_ctor, is_trait_method, match_def_path, paths}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{OptionNone, PollPending}; +use rustc_hir::{Arm, Expr, ExprKind, Node, Pat, PatKind, QPath, UnOp}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, subst::GenericArgKind, DefIdTree, Ty}; +use rustc_span::sym; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let Some(higher::WhileLet { let_pat, let_expr, .. }) = higher::WhileLet::hir(expr) { + find_sugg_for_if_let(cx, expr, let_pat, let_expr, "while", false); + } +} + +pub(super) fn check_if_let<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + pat: &'tcx Pat<'_>, + scrutinee: &'tcx Expr<'_>, + has_else: bool, +) { + find_sugg_for_if_let(cx, expr, pat, scrutinee, "if", has_else); +} + +// Extract the generic arguments out of a type +fn try_get_generic_ty(ty: Ty<'_>, index: usize) -> Option<Ty<'_>> { + if_chain! { + if let ty::Adt(_, subs) = ty.kind(); + if let Some(sub) = subs.get(index); + if let GenericArgKind::Type(sub_ty) = sub.unpack(); + then { + Some(sub_ty) + } else { + None + } + } +} + +fn find_sugg_for_if_let<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + let_pat: &Pat<'_>, + let_expr: &'tcx Expr<'_>, + keyword: &'static str, + has_else: bool, +) { + // also look inside refs + // if we have &None for example, peel it so we can detect "if let None = x" + let check_pat = match let_pat.kind { + PatKind::Ref(inner, _mutability) => inner, + _ => let_pat, + }; + let op_ty = cx.typeck_results().expr_ty(let_expr); + // Determine which function should be used, and the type contained by the corresponding + // variant. + let (good_method, inner_ty) = match check_pat.kind { + PatKind::TupleStruct(ref qpath, [sub_pat], _) => { + if let PatKind::Wild = sub_pat.kind { + let res = cx.typeck_results().qpath_res(qpath, check_pat.hir_id); + let Some(id) = res.opt_def_id().map(|ctor_id| cx.tcx.parent(ctor_id)) else { return }; + let lang_items = cx.tcx.lang_items(); + if Some(id) == lang_items.result_ok_variant() { + ("is_ok()", try_get_generic_ty(op_ty, 0).unwrap_or(op_ty)) + } else if Some(id) == lang_items.result_err_variant() { + ("is_err()", try_get_generic_ty(op_ty, 1).unwrap_or(op_ty)) + } else if Some(id) == lang_items.option_some_variant() { + ("is_some()", op_ty) + } else if Some(id) == lang_items.poll_ready_variant() { + ("is_ready()", op_ty) + } else if match_def_path(cx, id, &paths::IPADDR_V4) { + ("is_ipv4()", op_ty) + } else if match_def_path(cx, id, &paths::IPADDR_V6) { + ("is_ipv6()", op_ty) + } else { + return; + } + } else { + return; + } + }, + PatKind::Path(ref path) => { + let method = if is_lang_ctor(cx, path, OptionNone) { + "is_none()" + } else if is_lang_ctor(cx, path, PollPending) { + "is_pending()" + } else { + return; + }; + // `None` and `Pending` don't have an inner type. + (method, cx.tcx.types.unit) + }, + _ => return, + }; + + // If this is the last expression in a block or there is an else clause then the whole + // type needs to be considered, not just the inner type of the branch being matched on. + // Note the last expression in a block is dropped after all local bindings. + let check_ty = if has_else + || (keyword == "if" && matches!(cx.tcx.hir().parent_iter(expr.hir_id).next(), Some((_, Node::Block(..))))) + { + op_ty + } else { + inner_ty + }; + + // All temporaries created in the scrutinee expression are dropped at the same time as the + // scrutinee would be, so they have to be considered as well. + // e.g. in `if let Some(x) = foo.lock().unwrap().baz.as_ref() { .. }` the lock will be held + // for the duration if body. + let needs_drop = needs_ordered_drop(cx, check_ty) || any_temporaries_need_ordered_drop(cx, let_expr); + + // check that `while_let_on_iterator` lint does not trigger + if_chain! { + if keyword == "while"; + if let ExprKind::MethodCall(method_path, _, _) = let_expr.kind; + if method_path.ident.name == sym::next; + if is_trait_method(cx, let_expr, sym::Iterator); + then { + return; + } + } + + let result_expr = match &let_expr.kind { + ExprKind::AddrOf(_, _, borrowed) => borrowed, + ExprKind::Unary(UnOp::Deref, deref) => deref, + _ => let_expr, + }; + + span_lint_and_then( + cx, + REDUNDANT_PATTERN_MATCHING, + let_pat.span, + &format!("redundant pattern matching, consider using `{}`", good_method), + |diag| { + // if/while let ... = ... { ... } + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + let expr_span = expr.span; + + // if/while let ... = ... { ... } + // ^^^ + let op_span = result_expr.span.source_callsite(); + + // if/while let ... = ... { ... } + // ^^^^^^^^^^^^^^^^^^^ + let span = expr_span.until(op_span.shrink_to_hi()); + + let app = if needs_drop { + Applicability::MaybeIncorrect + } else { + Applicability::MachineApplicable + }; + + let sugg = Sugg::hir_with_macro_callsite(cx, result_expr, "_") + .maybe_par() + .to_string(); + + diag.span_suggestion(span, "try this", format!("{} {}.{}", keyword, sugg, good_method), app); + + if needs_drop { + diag.note("this will change drop order of the result, as well as all temporaries"); + diag.note("add `#[allow(clippy::redundant_pattern_matching)]` if this is important"); + } + }, + ); +} + +pub(super) fn check_match<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, op: &Expr<'_>, arms: &[Arm<'_>]) { + if arms.len() == 2 { + let node_pair = (&arms[0].pat.kind, &arms[1].pat.kind); + + let found_good_method = match node_pair { + ( + PatKind::TupleStruct(ref path_left, patterns_left, _), + PatKind::TupleStruct(ref path_right, patterns_right, _), + ) if patterns_left.len() == 1 && patterns_right.len() == 1 => { + if let (PatKind::Wild, PatKind::Wild) = (&patterns_left[0].kind, &patterns_right[0].kind) { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::RESULT_OK, + &paths::RESULT_ERR, + "is_ok()", + "is_err()", + ) + .or_else(|| { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::IPADDR_V4, + &paths::IPADDR_V6, + "is_ipv4()", + "is_ipv6()", + ) + }) + } else { + None + } + }, + (PatKind::TupleStruct(ref path_left, patterns, _), PatKind::Path(ref path_right)) + | (PatKind::Path(ref path_left), PatKind::TupleStruct(ref path_right, patterns, _)) + if patterns.len() == 1 => + { + if let PatKind::Wild = patterns[0].kind { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::OPTION_SOME, + &paths::OPTION_NONE, + "is_some()", + "is_none()", + ) + .or_else(|| { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::POLL_READY, + &paths::POLL_PENDING, + "is_ready()", + "is_pending()", + ) + }) + } else { + None + } + }, + _ => None, + }; + + if let Some(good_method) = found_good_method { + let span = expr.span.to(op.span); + let result_expr = match &op.kind { + ExprKind::AddrOf(_, _, borrowed) => borrowed, + _ => op, + }; + span_lint_and_then( + cx, + REDUNDANT_PATTERN_MATCHING, + expr.span, + &format!("redundant pattern matching, consider using `{}`", good_method), + |diag| { + diag.span_suggestion( + span, + "try this", + format!("{}.{}", snippet(cx, result_expr.span, "_"), good_method), + Applicability::MaybeIncorrect, // snippet + ); + }, + ); + } + } +} + +#[expect(clippy::too_many_arguments)] +fn find_good_method_for_match<'a>( + cx: &LateContext<'_>, + arms: &[Arm<'_>], + path_left: &QPath<'_>, + path_right: &QPath<'_>, + expected_left: &[&str], + expected_right: &[&str], + should_be_left: &'a str, + should_be_right: &'a str, +) -> Option<&'a str> { + let left_id = cx + .typeck_results() + .qpath_res(path_left, arms[0].pat.hir_id) + .opt_def_id()?; + let right_id = cx + .typeck_results() + .qpath_res(path_right, arms[1].pat.hir_id) + .opt_def_id()?; + let body_node_pair = if match_def_path(cx, left_id, expected_left) && match_def_path(cx, right_id, expected_right) { + (&arms[0].body.kind, &arms[1].body.kind) + } else if match_def_path(cx, right_id, expected_left) && match_def_path(cx, right_id, expected_right) { + (&arms[1].body.kind, &arms[0].body.kind) + } else { + return None; + }; + + match body_node_pair { + (ExprKind::Lit(ref lit_left), ExprKind::Lit(ref lit_right)) => match (&lit_left.node, &lit_right.node) { + (LitKind::Bool(true), LitKind::Bool(false)) => Some(should_be_left), + (LitKind::Bool(false), LitKind::Bool(true)) => Some(should_be_right), + _ => None, + }, + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/rest_pat_in_fully_bound_struct.rs b/src/tools/clippy/clippy_lints/src/matches/rest_pat_in_fully_bound_struct.rs new file mode 100644 index 000000000..0aadb482a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/rest_pat_in_fully_bound_struct.rs @@ -0,0 +1,30 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{Pat, PatKind, QPath}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::REST_PAT_IN_FULLY_BOUND_STRUCTS; + +pub(crate) fn check(cx: &LateContext<'_>, pat: &Pat<'_>) { + if_chain! { + if !pat.span.from_expansion(); + if let PatKind::Struct(QPath::Resolved(_, path), fields, true) = pat.kind; + if let Some(def_id) = path.res.opt_def_id(); + let ty = cx.tcx.type_of(def_id); + if let ty::Adt(def, _) = ty.kind(); + if def.is_struct() || def.is_union(); + if fields.len() == def.non_enum_variant().fields.len(); + if !def.non_enum_variant().is_field_list_non_exhaustive(); + + then { + span_lint_and_help( + cx, + REST_PAT_IN_FULLY_BOUND_STRUCTS, + pat.span, + "unnecessary use of `..` pattern in struct binding. All fields were already bound", + None, + "consider removing `..` from this binding", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/significant_drop_in_scrutinee.rs b/src/tools/clippy/clippy_lints/src/matches/significant_drop_in_scrutinee.rs new file mode 100644 index 000000000..b0b15b3f5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/significant_drop_in_scrutinee.rs @@ -0,0 +1,400 @@ +use crate::FxHashSet; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::{indent_of, snippet}; +use clippy_utils::{get_attr, is_lint_allowed}; +use rustc_errors::{Applicability, Diagnostic}; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{Arm, Expr, ExprKind, MatchSource}; +use rustc_lint::{LateContext, LintContext}; +use rustc_middle::ty::subst::GenericArgKind; +use rustc_middle::ty::{Ty, TypeAndMut}; +use rustc_span::Span; + +use super::SIGNIFICANT_DROP_IN_SCRUTINEE; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, + scrutinee: &'tcx Expr<'_>, + arms: &'tcx [Arm<'_>], + source: MatchSource, +) { + if is_lint_allowed(cx, SIGNIFICANT_DROP_IN_SCRUTINEE, expr.hir_id) { + return; + } + + if let Some((suggestions, message)) = has_significant_drop_in_scrutinee(cx, scrutinee, source) { + for found in suggestions { + span_lint_and_then(cx, SIGNIFICANT_DROP_IN_SCRUTINEE, found.found_span, message, |diag| { + set_diagnostic(diag, cx, expr, found); + let s = Span::new(expr.span.hi(), expr.span.hi(), expr.span.ctxt(), None); + diag.span_label(s, "temporary lives until here"); + for span in has_significant_drop_in_arms(cx, arms) { + diag.span_label(span, "another value with significant `Drop` created here"); + } + diag.note("this might lead to deadlocks or other unexpected behavior"); + }); + } + } +} + +fn set_diagnostic<'tcx>(diag: &mut Diagnostic, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, found: FoundSigDrop) { + if found.lint_suggestion == LintSuggestion::MoveAndClone { + // If our suggestion is to move and clone, then we want to leave it to the user to + // decide how to address this lint, since it may be that cloning is inappropriate. + // Therefore, we won't to emit a suggestion. + return; + } + + let original = snippet(cx, found.found_span, ".."); + let trailing_indent = " ".repeat(indent_of(cx, found.found_span).unwrap_or(0)); + + let replacement = if found.lint_suggestion == LintSuggestion::MoveAndDerefToCopy { + format!("let value = *{};\n{}", original, trailing_indent) + } else if found.is_unit_return_val { + // If the return value of the expression to be moved is unit, then we don't need to + // capture the result in a temporary -- we can just replace it completely with `()`. + format!("{};\n{}", original, trailing_indent) + } else { + format!("let value = {};\n{}", original, trailing_indent) + }; + + let suggestion_message = if found.lint_suggestion == LintSuggestion::MoveOnly { + "try moving the temporary above the match" + } else { + "try moving the temporary above the match and create a copy" + }; + + let scrutinee_replacement = if found.is_unit_return_val { + "()".to_owned() + } else { + "value".to_owned() + }; + + diag.multipart_suggestion( + suggestion_message, + vec![ + (expr.span.shrink_to_lo(), replacement), + (found.found_span, scrutinee_replacement), + ], + Applicability::MaybeIncorrect, + ); +} + +/// If the expression is an `ExprKind::Match`, check if the scrutinee has a significant drop that +/// may have a surprising lifetime. +fn has_significant_drop_in_scrutinee<'tcx, 'a>( + cx: &'a LateContext<'tcx>, + scrutinee: &'tcx Expr<'tcx>, + source: MatchSource, +) -> Option<(Vec<FoundSigDrop>, &'static str)> { + let mut helper = SigDropHelper::new(cx); + let scrutinee = match (source, &scrutinee.kind) { + (MatchSource::ForLoopDesugar, ExprKind::Call(_, [e])) => e, + _ => scrutinee, + }; + helper.find_sig_drop(scrutinee).map(|drops| { + let message = if source == MatchSource::Normal { + "temporary with significant `Drop` in `match` scrutinee will live until the end of the `match` expression" + } else { + "temporary with significant `Drop` in `for` loop condition will live until the end of the `for` expression" + }; + (drops, message) + }) +} + +struct SigDropChecker<'a, 'tcx> { + seen_types: FxHashSet<Ty<'tcx>>, + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> SigDropChecker<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> SigDropChecker<'a, 'tcx> { + SigDropChecker { + seen_types: FxHashSet::default(), + cx, + } + } + + fn get_type(&self, ex: &'tcx Expr<'_>) -> Ty<'tcx> { + self.cx.typeck_results().expr_ty(ex) + } + + fn has_seen_type(&mut self, ty: Ty<'tcx>) -> bool { + !self.seen_types.insert(ty) + } + + fn has_sig_drop_attr(&mut self, cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + if let Some(adt) = ty.ty_adt_def() { + if get_attr(cx.sess(), cx.tcx.get_attrs_unchecked(adt.did()), "has_significant_drop").count() > 0 { + return true; + } + } + + match ty.kind() { + rustc_middle::ty::Adt(a, b) => { + for f in a.all_fields() { + let ty = f.ty(cx.tcx, b); + if !self.has_seen_type(ty) && self.has_sig_drop_attr(cx, ty) { + return true; + } + } + + for generic_arg in b.iter() { + if let GenericArgKind::Type(ty) = generic_arg.unpack() { + if self.has_sig_drop_attr(cx, ty) { + return true; + } + } + } + false + }, + rustc_middle::ty::Array(ty, _) + | rustc_middle::ty::RawPtr(TypeAndMut { ty, .. }) + | rustc_middle::ty::Ref(_, ty, _) + | rustc_middle::ty::Slice(ty) => self.has_sig_drop_attr(cx, *ty), + _ => false, + } + } +} + +struct SigDropHelper<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + is_chain_end: bool, + has_significant_drop: bool, + current_sig_drop: Option<FoundSigDrop>, + sig_drop_spans: Option<Vec<FoundSigDrop>>, + special_handling_for_binary_op: bool, + sig_drop_checker: SigDropChecker<'a, 'tcx>, +} + +#[expect(clippy::enum_variant_names)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum LintSuggestion { + MoveOnly, + MoveAndDerefToCopy, + MoveAndClone, +} + +#[derive(Clone, Copy)] +struct FoundSigDrop { + found_span: Span, + is_unit_return_val: bool, + lint_suggestion: LintSuggestion, +} + +impl<'a, 'tcx> SigDropHelper<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> SigDropHelper<'a, 'tcx> { + SigDropHelper { + cx, + is_chain_end: true, + has_significant_drop: false, + current_sig_drop: None, + sig_drop_spans: None, + special_handling_for_binary_op: false, + sig_drop_checker: SigDropChecker::new(cx), + } + } + + fn find_sig_drop(&mut self, match_expr: &'tcx Expr<'_>) -> Option<Vec<FoundSigDrop>> { + self.visit_expr(match_expr); + + // If sig drop spans is empty but we found a significant drop, it means that we didn't find + // a type that was trivially copyable as we moved up the chain after finding a significant + // drop, so move the entire scrutinee. + if self.has_significant_drop && self.sig_drop_spans.is_none() { + self.try_setting_current_suggestion(match_expr, true); + self.move_current_suggestion(); + } + + self.sig_drop_spans.take() + } + + fn replace_current_sig_drop( + &mut self, + found_span: Span, + is_unit_return_val: bool, + lint_suggestion: LintSuggestion, + ) { + self.current_sig_drop.replace(FoundSigDrop { + found_span, + is_unit_return_val, + lint_suggestion, + }); + } + + /// This will try to set the current suggestion (so it can be moved into the suggestions vec + /// later). If `allow_move_and_clone` is false, the suggestion *won't* be set -- this gives us + /// an opportunity to look for another type in the chain that will be trivially copyable. + /// However, if we are at the the end of the chain, we want to accept whatever is there. (The + /// suggestion won't actually be output, but the diagnostic message will be output, so the user + /// can determine the best way to handle the lint.) + fn try_setting_current_suggestion(&mut self, expr: &'tcx Expr<'_>, allow_move_and_clone: bool) { + if self.current_sig_drop.is_some() { + return; + } + let ty = self.sig_drop_checker.get_type(expr); + if ty.is_ref() { + // We checked that the type was ref, so builtin_deref will return Some TypeAndMut, + // but let's avoid any chance of an ICE + if let Some(TypeAndMut { ty, .. }) = ty.builtin_deref(true) { + if ty.is_trivially_pure_clone_copy() { + self.replace_current_sig_drop(expr.span, false, LintSuggestion::MoveAndDerefToCopy); + } else if allow_move_and_clone { + self.replace_current_sig_drop(expr.span, false, LintSuggestion::MoveAndClone); + } + } + } else if ty.is_trivially_pure_clone_copy() { + self.replace_current_sig_drop(expr.span, false, LintSuggestion::MoveOnly); + } else if allow_move_and_clone { + self.replace_current_sig_drop(expr.span, false, LintSuggestion::MoveAndClone); + } + } + + fn move_current_suggestion(&mut self) { + if let Some(current) = self.current_sig_drop.take() { + self.sig_drop_spans.get_or_insert_with(Vec::new).push(current); + } + } + + fn visit_exprs_for_binary_ops( + &mut self, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, + is_unit_return_val: bool, + span: Span, + ) { + self.special_handling_for_binary_op = true; + self.visit_expr(left); + self.visit_expr(right); + + // If either side had a significant drop, suggest moving the entire scrutinee to avoid + // unnecessary copies and to simplify cases where both sides have significant drops. + if self.has_significant_drop { + self.replace_current_sig_drop(span, is_unit_return_val, LintSuggestion::MoveOnly); + } + + self.special_handling_for_binary_op = false; + } +} + +impl<'a, 'tcx> Visitor<'tcx> for SigDropHelper<'a, 'tcx> { + fn visit_expr(&mut self, ex: &'tcx Expr<'_>) { + if !self.is_chain_end + && self + .sig_drop_checker + .has_sig_drop_attr(self.cx, self.sig_drop_checker.get_type(ex)) + { + self.has_significant_drop = true; + return; + } + self.is_chain_end = false; + + match ex.kind { + ExprKind::MethodCall(_, [ref expr, ..], _) => { + self.visit_expr(expr); + } + ExprKind::Binary(_, left, right) => { + self.visit_exprs_for_binary_ops(left, right, false, ex.span); + } + ExprKind::Assign(left, right, _) | ExprKind::AssignOp(_, left, right) => { + self.visit_exprs_for_binary_ops(left, right, true, ex.span); + } + ExprKind::Tup(exprs) => { + for expr in exprs { + self.visit_expr(expr); + if self.has_significant_drop { + // We may have not have set current_sig_drop if all the suggestions were + // MoveAndClone, so add this tuple item's full expression in that case. + if self.current_sig_drop.is_none() { + self.try_setting_current_suggestion(expr, true); + } + + // Now we are guaranteed to have something, so add it to the final vec. + self.move_current_suggestion(); + } + // Reset `has_significant_drop` after each tuple expression so we can look for + // additional cases. + self.has_significant_drop = false; + } + if self.sig_drop_spans.is_some() { + self.has_significant_drop = true; + } + } + ExprKind::Box(..) | + ExprKind::Array(..) | + ExprKind::Call(..) | + ExprKind::Unary(..) | + ExprKind::If(..) | + ExprKind::Match(..) | + ExprKind::Field(..) | + ExprKind::Index(..) | + ExprKind::Ret(..) | + ExprKind::Repeat(..) | + ExprKind::Yield(..) | + ExprKind::MethodCall(..) => walk_expr(self, ex), + ExprKind::AddrOf(_, _, _) | + ExprKind::Block(_, _) | + ExprKind::Break(_, _) | + ExprKind::Cast(_, _) | + // Don't want to check the closure itself, only invocation, which is covered by MethodCall + ExprKind::Closure { .. } | + ExprKind::ConstBlock(_) | + ExprKind::Continue(_) | + ExprKind::DropTemps(_) | + ExprKind::Err | + ExprKind::InlineAsm(_) | + ExprKind::Let(_) | + ExprKind::Lit(_) | + ExprKind::Loop(_, _, _, _) | + ExprKind::Path(_) | + ExprKind::Struct(_, _, _) | + ExprKind::Type(_, _) => { + return; + } + } + + // Once a significant temporary has been found, we need to go back up at least 1 level to + // find the span to extract for replacement, so the temporary gets dropped. However, for + // binary ops, we want to move the whole scrutinee so we avoid unnecessary copies and to + // simplify cases where both sides have significant drops. + if self.has_significant_drop && !self.special_handling_for_binary_op { + self.try_setting_current_suggestion(ex, false); + } + } +} + +struct ArmSigDropHelper<'a, 'tcx> { + sig_drop_checker: SigDropChecker<'a, 'tcx>, + found_sig_drop_spans: FxHashSet<Span>, +} + +impl<'a, 'tcx> ArmSigDropHelper<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> ArmSigDropHelper<'a, 'tcx> { + ArmSigDropHelper { + sig_drop_checker: SigDropChecker::new(cx), + found_sig_drop_spans: FxHashSet::<Span>::default(), + } + } +} + +fn has_significant_drop_in_arms<'tcx, 'a>(cx: &'a LateContext<'tcx>, arms: &'tcx [Arm<'_>]) -> FxHashSet<Span> { + let mut helper = ArmSigDropHelper::new(cx); + for arm in arms { + helper.visit_expr(arm.body); + } + helper.found_sig_drop_spans +} + +impl<'a, 'tcx> Visitor<'tcx> for ArmSigDropHelper<'a, 'tcx> { + fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) { + if self + .sig_drop_checker + .has_sig_drop_attr(self.sig_drop_checker.cx, self.sig_drop_checker.get_type(ex)) + { + self.found_sig_drop_spans.insert(ex.span); + return; + } + walk_expr(self, ex); + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/single_match.rs b/src/tools/clippy/clippy_lints/src/matches/single_match.rs new file mode 100644 index 000000000..92091a0c3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/single_match.rs @@ -0,0 +1,248 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{expr_block, snippet}; +use clippy_utils::ty::{implements_trait, match_type, peel_mid_ty_refs}; +use clippy_utils::{ + is_lint_allowed, is_unit_expr, is_wild, paths, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs, +}; +use core::cmp::max; +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 super::{MATCH_BOOL, SINGLE_MATCH, SINGLE_MATCH_ELSE}; + +#[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() { + if expr.span.from_expansion() { + // Don't lint match expressions present in + // macro_rules! block + return; + } + if let PatKind::Or(..) = arms[0].pat.kind { + // don't lint for or patterns for now, this makes + // the lint noisy in unnecessary situations + return; + } + let els = arms[1].body; + let els = if is_unit_expr(peel_blocks(els)) { + 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() { + // single statement/expr "else" block, don't lint + return; + } + // block with 2+ statements or 1 expr and 1+ statement + Some(els) + } else { + // not a block, don't lint + return; + }; + + let ty = cx.typeck_results().expr_ty(ex); + if *ty.kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) { + check_single_pattern(cx, ex, arms, expr, els); + check_opt_like(cx, ex, arms, expr, ty, els); + } + } +} + +fn check_single_pattern( + cx: &LateContext<'_>, + ex: &Expr<'_>, + arms: &[Arm<'_>], + expr: &Expr<'_>, + els: Option<&Expr<'_>>, +) { + if is_wild(arms[1].pat) { + report_single_pattern(cx, ex, arms, expr, els); + } +} + +fn report_single_pattern( + cx: &LateContext<'_>, + ex: &Expr<'_>, + arms: &[Arm<'_>], + expr: &Expr<'_>, + els: Option<&Expr<'_>>, +) { + let lint = if els.is_some() { SINGLE_MATCH_ELSE } else { SINGLE_MATCH }; + let els_str = els.map_or(String::new(), |els| { + format!(" else {}", expr_block(cx, els, None, "..", Some(expr.span))) + }); + + let (pat, pat_ref_count) = peel_hir_pat_refs(arms[0].pat); + let (msg, sugg) = if_chain! { + if let PatKind::Path(_) | PatKind::Lit(_) = pat.kind; + let (ty, ty_ref_count) = peel_mid_ty_refs(cx.typeck_results().expr_ty(ex)); + if let Some(spe_trait_id) = cx.tcx.lang_items().structural_peq_trait(); + if let Some(pe_trait_id) = cx.tcx.lang_items().eq_trait(); + if ty.is_integral() || ty.is_char() || ty.is_str() + || (implements_trait(cx, ty, spe_trait_id, &[]) + && implements_trait(cx, ty, pe_trait_id, &[ty.into()])); + then { + // scrutinee derives PartialEq and the pattern is a constant. + let pat_ref_count = match pat.kind { + // string literals are already a reference. + PatKind::Lit(Expr { kind: ExprKind::Lit(lit), .. }) if lit.node.is_str() => pat_ref_count + 1, + _ => pat_ref_count, + }; + // References are only implicitly added to the pattern, so no overflow here. + // e.g. will work: match &Some(_) { Some(_) => () } + // will not: match Some(_) { &Some(_) => () } + let ref_count_diff = ty_ref_count - pat_ref_count; + + // Try to remove address of expressions first. + let (ex, removed) = peel_n_hir_expr_refs(ex, ref_count_diff); + let ref_count_diff = ref_count_diff - removed; + + let msg = "you seem to be trying to use `match` for an equality check. Consider using `if`"; + let sugg = format!( + "if {} == {}{} {}{}", + snippet(cx, ex.span, ".."), + // PartialEq for different reference counts may not exist. + "&".repeat(ref_count_diff), + snippet(cx, arms[0].pat.span, ".."), + expr_block(cx, arms[0].body, None, "..", Some(expr.span)), + els_str, + ); + (msg, sugg) + } else { + let msg = "you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`"; + let sugg = format!( + "if let {} = {} {}{}", + snippet(cx, arms[0].pat.span, ".."), + snippet(cx, ex.span, ".."), + expr_block(cx, arms[0].body, None, "..", Some(expr.span)), + els_str, + ); + (msg, sugg) + } + }; + + span_lint_and_sugg( + cx, + lint, + expr.span, + msg, + "try this", + sugg, + Applicability::HasPlaceholders, + ); +} + +fn check_opt_like<'a>( + cx: &LateContext<'a>, + ex: &Expr<'_>, + arms: &[Arm<'_>], + expr: &Expr<'_>, + ty: Ty<'a>, + els: Option<&Expr<'_>>, +) { + // We don't want to lint if the second arm contains an enum which could + // have more variants in the future. + if form_exhaustive_matches(cx, ty, arms[0].pat, arms[1].pat) { + report_single_pattern(cx, ex, arms, expr, els); + } +} + +/// Returns `true` if all of the types in the pattern are enums which we know +/// won't be expanded in the future +fn pat_in_candidate_enum<'a>(cx: &LateContext<'a>, ty: Ty<'a>, pat: &Pat<'_>) -> bool { + let mut paths_and_types = Vec::new(); + collect_pat_paths(&mut paths_and_types, cx, pat, ty); + paths_and_types.iter().all(|ty| in_candidate_enum(cx, *ty)) +} + +/// Returns `true` if the given type is an enum we know won't be expanded in the future +fn in_candidate_enum<'a>(cx: &LateContext<'a>, ty: Ty<'_>) -> bool { + // list of candidate `Enum`s we know will never get any more members + let candidates = [&paths::COW, &paths::OPTION, &paths::RESULT]; + + for candidate_ty in candidates { + if match_type(cx, ty, candidate_ty) { + return true; + } + } + false +} + +/// Collects types from the given pattern +fn collect_pat_paths<'a>(acc: &mut Vec<Ty<'a>>, cx: &LateContext<'a>, pat: &Pat<'_>, ty: Ty<'a>) { + match pat.kind { + PatKind::Tuple(inner, _) => inner.iter().for_each(|p| { + let p_ty = cx.typeck_results().pat_ty(p); + collect_pat_paths(acc, cx, p, p_ty); + }), + PatKind::TupleStruct(..) | PatKind::Binding(BindingAnnotation::Unannotated, .., None) | PatKind::Path(_) => { + acc.push(ty); + }, + _ => {}, + } +} + +/// Returns true if the given arm of pattern matching contains wildcard patterns. +fn contains_only_wilds(pat: &Pat<'_>) -> bool { + match pat.kind { + PatKind::Wild => true, + PatKind::Tuple(inner, _) | PatKind::TupleStruct(_, inner, ..) => inner.iter().all(contains_only_wilds), + _ => false, + } +} + +/// Returns true if the given patterns forms only exhaustive matches that don't contain enum +/// patterns without a wildcard. +fn form_exhaustive_matches<'a>(cx: &LateContext<'a>, ty: Ty<'a>, left: &Pat<'_>, right: &Pat<'_>) -> bool { + match (&left.kind, &right.kind) { + (PatKind::Wild, _) | (_, PatKind::Wild) => true, + (PatKind::Tuple(left_in, left_pos), PatKind::Tuple(right_in, right_pos)) => { + // We don't actually know the position and the presence of the `..` (dotdot) operator + // in the arms, so we need to evaluate the correct offsets here in order to iterate in + // both arms at the same time. + let len = max( + left_in.len() + { + if left_pos.is_some() { 1 } else { 0 } + }, + right_in.len() + { + if right_pos.is_some() { 1 } else { 0 } + }, + ); + let mut left_pos = left_pos.unwrap_or(usize::MAX); + let mut right_pos = right_pos.unwrap_or(usize::MAX); + let mut left_dot_space = 0; + let mut right_dot_space = 0; + for i in 0..len { + let mut found_dotdot = false; + if i == left_pos { + left_dot_space += 1; + if left_dot_space < len - left_in.len() { + left_pos += 1; + } + found_dotdot = true; + } + if i == right_pos { + right_dot_space += 1; + if right_dot_space < len - right_in.len() { + right_pos += 1; + } + found_dotdot = true; + } + if found_dotdot { + continue; + } + if !contains_only_wilds(&left_in[i - left_dot_space]) + && !contains_only_wilds(&right_in[i - right_dot_space]) + { + return false; + } + } + true + }, + (PatKind::TupleStruct(..), PatKind::Path(_)) => pat_in_candidate_enum(cx, ty, right), + (PatKind::TupleStruct(..), PatKind::TupleStruct(_, inner, _)) => { + pat_in_candidate_enum(cx, ty, right) && inner.iter().all(contains_only_wilds) + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/try_err.rs b/src/tools/clippy/clippy_lints/src/matches/try_err.rs new file mode 100644 index 000000000..0491a0679 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/try_err.rs @@ -0,0 +1,145 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{get_parent_expr, is_lang_ctor, match_def_path, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::ResultErr; +use rustc_hir::{Expr, ExprKind, LangItem, MatchSource, QPath}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::{hygiene, sym}; + +use super::TRY_ERR; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, scrutinee: &'tcx Expr<'_>) { + // Looks for a structure like this: + // match ::std::ops::Try::into_result(Err(5)) { + // ::std::result::Result::Err(err) => + // #[allow(unreachable_code)] + // return ::std::ops::Try::from_error(::std::convert::From::from(err)), + // ::std::result::Result::Ok(val) => + // #[allow(unreachable_code)] + // val, + // }; + if_chain! { + if let ExprKind::Call(match_fun, try_args) = scrutinee.kind; + if let ExprKind::Path(ref match_fun_path) = match_fun.kind; + if matches!(match_fun_path, QPath::LangItem(LangItem::TryTraitBranch, ..)); + if let Some(try_arg) = try_args.get(0); + if let ExprKind::Call(err_fun, err_args) = try_arg.kind; + if let Some(err_arg) = err_args.get(0); + if let ExprKind::Path(ref err_fun_path) = err_fun.kind; + if is_lang_ctor(cx, err_fun_path, ResultErr); + if let Some(return_ty) = find_return_type(cx, &expr.kind); + then { + let prefix; + let suffix; + let err_ty; + + if let Some(ty) = result_error_type(cx, return_ty) { + prefix = "Err("; + suffix = ")"; + err_ty = ty; + } else if let Some(ty) = poll_result_error_type(cx, return_ty) { + prefix = "Poll::Ready(Err("; + suffix = "))"; + err_ty = ty; + } else if let Some(ty) = poll_option_result_error_type(cx, return_ty) { + prefix = "Poll::Ready(Some(Err("; + suffix = ")))"; + err_ty = ty; + } else { + return; + }; + + let expr_err_ty = cx.typeck_results().expr_ty(err_arg); + let span = hygiene::walk_chain(err_arg.span, try_arg.span.ctxt()); + let mut applicability = Applicability::MachineApplicable; + let origin_snippet = snippet_with_applicability(cx, span, "_", &mut applicability); + let ret_prefix = if get_parent_expr(cx, expr).map_or(false, |e| matches!(e.kind, ExprKind::Ret(_))) { + "" // already returns + } else { + "return " + }; + let suggestion = if err_ty == expr_err_ty { + format!("{}{}{}{}", ret_prefix, prefix, origin_snippet, suffix) + } else { + format!("{}{}{}.into(){}", ret_prefix, prefix, origin_snippet, suffix) + }; + + span_lint_and_sugg( + cx, + TRY_ERR, + expr.span, + "returning an `Err(_)` with the `?` operator", + "try this", + suggestion, + applicability, + ); + } + } +} + +/// 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() { + if let ExprKind::Ret(Some(ret)) = arm.body.kind { + return Some(cx.typeck_results().expr_ty(ret)); + } + } + } + None +} + +/// Extracts the error type from Result<T, E>. +fn result_error_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> { + if_chain! { + if let ty::Adt(_, subst) = ty.kind(); + if is_type_diagnostic_item(cx, ty, sym::Result); + then { + Some(subst.type_at(1)) + } else { + None + } + } +} + +/// Extracts the error type from Poll<Result<T, E>>. +fn poll_result_error_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> { + if_chain! { + if let ty::Adt(def, subst) = ty.kind(); + if match_def_path(cx, def.did(), &paths::POLL); + let ready_ty = subst.type_at(0); + + if let ty::Adt(ready_def, ready_subst) = ready_ty.kind(); + if cx.tcx.is_diagnostic_item(sym::Result, ready_def.did()); + then { + Some(ready_subst.type_at(1)) + } else { + None + } + } +} + +/// Extracts the error type from Poll<Option<Result<T, E>>>. +fn poll_option_result_error_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> { + if_chain! { + if let ty::Adt(def, subst) = ty.kind(); + if match_def_path(cx, def.did(), &paths::POLL); + let ready_ty = subst.type_at(0); + + if let ty::Adt(ready_def, ready_subst) = ready_ty.kind(); + if cx.tcx.is_diagnostic_item(sym::Option, ready_def.did()); + let some_ty = ready_subst.type_at(0); + + if let ty::Adt(some_def, some_subst) = some_ty.kind(); + if cx.tcx.is_diagnostic_item(sym::Result, some_def.did()); + then { + Some(some_subst.type_at(1)) + } else { + None + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/matches/wild_in_or_pats.rs b/src/tools/clippy/clippy_lints/src/matches/wild_in_or_pats.rs new file mode 100644 index 000000000..459513e65 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/matches/wild_in_or_pats.rs @@ -0,0 +1,24 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_wild; +use rustc_hir::{Arm, PatKind}; +use rustc_lint::LateContext; + +use super::WILDCARD_IN_OR_PATTERNS; + +pub(crate) fn check(cx: &LateContext<'_>, arms: &[Arm<'_>]) { + for arm in arms { + if let PatKind::Or(fields) = arm.pat.kind { + // look for multiple fields in this arm that contains at least one Wild pattern + if fields.len() > 1 && fields.iter().any(is_wild) { + span_lint_and_help( + cx, + WILDCARD_IN_OR_PATTERNS, + arm.pat.span, + "wildcard pattern covers any other pattern as it will match anyway", + None, + "consider handling `_` separately", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/mem_forget.rs b/src/tools/clippy/clippy_lints/src/mem_forget.rs new file mode 100644 index 000000000..d6c235b5a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mem_forget.rs @@ -0,0 +1,46 @@ +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/src/tools/clippy/clippy_lints/src/mem_replace.rs b/src/tools/clippy/clippy_lints/src/mem_replace.rs new file mode 100644 index 000000000..41073d40f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mem_replace.rs @@ -0,0 +1,264 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::{snippet, snippet_with_applicability}; +use clippy_utils::ty::is_non_aggregate_primitive_type; +use clippy_utils::{is_default_equivalent, is_lang_ctor, meets_msrv, msrvs}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::OptionNone; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Span; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `mem::replace()` on an `Option` with + /// `None`. + /// + /// ### Why is this bad? + /// `Option` already has the method `take()` for + /// taking its current value (Some(..) or None) and replacing it with + /// `None`. + /// + /// ### Example + /// ```rust + /// use std::mem; + /// + /// let mut an_option = Some(0); + /// let replaced = mem::replace(&mut an_option, None); + /// ``` + /// Is better expressed with: + /// ```rust + /// let mut an_option = Some(0); + /// let taken = an_option.take(); + /// ``` + #[clippy::version = "1.31.0"] + pub MEM_REPLACE_OPTION_WITH_NONE, + style, + "replacing an `Option` with `None` instead of `take()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `mem::replace(&mut _, mem::uninitialized())` + /// and `mem::replace(&mut _, mem::zeroed())`. + /// + /// ### Why is this bad? + /// This will lead to undefined behavior even if the + /// value is overwritten later, because the uninitialized value may be + /// observed in the case of a panic. + /// + /// ### Example + /// ``` + /// use std::mem; + ///# fn may_panic(v: Vec<i32>) -> Vec<i32> { v } + /// + /// #[allow(deprecated, invalid_value)] + /// fn myfunc (v: &mut Vec<i32>) { + /// let taken_v = unsafe { mem::replace(v, mem::uninitialized()) }; + /// let new_v = may_panic(taken_v); // undefined behavior on panic + /// mem::forget(mem::replace(v, new_v)); + /// } + /// ``` + /// + /// The [take_mut](https://docs.rs/take_mut) crate offers a sound solution, + /// at the cost of either lazily creating a replacement value or aborting + /// on panic, to ensure that the uninitialized value cannot be observed. + #[clippy::version = "1.39.0"] + pub MEM_REPLACE_WITH_UNINIT, + correctness, + "`mem::replace(&mut _, mem::uninitialized())` or `mem::replace(&mut _, mem::zeroed())`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `std::mem::replace` on a value of type + /// `T` with `T::default()`. + /// + /// ### Why is this bad? + /// `std::mem` module already has the method `take` to + /// take the current value and replace it with the default value of that type. + /// + /// ### Example + /// ```rust + /// let mut text = String::from("foo"); + /// let replaced = std::mem::replace(&mut text, String::default()); + /// ``` + /// Is better expressed with: + /// ```rust + /// let mut text = String::from("foo"); + /// let taken = std::mem::take(&mut text); + /// ``` + #[clippy::version = "1.42.0"] + pub MEM_REPLACE_WITH_DEFAULT, + style, + "replacing a value of type `T` with `T::default()` instead of using `std::mem::take`" +} + +impl_lint_pass!(MemReplace => + [MEM_REPLACE_OPTION_WITH_NONE, MEM_REPLACE_WITH_UNINIT, MEM_REPLACE_WITH_DEFAULT]); + +fn check_replace_option_with_none(cx: &LateContext<'_>, src: &Expr<'_>, dest: &Expr<'_>, expr_span: Span) { + if let ExprKind::Path(ref replacement_qpath) = src.kind { + // Check that second argument is `Option::None` + if is_lang_ctor(cx, replacement_qpath, OptionNone) { + // Since this is a late pass (already type-checked), + // and we already know that the second argument is an + // `Option`, we do not need to check the first + // argument's type. All that's left is to get + // replacee's path. + let replaced_path = match dest.kind { + ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, replaced) => { + if let ExprKind::Path(QPath::Resolved(None, replaced_path)) = replaced.kind { + replaced_path + } else { + return; + } + }, + ExprKind::Path(QPath::Resolved(None, replaced_path)) => replaced_path, + _ => return, + }; + + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + MEM_REPLACE_OPTION_WITH_NONE, + expr_span, + "replacing an `Option` with `None`", + "consider `Option::take()` instead", + format!( + "{}.take()", + snippet_with_applicability(cx, replaced_path.span, "", &mut applicability) + ), + applicability, + ); + } + } +} + +fn check_replace_with_uninit(cx: &LateContext<'_>, src: &Expr<'_>, dest: &Expr<'_>, expr_span: Span) { + if_chain! { + // check if replacement is mem::MaybeUninit::uninit().assume_init() + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(src.hir_id); + if cx.tcx.is_diagnostic_item(sym::assume_init, method_def_id); + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + MEM_REPLACE_WITH_UNINIT, + expr_span, + "replacing with `mem::MaybeUninit::uninit().assume_init()`", + "consider using", + format!( + "std::ptr::read({})", + snippet_with_applicability(cx, dest.span, "", &mut applicability) + ), + applicability, + ); + return; + } + } + + if_chain! { + if let ExprKind::Call(repl_func, repl_args) = src.kind; + if repl_args.is_empty(); + if let ExprKind::Path(ref repl_func_qpath) = repl_func.kind; + if let Some(repl_def_id) = cx.qpath_res(repl_func_qpath, repl_func.hir_id).opt_def_id(); + then { + if cx.tcx.is_diagnostic_item(sym::mem_uninitialized, repl_def_id) { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + MEM_REPLACE_WITH_UNINIT, + expr_span, + "replacing with `mem::uninitialized()`", + "consider using", + format!( + "std::ptr::read({})", + snippet_with_applicability(cx, dest.span, "", &mut applicability) + ), + applicability, + ); + } else if cx.tcx.is_diagnostic_item(sym::mem_zeroed, repl_def_id) && + !cx.typeck_results().expr_ty(src).is_primitive() { + span_lint_and_help( + cx, + MEM_REPLACE_WITH_UNINIT, + expr_span, + "replacing with `mem::zeroed()`", + None, + "consider using a default value or the `take_mut` crate instead", + ); + } + } + } +} + +fn check_replace_with_default(cx: &LateContext<'_>, src: &Expr<'_>, dest: &Expr<'_>, expr_span: Span) { + // disable lint for primitives + let expr_type = cx.typeck_results().expr_ty_adjusted(src); + if is_non_aggregate_primitive_type(expr_type) { + return; + } + // disable lint for Option since it is covered in another lint + if let ExprKind::Path(q) = &src.kind { + if is_lang_ctor(cx, q, OptionNone) { + return; + } + } + if is_default_equivalent(cx, src) && !in_external_macro(cx.tcx.sess, expr_span) { + span_lint_and_then( + cx, + MEM_REPLACE_WITH_DEFAULT, + expr_span, + "replacing a value of type `T` with `T::default()` is better expressed using `std::mem::take`", + |diag| { + if !expr_span.from_expansion() { + let suggestion = format!("std::mem::take({})", snippet(cx, dest.span, "")); + + diag.span_suggestion( + expr_span, + "consider using", + suggestion, + Applicability::MachineApplicable, + ); + } + }, + ); + } +} + +pub struct MemReplace { + msrv: Option<RustcVersion>, +} + +impl MemReplace { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl<'tcx> LateLintPass<'tcx> for MemReplace { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + // Check that `expr` is a call to `mem::replace()` + if let ExprKind::Call(func, func_args) = expr.kind; + if let ExprKind::Path(ref func_qpath) = func.kind; + if let Some(def_id) = cx.qpath_res(func_qpath, func.hir_id).opt_def_id(); + if cx.tcx.is_diagnostic_item(sym::mem_replace, def_id); + if let [dest, src] = func_args; + then { + check_replace_option_with_none(cx, src, dest, expr.span); + check_replace_with_uninit(cx, src, dest, expr.span); + if meets_msrv(self.msrv, msrvs::MEM_TAKE) { + check_replace_with_default(cx, src, dest, expr.span); + } + } + } + } + extract_msrv_attr!(LateContext); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/bind_instead_of_map.rs b/src/tools/clippy/clippy_lints/src/methods/bind_instead_of_map.rs new file mode 100644 index 000000000..2f117e4dc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/bind_instead_of_map.rs @@ -0,0 +1,190 @@ +use super::{contains_return, BIND_INSTEAD_OF_MAP}; +use clippy_utils::diagnostics::{multispan_sugg_with_applicability, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::{snippet, snippet_with_macro_callsite}; +use clippy_utils::{peel_blocks, visitors::find_all_ret_expressions}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res}; +use rustc_hir::{LangItem, QPath}; +use rustc_lint::LateContext; +use rustc_middle::ty::DefIdTree; +use rustc_span::Span; + +pub(crate) struct OptionAndThenSome; + +impl BindInsteadOfMap for OptionAndThenSome { + const VARIANT_LANG_ITEM: LangItem = LangItem::OptionSome; + const BAD_METHOD_NAME: &'static str = "and_then"; + const GOOD_METHOD_NAME: &'static str = "map"; +} + +pub(crate) struct ResultAndThenOk; + +impl BindInsteadOfMap for ResultAndThenOk { + const VARIANT_LANG_ITEM: LangItem = LangItem::ResultOk; + const BAD_METHOD_NAME: &'static str = "and_then"; + const GOOD_METHOD_NAME: &'static str = "map"; +} + +pub(crate) struct ResultOrElseErrInfo; + +impl BindInsteadOfMap for ResultOrElseErrInfo { + const VARIANT_LANG_ITEM: LangItem = LangItem::ResultErr; + const BAD_METHOD_NAME: &'static str = "or_else"; + const GOOD_METHOD_NAME: &'static str = "map_err"; +} + +pub(crate) trait BindInsteadOfMap { + const VARIANT_LANG_ITEM: LangItem; + const BAD_METHOD_NAME: &'static str; + const GOOD_METHOD_NAME: &'static str; + + fn no_op_msg(cx: &LateContext<'_>) -> Option<String> { + let variant_id = cx.tcx.lang_items().require(Self::VARIANT_LANG_ITEM).ok()?; + let item_id = cx.tcx.parent(variant_id); + Some(format!( + "using `{}.{}({})`, which is a no-op", + cx.tcx.item_name(item_id), + Self::BAD_METHOD_NAME, + cx.tcx.item_name(variant_id), + )) + } + + fn lint_msg(cx: &LateContext<'_>) -> Option<String> { + let variant_id = cx.tcx.lang_items().require(Self::VARIANT_LANG_ITEM).ok()?; + let item_id = cx.tcx.parent(variant_id); + Some(format!( + "using `{}.{}(|x| {}(y))`, which is more succinctly expressed as `{}(|x| y)`", + cx.tcx.item_name(item_id), + Self::BAD_METHOD_NAME, + cx.tcx.item_name(variant_id), + Self::GOOD_METHOD_NAME + )) + } + + fn lint_closure_autofixable( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + recv: &hir::Expr<'_>, + closure_expr: &hir::Expr<'_>, + closure_args_span: Span, + ) -> bool { + if_chain! { + if let hir::ExprKind::Call(some_expr, [inner_expr]) = closure_expr.kind; + if let hir::ExprKind::Path(QPath::Resolved(_, path)) = some_expr.kind; + if Self::is_variant(cx, path.res); + if !contains_return(inner_expr); + if let Some(msg) = Self::lint_msg(cx); + then { + let some_inner_snip = if inner_expr.span.from_expansion() { + snippet_with_macro_callsite(cx, inner_expr.span, "_") + } else { + snippet(cx, inner_expr.span, "_") + }; + + let closure_args_snip = snippet(cx, closure_args_span, ".."); + let option_snip = snippet(cx, recv.span, ".."); + let note = format!("{}.{}({} {})", option_snip, Self::GOOD_METHOD_NAME, closure_args_snip, some_inner_snip); + span_lint_and_sugg( + cx, + BIND_INSTEAD_OF_MAP, + expr.span, + &msg, + "try this", + note, + Applicability::MachineApplicable, + ); + true + } else { + false + } + } + } + + fn lint_closure(cx: &LateContext<'_>, expr: &hir::Expr<'_>, closure_expr: &hir::Expr<'_>) -> bool { + let mut suggs = Vec::new(); + let can_sugg: bool = find_all_ret_expressions(cx, closure_expr, |ret_expr| { + if_chain! { + if !ret_expr.span.from_expansion(); + if let hir::ExprKind::Call(func_path, [arg]) = ret_expr.kind; + if let hir::ExprKind::Path(QPath::Resolved(_, path)) = func_path.kind; + if Self::is_variant(cx, path.res); + if !contains_return(arg); + then { + suggs.push((ret_expr.span, arg.span.source_callsite())); + true + } else { + false + } + } + }); + let (span, msg) = if_chain! { + if can_sugg; + if let hir::ExprKind::MethodCall(segment, ..) = expr.kind; + if let Some(msg) = Self::lint_msg(cx); + then { (segment.ident.span, msg) } else { return false; } + }; + span_lint_and_then(cx, BIND_INSTEAD_OF_MAP, expr.span, &msg, |diag| { + multispan_sugg_with_applicability( + diag, + "try this", + Applicability::MachineApplicable, + std::iter::once((span, Self::GOOD_METHOD_NAME.into())).chain( + suggs + .into_iter() + .map(|(span1, span2)| (span1, snippet(cx, span2, "_").into())), + ), + ); + }); + true + } + + /// Lint use of `_.and_then(|x| Some(y))` for `Option`s + fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>, arg: &hir::Expr<'_>) -> bool { + if_chain! { + if let Some(adt) = cx.typeck_results().expr_ty(recv).ty_adt_def(); + if let Ok(vid) = cx.tcx.lang_items().require(Self::VARIANT_LANG_ITEM); + if adt.did() == cx.tcx.parent(vid); + then {} else { return false; } + } + + match arg.kind { + hir::ExprKind::Closure(&hir::Closure { body, fn_decl_span, .. }) => { + let closure_body = cx.tcx.hir().body(body); + let closure_expr = peel_blocks(&closure_body.value); + + if Self::lint_closure_autofixable(cx, expr, recv, closure_expr, fn_decl_span) { + true + } else { + Self::lint_closure(cx, expr, closure_expr) + } + }, + // `_.and_then(Some)` case, which is no-op. + hir::ExprKind::Path(QPath::Resolved(_, path)) if Self::is_variant(cx, path.res) => { + if let Some(msg) = Self::no_op_msg(cx) { + span_lint_and_sugg( + cx, + BIND_INSTEAD_OF_MAP, + expr.span, + &msg, + "use the expression directly", + snippet(cx, recv.span, "..").into(), + Applicability::MachineApplicable, + ); + } + true + }, + _ => false, + } + } + + fn is_variant(cx: &LateContext<'_>, res: Res) -> bool { + if let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Fn), id) = res { + if let Ok(variant_id) = cx.tcx.lang_items().require(Self::VARIANT_LANG_ITEM) { + return cx.tcx.parent(id) == variant_id; + } + } + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/bytes_nth.rs b/src/tools/clippy/clippy_lints/src/methods/bytes_nth.rs new file mode 100644 index 000000000..44857d61f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/bytes_nth.rs @@ -0,0 +1,34 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::BYTES_NTH; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>, recv: &'tcx Expr<'tcx>, n_arg: &'tcx Expr<'tcx>) { + let ty = cx.typeck_results().expr_ty(recv).peel_refs(); + let caller_type = if ty.is_str() { + "str" + } else if is_type_diagnostic_item(cx, ty, sym::String) { + "String" + } else { + return; + }; + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + BYTES_NTH, + expr.span, + &format!("called `.bytes().nth()` on a `{}`", caller_type), + "try", + format!( + "{}.as_bytes().get({})", + snippet_with_applicability(cx, recv.span, "..", &mut applicability), + snippet_with_applicability(cx, n_arg.span, "..", &mut applicability) + ), + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/chars_cmp.rs b/src/tools/clippy/clippy_lints/src/methods/chars_cmp.rs new file mode 100644 index 000000000..f7b79f083 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/chars_cmp.rs @@ -0,0 +1,51 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{method_chain_args, path_def_id}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_lint::Lint; +use rustc_middle::ty::{self, DefIdTree}; + +/// Wrapper fn for `CHARS_NEXT_CMP` and `CHARS_LAST_CMP` lints. +pub(super) fn check( + cx: &LateContext<'_>, + info: &crate::methods::BinaryExprInfo<'_>, + chain_methods: &[&str], + lint: &'static Lint, + suggest: &str, +) -> bool { + if_chain! { + if let Some(args) = method_chain_args(info.chain, chain_methods); + if let hir::ExprKind::Call(fun, [arg_char]) = info.other.kind; + if let Some(id) = path_def_id(cx, fun).map(|ctor_id| cx.tcx.parent(ctor_id)); + if Some(id) == cx.tcx.lang_items().option_some_variant(); + then { + let mut applicability = Applicability::MachineApplicable; + let self_ty = cx.typeck_results().expr_ty_adjusted(&args[0][0]).peel_refs(); + + if *self_ty.kind() != ty::Str { + return false; + } + + span_lint_and_sugg( + cx, + lint, + info.expr.span, + &format!("you should use the `{}` method", suggest), + "like this", + format!("{}{}.{}({})", + if info.eq { "" } else { "!" }, + snippet_with_applicability(cx, args[0][0].span, "..", &mut applicability), + suggest, + snippet_with_applicability(cx, arg_char.span, "..", &mut applicability)), + applicability, + ); + + return true; + } + } + + false +} diff --git a/src/tools/clippy/clippy_lints/src/methods/chars_cmp_with_unwrap.rs b/src/tools/clippy/clippy_lints/src/methods/chars_cmp_with_unwrap.rs new file mode 100644 index 000000000..a7c0e4392 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/chars_cmp_with_unwrap.rs @@ -0,0 +1,44 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::method_chain_args; +use clippy_utils::source::snippet_with_applicability; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_lint::Lint; + +/// Wrapper fn for `CHARS_NEXT_CMP` and `CHARS_LAST_CMP` lints with `unwrap()`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + info: &crate::methods::BinaryExprInfo<'_>, + chain_methods: &[&str], + lint: &'static Lint, + suggest: &str, +) -> bool { + if_chain! { + if let Some(args) = method_chain_args(info.chain, chain_methods); + if let hir::ExprKind::Lit(ref lit) = info.other.kind; + if let ast::LitKind::Char(c) = lit.node; + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + lint, + info.expr.span, + &format!("you should use the `{}` method", suggest), + "like this", + format!("{}{}.{}('{}')", + if info.eq { "" } else { "!" }, + snippet_with_applicability(cx, args[0][0].span, "..", &mut applicability), + suggest, + c.escape_default()), + applicability, + ); + + true + } else { + false + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/chars_last_cmp.rs b/src/tools/clippy/clippy_lints/src/methods/chars_last_cmp.rs new file mode 100644 index 000000000..07bbc5ca1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/chars_last_cmp.rs @@ -0,0 +1,13 @@ +use crate::methods::chars_cmp; +use rustc_lint::LateContext; + +use super::CHARS_LAST_CMP; + +/// Checks for the `CHARS_LAST_CMP` lint. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, info: &crate::methods::BinaryExprInfo<'_>) -> bool { + if chars_cmp::check(cx, info, &["chars", "last"], CHARS_LAST_CMP, "ends_with") { + true + } else { + chars_cmp::check(cx, info, &["chars", "next_back"], CHARS_LAST_CMP, "ends_with") + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/chars_last_cmp_with_unwrap.rs b/src/tools/clippy/clippy_lints/src/methods/chars_last_cmp_with_unwrap.rs new file mode 100644 index 000000000..c29ee0ec8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/chars_last_cmp_with_unwrap.rs @@ -0,0 +1,13 @@ +use crate::methods::chars_cmp_with_unwrap; +use rustc_lint::LateContext; + +use super::CHARS_LAST_CMP; + +/// Checks for the `CHARS_LAST_CMP` lint with `unwrap()`. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, info: &crate::methods::BinaryExprInfo<'_>) -> bool { + if chars_cmp_with_unwrap::check(cx, info, &["chars", "last", "unwrap"], CHARS_LAST_CMP, "ends_with") { + true + } else { + chars_cmp_with_unwrap::check(cx, info, &["chars", "next_back", "unwrap"], CHARS_LAST_CMP, "ends_with") + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/chars_next_cmp.rs b/src/tools/clippy/clippy_lints/src/methods/chars_next_cmp.rs new file mode 100644 index 000000000..a6701d883 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/chars_next_cmp.rs @@ -0,0 +1,8 @@ +use rustc_lint::LateContext; + +use super::CHARS_NEXT_CMP; + +/// Checks for the `CHARS_NEXT_CMP` lint. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, info: &crate::methods::BinaryExprInfo<'_>) -> bool { + crate::methods::chars_cmp::check(cx, info, &["chars", "next"], CHARS_NEXT_CMP, "starts_with") +} diff --git a/src/tools/clippy/clippy_lints/src/methods/chars_next_cmp_with_unwrap.rs b/src/tools/clippy/clippy_lints/src/methods/chars_next_cmp_with_unwrap.rs new file mode 100644 index 000000000..28ede28e9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/chars_next_cmp_with_unwrap.rs @@ -0,0 +1,8 @@ +use rustc_lint::LateContext; + +use super::CHARS_NEXT_CMP; + +/// Checks for the `CHARS_NEXT_CMP` lint with `unwrap()`. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, info: &crate::methods::BinaryExprInfo<'_>) -> bool { + crate::methods::chars_cmp_with_unwrap::check(cx, info, &["chars", "next", "unwrap"], CHARS_NEXT_CMP, "starts_with") +} diff --git a/src/tools/clippy/clippy_lints/src/methods/clone_on_copy.rs b/src/tools/clippy/clippy_lints/src/methods/clone_on_copy.rs new file mode 100644 index 000000000..0b38a0720 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/clone_on_copy.rs @@ -0,0 +1,132 @@ +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::get_parent_node; +use clippy_utils::source::snippet_with_context; +use clippy_utils::sugg; +use clippy_utils::ty::is_copy; +use rustc_errors::Applicability; +use rustc_hir::{BindingAnnotation, Expr, ExprKind, MatchSource, Node, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, adjustment::Adjust}; +use rustc_span::symbol::{sym, Symbol}; + +use super::CLONE_DOUBLE_REF; +use super::CLONE_ON_COPY; + +/// Checks for the `CLONE_ON_COPY` lint. +#[allow(clippy::too_many_lines)] +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, method_name: Symbol, args: &[Expr<'_>]) { + let arg = match args { + [arg] if method_name == sym::clone => arg, + _ => return, + }; + if cx + .typeck_results() + .type_dependent_def_id(expr.hir_id) + .and_then(|id| cx.tcx.trait_of_item(id)) + .zip(cx.tcx.lang_items().clone_trait()) + .map_or(true, |(x, y)| x != y) + { + return; + } + let arg_adjustments = cx.typeck_results().expr_adjustments(arg); + let arg_ty = arg_adjustments + .last() + .map_or_else(|| cx.typeck_results().expr_ty(arg), |a| a.target); + + let ty = cx.typeck_results().expr_ty(expr); + if let ty::Ref(_, inner, _) = arg_ty.kind() { + if let ty::Ref(_, innermost, _) = inner.kind() { + span_lint_and_then( + cx, + CLONE_DOUBLE_REF, + expr.span, + &format!( + "using `clone` on a double-reference; \ + this will copy the reference of type `{}` instead of cloning the inner type", + ty + ), + |diag| { + if let Some(snip) = sugg::Sugg::hir_opt(cx, arg) { + let mut ty = innermost; + let mut n = 0; + while let ty::Ref(_, inner, _) = ty.kind() { + ty = inner; + n += 1; + } + let refs = "&".repeat(n + 1); + let derefs = "*".repeat(n); + let explicit = format!("<{}{}>::clone({})", refs, ty, snip); + diag.span_suggestion( + expr.span, + "try dereferencing it", + format!("{}({}{}).clone()", refs, derefs, snip.deref()), + Applicability::MaybeIncorrect, + ); + diag.span_suggestion( + expr.span, + "or try being explicit if you are sure, that you want to clone a reference", + explicit, + Applicability::MaybeIncorrect, + ); + } + }, + ); + return; // don't report clone_on_copy + } + } + + if is_copy(cx, ty) { + let parent_is_suffix_expr = match get_parent_node(cx.tcx, expr.hir_id) { + Some(Node::Expr(parent)) => match parent.kind { + // &*x is a nop, &x.clone() is not + ExprKind::AddrOf(..) => return, + // (*x).func() is useless, x.clone().func() can work in case func borrows self + ExprKind::MethodCall(_, [self_arg, ..], _) + if expr.hir_id == self_arg.hir_id && ty != cx.typeck_results().expr_ty_adjusted(expr) => + { + return; + }, + ExprKind::MethodCall(_, [self_arg, ..], _) if expr.hir_id == self_arg.hir_id => true, + ExprKind::Match(_, _, MatchSource::TryDesugar | MatchSource::AwaitDesugar) + | ExprKind::Field(..) + | ExprKind::Index(..) => true, + _ => false, + }, + // local binding capturing a reference + Some(Node::Local(l)) + if matches!( + l.pat.kind, + PatKind::Binding(BindingAnnotation::Ref | BindingAnnotation::RefMut, ..) + ) => + { + return; + }, + _ => false, + }; + + let mut app = Applicability::MachineApplicable; + let snip = snippet_with_context(cx, arg.span, expr.span.ctxt(), "_", &mut app).0; + + let deref_count = arg_adjustments + .iter() + .take_while(|adj| matches!(adj.kind, Adjust::Deref(_))) + .count(); + let (help, sugg) = if deref_count == 0 { + ("try removing the `clone` call", snip.into()) + } else if parent_is_suffix_expr { + ("try dereferencing it", format!("({}{})", "*".repeat(deref_count), snip)) + } else { + ("try dereferencing it", format!("{}{}", "*".repeat(deref_count), snip)) + }; + + span_lint_and_sugg( + cx, + CLONE_ON_COPY, + expr.span, + &format!("using `clone` on type `{}` which implements the `Copy` trait", ty), + help, + sugg, + app, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/clone_on_ref_ptr.rs b/src/tools/clippy/clippy_lints/src/methods/clone_on_ref_ptr.rs new file mode 100644 index 000000000..6417bc813 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/clone_on_ref_ptr.rs @@ -0,0 +1,43 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::paths; +use clippy_utils::source::snippet_with_macro_callsite; +use clippy_utils::ty::{is_type_diagnostic_item, match_type}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::symbol::{sym, Symbol}; + +use super::CLONE_ON_REF_PTR; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, method_name: Symbol, args: &[hir::Expr<'_>]) { + if !(args.len() == 1 && method_name == sym::clone) { + return; + } + let arg = &args[0]; + let obj_ty = cx.typeck_results().expr_ty(arg).peel_refs(); + + if let ty::Adt(_, subst) = obj_ty.kind() { + let caller_type = if is_type_diagnostic_item(cx, obj_ty, sym::Rc) { + "Rc" + } else if is_type_diagnostic_item(cx, obj_ty, sym::Arc) { + "Arc" + } else if match_type(cx, obj_ty, &paths::WEAK_RC) || match_type(cx, obj_ty, &paths::WEAK_ARC) { + "Weak" + } else { + return; + }; + + let snippet = snippet_with_macro_callsite(cx, arg.span, ".."); + + span_lint_and_sugg( + cx, + CLONE_ON_REF_PTR, + expr.span, + "using `.clone()` on a ref-counted pointer", + "try this", + format!("{}::<{}>::clone(&{})", caller_type, subst.type_at(0), snippet), + Applicability::Unspecified, // Sometimes unnecessary ::<_> after Rc/Arc/Weak + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/cloned_instead_of_copied.rs b/src/tools/clippy/clippy_lints/src/methods/cloned_instead_of_copied.rs new file mode 100644 index 000000000..e9aeab2d5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/cloned_instead_of_copied.rs @@ -0,0 +1,45 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::{get_iterator_item_ty, is_copy}; +use clippy_utils::{is_trait_method, meets_msrv, msrvs}; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_semver::RustcVersion; +use rustc_span::{sym, Span}; + +use super::CLONED_INSTEAD_OF_COPIED; + +pub fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span: Span, msrv: Option<RustcVersion>) { + let recv_ty = cx.typeck_results().expr_ty_adjusted(recv); + let inner_ty = match recv_ty.kind() { + // `Option<T>` -> `T` + ty::Adt(adt, subst) + if cx.tcx.is_diagnostic_item(sym::Option, adt.did()) && meets_msrv(msrv, msrvs::OPTION_COPIED) => + { + subst.type_at(0) + }, + _ if is_trait_method(cx, expr, sym::Iterator) && meets_msrv(msrv, msrvs::ITERATOR_COPIED) => { + match get_iterator_item_ty(cx, recv_ty) { + // <T as Iterator>::Item + Some(ty) => ty, + _ => return, + } + }, + _ => return, + }; + match inner_ty.kind() { + // &T where T: Copy + ty::Ref(_, ty, _) if is_copy(cx, *ty) => {}, + _ => return, + }; + span_lint_and_sugg( + cx, + CLONED_INSTEAD_OF_COPIED, + span, + "used `cloned` where `copied` could be used instead", + "try", + "copied".into(), + Applicability::MachineApplicable, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/err_expect.rs b/src/tools/clippy/clippy_lints/src/methods/err_expect.rs new file mode 100644 index 000000000..570a1b873 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/err_expect.rs @@ -0,0 +1,60 @@ +use super::ERR_EXPECT; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::implements_trait; +use clippy_utils::{meets_msrv, msrvs, ty::is_type_diagnostic_item}; +use rustc_errors::Applicability; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_middle::ty::Ty; +use rustc_semver::RustcVersion; +use rustc_span::{sym, Span}; + +pub(super) fn check( + cx: &LateContext<'_>, + _expr: &rustc_hir::Expr<'_>, + recv: &rustc_hir::Expr<'_>, + msrv: Option<RustcVersion>, + expect_span: Span, + err_span: Span, +) { + if_chain! { + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result); + // Test the version to make sure the lint can be showed (expect_err has been + // introduced in rust 1.17.0 : https://github.com/rust-lang/rust/pull/38982) + if meets_msrv(msrv, msrvs::EXPECT_ERR); + + // Grabs the `Result<T, E>` type + let result_type = cx.typeck_results().expr_ty(recv); + // Tests if the T type in a `Result<T, E>` is not None + if let Some(data_type) = get_data_type(cx, result_type); + // Tests if the T type in a `Result<T, E>` implements debug + if has_debug_impl(data_type, cx); + + then { + span_lint_and_sugg( + cx, + ERR_EXPECT, + err_span.to(expect_span), + "called `.err().expect()` on a `Result` value", + "try", + "expect_err".to_string(), + Applicability::MachineApplicable + ); + } + }; +} + +/// Given a `Result<T, E>` type, return its data (`T`). +fn get_data_type<'a>(cx: &LateContext<'_>, ty: Ty<'a>) -> Option<Ty<'a>> { + match ty.kind() { + ty::Adt(_, substs) if is_type_diagnostic_item(cx, ty, sym::Result) => substs.types().next(), + _ => None, + } +} + +/// Given a type, very if the Debug trait has been impl'd +fn has_debug_impl<'tcx>(ty: Ty<'tcx>, cx: &LateContext<'tcx>) -> bool { + cx.tcx + .get_diagnostic_item(sym::Debug) + .map_or(false, |debug| implements_trait(cx, ty, debug, &[])) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/expect_fun_call.rs b/src/tools/clippy/clippy_lints/src/methods/expect_fun_call.rs new file mode 100644 index 000000000..6f2307d8f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/expect_fun_call.rs @@ -0,0 +1,173 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn}; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::source_map::Span; +use rustc_span::symbol::sym; +use std::borrow::Cow; + +use super::EXPECT_FUN_CALL; + +/// Checks for the `EXPECT_FUN_CALL` lint. +#[allow(clippy::too_many_lines)] +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &hir::Expr<'_>, + method_span: Span, + name: &str, + args: &'tcx [hir::Expr<'tcx>], +) { + // Strip `&`, `as_ref()` and `as_str()` off `arg` until we're left with either a `String` or + // `&str` + fn get_arg_root<'a>(cx: &LateContext<'_>, arg: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> { + let mut arg_root = arg; + loop { + arg_root = match &arg_root.kind { + hir::ExprKind::AddrOf(hir::BorrowKind::Ref, _, expr) => expr, + hir::ExprKind::MethodCall(method_name, call_args, _) => { + if call_args.len() == 1 + && (method_name.ident.name == sym::as_str || method_name.ident.name == sym::as_ref) + && { + let arg_type = cx.typeck_results().expr_ty(&call_args[0]); + let base_type = arg_type.peel_refs(); + *base_type.kind() == ty::Str || is_type_diagnostic_item(cx, base_type, sym::String) + } + { + &call_args[0] + } else { + break; + } + }, + _ => break, + }; + } + arg_root + } + + // Only `&'static str` or `String` can be used directly in the `panic!`. Other types should be + // converted to string. + fn requires_to_string(cx: &LateContext<'_>, arg: &hir::Expr<'_>) -> bool { + let arg_ty = cx.typeck_results().expr_ty(arg); + if is_type_diagnostic_item(cx, arg_ty, sym::String) { + return false; + } + if let ty::Ref(_, ty, ..) = arg_ty.kind() { + if *ty.kind() == ty::Str && can_be_static_str(cx, arg) { + return false; + } + }; + true + } + + // Check if an expression could have type `&'static str`, knowing that it + // has type `&str` for some lifetime. + fn can_be_static_str(cx: &LateContext<'_>, arg: &hir::Expr<'_>) -> bool { + match arg.kind { + hir::ExprKind::Lit(_) => true, + hir::ExprKind::Call(fun, _) => { + if let hir::ExprKind::Path(ref p) = fun.kind { + match cx.qpath_res(p, fun.hir_id) { + hir::def::Res::Def(hir::def::DefKind::Fn | hir::def::DefKind::AssocFn, def_id) => matches!( + cx.tcx.fn_sig(def_id).output().skip_binder().kind(), + ty::Ref(re, ..) if re.is_static(), + ), + _ => false, + } + } else { + false + } + }, + hir::ExprKind::MethodCall(..) => { + cx.typeck_results() + .type_dependent_def_id(arg.hir_id) + .map_or(false, |method_id| { + matches!( + cx.tcx.fn_sig(method_id).output().skip_binder().kind(), + ty::Ref(re, ..) if re.is_static() + ) + }) + }, + hir::ExprKind::Path(ref p) => matches!( + cx.qpath_res(p, arg.hir_id), + hir::def::Res::Def(hir::def::DefKind::Const | hir::def::DefKind::Static(_), _) + ), + _ => false, + } + } + + fn is_call(node: &hir::ExprKind<'_>) -> bool { + match node { + hir::ExprKind::AddrOf(hir::BorrowKind::Ref, _, expr) => { + is_call(&expr.kind) + }, + hir::ExprKind::Call(..) + | hir::ExprKind::MethodCall(..) + // These variants are debatable or require further examination + | hir::ExprKind::If(..) + | hir::ExprKind::Match(..) + | hir::ExprKind::Block{ .. } => true, + _ => false, + } + } + + if args.len() != 2 || name != "expect" || !is_call(&args[1].kind) { + return; + } + + let receiver_type = cx.typeck_results().expr_ty_adjusted(&args[0]); + let closure_args = if is_type_diagnostic_item(cx, receiver_type, sym::Option) { + "||" + } else if is_type_diagnostic_item(cx, receiver_type, sym::Result) { + "|_|" + } else { + return; + }; + + let arg_root = get_arg_root(cx, &args[1]); + + let span_replace_word = method_span.with_hi(expr.span.hi()); + + let mut applicability = Applicability::MachineApplicable; + + //Special handling for `format!` as arg_root + if let Some(macro_call) = root_macro_call_first_node(cx, arg_root) { + if !cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id) { + return; + } + let Some(format_args) = FormatArgsExpn::find_nested(cx, arg_root, macro_call.expn) else { return }; + let span = format_args.inputs_span(); + let sugg = snippet_with_applicability(cx, span, "..", &mut applicability); + span_lint_and_sugg( + cx, + EXPECT_FUN_CALL, + span_replace_word, + &format!("use of `{}` followed by a function call", name), + "try this", + format!("unwrap_or_else({} panic!({}))", closure_args, sugg), + applicability, + ); + return; + } + + let mut arg_root_snippet: Cow<'_, _> = snippet_with_applicability(cx, arg_root.span, "..", &mut applicability); + if requires_to_string(cx, arg_root) { + arg_root_snippet.to_mut().push_str(".to_string()"); + } + + span_lint_and_sugg( + cx, + EXPECT_FUN_CALL, + span_replace_word, + &format!("use of `{}` followed by a function call", name), + "try this", + format!( + "unwrap_or_else({} {{ panic!(\"{{}}\", {}) }})", + closure_args, arg_root_snippet + ), + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/expect_used.rs b/src/tools/clippy/clippy_lints/src/methods/expect_used.rs new file mode 100644 index 000000000..fbc3348f1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/expect_used.rs @@ -0,0 +1,36 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_in_test_function; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::EXPECT_USED; + +/// lint use of `expect()` for `Option`s and `Result`s +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>, allow_expect_in_tests: bool) { + let obj_ty = cx.typeck_results().expr_ty(recv).peel_refs(); + + let mess = if is_type_diagnostic_item(cx, obj_ty, sym::Option) { + Some((EXPECT_USED, "an Option", "None")) + } else if is_type_diagnostic_item(cx, obj_ty, sym::Result) { + Some((EXPECT_USED, "a Result", "Err")) + } else { + None + }; + + if allow_expect_in_tests && is_in_test_function(cx.tcx, expr.hir_id) { + return; + } + + if let Some((lint, kind, none_value)) = mess { + span_lint_and_help( + cx, + lint, + expr.span, + &format!("used `expect()` on `{}` value", kind,), + None, + &format!("if this value is an `{}`, it will panic", none_value,), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/extend_with_drain.rs b/src/tools/clippy/clippy_lints/src/methods/extend_with_drain.rs new file mode 100644 index 000000000..a15fe6094 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/extend_with_drain.rs @@ -0,0 +1,45 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, LangItem}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +use super::EXTEND_WITH_DRAIN; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<'_>) { + let ty = cx.typeck_results().expr_ty(recv).peel_refs(); + if_chain! { + if is_type_diagnostic_item(cx, ty, sym::Vec); + //check source object + if let ExprKind::MethodCall(src_method, [drain_vec, drain_arg], _) = &arg.kind; + if src_method.ident.as_str() == "drain"; + let src_ty = cx.typeck_results().expr_ty(drain_vec); + //check if actual src type is mutable for code suggestion + let immutable = src_ty.is_mutable_ptr(); + let src_ty = src_ty.peel_refs(); + if is_type_diagnostic_item(cx, src_ty, sym::Vec); + //check drain range + if let src_ty_range = cx.typeck_results().expr_ty(drain_arg).peel_refs(); + if is_type_lang_item(cx, src_ty_range, LangItem::RangeFull); + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + EXTEND_WITH_DRAIN, + expr.span, + "use of `extend` instead of `append` for adding the full range of a second vector", + "try this", + format!( + "{}.append({}{})", + snippet_with_applicability(cx, recv.span, "..", &mut applicability), + if immutable { "" } else { "&mut " }, + snippet_with_applicability(cx, drain_vec.span, "..", &mut applicability) + ), + applicability, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/filetype_is_file.rs b/src/tools/clippy/clippy_lints/src/methods/filetype_is_file.rs new file mode 100644 index 000000000..7b2967feb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/filetype_is_file.rs @@ -0,0 +1,41 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::match_type; +use clippy_utils::{get_parent_expr, paths}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::source_map::Span; + +use super::FILETYPE_IS_FILE; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>) { + let ty = cx.typeck_results().expr_ty(recv); + + if !match_type(cx, ty, &paths::FILE_TYPE) { + return; + } + + let span: Span; + let verb: &str; + let lint_unary: &str; + let help_unary: &str; + if_chain! { + if let Some(parent) = get_parent_expr(cx, expr); + if let hir::ExprKind::Unary(op, _) = parent.kind; + if op == hir::UnOp::Not; + then { + lint_unary = "!"; + verb = "denies"; + help_unary = ""; + span = parent.span; + } else { + lint_unary = ""; + verb = "covers"; + help_unary = "!"; + span = expr.span; + } + } + let lint_msg = format!("`{}FileType::is_file()` only {} regular files", lint_unary, verb); + let help_msg = format!("use `{}FileType::is_dir()` instead", help_unary); + span_lint_and_help(cx, FILETYPE_IS_FILE, span, &lint_msg, None, &help_msg); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/filter_map.rs b/src/tools/clippy/clippy_lints/src/methods/filter_map.rs new file mode 100644 index 000000000..692e22a7c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/filter_map.rs @@ -0,0 +1,197 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{indent_of, reindent_multiline, snippet}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{is_trait_method, path_to_local_id, peel_blocks, SpanlessEq}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::def::Res; +use rustc_hir::{Closure, Expr, ExprKind, PatKind, PathSegment, QPath, UnOp}; +use rustc_lint::LateContext; +use rustc_middle::ty::adjustment::Adjust; +use rustc_span::source_map::Span; +use rustc_span::symbol::{sym, Symbol}; +use std::borrow::Cow; + +use super::MANUAL_FILTER_MAP; +use super::MANUAL_FIND_MAP; +use super::OPTION_FILTER_MAP; + +fn is_method<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>, method_name: Symbol) -> bool { + match &expr.kind { + hir::ExprKind::Path(QPath::TypeRelative(_, mname)) => mname.ident.name == method_name, + hir::ExprKind::Path(QPath::Resolved(_, segments)) => { + segments.segments.last().unwrap().ident.name == method_name + }, + hir::ExprKind::Closure(&hir::Closure { body, .. }) => { + let body = cx.tcx.hir().body(body); + let closure_expr = peel_blocks(&body.value); + let arg_id = body.params[0].pat.hir_id; + match closure_expr.kind { + hir::ExprKind::MethodCall(hir::PathSegment { ident, .. }, args, _) => { + if_chain! { + if ident.name == method_name; + if let hir::ExprKind::Path(path) = &args[0].kind; + if let Res::Local(ref local) = cx.qpath_res(path, args[0].hir_id); + then { + return arg_id == *local + } + } + false + }, + _ => false, + } + }, + _ => false, + } +} + +fn is_option_filter_map<'tcx>(cx: &LateContext<'tcx>, filter_arg: &hir::Expr<'_>, map_arg: &hir::Expr<'_>) -> bool { + is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_some)) +} + +/// is `filter(|x| x.is_some()).map(|x| x.unwrap())` +fn is_filter_some_map_unwrap( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + filter_recv: &hir::Expr<'_>, + filter_arg: &hir::Expr<'_>, + map_arg: &hir::Expr<'_>, +) -> bool { + let iterator = is_trait_method(cx, expr, sym::Iterator); + let option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(filter_recv), sym::Option); + + (iterator || option) && is_option_filter_map(cx, filter_arg, map_arg) +} + +/// lint use of `filter().map()` or `find().map()` for `Iterators` +#[allow(clippy::too_many_arguments)] +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &hir::Expr<'_>, + filter_recv: &hir::Expr<'_>, + filter_arg: &hir::Expr<'_>, + filter_span: Span, + map_recv: &hir::Expr<'_>, + map_arg: &hir::Expr<'_>, + map_span: Span, + is_find: bool, +) { + if is_filter_some_map_unwrap(cx, expr, filter_recv, filter_arg, map_arg) { + span_lint_and_sugg( + cx, + OPTION_FILTER_MAP, + filter_span.with_hi(expr.span.hi()), + "`filter` for `Some` followed by `unwrap`", + "consider using `flatten` instead", + reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, map_span)).into_owned(), + Applicability::MachineApplicable, + ); + + return; + } + + if_chain! { + if is_trait_method(cx, map_recv, sym::Iterator); + + // filter(|x| ...is_some())... + if let ExprKind::Closure(&Closure { body: filter_body_id, .. }) = filter_arg.kind; + let filter_body = cx.tcx.hir().body(filter_body_id); + if let [filter_param] = filter_body.params; + // optional ref pattern: `filter(|&x| ..)` + let (filter_pat, is_filter_param_ref) = if let PatKind::Ref(ref_pat, _) = filter_param.pat.kind { + (ref_pat, true) + } else { + (filter_param.pat, false) + }; + // closure ends with is_some() or is_ok() + if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind; + if let ExprKind::MethodCall(path, [filter_arg], _) = filter_body.value.kind; + if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).peel_refs().ty_adt_def(); + if let Some(is_result) = if cx.tcx.is_diagnostic_item(sym::Option, opt_ty.did()) { + Some(false) + } else if cx.tcx.is_diagnostic_item(sym::Result, opt_ty.did()) { + Some(true) + } else { + None + }; + if path.ident.name.as_str() == if is_result { "is_ok" } else { "is_some" }; + + // ...map(|x| ...unwrap()) + if let ExprKind::Closure(&Closure { body: map_body_id, .. }) = map_arg.kind; + let map_body = cx.tcx.hir().body(map_body_id); + if let [map_param] = map_body.params; + if let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind; + // closure ends with expect() or unwrap() + if let ExprKind::MethodCall(seg, [map_arg, ..], _) = map_body.value.kind; + if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or); + + // .filter(..).map(|y| f(y).copied().unwrap()) + // ~~~~ + let map_arg_peeled = match map_arg.kind { + ExprKind::MethodCall(method, [original_arg], _) if acceptable_methods(method) => { + original_arg + }, + _ => map_arg, + }; + + // .filter(|x| x.is_some()).map(|y| y[.acceptable_method()].unwrap()) + let simple_equal = path_to_local_id(filter_arg, filter_param_id) + && path_to_local_id(map_arg_peeled, map_param_id); + + let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| { + // in `filter(|x| ..)`, replace `*x` with `x` + let a_path = if_chain! { + if !is_filter_param_ref; + if let ExprKind::Unary(UnOp::Deref, expr_path) = a.kind; + then { expr_path } else { a } + }; + // let the filter closure arg and the map closure arg be equal + path_to_local_id(a_path, filter_param_id) + && path_to_local_id(b, map_param_id) + && cx.typeck_results().expr_ty_adjusted(a) == cx.typeck_results().expr_ty_adjusted(b) + }; + + if simple_equal || SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg_peeled); + then { + let span = filter_span.with_hi(expr.span.hi()); + let (filter_name, lint) = if is_find { + ("find", MANUAL_FIND_MAP) + } else { + ("filter", MANUAL_FILTER_MAP) + }; + let msg = format!("`{filter_name}(..).map(..)` can be simplified as `{filter_name}_map(..)`"); + let (to_opt, deref) = if is_result { + (".ok()", String::new()) + } else { + let derefs = cx.typeck_results() + .expr_adjustments(map_arg) + .iter() + .filter(|adj| matches!(adj.kind, Adjust::Deref(_))) + .count(); + + ("", "*".repeat(derefs)) + }; + let sugg = format!( + "{filter_name}_map(|{map_param_ident}| {deref}{}{to_opt})", + snippet(cx, map_arg.span, ".."), + ); + span_lint_and_sugg(cx, lint, span, &msg, "try", sugg, Applicability::MachineApplicable); + } + } +} + +fn acceptable_methods(method: &PathSegment<'_>) -> bool { + let methods: [Symbol; 8] = [ + sym::clone, + sym::as_ref, + sym!(copied), + sym!(cloned), + sym!(as_deref), + sym!(as_mut), + sym!(as_deref_mut), + sym!(to_owned), + ]; + + methods.contains(&method.ident.name) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/filter_map_identity.rs b/src/tools/clippy/clippy_lints/src/methods/filter_map_identity.rs new file mode 100644 index 000000000..d1b5e945d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/filter_map_identity.rs @@ -0,0 +1,22 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::{is_expr_identity_function, is_trait_method}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::{source_map::Span, sym}; + +use super::FILTER_MAP_IDENTITY; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, filter_map_arg: &hir::Expr<'_>, filter_map_span: Span) { + if is_trait_method(cx, expr, sym::Iterator) && is_expr_identity_function(cx, filter_map_arg) { + span_lint_and_sugg( + cx, + FILTER_MAP_IDENTITY, + filter_map_span.with_hi(expr.span.hi()), + "use of `filter_map` with an identity function", + "try", + "flatten()".to_string(), + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/filter_map_next.rs b/src/tools/clippy/clippy_lints/src/methods/filter_map_next.rs new file mode 100644 index 000000000..38ec4d8e3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/filter_map_next.rs @@ -0,0 +1,42 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; +use clippy_utils::source::snippet; +use clippy_utils::{is_trait_method, meets_msrv, msrvs}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_semver::RustcVersion; +use rustc_span::sym; + +use super::FILTER_MAP_NEXT; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + recv: &'tcx hir::Expr<'_>, + arg: &'tcx hir::Expr<'_>, + msrv: Option<RustcVersion>, +) { + if is_trait_method(cx, expr, sym::Iterator) { + if !meets_msrv(msrv, msrvs::ITERATOR_FIND_MAP) { + return; + } + + let msg = "called `filter_map(..).next()` on an `Iterator`. This is more succinctly expressed by calling \ + `.find_map(..)` instead"; + let filter_snippet = snippet(cx, arg.span, ".."); + if filter_snippet.lines().count() <= 1 { + let iter_snippet = snippet(cx, recv.span, ".."); + span_lint_and_sugg( + cx, + FILTER_MAP_NEXT, + expr.span, + msg, + "try this", + format!("{}.find_map({})", iter_snippet, filter_snippet), + Applicability::MachineApplicable, + ); + } else { + span_lint(cx, FILTER_MAP_NEXT, expr.span, msg); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/filter_next.rs b/src/tools/clippy/clippy_lints/src/methods/filter_next.rs new file mode 100644 index 000000000..bcf8d93b6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/filter_next.rs @@ -0,0 +1,42 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; +use clippy_utils::source::snippet; +use clippy_utils::ty::implements_trait; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::FILTER_NEXT; + +/// lint use of `filter().next()` for `Iterators` +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + recv: &'tcx hir::Expr<'_>, + filter_arg: &'tcx hir::Expr<'_>, +) { + // lint if caller of `.filter().next()` is an Iterator + let recv_impls_iterator = cx.tcx.get_diagnostic_item(sym::Iterator).map_or(false, |id| { + implements_trait(cx, cx.typeck_results().expr_ty(recv), id, &[]) + }); + if recv_impls_iterator { + let msg = "called `filter(..).next()` on an `Iterator`. This is more succinctly expressed by calling \ + `.find(..)` instead"; + let filter_snippet = snippet(cx, filter_arg.span, ".."); + if filter_snippet.lines().count() <= 1 { + let iter_snippet = snippet(cx, recv.span, ".."); + // add note if not multi-line + span_lint_and_sugg( + cx, + FILTER_NEXT, + expr.span, + msg, + "try this", + format!("{}.find({})", iter_snippet, filter_snippet), + Applicability::MachineApplicable, + ); + } else { + span_lint(cx, FILTER_NEXT, expr.span, msg); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/flat_map_identity.rs b/src/tools/clippy/clippy_lints/src/methods/flat_map_identity.rs new file mode 100644 index 000000000..6f911d79d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/flat_map_identity.rs @@ -0,0 +1,28 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::{is_expr_identity_function, is_trait_method}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::{source_map::Span, sym}; + +use super::FLAT_MAP_IDENTITY; + +/// lint use of `flat_map` for `Iterators` where `flatten` would be sufficient +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + flat_map_arg: &'tcx hir::Expr<'_>, + flat_map_span: Span, +) { + if is_trait_method(cx, expr, sym::Iterator) && is_expr_identity_function(cx, flat_map_arg) { + span_lint_and_sugg( + cx, + FLAT_MAP_IDENTITY, + flat_map_span.with_hi(expr.span.hi()), + "use of `flat_map` with an identity function", + "try", + "flatten()".to_string(), + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/flat_map_option.rs b/src/tools/clippy/clippy_lints/src/methods/flat_map_option.rs new file mode 100644 index 000000000..615bde941 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/flat_map_option.rs @@ -0,0 +1,34 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_trait_method; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::{source_map::Span, sym}; + +use super::FLAT_MAP_OPTION; +use clippy_utils::ty::is_type_diagnostic_item; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>, arg: &'tcx hir::Expr<'_>, span: Span) { + if !is_trait_method(cx, expr, sym::Iterator) { + return; + } + let arg_ty = cx.typeck_results().expr_ty_adjusted(arg); + let sig = match arg_ty.kind() { + ty::Closure(_, substs) => substs.as_closure().sig(), + _ if arg_ty.is_fn() => arg_ty.fn_sig(cx.tcx), + _ => return, + }; + if !is_type_diagnostic_item(cx, sig.output().skip_binder(), sym::Option) { + return; + } + span_lint_and_sugg( + cx, + FLAT_MAP_OPTION, + span, + "used `flat_map` where `filter_map` could be used instead", + "try", + "filter_map".into(), + Applicability::MachineApplicable, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/from_iter_instead_of_collect.rs b/src/tools/clippy/clippy_lints/src/methods/from_iter_instead_of_collect.rs new file mode 100644 index 000000000..6436e28a6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/from_iter_instead_of_collect.rs @@ -0,0 +1,83 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::implements_trait; +use clippy_utils::{is_expr_path_def_path, paths, sugg}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty::Ty; +use rustc_span::sym; + +use super::FROM_ITER_INSTEAD_OF_COLLECT; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, args: &[hir::Expr<'_>], func: &hir::Expr<'_>) { + if_chain! { + if is_expr_path_def_path(cx, func, &paths::FROM_ITERATOR_METHOD); + let ty = cx.typeck_results().expr_ty(expr); + let arg_ty = cx.typeck_results().expr_ty(&args[0]); + if let Some(iter_id) = cx.tcx.get_diagnostic_item(sym::Iterator); + + if implements_trait(cx, arg_ty, iter_id, &[]); + then { + // `expr` implements `FromIterator` trait + let iter_expr = sugg::Sugg::hir(cx, &args[0], "..").maybe_par(); + let turbofish = extract_turbofish(cx, expr, ty); + let sugg = format!("{}.collect::<{}>()", iter_expr, turbofish); + span_lint_and_sugg( + cx, + FROM_ITER_INSTEAD_OF_COLLECT, + expr.span, + "usage of `FromIterator::from_iter`", + "use `.collect()` instead of `::from_iter()`", + sugg, + Applicability::MaybeIncorrect, + ); + } + } +} + +fn extract_turbofish(cx: &LateContext<'_>, expr: &hir::Expr<'_>, ty: Ty<'_>) -> String { + fn strip_angle_brackets(s: &str) -> Option<&str> { + s.strip_prefix('<')?.strip_suffix('>') + } + + let call_site = expr.span.source_callsite(); + if_chain! { + if let Some(snippet) = snippet_opt(cx, call_site); + let snippet_split = snippet.split("::").collect::<Vec<_>>(); + if let Some((_, elements)) = snippet_split.split_last(); + + then { + if_chain! { + if let [type_specifier, _] = snippet_split.as_slice(); + if let Some(type_specifier) = strip_angle_brackets(type_specifier); + if let Some((type_specifier, ..)) = type_specifier.split_once(" as "); + then { + type_specifier.to_string() + } else { + // is there a type specifier? (i.e.: like `<u32>` in `collections::BTreeSet::<u32>::`) + if let Some(type_specifier) = snippet_split.iter().find(|e| strip_angle_brackets(e).is_some()) { + // remove the type specifier from the path elements + let without_ts = elements.iter().filter_map(|e| { + if e == type_specifier { None } else { Some((*e).to_string()) } + }).collect::<Vec<_>>(); + // join and add the type specifier at the end (i.e.: `collections::BTreeSet<u32>`) + format!("{}{}", without_ts.join("::"), type_specifier) + } else { + // type is not explicitly specified so wildcards are needed + // i.e.: 2 wildcards in `std::collections::BTreeMap<&i32, &char>` + let ty_str = ty.to_string(); + let start = ty_str.find('<').unwrap_or(0); + let end = ty_str.find('>').unwrap_or(ty_str.len()); + let nb_wildcard = ty_str[start..end].split(',').count(); + let wildcards = format!("_{}", ", _".repeat(nb_wildcard - 1)); + format!("{}<{}>", elements.join("::"), wildcards) + } + } + } + } else { + ty.to_string() + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/get_last_with_len.rs b/src/tools/clippy/clippy_lints/src/methods/get_last_with_len.rs new file mode 100644 index 000000000..23368238e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/get_last_with_len.rs @@ -0,0 +1,55 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::SpanlessEq; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::source_map::Spanned; +use rustc_span::sym; + +use super::GET_LAST_WITH_LEN; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<'_>) { + // Argument to "get" is a subtraction + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Sub, .. + }, + lhs, + rhs, + ) = arg.kind + + // LHS of subtraction is "x.len()" + && let ExprKind::MethodCall(lhs_path, [lhs_recv], _) = &lhs.kind + && lhs_path.ident.name == sym::len + + // RHS of subtraction is 1 + && let ExprKind::Lit(rhs_lit) = &rhs.kind + && let LitKind::Int(1, ..) = rhs_lit.node + + // check that recv == lhs_recv `recv.get(lhs_recv.len() - 1)` + && SpanlessEq::new(cx).eq_expr(recv, lhs_recv) + && !recv.can_have_side_effects() + { + let method = match cx.typeck_results().expr_ty_adjusted(recv).peel_refs().kind() { + ty::Adt(def, _) if cx.tcx.is_diagnostic_item(sym::VecDeque, def.did()) => "back", + ty::Slice(_) => "last", + _ => return, + }; + + let mut applicability = Applicability::MachineApplicable; + let recv_snippet = snippet_with_applicability(cx, recv.span, "_", &mut applicability); + + span_lint_and_sugg( + cx, + GET_LAST_WITH_LEN, + expr.span, + &format!("accessing last element with `{recv_snippet}.get({recv_snippet}.len() - 1)`"), + "try", + format!("{recv_snippet}.{method}()"), + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/get_unwrap.rs b/src/tools/clippy/clippy_lints/src/methods/get_unwrap.rs new file mode 100644 index 000000000..18e08d6ee --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/get_unwrap.rs @@ -0,0 +1,87 @@ +use super::utils::derefs_to_slice; +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; +use rustc_span::sym; + +use super::GET_UNWRAP; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &hir::Expr<'_>, + recv: &'tcx hir::Expr<'tcx>, + get_arg: &'tcx hir::Expr<'_>, + is_mut: bool, +) { + // Note: we don't want to lint `get_mut().unwrap` for `HashMap` or `BTreeMap`, + // because they do not implement `IndexMut` + 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 + }; + + 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; + span = parent.span; + } + } + + let mut_str = if is_mut { "_mut" } else { "" }; + let borrow_str = if !needs_ref { + "" + } else if is_mut { + "&mut " + } else { + "&" + }; + + span_lint_and_sugg( + cx, + GET_UNWRAP, + span, + &format!( + "called `.get{0}().unwrap()` on a {1}. Using `[]` is more clear and more concise", + mut_str, caller_type + ), + "try this", + format!( + "{}{}[{}]", + borrow_str, + snippet_with_applicability(cx, recv.span, "..", &mut applicability), + get_args_str + ), + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/implicit_clone.rs b/src/tools/clippy/clippy_lints/src/methods/implicit_clone.rs new file mode 100644 index 000000000..9651a52be --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/implicit_clone.rs @@ -0,0 +1,58 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_context; +use clippy_utils::ty::peel_mid_ty_refs; +use clippy_utils::{is_diag_item_method, is_diag_trait_item}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::IMPLICIT_CLONE; + +pub fn check(cx: &LateContext<'_>, method_name: &str, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>) { + if_chain! { + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if is_clone_like(cx, method_name, method_def_id); + let return_type = cx.typeck_results().expr_ty(expr); + let input_type = cx.typeck_results().expr_ty(recv); + let (input_type, ref_count) = peel_mid_ty_refs(input_type); + if let Some(ty_name) = input_type.ty_adt_def().map(|adt_def| cx.tcx.item_name(adt_def.did())); + if return_type == input_type; + then { + let mut app = Applicability::MachineApplicable; + let recv_snip = snippet_with_context(cx, recv.span, expr.span.ctxt(), "..", &mut app).0; + span_lint_and_sugg( + cx, + IMPLICIT_CLONE, + expr.span, + &format!("implicitly cloning a `{}` by calling `{}` on its dereferenced type", ty_name, method_name), + "consider using", + if ref_count > 1 { + format!("({}{}).clone()", "*".repeat(ref_count - 1), recv_snip) + } else { + format!("{}.clone()", recv_snip) + }, + app, + ); + } + } +} + +/// Returns true if the named method can be used to clone the receiver. +/// Note that `to_string` is not flagged by `implicit_clone`. So other lints that call +/// `is_clone_like` and that do flag `to_string` must handle it separately. See, e.g., +/// `is_to_owned_like` in `unnecessary_to_owned.rs`. +pub fn is_clone_like(cx: &LateContext<'_>, method_name: &str, method_def_id: hir::def_id::DefId) -> bool { + match method_name { + "to_os_string" => is_diag_item_method(cx, method_def_id, sym::OsStr), + "to_owned" => is_diag_trait_item(cx, method_def_id, sym::ToOwned), + "to_path_buf" => is_diag_item_method(cx, method_def_id, sym::Path), + "to_vec" => cx + .tcx + .impl_of_method(method_def_id) + .filter(|&impl_did| cx.tcx.type_of(impl_did).is_slice() && cx.tcx.impl_trait_ref(impl_did).is_none()) + .is_some(), + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/inefficient_to_string.rs b/src/tools/clippy/clippy_lints/src/methods/inefficient_to_string.rs new file mode 100644 index 000000000..f52170df6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/inefficient_to_string.rs @@ -0,0 +1,67 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::{is_type_diagnostic_item, walk_ptrs_ty_depth}; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::symbol::{sym, Symbol}; + +use super::INEFFICIENT_TO_STRING; + +/// Checks for the `INEFFICIENT_TO_STRING` lint +pub fn check<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>, method_name: Symbol, args: &[hir::Expr<'_>]) { + if_chain! { + if args.len() == 1 && method_name == sym::to_string; + if let Some(to_string_meth_did) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if match_def_path(cx, to_string_meth_did, &paths::TO_STRING_METHOD); + if let Some(substs) = cx.typeck_results().node_substs_opt(expr.hir_id); + let arg_ty = cx.typeck_results().expr_ty_adjusted(&args[0]); + let self_ty = substs.type_at(0); + let (deref_self_ty, deref_count) = walk_ptrs_ty_depth(self_ty); + if deref_count >= 1; + if specializes_tostring(cx, deref_self_ty); + then { + span_lint_and_then( + cx, + INEFFICIENT_TO_STRING, + expr.span, + &format!("calling `to_string` on `{}`", arg_ty), + |diag| { + diag.help(&format!( + "`{}` implements `ToString` through a slower blanket impl, but `{}` has a fast specialization of `ToString`", + self_ty, deref_self_ty + )); + let mut applicability = Applicability::MachineApplicable; + let arg_snippet = snippet_with_applicability(cx, args[0].span, "..", &mut applicability); + diag.span_suggestion( + expr.span, + "try dereferencing the receiver", + format!("({}{}).to_string()", "*".repeat(deref_count), arg_snippet), + applicability, + ); + }, + ); + } + } +} + +/// Returns whether `ty` specializes `ToString`. +/// Currently, these are `str`, `String`, and `Cow<'_, str>`. +fn specializes_tostring(cx: &LateContext<'_>, ty: Ty<'_>) -> bool { + if let ty::Str = ty.kind() { + return true; + } + + if is_type_diagnostic_item(cx, ty, sym::String) { + return true; + } + + if let ty::Adt(adt, substs) = ty.kind() { + match_def_path(cx, adt.did(), &paths::COW) && substs.type_at(1).is_str() + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/inspect_for_each.rs b/src/tools/clippy/clippy_lints/src/methods/inspect_for_each.rs new file mode 100644 index 000000000..7fd3ef1a6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/inspect_for_each.rs @@ -0,0 +1,23 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_trait_method; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::{source_map::Span, sym}; + +use super::INSPECT_FOR_EACH; + +/// lint use of `inspect().for_each()` for `Iterators` +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>, inspect_span: Span) { + if is_trait_method(cx, expr, sym::Iterator) { + let msg = "called `inspect(..).for_each(..)` on an `Iterator`"; + let hint = "move the code from `inspect(..)` to `for_each(..)` and remove the `inspect(..)`"; + span_lint_and_help( + cx, + INSPECT_FOR_EACH, + inspect_span.with_hi(expr.span.hi()), + msg, + None, + hint, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/into_iter_on_ref.rs b/src/tools/clippy/clippy_lints/src/methods/into_iter_on_ref.rs new file mode 100644 index 000000000..da13b4ba3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/into_iter_on_ref.rs @@ -0,0 +1,56 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_trait_method; +use clippy_utils::ty::has_iter_method; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::source_map::Span; +use rustc_span::symbol::{sym, Symbol}; + +use super::INTO_ITER_ON_REF; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + method_span: Span, + method_name: Symbol, + args: &[hir::Expr<'_>], +) { + let self_ty = cx.typeck_results().expr_ty_adjusted(&args[0]); + if_chain! { + if let ty::Ref(..) = self_ty.kind(); + if method_name == sym::into_iter; + if is_trait_method(cx, expr, sym::IntoIterator); + if let Some((kind, method_name)) = ty_has_iter_method(cx, self_ty); + then { + span_lint_and_sugg( + cx, + INTO_ITER_ON_REF, + method_span, + &format!( + "this `.into_iter()` call is equivalent to `.{}()` and will not consume the `{}`", + method_name, kind, + ), + "call directly", + method_name.to_string(), + Applicability::MachineApplicable, + ); + } + } +} + +fn ty_has_iter_method(cx: &LateContext<'_>, self_ref_ty: Ty<'_>) -> Option<(Symbol, &'static str)> { + has_iter_method(cx, self_ref_ty).map(|ty_name| { + let mutbl = match self_ref_ty.kind() { + ty::Ref(_, _, mutbl) => mutbl, + _ => unreachable!(), + }; + let method_name = match mutbl { + hir::Mutability::Not => "iter", + hir::Mutability::Mut => "iter_mut", + }; + (ty_name, method_name) + }) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/is_digit_ascii_radix.rs b/src/tools/clippy/clippy_lints/src/methods/is_digit_ascii_radix.rs new file mode 100644 index 000000000..aa176dcc8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/is_digit_ascii_radix.rs @@ -0,0 +1,50 @@ +//! Lint for `c.is_digit(10)` + +use super::IS_DIGIT_ASCII_RADIX; +use clippy_utils::{ + consts::constant_full_int, consts::FullInt, diagnostics::span_lint_and_sugg, meets_msrv, msrvs, + source::snippet_with_applicability, +}; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_semver::RustcVersion; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + self_arg: &'tcx Expr<'_>, + radix: &'tcx Expr<'_>, + msrv: Option<RustcVersion>, +) { + if !meets_msrv(msrv, msrvs::IS_ASCII_DIGIT) { + return; + } + + if !cx.typeck_results().expr_ty_adjusted(self_arg).peel_refs().is_char() { + return; + } + + if let Some(radix_val) = constant_full_int(cx, cx.typeck_results(), radix) { + let (num, replacement) = match radix_val { + FullInt::S(10) | FullInt::U(10) => (10, "is_ascii_digit"), + FullInt::S(16) | FullInt::U(16) => (16, "is_ascii_hexdigit"), + _ => return, + }; + let mut applicability = Applicability::MachineApplicable; + + span_lint_and_sugg( + cx, + IS_DIGIT_ASCII_RADIX, + expr.span, + &format!("use of `char::is_digit` with literal radix of {}", num), + "try", + format!( + "{}.{}()", + snippet_with_applicability(cx, self_arg.span, "..", &mut applicability), + replacement + ), + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_cloned_collect.rs b/src/tools/clippy/clippy_lints/src/methods/iter_cloned_collect.rs new file mode 100644 index 000000000..30d56113c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_cloned_collect.rs @@ -0,0 +1,31 @@ +use crate::methods::utils::derefs_to_slice; +use clippy_utils::diagnostics::span_lint_and_sugg; +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; +use rustc_span::sym; + +use super::ITER_CLONED_COLLECT; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, method_name: &str, expr: &hir::Expr<'_>, recv: &'tcx hir::Expr<'_>) { + if_chain! { + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(expr), sym::Vec); + if let Some(slice) = derefs_to_slice(cx, recv, cx.typeck_results().expr_ty(recv)); + if let Some(to_replace) = expr.span.trim_start(slice.span.source_callsite()); + + then { + span_lint_and_sugg( + cx, + ITER_CLONED_COLLECT, + to_replace, + &format!("called `iter().{}().collect()` on a slice to create a `Vec`. Calling `to_vec()` is both faster and \ + more readable", method_name), + "try", + ".to_vec()".to_string(), + Applicability::MachineApplicable, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_count.rs b/src/tools/clippy/clippy_lints/src/methods/iter_count.rs new file mode 100644 index 000000000..052be3d8e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_count.rs @@ -0,0 +1,48 @@ +use super::utils::derefs_to_slice; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::ITER_COUNT; + +pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>, recv: &'tcx Expr<'tcx>, iter_method: &str) { + let ty = cx.typeck_results().expr_ty(recv); + let caller_type = if derefs_to_slice(cx, recv, ty).is_some() { + "slice" + } else if is_type_diagnostic_item(cx, ty, sym::Vec) { + "Vec" + } else if is_type_diagnostic_item(cx, ty, sym::VecDeque) { + "VecDeque" + } else if is_type_diagnostic_item(cx, ty, sym::HashSet) { + "HashSet" + } else if is_type_diagnostic_item(cx, ty, sym::HashMap) { + "HashMap" + } else if is_type_diagnostic_item(cx, ty, sym::BTreeMap) { + "BTreeMap" + } else if is_type_diagnostic_item(cx, ty, sym::BTreeSet) { + "BTreeSet" + } else if is_type_diagnostic_item(cx, ty, sym::LinkedList) { + "LinkedList" + } else if is_type_diagnostic_item(cx, ty, sym::BinaryHeap) { + "BinaryHeap" + } else { + return; + }; + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + ITER_COUNT, + expr.span, + &format!("called `.{}().count()` on a `{}`", iter_method, caller_type), + "try", + format!( + "{}.len()", + snippet_with_applicability(cx, recv.span, "..", &mut applicability), + ), + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_next_slice.rs b/src/tools/clippy/clippy_lints/src/methods/iter_next_slice.rs new file mode 100644 index 000000000..b8d1dabe0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_next_slice.rs @@ -0,0 +1,74 @@ +use super::utils::derefs_to_slice; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{get_parent_expr, higher}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::symbol::sym; + +use super::ITER_NEXT_SLICE; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>, caller_expr: &'tcx hir::Expr<'_>) { + // Skip lint if the `iter().next()` expression is a for loop argument, + // since it is already covered by `&loops::ITER_NEXT_LOOP` + let mut parent_expr_opt = get_parent_expr(cx, expr); + while let Some(parent_expr) = parent_expr_opt { + if higher::ForLoop::hir(parent_expr).is_some() { + return; + } + parent_expr_opt = get_parent_expr(cx, parent_expr); + } + + if derefs_to_slice(cx, caller_expr, cx.typeck_results().expr_ty(caller_expr)).is_some() { + // caller is a Slice + if_chain! { + if let hir::ExprKind::Index(caller_var, index_expr) = &caller_expr.kind; + if let Some(higher::Range { start: Some(start_expr), end: None, limits: ast::RangeLimits::HalfOpen }) + = higher::Range::hir(index_expr); + if let hir::ExprKind::Lit(ref start_lit) = &start_expr.kind; + if let ast::LitKind::Int(start_idx, _) = start_lit.node; + then { + let mut applicability = Applicability::MachineApplicable; + let suggest = if start_idx == 0 { + format!("{}.first()", snippet_with_applicability(cx, caller_var.span, "..", &mut applicability)) + } else { + format!("{}.get({})", snippet_with_applicability(cx, caller_var.span, "..", &mut applicability), start_idx) + }; + span_lint_and_sugg( + cx, + ITER_NEXT_SLICE, + expr.span, + "using `.iter().next()` on a Slice without end index", + "try calling", + suggest, + applicability, + ); + } + } + } else if is_vec_or_array(cx, caller_expr) { + // caller is a Vec or an Array + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + ITER_NEXT_SLICE, + expr.span, + "using `.iter().next()` on an array", + "try calling", + format!( + "{}.first()", + snippet_with_applicability(cx, caller_expr.span, "..", &mut applicability) + ), + applicability, + ); + } +} + +fn is_vec_or_array<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) -> bool { + is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(expr), sym::Vec) + || matches!(&cx.typeck_results().expr_ty(expr).peel_refs().kind(), ty::Array(_, _)) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_nth.rs b/src/tools/clippy/clippy_lints/src/methods/iter_nth.rs new file mode 100644 index 000000000..80ca4c942 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_nth.rs @@ -0,0 +1,39 @@ +use super::utils::derefs_to_slice; +use crate::methods::iter_nth_zero; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +use super::ITER_NTH; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &hir::Expr<'_>, + iter_recv: &'tcx hir::Expr<'tcx>, + nth_recv: &hir::Expr<'_>, + nth_arg: &hir::Expr<'_>, + is_mut: bool, +) { + let mut_str = if is_mut { "_mut" } else { "" }; + 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" + } else if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(iter_recv), sym::VecDeque) { + "VecDeque" + } else { + iter_nth_zero::check(cx, expr, nth_recv, nth_arg); + return; // caller is not a type that we want to lint + }; + + span_lint_and_help( + cx, + ITER_NTH, + expr.span, + &format!("called `.iter{0}().nth()` on a {1}", mut_str, caller_type), + None, + &format!("calling `.get{}()` is both faster and more readable", mut_str), + ); +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_nth_zero.rs b/src/tools/clippy/clippy_lints/src/methods/iter_nth_zero.rs new file mode 100644 index 000000000..68d906c3e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_nth_zero.rs @@ -0,0 +1,30 @@ +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 rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::ITER_NTH_ZERO; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, 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, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs b/src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs new file mode 100644 index 000000000..06a39c599 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs @@ -0,0 +1,59 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::{get_associated_type, implements_trait, is_copy}; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::sym; + +use super::ITER_OVEREAGER_CLONED; +use crate::redundant_clone::REDUNDANT_CLONE; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + cloned_call: &'tcx Expr<'_>, + cloned_recv: &'tcx Expr<'_>, + is_count: bool, + needs_into_iter: bool, +) { + let typeck = cx.typeck_results(); + if let Some(iter_id) = cx.tcx.get_diagnostic_item(sym::Iterator) + && let Some(method_id) = typeck.type_dependent_def_id(expr.hir_id) + && cx.tcx.trait_of_item(method_id) == Some(iter_id) + && let Some(method_id) = typeck.type_dependent_def_id(cloned_call.hir_id) + && cx.tcx.trait_of_item(method_id) == Some(iter_id) + && let cloned_recv_ty = typeck.expr_ty_adjusted(cloned_recv) + && let Some(iter_assoc_ty) = get_associated_type(cx, cloned_recv_ty, iter_id, "Item") + && matches!(*iter_assoc_ty.kind(), ty::Ref(_, ty, _) if !is_copy(cx, ty)) + { + if needs_into_iter + && let Some(into_iter_id) = cx.tcx.get_diagnostic_item(sym::IntoIterator) + && !implements_trait(cx, iter_assoc_ty, into_iter_id, &[]) + { + return; + } + + let (lint, msg, trailing_clone) = if is_count { + (REDUNDANT_CLONE, "unneeded cloning of iterator items", "") + } else { + (ITER_OVEREAGER_CLONED, "unnecessarily eager cloning of iterator items", ".cloned()") + }; + + span_lint_and_then( + cx, + lint, + expr.span, + msg, + |diag| { + let method_span = expr.span.with_lo(cloned_call.span.hi()); + if let Some(mut snip) = snippet_opt(cx, method_span) { + snip.push_str(trailing_clone); + let replace_span = expr.span.with_lo(cloned_recv.span.hi()); + diag.span_suggestion(replace_span, "try this", snip, Applicability::MachineApplicable); + } + } + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_skip_next.rs b/src/tools/clippy/clippy_lints/src/methods/iter_skip_next.rs new file mode 100644 index 000000000..43e9451f7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_skip_next.rs @@ -0,0 +1,46 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::is_trait_method; +use clippy_utils::path_to_local; +use clippy_utils::source::snippet; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::{BindingAnnotation, Node, PatKind}; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::ITER_SKIP_NEXT; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>, arg: &hir::Expr<'_>) { + // lint if caller of skip is an Iterator + if is_trait_method(cx, expr, sym::Iterator) { + let mut application = Applicability::MachineApplicable; + span_lint_and_then( + cx, + ITER_SKIP_NEXT, + expr.span.trim_start(recv.span).unwrap(), + "called `skip(..).next()` on an iterator", + |diag| { + if_chain! { + if let Some(id) = path_to_local(recv); + if let Node::Pat(pat) = cx.tcx.hir().get(id); + if let PatKind::Binding(ann, _, _, _) = pat.kind; + if ann != BindingAnnotation::Mutable; + then { + application = Applicability::Unspecified; + diag.span_help( + pat.span, + &format!("for this change `{}` has to be mutable", snippet(cx, pat.span, "..")), + ); + } + } + + diag.span_suggestion( + expr.span.trim_start(recv.span).unwrap(), + "use `nth` instead", + format!(".nth({})", snippet(cx, arg.span, "..")), + application, + ); + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_with_drain.rs b/src/tools/clippy/clippy_lints/src/methods/iter_with_drain.rs new file mode 100644 index 000000000..152072e09 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iter_with_drain.rs @@ -0,0 +1,47 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::higher::Range; +use clippy_utils::is_integer_const; +use rustc_ast::ast::RangeLimits; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, QPath}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; +use rustc_span::Span; + +use super::ITER_WITH_DRAIN; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span: Span, arg: &Expr<'_>) { + if !matches!(recv.kind, ExprKind::Field(..)) + && let Some(adt) = cx.typeck_results().expr_ty(recv).ty_adt_def() + && let Some(ty_name) = cx.tcx.get_diagnostic_name(adt.did()) + && matches!(ty_name, sym::Vec | sym::VecDeque) + && let Some(range) = Range::hir(arg) + && is_full_range(cx, recv, range) + { + span_lint_and_sugg( + cx, + ITER_WITH_DRAIN, + span.with_hi(expr.span.hi()), + &format!("`drain(..)` used on a `{}`", ty_name), + "try this", + "into_iter()".to_string(), + Applicability::MaybeIncorrect, + ); + }; +} + +fn is_full_range(cx: &LateContext<'_>, container: &Expr<'_>, range: Range<'_>) -> bool { + range.start.map_or(true, |e| is_integer_const(cx, e, 0)) + && range.end.map_or(true, |e| { + if range.limits == RangeLimits::HalfOpen + && let ExprKind::Path(QPath::Resolved(None, container_path)) = container.kind + && let ExprKind::MethodCall(name, [self_arg], _) = e.kind + && name.ident.name == sym::len + && let ExprKind::Path(QPath::Resolved(None, path)) = self_arg.kind + { + container_path.res == path.res + } else { + false + } + }) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/iterator_step_by_zero.rs b/src/tools/clippy/clippy_lints/src/methods/iterator_step_by_zero.rs new file mode 100644 index 000000000..64c09214a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/iterator_step_by_zero.rs @@ -0,0 +1,21 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::is_trait_method; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::ITERATOR_STEP_BY_ZERO; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>, arg: &'tcx hir::Expr<'_>) { + if is_trait_method(cx, expr, sym::Iterator) { + if let Some((Constant::Int(0), _)) = constant(cx, cx.typeck_results(), arg) { + span_lint( + cx, + ITERATOR_STEP_BY_ZERO, + expr.span, + "`Iterator::step_by(0)` will panic at runtime", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/manual_saturating_arithmetic.rs b/src/tools/clippy/clippy_lints/src/methods/manual_saturating_arithmetic.rs new file mode 100644 index 000000000..0fe510bea --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/manual_saturating_arithmetic.rs @@ -0,0 +1,164 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{match_def_path, path_def_id}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty::layout::LayoutOf; + +pub fn check( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + arith_lhs: &hir::Expr<'_>, + arith_rhs: &hir::Expr<'_>, + unwrap_arg: &hir::Expr<'_>, + arith: &str, +) { + let ty = cx.typeck_results().expr_ty(arith_lhs); + if !ty.is_integral() { + return; + } + + let mm = if let Some(mm) = is_min_or_max(cx, unwrap_arg) { + mm + } else { + return; + }; + + if ty.is_signed() { + use self::{ + MinMax::{Max, Min}, + Sign::{Neg, Pos}, + }; + + let sign = if let Some(sign) = lit_sign(arith_rhs) { + sign + } else { + return; + }; + + match (arith, sign, mm) { + ("add", Pos, Max) | ("add", Neg, Min) | ("sub", Neg, Max) | ("sub", Pos, Min) => (), + // "mul" is omitted because lhs can be negative. + _ => return, + } + } else { + match (mm, arith) { + (MinMax::Max, "add" | "mul") | (MinMax::Min, "sub") => (), + _ => return, + } + } + + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + super::MANUAL_SATURATING_ARITHMETIC, + expr.span, + "manual saturating arithmetic", + &format!("try using `saturating_{}`", arith), + format!( + "{}.saturating_{}({})", + snippet_with_applicability(cx, arith_lhs.span, "..", &mut applicability), + arith, + snippet_with_applicability(cx, arith_rhs.span, "..", &mut applicability), + ), + applicability, + ); +} + +#[derive(PartialEq, Eq)] +enum MinMax { + Min, + Max, +} + +fn is_min_or_max<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>) -> Option<MinMax> { + // `T::max_value()` `T::min_value()` inherent methods + if_chain! { + if let hir::ExprKind::Call(func, args) = &expr.kind; + if args.is_empty(); + if let hir::ExprKind::Path(hir::QPath::TypeRelative(_, segment)) = &func.kind; + then { + match segment.ident.as_str() { + "max_value" => return Some(MinMax::Max), + "min_value" => return Some(MinMax::Min), + _ => {} + } + } + } + + let ty = cx.typeck_results().expr_ty(expr); + let ty_str = ty.to_string(); + + // `std::T::MAX` `std::T::MIN` constants + if let Some(id) = path_def_id(cx, expr) { + if match_def_path(cx, id, &["core", &ty_str, "MAX"]) { + return Some(MinMax::Max); + } + + if match_def_path(cx, id, &["core", &ty_str, "MIN"]) { + return Some(MinMax::Min); + } + } + + // Literals + let bits = cx.layout_of(ty).unwrap().size.bits(); + let (minval, maxval): (u128, u128) = if ty.is_signed() { + let minval = 1 << (bits - 1); + let mut maxval = !(1 << (bits - 1)); + if bits != 128 { + maxval &= (1 << bits) - 1; + } + (minval, maxval) + } else { + (0, if bits == 128 { !0 } else { (1 << bits) - 1 }) + }; + + let check_lit = |expr: &hir::Expr<'_>, check_min: bool| { + if let hir::ExprKind::Lit(lit) = &expr.kind { + if let ast::LitKind::Int(value, _) = lit.node { + if value == maxval { + return Some(MinMax::Max); + } + + if check_min && value == minval { + return Some(MinMax::Min); + } + } + } + + None + }; + + if let r @ Some(_) = check_lit(expr, !ty.is_signed()) { + return r; + } + + if ty.is_signed() { + if let hir::ExprKind::Unary(hir::UnOp::Neg, val) = &expr.kind { + return check_lit(val, true); + } + } + + None +} + +#[derive(PartialEq, Eq)] +enum Sign { + Pos, + Neg, +} + +fn lit_sign(expr: &hir::Expr<'_>) -> Option<Sign> { + if let hir::ExprKind::Unary(hir::UnOp::Neg, inner) = &expr.kind { + if let hir::ExprKind::Lit(..) = &inner.kind { + return Some(Sign::Neg); + } + } else if let hir::ExprKind::Lit(..) = &expr.kind { + return Some(Sign::Pos); + } + + None +} diff --git a/src/tools/clippy/clippy_lints/src/methods/manual_str_repeat.rs b/src/tools/clippy/clippy_lints/src/methods/manual_str_repeat.rs new file mode 100644 index 000000000..46d2fc493 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/manual_str_repeat.rs @@ -0,0 +1,99 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{snippet_with_applicability, snippet_with_context}; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item, match_type}; +use clippy_utils::{is_expr_path_def_path, paths}; +use if_chain::if_chain; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, LangItem}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::symbol::sym; +use std::borrow::Cow; + +use super::MANUAL_STR_REPEAT; + +enum RepeatKind { + String, + Char(char), +} + +fn get_ty_param(ty: Ty<'_>) -> Option<Ty<'_>> { + if let ty::Adt(_, subs) = ty.kind() { + subs.types().next() + } else { + None + } +} + +fn parse_repeat_arg(cx: &LateContext<'_>, e: &Expr<'_>) -> Option<RepeatKind> { + if let ExprKind::Lit(lit) = &e.kind { + match lit.node { + LitKind::Str(..) => Some(RepeatKind::String), + LitKind::Char(c) => Some(RepeatKind::Char(c)), + _ => None, + } + } else { + let ty = cx.typeck_results().expr_ty(e); + if is_type_diagnostic_item(cx, ty, sym::String) + || (is_type_lang_item(cx, ty, LangItem::OwnedBox) && get_ty_param(ty).map_or(false, Ty::is_str)) + || (match_type(cx, ty, &paths::COW) && get_ty_param(ty).map_or(false, Ty::is_str)) + { + Some(RepeatKind::String) + } else { + let ty = ty.peel_refs(); + (ty.is_str() || is_type_diagnostic_item(cx, ty, sym::String)).then_some(RepeatKind::String) + } + } +} + +pub(super) fn check( + cx: &LateContext<'_>, + collect_expr: &Expr<'_>, + take_expr: &Expr<'_>, + take_self_arg: &Expr<'_>, + take_arg: &Expr<'_>, +) { + if_chain! { + if let ExprKind::Call(repeat_fn, [repeat_arg]) = take_self_arg.kind; + if is_expr_path_def_path(cx, repeat_fn, &paths::ITER_REPEAT); + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(collect_expr), sym::String); + if let Some(collect_id) = cx.typeck_results().type_dependent_def_id(collect_expr.hir_id); + if let Some(take_id) = cx.typeck_results().type_dependent_def_id(take_expr.hir_id); + if let Some(iter_trait_id) = cx.tcx.get_diagnostic_item(sym::Iterator); + if cx.tcx.trait_of_item(collect_id) == Some(iter_trait_id); + if cx.tcx.trait_of_item(take_id) == Some(iter_trait_id); + if let Some(repeat_kind) = parse_repeat_arg(cx, repeat_arg); + let ctxt = collect_expr.span.ctxt(); + if ctxt == take_expr.span.ctxt(); + if ctxt == take_self_arg.span.ctxt(); + then { + let mut app = Applicability::MachineApplicable; + let count_snip = snippet_with_context(cx, take_arg.span, ctxt, "..", &mut app).0; + + let val_str = match repeat_kind { + RepeatKind::Char(_) if repeat_arg.span.ctxt() != ctxt => return, + RepeatKind::Char('\'') => r#""'""#.into(), + RepeatKind::Char('"') => r#""\"""#.into(), + RepeatKind::Char(_) => + match snippet_with_applicability(cx, repeat_arg.span, "..", &mut app) { + Cow::Owned(s) => Cow::Owned(format!("\"{}\"", &s[1..s.len() - 1])), + s @ Cow::Borrowed(_) => s, + }, + RepeatKind::String => + Sugg::hir_with_context(cx, repeat_arg, ctxt, "..", &mut app).maybe_par().to_string().into(), + }; + + span_lint_and_sugg( + cx, + MANUAL_STR_REPEAT, + collect_expr.span, + "manual implementation of `str::repeat` using iterators", + "try this", + format!("{}.repeat({})", val_str, count_snip), + app + ) + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/map_collect_result_unit.rs b/src/tools/clippy/clippy_lints/src/methods/map_collect_result_unit.rs new file mode 100644 index 000000000..d420f144e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/map_collect_result_unit.rs @@ -0,0 +1,47 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_trait_method; +use clippy_utils::source::snippet; +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; +use rustc_middle::ty; +use rustc_span::symbol::sym; + +use super::MAP_COLLECT_RESULT_UNIT; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + iter: &hir::Expr<'_>, + map_fn: &hir::Expr<'_>, + collect_recv: &hir::Expr<'_>, +) { + if_chain! { + // called on Iterator + if is_trait_method(cx, collect_recv, sym::Iterator); + // return of collect `Result<(),_>` + let collect_ret_ty = cx.typeck_results().expr_ty(expr); + if is_type_diagnostic_item(cx, collect_ret_ty, sym::Result); + if let ty::Adt(_, substs) = collect_ret_ty.kind(); + if let Some(result_t) = substs.types().next(); + if result_t.is_unit(); + // get parts for snippet + then { + span_lint_and_sugg( + cx, + MAP_COLLECT_RESULT_UNIT, + expr.span, + "`.map().collect()` can be replaced with `.try_for_each()`", + "try this", + format!( + "{}.try_for_each({})", + snippet(cx, iter.span, ".."), + snippet(cx, map_fn.span, "..") + ), + Applicability::MachineApplicable, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/map_flatten.rs b/src/tools/clippy/clippy_lints/src/methods/map_flatten.rs new file mode 100644 index 000000000..13853dec9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/map_flatten.rs @@ -0,0 +1,73 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_trait_method; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::{symbol::sym, Span}; + +use super::MAP_FLATTEN; + +/// lint use of `map().flatten()` for `Iterators` and 'Options' +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, map_arg: &Expr<'_>, map_span: Span) { + if let Some((caller_ty_name, method_to_use)) = try_get_caller_ty_name_and_method_name(cx, expr, recv, map_arg) { + let mut applicability = Applicability::MachineApplicable; + + let closure_snippet = snippet_with_applicability(cx, map_arg.span, "..", &mut applicability); + span_lint_and_sugg( + cx, + MAP_FLATTEN, + expr.span.with_lo(map_span.lo()), + &format!("called `map(..).flatten()` on `{}`", caller_ty_name), + &format!( + "try replacing `map` with `{}` and remove the `.flatten()`", + method_to_use + ), + format!("{}({})", method_to_use, closure_snippet), + applicability, + ); + } +} + +fn try_get_caller_ty_name_and_method_name( + cx: &LateContext<'_>, + expr: &Expr<'_>, + caller_expr: &Expr<'_>, + map_arg: &Expr<'_>, +) -> Option<(&'static str, &'static str)> { + if is_trait_method(cx, expr, sym::Iterator) { + if is_map_to_option(cx, map_arg) { + // `(...).map(...)` has type `impl Iterator<Item=Option<...>> + Some(("Iterator", "filter_map")) + } else { + // `(...).map(...)` has type `impl Iterator<Item=impl Iterator<...>> + Some(("Iterator", "flat_map")) + } + } else { + if let ty::Adt(adt, _) = cx.typeck_results().expr_ty(caller_expr).kind() { + if cx.tcx.is_diagnostic_item(sym::Option, adt.did()) { + return Some(("Option", "and_then")); + } else if cx.tcx.is_diagnostic_item(sym::Result, adt.did()) { + return Some(("Result", "and_then")); + } + } + None + } +} + +fn is_map_to_option(cx: &LateContext<'_>, map_arg: &Expr<'_>) -> bool { + let map_closure_ty = cx.typeck_results().expr_ty(map_arg); + match map_closure_ty.kind() { + ty::Closure(_, _) | ty::FnDef(_, _) | ty::FnPtr(_) => { + let map_closure_sig = match map_closure_ty.kind() { + ty::Closure(_, substs) => substs.as_closure().sig(), + _ => map_closure_ty.fn_sig(cx.tcx), + }; + let map_closure_return_ty = cx.tcx.erase_late_bound_regions(map_closure_sig.output()); + is_type_diagnostic_item(cx, map_closure_return_ty, sym::Option) + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/map_identity.rs b/src/tools/clippy/clippy_lints/src/methods/map_identity.rs new file mode 100644 index 000000000..862a9578e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/map_identity.rs @@ -0,0 +1,39 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{is_expr_identity_function, is_trait_method}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::{source_map::Span, sym}; + +use super::MAP_IDENTITY; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + caller: &hir::Expr<'_>, + map_arg: &hir::Expr<'_>, + name: &str, + _map_span: Span, +) { + let caller_ty = cx.typeck_results().expr_ty(caller); + + if_chain! { + if is_trait_method(cx, expr, sym::Iterator) + || is_type_diagnostic_item(cx, caller_ty, sym::Result) + || is_type_diagnostic_item(cx, caller_ty, sym::Option); + if is_expr_identity_function(cx, map_arg); + if let Some(sugg_span) = expr.span.trim_start(caller.span); + then { + span_lint_and_sugg( + cx, + MAP_IDENTITY, + sugg_span, + "unnecessary map of the identity function", + &format!("remove the call to `{}`", name), + String::new(), + Applicability::MachineApplicable, + ) + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/map_unwrap_or.rs b/src/tools/clippy/clippy_lints/src/methods/map_unwrap_or.rs new file mode 100644 index 000000000..4a8e7ce4d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/map_unwrap_or.rs @@ -0,0 +1,79 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::usage::mutated_variables; +use clippy_utils::{meets_msrv, msrvs}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_semver::RustcVersion; +use rustc_span::symbol::sym; + +use super::MAP_UNWRAP_OR; + +/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s +/// Return true if lint triggered +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + recv: &'tcx hir::Expr<'_>, + map_arg: &'tcx hir::Expr<'_>, + unwrap_arg: &'tcx hir::Expr<'_>, + msrv: Option<RustcVersion>, +) -> bool { + // lint if the caller of `map()` is an `Option` + let is_option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option); + let is_result = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result); + + if is_result && !meets_msrv(msrv, msrvs::RESULT_MAP_OR_ELSE) { + return false; + } + + if is_option || is_result { + // Don't make a suggestion that may fail to compile due to mutably borrowing + // the same variable twice. + let map_mutated_vars = mutated_variables(recv, cx); + let unwrap_mutated_vars = mutated_variables(unwrap_arg, cx); + if let (Some(map_mutated_vars), Some(unwrap_mutated_vars)) = (map_mutated_vars, unwrap_mutated_vars) { + if map_mutated_vars.intersection(&unwrap_mutated_vars).next().is_some() { + return false; + } + } else { + return false; + } + + // lint message + let msg = if is_option { + "called `map(<f>).unwrap_or_else(<g>)` on an `Option` value. This can be done more directly by calling \ + `map_or_else(<g>, <f>)` instead" + } else { + "called `map(<f>).unwrap_or_else(<g>)` on a `Result` value. This can be done more directly by calling \ + `.map_or_else(<g>, <f>)` instead" + }; + // get snippets for args to map() and unwrap_or_else() + let map_snippet = snippet(cx, map_arg.span, ".."); + let unwrap_snippet = snippet(cx, unwrap_arg.span, ".."); + // lint, with note if neither arg is > 1 line and both map() and + // unwrap_or_else() have the same span + let multiline = map_snippet.lines().count() > 1 || unwrap_snippet.lines().count() > 1; + let same_span = map_arg.span.ctxt() == unwrap_arg.span.ctxt(); + if same_span && !multiline { + let var_snippet = snippet(cx, recv.span, ".."); + span_lint_and_sugg( + cx, + MAP_UNWRAP_OR, + expr.span, + msg, + "try this", + format!("{}.map_or_else({}, {})", var_snippet, unwrap_snippet, map_snippet), + Applicability::MachineApplicable, + ); + return true; + } else if same_span && multiline { + span_lint(cx, MAP_UNWRAP_OR, expr.span, msg); + return true; + } + } + + false +} diff --git a/src/tools/clippy/clippy_lints/src/methods/mod.rs b/src/tools/clippy/clippy_lints/src/methods/mod.rs new file mode 100644 index 000000000..202fbc1f7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/mod.rs @@ -0,0 +1,3052 @@ +mod bind_instead_of_map; +mod bytes_nth; +mod chars_cmp; +mod chars_cmp_with_unwrap; +mod chars_last_cmp; +mod chars_last_cmp_with_unwrap; +mod chars_next_cmp; +mod chars_next_cmp_with_unwrap; +mod clone_on_copy; +mod clone_on_ref_ptr; +mod cloned_instead_of_copied; +mod err_expect; +mod expect_fun_call; +mod expect_used; +mod extend_with_drain; +mod filetype_is_file; +mod filter_map; +mod filter_map_identity; +mod filter_map_next; +mod filter_next; +mod flat_map_identity; +mod flat_map_option; +mod from_iter_instead_of_collect; +mod get_last_with_len; +mod get_unwrap; +mod implicit_clone; +mod inefficient_to_string; +mod inspect_for_each; +mod into_iter_on_ref; +mod is_digit_ascii_radix; +mod iter_cloned_collect; +mod iter_count; +mod iter_next_slice; +mod iter_nth; +mod iter_nth_zero; +mod iter_overeager_cloned; +mod iter_skip_next; +mod iter_with_drain; +mod iterator_step_by_zero; +mod manual_saturating_arithmetic; +mod manual_str_repeat; +mod map_collect_result_unit; +mod map_flatten; +mod map_identity; +mod map_unwrap_or; +mod needless_option_as_deref; +mod needless_option_take; +mod no_effect_replace; +mod obfuscated_if_else; +mod ok_expect; +mod option_as_ref_deref; +mod option_map_or_none; +mod option_map_unwrap_or; +mod or_fun_call; +mod or_then_unwrap; +mod search_is_some; +mod single_char_add_str; +mod single_char_insert_string; +mod single_char_pattern; +mod single_char_push_string; +mod skip_while_next; +mod str_splitn; +mod string_extend_chars; +mod suspicious_map; +mod suspicious_splitn; +mod uninit_assumed_init; +mod unnecessary_filter_map; +mod unnecessary_fold; +mod unnecessary_iter_cloned; +mod unnecessary_join; +mod unnecessary_lazy_eval; +mod unnecessary_to_owned; +mod unwrap_or_else_default; +mod unwrap_used; +mod useless_asref; +mod utils; +mod wrong_self_convention; +mod zst_offset; + +use bind_instead_of_map::BindInsteadOfMap; +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +use clippy_utils::ty::{contains_adt_constructor, contains_ty, implements_trait, is_copy, is_type_diagnostic_item}; +use clippy_utils::{contains_return, get_trait_def_id, iter_input_pats, meets_msrv, msrvs, paths, return_ty}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_hir::def::Res; +use rustc_hir::{Expr, ExprKind, PrimTy, QPath, TraitItem, TraitItemKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::{self, TraitRef, Ty}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{sym, Span}; +use rustc_typeck::hir_ty_to_ty; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `cloned()` on an `Iterator` or `Option` where + /// `copied()` could be used instead. + /// + /// ### Why is this bad? + /// `copied()` is better because it guarantees that the type being cloned + /// implements `Copy`. + /// + /// ### Example + /// ```rust + /// [1, 2, 3].iter().cloned(); + /// ``` + /// Use instead: + /// ```rust + /// [1, 2, 3].iter().copied(); + /// ``` + #[clippy::version = "1.53.0"] + pub CLONED_INSTEAD_OF_COPIED, + pedantic, + "used `cloned` where `copied` could be used instead" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.cloned().<func>()` where call to `.cloned()` can be postponed. + /// + /// ### Why is this bad? + /// It's often inefficient to clone all elements of an iterator, when eventually, only some + /// of them will be consumed. + /// + /// ### Known Problems + /// This `lint` removes the side of effect of cloning items in the iterator. + /// A code that relies on that side-effect could fail. + /// + /// ### Examples + /// ```rust + /// # let vec = vec!["string".to_string()]; + /// vec.iter().cloned().take(10); + /// vec.iter().cloned().last(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let vec = vec!["string".to_string()]; + /// vec.iter().take(10).cloned(); + /// vec.iter().last().cloned(); + /// ``` + #[clippy::version = "1.60.0"] + pub ITER_OVEREAGER_CLONED, + perf, + "using `cloned()` early with `Iterator::iter()` can lead to some performance inefficiencies" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `Iterator::flat_map()` where `filter_map()` could be + /// used instead. + /// + /// ### Why is this bad? + /// When applicable, `filter_map()` is more clear since it shows that + /// `Option` is used to produce 0 or 1 items. + /// + /// ### Example + /// ```rust + /// let nums: Vec<i32> = ["1", "2", "whee!"].iter().flat_map(|x| x.parse().ok()).collect(); + /// ``` + /// Use instead: + /// ```rust + /// let nums: Vec<i32> = ["1", "2", "whee!"].iter().filter_map(|x| x.parse().ok()).collect(); + /// ``` + #[clippy::version = "1.53.0"] + pub FLAT_MAP_OPTION, + pedantic, + "used `flat_map` where `filter_map` could be used instead" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `.unwrap()` calls on `Option`s and on `Result`s. + /// + /// ### Why is this bad? + /// It is better to handle the `None` or `Err` case, + /// or at least call `.expect(_)` with a more helpful message. Still, for a lot of + /// quick-and-dirty code, `unwrap` is a good choice, which is why this lint is + /// `Allow` by default. + /// + /// `result.unwrap()` will let the thread panic on `Err` values. + /// Normally, you want to implement more sophisticated error handling, + /// and propagate errors upwards with `?` operator. + /// + /// Even if you want to panic on errors, not all `Error`s implement good + /// messages on display. Therefore, it may be beneficial to look at the places + /// where they may get displayed. Activate this lint to do just that. + /// + /// ### Examples + /// ```rust + /// # let option = Some(1); + /// # let result: Result<usize, ()> = Ok(1); + /// option.unwrap(); + /// result.unwrap(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let option = Some(1); + /// # let result: Result<usize, ()> = Ok(1); + /// option.expect("more helpful message"); + /// result.expect("more helpful message"); + /// ``` + #[clippy::version = "1.45.0"] + pub UNWRAP_USED, + restriction, + "using `.unwrap()` on `Result` or `Option`, which should at least get a better message using `expect()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `.expect()` calls on `Option`s and `Result`s. + /// + /// ### Why is this bad? + /// Usually it is better to handle the `None` or `Err` case. + /// Still, for a lot of quick-and-dirty code, `expect` is a good choice, which is why + /// this lint is `Allow` by default. + /// + /// `result.expect()` will let the thread panic on `Err` + /// values. Normally, you want to implement more sophisticated error handling, + /// and propagate errors upwards with `?` operator. + /// + /// ### Examples + /// ```rust,ignore + /// # let option = Some(1); + /// # let result: Result<usize, ()> = Ok(1); + /// option.expect("one"); + /// result.expect("one"); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// # let option = Some(1); + /// # let result: Result<usize, ()> = Ok(1); + /// option?; + /// + /// // or + /// + /// result?; + /// ``` + #[clippy::version = "1.45.0"] + pub EXPECT_USED, + restriction, + "using `.expect()` on `Result` or `Option`, which might be better handled" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for methods that should live in a trait + /// implementation of a `std` trait (see [llogiq's blog + /// post](http://llogiq.github.io/2015/07/30/traits.html) for further + /// information) instead of an inherent implementation. + /// + /// ### Why is this bad? + /// Implementing the traits improve ergonomics for users of + /// the code, often with very little cost. Also people seeing a `mul(...)` + /// method + /// may expect `*` to work equally, so you should have good reason to disappoint + /// them. + /// + /// ### Example + /// ```rust + /// struct X; + /// impl X { + /// fn add(&self, other: &X) -> X { + /// // .. + /// # X + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SHOULD_IMPLEMENT_TRAIT, + style, + "defining a method that should be implementing a std trait" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for methods with certain name prefixes and which + /// doesn't match how self is taken. The actual rules are: + /// + /// |Prefix |Postfix |`self` taken | `self` type | + /// |-------|------------|-------------------------------|--------------| + /// |`as_` | none |`&self` or `&mut self` | any | + /// |`from_`| none | none | any | + /// |`into_`| none |`self` | any | + /// |`is_` | none |`&mut self` or `&self` or none | any | + /// |`to_` | `_mut` |`&mut self` | any | + /// |`to_` | not `_mut` |`self` | `Copy` | + /// |`to_` | not `_mut` |`&self` | not `Copy` | + /// + /// Note: Clippy doesn't trigger methods with `to_` prefix in: + /// - Traits definition. + /// Clippy can not tell if a type that implements a trait is `Copy` or not. + /// - Traits implementation, when `&self` is taken. + /// The method signature is controlled by the trait and often `&self` is required for all types that implement the trait + /// (see e.g. the `std::string::ToString` trait). + /// + /// Clippy allows `Pin<&Self>` and `Pin<&mut Self>` if `&self` and `&mut self` is required. + /// + /// Please find more info here: + /// https://rust-lang.github.io/api-guidelines/naming.html#ad-hoc-conversions-follow-as_-to_-into_-conventions-c-conv + /// + /// ### Why is this bad? + /// Consistency breeds readability. If you follow the + /// conventions, your users won't be surprised that they, e.g., need to supply a + /// mutable reference to a `as_..` function. + /// + /// ### Example + /// ```rust + /// # struct X; + /// impl X { + /// fn as_str(self) -> &'static str { + /// // .. + /// # "" + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WRONG_SELF_CONVENTION, + style, + "defining a method named with an established prefix (like \"into_\") that takes `self` with the wrong convention" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `ok().expect(..)`. + /// + /// ### Why is this bad? + /// Because you usually call `expect()` on the `Result` + /// directly to get a better error message. + /// + /// ### Known problems + /// The error type needs to implement `Debug` + /// + /// ### Example + /// ```rust + /// # let x = Ok::<_, ()>(()); + /// x.ok().expect("why did I do this again?"); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = Ok::<_, ()>(()); + /// x.expect("why did I do this again?"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OK_EXPECT, + style, + "using `ok().expect()`, which gives worse error messages than calling `expect` directly on the Result" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `.err().expect()` calls on the `Result` type. + /// + /// ### Why is this bad? + /// `.expect_err()` can be called directly to avoid the extra type conversion from `err()`. + /// + /// ### Example + /// ```should_panic + /// let x: Result<u32, &str> = Ok(10); + /// x.err().expect("Testing err().expect()"); + /// ``` + /// Use instead: + /// ```should_panic + /// let x: Result<u32, &str> = Ok(10); + /// x.expect_err("Testing expect_err"); + /// ``` + #[clippy::version = "1.62.0"] + pub ERR_EXPECT, + style, + r#"using `.err().expect("")` when `.expect_err("")` can be used"# +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `_.unwrap_or_else(Default::default)` on `Option` and + /// `Result` values. + /// + /// ### Why is this bad? + /// Readability, these can be written as `_.unwrap_or_default`, which is + /// simpler and more concise. + /// + /// ### Examples + /// ```rust + /// # let x = Some(1); + /// x.unwrap_or_else(Default::default); + /// x.unwrap_or_else(u32::default); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = Some(1); + /// x.unwrap_or_default(); + /// ``` + #[clippy::version = "1.56.0"] + pub UNWRAP_OR_ELSE_DEFAULT, + style, + "using `.unwrap_or_else(Default::default)`, which is more succinctly expressed as `.unwrap_or_default()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `option.map(_).unwrap_or(_)` or `option.map(_).unwrap_or_else(_)` or + /// `result.map(_).unwrap_or_else(_)`. + /// + /// ### Why is this bad? + /// Readability, these can be written more concisely (resp.) as + /// `option.map_or(_, _)`, `option.map_or_else(_, _)` and `result.map_or_else(_, _)`. + /// + /// ### Known problems + /// The order of the arguments is not in execution order + /// + /// ### Examples + /// ```rust + /// # let option = Some(1); + /// # let result: Result<usize, ()> = Ok(1); + /// # fn some_function(foo: ()) -> usize { 1 } + /// option.map(|a| a + 1).unwrap_or(0); + /// result.map(|a| a + 1).unwrap_or_else(some_function); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let option = Some(1); + /// # let result: Result<usize, ()> = Ok(1); + /// # fn some_function(foo: ()) -> usize { 1 } + /// option.map_or(0, |a| a + 1); + /// result.map_or_else(some_function, |a| a + 1); + /// ``` + #[clippy::version = "1.45.0"] + pub MAP_UNWRAP_OR, + pedantic, + "using `.map(f).unwrap_or(a)` or `.map(f).unwrap_or_else(func)`, which are more succinctly expressed as `map_or(a, f)` or `map_or_else(a, f)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.map_or(None, _)`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.and_then(_)`. + /// + /// ### Known problems + /// The order of the arguments is not in execution order. + /// + /// ### Example + /// ```rust + /// # let opt = Some(1); + /// opt.map_or(None, |a| Some(a + 1)); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let opt = Some(1); + /// opt.and_then(|a| Some(a + 1)); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OPTION_MAP_OR_NONE, + style, + "using `Option.map_or(None, f)`, which is more succinctly expressed as `and_then(f)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.map_or(None, Some)`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.ok()`. + /// + /// ### Example + /// ```rust + /// # let r: Result<u32, &str> = Ok(1); + /// assert_eq!(Some(1), r.map_or(None, Some)); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let r: Result<u32, &str> = Ok(1); + /// assert_eq!(Some(1), r.ok()); + /// ``` + #[clippy::version = "1.44.0"] + pub RESULT_MAP_OR_INTO_OPTION, + style, + "using `Result.map_or(None, Some)`, which is more succinctly expressed as `ok()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.and_then(|x| Some(y))`, `_.and_then(|x| Ok(y))` or + /// `_.or_else(|x| Err(y))`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.map(|x| y)` or `_.map_err(|x| y)`. + /// + /// ### Example + /// ```rust + /// # fn opt() -> Option<&'static str> { Some("42") } + /// # fn res() -> Result<&'static str, &'static str> { Ok("42") } + /// let _ = opt().and_then(|s| Some(s.len())); + /// let _ = res().and_then(|s| if s.len() == 42 { Ok(10) } else { Ok(20) }); + /// let _ = res().or_else(|s| if s.len() == 42 { Err(10) } else { Err(20) }); + /// ``` + /// + /// The correct use would be: + /// + /// ```rust + /// # fn opt() -> Option<&'static str> { Some("42") } + /// # fn res() -> Result<&'static str, &'static str> { Ok("42") } + /// let _ = opt().map(|s| s.len()); + /// let _ = res().map(|s| if s.len() == 42 { 10 } else { 20 }); + /// let _ = res().map_err(|s| if s.len() == 42 { 10 } else { 20 }); + /// ``` + #[clippy::version = "1.45.0"] + pub BIND_INSTEAD_OF_MAP, + complexity, + "using `Option.and_then(|x| Some(y))`, which is more succinctly expressed as `map(|x| y)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.filter(_).next()`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.find(_)`. + /// + /// ### Example + /// ```rust + /// # let vec = vec![1]; + /// vec.iter().filter(|x| **x == 0).next(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let vec = vec![1]; + /// vec.iter().find(|x| **x == 0); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FILTER_NEXT, + complexity, + "using `filter(p).next()`, which is more succinctly expressed as `.find(p)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.skip_while(condition).next()`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.find(!condition)`. + /// + /// ### Example + /// ```rust + /// # let vec = vec![1]; + /// vec.iter().skip_while(|x| **x == 0).next(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let vec = vec![1]; + /// vec.iter().find(|x| **x != 0); + /// ``` + #[clippy::version = "1.42.0"] + pub SKIP_WHILE_NEXT, + complexity, + "using `skip_while(p).next()`, which is more succinctly expressed as `.find(!p)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.map(_).flatten(_)` on `Iterator` and `Option` + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.flat_map(_)` for `Iterator` or `_.and_then(_)` for `Option` + /// + /// ### Example + /// ```rust + /// let vec = vec![vec![1]]; + /// let opt = Some(5); + /// + /// vec.iter().map(|x| x.iter()).flatten(); + /// opt.map(|x| Some(x * 2)).flatten(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let vec = vec![vec![1]]; + /// # let opt = Some(5); + /// vec.iter().flat_map(|x| x.iter()); + /// opt.and_then(|x| Some(x * 2)); + /// ``` + #[clippy::version = "1.31.0"] + pub MAP_FLATTEN, + complexity, + "using combinations of `flatten` and `map` which can usually be written as a single method call" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.filter(_).map(_)` that can be written more simply + /// as `filter_map(_)`. + /// + /// ### Why is this bad? + /// Redundant code in the `filter` and `map` operations is poor style and + /// less performant. + /// + /// ### Example + /// ```rust + /// # #![allow(unused)] + /// (0_i32..10) + /// .filter(|n| n.checked_add(1).is_some()) + /// .map(|n| n.checked_add(1).unwrap()); + /// ``` + /// + /// Use instead: + /// ```rust + /// # #[allow(unused)] + /// (0_i32..10).filter_map(|n| n.checked_add(1)); + /// ``` + #[clippy::version = "1.51.0"] + pub MANUAL_FILTER_MAP, + complexity, + "using `_.filter(_).map(_)` in a way that can be written more simply as `filter_map(_)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.find(_).map(_)` that can be written more simply + /// as `find_map(_)`. + /// + /// ### Why is this bad? + /// Redundant code in the `find` and `map` operations is poor style and + /// less performant. + /// + /// ### Example + /// ```rust + /// (0_i32..10) + /// .find(|n| n.checked_add(1).is_some()) + /// .map(|n| n.checked_add(1).unwrap()); + /// ``` + /// + /// Use instead: + /// ```rust + /// (0_i32..10).find_map(|n| n.checked_add(1)); + /// ``` + #[clippy::version = "1.51.0"] + pub MANUAL_FIND_MAP, + complexity, + "using `_.find(_).map(_)` in a way that can be written more simply as `find_map(_)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.filter_map(_).next()`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.find_map(_)`. + /// + /// ### Example + /// ```rust + /// (0..3).filter_map(|x| if x == 2 { Some(x) } else { None }).next(); + /// ``` + /// Can be written as + /// + /// ```rust + /// (0..3).find_map(|x| if x == 2 { Some(x) } else { None }); + /// ``` + #[clippy::version = "1.36.0"] + pub FILTER_MAP_NEXT, + pedantic, + "using combination of `filter_map` and `next` which can usually be written as a single method call" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `flat_map(|x| x)`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely by using `flatten`. + /// + /// ### Example + /// ```rust + /// # let iter = vec![vec![0]].into_iter(); + /// iter.flat_map(|x| x); + /// ``` + /// Can be written as + /// ```rust + /// # let iter = vec![vec![0]].into_iter(); + /// iter.flatten(); + /// ``` + #[clippy::version = "1.39.0"] + pub FLAT_MAP_IDENTITY, + complexity, + "call to `flat_map` where `flatten` is sufficient" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for an iterator or string search (such as `find()`, + /// `position()`, or `rposition()`) followed by a call to `is_some()` or `is_none()`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as: + /// * `_.any(_)`, or `_.contains(_)` for `is_some()`, + /// * `!_.any(_)`, or `!_.contains(_)` for `is_none()`. + /// + /// ### Example + /// ```rust + /// # #![allow(unused)] + /// let vec = vec![1]; + /// vec.iter().find(|x| **x == 0).is_some(); + /// + /// "hello world".find("world").is_none(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let vec = vec![1]; + /// vec.iter().any(|x| *x == 0); + /// + /// # #[allow(unused)] + /// !"hello world".contains("world"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SEARCH_IS_SOME, + complexity, + "using an iterator or string search followed by `is_some()` or `is_none()`, which is more succinctly expressed as a call to `any()` or `contains()` (with negation in case of `is_none()`)" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.chars().next()` on a `str` to check + /// if it starts with a given char. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.starts_with(_)`. + /// + /// ### Example + /// ```rust + /// let name = "foo"; + /// if name.chars().next() == Some('_') {}; + /// ``` + /// + /// Use instead: + /// ```rust + /// let name = "foo"; + /// if name.starts_with('_') {}; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CHARS_NEXT_CMP, + style, + "using `.chars().next()` to check if a string starts with a char" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `.or(foo(..))`, `.unwrap_or(foo(..))`, + /// etc., and suggests to use `or_else`, `unwrap_or_else`, etc., or + /// `unwrap_or_default` instead. + /// + /// ### Why is this bad? + /// The function will always be called and potentially + /// allocate an object acting as the default. + /// + /// ### Known problems + /// If the function has side-effects, not calling it will + /// change the semantic of the program, but you shouldn't rely on that anyway. + /// + /// ### Example + /// ```rust + /// # let foo = Some(String::new()); + /// foo.unwrap_or(String::new()); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let foo = Some(String::new()); + /// foo.unwrap_or_else(String::new); + /// + /// // or + /// + /// # let foo = Some(String::new()); + /// foo.unwrap_or_default(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OR_FUN_CALL, + perf, + "using any `*or` method with a function call, which suggests `*or_else`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `.or(…).unwrap()` calls to Options and Results. + /// + /// ### Why is this bad? + /// You should use `.unwrap_or(…)` instead for clarity. + /// + /// ### Example + /// ```rust + /// # let fallback = "fallback"; + /// // Result + /// # type Error = &'static str; + /// # let result: Result<&str, Error> = Err("error"); + /// let value = result.or::<Error>(Ok(fallback)).unwrap(); + /// + /// // Option + /// # let option: Option<&str> = None; + /// let value = option.or(Some(fallback)).unwrap(); + /// ``` + /// Use instead: + /// ```rust + /// # let fallback = "fallback"; + /// // Result + /// # let result: Result<&str, &str> = Err("error"); + /// let value = result.unwrap_or(fallback); + /// + /// // Option + /// # let option: Option<&str> = None; + /// let value = option.unwrap_or(fallback); + /// ``` + #[clippy::version = "1.61.0"] + pub OR_THEN_UNWRAP, + complexity, + "checks for `.or(…).unwrap()` calls to Options and Results." +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `.expect(&format!(...))`, `.expect(foo(..))`, + /// etc., and suggests to use `unwrap_or_else` instead + /// + /// ### Why is this bad? + /// The function will always be called. + /// + /// ### Known problems + /// If the function has side-effects, not calling it will + /// change the semantics of the program, but you shouldn't rely on that anyway. + /// + /// ### Example + /// ```rust + /// # let foo = Some(String::new()); + /// # let err_code = "418"; + /// # let err_msg = "I'm a teapot"; + /// foo.expect(&format!("Err {}: {}", err_code, err_msg)); + /// + /// // or + /// + /// # let foo = Some(String::new()); + /// foo.expect(format!("Err {}: {}", err_code, err_msg).as_str()); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let foo = Some(String::new()); + /// # let err_code = "418"; + /// # let err_msg = "I'm a teapot"; + /// foo.unwrap_or_else(|| panic!("Err {}: {}", err_code, err_msg)); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EXPECT_FUN_CALL, + perf, + "using any `expect` method with a function call" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.clone()` on a `Copy` type. + /// + /// ### Why is this bad? + /// The only reason `Copy` types implement `Clone` is for + /// generics, not for using the `clone` method on a concrete type. + /// + /// ### Example + /// ```rust + /// 42u64.clone(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CLONE_ON_COPY, + complexity, + "using `clone` on a `Copy` type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.clone()` on a ref-counted pointer, + /// (`Rc`, `Arc`, `rc::Weak`, or `sync::Weak`), and suggests calling Clone via unified + /// function syntax instead (e.g., `Rc::clone(foo)`). + /// + /// ### Why is this bad? + /// Calling '.clone()' on an Rc, Arc, or Weak + /// can obscure the fact that only the pointer is being cloned, not the underlying + /// data. + /// + /// ### Example + /// ```rust + /// # use std::rc::Rc; + /// let x = Rc::new(1); + /// + /// x.clone(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::rc::Rc; + /// # let x = Rc::new(1); + /// Rc::clone(&x); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CLONE_ON_REF_PTR, + restriction, + "using 'clone' on a ref-counted pointer" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.clone()` on an `&&T`. + /// + /// ### Why is this bad? + /// Cloning an `&&T` copies the inner `&T`, instead of + /// cloning the underlying `T`. + /// + /// ### Example + /// ```rust + /// fn main() { + /// let x = vec![1]; + /// let y = &&x; + /// let z = y.clone(); + /// println!("{:p} {:p}", *y, z); // prints out the same pointer + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CLONE_DOUBLE_REF, + correctness, + "using `clone` on `&&T`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.to_string()` on an `&&T` where + /// `T` implements `ToString` directly (like `&&str` or `&&String`). + /// + /// ### Why is this bad? + /// This bypasses the specialized implementation of + /// `ToString` and instead goes through the more expensive string formatting + /// facilities. + /// + /// ### Example + /// ```rust + /// // Generic implementation for `T: Display` is used (slow) + /// ["foo", "bar"].iter().map(|s| s.to_string()); + /// + /// // OK, the specialized impl is used + /// ["foo", "bar"].iter().map(|&s| s.to_string()); + /// ``` + #[clippy::version = "1.40.0"] + pub INEFFICIENT_TO_STRING, + pedantic, + "using `to_string` on `&&T` where `T: ToString`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `new` not returning a type that contains `Self`. + /// + /// ### Why is this bad? + /// As a convention, `new` methods are used to make a new + /// instance of a type. + /// + /// ### Example + /// In an impl block: + /// ```rust + /// # struct Foo; + /// # struct NotAFoo; + /// impl Foo { + /// fn new() -> NotAFoo { + /// # NotAFoo + /// } + /// } + /// ``` + /// + /// ```rust + /// # struct Foo; + /// struct Bar(Foo); + /// impl Foo { + /// // Bad. The type name must contain `Self` + /// fn new() -> Bar { + /// # Bar(Foo) + /// } + /// } + /// ``` + /// + /// ```rust + /// # struct Foo; + /// # struct FooError; + /// impl Foo { + /// // Good. Return type contains `Self` + /// fn new() -> Result<Foo, FooError> { + /// # Ok(Foo) + /// } + /// } + /// ``` + /// + /// Or in a trait definition: + /// ```rust + /// pub trait Trait { + /// // Bad. The type name must contain `Self` + /// fn new(); + /// } + /// ``` + /// + /// ```rust + /// pub trait Trait { + /// // Good. Return type contains `Self` + /// fn new() -> Self; + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEW_RET_NO_SELF, + style, + "not returning type containing `Self` in a `new` method" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for string methods that receive a single-character + /// `str` as an argument, e.g., `_.split("x")`. + /// + /// ### Why is this bad? + /// Performing these methods using a `char` is faster than + /// using a `str`. + /// + /// ### Known problems + /// Does not catch multi-byte unicode characters. + /// + /// ### Example + /// ```rust,ignore + /// _.split("x"); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// _.split('x'); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SINGLE_CHAR_PATTERN, + perf, + "using a single-character str where a char could be used, e.g., `_.split(\"x\")`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calling `.step_by(0)` on iterators which panics. + /// + /// ### Why is this bad? + /// This very much looks like an oversight. Use `panic!()` instead if you + /// actually intend to panic. + /// + /// ### Example + /// ```rust,should_panic + /// for x in (0..100).step_by(0) { + /// //.. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ITERATOR_STEP_BY_ZERO, + correctness, + "using `Iterator::step_by(0)`, which will panic at runtime" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for indirect collection of populated `Option` + /// + /// ### Why is this bad? + /// `Option` is like a collection of 0-1 things, so `flatten` + /// automatically does this without suspicious-looking `unwrap` calls. + /// + /// ### Example + /// ```rust + /// let _ = std::iter::empty::<Option<i32>>().filter(Option::is_some).map(Option::unwrap); + /// ``` + /// Use instead: + /// ```rust + /// let _ = std::iter::empty::<Option<i32>>().flatten(); + /// ``` + #[clippy::version = "1.53.0"] + pub OPTION_FILTER_MAP, + complexity, + "filtering `Option` for `Some` then force-unwrapping, which can be one type-safe operation" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of `iter.nth(0)`. + /// + /// ### Why is this bad? + /// `iter.next()` is equivalent to + /// `iter.nth(0)`, as they both consume the next element, + /// but is more readable. + /// + /// ### Example + /// ```rust + /// # use std::collections::HashSet; + /// # let mut s = HashSet::new(); + /// # s.insert(1); + /// let x = s.iter().nth(0); + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::collections::HashSet; + /// # let mut s = HashSet::new(); + /// # s.insert(1); + /// let x = s.iter().next(); + /// ``` + #[clippy::version = "1.42.0"] + pub ITER_NTH_ZERO, + style, + "replace `iter.nth(0)` with `iter.next()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `.iter().nth()` (and the related + /// `.iter_mut().nth()`) on standard library types with *O*(1) element access. + /// + /// ### Why is this bad? + /// `.get()` and `.get_mut()` are more efficient and more + /// readable. + /// + /// ### Example + /// ```rust + /// let some_vec = vec![0, 1, 2, 3]; + /// let bad_vec = some_vec.iter().nth(3); + /// let bad_slice = &some_vec[..].iter().nth(3); + /// ``` + /// The correct use would be: + /// ```rust + /// let some_vec = vec![0, 1, 2, 3]; + /// let bad_vec = some_vec.get(3); + /// let bad_slice = &some_vec[..].get(3); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ITER_NTH, + perf, + "using `.iter().nth()` on a standard library type with O(1) element access" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `.skip(x).next()` on iterators. + /// + /// ### Why is this bad? + /// `.nth(x)` is cleaner + /// + /// ### Example + /// ```rust + /// let some_vec = vec![0, 1, 2, 3]; + /// let bad_vec = some_vec.iter().skip(3).next(); + /// let bad_slice = &some_vec[..].iter().skip(3).next(); + /// ``` + /// The correct use would be: + /// ```rust + /// let some_vec = vec![0, 1, 2, 3]; + /// let bad_vec = some_vec.iter().nth(3); + /// let bad_slice = &some_vec[..].iter().nth(3); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ITER_SKIP_NEXT, + style, + "using `.skip(x).next()` on an iterator" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `.drain(..)` on `Vec` and `VecDeque` for iteration. + /// + /// ### Why is this bad? + /// `.into_iter()` is simpler with better performance. + /// + /// ### Example + /// ```rust + /// # use std::collections::HashSet; + /// let mut foo = vec![0, 1, 2, 3]; + /// let bar: HashSet<usize> = foo.drain(..).collect(); + /// ``` + /// Use instead: + /// ```rust + /// # use std::collections::HashSet; + /// let foo = vec![0, 1, 2, 3]; + /// let bar: HashSet<usize> = foo.into_iter().collect(); + /// ``` + #[clippy::version = "1.61.0"] + pub ITER_WITH_DRAIN, + nursery, + "replace `.drain(..)` with `.into_iter()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for using `x.get(x.len() - 1)` instead of + /// `x.last()`. + /// + /// ### Why is this bad? + /// Using `x.last()` is easier to read and has the same + /// result. + /// + /// Note that using `x[x.len() - 1]` is semantically different from + /// `x.last()`. Indexing into the array will panic on out-of-bounds + /// accesses, while `x.get()` and `x.last()` will return `None`. + /// + /// There is another lint (get_unwrap) that covers the case of using + /// `x.get(index).unwrap()` instead of `x[index]`. + /// + /// ### Example + /// ```rust + /// let x = vec![2, 3, 5]; + /// let last_element = x.get(x.len() - 1); + /// ``` + /// + /// Use instead: + /// ```rust + /// let x = vec![2, 3, 5]; + /// let last_element = x.last(); + /// ``` + #[clippy::version = "1.37.0"] + pub GET_LAST_WITH_LEN, + complexity, + "Using `x.get(x.len() - 1)` when `x.last()` is correct and simpler" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `.get().unwrap()` (or + /// `.get_mut().unwrap`) on a standard library type which implements `Index` + /// + /// ### Why is this bad? + /// Using the Index trait (`[]`) is more clear and more + /// concise. + /// + /// ### Known problems + /// Not a replacement for error handling: Using either + /// `.unwrap()` or the Index trait (`[]`) carries the risk of causing a `panic` + /// if the value being accessed is `None`. If the use of `.get().unwrap()` is a + /// temporary placeholder for dealing with the `Option` type, then this does + /// not mitigate the need for error handling. If there is a chance that `.get()` + /// will be `None` in your program, then it is advisable that the `None` case + /// is handled in a future refactor instead of using `.unwrap()` or the Index + /// trait. + /// + /// ### Example + /// ```rust + /// let mut some_vec = vec![0, 1, 2, 3]; + /// let last = some_vec.get(3).unwrap(); + /// *some_vec.get_mut(0).unwrap() = 1; + /// ``` + /// The correct use would be: + /// ```rust + /// let mut some_vec = vec![0, 1, 2, 3]; + /// let last = some_vec[3]; + /// some_vec[0] = 1; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub GET_UNWRAP, + restriction, + "using `.get().unwrap()` or `.get_mut().unwrap()` when using `[]` would work instead" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for occurrences where one vector gets extended instead of append + /// + /// ### Why is this bad? + /// Using `append` instead of `extend` is more concise and faster + /// + /// ### Example + /// ```rust + /// let mut a = vec![1, 2, 3]; + /// let mut b = vec![4, 5, 6]; + /// + /// a.extend(b.drain(..)); + /// ``` + /// + /// Use instead: + /// ```rust + /// let mut a = vec![1, 2, 3]; + /// let mut b = vec![4, 5, 6]; + /// + /// a.append(&mut b); + /// ``` + #[clippy::version = "1.55.0"] + pub EXTEND_WITH_DRAIN, + perf, + "using vec.append(&mut vec) to move the full range of a vector to another" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of `.extend(s.chars())` where s is a + /// `&str` or `String`. + /// + /// ### Why is this bad? + /// `.push_str(s)` is clearer + /// + /// ### Example + /// ```rust + /// let abc = "abc"; + /// let def = String::from("def"); + /// let mut s = String::new(); + /// s.extend(abc.chars()); + /// s.extend(def.chars()); + /// ``` + /// The correct use would be: + /// ```rust + /// let abc = "abc"; + /// let def = String::from("def"); + /// let mut s = String::new(); + /// s.push_str(abc); + /// s.push_str(&def); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub STRING_EXTEND_CHARS, + style, + "using `x.extend(s.chars())` where s is a `&str` or `String`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of `.cloned().collect()` on slice to + /// create a `Vec`. + /// + /// ### Why is this bad? + /// `.to_vec()` is clearer + /// + /// ### Example + /// ```rust + /// let s = [1, 2, 3, 4, 5]; + /// let s2: Vec<isize> = s[..].iter().cloned().collect(); + /// ``` + /// The better use would be: + /// ```rust + /// let s = [1, 2, 3, 4, 5]; + /// let s2: Vec<isize> = s.to_vec(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ITER_CLONED_COLLECT, + style, + "using `.cloned().collect()` on slice to create a `Vec`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.chars().last()` or + /// `_.chars().next_back()` on a `str` to check if it ends with a given char. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.ends_with(_)`. + /// + /// ### Example + /// ```rust + /// # let name = "_"; + /// name.chars().last() == Some('_') || name.chars().next_back() == Some('-'); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let name = "_"; + /// name.ends_with('_') || name.ends_with('-'); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CHARS_LAST_CMP, + style, + "using `.chars().last()` or `.chars().next_back()` to check if a string ends with a char" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `.as_ref()` or `.as_mut()` where the + /// types before and after the call are the same. + /// + /// ### Why is this bad? + /// The call is unnecessary. + /// + /// ### Example + /// ```rust + /// # fn do_stuff(x: &[i32]) {} + /// let x: &[i32] = &[1, 2, 3, 4, 5]; + /// do_stuff(x.as_ref()); + /// ``` + /// The correct use would be: + /// ```rust + /// # fn do_stuff(x: &[i32]) {} + /// let x: &[i32] = &[1, 2, 3, 4, 5]; + /// do_stuff(x); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USELESS_ASREF, + complexity, + "using `as_ref` where the types before and after the call are the same" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for using `fold` when a more succinct alternative exists. + /// Specifically, this checks for `fold`s which could be replaced by `any`, `all`, + /// `sum` or `product`. + /// + /// ### Why is this bad? + /// Readability. + /// + /// ### Example + /// ```rust + /// # #[allow(unused)] + /// (0..3).fold(false, |acc, x| acc || x > 2); + /// ``` + /// + /// Use instead: + /// ```rust + /// (0..3).any(|x| x > 2); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNNECESSARY_FOLD, + style, + "using `fold` when a more succinct alternative exists" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `filter_map` calls that could be replaced by `filter` or `map`. + /// More specifically it checks if the closure provided is only performing one of the + /// filter or map operations and suggests the appropriate option. + /// + /// ### Why is this bad? + /// Complexity. The intent is also clearer if only a single + /// operation is being performed. + /// + /// ### Example + /// ```rust + /// let _ = (0..3).filter_map(|x| if x > 2 { Some(x) } else { None }); + /// + /// // As there is no transformation of the argument this could be written as: + /// let _ = (0..3).filter(|&x| x > 2); + /// ``` + /// + /// ```rust + /// let _ = (0..4).filter_map(|x| Some(x + 1)); + /// + /// // As there is no conditional check on the argument this could be written as: + /// let _ = (0..4).map(|x| x + 1); + /// ``` + #[clippy::version = "1.31.0"] + pub UNNECESSARY_FILTER_MAP, + complexity, + "using `filter_map` when a more succinct alternative exists" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `find_map` calls that could be replaced by `find` or `map`. More + /// specifically it checks if the closure provided is only performing one of the + /// find or map operations and suggests the appropriate option. + /// + /// ### Why is this bad? + /// Complexity. The intent is also clearer if only a single + /// operation is being performed. + /// + /// ### Example + /// ```rust + /// let _ = (0..3).find_map(|x| if x > 2 { Some(x) } else { None }); + /// + /// // As there is no transformation of the argument this could be written as: + /// let _ = (0..3).find(|&x| x > 2); + /// ``` + /// + /// ```rust + /// let _ = (0..4).find_map(|x| Some(x + 1)); + /// + /// // As there is no conditional check on the argument this could be written as: + /// let _ = (0..4).map(|x| x + 1).next(); + /// ``` + #[clippy::version = "1.61.0"] + pub UNNECESSARY_FIND_MAP, + complexity, + "using `find_map` when a more succinct alternative exists" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `into_iter` calls on references which should be replaced by `iter` + /// or `iter_mut`. + /// + /// ### Why is this bad? + /// Readability. Calling `into_iter` on a reference will not move out its + /// content into the resulting iterator, which is confusing. It is better just call `iter` or + /// `iter_mut` directly. + /// + /// ### Example + /// ```rust + /// # let vec = vec![3, 4, 5]; + /// (&vec).into_iter(); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let vec = vec![3, 4, 5]; + /// (&vec).iter(); + /// ``` + #[clippy::version = "1.32.0"] + pub INTO_ITER_ON_REF, + style, + "using `.into_iter()` on a reference" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `map` followed by a `count`. + /// + /// ### Why is this bad? + /// It looks suspicious. Maybe `map` was confused with `filter`. + /// If the `map` call is intentional, this should be rewritten + /// using `inspect`. Or, if you intend to drive the iterator to + /// completion, you can just use `for_each` instead. + /// + /// ### Example + /// ```rust + /// let _ = (0..3).map(|x| x + 2).count(); + /// ``` + #[clippy::version = "1.39.0"] + pub SUSPICIOUS_MAP, + suspicious, + "suspicious usage of map" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `MaybeUninit::uninit().assume_init()`. + /// + /// ### Why is this bad? + /// For most types, this is undefined behavior. + /// + /// ### Known problems + /// For now, we accept empty tuples and tuples / arrays + /// of `MaybeUninit`. There may be other types that allow uninitialized + /// data, but those are not yet rigorously defined. + /// + /// ### Example + /// ```rust + /// // Beware the UB + /// use std::mem::MaybeUninit; + /// + /// let _: usize = unsafe { MaybeUninit::uninit().assume_init() }; + /// ``` + /// + /// Note that the following is OK: + /// + /// ```rust + /// use std::mem::MaybeUninit; + /// + /// let _: [MaybeUninit<bool>; 5] = unsafe { + /// MaybeUninit::uninit().assume_init() + /// }; + /// ``` + #[clippy::version = "1.39.0"] + pub UNINIT_ASSUMED_INIT, + correctness, + "`MaybeUninit::uninit().assume_init()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `.checked_add/sub(x).unwrap_or(MAX/MIN)`. + /// + /// ### Why is this bad? + /// These can be written simply with `saturating_add/sub` methods. + /// + /// ### Example + /// ```rust + /// # let y: u32 = 0; + /// # let x: u32 = 100; + /// let add = x.checked_add(y).unwrap_or(u32::MAX); + /// let sub = x.checked_sub(y).unwrap_or(u32::MIN); + /// ``` + /// + /// can be written using dedicated methods for saturating addition/subtraction as: + /// + /// ```rust + /// # let y: u32 = 0; + /// # let x: u32 = 100; + /// let add = x.saturating_add(y); + /// let sub = x.saturating_sub(y); + /// ``` + #[clippy::version = "1.39.0"] + pub MANUAL_SATURATING_ARITHMETIC, + style, + "`.checked_add/sub(x).unwrap_or(MAX/MIN)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `offset(_)`, `wrapping_`{`add`, `sub`}, etc. on raw pointers to + /// zero-sized types + /// + /// ### Why is this bad? + /// This is a no-op, and likely unintended + /// + /// ### Example + /// ```rust + /// unsafe { (&() as *const ()).offset(1) }; + /// ``` + #[clippy::version = "1.41.0"] + pub ZST_OFFSET, + correctness, + "Check for offset calculations on raw pointers to zero-sized types" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `FileType::is_file()`. + /// + /// ### Why is this bad? + /// When people testing a file type with `FileType::is_file` + /// they are testing whether a path is something they can get bytes from. But + /// `is_file` doesn't cover special file types in unix-like systems, and doesn't cover + /// symlink in windows. Using `!FileType::is_dir()` is a better way to that intention. + /// + /// ### Example + /// ```rust + /// # || { + /// let metadata = std::fs::metadata("foo.txt")?; + /// let filetype = metadata.file_type(); + /// + /// if filetype.is_file() { + /// // read file + /// } + /// # Ok::<_, std::io::Error>(()) + /// # }; + /// ``` + /// + /// should be written as: + /// + /// ```rust + /// # || { + /// let metadata = std::fs::metadata("foo.txt")?; + /// let filetype = metadata.file_type(); + /// + /// if !filetype.is_dir() { + /// // read file + /// } + /// # Ok::<_, std::io::Error>(()) + /// # }; + /// ``` + #[clippy::version = "1.42.0"] + pub FILETYPE_IS_FILE, + restriction, + "`FileType::is_file` is not recommended to test for readable file type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.as_ref().map(Deref::deref)` or it's aliases (such as String::as_str). + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely as + /// `_.as_deref()`. + /// + /// ### Example + /// ```rust + /// # let opt = Some("".to_string()); + /// opt.as_ref().map(String::as_str) + /// # ; + /// ``` + /// Can be written as + /// ```rust + /// # let opt = Some("".to_string()); + /// opt.as_deref() + /// # ; + /// ``` + #[clippy::version = "1.42.0"] + pub OPTION_AS_REF_DEREF, + complexity, + "using `as_ref().map(Deref::deref)`, which is more succinctly expressed as `as_deref()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `iter().next()` on a Slice or an Array + /// + /// ### Why is this bad? + /// These can be shortened into `.get()` + /// + /// ### Example + /// ```rust + /// # let a = [1, 2, 3]; + /// # let b = vec![1, 2, 3]; + /// a[2..].iter().next(); + /// b.iter().next(); + /// ``` + /// should be written as: + /// ```rust + /// # let a = [1, 2, 3]; + /// # let b = vec![1, 2, 3]; + /// a.get(2); + /// b.get(0); + /// ``` + #[clippy::version = "1.46.0"] + pub ITER_NEXT_SLICE, + style, + "using `.iter().next()` on a sliced array, which can be shortened to just `.get()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns when using `push_str`/`insert_str` with a single-character string literal + /// where `push`/`insert` with a `char` would work fine. + /// + /// ### Why is this bad? + /// It's less clear that we are pushing a single character. + /// + /// ### Example + /// ```rust + /// # let mut string = String::new(); + /// string.insert_str(0, "R"); + /// string.push_str("R"); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let mut string = String::new(); + /// string.insert(0, 'R'); + /// string.push('R'); + /// ``` + #[clippy::version = "1.49.0"] + pub SINGLE_CHAR_ADD_STR, + style, + "`push_str()` or `insert_str()` used with a single-character string literal as parameter" +} + +declare_clippy_lint! { + /// ### What it does + /// As the counterpart to `or_fun_call`, this lint looks for unnecessary + /// lazily evaluated closures on `Option` and `Result`. + /// + /// This lint suggests changing the following functions, when eager evaluation results in + /// simpler code: + /// - `unwrap_or_else` to `unwrap_or` + /// - `and_then` to `and` + /// - `or_else` to `or` + /// - `get_or_insert_with` to `get_or_insert` + /// - `ok_or_else` to `ok_or` + /// + /// ### Why is this bad? + /// Using eager evaluation is shorter and simpler in some cases. + /// + /// ### Known problems + /// It is possible, but not recommended for `Deref` and `Index` to have + /// side effects. Eagerly evaluating them can change the semantics of the program. + /// + /// ### Example + /// ```rust + /// // example code where clippy issues a warning + /// let opt: Option<u32> = None; + /// + /// opt.unwrap_or_else(|| 42); + /// ``` + /// Use instead: + /// ```rust + /// let opt: Option<u32> = None; + /// + /// opt.unwrap_or(42); + /// ``` + #[clippy::version = "1.48.0"] + pub UNNECESSARY_LAZY_EVALUATIONS, + style, + "using unnecessary lazy evaluation, which can be replaced with simpler eager evaluation" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `_.map(_).collect::<Result<(), _>()`. + /// + /// ### Why is this bad? + /// Using `try_for_each` instead is more readable and idiomatic. + /// + /// ### Example + /// ```rust + /// (0..3).map(|t| Err(t)).collect::<Result<(), _>>(); + /// ``` + /// Use instead: + /// ```rust + /// (0..3).try_for_each(|t| Err(t)); + /// ``` + #[clippy::version = "1.49.0"] + pub MAP_COLLECT_RESULT_UNIT, + style, + "using `.map(_).collect::<Result<(),_>()`, which can be replaced with `try_for_each`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `from_iter()` function calls on types that implement the `FromIterator` + /// trait. + /// + /// ### Why is this bad? + /// It is recommended style to use collect. See + /// [FromIterator documentation](https://doc.rust-lang.org/std/iter/trait.FromIterator.html) + /// + /// ### Example + /// ```rust + /// let five_fives = std::iter::repeat(5).take(5); + /// + /// let v = Vec::from_iter(five_fives); + /// + /// assert_eq!(v, vec![5, 5, 5, 5, 5]); + /// ``` + /// Use instead: + /// ```rust + /// let five_fives = std::iter::repeat(5).take(5); + /// + /// let v: Vec<i32> = five_fives.collect(); + /// + /// assert_eq!(v, vec![5, 5, 5, 5, 5]); + /// ``` + #[clippy::version = "1.49.0"] + pub FROM_ITER_INSTEAD_OF_COLLECT, + pedantic, + "use `.collect()` instead of `::from_iter()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `inspect().for_each()`. + /// + /// ### Why is this bad? + /// It is the same as performing the computation + /// inside `inspect` at the beginning of the closure in `for_each`. + /// + /// ### Example + /// ```rust + /// [1,2,3,4,5].iter() + /// .inspect(|&x| println!("inspect the number: {}", x)) + /// .for_each(|&x| { + /// assert!(x >= 0); + /// }); + /// ``` + /// Can be written as + /// ```rust + /// [1,2,3,4,5].iter() + /// .for_each(|&x| { + /// println!("inspect the number: {}", x); + /// assert!(x >= 0); + /// }); + /// ``` + #[clippy::version = "1.51.0"] + pub INSPECT_FOR_EACH, + complexity, + "using `.inspect().for_each()`, which can be replaced with `.for_each()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `filter_map(|x| x)`. + /// + /// ### Why is this bad? + /// Readability, this can be written more concisely by using `flatten`. + /// + /// ### Example + /// ```rust + /// # let iter = vec![Some(1)].into_iter(); + /// iter.filter_map(|x| x); + /// ``` + /// Use instead: + /// ```rust + /// # let iter = vec![Some(1)].into_iter(); + /// iter.flatten(); + /// ``` + #[clippy::version = "1.52.0"] + pub FILTER_MAP_IDENTITY, + complexity, + "call to `filter_map` where `flatten` is sufficient" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for instances of `map(f)` where `f` is the identity function. + /// + /// ### Why is this bad? + /// It can be written more concisely without the call to `map`. + /// + /// ### Example + /// ```rust + /// let x = [1, 2, 3]; + /// let y: Vec<_> = x.iter().map(|x| x).map(|x| 2*x).collect(); + /// ``` + /// Use instead: + /// ```rust + /// let x = [1, 2, 3]; + /// let y: Vec<_> = x.iter().map(|x| 2*x).collect(); + /// ``` + #[clippy::version = "1.47.0"] + pub MAP_IDENTITY, + complexity, + "using iterator.map(|x| x)" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of `.bytes().nth()`. + /// + /// ### Why is this bad? + /// `.as_bytes().get()` is more efficient and more + /// readable. + /// + /// ### Example + /// ```rust + /// # #[allow(unused)] + /// "Hello".bytes().nth(3); + /// ``` + /// + /// Use instead: + /// ```rust + /// # #[allow(unused)] + /// "Hello".as_bytes().get(3); + /// ``` + #[clippy::version = "1.52.0"] + pub BYTES_NTH, + style, + "replace `.bytes().nth()` with `.as_bytes().get()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the usage of `_.to_owned()`, `vec.to_vec()`, or similar when calling `_.clone()` would be clearer. + /// + /// ### Why is this bad? + /// These methods do the same thing as `_.clone()` but may be confusing as + /// to why we are calling `to_vec` on something that is already a `Vec` or calling `to_owned` on something that is already owned. + /// + /// ### Example + /// ```rust + /// let a = vec![1, 2, 3]; + /// let b = a.to_vec(); + /// let c = a.to_owned(); + /// ``` + /// Use instead: + /// ```rust + /// let a = vec![1, 2, 3]; + /// let b = a.clone(); + /// let c = a.clone(); + /// ``` + #[clippy::version = "1.52.0"] + pub IMPLICIT_CLONE, + pedantic, + "implicitly cloning a value by invoking a function on its dereferenced type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of `.iter().count()`. + /// + /// ### Why is this bad? + /// `.len()` is more efficient and more + /// readable. + /// + /// ### Example + /// ```rust + /// # #![allow(unused)] + /// let some_vec = vec![0, 1, 2, 3]; + /// + /// some_vec.iter().count(); + /// &some_vec[..].iter().count(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let some_vec = vec![0, 1, 2, 3]; + /// + /// some_vec.len(); + /// &some_vec[..].len(); + /// ``` + #[clippy::version = "1.52.0"] + pub ITER_COUNT, + complexity, + "replace `.iter().count()` with `.len()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to [`splitn`] + /// (https://doc.rust-lang.org/std/primitive.str.html#method.splitn) and + /// related functions with either zero or one splits. + /// + /// ### Why is this bad? + /// These calls don't actually split the value and are + /// likely to be intended as a different number. + /// + /// ### Example + /// ```rust + /// # let s = ""; + /// for x in s.splitn(1, ":") { + /// // .. + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let s = ""; + /// for x in s.splitn(2, ":") { + /// // .. + /// } + /// ``` + #[clippy::version = "1.54.0"] + pub SUSPICIOUS_SPLITN, + correctness, + "checks for `.splitn(0, ..)` and `.splitn(1, ..)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for manual implementations of `str::repeat` + /// + /// ### Why is this bad? + /// These are both harder to read, as well as less performant. + /// + /// ### Example + /// ```rust + /// let x: String = std::iter::repeat('x').take(10).collect(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let x: String = "x".repeat(10); + /// ``` + #[clippy::version = "1.54.0"] + pub MANUAL_STR_REPEAT, + perf, + "manual implementation of `str::repeat`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `str::splitn(2, _)` + /// + /// ### Why is this bad? + /// `split_once` is both clearer in intent and slightly more efficient. + /// + /// ### Example + /// ```rust,ignore + /// let s = "key=value=add"; + /// let (key, value) = s.splitn(2, '=').next_tuple()?; + /// let value = s.splitn(2, '=').nth(1)?; + /// + /// let mut parts = s.splitn(2, '='); + /// let key = parts.next()?; + /// let value = parts.next()?; + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// let s = "key=value=add"; + /// let (key, value) = s.split_once('=')?; + /// let value = s.split_once('=')?.1; + /// + /// let (key, value) = s.split_once('=')?; + /// ``` + /// + /// ### Limitations + /// The multiple statement variant currently only detects `iter.next()?`/`iter.next().unwrap()` + /// in two separate `let` statements that immediately follow the `splitn()` + #[clippy::version = "1.57.0"] + pub MANUAL_SPLIT_ONCE, + complexity, + "replace `.splitn(2, pat)` with `.split_once(pat)`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `str::splitn` (or `str::rsplitn`) where using `str::split` would be the same. + /// ### Why is this bad? + /// The function `split` is simpler and there is no performance difference in these cases, considering + /// that both functions return a lazy iterator. + /// ### Example + /// ```rust + /// let str = "key=value=add"; + /// let _ = str.splitn(3, '=').next().unwrap(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let str = "key=value=add"; + /// let _ = str.split('=').next().unwrap(); + /// ``` + #[clippy::version = "1.59.0"] + pub NEEDLESS_SPLITN, + complexity, + "usages of `str::splitn` that can be replaced with `str::split`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary calls to [`ToOwned::to_owned`](https://doc.rust-lang.org/std/borrow/trait.ToOwned.html#tymethod.to_owned) + /// and other `to_owned`-like functions. + /// + /// ### Why is this bad? + /// The unnecessary calls result in useless allocations. + /// + /// ### Known problems + /// `unnecessary_to_owned` can falsely trigger if `IntoIterator::into_iter` is applied to an + /// owned copy of a resource and the resource is later used mutably. See + /// [#8148](https://github.com/rust-lang/rust-clippy/issues/8148). + /// + /// ### Example + /// ```rust + /// let path = std::path::Path::new("x"); + /// foo(&path.to_string_lossy().to_string()); + /// fn foo(s: &str) {} + /// ``` + /// Use instead: + /// ```rust + /// let path = std::path::Path::new("x"); + /// foo(&path.to_string_lossy()); + /// fn foo(s: &str) {} + /// ``` + #[clippy::version = "1.59.0"] + pub UNNECESSARY_TO_OWNED, + perf, + "unnecessary calls to `to_owned`-like functions" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `.collect::<Vec<String>>().join("")` on iterators. + /// + /// ### Why is this bad? + /// `.collect::<String>()` is more concise and might be more performant + /// + /// ### Example + /// ```rust + /// let vector = vec!["hello", "world"]; + /// let output = vector.iter().map(|item| item.to_uppercase()).collect::<Vec<String>>().join(""); + /// println!("{}", output); + /// ``` + /// The correct use would be: + /// ```rust + /// let vector = vec!["hello", "world"]; + /// let output = vector.iter().map(|item| item.to_uppercase()).collect::<String>(); + /// println!("{}", output); + /// ``` + /// ### Known problems + /// While `.collect::<String>()` is sometimes more performant, there are cases where + /// using `.collect::<String>()` over `.collect::<Vec<String>>().join("")` + /// will prevent loop unrolling and will result in a negative performance impact. + /// + /// Additionally, differences have been observed between aarch64 and x86_64 assembly output, + /// with aarch64 tending to producing faster assembly in more cases when using `.collect::<String>()` + #[clippy::version = "1.61.0"] + pub UNNECESSARY_JOIN, + pedantic, + "using `.collect::<Vec<String>>().join(\"\")` on an iterator" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for no-op uses of `Option::{as_deref, as_deref_mut}`, + /// for example, `Option<&T>::as_deref()` returns the same type. + /// + /// ### Why is this bad? + /// Redundant code and improving readability. + /// + /// ### Example + /// ```rust + /// let a = Some(&1); + /// let b = a.as_deref(); // goes from Option<&i32> to Option<&i32> + /// ``` + /// + /// Use instead: + /// ```rust + /// let a = Some(&1); + /// let b = a; + /// ``` + #[clippy::version = "1.57.0"] + pub NEEDLESS_OPTION_AS_DEREF, + complexity, + "no-op use of `deref` or `deref_mut` method to `Option`." +} + +declare_clippy_lint! { + /// ### What it does + /// Finds usages of [`char::is_digit`](https://doc.rust-lang.org/stable/std/primitive.char.html#method.is_digit) that + /// can be replaced with [`is_ascii_digit`](https://doc.rust-lang.org/stable/std/primitive.char.html#method.is_ascii_digit) or + /// [`is_ascii_hexdigit`](https://doc.rust-lang.org/stable/std/primitive.char.html#method.is_ascii_hexdigit). + /// + /// ### Why is this bad? + /// `is_digit(..)` is slower and requires specifying the radix. + /// + /// ### Example + /// ```rust + /// let c: char = '6'; + /// c.is_digit(10); + /// c.is_digit(16); + /// ``` + /// Use instead: + /// ```rust + /// let c: char = '6'; + /// c.is_ascii_digit(); + /// c.is_ascii_hexdigit(); + /// ``` + #[clippy::version = "1.62.0"] + pub IS_DIGIT_ASCII_RADIX, + style, + "use of `char::is_digit(..)` with literal radix of 10 or 16" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calling `take` function after `as_ref`. + /// + /// ### Why is this bad? + /// Redundant code. `take` writes `None` to its argument. + /// In this case the modification is useless as it's a temporary that cannot be read from afterwards. + /// + /// ### Example + /// ```rust + /// let x = Some(3); + /// x.as_ref().take(); + /// ``` + /// Use instead: + /// ```rust + /// let x = Some(3); + /// x.as_ref(); + /// ``` + #[clippy::version = "1.62.0"] + pub NEEDLESS_OPTION_TAKE, + complexity, + "using `.as_ref().take()` on a temporary value" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `replace` statements which have no effect. + /// + /// ### Why is this bad? + /// It's either a mistake or confusing. + /// + /// ### Example + /// ```rust + /// "1234".replace("12", "12"); + /// "1234".replacen("12", "12", 1); + /// ``` + #[clippy::version = "1.62.0"] + pub NO_EFFECT_REPLACE, + suspicious, + "replace with no effect" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `.then_some(..).unwrap_or(..)` + /// + /// ### Why is this bad? + /// This can be written more clearly with `if .. else ..` + /// + /// ### Limitations + /// This lint currently only looks for usages of + /// `.then_some(..).unwrap_or(..)`, but will be expanded + /// to account for similar patterns. + /// + /// ### Example + /// ```rust + /// let x = true; + /// x.then_some("a").unwrap_or("b"); + /// ``` + /// Use instead: + /// ```rust + /// let x = true; + /// if x { "a" } else { "b" }; + /// ``` + #[clippy::version = "1.64.0"] + pub OBFUSCATED_IF_ELSE, + style, + "use of `.then_some(..).unwrap_or(..)` can be written \ + more clearly with `if .. else ..`" +} + +pub struct Methods { + avoid_breaking_exported_api: bool, + msrv: Option<RustcVersion>, + allow_expect_in_tests: bool, + allow_unwrap_in_tests: bool, +} + +impl Methods { + #[must_use] + pub fn new( + avoid_breaking_exported_api: bool, + msrv: Option<RustcVersion>, + allow_expect_in_tests: bool, + allow_unwrap_in_tests: bool, + ) -> Self { + Self { + avoid_breaking_exported_api, + msrv, + allow_expect_in_tests, + allow_unwrap_in_tests, + } + } +} + +impl_lint_pass!(Methods => [ + UNWRAP_USED, + EXPECT_USED, + SHOULD_IMPLEMENT_TRAIT, + WRONG_SELF_CONVENTION, + OK_EXPECT, + UNWRAP_OR_ELSE_DEFAULT, + MAP_UNWRAP_OR, + RESULT_MAP_OR_INTO_OPTION, + OPTION_MAP_OR_NONE, + BIND_INSTEAD_OF_MAP, + OR_FUN_CALL, + OR_THEN_UNWRAP, + EXPECT_FUN_CALL, + CHARS_NEXT_CMP, + CHARS_LAST_CMP, + CLONE_ON_COPY, + CLONE_ON_REF_PTR, + CLONE_DOUBLE_REF, + ITER_OVEREAGER_CLONED, + CLONED_INSTEAD_OF_COPIED, + FLAT_MAP_OPTION, + INEFFICIENT_TO_STRING, + NEW_RET_NO_SELF, + SINGLE_CHAR_PATTERN, + SINGLE_CHAR_ADD_STR, + SEARCH_IS_SOME, + FILTER_NEXT, + SKIP_WHILE_NEXT, + FILTER_MAP_IDENTITY, + MAP_IDENTITY, + MANUAL_FILTER_MAP, + MANUAL_FIND_MAP, + OPTION_FILTER_MAP, + FILTER_MAP_NEXT, + FLAT_MAP_IDENTITY, + MAP_FLATTEN, + ITERATOR_STEP_BY_ZERO, + ITER_NEXT_SLICE, + ITER_COUNT, + ITER_NTH, + ITER_NTH_ZERO, + BYTES_NTH, + ITER_SKIP_NEXT, + GET_UNWRAP, + GET_LAST_WITH_LEN, + STRING_EXTEND_CHARS, + ITER_CLONED_COLLECT, + ITER_WITH_DRAIN, + USELESS_ASREF, + UNNECESSARY_FOLD, + UNNECESSARY_FILTER_MAP, + UNNECESSARY_FIND_MAP, + INTO_ITER_ON_REF, + SUSPICIOUS_MAP, + UNINIT_ASSUMED_INIT, + MANUAL_SATURATING_ARITHMETIC, + ZST_OFFSET, + FILETYPE_IS_FILE, + OPTION_AS_REF_DEREF, + UNNECESSARY_LAZY_EVALUATIONS, + MAP_COLLECT_RESULT_UNIT, + FROM_ITER_INSTEAD_OF_COLLECT, + INSPECT_FOR_EACH, + IMPLICIT_CLONE, + SUSPICIOUS_SPLITN, + MANUAL_STR_REPEAT, + EXTEND_WITH_DRAIN, + MANUAL_SPLIT_ONCE, + NEEDLESS_SPLITN, + UNNECESSARY_TO_OWNED, + UNNECESSARY_JOIN, + ERR_EXPECT, + NEEDLESS_OPTION_AS_DEREF, + IS_DIGIT_ASCII_RADIX, + NEEDLESS_OPTION_TAKE, + NO_EFFECT_REPLACE, + OBFUSCATED_IF_ELSE, +]); + +/// Extracts a method call name, args, and `Span` of the method name. +fn method_call<'tcx>(recv: &'tcx hir::Expr<'tcx>) -> Option<(&'tcx str, &'tcx [hir::Expr<'tcx>], Span)> { + if let ExprKind::MethodCall(path, args, _) = recv.kind { + if !args.iter().any(|e| e.span.from_expansion()) { + let name = path.ident.name.as_str(); + return Some((name, args, path.ident.span)); + } + } + None +} + +impl<'tcx> LateLintPass<'tcx> for Methods { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if expr.span.from_expansion() { + return; + } + + self.check_methods(cx, expr); + + match expr.kind { + hir::ExprKind::Call(func, args) => { + from_iter_instead_of_collect::check(cx, expr, args, func); + }, + hir::ExprKind::MethodCall(method_call, args, _) => { + let method_span = method_call.ident.span; + or_fun_call::check(cx, expr, method_span, method_call.ident.as_str(), args); + expect_fun_call::check(cx, expr, method_span, method_call.ident.as_str(), args); + clone_on_copy::check(cx, expr, method_call.ident.name, args); + clone_on_ref_ptr::check(cx, expr, method_call.ident.name, args); + inefficient_to_string::check(cx, expr, method_call.ident.name, args); + single_char_add_str::check(cx, expr, args); + into_iter_on_ref::check(cx, expr, method_span, method_call.ident.name, args); + single_char_pattern::check(cx, expr, method_call.ident.name, args); + unnecessary_to_owned::check(cx, expr, method_call.ident.name, args, self.msrv); + }, + hir::ExprKind::Binary(op, lhs, rhs) if op.node == hir::BinOpKind::Eq || op.node == hir::BinOpKind::Ne => { + let mut info = BinaryExprInfo { + expr, + chain: lhs, + other: rhs, + eq: op.node == hir::BinOpKind::Eq, + }; + lint_binary_expr_with_method_call(cx, &mut info); + }, + _ => (), + } + } + + #[allow(clippy::too_many_lines)] + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx hir::ImplItem<'_>) { + if in_external_macro(cx.sess(), impl_item.span) { + return; + } + let name = impl_item.ident.name.as_str(); + let parent = cx.tcx.hir().get_parent_item(impl_item.hir_id()); + let item = cx.tcx.hir().expect_item(parent); + let self_ty = cx.tcx.type_of(item.def_id); + + let implements_trait = matches!(item.kind, hir::ItemKind::Impl(hir::Impl { of_trait: Some(_), .. })); + if_chain! { + if let hir::ImplItemKind::Fn(ref sig, id) = impl_item.kind; + if let Some(first_arg) = iter_input_pats(sig.decl, cx.tcx.hir().body(id)).next(); + + let method_sig = cx.tcx.fn_sig(impl_item.def_id); + let method_sig = cx.tcx.erase_late_bound_regions(method_sig); + + let first_arg_ty = method_sig.inputs().iter().next(); + + // check conventions w.r.t. conversion method names and predicates + if let Some(first_arg_ty) = first_arg_ty; + + then { + // if this impl block implements a trait, lint in trait definition instead + if !implements_trait && cx.access_levels.is_exported(impl_item.def_id) { + // check missing trait implementations + for method_config in &TRAIT_METHODS { + if name == method_config.method_name && + sig.decl.inputs.len() == method_config.param_count && + method_config.output_type.matches(&sig.decl.output) && + method_config.self_kind.matches(cx, self_ty, *first_arg_ty) && + fn_header_equals(method_config.fn_header, sig.header) && + method_config.lifetime_param_cond(impl_item) + { + span_lint_and_help( + cx, + SHOULD_IMPLEMENT_TRAIT, + impl_item.span, + &format!( + "method `{}` can be confused for the standard trait method `{}::{}`", + method_config.method_name, + method_config.trait_name, + method_config.method_name + ), + None, + &format!( + "consider implementing the trait `{}` or choosing a less ambiguous method name", + method_config.trait_name + ) + ); + } + } + } + + if sig.decl.implicit_self.has_implicit_self() + && !(self.avoid_breaking_exported_api + && cx.access_levels.is_exported(impl_item.def_id)) + { + wrong_self_convention::check( + cx, + name, + self_ty, + *first_arg_ty, + first_arg.pat.span, + implements_trait, + false + ); + } + } + } + + // if this impl block implements a trait, lint in trait definition instead + if implements_trait { + return; + } + + if let hir::ImplItemKind::Fn(_, _) = impl_item.kind { + let ret_ty = return_ty(cx, impl_item.hir_id()); + + // walk the return type and check for Self (this does not check associated types) + if let Some(self_adt) = self_ty.ty_adt_def() { + if contains_adt_constructor(ret_ty, self_adt) { + return; + } + } else if contains_ty(ret_ty, self_ty) { + return; + } + + // if return type is impl trait, check the associated types + if let ty::Opaque(def_id, _) = *ret_ty.kind() { + // one of the associated types must be Self + for &(predicate, _span) in cx.tcx.explicit_item_bounds(def_id) { + if let ty::PredicateKind::Projection(projection_predicate) = predicate.kind().skip_binder() { + let assoc_ty = match projection_predicate.term { + ty::Term::Ty(ty) => ty, + ty::Term::Const(_c) => continue, + }; + // walk the associated type and check for Self + if let Some(self_adt) = self_ty.ty_adt_def() { + if contains_adt_constructor(assoc_ty, self_adt) { + return; + } + } else if contains_ty(assoc_ty, self_ty) { + return; + } + } + } + } + + if name == "new" && ret_ty != self_ty { + span_lint( + cx, + NEW_RET_NO_SELF, + impl_item.span, + "methods called `new` usually return `Self`", + ); + } + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) { + if in_external_macro(cx.tcx.sess, item.span) { + return; + } + + if_chain! { + if let TraitItemKind::Fn(ref sig, _) = item.kind; + if sig.decl.implicit_self.has_implicit_self(); + if let Some(first_arg_ty) = sig.decl.inputs.iter().next(); + + then { + let first_arg_span = first_arg_ty.span; + let first_arg_ty = hir_ty_to_ty(cx.tcx, first_arg_ty); + let self_ty = TraitRef::identity(cx.tcx, item.def_id.to_def_id()).self_ty().skip_binder(); + wrong_self_convention::check( + cx, + item.ident.name.as_str(), + self_ty, + first_arg_ty, + first_arg_span, + false, + true + ); + } + } + + if_chain! { + if item.ident.name == sym::new; + if let TraitItemKind::Fn(_, _) = item.kind; + let ret_ty = return_ty(cx, item.hir_id()); + let self_ty = TraitRef::identity(cx.tcx, item.def_id.to_def_id()).self_ty().skip_binder(); + if !contains_ty(ret_ty, self_ty); + + then { + span_lint( + cx, + NEW_RET_NO_SELF, + item.span, + "methods called `new` usually return `Self`", + ); + } + } + } + + extract_msrv_attr!(LateContext); +} + +impl Methods { + #[allow(clippy::too_many_lines)] + fn check_methods<'tcx>(&self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let Some((name, [recv, args @ ..], span)) = method_call(expr) { + match (name, args) { + ("add" | "offset" | "sub" | "wrapping_offset" | "wrapping_add" | "wrapping_sub", [_arg]) => { + zst_offset::check(cx, expr, recv); + }, + ("and_then", [arg]) => { + let biom_option_linted = bind_instead_of_map::OptionAndThenSome::check(cx, expr, recv, arg); + let biom_result_linted = bind_instead_of_map::ResultAndThenOk::check(cx, expr, recv, arg); + if !biom_option_linted && !biom_result_linted { + unnecessary_lazy_eval::check(cx, expr, recv, arg, "and"); + } + }, + ("as_deref" | "as_deref_mut", []) => { + needless_option_as_deref::check(cx, expr, recv, name); + }, + ("as_mut", []) => useless_asref::check(cx, expr, "as_mut", recv), + ("as_ref", []) => useless_asref::check(cx, expr, "as_ref", recv), + ("assume_init", []) => uninit_assumed_init::check(cx, expr, recv), + ("cloned", []) => cloned_instead_of_copied::check(cx, expr, recv, span, self.msrv), + ("collect", []) => match method_call(recv) { + Some((name @ ("cloned" | "copied"), [recv2], _)) => { + iter_cloned_collect::check(cx, name, expr, recv2); + }, + Some(("map", [m_recv, m_arg], _)) => { + map_collect_result_unit::check(cx, expr, m_recv, m_arg, recv); + }, + Some(("take", [take_self_arg, take_arg], _)) => { + if meets_msrv(self.msrv, msrvs::STR_REPEAT) { + manual_str_repeat::check(cx, expr, recv, take_self_arg, take_arg); + } + }, + _ => {}, + }, + ("count", []) => match method_call(recv) { + Some(("cloned", [recv2], _)) => iter_overeager_cloned::check(cx, expr, recv, recv2, true, false), + Some((name2 @ ("into_iter" | "iter" | "iter_mut"), [recv2], _)) => { + iter_count::check(cx, expr, recv2, name2); + }, + Some(("map", [_, arg], _)) => suspicious_map::check(cx, expr, recv, arg), + _ => {}, + }, + ("drain", [arg]) => { + iter_with_drain::check(cx, expr, recv, span, 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, self.msrv, span, err_span), + _ => expect_used::check(cx, expr, recv, self.allow_expect_in_tests), + }, + ("extend", [arg]) => { + string_extend_chars::check(cx, expr, recv, arg); + extend_with_drain::check(cx, expr, recv, arg); + }, + ("filter_map", [arg]) => { + unnecessary_filter_map::check(cx, expr, arg, name); + filter_map_identity::check(cx, expr, arg, span); + }, + ("find_map", [arg]) => { + unnecessary_filter_map::check(cx, expr, arg, name); + }, + ("flat_map", [arg]) => { + flat_map_identity::check(cx, expr, arg, span); + flat_map_option::check(cx, expr, arg, span); + }, + ("flatten", []) => match method_call(recv) { + Some(("map", [recv, map_arg], map_span)) => map_flatten::check(cx, expr, recv, map_arg, map_span), + Some(("cloned", [recv2], _)) => iter_overeager_cloned::check(cx, expr, recv, recv2, false, true), + _ => {}, + }, + ("fold", [init, acc]) => 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); + } + }, + ("get", [arg]) => get_last_with_len::check(cx, expr, recv, arg), + ("get_or_insert_with", [arg]) => unnecessary_lazy_eval::check(cx, expr, recv, arg, "get_or_insert"), + ("is_file", []) => filetype_is_file::check(cx, expr, recv), + ("is_digit", [radix]) => is_digit_ascii_radix::check(cx, expr, recv, radix, self.msrv), + ("is_none", []) => check_is_some_is_none(cx, expr, recv, false), + ("is_some", []) => check_is_some_is_none(cx, expr, recv, true), + ("join", [join_arg]) => { + if let Some(("collect", _, span)) = method_call(recv) { + unnecessary_join::check(cx, expr, recv, join_arg, span); + } + }, + ("last", []) | ("skip", [_]) => { + if let Some((name2, [recv2, args2 @ ..], _span2)) = method_call(recv) { + if let ("cloned", []) = (name2, args2) { + iter_overeager_cloned::check(cx, expr, recv, recv2, false, false); + } + } + }, + (name @ ("map" | "map_err"), [m_arg]) => { + if let Some((name, [recv2, args @ ..], span2)) = method_call(recv) { + match (name, args) { + ("as_mut", []) => option_as_ref_deref::check(cx, expr, recv2, m_arg, true, self.msrv), + ("as_ref", []) => option_as_ref_deref::check(cx, expr, recv2, m_arg, false, self.msrv), + ("filter", [f_arg]) => { + filter_map::check(cx, expr, recv2, f_arg, span2, recv, m_arg, span, false); + }, + ("find", [f_arg]) => { + filter_map::check(cx, expr, recv2, f_arg, span2, recv, m_arg, span, true); + }, + _ => {}, + } + } + map_identity::check(cx, expr, recv, m_arg, name, span); + }, + ("map_or", [def, map]) => option_map_or_none::check(cx, expr, recv, def, map), + ("next", []) => { + if let Some((name2, [recv2, args2 @ ..], _)) = method_call(recv) { + match (name2, args2) { + ("cloned", []) => iter_overeager_cloned::check(cx, expr, recv, recv2, false, false), + ("filter", [arg]) => filter_next::check(cx, expr, recv2, arg), + ("filter_map", [arg]) => filter_map_next::check(cx, expr, recv2, arg, self.msrv), + ("iter", []) => iter_next_slice::check(cx, expr, recv2), + ("skip", [arg]) => iter_skip_next::check(cx, expr, recv2, arg), + ("skip_while", [_]) => skip_while_next::check(cx, expr), + _ => {}, + } + } + }, + ("nth", [n_arg]) => match method_call(recv) { + Some(("bytes", [recv2], _)) => bytes_nth::check(cx, expr, recv2, n_arg), + Some(("cloned", [recv2], _)) => iter_overeager_cloned::check(cx, expr, recv, recv2, false, false), + Some(("iter", [recv2], _)) => iter_nth::check(cx, expr, recv2, recv, n_arg, false), + Some(("iter_mut", [recv2], _)) => iter_nth::check(cx, expr, recv2, recv, n_arg, true), + _ => iter_nth_zero::check(cx, expr, recv, n_arg), + }, + ("ok_or_else", [arg]) => unnecessary_lazy_eval::check(cx, expr, recv, arg, "ok_or"), + ("or_else", [arg]) => { + if !bind_instead_of_map::ResultOrElseErrInfo::check(cx, expr, recv, arg) { + unnecessary_lazy_eval::check(cx, expr, recv, arg, "or"); + } + }, + ("splitn" | "rsplitn", [count_arg, pat_arg]) => { + if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg) { + suspicious_splitn::check(cx, name, expr, recv, count); + str_splitn::check(cx, name, expr, recv, pat_arg, count, self.msrv); + } + }, + ("splitn_mut" | "rsplitn_mut", [count_arg, _]) => { + if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg) { + suspicious_splitn::check(cx, name, expr, recv, count); + } + }, + ("step_by", [arg]) => iterator_step_by_zero::check(cx, expr, arg), + ("take", [_arg]) => { + if let Some((name2, [recv2, args2 @ ..], _span2)) = method_call(recv) { + if let ("cloned", []) = (name2, args2) { + iter_overeager_cloned::check(cx, expr, recv, recv2, false, false); + } + } + }, + ("take", []) => needless_option_take::check(cx, expr, recv), + ("then", [arg]) => { + if !meets_msrv(self.msrv, msrvs::BOOL_THEN_SOME) { + return; + } + unnecessary_lazy_eval::check(cx, expr, recv, arg, "then_some"); + }, + ("to_os_string" | "to_owned" | "to_path_buf" | "to_vec", []) => { + implicit_clone::check(cx, name, expr, recv); + }, + ("unwrap", []) => { + match method_call(recv) { + Some(("get", [recv, get_arg], _)) => { + get_unwrap::check(cx, expr, recv, get_arg, false); + }, + Some(("get_mut", [recv, get_arg], _)) => { + get_unwrap::check(cx, expr, recv, get_arg, true); + }, + Some(("or", [recv, or_arg], or_span)) => { + or_then_unwrap::check(cx, expr, recv, or_arg, or_span); + }, + _ => {}, + } + unwrap_used::check(cx, expr, recv, 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_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"); + }, + }, + ("replace" | "replacen", [arg1, arg2] | [arg1, arg2, _]) => { + no_effect_replace::check(cx, expr, arg1, arg2); + }, + _ => {}, + } + } + } +} + +fn check_is_some_is_none(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, is_some: bool) { + if let Some((name @ ("find" | "position" | "rposition"), [f_recv, arg], span)) = method_call(recv) { + search_is_some::check(cx, expr, name, is_some, f_recv, arg, recv, span); + } +} + +/// Used for `lint_binary_expr_with_method_call`. +#[derive(Copy, Clone)] +struct BinaryExprInfo<'a> { + expr: &'a hir::Expr<'a>, + chain: &'a hir::Expr<'a>, + other: &'a hir::Expr<'a>, + eq: bool, +} + +/// Checks for the `CHARS_NEXT_CMP` and `CHARS_LAST_CMP` lints. +fn lint_binary_expr_with_method_call(cx: &LateContext<'_>, info: &mut BinaryExprInfo<'_>) { + macro_rules! lint_with_both_lhs_and_rhs { + ($func:expr, $cx:expr, $info:ident) => { + if !$func($cx, $info) { + ::std::mem::swap(&mut $info.chain, &mut $info.other); + if $func($cx, $info) { + return; + } + } + }; + } + + lint_with_both_lhs_and_rhs!(chars_next_cmp::check, cx, info); + lint_with_both_lhs_and_rhs!(chars_last_cmp::check, cx, info); + lint_with_both_lhs_and_rhs!(chars_next_cmp_with_unwrap::check, cx, info); + lint_with_both_lhs_and_rhs!(chars_last_cmp_with_unwrap::check, cx, info); +} + +const FN_HEADER: hir::FnHeader = hir::FnHeader { + unsafety: hir::Unsafety::Normal, + constness: hir::Constness::NotConst, + asyncness: hir::IsAsync::NotAsync, + abi: rustc_target::spec::abi::Abi::Rust, +}; + +struct ShouldImplTraitCase { + trait_name: &'static str, + method_name: &'static str, + param_count: usize, + fn_header: hir::FnHeader, + // implicit self kind expected (none, self, &self, ...) + self_kind: SelfKind, + // checks against the output type + output_type: OutType, + // certain methods with explicit lifetimes can't implement the equivalent trait method + lint_explicit_lifetime: bool, +} +impl ShouldImplTraitCase { + const fn new( + trait_name: &'static str, + method_name: &'static str, + param_count: usize, + fn_header: hir::FnHeader, + self_kind: SelfKind, + output_type: OutType, + lint_explicit_lifetime: bool, + ) -> ShouldImplTraitCase { + ShouldImplTraitCase { + trait_name, + method_name, + param_count, + fn_header, + self_kind, + output_type, + lint_explicit_lifetime, + } + } + + fn lifetime_param_cond(&self, impl_item: &hir::ImplItem<'_>) -> bool { + self.lint_explicit_lifetime + || !impl_item.generics.params.iter().any(|p| { + matches!( + p.kind, + hir::GenericParamKind::Lifetime { + kind: hir::LifetimeParamKind::Explicit + } + ) + }) + } +} + +#[rustfmt::skip] +const TRAIT_METHODS: [ShouldImplTraitCase; 30] = [ + ShouldImplTraitCase::new("std::ops::Add", "add", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::convert::AsMut", "as_mut", 1, FN_HEADER, SelfKind::RefMut, OutType::Ref, true), + ShouldImplTraitCase::new("std::convert::AsRef", "as_ref", 1, FN_HEADER, SelfKind::Ref, OutType::Ref, true), + ShouldImplTraitCase::new("std::ops::BitAnd", "bitand", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::BitOr", "bitor", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::BitXor", "bitxor", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::borrow::Borrow", "borrow", 1, FN_HEADER, SelfKind::Ref, OutType::Ref, true), + ShouldImplTraitCase::new("std::borrow::BorrowMut", "borrow_mut", 1, FN_HEADER, SelfKind::RefMut, OutType::Ref, true), + ShouldImplTraitCase::new("std::clone::Clone", "clone", 1, FN_HEADER, SelfKind::Ref, OutType::Any, true), + ShouldImplTraitCase::new("std::cmp::Ord", "cmp", 2, FN_HEADER, SelfKind::Ref, OutType::Any, true), + // FIXME: default doesn't work + ShouldImplTraitCase::new("std::default::Default", "default", 0, FN_HEADER, SelfKind::No, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Deref", "deref", 1, FN_HEADER, SelfKind::Ref, OutType::Ref, true), + ShouldImplTraitCase::new("std::ops::DerefMut", "deref_mut", 1, FN_HEADER, SelfKind::RefMut, OutType::Ref, true), + ShouldImplTraitCase::new("std::ops::Div", "div", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Drop", "drop", 1, FN_HEADER, SelfKind::RefMut, OutType::Unit, true), + ShouldImplTraitCase::new("std::cmp::PartialEq", "eq", 2, FN_HEADER, SelfKind::Ref, OutType::Bool, true), + ShouldImplTraitCase::new("std::iter::FromIterator", "from_iter", 1, FN_HEADER, SelfKind::No, OutType::Any, true), + ShouldImplTraitCase::new("std::str::FromStr", "from_str", 1, FN_HEADER, SelfKind::No, OutType::Any, true), + ShouldImplTraitCase::new("std::hash::Hash", "hash", 2, FN_HEADER, SelfKind::Ref, OutType::Unit, true), + ShouldImplTraitCase::new("std::ops::Index", "index", 2, FN_HEADER, SelfKind::Ref, OutType::Ref, true), + ShouldImplTraitCase::new("std::ops::IndexMut", "index_mut", 2, FN_HEADER, SelfKind::RefMut, OutType::Ref, true), + ShouldImplTraitCase::new("std::iter::IntoIterator", "into_iter", 1, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Mul", "mul", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Neg", "neg", 1, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::iter::Iterator", "next", 1, FN_HEADER, SelfKind::RefMut, OutType::Any, false), + ShouldImplTraitCase::new("std::ops::Not", "not", 1, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Rem", "rem", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Shl", "shl", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Shr", "shr", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), + ShouldImplTraitCase::new("std::ops::Sub", "sub", 2, FN_HEADER, SelfKind::Value, OutType::Any, true), +]; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum SelfKind { + Value, + Ref, + RefMut, + No, +} + +impl SelfKind { + fn matches<'a>(self, cx: &LateContext<'a>, parent_ty: Ty<'a>, ty: Ty<'a>) -> bool { + fn matches_value<'a>(cx: &LateContext<'a>, parent_ty: Ty<'a>, ty: Ty<'a>) -> bool { + if ty == parent_ty { + true + } else if ty.is_box() { + ty.boxed_ty() == parent_ty + } else if is_type_diagnostic_item(cx, ty, sym::Rc) || is_type_diagnostic_item(cx, ty, sym::Arc) { + if let ty::Adt(_, substs) = ty.kind() { + substs.types().next().map_or(false, |t| t == parent_ty) + } else { + false + } + } else { + false + } + } + + fn matches_ref<'a>(cx: &LateContext<'a>, mutability: hir::Mutability, parent_ty: Ty<'a>, ty: Ty<'a>) -> bool { + if let ty::Ref(_, t, m) = *ty.kind() { + return m == mutability && t == parent_ty; + } + + let trait_path = match mutability { + hir::Mutability::Not => &paths::ASREF_TRAIT, + hir::Mutability::Mut => &paths::ASMUT_TRAIT, + }; + + let trait_def_id = match get_trait_def_id(cx, trait_path) { + Some(did) => did, + None => return false, + }; + implements_trait(cx, ty, trait_def_id, &[parent_ty.into()]) + } + + fn matches_none<'a>(cx: &LateContext<'a>, parent_ty: Ty<'a>, ty: Ty<'a>) -> bool { + !matches_value(cx, parent_ty, ty) + && !matches_ref(cx, hir::Mutability::Not, parent_ty, ty) + && !matches_ref(cx, hir::Mutability::Mut, parent_ty, ty) + } + + match self { + Self::Value => matches_value(cx, parent_ty, ty), + Self::Ref => matches_ref(cx, hir::Mutability::Not, parent_ty, ty) || ty == parent_ty && is_copy(cx, ty), + Self::RefMut => matches_ref(cx, hir::Mutability::Mut, parent_ty, ty), + Self::No => matches_none(cx, parent_ty, ty), + } + } + + #[must_use] + fn description(self) -> &'static str { + match self { + Self::Value => "`self` by value", + Self::Ref => "`self` by reference", + Self::RefMut => "`self` by mutable reference", + Self::No => "no `self`", + } + } +} + +#[derive(Clone, Copy)] +enum OutType { + Unit, + Bool, + Any, + Ref, +} + +impl OutType { + fn matches(self, ty: &hir::FnRetTy<'_>) -> bool { + let is_unit = |ty: &hir::Ty<'_>| matches!(ty.kind, hir::TyKind::Tup(&[])); + match (self, ty) { + (Self::Unit, &hir::FnRetTy::DefaultReturn(_)) => true, + (Self::Unit, &hir::FnRetTy::Return(ty)) if is_unit(ty) => true, + (Self::Bool, &hir::FnRetTy::Return(ty)) if is_bool(ty) => true, + (Self::Any, &hir::FnRetTy::Return(ty)) if !is_unit(ty) => true, + (Self::Ref, &hir::FnRetTy::Return(ty)) => matches!(ty.kind, hir::TyKind::Rptr(_, _)), + _ => false, + } + } +} + +fn is_bool(ty: &hir::Ty<'_>) -> bool { + if let hir::TyKind::Path(QPath::Resolved(_, path)) = ty.kind { + matches!(path.res, Res::PrimTy(PrimTy::Bool)) + } else { + false + } +} + +fn fn_header_equals(expected: hir::FnHeader, actual: hir::FnHeader) -> bool { + expected.constness == actual.constness + && expected.unsafety == actual.unsafety + && expected.asyncness == actual.asyncness +} diff --git a/src/tools/clippy/clippy_lints/src/methods/needless_option_as_deref.rs b/src/tools/clippy/clippy_lints/src/methods/needless_option_as_deref.rs new file mode 100644 index 000000000..7030baf19 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/needless_option_as_deref.rs @@ -0,0 +1,37 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::path_res; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::usage::local_used_after_expr; +use rustc_errors::Applicability; +use rustc_hir::def::Res; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::NEEDLESS_OPTION_AS_DEREF; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, name: &str) { + let typeck = cx.typeck_results(); + let outer_ty = typeck.expr_ty(expr); + + if is_type_diagnostic_item(cx, outer_ty, sym::Option) && outer_ty == typeck.expr_ty(recv) { + if name == "as_deref_mut" && recv.is_syntactic_place_expr() { + let Res::Local(binding_id) = path_res(cx, recv) else { return }; + + if local_used_after_expr(cx, binding_id, recv) { + return; + } + } + + span_lint_and_sugg( + cx, + NEEDLESS_OPTION_AS_DEREF, + expr.span, + "derefed type is same as origin", + "try this", + snippet_opt(cx, recv.span).unwrap(), + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/needless_option_take.rs b/src/tools/clippy/clippy_lints/src/methods/needless_option_take.rs new file mode 100644 index 000000000..829c118d2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/needless_option_take.rs @@ -0,0 +1,41 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::match_def_path; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::NEEDLESS_OPTION_TAKE; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, recv: &'tcx Expr<'_>) { + // Checks if expression type is equal to sym::Option and if the expr is not a syntactic place + if !recv.is_syntactic_place_expr() && is_expr_option(cx, recv) && has_expr_as_ref_path(cx, recv) { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + NEEDLESS_OPTION_TAKE, + expr.span, + "called `Option::take()` on a temporary value", + "try", + format!( + "{}", + snippet_with_applicability(cx, recv.span, "..", &mut applicability) + ), + applicability, + ); + } +} + +fn is_expr_option(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let expr_type = cx.typeck_results().expr_ty(expr); + is_type_diagnostic_item(cx, expr_type, sym::Option) +} + +fn has_expr_as_ref_path(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + if let Some(ref_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) { + return match_def_path(cx, ref_id, &["core", "option", "Option", "as_ref"]); + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/methods/no_effect_replace.rs b/src/tools/clippy/clippy_lints/src/methods/no_effect_replace.rs new file mode 100644 index 000000000..a76341855 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/no_effect_replace.rs @@ -0,0 +1,47 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::SpanlessEq; +use if_chain::if_chain; +use rustc_ast::LitKind; +use rustc_hir::ExprKind; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::NO_EFFECT_REPLACE; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx rustc_hir::Expr<'_>, + arg1: &'tcx rustc_hir::Expr<'_>, + arg2: &'tcx rustc_hir::Expr<'_>, +) { + let ty = cx.typeck_results().expr_ty(expr).peel_refs(); + if !(ty.is_str() || is_type_diagnostic_item(cx, ty, sym::String)) { + return; + } + + if_chain! { + if let ExprKind::Lit(spanned) = &arg1.kind; + if let Some(param1) = lit_string_value(&spanned.node); + + if let ExprKind::Lit(spanned) = &arg2.kind; + if let LitKind::Str(param2, _) = &spanned.node; + if param1 == param2.as_str(); + + then { + span_lint(cx, NO_EFFECT_REPLACE, expr.span, "replacing text with itself"); + } + } + + if SpanlessEq::new(cx).eq_expr(arg1, arg2) { + span_lint(cx, NO_EFFECT_REPLACE, expr.span, "replacing text with itself"); + } +} + +fn lit_string_value(node: &LitKind) -> Option<String> { + match node { + LitKind::Char(value) => Some(value.to_string()), + LitKind::Str(value, _) => Some(value.as_str().to_owned()), + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/obfuscated_if_else.rs b/src/tools/clippy/clippy_lints/src/methods/obfuscated_if_else.rs new file mode 100644 index 000000000..4d7427b26 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/obfuscated_if_else.rs @@ -0,0 +1,42 @@ +// run-rustfix + +use super::OBFUSCATED_IF_ELSE; +use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet_with_applicability}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + then_recv: &'tcx hir::Expr<'_>, + then_arg: &'tcx hir::Expr<'_>, + unwrap_arg: &'tcx hir::Expr<'_>, +) { + // something.then_some(blah).unwrap_or(blah) + // ^^^^^^^^^-then_recv ^^^^-then_arg ^^^^- unwrap_arg + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- expr + + let recv_ty = cx.typeck_results().expr_ty(then_recv); + + if recv_ty.is_bool() { + let mut applicability = Applicability::MachineApplicable; + let sugg = format!( + "if {} {{ {} }} else {{ {} }}", + snippet_with_applicability(cx, then_recv.span, "..", &mut applicability), + snippet_with_applicability(cx, then_arg.span, "..", &mut applicability), + snippet_with_applicability(cx, unwrap_arg.span, "..", &mut applicability) + ); + + span_lint_and_sugg( + cx, + OBFUSCATED_IF_ELSE, + expr.span, + "use of `.then_some(..).unwrap_or(..)` can be written \ + more clearly with `if .. else ..`", + "try", + sugg, + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/ok_expect.rs b/src/tools/clippy/clippy_lints/src/methods/ok_expect.rs new file mode 100644 index 000000000..d64a9f320 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/ok_expect.rs @@ -0,0 +1,46 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::sym; + +use super::OK_EXPECT; + +/// lint use of `ok().expect()` for `Result`s +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>) { + if_chain! { + // lint if the caller of `ok()` is a `Result` + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result); + let result_type = cx.typeck_results().expr_ty(recv); + if let Some(error_type) = get_error_type(cx, result_type); + if has_debug_impl(error_type, cx); + + then { + span_lint_and_help( + cx, + OK_EXPECT, + expr.span, + "called `ok().expect()` on a `Result` value", + None, + "you can call `expect()` directly on the `Result`", + ); + } + } +} + +/// Given a `Result<T, E>` type, return its error type (`E`). +fn get_error_type<'a>(cx: &LateContext<'_>, ty: Ty<'a>) -> Option<Ty<'a>> { + match ty.kind() { + ty::Adt(_, substs) if is_type_diagnostic_item(cx, ty, sym::Result) => substs.types().nth(1), + _ => None, + } +} + +/// This checks whether a given type is known to implement Debug. +fn has_debug_impl<'tcx>(ty: Ty<'tcx>, cx: &LateContext<'tcx>) -> bool { + cx.tcx + .get_diagnostic_item(sym::Debug) + .map_or(false, |debug| implements_trait(cx, ty, debug, &[])) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/option_as_ref_deref.rs b/src/tools/clippy/clippy_lints/src/methods/option_as_ref_deref.rs new file mode 100644 index 000000000..20cad0f18 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/option_as_ref_deref.rs @@ -0,0 +1,120 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{match_def_path, meets_msrv, msrvs, path_to_local_id, paths, peel_blocks}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_semver::RustcVersion; +use rustc_span::sym; + +use super::OPTION_AS_REF_DEREF; + +/// lint use of `_.as_ref().map(Deref::deref)` for `Option`s +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &hir::Expr<'_>, + as_ref_recv: &hir::Expr<'_>, + map_arg: &hir::Expr<'_>, + is_mut: bool, + msrv: Option<RustcVersion>, +) { + if !meets_msrv(msrv, msrvs::OPTION_AS_DEREF) { + return; + } + + let same_mutability = |m| (is_mut && m == &hir::Mutability::Mut) || (!is_mut && m == &hir::Mutability::Not); + + let option_ty = cx.typeck_results().expr_ty(as_ref_recv); + if !is_type_diagnostic_item(cx, option_ty, sym::Option) { + return; + } + + let deref_aliases: [&[&str]; 9] = [ + &paths::DEREF_TRAIT_METHOD, + &paths::DEREF_MUT_TRAIT_METHOD, + &paths::CSTRING_AS_C_STR, + &paths::OS_STRING_AS_OS_STR, + &paths::PATH_BUF_AS_PATH, + &paths::STRING_AS_STR, + &paths::STRING_AS_MUT_STR, + &paths::VEC_AS_SLICE, + &paths::VEC_AS_MUT_SLICE, + ]; + + let is_deref = match map_arg.kind { + hir::ExprKind::Path(ref expr_qpath) => cx + .qpath_res(expr_qpath, map_arg.hir_id) + .opt_def_id() + .map_or(false, |fun_def_id| { + deref_aliases.iter().any(|path| match_def_path(cx, fun_def_id, path)) + }), + hir::ExprKind::Closure(&hir::Closure { body, .. }) => { + let closure_body = cx.tcx.hir().body(body); + let closure_expr = peel_blocks(&closure_body.value); + + match &closure_expr.kind { + hir::ExprKind::MethodCall(_, args, _) => { + if_chain! { + if args.len() == 1; + if path_to_local_id(&args[0], closure_body.params[0].pat.hir_id); + let adj = cx + .typeck_results() + .expr_adjustments(&args[0]) + .iter() + .map(|x| &x.kind) + .collect::<Box<[_]>>(); + if let [ty::adjustment::Adjust::Deref(None), ty::adjustment::Adjust::Borrow(_)] = *adj; + then { + let method_did = cx.typeck_results().type_dependent_def_id(closure_expr.hir_id).unwrap(); + deref_aliases.iter().any(|path| match_def_path(cx, method_did, path)) + } else { + false + } + } + }, + hir::ExprKind::AddrOf(hir::BorrowKind::Ref, m, inner) if same_mutability(m) => { + if_chain! { + if let hir::ExprKind::Unary(hir::UnOp::Deref, inner1) = inner.kind; + if let hir::ExprKind::Unary(hir::UnOp::Deref, inner2) = inner1.kind; + then { + path_to_local_id(inner2, closure_body.params[0].pat.hir_id) + } else { + false + } + } + }, + _ => false, + } + }, + _ => false, + }; + + if is_deref { + let current_method = if is_mut { + format!(".as_mut().map({})", snippet(cx, map_arg.span, "..")) + } else { + format!(".as_ref().map({})", snippet(cx, map_arg.span, "..")) + }; + let method_hint = if is_mut { "as_deref_mut" } else { "as_deref" }; + let hint = format!("{}.{}()", snippet(cx, as_ref_recv.span, ".."), method_hint); + let suggestion = format!("try using {} instead", method_hint); + + let msg = format!( + "called `{0}` on an Option value. This can be done more directly \ + by calling `{1}` instead", + current_method, hint + ); + span_lint_and_sugg( + cx, + OPTION_AS_REF_DEREF, + expr.span, + &msg, + &suggestion, + hint, + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/option_map_or_none.rs b/src/tools/clippy/clippy_lints/src/methods/option_map_or_none.rs new file mode 100644 index 000000000..5a39b82b0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/option_map_or_none.rs @@ -0,0 +1,122 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{is_lang_ctor, path_def_id}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::LangItem::{OptionNone, OptionSome}; +use rustc_lint::LateContext; +use rustc_middle::ty::DefIdTree; +use rustc_span::symbol::sym; + +use super::OPTION_MAP_OR_NONE; +use super::RESULT_MAP_OR_INTO_OPTION; + +// The expression inside a closure may or may not have surrounding braces +// which causes problems when generating a suggestion. +fn reduce_unit_expression<'a>(expr: &'a hir::Expr<'_>) -> Option<(&'a hir::Expr<'a>, &'a [hir::Expr<'a>])> { + match expr.kind { + hir::ExprKind::Call(func, arg_char) => Some((func, arg_char)), + hir::ExprKind::Block(block, _) => { + match (block.stmts, block.expr) { + (&[], Some(inner_expr)) => { + // If block only contains an expression, + // reduce `|x| { x + 1 }` to `|x| x + 1` + reduce_unit_expression(inner_expr) + }, + _ => None, + } + }, + _ => None, + } +} + +/// lint use of `_.map_or(None, _)` for `Option`s and `Result`s +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + recv: &'tcx hir::Expr<'_>, + def_arg: &'tcx hir::Expr<'_>, + map_arg: &'tcx hir::Expr<'_>, +) { + let is_option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option); + let is_result = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result); + + // There are two variants of this `map_or` lint: + // (1) using `map_or` as an adapter from `Result<T,E>` to `Option<T>` + // (2) using `map_or` as a combinator instead of `and_then` + // + // (For this lint) we don't care if any other type calls `map_or` + if !is_option && !is_result { + return; + } + + let default_arg_is_none = if let hir::ExprKind::Path(ref qpath) = def_arg.kind { + is_lang_ctor(cx, qpath, OptionNone) + } else { + return; + }; + + if !default_arg_is_none { + // nothing to lint! + return; + } + + let f_arg_is_some = if let hir::ExprKind::Path(ref qpath) = map_arg.kind { + is_lang_ctor(cx, qpath, OptionSome) + } else { + false + }; + + if is_option { + let self_snippet = snippet(cx, recv.span, ".."); + if_chain! { + if let hir::ExprKind::Closure(&hir::Closure { body, fn_decl_span, .. }) = map_arg.kind; + let arg_snippet = snippet(cx, fn_decl_span, ".."); + let body = cx.tcx.hir().body(body); + if let Some((func, [arg_char])) = reduce_unit_expression(&body.value); + if let Some(id) = path_def_id(cx, func).map(|ctor_id| cx.tcx.parent(ctor_id)); + if Some(id) == cx.tcx.lang_items().option_some_variant(); + then { + let func_snippet = snippet(cx, arg_char.span, ".."); + let msg = "called `map_or(None, ..)` on an `Option` value. This can be done more directly by calling \ + `map(..)` instead"; + return span_lint_and_sugg( + cx, + OPTION_MAP_OR_NONE, + expr.span, + msg, + "try using `map` instead", + format!("{0}.map({1} {2})", self_snippet, arg_snippet,func_snippet), + Applicability::MachineApplicable, + ); + } + } + + let func_snippet = snippet(cx, map_arg.span, ".."); + let msg = "called `map_or(None, ..)` on an `Option` value. This can be done more directly by calling \ + `and_then(..)` instead"; + span_lint_and_sugg( + cx, + OPTION_MAP_OR_NONE, + expr.span, + msg, + "try using `and_then` instead", + format!("{0}.and_then({1})", self_snippet, func_snippet), + Applicability::MachineApplicable, + ); + } else if f_arg_is_some { + let msg = "called `map_or(None, Some)` on a `Result` value. This can be done more directly by calling \ + `ok()` instead"; + let self_snippet = snippet(cx, recv.span, ".."); + span_lint_and_sugg( + cx, + RESULT_MAP_OR_INTO_OPTION, + expr.span, + msg, + "try using `ok` instead", + format!("{0}.ok()", self_snippet), + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/option_map_unwrap_or.rs b/src/tools/clippy/clippy_lints/src/methods/option_map_unwrap_or.rs new file mode 100644 index 000000000..6c641af59 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/option_map_unwrap_or.rs @@ -0,0 +1,139 @@ +use clippy_utils::diagnostics::span_lint_and_then; +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::intravisit::{walk_path, Visitor}; +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 super::MAP_UNWRAP_OR; + +/// lint use of `map().unwrap_or()` for `Option`s +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &rustc_hir::Expr<'_>, + recv: &rustc_hir::Expr<'_>, + map_arg: &'tcx rustc_hir::Expr<'_>, + unwrap_recv: &rustc_hir::Expr<'_>, + unwrap_arg: &'tcx rustc_hir::Expr<'_>, + map_span: Span, +) { + // 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 + + let mut unwrap_visitor = UnwrapVisitor { + cx, + identifiers: FxHashSet::default(), + }; + unwrap_visitor.visit_expr(unwrap_arg); + + let mut map_expr_visitor = MapExprVisitor { + cx, + identifiers: unwrap_visitor.identifiers, + found_identifier: false, + }; + map_expr_visitor.visit_expr(map_arg); + + if map_expr_visitor.found_identifier { + return; + } + } + + if unwrap_arg.span.ctxt() != map_span.ctxt() { + return; + } + + 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 unwrap_snippet_none = unwrap_snippet == "None"; + let suggest = if unwrap_snippet_none { + "and_then(<f>)" + } else { + "map_or(<a>, <f>)" + }; + let msg = &format!( + "called `map(<f>).unwrap_or({})` on an `Option` value. \ + This can be done more directly by calling `{}` instead", + arg, suggest + ); + + span_lint_and_then(cx, MAP_UNWRAP_OR, expr.span, msg, |diag| { + let map_arg_span = map_arg.span; + + let mut suggestion = vec![ + ( + map_span, + String::from(if unwrap_snippet_none { "and_then" } else { "map_or" }), + ), + (expr.span.with_lo(unwrap_recv.span.hi()), String::from("")), + ]; + + if !unwrap_snippet_none { + suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{}, ", unwrap_snippet))); + } + + diag.multipart_suggestion(&format!("use `{}` instead", suggest), suggestion, applicability); + }); + } +} + +struct UnwrapVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + identifiers: FxHashSet<Symbol>, +} + +impl<'a, 'tcx> Visitor<'tcx> for UnwrapVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::All; + + fn visit_path(&mut self, path: &'tcx Path<'_>, _id: HirId) { + self.identifiers.insert(ident(path)); + walk_path(self, path); + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +struct MapExprVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + identifiers: FxHashSet<Symbol>, + found_identifier: bool, +} + +impl<'a, 'tcx> Visitor<'tcx> for MapExprVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::All; + + fn visit_path(&mut self, path: &'tcx Path<'_>, _id: HirId) { + if self.identifiers.contains(&ident(path)) { + self.found_identifier = true; + return; + } + 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/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs b/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs new file mode 100644 index 000000000..6af134019 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs @@ -0,0 +1,175 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::eager_or_lazy::switch_to_lazy_eval; +use clippy_utils::source::{snippet, snippet_with_macro_callsite}; +use clippy_utils::ty::{implements_trait, match_type}; +use clippy_utils::{contains_return, is_trait_item, last_path_segment, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::source_map::Span; +use rustc_span::symbol::{kw, sym}; +use std::borrow::Cow; + +use super::OR_FUN_CALL; + +/// Checks for the `OR_FUN_CALL` lint. +#[allow(clippy::too_many_lines)] +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &hir::Expr<'_>, + method_span: Span, + name: &str, + args: &'tcx [hir::Expr<'_>], +) { + /// Checks for `unwrap_or(T::new())` or `unwrap_or(T::default())`. + #[allow(clippy::too_many_arguments)] + fn check_unwrap_or_default( + cx: &LateContext<'_>, + name: &str, + fun: &hir::Expr<'_>, + arg: &hir::Expr<'_>, + or_has_args: bool, + span: Span, + method_span: Span, + ) -> bool { + let is_default_default = || is_trait_item(cx, fun, sym::Default); + + let implements_default = |arg, default_trait_id| { + let arg_ty = cx.typeck_results().expr_ty(arg); + implements_trait(cx, arg_ty, default_trait_id, &[]) + }; + + if_chain! { + if !or_has_args; + if name == "unwrap_or"; + if let hir::ExprKind::Path(ref qpath) = fun.kind; + if let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default); + let path = last_path_segment(qpath).ident.name; + // needs to target Default::default in particular or be *::new and have a Default impl + // available + if (matches!(path, kw::Default) && is_default_default()) + || (matches!(path, sym::new) && implements_default(arg, default_trait_id)); + + then { + span_lint_and_sugg( + cx, + OR_FUN_CALL, + method_span.with_hi(span.hi()), + &format!("use of `{}` followed by a call to `{}`", name, path), + "try this", + "unwrap_or_default()".to_string(), + Applicability::MachineApplicable, + ); + + true + } else { + false + } + } + } + + /// Checks for `*or(foo())`. + #[allow(clippy::too_many_arguments)] + fn check_general_case<'tcx>( + cx: &LateContext<'tcx>, + name: &str, + method_span: Span, + self_expr: &hir::Expr<'_>, + arg: &'tcx hir::Expr<'_>, + span: Span, + // None if lambda is required + fun_span: Option<Span>, + ) { + // (path, fn_has_argument, methods, suffix) + static KNOW_TYPES: [(&[&str], bool, &[&str], &str); 4] = [ + (&paths::BTREEMAP_ENTRY, false, &["or_insert"], "with"), + (&paths::HASHMAP_ENTRY, false, &["or_insert"], "with"), + (&paths::OPTION, false, &["map_or", "ok_or", "or", "unwrap_or"], "else"), + (&paths::RESULT, true, &["or", "unwrap_or"], "else"), + ]; + + if_chain! { + if KNOW_TYPES.iter().any(|k| k.2.contains(&name)); + + if switch_to_lazy_eval(cx, arg); + if !contains_return(arg); + + let self_ty = cx.typeck_results().expr_ty(self_expr); + + if let Some(&(_, fn_has_arguments, poss, suffix)) = + KNOW_TYPES.iter().find(|&&i| match_type(cx, self_ty, i.0)); + + if poss.contains(&name); + + then { + let macro_expanded_snipped; + let sugg: Cow<'_, str> = { + let (snippet_span, use_lambda) = match (fn_has_arguments, fun_span) { + (false, Some(fun_span)) => (fun_span, false), + _ => (arg.span, true), + }; + let snippet = { + let not_macro_argument_snippet = snippet_with_macro_callsite(cx, snippet_span, ".."); + if not_macro_argument_snippet == "vec![]" { + macro_expanded_snipped = snippet(cx, snippet_span, ".."); + match macro_expanded_snipped.strip_prefix("$crate::vec::") { + Some(stripped) => Cow::from(stripped), + None => macro_expanded_snipped + } + } + else { + not_macro_argument_snippet + } + }; + + if use_lambda { + let l_arg = if fn_has_arguments { "_" } else { "" }; + format!("|{}| {}", l_arg, snippet).into() + } else { + snippet + } + }; + let span_replace_word = method_span.with_hi(span.hi()); + span_lint_and_sugg( + cx, + OR_FUN_CALL, + span_replace_word, + &format!("use of `{}` followed by a function call", name), + "try this", + format!("{}_{}({})", name, suffix, sugg), + Applicability::HasPlaceholders, + ); + } + } + } + + if let [self_arg, arg] = args { + let inner_arg = if let hir::ExprKind::Block( + hir::Block { + stmts: [], + expr: Some(expr), + .. + }, + _, + ) = arg.kind + { + expr + } else { + arg + }; + match inner_arg.kind { + hir::ExprKind::Call(fun, or_args) => { + let or_has_args = !or_args.is_empty(); + if !check_unwrap_or_default(cx, name, fun, arg, or_has_args, expr.span, method_span) { + let fun_span = if or_has_args { None } else { Some(fun.span) }; + check_general_case(cx, name, method_span, self_arg, arg, expr.span, fun_span); + } + }, + hir::ExprKind::Index(..) | hir::ExprKind::MethodCall(..) => { + check_general_case(cx, name, method_span, self_arg, arg, expr.span, None); + }, + _ => (), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/or_then_unwrap.rs b/src/tools/clippy/clippy_lints/src/methods/or_then_unwrap.rs new file mode 100644 index 000000000..be5768c35 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/or_then_unwrap.rs @@ -0,0 +1,68 @@ +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{diagnostics::span_lint_and_sugg, is_lang_ctor}; +use rustc_errors::Applicability; +use rustc_hir::{lang_items::LangItem, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_span::{sym, Span}; + +use super::OR_THEN_UNWRAP; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + unwrap_expr: &Expr<'_>, + recv: &'tcx Expr<'tcx>, + or_arg: &'tcx Expr<'_>, + or_span: Span, +) { + let ty = cx.typeck_results().expr_ty(recv); // get type of x (we later check if it's Option or Result) + let title; + let or_arg_content: Span; + + if is_type_diagnostic_item(cx, ty, sym::Option) { + title = "found `.or(Some(…)).unwrap()`"; + if let Some(content) = get_content_if_ctor_matches(cx, or_arg, LangItem::OptionSome) { + or_arg_content = content; + } else { + return; + } + } else if is_type_diagnostic_item(cx, ty, sym::Result) { + title = "found `.or(Ok(…)).unwrap()`"; + if let Some(content) = get_content_if_ctor_matches(cx, or_arg, LangItem::ResultOk) { + or_arg_content = content; + } else { + return; + } + } else { + // Someone has implemented a struct with .or(...).unwrap() chaining, + // but it's not an Option or a Result, so bail + return; + } + + let mut applicability = Applicability::MachineApplicable; + let suggestion = format!( + "unwrap_or({})", + snippet_with_applicability(cx, or_arg_content, "..", &mut applicability) + ); + + span_lint_and_sugg( + cx, + OR_THEN_UNWRAP, + unwrap_expr.span.with_lo(or_span.lo()), + title, + "try this", + suggestion, + applicability, + ); +} + +fn get_content_if_ctor_matches(cx: &LateContext<'_>, expr: &Expr<'_>, item: LangItem) -> Option<Span> { + if let ExprKind::Call(some_expr, [arg]) = expr.kind + && let ExprKind::Path(qpath) = &some_expr.kind + && is_lang_ctor(cx, qpath, item) + { + Some(arg.span) + } else { + None + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/search_is_some.rs b/src/tools/clippy/clippy_lints/src/methods/search_is_some.rs new file mode 100644 index 000000000..7572ba3fe --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/search_is_some.rs @@ -0,0 +1,156 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; +use clippy_utils::source::{snippet, snippet_with_applicability}; +use clippy_utils::sugg::deref_closure_args; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{is_trait_method, strip_pat_refs}; +use if_chain::if_chain; +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; +use rustc_span::symbol::sym; + +use super::SEARCH_IS_SOME; + +/// lint searching an Iterator followed by `is_some()` +/// or calling `find()` on a string followed by `is_some()` or `is_none()` +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] +pub(super) fn check<'tcx>( + cx: &LateContext<'_>, + expr: &'tcx hir::Expr<'_>, + search_method: &str, + is_some: bool, + search_recv: &hir::Expr<'_>, + search_arg: &'tcx hir::Expr<'_>, + is_some_recv: &hir::Expr<'_>, + method_span: Span, +) { + let option_check_method = if is_some { "is_some" } else { "is_none" }; + // lint if caller of search is an Iterator + if is_trait_method(cx, is_some_recv, sym::Iterator) { + let msg = format!( + "called `{}()` after searching an `Iterator` with `{}`", + option_check_method, search_method + ); + let search_snippet = snippet(cx, search_arg.span, ".."); + if search_snippet.lines().count() <= 1 { + // suggest `any(|x| ..)` instead of `any(|&x| ..)` for `find(|&x| ..).is_some()` + // suggest `any(|..| *..)` instead of `any(|..| **..)` for `find(|..| **..).is_some()` + let mut applicability = Applicability::MachineApplicable; + let any_search_snippet = if_chain! { + if search_method == "find"; + if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = search_arg.kind; + let closure_body = cx.tcx.hir().body(body); + if let Some(closure_arg) = closure_body.params.get(0); + then { + if let hir::PatKind::Ref(..) = closure_arg.pat.kind { + Some(search_snippet.replacen('&', "", 1)) + } else if let PatKind::Binding(..) = strip_pat_refs(closure_arg.pat).kind { + // `find()` provides a reference to the item, but `any` does not, + // so we should fix item usages for suggestion + if let Some(closure_sugg) = deref_closure_args(cx, search_arg) { + applicability = closure_sugg.applicability; + Some(closure_sugg.suggestion) + } else { + Some(search_snippet.to_string()) + } + } else { + None + } + } else { + None + } + }; + // add note if not multi-line + if is_some { + span_lint_and_sugg( + cx, + SEARCH_IS_SOME, + method_span.with_hi(expr.span.hi()), + &msg, + "use `any()` instead", + format!( + "any({})", + any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str) + ), + applicability, + ); + } else { + let iter = snippet(cx, search_recv.span, ".."); + span_lint_and_sugg( + cx, + SEARCH_IS_SOME, + expr.span, + &msg, + "use `!_.any()` instead", + format!( + "!{}.any({})", + iter, + any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str) + ), + applicability, + ); + } + } else { + let hint = format!( + "this is more succinctly expressed by calling `any()`{}", + if option_check_method == "is_none" { + " with negation" + } else { + "" + } + ); + span_lint_and_help(cx, SEARCH_IS_SOME, expr.span, &msg, None, &hint); + } + } + // lint if `find()` is called by `String` or `&str` + else if search_method == "find" { + let is_string_or_str_slice = |e| { + let self_ty = cx.typeck_results().expr_ty(e).peel_refs(); + if is_type_diagnostic_item(cx, self_ty, sym::String) { + true + } else { + *self_ty.kind() == ty::Str + } + }; + if_chain! { + if is_string_or_str_slice(search_recv); + if is_string_or_str_slice(search_arg); + then { + let msg = format!("called `{}()` after calling `find()` on a string", option_check_method); + match option_check_method { + "is_some" => { + let mut applicability = Applicability::MachineApplicable; + let find_arg = snippet_with_applicability(cx, search_arg.span, "..", &mut applicability); + span_lint_and_sugg( + cx, + SEARCH_IS_SOME, + method_span.with_hi(expr.span.hi()), + &msg, + "use `contains()` instead", + format!("contains({})", find_arg), + applicability, + ); + }, + "is_none" => { + let string = snippet(cx, search_recv.span, ".."); + let mut applicability = Applicability::MachineApplicable; + let find_arg = snippet_with_applicability(cx, search_arg.span, "..", &mut applicability); + span_lint_and_sugg( + cx, + SEARCH_IS_SOME, + expr.span, + &msg, + "use `!_.contains()` instead", + format!("!{}.contains({})", string, find_arg), + applicability, + ); + }, + _ => (), + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/single_char_add_str.rs b/src/tools/clippy/clippy_lints/src/methods/single_char_add_str.rs new file mode 100644 index 000000000..9a5fabcf7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/single_char_add_str.rs @@ -0,0 +1,14 @@ +use crate::methods::{single_char_insert_string, single_char_push_string}; +use clippy_utils::{match_def_path, paths}; +use rustc_hir as hir; +use rustc_lint::LateContext; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, args: &[hir::Expr<'_>]) { + if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) { + if match_def_path(cx, fn_def_id, &paths::PUSH_STR) { + single_char_push_string::check(cx, expr, args); + } else if match_def_path(cx, fn_def_id, &paths::INSERT_STR) { + single_char_insert_string::check(cx, expr, args); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/single_char_insert_string.rs b/src/tools/clippy/clippy_lints/src/methods/single_char_insert_string.rs new file mode 100644 index 000000000..6cdc954c0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/single_char_insert_string.rs @@ -0,0 +1,28 @@ +use super::utils::get_hint_if_single_char_arg; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; + +use super::SINGLE_CHAR_ADD_STR; + +/// lint for length-1 `str`s as argument for `insert_str` +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, args: &[hir::Expr<'_>]) { + let mut applicability = Applicability::MachineApplicable; + if let Some(extension_string) = get_hint_if_single_char_arg(cx, &args[2], &mut applicability) { + let base_string_snippet = + snippet_with_applicability(cx, args[0].span.source_callsite(), "_", &mut applicability); + let pos_arg = snippet_with_applicability(cx, args[1].span, "..", &mut applicability); + let sugg = format!("{}.insert({}, {})", base_string_snippet, pos_arg, extension_string); + span_lint_and_sugg( + cx, + SINGLE_CHAR_ADD_STR, + expr.span, + "calling `insert_str()` using a single-character string literal", + "consider using `insert` with a character literal", + sugg, + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/single_char_pattern.rs b/src/tools/clippy/clippy_lints/src/methods/single_char_pattern.rs new file mode 100644 index 000000000..bf9006c69 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/single_char_pattern.rs @@ -0,0 +1,62 @@ +use super::utils::get_hint_if_single_char_arg; +use clippy_utils::diagnostics::span_lint_and_sugg; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::symbol::Symbol; + +use super::SINGLE_CHAR_PATTERN; + +const PATTERN_METHODS: [(&str, usize); 24] = [ + ("contains", 1), + ("starts_with", 1), + ("ends_with", 1), + ("find", 1), + ("rfind", 1), + ("split", 1), + ("split_inclusive", 1), + ("rsplit", 1), + ("split_terminator", 1), + ("rsplit_terminator", 1), + ("splitn", 2), + ("rsplitn", 2), + ("split_once", 1), + ("rsplit_once", 1), + ("matches", 1), + ("rmatches", 1), + ("match_indices", 1), + ("rmatch_indices", 1), + ("strip_prefix", 1), + ("strip_suffix", 1), + ("trim_start_matches", 1), + ("trim_end_matches", 1), + ("replace", 1), + ("replacen", 1), +]; + +/// lint for length-1 `str`s for methods in `PATTERN_METHODS` +pub(super) fn check(cx: &LateContext<'_>, _expr: &hir::Expr<'_>, method_name: Symbol, args: &[hir::Expr<'_>]) { + for &(method, pos) in &PATTERN_METHODS { + if_chain! { + if let ty::Ref(_, ty, _) = cx.typeck_results().expr_ty_adjusted(&args[0]).kind(); + if *ty.kind() == ty::Str; + if method_name.as_str() == method && args.len() > pos; + let arg = &args[pos]; + let mut applicability = Applicability::MachineApplicable; + if let Some(hint) = get_hint_if_single_char_arg(cx, arg, &mut applicability); + then { + span_lint_and_sugg( + cx, + SINGLE_CHAR_PATTERN, + arg.span, + "single-character string constant used as pattern", + "try using a `char` instead", + hint, + applicability, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/single_char_push_string.rs b/src/tools/clippy/clippy_lints/src/methods/single_char_push_string.rs new file mode 100644 index 000000000..0237d39cb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/single_char_push_string.rs @@ -0,0 +1,27 @@ +use super::utils::get_hint_if_single_char_arg; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; + +use super::SINGLE_CHAR_ADD_STR; + +/// lint for length-1 `str`s as argument for `push_str` +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, args: &[hir::Expr<'_>]) { + let mut applicability = Applicability::MachineApplicable; + if let Some(extension_string) = get_hint_if_single_char_arg(cx, &args[1], &mut applicability) { + let base_string_snippet = + snippet_with_applicability(cx, args[0].span.source_callsite(), "..", &mut applicability); + let sugg = format!("{}.push({})", base_string_snippet, extension_string); + span_lint_and_sugg( + cx, + SINGLE_CHAR_ADD_STR, + expr.span, + "calling `push_str()` using a single-character string literal", + "consider using `push` with a character literal", + sugg, + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/skip_while_next.rs b/src/tools/clippy/clippy_lints/src/methods/skip_while_next.rs new file mode 100644 index 000000000..9f0b6c34e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/skip_while_next.rs @@ -0,0 +1,22 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_trait_method; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::SKIP_WHILE_NEXT; + +/// lint use of `skip_while().next()` for `Iterators` +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + // lint if caller of `.skip_while().next()` is an Iterator + if is_trait_method(cx, expr, sym::Iterator) { + span_lint_and_help( + cx, + SKIP_WHILE_NEXT, + expr.span, + "called `skip_while(<p>).next()` on an `Iterator`", + None, + "this is more succinctly expressed by calling `.find(!<p>)` instead", + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/str_splitn.rs b/src/tools/clippy/clippy_lints/src/methods/str_splitn.rs new file mode 100644 index 000000000..4ac738272 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/str_splitn.rs @@ -0,0 +1,390 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::snippet_with_context; +use clippy_utils::usage::local_used_after_expr; +use clippy_utils::visitors::expr_visitor; +use clippy_utils::{is_diag_item_method, match_def_path, meets_msrv, msrvs, path_to_local_id, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::intravisit::Visitor; +use rustc_hir::{ + BindingAnnotation, Expr, ExprKind, HirId, LangItem, Local, MatchSource, Node, Pat, PatKind, QPath, Stmt, StmtKind, +}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_semver::RustcVersion; +use rustc_span::{sym, Span, Symbol, SyntaxContext}; + +use super::{MANUAL_SPLIT_ONCE, NEEDLESS_SPLITN}; + +pub(super) fn check( + cx: &LateContext<'_>, + method_name: &str, + expr: &Expr<'_>, + self_arg: &Expr<'_>, + pat_arg: &Expr<'_>, + count: u128, + msrv: Option<RustcVersion>, +) { + if count < 2 || !cx.typeck_results().expr_ty_adjusted(self_arg).peel_refs().is_str() { + return; + } + + let needless = |usage_kind| match usage_kind { + IterUsageKind::Nth(n) => count > n + 1, + IterUsageKind::NextTuple => count > 2, + }; + let manual = count == 2 && meets_msrv(msrv, msrvs::STR_SPLIT_ONCE); + + match parse_iter_usage(cx, expr.span.ctxt(), cx.tcx.hir().parent_iter(expr.hir_id)) { + Some(usage) if needless(usage.kind) => lint_needless(cx, method_name, expr, self_arg, pat_arg), + Some(usage) if manual => check_manual_split_once(cx, method_name, expr, self_arg, pat_arg, &usage), + None if manual => { + check_manual_split_once_indirect(cx, method_name, expr, self_arg, pat_arg); + }, + _ => {}, + } +} + +fn lint_needless(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>) { + let mut app = Applicability::MachineApplicable; + let r = if method_name == "splitn" { "" } else { "r" }; + + span_lint_and_sugg( + cx, + NEEDLESS_SPLITN, + expr.span, + &format!("unnecessary use of `{r}splitn`"), + "try this", + format!( + "{}.{r}split({})", + snippet_with_context(cx, self_arg.span, expr.span.ctxt(), "..", &mut app).0, + snippet_with_context(cx, pat_arg.span, expr.span.ctxt(), "..", &mut app).0, + ), + app, + ); +} + +fn check_manual_split_once( + cx: &LateContext<'_>, + method_name: &str, + expr: &Expr<'_>, + self_arg: &Expr<'_>, + pat_arg: &Expr<'_>, + usage: &IterUsage, +) { + let ctxt = expr.span.ctxt(); + let (msg, reverse) = if method_name == "splitn" { + ("manual implementation of `split_once`", false) + } else { + ("manual implementation of `rsplit_once`", true) + }; + + let mut app = Applicability::MachineApplicable; + let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0; + let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0; + + let sugg = match usage.kind { + IterUsageKind::NextTuple => { + if reverse { + format!("{self_snip}.rsplit_once({pat_snip}).map(|(x, y)| (y, x))") + } else { + format!("{self_snip}.split_once({pat_snip})") + } + }, + IterUsageKind::Nth(1) => { + let (r, field) = if reverse { ("r", 0) } else { ("", 1) }; + + match usage.unwrap_kind { + Some(UnwrapKind::Unwrap) => { + format!("{self_snip}.{r}split_once({pat_snip}).unwrap().{field}") + }, + Some(UnwrapKind::QuestionMark) => { + format!("{self_snip}.{r}split_once({pat_snip})?.{field}") + }, + None => { + format!("{self_snip}.{r}split_once({pat_snip}).map(|x| x.{field})") + }, + } + }, + IterUsageKind::Nth(_) => return, + }; + + span_lint_and_sugg(cx, MANUAL_SPLIT_ONCE, usage.span, msg, "try this", sugg, app); +} + +/// checks for +/// +/// ``` +/// let mut iter = "a.b.c".splitn(2, '.'); +/// let a = iter.next(); +/// let b = iter.next(); +/// ``` +fn check_manual_split_once_indirect( + cx: &LateContext<'_>, + method_name: &str, + expr: &Expr<'_>, + self_arg: &Expr<'_>, + pat_arg: &Expr<'_>, +) -> Option<()> { + let ctxt = expr.span.ctxt(); + let mut parents = cx.tcx.hir().parent_iter(expr.hir_id); + if let (_, Node::Local(local)) = parents.next()? + && let PatKind::Binding(BindingAnnotation::Mutable, iter_binding_id, iter_ident, None) = local.pat.kind + && let (iter_stmt_id, Node::Stmt(_)) = parents.next()? + && let (_, Node::Block(enclosing_block)) = parents.next()? + + && let mut stmts = enclosing_block + .stmts + .iter() + .skip_while(|stmt| stmt.hir_id != iter_stmt_id) + .skip(1) + + && let first = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)? + && let second = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)? + && first.unwrap_kind == second.unwrap_kind + && first.name != second.name + && !local_used_after_expr(cx, iter_binding_id, second.init_expr) + { + let (r, lhs, rhs) = if method_name == "splitn" { + ("", first.name, second.name) + } else { + ("r", second.name, first.name) + }; + let msg = format!("manual implementation of `{r}split_once`"); + + let mut app = Applicability::MachineApplicable; + let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0; + let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0; + + span_lint_and_then(cx, MANUAL_SPLIT_ONCE, local.span, &msg, |diag| { + diag.span_label(first.span, "first usage here"); + diag.span_label(second.span, "second usage here"); + + let unwrap = match first.unwrap_kind { + UnwrapKind::Unwrap => ".unwrap()", + UnwrapKind::QuestionMark => "?", + }; + diag.span_suggestion_verbose( + local.span, + &format!("try `{r}split_once`"), + format!("let ({lhs}, {rhs}) = {self_snip}.{r}split_once({pat_snip}){unwrap};"), + app, + ); + + let remove_msg = format!("remove the `{iter_ident}` usages"); + diag.span_suggestion( + first.span, + &remove_msg, + "", + app, + ); + diag.span_suggestion( + second.span, + &remove_msg, + "", + app, + ); + }); + } + + Some(()) +} + +#[derive(Debug)] +struct IndirectUsage<'a> { + name: Symbol, + span: Span, + init_expr: &'a Expr<'a>, + unwrap_kind: UnwrapKind, +} + +/// returns `Some(IndirectUsage)` for e.g. +/// +/// ```ignore +/// let name = binding.next()?; +/// let name = binding.next().unwrap(); +/// ``` +fn indirect_usage<'tcx>( + cx: &LateContext<'tcx>, + stmt: &Stmt<'tcx>, + binding: HirId, + ctxt: SyntaxContext, +) -> Option<IndirectUsage<'tcx>> { + if let StmtKind::Local(Local { + pat: + Pat { + kind: PatKind::Binding(BindingAnnotation::Unannotated, _, ident, None), + .. + }, + init: Some(init_expr), + hir_id: local_hir_id, + .. + }) = stmt.kind + { + let mut path_to_binding = None; + expr_visitor(cx, |expr| { + if path_to_local_id(expr, binding) { + path_to_binding = Some(expr); + } + + path_to_binding.is_none() + }) + .visit_expr(init_expr); + + let mut parents = cx.tcx.hir().parent_iter(path_to_binding?.hir_id); + let iter_usage = parse_iter_usage(cx, ctxt, &mut parents)?; + + let (parent_id, _) = parents.find(|(_, node)| { + !matches!( + node, + Node::Expr(Expr { + kind: ExprKind::Match(.., MatchSource::TryDesugar), + .. + }) + ) + })?; + + if let IterUsage { + kind: IterUsageKind::Nth(0), + unwrap_kind: Some(unwrap_kind), + .. + } = iter_usage + { + if parent_id == *local_hir_id { + return Some(IndirectUsage { + name: ident.name, + span: stmt.span, + init_expr, + unwrap_kind, + }); + } + } + } + + None +} + +#[derive(Debug, Clone, Copy)] +enum IterUsageKind { + Nth(u128), + NextTuple, +} + +#[derive(Debug, PartialEq, Eq)] +enum UnwrapKind { + Unwrap, + QuestionMark, +} + +#[derive(Debug)] +struct IterUsage { + kind: IterUsageKind, + unwrap_kind: Option<UnwrapKind>, + span: Span, +} + +#[allow(clippy::too_many_lines)] +fn parse_iter_usage<'tcx>( + cx: &LateContext<'tcx>, + ctxt: SyntaxContext, + mut iter: impl Iterator<Item = (HirId, Node<'tcx>)>, +) -> Option<IterUsage> { + let (kind, span) = match iter.next() { + Some((_, Node::Expr(e))) if e.span.ctxt() == ctxt => { + let (name, args) = if let ExprKind::MethodCall(name, [_, args @ ..], _) = e.kind { + (name, args) + } else { + return None; + }; + let did = cx.typeck_results().type_dependent_def_id(e.hir_id)?; + let iter_id = cx.tcx.get_diagnostic_item(sym::Iterator)?; + + match (name.ident.as_str(), args) { + ("next", []) if cx.tcx.trait_of_item(did) == Some(iter_id) => (IterUsageKind::Nth(0), e.span), + ("next_tuple", []) => { + return if_chain! { + if match_def_path(cx, did, &paths::ITERTOOLS_NEXT_TUPLE); + if let ty::Adt(adt_def, subs) = cx.typeck_results().expr_ty(e).kind(); + if cx.tcx.is_diagnostic_item(sym::Option, adt_def.did()); + if let ty::Tuple(subs) = subs.type_at(0).kind(); + if subs.len() == 2; + then { + Some(IterUsage { + kind: IterUsageKind::NextTuple, + span: e.span, + unwrap_kind: None + }) + } else { + None + } + }; + }, + ("nth" | "skip", [idx_expr]) if cx.tcx.trait_of_item(did) == Some(iter_id) => { + if let Some((Constant::Int(idx), _)) = constant(cx, cx.typeck_results(), idx_expr) { + let span = if name.ident.as_str() == "nth" { + e.span + } else { + if_chain! { + if let Some((_, Node::Expr(next_expr))) = iter.next(); + if let ExprKind::MethodCall(next_name, [_], _) = next_expr.kind; + if next_name.ident.name == sym::next; + if next_expr.span.ctxt() == ctxt; + if let Some(next_id) = cx.typeck_results().type_dependent_def_id(next_expr.hir_id); + if cx.tcx.trait_of_item(next_id) == Some(iter_id); + then { + next_expr.span + } else { + return None; + } + } + }; + (IterUsageKind::Nth(idx), span) + } else { + return None; + } + }, + _ => return None, + } + }, + _ => return None, + }; + + let (unwrap_kind, span) = if let Some((_, Node::Expr(e))) = iter.next() { + match e.kind { + ExprKind::Call( + Expr { + kind: ExprKind::Path(QPath::LangItem(LangItem::TryTraitBranch, ..)), + .. + }, + _, + ) => { + let parent_span = e.span.parent_callsite().unwrap(); + if parent_span.ctxt() == ctxt { + (Some(UnwrapKind::QuestionMark), parent_span) + } else { + (None, span) + } + }, + _ if e.span.ctxt() != ctxt => (None, span), + ExprKind::MethodCall(name, [_], _) + if name.ident.name == sym::unwrap + && cx + .typeck_results() + .type_dependent_def_id(e.hir_id) + .map_or(false, |id| is_diag_item_method(cx, id, sym::Option)) => + { + (Some(UnwrapKind::Unwrap), e.span) + }, + _ => (None, span), + } + } else { + (None, span) + }; + + Some(IterUsage { + kind, + unwrap_kind, + span, + }) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/string_extend_chars.rs b/src/tools/clippy/clippy_lints/src/methods/string_extend_chars.rs new file mode 100644 index 000000000..d06658f2a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/string_extend_chars.rs @@ -0,0 +1,45 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::method_chain_args; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::symbol::sym; + +use super::STRING_EXTEND_CHARS; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>, arg: &hir::Expr<'_>) { + let obj_ty = cx.typeck_results().expr_ty(recv).peel_refs(); + if !is_type_diagnostic_item(cx, obj_ty, sym::String) { + return; + } + if let Some(arglists) = method_chain_args(arg, &["chars"]) { + let target = &arglists[0][0]; + let self_ty = cx.typeck_results().expr_ty(target).peel_refs(); + let ref_str = if *self_ty.kind() == ty::Str { + "" + } else if is_type_diagnostic_item(cx, self_ty, sym::String) { + "&" + } else { + return; + }; + + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + STRING_EXTEND_CHARS, + expr.span, + "calling `.extend(_.chars())`", + "try this", + format!( + "{}.push_str({}{})", + snippet_with_applicability(cx, recv.span, "..", &mut applicability), + ref_str, + snippet_with_applicability(cx, target.span, "..", &mut applicability) + ), + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/suspicious_map.rs b/src/tools/clippy/clippy_lints/src/methods/suspicious_map.rs new file mode 100644 index 000000000..9c3375bf3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/suspicious_map.rs @@ -0,0 +1,36 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::usage::mutated_variables; +use clippy_utils::{expr_or_init, is_trait_method}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::SUSPICIOUS_MAP; + +pub fn check<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>, count_recv: &hir::Expr<'_>, map_arg: &hir::Expr<'_>) { + if_chain! { + if is_trait_method(cx, count_recv, sym::Iterator); + let closure = expr_or_init(cx, map_arg); + if let Some(def_id) = cx.tcx.hir().opt_local_def_id(closure.hir_id); + if let Some(body_id) = cx.tcx.hir().maybe_body_owned_by(def_id); + let closure_body = cx.tcx.hir().body(body_id); + if !cx.typeck_results().expr_ty(&closure_body.value).is_unit(); + then { + if let Some(map_mutated_vars) = mutated_variables(&closure_body.value, cx) { + // A variable is used mutably inside of the closure. Suppress the lint. + if !map_mutated_vars.is_empty() { + return; + } + } + span_lint_and_help( + cx, + SUSPICIOUS_MAP, + expr.span, + "this call to `map()` won't have an effect on the call to `count()`", + None, + "make sure you did not confuse `map` with `filter`, `for_each` or `inspect`", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/suspicious_splitn.rs b/src/tools/clippy/clippy_lints/src/methods/suspicious_splitn.rs new file mode 100644 index 000000000..55567d862 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/suspicious_splitn.rs @@ -0,0 +1,48 @@ +use clippy_utils::diagnostics::span_lint_and_note; +use if_chain::if_chain; +use rustc_ast::LitKind; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_span::source_map::Spanned; + +use super::SUSPICIOUS_SPLITN; + +pub(super) fn check(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, count: u128) { + if_chain! { + if count <= 1; + if let Some(call_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if let Some(impl_id) = cx.tcx.impl_of_method(call_id); + if cx.tcx.impl_trait_ref(impl_id).is_none(); + let self_ty = cx.tcx.type_of(impl_id); + if self_ty.is_slice() || self_ty.is_str(); + then { + // Ignore empty slice and string literals when used with a literal count. + if matches!(self_arg.kind, ExprKind::Array([])) + || matches!(self_arg.kind, ExprKind::Lit(Spanned { node: LitKind::Str(s, _), .. }) if s.is_empty()) + { + return; + } + + let (msg, note_msg) = if count == 0 { + (format!("`{}` called with `0` splits", method_name), + "the resulting iterator will always return `None`") + } else { + (format!("`{}` called with `1` split", method_name), + if self_ty.is_slice() { + "the resulting iterator will always return the entire slice followed by `None`" + } else { + "the resulting iterator will always return the entire string followed by `None`" + }) + }; + + span_lint_and_note( + cx, + SUSPICIOUS_SPLITN, + expr.span, + &msg, + None, + note_msg, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/uninit_assumed_init.rs b/src/tools/clippy/clippy_lints/src/methods/uninit_assumed_init.rs new file mode 100644 index 000000000..77d21f1d3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/uninit_assumed_init.rs @@ -0,0 +1,26 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{is_expr_diagnostic_item, ty::is_uninit_value_valid_for_ty}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::UNINIT_ASSUMED_INIT; + +/// lint for `MaybeUninit::uninit().assume_init()` (we already have the latter) +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>) { + if_chain! { + if let hir::ExprKind::Call(callee, args) = recv.kind; + if args.is_empty(); + if is_expr_diagnostic_item(cx, callee, sym::maybe_uninit_uninit); + if !is_uninit_value_valid_for_ty(cx, cx.typeck_results().expr_ty_adjusted(expr)); + then { + span_lint( + cx, + UNINIT_ASSUMED_INIT, + expr.span, + "this call for this type may be undefined behavior" + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_filter_map.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_filter_map.rs new file mode 100644 index 000000000..bafa6fc58 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_filter_map.rs @@ -0,0 +1,132 @@ +use super::utils::clone_or_copy_needed; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::is_copy; +use clippy_utils::usage::mutated_variables; +use clippy_utils::{is_lang_ctor, is_trait_method, path_to_local_id}; +use rustc_hir as hir; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::LangItem::{OptionNone, OptionSome}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::sym; + +use super::UNNECESSARY_FILTER_MAP; +use super::UNNECESSARY_FIND_MAP; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, arg: &hir::Expr<'_>, name: &str) { + if !is_trait_method(cx, expr, sym::Iterator) { + return; + } + + if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = arg.kind { + let body = cx.tcx.hir().body(body); + let arg_id = body.params[0].pat.hir_id; + let mutates_arg = + mutated_variables(&body.value, cx).map_or(true, |used_mutably| used_mutably.contains(&arg_id)); + let (clone_or_copy_needed, _) = clone_or_copy_needed(cx, body.params[0].pat, &body.value); + + let (mut found_mapping, mut found_filtering) = check_expression(cx, arg_id, &body.value); + + let mut return_visitor = ReturnVisitor::new(cx, arg_id); + return_visitor.visit_expr(&body.value); + found_mapping |= return_visitor.found_mapping; + found_filtering |= return_visitor.found_filtering; + + let in_ty = cx.typeck_results().node_type(body.params[0].hir_id); + let sugg = if !found_filtering { + if name == "filter_map" { "map" } else { "map(..).next()" } + } else if !found_mapping && !mutates_arg && (!clone_or_copy_needed || is_copy(cx, in_ty)) { + match cx.typeck_results().expr_ty(&body.value).kind() { + ty::Adt(adt, subst) + if cx.tcx.is_diagnostic_item(sym::Option, adt.did()) && in_ty == subst.type_at(0) => + { + if name == "filter_map" { "filter" } else { "find" } + }, + _ => return, + } + } else { + return; + }; + span_lint( + cx, + if name == "filter_map" { + UNNECESSARY_FILTER_MAP + } else { + UNNECESSARY_FIND_MAP + }, + expr.span, + &format!("this `.{}` can be written more simply using `.{}`", name, sugg), + ); + } +} + +// returns (found_mapping, found_filtering) +fn check_expression<'tcx>(cx: &LateContext<'tcx>, arg_id: hir::HirId, expr: &'tcx hir::Expr<'_>) -> (bool, bool) { + match &expr.kind { + hir::ExprKind::Call(func, args) => { + if let hir::ExprKind::Path(ref path) = func.kind { + if is_lang_ctor(cx, path, OptionSome) { + if path_to_local_id(&args[0], arg_id) { + return (false, false); + } + return (true, false); + } + } + (true, true) + }, + hir::ExprKind::Block(block, _) => block + .expr + .as_ref() + .map_or((false, false), |expr| check_expression(cx, arg_id, expr)), + hir::ExprKind::Match(_, arms, _) => { + let mut found_mapping = false; + let mut found_filtering = false; + for arm in *arms { + let (m, f) = check_expression(cx, arg_id, arm.body); + found_mapping |= m; + found_filtering |= f; + } + (found_mapping, found_filtering) + }, + // There must be an else_arm or there will be a type error + hir::ExprKind::If(_, if_arm, Some(else_arm)) => { + let if_check = check_expression(cx, arg_id, if_arm); + let else_check = check_expression(cx, arg_id, else_arm); + (if_check.0 | else_check.0, if_check.1 | else_check.1) + }, + hir::ExprKind::Path(path) if is_lang_ctor(cx, path, OptionNone) => (false, true), + _ => (true, true), + } +} + +struct ReturnVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + arg_id: hir::HirId, + // Found a non-None return that isn't Some(input) + found_mapping: bool, + // Found a return that isn't Some + found_filtering: bool, +} + +impl<'a, 'tcx> ReturnVisitor<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>, arg_id: hir::HirId) -> ReturnVisitor<'a, 'tcx> { + ReturnVisitor { + cx, + arg_id, + found_mapping: false, + found_filtering: false, + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for ReturnVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) { + if let hir::ExprKind::Ret(Some(expr)) = &expr.kind { + let (found_mapping, found_filtering) = check_expression(self.cx, self.arg_id, expr); + self.found_mapping |= found_mapping; + self.found_filtering |= found_filtering; + } else { + walk_expr(self, expr); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_fold.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_fold.rs new file mode 100644 index 000000000..c3531d4d0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_fold.rs @@ -0,0 +1,95 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{is_trait_method, path_to_local_id, peel_blocks, strip_pat_refs}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::PatKind; +use rustc_lint::LateContext; +use rustc_span::{source_map::Span, sym}; + +use super::UNNECESSARY_FOLD; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + init: &hir::Expr<'_>, + acc: &hir::Expr<'_>, + fold_span: Span, +) { + 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}(|{s}| {r})", + replacement = replacement_method_name, + s = second_arg_ident, + r = snippet_with_applicability(cx, right_expr.span, "EXPR", &mut applicability), + ) + } else { + format!( + "{replacement}()", + replacement = 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, + ); + } + } + } + + // 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; + } + + // Check if the first argument to .fold is a suitable literal + if let hir::ExprKind::Lit(ref 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::Int(1, _) => { + check_fold_with_op(cx, expr, acc, fold_span, hir::BinOpKind::Mul, "product", false); + }, + _ => (), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs new file mode 100644 index 000000000..19037093e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs @@ -0,0 +1,104 @@ +use super::utils::clone_or_copy_needed; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::higher::ForLoop; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::{get_associated_type, get_iterator_item_ty, implements_trait}; +use clippy_utils::{fn_def_id, get_parent_expr}; +use rustc_errors::Applicability; +use rustc_hir::{def_id::DefId, Expr, ExprKind, LangItem}; +use rustc_lint::LateContext; +use rustc_span::{sym, Symbol}; + +use super::UNNECESSARY_TO_OWNED; + +pub fn check(cx: &LateContext<'_>, expr: &Expr<'_>, method_name: Symbol, receiver: &Expr<'_>) -> bool { + if_chain! { + if let Some(parent) = get_parent_expr(cx, expr); + if let Some(callee_def_id) = fn_def_id(cx, parent); + if is_into_iter(cx, callee_def_id); + then { + check_for_loop_iter(cx, parent, method_name, receiver, false) + } else { + false + } + } +} + +/// Checks whether `expr` is an iterator in a `for` loop and, if so, determines whether the +/// iterated-over items could be iterated over by reference. The reason why `check` above does not +/// include this code directly is so that it can be called from +/// `unnecessary_into_owned::check_into_iter_call_arg`. +pub fn check_for_loop_iter( + cx: &LateContext<'_>, + expr: &Expr<'_>, + method_name: Symbol, + receiver: &Expr<'_>, + cloned_before_iter: bool, +) -> bool { + if_chain! { + if let Some(grandparent) = get_parent_expr(cx, expr).and_then(|parent| get_parent_expr(cx, parent)); + if let Some(ForLoop { pat, body, .. }) = ForLoop::hir(grandparent); + let (clone_or_copy_needed, addr_of_exprs) = clone_or_copy_needed(cx, pat, body); + if !clone_or_copy_needed; + if let Some(receiver_snippet) = snippet_opt(cx, receiver.span); + then { + let snippet = if_chain! { + if let ExprKind::MethodCall(maybe_iter_method_name, [collection], _) = receiver.kind; + if maybe_iter_method_name.ident.name == sym::iter; + + if let Some(iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::Iterator); + let receiver_ty = cx.typeck_results().expr_ty(receiver); + if implements_trait(cx, receiver_ty, iterator_trait_id, &[]); + if let Some(iter_item_ty) = get_iterator_item_ty(cx, receiver_ty); + + if let Some(into_iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::IntoIterator); + let collection_ty = cx.typeck_results().expr_ty(collection); + if implements_trait(cx, collection_ty, into_iterator_trait_id, &[]); + if let Some(into_iter_item_ty) = get_associated_type(cx, collection_ty, into_iterator_trait_id, "Item"); + + if iter_item_ty == into_iter_item_ty; + if let Some(collection_snippet) = snippet_opt(cx, collection.span); + then { + collection_snippet + } else { + receiver_snippet + } + }; + span_lint_and_then( + cx, + UNNECESSARY_TO_OWNED, + expr.span, + &format!("unnecessary use of `{}`", method_name), + |diag| { + // If `check_into_iter_call_arg` called `check_for_loop_iter` because a call to + // a `to_owned`-like function was removed, then the next suggestion may be + // incorrect. This is because the iterator that results from the call's removal + // could hold a reference to a resource that is used mutably. See + // https://github.com/rust-lang/rust-clippy/issues/8148. + let applicability = if cloned_before_iter { + Applicability::MaybeIncorrect + } else { + Applicability::MachineApplicable + }; + diag.span_suggestion(expr.span, "use", snippet, applicability); + for addr_of_expr in addr_of_exprs { + match addr_of_expr.kind { + ExprKind::AddrOf(_, _, referent) => { + let span = addr_of_expr.span.with_hi(referent.span.lo()); + diag.span_suggestion(span, "remove this `&`", "", applicability); + } + _ => unreachable!(), + } + } + } + ); + return true; + } + } + false +} + +/// Returns true if the named method is `IntoIterator::into_iter`. +pub fn is_into_iter(cx: &LateContext<'_>, callee_def_id: DefId) -> bool { + cx.tcx.lang_items().require(LangItem::IntoIterIntoIter) == Ok(callee_def_id) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_join.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_join.rs new file mode 100644 index 000000000..973b8a7e6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_join.rs @@ -0,0 +1,41 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, ty::is_type_diagnostic_item}; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{Ref, Slice}; +use rustc_span::{sym, Span}; + +use super::UNNECESSARY_JOIN; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, + join_self_arg: &'tcx Expr<'tcx>, + join_arg: &'tcx Expr<'tcx>, + span: Span, +) { + let applicability = Applicability::MachineApplicable; + let collect_output_adjusted_type = cx.typeck_results().expr_ty_adjusted(join_self_arg); + if_chain! { + // the turbofish for collect is ::<Vec<String>> + if let Ref(_, ref_type, _) = collect_output_adjusted_type.kind(); + if let Slice(slice) = ref_type.kind(); + if is_type_diagnostic_item(cx, *slice, sym::String); + // the argument for join is "" + if let ExprKind::Lit(spanned) = &join_arg.kind; + if let LitKind::Str(symbol, _) = spanned.node; + if symbol.is_empty(); + then { + span_lint_and_sugg( + cx, + UNNECESSARY_JOIN, + span.with_hi(expr.span.hi()), + r#"called `.collect<Vec<String>>().join("")` on an iterator"#, + "try using", + "collect::<String>()".to_owned(), + applicability, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_lazy_eval.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_lazy_eval.rs new file mode 100644 index 000000000..1876c7fb9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_lazy_eval.rs @@ -0,0 +1,70 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{eager_or_lazy, usage}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::UNNECESSARY_LAZY_EVALUATIONS; + +/// lint use of `<fn>_else(simple closure)` for `Option`s and `Result`s that can be +/// replaced with `<fn>(return value of simple closure)` +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + recv: &'tcx hir::Expr<'_>, + arg: &'tcx hir::Expr<'_>, + simplify_using: &str, +) { + let is_option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option); + let is_result = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result); + let is_bool = cx.typeck_results().expr_ty(recv).is_bool(); + + if is_option || is_result || is_bool { + if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = arg.kind { + let body = cx.tcx.hir().body(body); + let body_expr = &body.value; + + if usage::BindingUsageFinder::are_params_used(cx, body) { + return; + } + + if eager_or_lazy::switch_to_eager_eval(cx, body_expr) { + let msg = if is_option { + "unnecessary closure used to substitute value for `Option::None`" + } else if is_result { + "unnecessary closure used to substitute value for `Result::Err`" + } else { + "unnecessary closure used with `bool::then`" + }; + let applicability = if body + .params + .iter() + // bindings are checked to be unused above + .all(|param| matches!(param.pat.kind, hir::PatKind::Binding(..) | hir::PatKind::Wild)) + { + Applicability::MachineApplicable + } else { + // replacing the lambda may break type inference + Applicability::MaybeIncorrect + }; + + // This is a duplicate of what's happening in clippy_lints::methods::method_call, + // which isn't ideal, We want to get the method call span, + // but prefer to avoid changing the signature of the function itself. + if let hir::ExprKind::MethodCall(_, _, span) = expr.kind { + span_lint_and_then(cx, UNNECESSARY_LAZY_EVALUATIONS, expr.span, msg, |diag| { + diag.span_suggestion( + span, + &format!("use `{}(..)` instead", simplify_using), + format!("{}({})", simplify_using, snippet(cx, body_expr.span, "..")), + applicability, + ); + }); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs new file mode 100644 index 000000000..b3276f139 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs @@ -0,0 +1,431 @@ +use super::implicit_clone::is_clone_like; +use super::unnecessary_iter_cloned::{self, is_into_iter}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::{ + contains_ty, get_associated_type, get_iterator_item_ty, implements_trait, is_copy, peel_mid_ty_refs, +}; +use clippy_utils::{meets_msrv, msrvs}; + +use clippy_utils::{fn_def_id, get_parent_expr, is_diag_item_method, is_diag_trait_item}; +use rustc_errors::Applicability; +use rustc_hir::{def_id::DefId, BorrowKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::mir::Mutability; +use rustc_middle::ty::adjustment::{Adjust, Adjustment, OverloadedDeref}; +use rustc_middle::ty::subst::{GenericArg, GenericArgKind, SubstsRef}; +use rustc_middle::ty::{self, PredicateKind, ProjectionPredicate, TraitPredicate, Ty}; +use rustc_semver::RustcVersion; +use rustc_span::{sym, Symbol}; +use std::cmp::max; + +use super::UNNECESSARY_TO_OWNED; + +pub fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, + method_name: Symbol, + args: &'tcx [Expr<'tcx>], + msrv: Option<RustcVersion>, +) { + if_chain! { + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if let [receiver] = args; + then { + if is_cloned_or_copied(cx, method_name, method_def_id) { + unnecessary_iter_cloned::check(cx, expr, method_name, receiver); + } else if is_to_owned_like(cx, method_name, method_def_id) { + // At this point, we know the call is of a `to_owned`-like function. The functions + // `check_addr_of_expr` and `check_call_arg` determine whether the call is unnecessary + // based on its context, that is, whether it is a referent in an `AddrOf` expression, an + // argument in a `into_iter` call, or an argument in the call of some other function. + if check_addr_of_expr(cx, expr, method_name, method_def_id, receiver) { + return; + } + if check_into_iter_call_arg(cx, expr, method_name, receiver, msrv) { + return; + } + check_other_call_arg(cx, expr, method_name, receiver); + } + } + } +} + +/// Checks whether `expr` is a referent in an `AddrOf` expression and, if so, determines whether its +/// call of a `to_owned`-like function is unnecessary. +#[allow(clippy::too_many_lines)] +fn check_addr_of_expr( + cx: &LateContext<'_>, + expr: &Expr<'_>, + method_name: Symbol, + method_def_id: DefId, + receiver: &Expr<'_>, +) -> bool { + if_chain! { + if let Some(parent) = get_parent_expr(cx, expr); + if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, _) = parent.kind; + let adjustments = cx.typeck_results().expr_adjustments(parent).iter().collect::<Vec<_>>(); + if let + // For matching uses of `Cow::from` + [ + Adjustment { + kind: Adjust::Deref(None), + target: referent_ty, + }, + Adjustment { + kind: Adjust::Borrow(_), + target: target_ty, + }, + ] + // For matching uses of arrays + | [ + Adjustment { + kind: Adjust::Deref(None), + target: referent_ty, + }, + Adjustment { + kind: Adjust::Borrow(_), + .. + }, + Adjustment { + kind: Adjust::Pointer(_), + target: target_ty, + }, + ] + // For matching everything else + | [ + Adjustment { + kind: Adjust::Deref(None), + target: referent_ty, + }, + Adjustment { + kind: Adjust::Deref(Some(OverloadedDeref { .. })), + .. + }, + Adjustment { + kind: Adjust::Borrow(_), + target: target_ty, + }, + ] = adjustments[..]; + let receiver_ty = cx.typeck_results().expr_ty(receiver); + let (target_ty, n_target_refs) = peel_mid_ty_refs(*target_ty); + let (receiver_ty, n_receiver_refs) = peel_mid_ty_refs(receiver_ty); + // Only flag cases satisfying at least one of the following three conditions: + // * the referent and receiver types are distinct + // * the referent/receiver type is a copyable array + // * the method is `Cow::into_owned` + // This restriction is to ensure there is no overlap between `redundant_clone` and this + // lint. It also avoids the following false positive: + // https://github.com/rust-lang/rust-clippy/issues/8759 + // Arrays are a bit of a corner case. Non-copyable arrays are handled by + // `redundant_clone`, but copyable arrays are not. + if *referent_ty != receiver_ty + || (matches!(referent_ty.kind(), ty::Array(..)) && is_copy(cx, *referent_ty)) + || is_cow_into_owned(cx, method_name, method_def_id); + if let Some(receiver_snippet) = snippet_opt(cx, receiver.span); + then { + if receiver_ty == target_ty && n_target_refs >= n_receiver_refs { + span_lint_and_sugg( + cx, + UNNECESSARY_TO_OWNED, + parent.span, + &format!("unnecessary use of `{}`", method_name), + "use", + format!( + "{:&>width$}{}", + "", + receiver_snippet, + width = n_target_refs - n_receiver_refs + ), + Applicability::MachineApplicable, + ); + return true; + } + if_chain! { + if let Some(deref_trait_id) = cx.tcx.get_diagnostic_item(sym::Deref); + if implements_trait(cx, receiver_ty, deref_trait_id, &[]); + if get_associated_type(cx, receiver_ty, deref_trait_id, "Target") == Some(target_ty); + then { + if n_receiver_refs > 0 { + span_lint_and_sugg( + cx, + UNNECESSARY_TO_OWNED, + parent.span, + &format!("unnecessary use of `{}`", method_name), + "use", + receiver_snippet, + Applicability::MachineApplicable, + ); + } else { + span_lint_and_sugg( + cx, + UNNECESSARY_TO_OWNED, + expr.span.with_lo(receiver.span.hi()), + &format!("unnecessary use of `{}`", method_name), + "remove this", + String::new(), + Applicability::MachineApplicable, + ); + } + return true; + } + } + if_chain! { + if let Some(as_ref_trait_id) = cx.tcx.get_diagnostic_item(sym::AsRef); + if implements_trait(cx, receiver_ty, as_ref_trait_id, &[GenericArg::from(target_ty)]); + then { + span_lint_and_sugg( + cx, + UNNECESSARY_TO_OWNED, + parent.span, + &format!("unnecessary use of `{}`", method_name), + "use", + format!("{}.as_ref()", receiver_snippet), + Applicability::MachineApplicable, + ); + return true; + } + } + } + } + false +} + +/// Checks whether `expr` is an argument in an `into_iter` call and, if so, determines whether its +/// call of a `to_owned`-like function is unnecessary. +fn check_into_iter_call_arg( + cx: &LateContext<'_>, + expr: &Expr<'_>, + method_name: Symbol, + receiver: &Expr<'_>, + msrv: Option<RustcVersion>, +) -> bool { + if_chain! { + if let Some(parent) = get_parent_expr(cx, expr); + if let Some(callee_def_id) = fn_def_id(cx, parent); + if is_into_iter(cx, callee_def_id); + if let Some(iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::Iterator); + let parent_ty = cx.typeck_results().expr_ty(parent); + if implements_trait(cx, parent_ty, iterator_trait_id, &[]); + if let Some(item_ty) = get_iterator_item_ty(cx, parent_ty); + if let Some(receiver_snippet) = snippet_opt(cx, receiver.span); + then { + if unnecessary_iter_cloned::check_for_loop_iter(cx, parent, method_name, receiver, true) { + return true; + } + let cloned_or_copied = if is_copy(cx, item_ty) && meets_msrv(msrv, msrvs::ITERATOR_COPIED) { + "copied" + } else { + "cloned" + }; + // The next suggestion may be incorrect because the removal of the `to_owned`-like + // function could cause the iterator to hold a reference to a resource that is used + // mutably. See https://github.com/rust-lang/rust-clippy/issues/8148. + span_lint_and_sugg( + cx, + UNNECESSARY_TO_OWNED, + parent.span, + &format!("unnecessary use of `{}`", method_name), + "use", + format!("{}.iter().{}()", receiver_snippet, cloned_or_copied), + Applicability::MaybeIncorrect, + ); + return true; + } + } + false +} + +/// Checks whether `expr` is an argument in a function call and, if so, determines whether its call +/// of a `to_owned`-like function is unnecessary. +fn check_other_call_arg<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, + method_name: Symbol, + receiver: &'tcx Expr<'tcx>, +) -> bool { + if_chain! { + if let Some((maybe_call, maybe_arg)) = skip_addr_of_ancestors(cx, expr); + if let Some((callee_def_id, call_substs, call_args)) = get_callee_substs_and_args(cx, maybe_call); + let fn_sig = cx.tcx.fn_sig(callee_def_id).skip_binder(); + if let Some(i) = call_args.iter().position(|arg| arg.hir_id == maybe_arg.hir_id); + if let Some(input) = fn_sig.inputs().get(i); + let (input, n_refs) = peel_mid_ty_refs(*input); + if let (trait_predicates, projection_predicates) = get_input_traits_and_projections(cx, callee_def_id, input); + if let Some(sized_def_id) = cx.tcx.lang_items().sized_trait(); + if let [trait_predicate] = trait_predicates + .iter() + .filter(|trait_predicate| trait_predicate.def_id() != sized_def_id) + .collect::<Vec<_>>()[..]; + if let Some(deref_trait_id) = cx.tcx.get_diagnostic_item(sym::Deref); + if let Some(as_ref_trait_id) = cx.tcx.get_diagnostic_item(sym::AsRef); + let receiver_ty = cx.typeck_results().expr_ty(receiver); + // If the callee has type parameters, they could appear in `projection_predicate.ty` or the + // types of `trait_predicate.trait_ref.substs`. + if if trait_predicate.def_id() == deref_trait_id { + if let [projection_predicate] = projection_predicates[..] { + let normalized_ty = + cx.tcx + .subst_and_normalize_erasing_regions(call_substs, cx.param_env, projection_predicate.term); + implements_trait(cx, receiver_ty, deref_trait_id, &[]) + && get_associated_type(cx, receiver_ty, deref_trait_id, "Target") + .map_or(false, |ty| ty::Term::Ty(ty) == normalized_ty) + } else { + false + } + } else if trait_predicate.def_id() == as_ref_trait_id { + let composed_substs = compose_substs( + cx, + &trait_predicate.trait_ref.substs.iter().skip(1).collect::<Vec<_>>()[..], + call_substs, + ); + implements_trait(cx, receiver_ty, as_ref_trait_id, &composed_substs) + } else { + false + }; + // We can't add an `&` when the trait is `Deref` because `Target = &T` won't match + // `Target = T`. + if n_refs > 0 || is_copy(cx, receiver_ty) || trait_predicate.def_id() != deref_trait_id; + let n_refs = max(n_refs, if is_copy(cx, receiver_ty) { 0 } else { 1 }); + // If the trait is `AsRef` and the input type variable `T` occurs in the output type, then + // `T` must not be instantiated with a reference + // (https://github.com/rust-lang/rust-clippy/issues/8507). + if (n_refs == 0 && !receiver_ty.is_ref()) + || trait_predicate.def_id() != as_ref_trait_id + || !contains_ty(fn_sig.output(), input); + if let Some(receiver_snippet) = snippet_opt(cx, receiver.span); + then { + span_lint_and_sugg( + cx, + UNNECESSARY_TO_OWNED, + maybe_arg.span, + &format!("unnecessary use of `{}`", method_name), + "use", + format!("{:&>width$}{}", "", receiver_snippet, width = n_refs), + Applicability::MachineApplicable, + ); + return true; + } + } + false +} + +/// Walks an expression's ancestors until it finds a non-`AddrOf` expression. Returns the first such +/// expression found (if any) along with the immediately prior expression. +fn skip_addr_of_ancestors<'tcx>( + cx: &LateContext<'tcx>, + mut expr: &'tcx Expr<'tcx>, +) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> { + while let Some(parent) = get_parent_expr(cx, expr) { + if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, _) = parent.kind { + expr = parent; + } else { + return Some((parent, expr)); + } + } + None +} + +/// Checks whether an expression is a function or method call and, if so, returns its `DefId`, +/// `Substs`, and arguments. +fn get_callee_substs_and_args<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, +) -> Option<(DefId, SubstsRef<'tcx>, &'tcx [Expr<'tcx>])> { + if_chain! { + if let ExprKind::Call(callee, args) = expr.kind; + let callee_ty = cx.typeck_results().expr_ty(callee); + if let ty::FnDef(callee_def_id, _) = callee_ty.kind(); + then { + let substs = cx.typeck_results().node_substs(callee.hir_id); + return Some((*callee_def_id, substs, args)); + } + } + if_chain! { + if let ExprKind::MethodCall(_, args, _) = expr.kind; + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + then { + let substs = cx.typeck_results().node_substs(expr.hir_id); + return Some((method_def_id, substs, args)); + } + } + None +} + +/// Returns the `TraitPredicate`s and `ProjectionPredicate`s for a function's input type. +fn get_input_traits_and_projections<'tcx>( + cx: &LateContext<'tcx>, + callee_def_id: DefId, + input: Ty<'tcx>, +) -> (Vec<TraitPredicate<'tcx>>, Vec<ProjectionPredicate<'tcx>>) { + let mut trait_predicates = Vec::new(); + let mut projection_predicates = Vec::new(); + for (predicate, _) in cx.tcx.predicates_of(callee_def_id).predicates.iter() { + // `substs` should have 1 + n elements. The first is the type on the left hand side of an + // `as`. The remaining n are trait parameters. + let is_input_substs = |substs: SubstsRef<'tcx>| { + if_chain! { + if let Some(arg) = substs.iter().next(); + if let GenericArgKind::Type(arg_ty) = arg.unpack(); + if arg_ty == input; + then { true } else { false } + } + }; + match predicate.kind().skip_binder() { + PredicateKind::Trait(trait_predicate) => { + if is_input_substs(trait_predicate.trait_ref.substs) { + trait_predicates.push(trait_predicate); + } + }, + PredicateKind::Projection(projection_predicate) => { + if is_input_substs(projection_predicate.projection_ty.substs) { + projection_predicates.push(projection_predicate); + } + }, + _ => {}, + } + } + (trait_predicates, projection_predicates) +} + +/// Composes two substitutions by applying the latter to the types of the former. +fn compose_substs<'tcx>( + cx: &LateContext<'tcx>, + left: &[GenericArg<'tcx>], + right: SubstsRef<'tcx>, +) -> Vec<GenericArg<'tcx>> { + left.iter() + .map(|arg| { + if let GenericArgKind::Type(arg_ty) = arg.unpack() { + let normalized_ty = cx.tcx.subst_and_normalize_erasing_regions(right, cx.param_env, arg_ty); + GenericArg::from(normalized_ty) + } else { + *arg + } + }) + .collect() +} + +/// Returns true if the named method is `Iterator::cloned` or `Iterator::copied`. +fn is_cloned_or_copied(cx: &LateContext<'_>, method_name: Symbol, method_def_id: DefId) -> bool { + (method_name.as_str() == "cloned" || method_name.as_str() == "copied") + && is_diag_trait_item(cx, method_def_id, sym::Iterator) +} + +/// Returns true if the named method can be used to convert the receiver to its "owned" +/// representation. +fn is_to_owned_like(cx: &LateContext<'_>, method_name: Symbol, method_def_id: DefId) -> bool { + is_clone_like(cx, method_name.as_str(), method_def_id) + || is_cow_into_owned(cx, method_name, method_def_id) + || is_to_string(cx, method_name, method_def_id) +} + +/// Returns true if the named method is `Cow::into_owned`. +fn is_cow_into_owned(cx: &LateContext<'_>, method_name: Symbol, method_def_id: DefId) -> bool { + method_name.as_str() == "into_owned" && is_diag_item_method(cx, method_def_id, sym::Cow) +} + +/// Returns true if the named method is `ToString::to_string`. +fn is_to_string(cx: &LateContext<'_>, method_name: Symbol, method_def_id: DefId) -> bool { + method_name == sym::to_string && is_diag_trait_item(cx, method_def_id, sym::ToString) +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unwrap_or_else_default.rs b/src/tools/clippy/clippy_lints/src/methods/unwrap_or_else_default.rs new file mode 100644 index 000000000..f3af281d6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unwrap_or_else_default.rs @@ -0,0 +1,46 @@ +//! Lint for `some_result_or_option.unwrap_or_else(Default::default)` + +use super::UNWRAP_OR_ELSE_DEFAULT; +use clippy_utils::{ + diagnostics::span_lint_and_sugg, is_default_equivalent_call, source::snippet_with_applicability, + ty::is_type_diagnostic_item, +}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + recv: &'tcx hir::Expr<'_>, + u_arg: &'tcx hir::Expr<'_>, +) { + // something.unwrap_or_else(Default::default) + // ^^^^^^^^^- recv ^^^^^^^^^^^^^^^^- u_arg + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- expr + let recv_ty = cx.typeck_results().expr_ty(recv); + let is_option = is_type_diagnostic_item(cx, recv_ty, sym::Option); + let is_result = is_type_diagnostic_item(cx, recv_ty, sym::Result); + + if_chain! { + if is_option || is_result; + if is_default_equivalent_call(cx, u_arg); + then { + let mut applicability = Applicability::MachineApplicable; + + span_lint_and_sugg( + cx, + UNWRAP_OR_ELSE_DEFAULT, + expr.span, + "use of `.unwrap_or_else(..)` to construct default value", + "try", + format!( + "{}.unwrap_or_default()", + snippet_with_applicability(cx, recv.span, "..", &mut applicability) + ), + applicability, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/unwrap_used.rs b/src/tools/clippy/clippy_lints/src/methods/unwrap_used.rs new file mode 100644 index 000000000..5c7610149 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/unwrap_used.rs @@ -0,0 +1,40 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_in_test_function; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::UNWRAP_USED; + +/// lint use of `unwrap()` for `Option`s and `Result`s +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>, allow_unwrap_in_tests: bool) { + let obj_ty = cx.typeck_results().expr_ty(recv).peel_refs(); + + let mess = if is_type_diagnostic_item(cx, obj_ty, sym::Option) { + Some((UNWRAP_USED, "an Option", "None")) + } else if is_type_diagnostic_item(cx, obj_ty, sym::Result) { + Some((UNWRAP_USED, "a Result", "Err")) + } else { + None + }; + + if allow_unwrap_in_tests && is_in_test_function(cx.tcx, expr.hir_id) { + return; + } + + if let Some((lint, kind, none_value)) = mess { + span_lint_and_help( + cx, + lint, + expr.span, + &format!("used `unwrap()` on `{}` value", kind,), + None, + &format!( + "if you don't want to handle the `{}` case gracefully, consider \ + using `expect()` to provide a better panic message", + none_value, + ), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/useless_asref.rs b/src/tools/clippy/clippy_lints/src/methods/useless_asref.rs new file mode 100644 index 000000000..ca5d33ee8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/useless_asref.rs @@ -0,0 +1,45 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::walk_ptrs_ty_depth; +use clippy_utils::{get_parent_expr, match_trait_method, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; + +use super::USELESS_ASREF; + +/// Checks for the `USELESS_ASREF` lint. +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, call_name: &str, recvr: &hir::Expr<'_>) { + // when we get here, we've already checked that the call name is "as_ref" or "as_mut" + // check if the call is to the actual `AsRef` or `AsMut` trait + if match_trait_method(cx, expr, &paths::ASREF_TRAIT) || match_trait_method(cx, expr, &paths::ASMUT_TRAIT) { + // check if the type after `as_ref` or `as_mut` is the same as before + let rcv_ty = cx.typeck_results().expr_ty(recvr); + let res_ty = cx.typeck_results().expr_ty(expr); + let (base_res_ty, res_depth) = walk_ptrs_ty_depth(res_ty); + let (base_rcv_ty, rcv_depth) = walk_ptrs_ty_depth(rcv_ty); + if base_rcv_ty == base_res_ty && rcv_depth >= res_depth { + // allow the `as_ref` or `as_mut` if it is followed by another method call + if_chain! { + if let Some(parent) = get_parent_expr(cx, expr); + if let hir::ExprKind::MethodCall(segment, ..) = parent.kind; + if segment.ident.span != expr.span; + then { + return; + } + } + + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + USELESS_ASREF, + expr.span, + &format!("this call to `{}` does nothing", call_name), + "try this", + snippet_with_applicability(cx, recvr.span, "..", &mut applicability).to_string(), + applicability, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/utils.rs b/src/tools/clippy/clippy_lints/src/methods/utils.rs new file mode 100644 index 000000000..3015531e8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/utils.rs @@ -0,0 +1,168 @@ +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{get_parent_expr, path_to_local_id, usage}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{BorrowKind, Expr, ExprKind, HirId, Mutability, Pat}; +use rustc_lint::LateContext; +use rustc_middle::hir::nested_filter; +use rustc_middle::ty::{self, Ty}; +use rustc_span::symbol::sym; + +pub(super) fn derefs_to_slice<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'tcx>, + ty: Ty<'tcx>, +) -> Option<&'tcx hir::Expr<'tcx>> { + fn may_slice<'a>(cx: &LateContext<'a>, ty: Ty<'a>) -> bool { + match ty.kind() { + ty::Slice(_) => true, + ty::Adt(def, _) if def.is_box() => may_slice(cx, ty.boxed_ty()), + ty::Adt(..) => is_type_diagnostic_item(cx, ty, sym::Vec), + ty::Array(_, size) => size.try_eval_usize(cx.tcx, cx.param_env).is_some(), + ty::Ref(_, inner, _) => may_slice(cx, *inner), + _ => false, + } + } + + if let hir::ExprKind::MethodCall(path, [self_arg, ..], _) = &expr.kind { + if path.ident.name == sym::iter && may_slice(cx, cx.typeck_results().expr_ty(self_arg)) { + Some(self_arg) + } else { + None + } + } else { + match ty.kind() { + ty::Slice(_) => Some(expr), + ty::Adt(def, _) if def.is_box() && may_slice(cx, ty.boxed_ty()) => Some(expr), + ty::Ref(_, inner, _) => { + if may_slice(cx, *inner) { + Some(expr) + } else { + None + } + }, + _ => None, + } + } +} + +pub(super) fn get_hint_if_single_char_arg( + cx: &LateContext<'_>, + arg: &hir::Expr<'_>, + applicability: &mut Applicability, +) -> Option<String> { + if_chain! { + if let hir::ExprKind::Lit(lit) = &arg.kind; + if let ast::LitKind::Str(r, style) = lit.node; + let string = r.as_str(); + if string.chars().count() == 1; + then { + let snip = snippet_with_applicability(cx, arg.span, string, applicability); + let ch = if let ast::StrStyle::Raw(nhash) = style { + let nhash = nhash as usize; + // for raw string: r##"a"## + &snip[(nhash + 2)..(snip.len() - 1 - nhash)] + } else { + // for regular string: "a" + &snip[1..(snip.len() - 1)] + }; + + let hint = format!("'{}'", match ch { + "'" => "\\'" , + r"\" => "\\\\", + _ => ch, + }); + + Some(hint) + } else { + None + } + } +} + +/// The core logic of `check_for_loop_iter` in `unnecessary_iter_cloned.rs`, this function wraps a +/// use of `CloneOrCopyVisitor`. +pub(super) fn clone_or_copy_needed<'tcx>( + cx: &LateContext<'tcx>, + pat: &Pat<'tcx>, + body: &'tcx Expr<'tcx>, +) -> (bool, Vec<&'tcx Expr<'tcx>>) { + let mut visitor = CloneOrCopyVisitor { + cx, + binding_hir_ids: pat_bindings(pat), + clone_or_copy_needed: false, + addr_of_exprs: Vec::new(), + }; + visitor.visit_expr(body); + (visitor.clone_or_copy_needed, visitor.addr_of_exprs) +} + +/// Returns a vector of all `HirId`s bound by the pattern. +fn pat_bindings(pat: &Pat<'_>) -> Vec<HirId> { + let mut collector = usage::ParamBindingIdCollector { + binding_hir_ids: Vec::new(), + }; + collector.visit_pat(pat); + collector.binding_hir_ids +} + +/// `clone_or_copy_needed` will be false when `CloneOrCopyVisitor` is done visiting if the only +/// operations performed on `binding_hir_ids` are: +/// * to take non-mutable references to them +/// * to use them as non-mutable `&self` in method calls +/// If any of `binding_hir_ids` is used in any other way, then `clone_or_copy_needed` will be true +/// when `CloneOrCopyVisitor` is done visiting. +struct CloneOrCopyVisitor<'cx, 'tcx> { + cx: &'cx LateContext<'tcx>, + binding_hir_ids: Vec<HirId>, + clone_or_copy_needed: bool, + addr_of_exprs: Vec<&'tcx Expr<'tcx>>, +} + +impl<'cx, 'tcx> Visitor<'tcx> for CloneOrCopyVisitor<'cx, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { + walk_expr(self, expr); + if self.is_binding(expr) { + if let Some(parent) = get_parent_expr(self.cx, expr) { + match parent.kind { + ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, _) => { + self.addr_of_exprs.push(parent); + return; + }, + ExprKind::MethodCall(_, args, _) => { + if_chain! { + if args.iter().skip(1).all(|arg| !self.is_binding(arg)); + if let Some(method_def_id) = self.cx.typeck_results().type_dependent_def_id(parent.hir_id); + let method_ty = self.cx.tcx.type_of(method_def_id); + let self_ty = method_ty.fn_sig(self.cx.tcx).input(0).skip_binder(); + if matches!(self_ty.kind(), ty::Ref(_, _, Mutability::Not)); + then { + return; + } + } + }, + _ => {}, + } + } + self.clone_or_copy_needed = true; + } + } +} + +impl<'cx, 'tcx> CloneOrCopyVisitor<'cx, 'tcx> { + fn is_binding(&self, expr: &Expr<'tcx>) -> bool { + self.binding_hir_ids + .iter() + .any(|hir_id| path_to_local_id(expr, *hir_id)) + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/wrong_self_convention.rs b/src/tools/clippy/clippy_lints/src/methods/wrong_self_convention.rs new file mode 100644 index 000000000..4b368d3ff --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/wrong_self_convention.rs @@ -0,0 +1,154 @@ +use crate::methods::SelfKind; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::is_copy; +use rustc_lint::LateContext; +use rustc_middle::ty::Ty; +use rustc_span::source_map::Span; +use std::fmt; + +use super::WRONG_SELF_CONVENTION; + +#[rustfmt::skip] +const CONVENTIONS: [(&[Convention], &[SelfKind]); 9] = [ + (&[Convention::Eq("new")], &[SelfKind::No]), + (&[Convention::StartsWith("as_")], &[SelfKind::Ref, SelfKind::RefMut]), + (&[Convention::StartsWith("from_")], &[SelfKind::No]), + (&[Convention::StartsWith("into_")], &[SelfKind::Value]), + (&[Convention::StartsWith("is_")], &[SelfKind::RefMut, SelfKind::Ref, SelfKind::No]), + (&[Convention::Eq("to_mut")], &[SelfKind::RefMut]), + (&[Convention::StartsWith("to_"), Convention::EndsWith("_mut")], &[SelfKind::RefMut]), + + // Conversion using `to_` can use borrowed (non-Copy types) or owned (Copy types). + // Source: https://rust-lang.github.io/api-guidelines/naming.html#ad-hoc-conversions-follow-as_-to_-into_-conventions-c-conv + (&[Convention::StartsWith("to_"), Convention::NotEndsWith("_mut"), Convention::IsSelfTypeCopy(false), + Convention::IsTraitItem(false), Convention::ImplementsTrait(false)], &[SelfKind::Ref]), + (&[Convention::StartsWith("to_"), Convention::NotEndsWith("_mut"), Convention::IsSelfTypeCopy(true), + Convention::IsTraitItem(false), Convention::ImplementsTrait(false)], &[SelfKind::Value]), +]; + +enum Convention { + Eq(&'static str), + StartsWith(&'static str), + EndsWith(&'static str), + NotEndsWith(&'static str), + IsSelfTypeCopy(bool), + ImplementsTrait(bool), + IsTraitItem(bool), +} + +impl Convention { + #[must_use] + fn check<'tcx>( + &self, + cx: &LateContext<'tcx>, + self_ty: Ty<'tcx>, + other: &str, + implements_trait: bool, + is_trait_item: bool, + ) -> bool { + match *self { + Self::Eq(this) => this == other, + Self::StartsWith(this) => other.starts_with(this) && this != other, + Self::EndsWith(this) => other.ends_with(this) && this != other, + Self::NotEndsWith(this) => !Self::EndsWith(this).check(cx, self_ty, other, implements_trait, is_trait_item), + Self::IsSelfTypeCopy(is_true) => is_true == is_copy(cx, self_ty), + Self::ImplementsTrait(is_true) => is_true == implements_trait, + Self::IsTraitItem(is_true) => is_true == is_trait_item, + } + } +} + +impl fmt::Display for Convention { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match *self { + Self::Eq(this) => format!("`{}`", this).fmt(f), + Self::StartsWith(this) => format!("`{}*`", this).fmt(f), + Self::EndsWith(this) => format!("`*{}`", this).fmt(f), + Self::NotEndsWith(this) => format!("`~{}`", this).fmt(f), + Self::IsSelfTypeCopy(is_true) => { + format!("`self` type is{} `Copy`", if is_true { "" } else { " not" }).fmt(f) + }, + Self::ImplementsTrait(is_true) => { + let (negation, s_suffix) = if is_true { ("", "s") } else { (" does not", "") }; + format!("method{} implement{} a trait", negation, s_suffix).fmt(f) + }, + Self::IsTraitItem(is_true) => { + let suffix = if is_true { " is" } else { " is not" }; + format!("method{} a trait item", suffix).fmt(f) + }, + } + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + item_name: &str, + self_ty: Ty<'tcx>, + first_arg_ty: Ty<'tcx>, + first_arg_span: Span, + implements_trait: bool, + is_trait_item: bool, +) { + if let Some((conventions, self_kinds)) = &CONVENTIONS.iter().find(|(convs, _)| { + convs + .iter() + .all(|conv| conv.check(cx, self_ty, item_name, implements_trait, is_trait_item)) + }) { + // don't lint if it implements a trait but not willing to check `Copy` types conventions (see #7032) + if implements_trait + && !conventions + .iter() + .any(|conv| matches!(conv, Convention::IsSelfTypeCopy(_))) + { + return; + } + if !self_kinds.iter().any(|k| k.matches(cx, self_ty, first_arg_ty)) { + let suggestion = { + if conventions.len() > 1 { + // Don't mention `NotEndsWith` when there is also `StartsWith` convention present + let cut_ends_with_conv = conventions.iter().any(|conv| matches!(conv, Convention::StartsWith(_))) + && conventions + .iter() + .any(|conv| matches!(conv, Convention::NotEndsWith(_))); + + let s = conventions + .iter() + .filter_map(|conv| { + if (cut_ends_with_conv && matches!(conv, Convention::NotEndsWith(_))) + || matches!(conv, Convention::ImplementsTrait(_)) + || matches!(conv, Convention::IsTraitItem(_)) + { + None + } else { + Some(conv.to_string()) + } + }) + .collect::<Vec<_>>() + .join(" and "); + + format!("methods with the following characteristics: ({})", &s) + } else { + format!("methods called {}", &conventions[0]) + } + }; + + span_lint_and_help( + cx, + WRONG_SELF_CONVENTION, + first_arg_span, + &format!( + "{} usually take {}", + suggestion, + &self_kinds + .iter() + .map(|k| k.description()) + .collect::<Vec<_>>() + .join(" or ") + ), + None, + "consider choosing a less ambiguous name", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/methods/zst_offset.rs b/src/tools/clippy/clippy_lints/src/methods/zst_offset.rs new file mode 100644 index 000000000..e9f268da6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/methods/zst_offset.rs @@ -0,0 +1,18 @@ +use clippy_utils::diagnostics::span_lint; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::ZST_OFFSET; + +pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>) { + if_chain! { + if let ty::RawPtr(ty::TypeAndMut { ty, .. }) = cx.typeck_results().expr_ty(recv).kind(); + if let Ok(layout) = cx.tcx.layout_of(cx.param_env.and(*ty)); + if layout.is_zst(); + then { + span_lint(cx, ZST_OFFSET, expr.span, "offset calculation on zero-sized value"); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/minmax.rs b/src/tools/clippy/clippy_lints/src/minmax.rs new file mode 100644 index 000000000..a081cde85 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/minmax.rs @@ -0,0 +1,122 @@ +use clippy_utils::consts::{constant_simple, Constant}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{match_trait_method, paths}; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; +use std::cmp::Ordering; + +declare_clippy_lint! { + /// ### What it does + /// Checks for expressions where `std::cmp::min` and `max` are + /// used to clamp values, but switched so that the result is constant. + /// + /// ### Why is this bad? + /// This is in all probability not the intended outcome. At + /// the least it hurts readability of the code. + /// + /// ### Example + /// ```rust,ignore + /// min(0, max(100, x)) + /// + /// // or + /// + /// x.max(100).min(0) + /// ``` + /// It will always be equal to `0`. Probably the author meant to clamp the value + /// between 0 and 100, but has erroneously swapped `min` and `max`. + #[clippy::version = "pre 1.29.0"] + pub MIN_MAX, + correctness, + "`min(_, max(_, _))` (or vice versa) with bounds clamping the result to a constant" +} + +declare_lint_pass!(MinMaxPass => [MIN_MAX]); + +impl<'tcx> LateLintPass<'tcx> for MinMaxPass { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let Some((outer_max, outer_c, oe)) = min_max(cx, expr) { + if let Some((inner_max, inner_c, ie)) = min_max(cx, oe) { + if outer_max == inner_max { + return; + } + match ( + outer_max, + Constant::partial_cmp(cx.tcx, cx.typeck_results().expr_ty(ie), &outer_c, &inner_c), + ) { + (_, None) | (MinMax::Max, Some(Ordering::Less)) | (MinMax::Min, Some(Ordering::Greater)) => (), + _ => { + span_lint( + cx, + MIN_MAX, + expr.span, + "this `min`/`max` combination leads to constant result", + ); + }, + } + } + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +enum MinMax { + Min, + Max, +} + +fn min_max<'a>(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<(MinMax, Constant, &'a Expr<'a>)> { + match expr.kind { + ExprKind::Call(path, args) => { + if let ExprKind::Path(ref qpath) = path.kind { + cx.typeck_results() + .qpath_res(qpath, path.hir_id) + .opt_def_id() + .and_then(|def_id| match cx.tcx.get_diagnostic_name(def_id) { + Some(sym::cmp_min) => fetch_const(cx, args, MinMax::Min), + Some(sym::cmp_max) => fetch_const(cx, args, MinMax::Max), + _ => None, + }) + } else { + None + } + }, + ExprKind::MethodCall(path, args, _) => { + if_chain! { + if let [obj, _] = args; + if cx.typeck_results().expr_ty(obj).is_floating_point() || match_trait_method(cx, expr, &paths::ORD); + then { + if path.ident.name == sym!(max) { + fetch_const(cx, args, MinMax::Max) + } else if path.ident.name == sym!(min) { + fetch_const(cx, args, MinMax::Min) + } else { + None + } + } else { + None + } + } + }, + _ => None, + } +} + +fn fetch_const<'a>(cx: &LateContext<'_>, args: &'a [Expr<'a>], m: MinMax) -> Option<(MinMax, Constant, &'a Expr<'a>)> { + if args.len() != 2 { + return None; + } + constant_simple(cx, cx.typeck_results(), &args[0]).map_or_else( + || constant_simple(cx, cx.typeck_results(), &args[1]).map(|c| (m, c, &args[0])), + |c| { + if constant_simple(cx, cx.typeck_results(), &args[1]).is_none() { + // otherwise ignore + Some((m, c, &args[1])) + } else { + None + } + }, + ) +} diff --git a/src/tools/clippy/clippy_lints/src/misc.rs b/src/tools/clippy/clippy_lints/src/misc.rs new file mode 100644 index 000000000..8224e80c9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc.rs @@ -0,0 +1,342 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_hir_and_then}; +use clippy_utils::source::{snippet, snippet_opt}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{ + self as hir, def, BinOpKind, BindingAnnotation, Body, Expr, ExprKind, FnDecl, HirId, Mutability, PatKind, Stmt, + StmtKind, TyKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::hygiene::DesugaringKind; +use rustc_span::source_map::{ExpnKind, Span}; + +use clippy_utils::sugg::Sugg; +use clippy_utils::{get_parent_expr, in_constant, iter_input_pats, last_path_segment, SpanlessEq}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for function arguments and let bindings denoted as + /// `ref`. + /// + /// ### Why is this bad? + /// The `ref` declaration makes the function take an owned + /// value, but turns the argument into a reference (which means that the value + /// is destroyed when exiting the function). This adds not much value: either + /// take a reference type, or take an owned value and create references in the + /// body. + /// + /// For let bindings, `let x = &foo;` is preferred over `let ref x = foo`. The + /// type of `x` is more obvious with the former. + /// + /// ### Known problems + /// If the argument is dereferenced within the function, + /// removing the `ref` will lead to errors. This can be fixed by removing the + /// dereferences, e.g., changing `*x` to `x` within the function. + /// + /// ### Example + /// ```rust + /// fn foo(ref _x: u8) {} + /// ``` + /// + /// Use instead: + /// ```rust + /// fn foo(_x: &u8) {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TOPLEVEL_REF_ARG, + style, + "an entire binding declared as `ref`, in a function argument or a `let` statement" +} +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of bindings with a single leading + /// underscore. + /// + /// ### Why is this bad? + /// A single leading underscore is usually used to indicate + /// that a binding will not be used. Using such a binding breaks this + /// expectation. + /// + /// ### Known problems + /// The lint does not work properly with desugaring and + /// macro, it has been allowed in the mean time. + /// + /// ### Example + /// ```rust + /// let _x = 0; + /// let y = _x + 1; // Here we are using `_x`, even though it has a leading + /// // underscore. We should rename `_x` to `x` + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USED_UNDERSCORE_BINDING, + pedantic, + "using a binding which is prefixed with an underscore" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the use of short circuit boolean conditions as + /// a + /// statement. + /// + /// ### Why is this bad? + /// Using a short circuit boolean condition as a statement + /// may hide the fact that the second part is executed or not depending on the + /// outcome of the first part. + /// + /// ### Example + /// ```rust,ignore + /// f() && g(); // We should write `if f() { g(); }`. + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SHORT_CIRCUIT_STATEMENT, + complexity, + "using a short circuit boolean condition as a statement" +} + +declare_clippy_lint! { + /// ### What it does + /// Catch casts from `0` to some pointer type + /// + /// ### Why is this bad? + /// This generally means `null` and is better expressed as + /// {`std`, `core`}`::ptr::`{`null`, `null_mut`}. + /// + /// ### Example + /// ```rust + /// let a = 0 as *const u32; + /// ``` + /// + /// Use instead: + /// ```rust + /// let a = std::ptr::null::<u32>(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ZERO_PTR, + style, + "using `0 as *{const, mut} T`" +} + +declare_lint_pass!(MiscLints => [ + TOPLEVEL_REF_ARG, + USED_UNDERSCORE_BINDING, + SHORT_CIRCUIT_STATEMENT, + ZERO_PTR, +]); + +impl<'tcx> LateLintPass<'tcx> for MiscLints { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + k: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + span: Span, + _: HirId, + ) { + if let FnKind::Closure = k { + // Does not apply to closures + return; + } + if in_external_macro(cx.tcx.sess, span) { + return; + } + for arg in iter_input_pats(decl, body) { + if let PatKind::Binding(BindingAnnotation::Ref | BindingAnnotation::RefMut, ..) = arg.pat.kind { + span_lint( + cx, + TOPLEVEL_REF_ARG, + arg.pat.span, + "`ref` directly on a function argument is ignored. \ + Consider using a reference type instead", + ); + } + } + } + + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + if_chain! { + if !in_external_macro(cx.tcx.sess, stmt.span); + if let StmtKind::Local(local) = stmt.kind; + if let PatKind::Binding(an, .., name, None) = local.pat.kind; + if let Some(init) = local.init; + if an == BindingAnnotation::Ref || an == BindingAnnotation::RefMut; + then { + // use the macro callsite when the init span (but not the whole local span) + // comes from an expansion like `vec![1, 2, 3]` in `let ref _ = vec![1, 2, 3];` + let sugg_init = if init.span.from_expansion() && !local.span.from_expansion() { + Sugg::hir_with_macro_callsite(cx, init, "..") + } else { + Sugg::hir(cx, init, "..") + }; + let (mutopt, initref) = if an == BindingAnnotation::RefMut { + ("mut ", sugg_init.mut_addr()) + } else { + ("", sugg_init.addr()) + }; + let tyopt = if let Some(ty) = local.ty { + format!(": &{mutopt}{ty}", mutopt=mutopt, ty=snippet(cx, ty.span, "..")) + } else { + String::new() + }; + span_lint_hir_and_then( + cx, + TOPLEVEL_REF_ARG, + init.hir_id, + local.pat.span, + "`ref` on an entire `let` pattern is discouraged, take a reference with `&` instead", + |diag| { + diag.span_suggestion( + stmt.span, + "try", + format!( + "let {name}{tyopt} = {initref};", + name=snippet(cx, name.span, ".."), + tyopt=tyopt, + initref=initref, + ), + Applicability::MachineApplicable, + ); + } + ); + } + }; + if_chain! { + if let StmtKind::Semi(expr) = stmt.kind; + if let ExprKind::Binary(ref binop, a, b) = expr.kind; + if binop.node == BinOpKind::And || binop.node == BinOpKind::Or; + if let Some(sugg) = Sugg::hir_opt(cx, a); + then { + span_lint_hir_and_then( + cx, + SHORT_CIRCUIT_STATEMENT, + expr.hir_id, + stmt.span, + "boolean short circuit operator in statement may be clearer using an explicit test", + |diag| { + let sugg = if binop.node == BinOpKind::Or { !sugg } else { sugg }; + diag.span_suggestion( + stmt.span, + "replace it with", + format!( + "if {} {{ {}; }}", + sugg, + &snippet(cx, b.span, ".."), + ), + Applicability::MachineApplicable, // snippet + ); + }); + } + }; + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Cast(e, ty) = expr.kind { + check_cast(cx, expr.span, e, ty); + return; + } + if in_attributes_expansion(expr) || expr.span.is_desugaring(DesugaringKind::Await) { + // Don't lint things expanded by #[derive(...)], etc or `await` desugaring + return; + } + let sym; + let binding = match expr.kind { + ExprKind::Path(ref qpath) if !matches!(qpath, hir::QPath::LangItem(..)) => { + let binding = last_path_segment(qpath).ident.as_str(); + if binding.starts_with('_') && + !binding.starts_with("__") && + binding != "_result" && // FIXME: #944 + is_used(cx, expr) && + // don't lint if the declaration is in a macro + non_macro_local(cx, cx.qpath_res(qpath, expr.hir_id)) + { + Some(binding) + } else { + None + } + }, + ExprKind::Field(_, ident) => { + sym = ident.name; + let name = sym.as_str(); + if name.starts_with('_') && !name.starts_with("__") { + Some(name) + } else { + None + } + }, + _ => None, + }; + if let Some(binding) = binding { + span_lint( + cx, + USED_UNDERSCORE_BINDING, + expr.span, + &format!( + "used binding `{}` which is prefixed with an underscore. A leading \ + underscore signals that a binding will not be used", + binding + ), + ); + } + } +} + +/// Heuristic to see if an expression is used. Should be compatible with +/// `unused_variables`'s idea +/// of what it means for an expression to be "used". +fn is_used(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + get_parent_expr(cx, expr).map_or(true, |parent| match parent.kind { + ExprKind::Assign(_, rhs, _) | ExprKind::AssignOp(_, _, rhs) => SpanlessEq::new(cx).eq_expr(rhs, expr), + _ => is_used(cx, parent), + }) +} + +/// Tests whether an expression is in a macro expansion (e.g., something +/// generated by `#[derive(...)]` or the like). +fn in_attributes_expansion(expr: &Expr<'_>) -> bool { + use rustc_span::hygiene::MacroKind; + if expr.span.from_expansion() { + let data = expr.span.ctxt().outer_expn_data(); + matches!(data.kind, ExpnKind::Macro(MacroKind::Attr | MacroKind::Derive, _)) + } else { + false + } +} + +/// Tests whether `res` is a variable defined outside a macro. +fn non_macro_local(cx: &LateContext<'_>, res: def::Res) -> bool { + if let def::Res::Local(id) = res { + !cx.tcx.hir().span(id).from_expansion() + } else { + false + } +} + +fn check_cast(cx: &LateContext<'_>, span: Span, e: &Expr<'_>, ty: &hir::Ty<'_>) { + if_chain! { + if let TyKind::Ptr(ref mut_ty) = ty.kind; + if let ExprKind::Lit(ref lit) = e.kind; + if let LitKind::Int(0, _) = lit.node; + if !in_constant(cx, e.hir_id); + then { + let (msg, sugg_fn) = match mut_ty.mutbl { + Mutability::Mut => ("`0 as *mut _` detected", "std::ptr::null_mut"), + Mutability::Not => ("`0 as *const _` detected", "std::ptr::null"), + }; + + let (sugg, appl) = if let TyKind::Infer = mut_ty.ty.kind { + (format!("{}()", sugg_fn), Applicability::MachineApplicable) + } else if let Some(mut_ty_snip) = snippet_opt(cx, mut_ty.ty.span) { + (format!("{}::<{}>()", sugg_fn, mut_ty_snip), Applicability::MachineApplicable) + } else { + // `MaybeIncorrect` as type inference may not work with the suggested code + (format!("{}()", sugg_fn), Applicability::MaybeIncorrect) + }; + span_lint_and_sugg(cx, ZERO_PTR, span, msg, "try", sugg, appl); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/builtin_type_shadow.rs b/src/tools/clippy/clippy_lints/src/misc_early/builtin_type_shadow.rs new file mode 100644 index 000000000..9f6b0bdc7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/builtin_type_shadow.rs @@ -0,0 +1,19 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast::{GenericParam, GenericParamKind}; +use rustc_hir::PrimTy; +use rustc_lint::EarlyContext; + +use super::BUILTIN_TYPE_SHADOW; + +pub(super) fn check(cx: &EarlyContext<'_>, param: &GenericParam) { + if let GenericParamKind::Type { .. } = param.kind { + if let Some(prim_ty) = PrimTy::from_name(param.ident.name) { + span_lint( + cx, + BUILTIN_TYPE_SHADOW, + param.ident.span, + &format!("this generic shadows the built-in type `{}`", prim_ty.name()), + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/double_neg.rs b/src/tools/clippy/clippy_lints/src/misc_early/double_neg.rs new file mode 100644 index 000000000..06ba968fa --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/double_neg.rs @@ -0,0 +1,18 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast::{Expr, ExprKind, UnOp}; +use rustc_lint::EarlyContext; + +use super::DOUBLE_NEG; + +pub(super) fn check(cx: &EarlyContext<'_>, expr: &Expr) { + if let ExprKind::Unary(UnOp::Neg, ref inner) = expr.kind { + if let ExprKind::Unary(UnOp::Neg, _) = inner.kind { + span_lint( + cx, + DOUBLE_NEG, + expr.span, + "`--x` could be misinterpreted as pre-decrement by C programmers, is usually a no-op", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/literal_suffix.rs b/src/tools/clippy/clippy_lints/src/misc_early/literal_suffix.rs new file mode 100644 index 000000000..1165c19a0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/literal_suffix.rs @@ -0,0 +1,38 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::ast::Lit; +use rustc_errors::Applicability; +use rustc_lint::EarlyContext; + +use super::{SEPARATED_LITERAL_SUFFIX, UNSEPARATED_LITERAL_SUFFIX}; + +pub(super) fn check(cx: &EarlyContext<'_>, lit: &Lit, lit_snip: &str, suffix: &str, sugg_type: &str) { + let maybe_last_sep_idx = if let Some(val) = lit_snip.len().checked_sub(suffix.len() + 1) { + val + } else { + return; // It's useless so shouldn't lint. + }; + // Do not lint when literal is unsuffixed. + if !suffix.is_empty() { + if lit_snip.as_bytes()[maybe_last_sep_idx] == b'_' { + span_lint_and_sugg( + cx, + SEPARATED_LITERAL_SUFFIX, + lit.span, + &format!("{} type suffix should not be separated by an underscore", sugg_type), + "remove the underscore", + format!("{}{}", &lit_snip[..maybe_last_sep_idx], suffix), + Applicability::MachineApplicable, + ); + } else { + span_lint_and_sugg( + cx, + UNSEPARATED_LITERAL_SUFFIX, + lit.span, + &format!("{} type suffix should be separated by an underscore", sugg_type), + "add an underscore", + format!("{}_{}", &lit_snip[..=maybe_last_sep_idx], suffix), + Applicability::MachineApplicable, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/mixed_case_hex_literals.rs b/src/tools/clippy/clippy_lints/src/misc_early/mixed_case_hex_literals.rs new file mode 100644 index 000000000..80e242131 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/mixed_case_hex_literals.rs @@ -0,0 +1,34 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast::Lit; +use rustc_lint::EarlyContext; + +use super::MIXED_CASE_HEX_LITERALS; + +pub(super) fn check(cx: &EarlyContext<'_>, lit: &Lit, suffix: &str, lit_snip: &str) { + let maybe_last_sep_idx = if let Some(val) = lit_snip.len().checked_sub(suffix.len() + 1) { + val + } else { + return; // It's useless so shouldn't lint. + }; + if maybe_last_sep_idx <= 2 { + // It's meaningless or causes range error. + return; + } + let mut seen = (false, false); + for ch in lit_snip.as_bytes()[2..=maybe_last_sep_idx].iter() { + match ch { + b'a'..=b'f' => seen.0 = true, + b'A'..=b'F' => seen.1 = true, + _ => {}, + } + if seen.0 && seen.1 { + span_lint( + cx, + MIXED_CASE_HEX_LITERALS, + lit.span, + "inconsistent casing in hexadecimal literal", + ); + break; + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/mod.rs b/src/tools/clippy/clippy_lints/src/misc_early/mod.rs new file mode 100644 index 000000000..704918c0b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/mod.rs @@ -0,0 +1,416 @@ +mod builtin_type_shadow; +mod double_neg; +mod literal_suffix; +mod mixed_case_hex_literals; +mod redundant_pattern; +mod unneeded_field_pattern; +mod unneeded_wildcard_pattern; +mod zero_prefixed_literal; + +use clippy_utils::diagnostics::span_lint; +use clippy_utils::source::snippet_opt; +use rustc_ast::ast::{Expr, ExprKind, Generics, Lit, LitFloatType, LitIntType, LitKind, NodeId, Pat, PatKind}; +use rustc_ast::visit::FnKind; +use rustc_data_structures::fx::FxHashMap; +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::source_map::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for structure field patterns bound to wildcards. + /// + /// ### Why is this bad? + /// Using `..` instead is shorter and leaves the focus on + /// the fields that are actually bound. + /// + /// ### Example + /// ```rust + /// # struct Foo { + /// # a: i32, + /// # b: i32, + /// # c: i32, + /// # } + /// let f = Foo { a: 0, b: 0, c: 0 }; + /// + /// match f { + /// Foo { a: _, b: 0, .. } => {}, + /// Foo { a: _, b: _, c: _ } => {}, + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # struct Foo { + /// # a: i32, + /// # b: i32, + /// # c: i32, + /// # } + /// let f = Foo { a: 0, b: 0, c: 0 }; + /// + /// match f { + /// Foo { b: 0, .. } => {}, + /// Foo { .. } => {}, + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNNEEDED_FIELD_PATTERN, + restriction, + "struct fields bound to a wildcard instead of using `..`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for function arguments having the similar names + /// differing by an underscore. + /// + /// ### Why is this bad? + /// It affects code readability. + /// + /// ### Example + /// ```rust + /// fn foo(a: i32, _a: i32) {} + /// ``` + /// + /// Use instead: + /// ```rust + /// fn bar(a: i32, _b: i32) {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DUPLICATE_UNDERSCORE_ARGUMENT, + style, + "function arguments having names which only differ by an underscore" +} + +declare_clippy_lint! { + /// ### What it does + /// Detects expressions of the form `--x`. + /// + /// ### Why is this bad? + /// It can mislead C/C++ programmers to think `x` was + /// decremented. + /// + /// ### Example + /// ```rust + /// let mut x = 3; + /// --x; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DOUBLE_NEG, + style, + "`--x`, which is a double negation of `x` and not a pre-decrement as in C/C++" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns on hexadecimal literals with mixed-case letter + /// digits. + /// + /// ### Why is this bad? + /// It looks confusing. + /// + /// ### Example + /// ```rust + /// # let _ = + /// 0x1a9BAcD + /// # ; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let _ = + /// 0x1A9BACD + /// # ; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MIXED_CASE_HEX_LITERALS, + style, + "hex literals whose letter digits are not consistently upper- or lowercased" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if literal suffixes are not separated by an + /// underscore. + /// To enforce unseparated literal suffix style, + /// see the `separated_literal_suffix` lint. + /// + /// ### Why is this bad? + /// Suffix style should be consistent. + /// + /// ### Example + /// ```rust + /// # let _ = + /// 123832i32 + /// # ; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let _ = + /// 123832_i32 + /// # ; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNSEPARATED_LITERAL_SUFFIX, + restriction, + "literals whose suffix is not separated by an underscore" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if literal suffixes are separated by an underscore. + /// To enforce separated literal suffix style, + /// see the `unseparated_literal_suffix` lint. + /// + /// ### Why is this bad? + /// Suffix style should be consistent. + /// + /// ### Example + /// ```rust + /// # let _ = + /// 123832_i32 + /// # ; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let _ = + /// 123832i32 + /// # ; + /// ``` + #[clippy::version = "1.58.0"] + pub SEPARATED_LITERAL_SUFFIX, + restriction, + "literals whose suffix is separated by an underscore" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if an integral constant literal starts with `0`. + /// + /// ### Why is this bad? + /// In some languages (including the infamous C language + /// and most of its + /// family), this marks an octal constant. In Rust however, this is a decimal + /// constant. This could + /// be confusing for both the writer and a reader of the constant. + /// + /// ### Example + /// + /// In Rust: + /// ```rust + /// fn main() { + /// let a = 0123; + /// println!("{}", a); + /// } + /// ``` + /// + /// prints `123`, while in C: + /// + /// ```c + /// #include <stdio.h> + /// + /// int main() { + /// int a = 0123; + /// printf("%d\n", a); + /// } + /// ``` + /// + /// prints `83` (as `83 == 0o123` while `123 == 0o173`). + #[clippy::version = "pre 1.29.0"] + pub ZERO_PREFIXED_LITERAL, + complexity, + "integer literals starting with `0`" +} + +declare_clippy_lint! { + /// ### What it does + /// Warns if a generic shadows a built-in type. + /// + /// ### Why is this bad? + /// This gives surprising type errors. + /// + /// ### Example + /// + /// ```ignore + /// impl<u32> Foo<u32> { + /// fn impl_func(&self) -> u32 { + /// 42 + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub BUILTIN_TYPE_SHADOW, + style, + "shadowing a builtin type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for patterns in the form `name @ _`. + /// + /// ### Why is this bad? + /// It's almost always more readable to just use direct + /// bindings. + /// + /// ### Example + /// ```rust + /// # let v = Some("abc"); + /// match v { + /// Some(x) => (), + /// y @ _ => (), + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let v = Some("abc"); + /// match v { + /// Some(x) => (), + /// y => (), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub REDUNDANT_PATTERN, + style, + "using `name @ _` in a pattern" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for tuple patterns with a wildcard + /// pattern (`_`) is next to a rest pattern (`..`). + /// + /// _NOTE_: While `_, ..` means there is at least one element left, `..` + /// means there are 0 or more elements left. This can make a difference + /// when refactoring, but shouldn't result in errors in the refactored code, + /// since the wildcard pattern isn't used anyway. + /// + /// ### Why is this bad? + /// The wildcard pattern is unneeded as the rest pattern + /// can match that element as well. + /// + /// ### Example + /// ```rust + /// # struct TupleStruct(u32, u32, u32); + /// # let t = TupleStruct(1, 2, 3); + /// match t { + /// TupleStruct(0, .., _) => (), + /// _ => (), + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # struct TupleStruct(u32, u32, u32); + /// # let t = TupleStruct(1, 2, 3); + /// match t { + /// TupleStruct(0, ..) => (), + /// _ => (), + /// } + /// ``` + #[clippy::version = "1.40.0"] + pub UNNEEDED_WILDCARD_PATTERN, + complexity, + "tuple patterns with a wildcard pattern (`_`) is next to a rest pattern (`..`)" +} + +declare_lint_pass!(MiscEarlyLints => [ + UNNEEDED_FIELD_PATTERN, + DUPLICATE_UNDERSCORE_ARGUMENT, + DOUBLE_NEG, + MIXED_CASE_HEX_LITERALS, + UNSEPARATED_LITERAL_SUFFIX, + SEPARATED_LITERAL_SUFFIX, + ZERO_PREFIXED_LITERAL, + BUILTIN_TYPE_SHADOW, + REDUNDANT_PATTERN, + UNNEEDED_WILDCARD_PATTERN, +]); + +impl EarlyLintPass for MiscEarlyLints { + fn check_generics(&mut self, cx: &EarlyContext<'_>, gen: &Generics) { + for param in &gen.params { + builtin_type_shadow::check(cx, param); + } + } + + fn check_pat(&mut self, cx: &EarlyContext<'_>, pat: &Pat) { + unneeded_field_pattern::check(cx, pat); + redundant_pattern::check(cx, pat); + unneeded_wildcard_pattern::check(cx, pat); + } + + fn check_fn(&mut self, cx: &EarlyContext<'_>, fn_kind: FnKind<'_>, _: Span, _: NodeId) { + let mut registered_names: FxHashMap<String, Span> = FxHashMap::default(); + + for arg in &fn_kind.decl().inputs { + if let PatKind::Ident(_, ident, None) = arg.pat.kind { + let arg_name = ident.to_string(); + + if let Some(arg_name) = arg_name.strip_prefix('_') { + if let Some(correspondence) = registered_names.get(arg_name) { + span_lint( + cx, + DUPLICATE_UNDERSCORE_ARGUMENT, + *correspondence, + &format!( + "`{}` already exists, having another argument having almost the same \ + name makes code comprehension and documentation more difficult", + arg_name + ), + ); + } + } else { + registered_names.insert(arg_name, arg.pat.span); + } + } + } + } + + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if let ExprKind::Lit(ref lit) = expr.kind { + MiscEarlyLints::check_lit(cx, lit); + } + double_neg::check(cx, expr); + } +} + +impl MiscEarlyLints { + fn check_lit(cx: &EarlyContext<'_>, lit: &Lit) { + // We test if first character in snippet is a number, because the snippet could be an expansion + // from a built-in macro like `line!()` or a proc-macro like `#[wasm_bindgen]`. + // Note that this check also covers special case that `line!()` is eagerly expanded by compiler. + // See <https://github.com/rust-lang/rust-clippy/issues/4507> for a regression. + // FIXME: Find a better way to detect those cases. + let lit_snip = match snippet_opt(cx, lit.span) { + Some(snip) if snip.chars().next().map_or(false, |c| c.is_ascii_digit()) => snip, + _ => return, + }; + + if let LitKind::Int(value, lit_int_type) = lit.kind { + let suffix = match lit_int_type { + LitIntType::Signed(ty) => ty.name_str(), + LitIntType::Unsigned(ty) => ty.name_str(), + LitIntType::Unsuffixed => "", + }; + literal_suffix::check(cx, lit, &lit_snip, suffix, "integer"); + if lit_snip.starts_with("0x") { + mixed_case_hex_literals::check(cx, lit, suffix, &lit_snip); + } else if lit_snip.starts_with("0b") || lit_snip.starts_with("0o") { + // nothing to do + } else if value != 0 && lit_snip.starts_with('0') { + zero_prefixed_literal::check(cx, lit, &lit_snip); + } + } else if let LitKind::Float(_, LitFloatType::Suffixed(float_ty)) = lit.kind { + let suffix = float_ty.name_str(); + literal_suffix::check(cx, lit, &lit_snip, suffix, "float"); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/redundant_pattern.rs b/src/tools/clippy/clippy_lints/src/misc_early/redundant_pattern.rs new file mode 100644 index 000000000..525dbf775 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/redundant_pattern.rs @@ -0,0 +1,31 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::ast::{BindingMode, Mutability, Pat, PatKind}; +use rustc_errors::Applicability; +use rustc_lint::EarlyContext; + +use super::REDUNDANT_PATTERN; + +pub(super) fn check(cx: &EarlyContext<'_>, pat: &Pat) { + if let PatKind::Ident(left, ident, Some(ref right)) = pat.kind { + let left_binding = match left { + BindingMode::ByRef(Mutability::Mut) => "ref mut ", + BindingMode::ByRef(Mutability::Not) => "ref ", + BindingMode::ByValue(..) => "", + }; + + if let PatKind::Wild = right.kind { + span_lint_and_sugg( + cx, + REDUNDANT_PATTERN, + pat.span, + &format!( + "the `{} @ _` pattern can be written as just `{}`", + ident.name, ident.name, + ), + "try", + format!("{}{}", left_binding, ident.name), + Applicability::MachineApplicable, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/unneeded_field_pattern.rs b/src/tools/clippy/clippy_lints/src/misc_early/unneeded_field_pattern.rs new file mode 100644 index 000000000..fff533167 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/unneeded_field_pattern.rs @@ -0,0 +1,73 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +use clippy_utils::source::snippet_opt; +use rustc_ast::ast::{Pat, PatKind}; +use rustc_lint::EarlyContext; + +use super::UNNEEDED_FIELD_PATTERN; + +pub(super) fn check(cx: &EarlyContext<'_>, pat: &Pat) { + if let PatKind::Struct(_, ref npat, ref pfields, _) = pat.kind { + let mut wilds = 0; + let type_name = npat + .segments + .last() + .expect("A path must have at least one segment") + .ident + .name; + + for field in pfields { + if let PatKind::Wild = field.pat.kind { + wilds += 1; + } + } + if !pfields.is_empty() && wilds == pfields.len() { + span_lint_and_help( + cx, + UNNEEDED_FIELD_PATTERN, + pat.span, + "all the struct fields are matched to a wildcard pattern, consider using `..`", + None, + &format!("try with `{} {{ .. }}` instead", type_name), + ); + return; + } + if wilds > 0 { + for field in pfields { + if let PatKind::Wild = field.pat.kind { + wilds -= 1; + if wilds > 0 { + span_lint( + cx, + UNNEEDED_FIELD_PATTERN, + field.span, + "you matched a field with a wildcard pattern, consider using `..` instead", + ); + } else { + let mut normal = vec![]; + + for field in pfields { + match field.pat.kind { + PatKind::Wild => {}, + _ => { + if let Some(n) = snippet_opt(cx, field.span) { + normal.push(n); + } + }, + } + } + + span_lint_and_help( + cx, + UNNEEDED_FIELD_PATTERN, + field.span, + "you matched a field with a wildcard pattern, consider using `..` \ + instead", + None, + &format!("try with `{} {{ {}, .. }}`", type_name, normal[..].join(", ")), + ); + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/unneeded_wildcard_pattern.rs b/src/tools/clippy/clippy_lints/src/misc_early/unneeded_wildcard_pattern.rs new file mode 100644 index 000000000..df044538f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/unneeded_wildcard_pattern.rs @@ -0,0 +1,52 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::ast::{Pat, PatKind}; +use rustc_errors::Applicability; +use rustc_lint::EarlyContext; +use rustc_span::source_map::Span; + +use super::UNNEEDED_WILDCARD_PATTERN; + +pub(super) fn check(cx: &EarlyContext<'_>, pat: &Pat) { + if let PatKind::TupleStruct(_, _, ref patterns) | PatKind::Tuple(ref patterns) = pat.kind { + if let Some(rest_index) = patterns.iter().position(|pat| pat.is_rest()) { + if let Some((left_index, left_pat)) = patterns[..rest_index] + .iter() + .rev() + .take_while(|pat| matches!(pat.kind, PatKind::Wild)) + .enumerate() + .last() + { + span_lint(cx, left_pat.span.until(patterns[rest_index].span), left_index == 0); + } + + if let Some((right_index, right_pat)) = patterns[rest_index + 1..] + .iter() + .take_while(|pat| matches!(pat.kind, PatKind::Wild)) + .enumerate() + .last() + { + span_lint( + cx, + patterns[rest_index].span.shrink_to_hi().to(right_pat.span), + right_index == 0, + ); + } + } + } +} + +fn span_lint(cx: &EarlyContext<'_>, span: Span, only_one: bool) { + span_lint_and_sugg( + cx, + UNNEEDED_WILDCARD_PATTERN, + span, + if only_one { + "this pattern is unneeded as the `..` pattern can match that element" + } else { + "these patterns are unneeded as the `..` pattern can match those elements" + }, + if only_one { "remove it" } else { "remove them" }, + "".to_string(), + Applicability::MachineApplicable, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/misc_early/zero_prefixed_literal.rs b/src/tools/clippy/clippy_lints/src/misc_early/zero_prefixed_literal.rs new file mode 100644 index 000000000..4963bba82 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/misc_early/zero_prefixed_literal.rs @@ -0,0 +1,29 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use rustc_ast::ast::Lit; +use rustc_errors::Applicability; +use rustc_lint::EarlyContext; + +use super::ZERO_PREFIXED_LITERAL; + +pub(super) fn check(cx: &EarlyContext<'_>, lit: &Lit, lit_snip: &str) { + span_lint_and_then( + cx, + ZERO_PREFIXED_LITERAL, + lit.span, + "this is a decimal constant", + |diag| { + diag.span_suggestion( + lit.span, + "if you mean to use a decimal constant, remove the `0` to avoid confusion", + lit_snip.trim_start_matches(|c| c == '_' || c == '0').to_string(), + Applicability::MaybeIncorrect, + ); + diag.span_suggestion( + lit.span, + "if you mean to use an octal constant, use `0o`", + format!("0o{}", lit_snip.trim_start_matches(|c| c == '_' || c == '0')), + Applicability::MaybeIncorrect, + ); + }, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/mismatching_type_param_order.rs b/src/tools/clippy/clippy_lints/src/mismatching_type_param_order.rs new file mode 100644 index 000000000..f763e0d24 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mismatching_type_param_order.rs @@ -0,0 +1,122 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{GenericArg, Item, ItemKind, QPath, Ty, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::GenericParamDefKind; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for type parameters which are positioned inconsistently between + /// a type definition and impl block. Specifically, a parameter in an impl + /// block which has the same name as a parameter in the type def, but is in + /// a different place. + /// + /// ### Why is this bad? + /// Type parameters are determined by their position rather than name. + /// Naming type parameters inconsistently may cause you to refer to the + /// wrong type parameter. + /// + /// ### Limitations + /// This lint only applies to impl blocks with simple generic params, e.g. + /// `A`. If there is anything more complicated, such as a tuple, it will be + /// ignored. + /// + /// ### Example + /// ```rust + /// struct Foo<A, B> { + /// x: A, + /// y: B, + /// } + /// // inside the impl, B refers to Foo::A + /// impl<B, A> Foo<B, A> {} + /// ``` + /// Use instead: + /// ```rust + /// struct Foo<A, B> { + /// x: A, + /// y: B, + /// } + /// impl<A, B> Foo<A, B> {} + /// ``` + #[clippy::version = "1.62.0"] + pub MISMATCHING_TYPE_PARAM_ORDER, + pedantic, + "type parameter positioned inconsistently between type def and impl block" +} +declare_lint_pass!(TypeParamMismatch => [MISMATCHING_TYPE_PARAM_ORDER]); + +impl<'tcx> LateLintPass<'tcx> for TypeParamMismatch { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if_chain! { + if !item.span.from_expansion(); + if let ItemKind::Impl(imp) = &item.kind; + if let TyKind::Path(QPath::Resolved(_, path)) = &imp.self_ty.kind; + if let Some(segment) = path.segments.iter().next(); + if let Some(generic_args) = segment.args; + if !generic_args.args.is_empty(); + 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() { + match p { + GenericArg::Type(Ty {kind: TyKind::Path(QPath::Resolved(_, path)), ..}) => + impl_params.push((path.segments[0].ident.to_string(), path.span)), + GenericArg::Type(_) => return, + _ => (), + }; + } + + // find the type that the Impl is for + // only lint on struct/enum/union for now + let defid = match path.res { + Res::Def(DefKind::Struct | DefKind::Enum | DefKind::Union, defid) => defid, + _ => return, + }; + + // get the names of the generic parameters in the type + let type_params = &cx.tcx.generics_of(defid).params; + let type_param_names: Vec<_> = type_params.iter() + .filter_map(|p| + match p.kind { + GenericParamDefKind::Type {..} => Some(p.name.to_string()), + _ => None, + } + ).collect(); + // hashmap of name -> index for mismatch_param_name + let type_param_names_hashmap: FxHashMap<&String, usize> = + type_param_names.iter().enumerate().map(|(i, param)| (param, i)).collect(); + + let type_name = segment.ident; + for (i, (impl_param_name, impl_param_span)) in impl_params.iter().enumerate() { + if mismatch_param_name(i, impl_param_name, &type_param_names_hashmap) { + let msg = format!("`{}` has a similarly named generic type parameter `{}` in its declaration, but in a different order", + type_name, impl_param_name); + let help = format!("try `{}`, or a name that does not conflict with `{}`'s generic params", + type_param_names[i], type_name); + span_lint_and_help( + cx, + MISMATCHING_TYPE_PARAM_ORDER, + *impl_param_span, + &msg, + None, + &help + ); + } + } + } + } + } +} + +// Checks if impl_param_name is the same as one of type_param_names, +// and is in a different position +fn mismatch_param_name(i: usize, impl_param_name: &String, type_param_names: &FxHashMap<&String, usize>) -> bool { + if let Some(j) = type_param_names.get(impl_param_name) { + if i != *j { + return true; + } + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/missing_const_for_fn.rs b/src/tools/clippy/clippy_lints/src/missing_const_for_fn.rs new file mode 100644 index 000000000..16d65966c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/missing_const_for_fn.rs @@ -0,0 +1,174 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::qualify_min_const_fn::is_min_const_fn; +use clippy_utils::ty::has_drop; +use clippy_utils::{fn_has_unsatisfiable_preds, is_entrypoint_fn, meets_msrv, msrvs, trait_ref_of_method}; +use rustc_hir as hir; +use rustc_hir::def_id::CRATE_DEF_ID; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{Body, Constness, FnDecl, GenericParamKind, HirId}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; +use rustc_typeck::hir_ty_to_ty; + +declare_clippy_lint! { + /// ### What it does + /// Suggests the use of `const` in functions and methods where possible. + /// + /// ### Why is this bad? + /// Not having the function const prevents callers of the function from being const as well. + /// + /// ### Known problems + /// Const functions are currently still being worked on, with some features only being available + /// on nightly. This lint does not consider all edge cases currently and the suggestions may be + /// incorrect if you are using this lint on stable. + /// + /// Also, the lint only runs one pass over the code. Consider these two non-const functions: + /// + /// ```rust + /// fn a() -> i32 { + /// 0 + /// } + /// fn b() -> i32 { + /// a() + /// } + /// ``` + /// + /// When running Clippy, the lint will only suggest to make `a` const, because `b` at this time + /// can't be const as it calls a non-const function. Making `a` const and running Clippy again, + /// will suggest to make `b` const, too. + /// + /// ### Example + /// ```rust + /// # struct Foo { + /// # random_number: usize, + /// # } + /// # impl Foo { + /// fn new() -> Self { + /// Self { random_number: 42 } + /// } + /// # } + /// ``` + /// + /// Could be a const fn: + /// + /// ```rust + /// # struct Foo { + /// # random_number: usize, + /// # } + /// # impl Foo { + /// const fn new() -> Self { + /// Self { random_number: 42 } + /// } + /// # } + /// ``` + #[clippy::version = "1.34.0"] + pub MISSING_CONST_FOR_FN, + nursery, + "Lint functions definitions that could be made `const fn`" +} + +impl_lint_pass!(MissingConstForFn => [MISSING_CONST_FOR_FN]); + +pub struct MissingConstForFn { + msrv: Option<RustcVersion>, +} + +impl MissingConstForFn { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl<'tcx> LateLintPass<'tcx> for MissingConstForFn { + fn check_fn( + &mut self, + cx: &LateContext<'_>, + kind: FnKind<'_>, + _: &FnDecl<'_>, + _: &Body<'_>, + span: Span, + hir_id: HirId, + ) { + if !meets_msrv(self.msrv, msrvs::CONST_IF_MATCH) { + return; + } + + let def_id = cx.tcx.hir().local_def_id(hir_id); + + if in_external_macro(cx.tcx.sess, span) || is_entrypoint_fn(cx, def_id.to_def_id()) { + return; + } + + // Building MIR for `fn`s with unsatisfiable preds results in ICE. + if fn_has_unsatisfiable_preds(cx, def_id.to_def_id()) { + return; + } + + // Perform some preliminary checks that rule out constness on the Clippy side. This way we + // can skip the actual const check and return early. + match kind { + FnKind::ItemFn(_, generics, header, ..) => { + let has_const_generic_params = generics + .params + .iter() + .any(|param| matches!(param.kind, GenericParamKind::Const { .. })); + + if already_const(header) || has_const_generic_params { + return; + } + }, + FnKind::Method(_, sig, ..) => { + if trait_ref_of_method(cx, def_id).is_some() + || already_const(sig.header) + || method_accepts_dropable(cx, sig.decl.inputs) + { + return; + } + }, + FnKind::Closure => return, + } + + // Const fns are not allowed as methods in a trait. + { + let parent = cx.tcx.hir().get_parent_item(hir_id); + if parent != CRATE_DEF_ID { + if let hir::Node::Item(item) = cx.tcx.hir().get_by_def_id(parent) { + if let hir::ItemKind::Trait(..) = &item.kind { + return; + } + } + } + } + + let mir = cx.tcx.optimized_mir(def_id); + + if let Err((span, err)) = is_min_const_fn(cx.tcx, mir, self.msrv) { + if cx.tcx.is_const_fn_raw(def_id.to_def_id()) { + cx.tcx.sess.span_err(span, err.as_ref()); + } + } else { + span_lint(cx, MISSING_CONST_FOR_FN, span, "this could be a `const fn`"); + } + } + extract_msrv_attr!(LateContext); +} + +/// Returns true if any of the method parameters is a type that implements `Drop`. The method +/// can't be made const then, because `drop` can't be const-evaluated. +fn method_accepts_dropable(cx: &LateContext<'_>, param_tys: &[hir::Ty<'_>]) -> bool { + // If any of the params are droppable, return true + param_tys.iter().any(|hir_ty| { + let ty_ty = hir_ty_to_ty(cx.tcx, hir_ty); + has_drop(cx, ty_ty) + }) +} + +// We don't have to lint on something that's already `const` +#[must_use] +fn already_const(header: hir::FnHeader) -> bool { + header.constness == Constness::Const +} diff --git a/src/tools/clippy/clippy_lints/src/missing_doc.rs b/src/tools/clippy/clippy_lints/src/missing_doc.rs new file mode 100644 index 000000000..88ba00292 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/missing_doc.rs @@ -0,0 +1,180 @@ +// Note: More specifically this lint is largely inspired (aka copied) from +// *rustc*'s +// [`missing_doc`]. +// +// [`missing_doc`]: https://github.com/rust-lang/rust/blob/cf9cf7c923eb01146971429044f216a3ca905e06/compiler/rustc_lint/src/builtin.rs#L415 +// + +use clippy_utils::attrs::is_doc_hidden; +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::ty::DefIdTree; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::def_id::CRATE_DEF_ID; +use rustc_span::source_map::Span; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Warns if there is missing doc for any documentable item + /// (public or private). + /// + /// ### Why is this bad? + /// Doc is good. *rustc* has a `MISSING_DOCS` + /// allowed-by-default lint for + /// public members, but has no way to enforce documentation of private items. + /// This lint fixes that. + #[clippy::version = "pre 1.29.0"] + pub MISSING_DOCS_IN_PRIVATE_ITEMS, + restriction, + "detects missing documentation for public and private members" +} + +pub struct MissingDoc { + /// Stack of whether #[doc(hidden)] is set + /// at each level which has lint attributes. + doc_hidden_stack: Vec<bool>, +} + +impl Default for MissingDoc { + #[must_use] + fn default() -> Self { + Self::new() + } +} + +impl MissingDoc { + #[must_use] + pub fn new() -> Self { + Self { + doc_hidden_stack: vec![false], + } + } + + fn doc_hidden(&self) -> bool { + *self.doc_hidden_stack.last().expect("empty doc_hidden_stack") + } + + fn check_missing_docs_attrs( + &self, + cx: &LateContext<'_>, + attrs: &[ast::Attribute], + sp: Span, + article: &'static str, + desc: &'static str, + ) { + // If we're building a test harness, then warning about + // documentation is probably not really relevant right now. + if cx.sess().opts.test { + return; + } + + // `#[doc(hidden)]` disables missing_docs check. + if self.doc_hidden() { + return; + } + + if sp.from_expansion() { + return; + } + + let has_doc = attrs.iter().any(|a| a.doc_str().is_some()); + if !has_doc { + span_lint( + cx, + MISSING_DOCS_IN_PRIVATE_ITEMS, + sp, + &format!("missing documentation for {} {}", article, desc), + ); + } + } +} + +impl_lint_pass!(MissingDoc => [MISSING_DOCS_IN_PRIVATE_ITEMS]); + +impl<'tcx> LateLintPass<'tcx> for MissingDoc { + fn enter_lint_attrs(&mut self, _: &LateContext<'tcx>, attrs: &'tcx [ast::Attribute]) { + let doc_hidden = self.doc_hidden() || is_doc_hidden(attrs); + self.doc_hidden_stack.push(doc_hidden); + } + + fn exit_lint_attrs(&mut self, _: &LateContext<'tcx>, _: &'tcx [ast::Attribute]) { + self.doc_hidden_stack.pop().expect("empty doc_hidden_stack"); + } + + fn check_crate(&mut self, cx: &LateContext<'tcx>) { + let attrs = cx.tcx.hir().attrs(hir::CRATE_HIR_ID); + self.check_missing_docs_attrs(cx, attrs, cx.tcx.def_span(CRATE_DEF_ID), "the", "crate"); + } + + fn check_item(&mut self, cx: &LateContext<'tcx>, it: &'tcx hir::Item<'_>) { + match it.kind { + hir::ItemKind::Fn(..) => { + // ignore main() + if it.ident.name == sym::main { + let at_root = cx.tcx.local_parent(it.def_id) == CRATE_DEF_ID; + if at_root { + return; + } + } + }, + hir::ItemKind::Const(..) + | hir::ItemKind::Enum(..) + | hir::ItemKind::Macro(..) + | hir::ItemKind::Mod(..) + | hir::ItemKind::Static(..) + | hir::ItemKind::Struct(..) + | hir::ItemKind::Trait(..) + | hir::ItemKind::TraitAlias(..) + | hir::ItemKind::TyAlias(..) + | hir::ItemKind::Union(..) + | hir::ItemKind::OpaqueTy(..) => {}, + hir::ItemKind::ExternCrate(..) + | hir::ItemKind::ForeignMod { .. } + | hir::ItemKind::GlobalAsm(..) + | hir::ItemKind::Impl { .. } + | hir::ItemKind::Use(..) => return, + }; + + let (article, desc) = cx.tcx.article_and_description(it.def_id.to_def_id()); + + let attrs = cx.tcx.hir().attrs(it.hir_id()); + self.check_missing_docs_attrs(cx, attrs, it.span, article, desc); + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, trait_item: &'tcx hir::TraitItem<'_>) { + let (article, desc) = cx.tcx.article_and_description(trait_item.def_id.to_def_id()); + + let attrs = cx.tcx.hir().attrs(trait_item.hir_id()); + self.check_missing_docs_attrs(cx, attrs, trait_item.span, article, desc); + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx hir::ImplItem<'_>) { + // If the method is an impl for a trait, don't doc. + if let Some(cid) = cx.tcx.associated_item(impl_item.def_id).impl_container(cx.tcx) { + if cx.tcx.impl_trait_ref(cid).is_some() { + return; + } + } else { + return; + } + + let (article, desc) = cx.tcx.article_and_description(impl_item.def_id.to_def_id()); + let attrs = cx.tcx.hir().attrs(impl_item.hir_id()); + self.check_missing_docs_attrs(cx, attrs, impl_item.span, article, desc); + } + + fn check_field_def(&mut self, cx: &LateContext<'tcx>, sf: &'tcx hir::FieldDef<'_>) { + if !sf.is_positional() { + let attrs = cx.tcx.hir().attrs(sf.hir_id); + self.check_missing_docs_attrs(cx, attrs, sf.span, "a", "struct field"); + } + } + + fn check_variant(&mut self, cx: &LateContext<'tcx>, v: &'tcx hir::Variant<'_>) { + let attrs = cx.tcx.hir().attrs(v.id); + self.check_missing_docs_attrs(cx, attrs, v.span, "a", "variant"); + } +} diff --git a/src/tools/clippy/clippy_lints/src/missing_enforced_import_rename.rs b/src/tools/clippy/clippy_lints/src/missing_enforced_import_rename.rs new file mode 100644 index 000000000..3d0a23822 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/missing_enforced_import_rename.rs @@ -0,0 +1,102 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet_opt}; + +use rustc_data_structures::fx::FxHashMap; +use rustc_errors::Applicability; +use rustc_hir::{def::Res, def_id::DefId, Item, ItemKind, UseKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Symbol; + +use crate::utils::conf::Rename; + +declare_clippy_lint! { + /// ### What it does + /// Checks for imports that do not rename the item as specified + /// in the `enforce-import-renames` config option. + /// + /// ### Why is this bad? + /// Consistency is important, if a project has defined import + /// renames they should be followed. More practically, some item names are too + /// vague outside of their defining scope this can enforce a more meaningful naming. + /// + /// ### Example + /// An example clippy.toml configuration: + /// ```toml + /// # clippy.toml + /// enforced-import-renames = [ { path = "serde_json::Value", rename = "JsonValue" }] + /// ``` + /// + /// ```rust,ignore + /// use serde_json::Value; + /// ``` + /// Use instead: + /// ```rust,ignore + /// use serde_json::Value as JsonValue; + /// ``` + #[clippy::version = "1.55.0"] + pub MISSING_ENFORCED_IMPORT_RENAMES, + restriction, + "enforce import renames" +} + +pub struct ImportRename { + conf_renames: Vec<Rename>, + renames: FxHashMap<DefId, Symbol>, +} + +impl ImportRename { + pub fn new(conf_renames: Vec<Rename>) -> Self { + Self { + conf_renames, + renames: FxHashMap::default(), + } + } +} + +impl_lint_pass!(ImportRename => [MISSING_ENFORCED_IMPORT_RENAMES]); + +impl LateLintPass<'_> for ImportRename { + fn check_crate(&mut self, cx: &LateContext<'_>) { + for Rename { path, rename } in &self.conf_renames { + if let Res::Def(_, id) = clippy_utils::def_path_res(cx, &path.split("::").collect::<Vec<_>>()) { + self.renames.insert(id, Symbol::intern(rename)); + } + } + } + + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if_chain! { + if let ItemKind::Use(path, UseKind::Single) = &item.kind; + if let Res::Def(_, id) = path.res; + if let Some(name) = self.renames.get(&id); + // Remove semicolon since it is not present for nested imports + let span_without_semi = cx.sess().source_map().span_until_char(item.span, ';'); + if let Some(snip) = snippet_opt(cx, span_without_semi); + if let Some(import) = match snip.split_once(" as ") { + None => Some(snip.as_str()), + Some((import, rename)) => { + if rename.trim() == name.as_str() { + None + } else { + Some(import.trim()) + } + }, + }; + then { + span_lint_and_sugg( + cx, + MISSING_ENFORCED_IMPORT_RENAMES, + span_without_semi, + "this import should be renamed", + "try", + format!( + "{} as {}", + import, + name, + ), + Applicability::MachineApplicable, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/missing_inline.rs b/src/tools/clippy/clippy_lints/src/missing_inline.rs new file mode 100644 index 000000000..07bc2ca5d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/missing_inline.rs @@ -0,0 +1,172 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast; +use rustc_hir as hir; +use rustc_lint::{self, LateContext, LateLintPass, LintContext}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// It lints if an exported function, method, trait method with default impl, + /// or trait method impl is not `#[inline]`. + /// + /// ### Why is this bad? + /// In general, it is not. Functions can be inlined across + /// crates when that's profitable as long as any form of LTO is used. When LTO is disabled, + /// functions that are not `#[inline]` cannot be inlined across crates. Certain types of crates + /// might intend for most of the methods in their public API to be able to be inlined across + /// crates even when LTO is disabled. For these types of crates, enabling this lint might make + /// sense. It allows the crate to require all exported methods to be `#[inline]` by default, and + /// then opt out for specific methods where this might not make sense. + /// + /// ### Example + /// ```rust + /// pub fn foo() {} // missing #[inline] + /// fn ok() {} // ok + /// #[inline] pub fn bar() {} // ok + /// #[inline(always)] pub fn baz() {} // ok + /// + /// pub trait Bar { + /// fn bar(); // ok + /// fn def_bar() {} // missing #[inline] + /// } + /// + /// struct Baz; + /// impl Baz { + /// fn private() {} // ok + /// } + /// + /// impl Bar for Baz { + /// fn bar() {} // ok - Baz is not exported + /// } + /// + /// pub struct PubBaz; + /// impl PubBaz { + /// fn private() {} // ok + /// pub fn not_private() {} // missing #[inline] + /// } + /// + /// impl Bar for PubBaz { + /// fn bar() {} // missing #[inline] + /// fn def_bar() {} // missing #[inline] + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MISSING_INLINE_IN_PUBLIC_ITEMS, + restriction, + "detects missing `#[inline]` attribute for public callables (functions, trait methods, methods...)" +} + +fn check_missing_inline_attrs(cx: &LateContext<'_>, attrs: &[ast::Attribute], sp: Span, desc: &'static str) { + let has_inline = attrs.iter().any(|a| a.has_name(sym::inline)); + if !has_inline { + span_lint( + cx, + MISSING_INLINE_IN_PUBLIC_ITEMS, + sp, + &format!("missing `#[inline]` for {}", desc), + ); + } +} + +fn is_executable_or_proc_macro(cx: &LateContext<'_>) -> bool { + use rustc_session::config::CrateType; + + cx.tcx + .sess + .crate_types() + .iter() + .any(|t: &CrateType| matches!(t, CrateType::Executable | CrateType::ProcMacro)) +} + +declare_lint_pass!(MissingInline => [MISSING_INLINE_IN_PUBLIC_ITEMS]); + +impl<'tcx> LateLintPass<'tcx> for MissingInline { + fn check_item(&mut self, cx: &LateContext<'tcx>, it: &'tcx hir::Item<'_>) { + if rustc_middle::lint::in_external_macro(cx.sess(), it.span) || is_executable_or_proc_macro(cx) { + return; + } + + if !cx.access_levels.is_exported(it.def_id) { + return; + } + match it.kind { + hir::ItemKind::Fn(..) => { + let desc = "a function"; + let attrs = cx.tcx.hir().attrs(it.hir_id()); + check_missing_inline_attrs(cx, attrs, it.span, desc); + }, + hir::ItemKind::Trait(ref _is_auto, ref _unsafe, _generics, _bounds, trait_items) => { + // note: we need to check if the trait is exported so we can't use + // `LateLintPass::check_trait_item` here. + for tit in trait_items { + let tit_ = cx.tcx.hir().trait_item(tit.id); + match tit_.kind { + hir::TraitItemKind::Const(..) | hir::TraitItemKind::Type(..) => {}, + hir::TraitItemKind::Fn(..) => { + if cx.tcx.impl_defaultness(tit.id.def_id).has_value() { + // trait method with default body needs inline in case + // an impl is not provided + let desc = "a default trait method"; + let item = cx.tcx.hir().trait_item(tit.id); + let attrs = cx.tcx.hir().attrs(item.hir_id()); + check_missing_inline_attrs(cx, attrs, item.span, desc); + } + }, + } + } + }, + hir::ItemKind::Const(..) + | hir::ItemKind::Enum(..) + | hir::ItemKind::Macro(..) + | hir::ItemKind::Mod(..) + | hir::ItemKind::Static(..) + | hir::ItemKind::Struct(..) + | hir::ItemKind::TraitAlias(..) + | hir::ItemKind::GlobalAsm(..) + | hir::ItemKind::TyAlias(..) + | hir::ItemKind::Union(..) + | hir::ItemKind::OpaqueTy(..) + | hir::ItemKind::ExternCrate(..) + | hir::ItemKind::ForeignMod { .. } + | hir::ItemKind::Impl { .. } + | hir::ItemKind::Use(..) => {}, + }; + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx hir::ImplItem<'_>) { + use rustc_middle::ty::{ImplContainer, TraitContainer}; + if rustc_middle::lint::in_external_macro(cx.sess(), impl_item.span) || is_executable_or_proc_macro(cx) { + return; + } + + // If the item being implemented is not exported, then we don't need #[inline] + if !cx.access_levels.is_exported(impl_item.def_id) { + return; + } + + let desc = match impl_item.kind { + hir::ImplItemKind::Fn(..) => "a method", + hir::ImplItemKind::Const(..) | hir::ImplItemKind::TyAlias(_) => return, + }; + + let assoc_item = cx.tcx.associated_item(impl_item.def_id); + let container_id = assoc_item.container_id(cx.tcx); + let trait_def_id = match assoc_item.container { + TraitContainer => Some(container_id), + ImplContainer => cx.tcx.impl_trait_ref(container_id).map(|t| t.def_id), + }; + + if let Some(trait_def_id) = trait_def_id { + if trait_def_id.is_local() && !cx.access_levels.is_exported(impl_item.def_id) { + // If a trait is being implemented for an item, and the + // trait is not exported, we don't need #[inline] + return; + } + } + + let attrs = cx.tcx.hir().attrs(impl_item.hir_id()); + check_missing_inline_attrs(cx, attrs, impl_item.span, desc); + } +} diff --git a/src/tools/clippy/clippy_lints/src/mixed_read_write_in_expression.rs b/src/tools/clippy/clippy_lints/src/mixed_read_write_in_expression.rs new file mode 100644 index 000000000..a2419c277 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mixed_read_write_in_expression.rs @@ -0,0 +1,350 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_note}; +use clippy_utils::{get_parent_expr, path_to_local, path_to_local_id}; +use if_chain::if_chain; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{BinOpKind, Block, Expr, ExprKind, Guard, HirId, Local, Node, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for a read and a write to the same variable where + /// whether the read occurs before or after the write depends on the evaluation + /// order of sub-expressions. + /// + /// ### Why is this bad? + /// It is often confusing to read. As described [here](https://doc.rust-lang.org/reference/expressions.html?highlight=subexpression#evaluation-order-of-operands), + /// the operands of these expressions are evaluated before applying the effects of the expression. + /// + /// ### Known problems + /// Code which intentionally depends on the evaluation + /// order, or which is correct for any evaluation order. + /// + /// ### Example + /// ```rust + /// let mut x = 0; + /// + /// let a = { + /// x = 1; + /// 1 + /// } + x; + /// // Unclear whether a is 1 or 2. + /// ``` + /// + /// Use instead: + /// ```rust + /// # let mut x = 0; + /// let tmp = { + /// x = 1; + /// 1 + /// }; + /// let a = tmp + x; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MIXED_READ_WRITE_IN_EXPRESSION, + restriction, + "whether a variable read occurs before a write depends on sub-expression evaluation order" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for diverging calls that are not match arms or + /// statements. + /// + /// ### Why is this bad? + /// It is often confusing to read. In addition, the + /// sub-expression evaluation order for Rust is not well documented. + /// + /// ### Known problems + /// Someone might want to use `some_bool || panic!()` as a + /// shorthand. + /// + /// ### Example + /// ```rust,no_run + /// # fn b() -> bool { true } + /// # fn c() -> bool { true } + /// let a = b() || panic!() || c(); + /// // `c()` is dead, `panic!()` is only called if `b()` returns `false` + /// let x = (a, b, c, panic!()); + /// // can simply be replaced by `panic!()` + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DIVERGING_SUB_EXPRESSION, + complexity, + "whether an expression contains a diverging sub expression" +} + +declare_lint_pass!(EvalOrderDependence => [MIXED_READ_WRITE_IN_EXPRESSION, DIVERGING_SUB_EXPRESSION]); + +impl<'tcx> LateLintPass<'tcx> for EvalOrderDependence { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // Find a write to a local variable. + let var = if_chain! { + if let ExprKind::Assign(lhs, ..) | ExprKind::AssignOp(_, lhs, _) = expr.kind; + if let Some(var) = path_to_local(lhs); + if expr.span.desugaring_kind().is_none(); + then { var } else { return; } + }; + let mut visitor = ReadVisitor { + cx, + var, + write_expr: expr, + last_expr: expr, + }; + check_for_unsequenced_reads(&mut visitor); + } + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + match stmt.kind { + StmtKind::Local(local) => { + if let Local { init: Some(e), .. } = local { + DivergenceVisitor { cx }.visit_expr(e); + } + }, + StmtKind::Expr(e) | StmtKind::Semi(e) => DivergenceVisitor { cx }.maybe_walk_expr(e), + StmtKind::Item(..) => {}, + } + } +} + +struct DivergenceVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> DivergenceVisitor<'a, 'tcx> { + fn maybe_walk_expr(&mut self, e: &'tcx Expr<'_>) { + match e.kind { + ExprKind::Closure { .. } => {}, + ExprKind::Match(e, arms, _) => { + self.visit_expr(e); + for arm in arms { + if let Some(Guard::If(if_expr)) = arm.guard { + self.visit_expr(if_expr); + } + // make sure top level arm expressions aren't linted + self.maybe_walk_expr(arm.body); + } + }, + _ => 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"); + } +} + +impl<'a, 'tcx> Visitor<'tcx> for DivergenceVisitor<'a, 'tcx> { + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + match e.kind { + ExprKind::Continue(_) | ExprKind::Break(_, _) | ExprKind::Ret(_) => self.report_diverging_sub_expr(e), + ExprKind::Call(func, _) => { + let typ = self.cx.typeck_results().expr_ty(func); + match typ.kind() { + ty::FnDef(..) | ty::FnPtr(_) => { + let sig = typ.fn_sig(self.cx.tcx); + if self.cx.tcx.erase_late_bound_regions(sig).output().kind() == &ty::Never { + self.report_diverging_sub_expr(e); + } + }, + _ => {}, + } + }, + ExprKind::MethodCall(..) => { + let borrowed_table = self.cx.typeck_results(); + if borrowed_table.expr_ty(e).is_never() { + self.report_diverging_sub_expr(e); + } + }, + _ => { + // do not lint expressions referencing objects of type `!`, as that required a + // diverging expression + // to begin with + }, + } + self.maybe_walk_expr(e); + } + fn visit_block(&mut self, _: &'tcx Block<'_>) { + // don't continue over blocks, LateLintPass already does that + } +} + +/// Walks up the AST from the given write expression (`vis.write_expr`) looking +/// for reads to the same variable that are unsequenced relative to the write. +/// +/// This means reads for which there is a common ancestor between the read and +/// the write such that +/// +/// * evaluating the ancestor necessarily evaluates both the read and the write (for example, `&x` +/// and `|| x = 1` don't necessarily evaluate `x`), and +/// +/// * which one is evaluated first depends on the order of sub-expression evaluation. Blocks, `if`s, +/// loops, `match`es, and the short-circuiting logical operators are considered to have a defined +/// evaluation order. +/// +/// When such a read is found, the lint is triggered. +fn check_for_unsequenced_reads(vis: &mut ReadVisitor<'_, '_>) { + let map = &vis.cx.tcx.hir(); + let mut cur_id = vis.write_expr.hir_id; + loop { + let parent_id = map.get_parent_node(cur_id); + if parent_id == cur_id { + break; + } + let parent_node = match map.find(parent_id) { + Some(parent) => parent, + None => break, + }; + + let stop_early = match parent_node { + Node::Expr(expr) => check_expr(vis, expr), + Node::Stmt(stmt) => check_stmt(vis, stmt), + Node::Item(_) => { + // We reached the top of the function, stop. + break; + }, + _ => StopEarly::KeepGoing, + }; + match stop_early { + StopEarly::Stop => break, + StopEarly::KeepGoing => {}, + } + + cur_id = parent_id; + } +} + +/// Whether to stop early for the loop in `check_for_unsequenced_reads`. (If +/// `check_expr` weren't an independent function, this would be unnecessary and +/// we could just use `break`). +enum StopEarly { + KeepGoing, + Stop, +} + +fn check_expr<'a, 'tcx>(vis: &mut ReadVisitor<'a, 'tcx>, expr: &'tcx Expr<'_>) -> StopEarly { + if expr.hir_id == vis.last_expr.hir_id { + return StopEarly::KeepGoing; + } + + match expr.kind { + ExprKind::Array(_) + | ExprKind::Tup(_) + | ExprKind::MethodCall(..) + | ExprKind::Call(_, _) + | ExprKind::Assign(..) + | ExprKind::Index(_, _) + | ExprKind::Repeat(_, _) + | ExprKind::Struct(_, _, _) => { + walk_expr(vis, expr); + }, + ExprKind::Binary(op, _, _) | ExprKind::AssignOp(op, _, _) => { + if op.node == BinOpKind::And || op.node == BinOpKind::Or { + // x && y and x || y always evaluate x first, so these are + // strictly sequenced. + } else { + walk_expr(vis, expr); + } + }, + ExprKind::Closure { .. } => { + // Either + // + // * `var` is defined in the closure body, in which case we've reached the top of the enclosing + // function and can stop, or + // + // * `var` is captured by the closure, in which case, because evaluating a closure does not evaluate + // its body, we don't necessarily have a write, so we need to stop to avoid generating false + // positives. + // + // This is also the only place we need to stop early (grrr). + return StopEarly::Stop; + }, + // All other expressions either have only one child or strictly + // sequence the evaluation order of their sub-expressions. + _ => {}, + } + + vis.last_expr = expr; + + StopEarly::KeepGoing +} + +fn check_stmt<'a, 'tcx>(vis: &mut ReadVisitor<'a, 'tcx>, stmt: &'tcx Stmt<'_>) -> StopEarly { + match stmt.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => check_expr(vis, expr), + // If the declaration is of a local variable, check its initializer + // expression if it has one. Otherwise, keep going. + StmtKind::Local(local) => local + .init + .as_ref() + .map_or(StopEarly::KeepGoing, |expr| check_expr(vis, expr)), + StmtKind::Item(..) => StopEarly::KeepGoing, + } +} + +/// A visitor that looks for reads from a variable. +struct ReadVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + /// The ID of the variable we're looking for. + var: HirId, + /// The expressions where the write to the variable occurred (for reporting + /// in the lint). + write_expr: &'tcx Expr<'tcx>, + /// The last (highest in the AST) expression we've checked, so we know not + /// to recheck it. + last_expr: &'tcx Expr<'tcx>, +} + +impl<'a, 'tcx> Visitor<'tcx> for ReadVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if expr.hir_id == self.last_expr.hir_id { + return; + } + + if path_to_local_id(expr, self.var) { + // Check that this is a read, not a write. + if !is_in_assignment_position(self.cx, expr) { + span_lint_and_note( + self.cx, + MIXED_READ_WRITE_IN_EXPRESSION, + expr.span, + &format!("unsequenced read of `{}`", self.cx.tcx.hir().name(self.var)), + Some(self.write_expr.span), + "whether read occurs before this write depends on evaluation order", + ); + } + } + match expr.kind { + // We're about to descend a closure. Since we don't know when (or + // if) the closure will be evaluated, any reads in it might not + // occur here (or ever). Like above, bail to avoid false positives. + ExprKind::Closure{..} | + + // We want to avoid a false positive when a variable name occurs + // only to have its address taken, so we stop here. Technically, + // this misses some weird cases, eg. + // + // ```rust + // let mut x = 0; + // let a = foo(&{x = 1; x}, x); + // ``` + // + // TODO: fix this + ExprKind::AddrOf(_, _, _) => { + return; + } + _ => {} + } + + walk_expr(self, expr); + } +} + +/// Returns `true` if `expr` is the LHS of an assignment, like `expr = ...`. +fn is_in_assignment_position(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + if let Some(parent) = get_parent_expr(cx, expr) { + if let ExprKind::Assign(lhs, ..) = parent.kind { + return lhs.hir_id == expr.hir_id; + } + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/module_style.rs b/src/tools/clippy/clippy_lints/src/module_style.rs new file mode 100644 index 000000000..0a3936572 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/module_style.rs @@ -0,0 +1,166 @@ +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::{FileName, RealFileName, SourceFile, Span, SyntaxContext}; +use std::ffi::OsStr; +use std::path::{Component, Path}; + +declare_clippy_lint! { + /// ### What it does + /// Checks that module layout uses only self named module files, bans `mod.rs` files. + /// + /// ### Why is this bad? + /// Having multiple module layout styles in a project can be confusing. + /// + /// ### Example + /// ```text + /// src/ + /// stuff/ + /// stuff_files.rs + /// mod.rs + /// lib.rs + /// ``` + /// Use instead: + /// ```text + /// src/ + /// stuff/ + /// stuff_files.rs + /// stuff.rs + /// lib.rs + /// ``` + #[clippy::version = "1.57.0"] + pub MOD_MODULE_FILES, + restriction, + "checks that module layout is consistent" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks that module layout uses only `mod.rs` files. + /// + /// ### Why is this bad? + /// Having multiple module layout styles in a project can be confusing. + /// + /// ### Example + /// ```text + /// src/ + /// stuff/ + /// stuff_files.rs + /// stuff.rs + /// lib.rs + /// ``` + /// Use instead: + /// ```text + /// src/ + /// stuff/ + /// stuff_files.rs + /// mod.rs + /// lib.rs + /// ``` + + #[clippy::version = "1.57.0"] + pub SELF_NAMED_MODULE_FILES, + restriction, + "checks that module layout is consistent" +} + +pub struct ModStyle; + +impl_lint_pass!(ModStyle => [MOD_MODULE_FILES, SELF_NAMED_MODULE_FILES]); + +impl EarlyLintPass for ModStyle { + fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &ast::Crate) { + if cx.builder.lint_level(MOD_MODULE_FILES).0 == Level::Allow + && cx.builder.lint_level(SELF_NAMED_MODULE_FILES).0 == Level::Allow + { + return; + } + + let files = cx.sess().source_map().files(); + + let RealFileName::LocalPath(trim_to_src) = &cx.sess().opts.working_dir else { return }; + + // `folder_segments` is all unique folder path segments `path/to/foo.rs` gives + // `[path, to]` but not foo + let mut folder_segments = FxHashSet::default(); + // `mod_folders` is all the unique folder names that contain a mod.rs file + let mut mod_folders = FxHashSet::default(); + // `file_map` maps file names to the full path including the file name + // `{ foo => path/to/foo.rs, .. } + let mut file_map = FxHashMap::default(); + for file in files.iter() { + if let FileName::Real(RealFileName::LocalPath(lp)) = &file.name { + let path = if lp.is_relative() { + lp + } else if let Ok(relative) = lp.strip_prefix(trim_to_src) { + relative + } else { + continue; + }; + + if let Some(stem) = path.file_stem() { + file_map.insert(stem, (file, path)); + } + process_paths_for_mod_files(path, &mut folder_segments, &mut mod_folders); + check_self_named_mod_exists(cx, path, file); + } + } + + for folder in &folder_segments { + if !mod_folders.contains(folder) { + if let Some((file, path)) = file_map.get(folder) { + let mut correct = path.to_path_buf(); + correct.pop(); + correct.push(folder); + correct.push("mod.rs"); + cx.struct_span_lint( + SELF_NAMED_MODULE_FILES, + Span::new(file.start_pos, file.start_pos, SyntaxContext::root(), None), + |build| { + let mut lint = + build.build(&format!("`mod.rs` files are required, found `{}`", path.display())); + lint.help(&format!("move `{}` to `{}`", path.display(), correct.display(),)); + lint.emit(); + }, + ); + } + } + } + } +} + +/// For each `path` we add each folder component to `folder_segments` and if the file name +/// is `mod.rs` we add it's parent folder to `mod_folders`. +fn process_paths_for_mod_files<'a>( + path: &'a Path, + folder_segments: &mut FxHashSet<&'a OsStr>, + mod_folders: &mut FxHashSet<&'a OsStr>, +) { + let mut comp = path.components().rev().peekable(); + let _ = comp.next(); + if path.ends_with("mod.rs") { + mod_folders.insert(comp.peek().map(|c| c.as_os_str()).unwrap_or_default()); + } + let folders = comp.filter_map(|c| if let Component::Normal(s) = c { Some(s) } else { None }); + folder_segments.extend(folders); +} + +/// Checks every path for the presence of `mod.rs` files and emits the lint if found. +fn check_self_named_mod_exists(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) { + if path.ends_with("mod.rs") { + let mut mod_file = path.to_path_buf(); + mod_file.pop(); + mod_file.set_extension("rs"); + + cx.struct_span_lint( + MOD_MODULE_FILES, + Span::new(file.start_pos, file.start_pos, SyntaxContext::root(), None), + |build| { + let mut lint = build.build(&format!("`mod.rs` files are not allowed, found `{}`", path.display())); + lint.help(&format!("move `{}` to `{}`", path.display(), mod_file.display(),)); + lint.emit(); + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/mut_key.rs b/src/tools/clippy/clippy_lints/src/mut_key.rs new file mode 100644 index 000000000..4db103bbc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mut_key.rs @@ -0,0 +1,175 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::trait_ref_of_method; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::TypeVisitable; +use rustc_middle::ty::{Adt, Array, Ref, Slice, Tuple, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::symbol::sym; +use std::iter; + +declare_clippy_lint! { + /// ### What it does + /// Checks for sets/maps with mutable key types. + /// + /// ### Why is this bad? + /// All of `HashMap`, `HashSet`, `BTreeMap` and + /// `BtreeSet` rely on either the hash or the order of keys be unchanging, + /// so having types with interior mutability is a bad idea. + /// + /// ### Known problems + /// + /// #### False Positives + /// It's correct to use a struct that contains interior mutability as a key, when its + /// implementation of `Hash` or `Ord` doesn't access any of the interior mutable types. + /// However, this lint is unable to recognize this, so it will often cause false positives in + /// theses cases. The `bytes` crate is a great example of this. + /// + /// #### False Negatives + /// For custom `struct`s/`enum`s, this lint is unable to check for interior mutability behind + /// indirection. For example, `struct BadKey<'a>(&'a Cell<usize>)` will be seen as immutable + /// and cause a false negative if its implementation of `Hash`/`Ord` accesses the `Cell`. + /// + /// This lint does check a few cases for indirection. Firstly, using some standard library + /// types (`Option`, `Result`, `Box`, `Rc`, `Arc`, `Vec`, `VecDeque`, `BTreeMap` and + /// `BTreeSet`) directly as keys (e.g. in `HashMap<Box<Cell<usize>>, ()>`) **will** trigger the + /// lint, because the impls of `Hash`/`Ord` for these types directly call `Hash`/`Ord` on their + /// contained type. + /// + /// Secondly, the implementations of `Hash` and `Ord` for raw pointers (`*const T` or `*mut T`) + /// apply only to the **address** of the contained value. Therefore, interior mutability + /// behind raw pointers (e.g. in `HashSet<*mut Cell<usize>>`) can't impact the value of `Hash` + /// or `Ord`, and therefore will not trigger this link. For more info, see issue + /// [#6745](https://github.com/rust-lang/rust-clippy/issues/6745). + /// + /// ### Example + /// ```rust + /// use std::cmp::{PartialEq, Eq}; + /// use std::collections::HashSet; + /// use std::hash::{Hash, Hasher}; + /// use std::sync::atomic::AtomicUsize; + ///# #[allow(unused)] + /// + /// struct Bad(AtomicUsize); + /// impl PartialEq for Bad { + /// fn eq(&self, rhs: &Self) -> bool { + /// .. + /// ; unimplemented!(); + /// } + /// } + /// + /// impl Eq for Bad {} + /// + /// impl Hash for Bad { + /// fn hash<H: Hasher>(&self, h: &mut H) { + /// .. + /// ; unimplemented!(); + /// } + /// } + /// + /// fn main() { + /// let _: HashSet<Bad> = HashSet::new(); + /// } + /// ``` + #[clippy::version = "1.42.0"] + pub MUTABLE_KEY_TYPE, + suspicious, + "Check for mutable `Map`/`Set` key type" +} + +declare_lint_pass!(MutableKeyType => [ MUTABLE_KEY_TYPE ]); + +impl<'tcx> LateLintPass<'tcx> for MutableKeyType { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) { + if let hir::ItemKind::Fn(ref sig, ..) = item.kind { + check_sig(cx, item.hir_id(), sig.decl); + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'tcx>) { + if let hir::ImplItemKind::Fn(ref sig, ..) = item.kind { + if trait_ref_of_method(cx, item.def_id).is_none() { + check_sig(cx, item.hir_id(), sig.decl); + } + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'tcx>) { + if let hir::TraitItemKind::Fn(ref sig, ..) = item.kind { + check_sig(cx, item.hir_id(), sig.decl); + } + } + + fn check_local(&mut self, cx: &LateContext<'_>, local: &hir::Local<'_>) { + if let hir::PatKind::Wild = local.pat.kind { + return; + } + check_ty(cx, local.span, cx.typeck_results().pat_ty(local.pat)); + } +} + +fn check_sig<'tcx>(cx: &LateContext<'tcx>, item_hir_id: hir::HirId, decl: &hir::FnDecl<'_>) { + let fn_def_id = cx.tcx.hir().local_def_id(item_hir_id); + let fn_sig = cx.tcx.fn_sig(fn_def_id); + for (hir_ty, ty) in iter::zip(decl.inputs, fn_sig.inputs().skip_binder()) { + check_ty(cx, hir_ty.span, *ty); + } + check_ty(cx, decl.output.span(), cx.tcx.erase_late_bound_regions(fn_sig.output())); +} + +// We want to lint 1. sets or maps with 2. not immutable key types and 3. no unerased +// generics (because the compiler cannot ensure immutability for unknown types). +fn check_ty<'tcx>(cx: &LateContext<'tcx>, span: Span, ty: Ty<'tcx>) { + let ty = ty.peel_refs(); + if let Adt(def, substs) = ty.kind() { + let is_keyed_type = [sym::HashMap, sym::BTreeMap, sym::HashSet, sym::BTreeSet] + .iter() + .any(|diag_item| cx.tcx.is_diagnostic_item(*diag_item, def.did())); + if is_keyed_type && is_interior_mutable_type(cx, substs.type_at(0), span) { + span_lint(cx, MUTABLE_KEY_TYPE, span, "mutable key type"); + } + } +} + +/// Determines if a type contains interior mutability which would affect its implementation of +/// [`Hash`] or [`Ord`]. +fn is_interior_mutable_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, span: Span) -> bool { + match *ty.kind() { + Ref(_, inner_ty, mutbl) => mutbl == hir::Mutability::Mut || is_interior_mutable_type(cx, inner_ty, span), + Slice(inner_ty) => is_interior_mutable_type(cx, inner_ty, span), + Array(inner_ty, size) => { + size.try_eval_usize(cx.tcx, cx.param_env).map_or(true, |u| u != 0) + && is_interior_mutable_type(cx, inner_ty, span) + }, + Tuple(fields) => fields.iter().any(|ty| is_interior_mutable_type(cx, ty, span)), + Adt(def, substs) => { + // Special case for collections in `std` who's impl of `Hash` or `Ord` delegates to + // that of their type parameters. Note: we don't include `HashSet` and `HashMap` + // because they have no impl for `Hash` or `Ord`. + let is_std_collection = [ + sym::Option, + sym::Result, + sym::LinkedList, + sym::Vec, + sym::VecDeque, + sym::BTreeMap, + sym::BTreeSet, + sym::Rc, + sym::Arc, + ] + .iter() + .any(|diag_item| cx.tcx.is_diagnostic_item(*diag_item, def.did())); + let is_box = Some(def.did()) == cx.tcx.lang_items().owned_box(); + if is_std_collection || is_box { + // The type is mutable if any of its type parameters are + substs.types().any(|ty| is_interior_mutable_type(cx, ty, span)) + } else { + !ty.has_escaping_bound_vars() + && cx.tcx.layout_of(cx.param_env.and(ty)).is_ok() + && !ty.is_freeze(cx.tcx.at(span), cx.param_env) + } + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/mut_mut.rs b/src/tools/clippy/clippy_lints/src/mut_mut.rs new file mode 100644 index 000000000..cb16f0004 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mut_mut.rs @@ -0,0 +1,114 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::higher; +use rustc_hir as hir; +use rustc_hir::intravisit; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for instances of `mut mut` references. + /// + /// ### Why is this bad? + /// Multiple `mut`s don't add anything meaningful to the + /// source. This is either a copy'n'paste error, or it shows a fundamental + /// misunderstanding of references. + /// + /// ### Example + /// ```rust + /// # let mut y = 1; + /// let x = &mut &mut y; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MUT_MUT, + pedantic, + "usage of double-mut refs, e.g., `&mut &mut ...`" +} + +declare_lint_pass!(MutMut => [MUT_MUT]); + +impl<'tcx> LateLintPass<'tcx> for MutMut { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) { + intravisit::walk_block(&mut MutVisitor { cx }, block); + } + + fn check_ty(&mut self, cx: &LateContext<'tcx>, ty: &'tcx hir::Ty<'_>) { + use rustc_hir::intravisit::Visitor; + + MutVisitor { cx }.visit_ty(ty); + } +} + +pub struct MutVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> intravisit::Visitor<'tcx> for MutVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) { + if in_external_macro(self.cx.sess(), expr.span) { + return; + } + + if let Some(higher::ForLoop { arg, body, .. }) = higher::ForLoop::hir(expr) { + // A `for` loop lowers to: + // ```rust + // match ::std::iter::Iterator::next(&mut iter) { + // // ^^^^ + // ``` + // Let's ignore the generated code. + intravisit::walk_expr(self, arg); + intravisit::walk_expr(self, body); + } else if let hir::ExprKind::AddrOf(hir::BorrowKind::Ref, hir::Mutability::Mut, e) = expr.kind { + if let hir::ExprKind::AddrOf(hir::BorrowKind::Ref, hir::Mutability::Mut, _) = e.kind { + span_lint( + self.cx, + MUT_MUT, + expr.span, + "generally you want to avoid `&mut &mut _` if possible", + ); + } else if let ty::Ref(_, _, hir::Mutability::Mut) = self.cx.typeck_results().expr_ty(e).kind() { + span_lint( + self.cx, + MUT_MUT, + expr.span, + "this expression mutably borrows a mutable reference. Consider reborrowing", + ); + } + } + } + + fn visit_ty(&mut self, ty: &'tcx hir::Ty<'_>) { + if in_external_macro(self.cx.sess(), ty.span) { + return; + } + + if let hir::TyKind::Rptr( + _, + hir::MutTy { + ty: pty, + mutbl: hir::Mutability::Mut, + }, + ) = ty.kind + { + if let hir::TyKind::Rptr( + _, + hir::MutTy { + mutbl: hir::Mutability::Mut, + .. + }, + ) = pty.kind + { + span_lint( + self.cx, + MUT_MUT, + ty.span, + "generally you want to avoid `&mut &mut _` if possible", + ); + } + } + + intravisit::walk_ty(self, ty); + } +} diff --git a/src/tools/clippy/clippy_lints/src/mut_mutex_lock.rs b/src/tools/clippy/clippy_lints/src/mut_mutex_lock.rs new file mode 100644 index 000000000..b7f981faa --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mut_mutex_lock.rs @@ -0,0 +1,70 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, Mutability}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `&mut Mutex::lock` calls + /// + /// ### Why is this bad? + /// `Mutex::lock` is less efficient than + /// calling `Mutex::get_mut`. In addition you also have a statically + /// guarantee that the mutex isn't locked, instead of just a runtime + /// guarantee. + /// + /// ### Example + /// ```rust + /// use std::sync::{Arc, Mutex}; + /// + /// let mut value_rc = Arc::new(Mutex::new(42_u8)); + /// let value_mutex = Arc::get_mut(&mut value_rc).unwrap(); + /// + /// let mut value = value_mutex.lock().unwrap(); + /// *value += 1; + /// ``` + /// Use instead: + /// ```rust + /// use std::sync::{Arc, Mutex}; + /// + /// let mut value_rc = Arc::new(Mutex::new(42_u8)); + /// let value_mutex = Arc::get_mut(&mut value_rc).unwrap(); + /// + /// let value = value_mutex.get_mut().unwrap(); + /// *value += 1; + /// ``` + #[clippy::version = "1.49.0"] + pub MUT_MUTEX_LOCK, + style, + "`&mut Mutex::lock` does unnecessary locking" +} + +declare_lint_pass!(MutMutexLock => [MUT_MUTEX_LOCK]); + +impl<'tcx> LateLintPass<'tcx> for MutMutexLock { + fn check_expr(&mut self, cx: &LateContext<'tcx>, ex: &'tcx Expr<'tcx>) { + if_chain! { + if let ExprKind::MethodCall(path, [self_arg, ..], _) = &ex.kind; + if path.ident.name == sym!(lock); + let ty = cx.typeck_results().expr_ty(self_arg); + if let ty::Ref(_, inner_ty, Mutability::Mut) = ty.kind(); + if is_type_diagnostic_item(cx, *inner_ty, sym::Mutex); + then { + span_lint_and_sugg( + cx, + MUT_MUTEX_LOCK, + path.ident.span, + "calling `&mut Mutex::lock` unnecessarily locks an exclusive (mutable) reference", + "change this to", + "get_mut".to_owned(), + Applicability::MaybeIncorrect, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/mut_reference.rs b/src/tools/clippy/clippy_lints/src/mut_reference.rs new file mode 100644 index 000000000..f434a655f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mut_reference.rs @@ -0,0 +1,95 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::subst::Subst; +use rustc_middle::ty::{self, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use std::iter; + +declare_clippy_lint! { + /// ### What it does + /// Detects passing a mutable reference to a function that only + /// requires an immutable reference. + /// + /// ### Why is this bad? + /// The mutable reference rules out all other references to + /// the value. Also the code misleads about the intent of the call site. + /// + /// ### Example + /// ```rust + /// # let mut vec = Vec::new(); + /// # let mut value = 5; + /// vec.push(&mut value); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let mut vec = Vec::new(); + /// # let value = 5; + /// vec.push(&value); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNNECESSARY_MUT_PASSED, + style, + "an argument passed as a mutable reference although the callee only demands an immutable reference" +} + +declare_lint_pass!(UnnecessaryMutPassed => [UNNECESSARY_MUT_PASSED]); + +impl<'tcx> LateLintPass<'tcx> for UnnecessaryMutPassed { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + match e.kind { + ExprKind::Call(fn_expr, arguments) => { + if let ExprKind::Path(ref path) = fn_expr.kind { + check_arguments( + cx, + arguments, + cx.typeck_results().expr_ty(fn_expr), + &rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false)), + "function", + ); + } + }, + ExprKind::MethodCall(path, arguments, _) => { + let def_id = cx.typeck_results().type_dependent_def_id(e.hir_id).unwrap(); + let substs = cx.typeck_results().node_substs(e.hir_id); + let method_type = cx.tcx.bound_type_of(def_id).subst(cx.tcx, substs); + check_arguments(cx, arguments, method_type, path.ident.as_str(), "method"); + }, + _ => (), + } + } +} + +fn check_arguments<'tcx>( + cx: &LateContext<'tcx>, + arguments: &[Expr<'_>], + type_definition: Ty<'tcx>, + name: &str, + fn_kind: &str, +) { + match type_definition.kind() { + ty::FnDef(..) | ty::FnPtr(_) => { + let parameters = type_definition.fn_sig(cx.tcx).skip_binder().inputs(); + for (argument, parameter) in iter::zip(arguments, parameters) { + match parameter.kind() { + ty::Ref(_, _, Mutability::Not) + | ty::RawPtr(ty::TypeAndMut { + mutbl: Mutability::Not, .. + }) => { + if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _) = argument.kind { + span_lint( + cx, + UNNECESSARY_MUT_PASSED, + argument.span, + &format!("the {} `{}` doesn't need a mutable reference", fn_kind, name), + ); + } + }, + _ => (), + } + } + }, + _ => (), + } +} diff --git a/src/tools/clippy/clippy_lints/src/mutable_debug_assertion.rs b/src/tools/clippy/clippy_lints/src/mutable_debug_assertion.rs new file mode 100644 index 000000000..44fdf84c6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mutable_debug_assertion.rs @@ -0,0 +1,124 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node}; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{BorrowKind, Expr, ExprKind, MatchSource, Mutability}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for function/method calls with a mutable + /// parameter in `debug_assert!`, `debug_assert_eq!` and `debug_assert_ne!` macros. + /// + /// ### Why is this bad? + /// In release builds `debug_assert!` macros are optimized out by the + /// compiler. + /// Therefore mutating something in a `debug_assert!` macro results in different behavior + /// between a release and debug build. + /// + /// ### Example + /// ```rust,ignore + /// debug_assert_eq!(vec![3].pop(), Some(3)); + /// + /// // or + /// + /// # let mut x = 5; + /// # fn takes_a_mut_parameter(_: &mut u32) -> bool { unimplemented!() } + /// debug_assert!(takes_a_mut_parameter(&mut x)); + /// ``` + #[clippy::version = "1.40.0"] + pub DEBUG_ASSERT_WITH_MUT_CALL, + nursery, + "mutable arguments in `debug_assert{,_ne,_eq}!`" +} + +declare_lint_pass!(DebugAssertWithMutCall => [DEBUG_ASSERT_WITH_MUT_CALL]); + +impl<'tcx> LateLintPass<'tcx> for DebugAssertWithMutCall { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + let Some(macro_call) = root_macro_call_first_node(cx, e) else { return }; + let macro_name = cx.tcx.item_name(macro_call.def_id); + if !matches!( + macro_name.as_str(), + "debug_assert" | "debug_assert_eq" | "debug_assert_ne" + ) { + return; + } + let Some((lhs, rhs, _)) = find_assert_eq_args(cx, e, macro_call.expn) else { return }; + for arg in [lhs, rhs] { + let mut visitor = MutArgVisitor::new(cx); + visitor.visit_expr(arg); + if let Some(span) = visitor.expr_span() { + span_lint( + cx, + DEBUG_ASSERT_WITH_MUT_CALL, + span, + &format!( + "do not call a function with mutable arguments inside of `{}!`", + macro_name + ), + ); + } + } + } +} + +struct MutArgVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + expr_span: Option<Span>, + found: bool, +} + +impl<'a, 'tcx> MutArgVisitor<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> Self { + Self { + cx, + expr_span: None, + found: false, + } + } + + fn expr_span(&self) -> Option<Span> { + if self.found { self.expr_span } else { None } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for MutArgVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + match expr.kind { + ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _) => { + self.found = true; + return; + }, + ExprKind::If(..) => { + self.found = true; + return; + }, + ExprKind::Path(_) => { + if let Some(adj) = self.cx.typeck_results().adjustments().get(expr.hir_id) { + if adj + .iter() + .any(|a| matches!(a.target.kind(), ty::Ref(_, _, Mutability::Mut))) + { + self.found = true; + return; + } + } + }, + // Don't check await desugars + ExprKind::Match(_, _, MatchSource::AwaitDesugar) => return, + _ if !self.found => self.expr_span = Some(expr.span), + _ => return, + } + walk_expr(self, expr); + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} diff --git a/src/tools/clippy/clippy_lints/src/mutex_atomic.rs b/src/tools/clippy/clippy_lints/src/mutex_atomic.rs new file mode 100644 index 000000000..a98577093 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/mutex_atomic.rs @@ -0,0 +1,110 @@ +//! Checks for uses of mutex where an atomic value could be used +//! +//! This lint is **warn** by default + +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_hir::Expr; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `Mutex<X>` where an atomic will do. + /// + /// ### Why is this bad? + /// Using a mutex just to make access to a plain bool or + /// reference sequential is shooting flies with cannons. + /// `std::sync::atomic::AtomicBool` and `std::sync::atomic::AtomicPtr` are leaner and + /// faster. + /// + /// ### Known problems + /// This lint cannot detect if the mutex is actually used + /// for waiting before a critical section. + /// + /// ### Example + /// ```rust + /// # let y = true; + /// # use std::sync::Mutex; + /// let x = Mutex::new(&y); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let y = true; + /// # use std::sync::atomic::AtomicBool; + /// let x = AtomicBool::new(y); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MUTEX_ATOMIC, + nursery, + "using a mutex where an atomic value could be used instead" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usages of `Mutex<X>` where `X` is an integral + /// type. + /// + /// ### Why is this bad? + /// Using a mutex just to make access to a plain integer + /// sequential is + /// shooting flies with cannons. `std::sync::atomic::AtomicUsize` is leaner and faster. + /// + /// ### Known problems + /// This lint cannot detect if the mutex is actually used + /// for waiting before a critical section. + /// + /// ### Example + /// ```rust + /// # use std::sync::Mutex; + /// let x = Mutex::new(0usize); + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::sync::atomic::AtomicUsize; + /// let x = AtomicUsize::new(0usize); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MUTEX_INTEGER, + nursery, + "using a mutex for an integer type" +} + +declare_lint_pass!(Mutex => [MUTEX_ATOMIC, MUTEX_INTEGER]); + +impl<'tcx> LateLintPass<'tcx> for Mutex { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let ty = cx.typeck_results().expr_ty(expr); + if let ty::Adt(_, subst) = ty.kind() { + if is_type_diagnostic_item(cx, ty, sym::Mutex) { + let mutex_param = subst.type_at(0); + if let Some(atomic_name) = get_atomic_name(mutex_param) { + let msg = format!( + "consider using an `{}` instead of a `Mutex` here; if you just want the locking \ + behavior and not the internal type, consider using `Mutex<()>`", + atomic_name + ); + match *mutex_param.kind() { + ty::Uint(t) if t != ty::UintTy::Usize => span_lint(cx, MUTEX_INTEGER, expr.span, &msg), + ty::Int(t) if t != ty::IntTy::Isize => span_lint(cx, MUTEX_INTEGER, expr.span, &msg), + _ => span_lint(cx, MUTEX_ATOMIC, expr.span, &msg), + }; + } + } + } + } +} + +fn get_atomic_name(ty: Ty<'_>) -> Option<&'static str> { + match ty.kind() { + ty::Bool => Some("AtomicBool"), + ty::Uint(_) => Some("AtomicUsize"), + ty::Int(_) => Some("AtomicIsize"), + ty::RawPtr(_) => Some("AtomicPtr"), + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_arbitrary_self_type.rs b/src/tools/clippy/clippy_lints/src/needless_arbitrary_self_type.rs new file mode 100644 index 000000000..9838d3cad --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_arbitrary_self_type.rs @@ -0,0 +1,139 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use if_chain::if_chain; +use rustc_ast::ast::{BindingMode, Lifetime, Mutability, Param, PatKind, Path, TyKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::kw; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// The lint checks for `self` in fn parameters that + /// specify the `Self`-type explicitly + /// ### Why is this bad? + /// Increases the amount and decreases the readability of code + /// + /// ### Example + /// ```rust + /// enum ValType { + /// I32, + /// I64, + /// F32, + /// F64, + /// } + /// + /// impl ValType { + /// pub fn bytes(self: Self) -> usize { + /// match self { + /// Self::I32 | Self::F32 => 4, + /// Self::I64 | Self::F64 => 8, + /// } + /// } + /// } + /// ``` + /// + /// Could be rewritten as + /// + /// ```rust + /// enum ValType { + /// I32, + /// I64, + /// F32, + /// F64, + /// } + /// + /// impl ValType { + /// pub fn bytes(self) -> usize { + /// match self { + /// Self::I32 | Self::F32 => 4, + /// Self::I64 | Self::F64 => 8, + /// } + /// } + /// } + /// ``` + #[clippy::version = "1.47.0"] + pub NEEDLESS_ARBITRARY_SELF_TYPE, + complexity, + "type of `self` parameter is already by default `Self`" +} + +declare_lint_pass!(NeedlessArbitrarySelfType => [NEEDLESS_ARBITRARY_SELF_TYPE]); + +enum Mode { + Ref(Option<Lifetime>), + Value, +} + +fn check_param_inner(cx: &EarlyContext<'_>, path: &Path, span: Span, binding_mode: &Mode, mutbl: Mutability) { + if_chain! { + if let [segment] = &path.segments[..]; + if segment.ident.name == kw::SelfUpper; + then { + // In case we have a named lifetime, we check if the name comes from expansion. + // If it does, at this point we know the rest of the parameter was written by the user, + // so let them decide what the name of the lifetime should be. + // See #6089 for more details. + let mut applicability = Applicability::MachineApplicable; + let self_param = match (binding_mode, mutbl) { + (Mode::Ref(None), Mutability::Mut) => "&mut self".to_string(), + (Mode::Ref(Some(lifetime)), Mutability::Mut) => { + if lifetime.ident.span.from_expansion() { + applicability = Applicability::HasPlaceholders; + "&'_ mut self".to_string() + } else { + format!("&{} mut self", &lifetime.ident.name) + } + }, + (Mode::Ref(None), Mutability::Not) => "&self".to_string(), + (Mode::Ref(Some(lifetime)), Mutability::Not) => { + if lifetime.ident.span.from_expansion() { + applicability = Applicability::HasPlaceholders; + "&'_ self".to_string() + } else { + format!("&{} self", &lifetime.ident.name) + } + }, + (Mode::Value, Mutability::Mut) => "mut self".to_string(), + (Mode::Value, Mutability::Not) => "self".to_string(), + }; + + span_lint_and_sugg( + cx, + NEEDLESS_ARBITRARY_SELF_TYPE, + span, + "the type of the `self` parameter does not need to be arbitrary", + "consider to change this parameter to", + self_param, + applicability, + ) + } + } +} + +impl EarlyLintPass for NeedlessArbitrarySelfType { + fn check_param(&mut self, cx: &EarlyContext<'_>, p: &Param) { + // Bail out if the parameter it's not a receiver or was not written by the user + if !p.is_self() || p.span.from_expansion() { + return; + } + + match &p.ty.kind { + TyKind::Path(None, path) => { + if let PatKind::Ident(BindingMode::ByValue(mutbl), _, _) = p.pat.kind { + check_param_inner(cx, path, p.span.to(p.ty.span), &Mode::Value, mutbl); + } + }, + TyKind::Rptr(lifetime, mut_ty) => { + if_chain! { + if let TyKind::Path(None, path) = &mut_ty.ty.kind; + if let PatKind::Ident(BindingMode::ByValue(Mutability::Not), _, _) = p.pat.kind; + then { + check_param_inner(cx, path, p.span.to(p.ty.span), &Mode::Ref(*lifetime), mut_ty.mutbl); + } + } + }, + _ => {}, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_bool.rs b/src/tools/clippy/clippy_lints/src/needless_bool.rs new file mode 100644 index 000000000..a4eec95b3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_bool.rs @@ -0,0 +1,385 @@ +//! Checks for needless boolean results of if-else expressions +//! +//! This lint is **warn** by default + +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; +use clippy_utils::higher; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::sugg::Sugg; +use clippy_utils::{get_parent_node, is_else_clause, is_expn_of, peel_blocks, peel_blocks_with_stmt}; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, Node, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for expressions of the form `if c { true } else { + /// false }` (or vice versa) and suggests using the condition directly. + /// + /// ### Why is this bad? + /// Redundant code. + /// + /// ### Known problems + /// Maybe false positives: Sometimes, the two branches are + /// painstakingly documented (which we, of course, do not detect), so they *may* + /// have some value. Even then, the documentation can be rewritten to match the + /// shorter code. + /// + /// ### Example + /// ```rust + /// # let x = true; + /// if x { + /// false + /// } else { + /// true + /// } + /// # ; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = true; + /// !x + /// # ; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_BOOL, + complexity, + "if-statements with plain booleans in the then- and else-clause, e.g., `if p { true } else { false }`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for expressions of the form `x == true`, + /// `x != true` and order comparisons such as `x < true` (or vice versa) and + /// suggest using the variable directly. + /// + /// ### Why is this bad? + /// Unnecessary code. + /// + /// ### Example + /// ```rust,ignore + /// if x == true {} + /// if y == false {} + /// ``` + /// use `x` directly: + /// ```rust,ignore + /// if x {} + /// if !y {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub BOOL_COMPARISON, + complexity, + "comparing a variable to a boolean, e.g., `if x == true` or `if x != true`" +} + +declare_lint_pass!(NeedlessBool => [NEEDLESS_BOOL]); + +fn condition_needs_parentheses(e: &Expr<'_>) -> bool { + let mut inner = e; + while let ExprKind::Binary(_, i, _) + | ExprKind::Call(i, _) + | ExprKind::Cast(i, _) + | ExprKind::Type(i, _) + | ExprKind::Index(i, _) = inner.kind + { + if matches!( + i.kind, + ExprKind::Block(..) + | ExprKind::ConstBlock(..) + | ExprKind::If(..) + | ExprKind::Loop(..) + | ExprKind::Match(..) + ) { + return true; + } + inner = i; + } + false +} + +fn is_parent_stmt(cx: &LateContext<'_>, id: HirId) -> bool { + matches!( + get_parent_node(cx.tcx, id), + Some(Node::Stmt(..) | Node::Block(Block { stmts: &[], .. })) + ) +} + +impl<'tcx> LateLintPass<'tcx> for NeedlessBool { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + use self::Expression::{Bool, RetBool}; + if e.span.from_expansion() { + return; + } + if let Some(higher::If { + cond, + then, + r#else: Some(r#else), + }) = higher::If::hir(e) + { + let reduce = |ret, not| { + let mut applicability = Applicability::MachineApplicable; + let snip = Sugg::hir_with_applicability(cx, cond, "<predicate>", &mut applicability); + let mut snip = if not { !snip } else { snip }; + + if ret { + snip = snip.make_return(); + } + + if is_else_clause(cx.tcx, e) { + snip = snip.blockify(); + } + + if condition_needs_parentheses(cond) && is_parent_stmt(cx, e.hir_id) { + snip = snip.maybe_par(); + } + + span_lint_and_sugg( + cx, + NEEDLESS_BOOL, + e.span, + "this if-then-else expression returns a bool literal", + "you can reduce it to", + snip.to_string(), + applicability, + ); + }; + if let Some((a, b)) = fetch_bool_block(then).and_then(|a| Some((a, fetch_bool_block(r#else)?))) { + match (a, b) { + (RetBool(true), RetBool(true)) | (Bool(true), Bool(true)) => { + span_lint( + cx, + NEEDLESS_BOOL, + e.span, + "this if-then-else expression will always return true", + ); + }, + (RetBool(false), RetBool(false)) | (Bool(false), Bool(false)) => { + span_lint( + cx, + NEEDLESS_BOOL, + e.span, + "this if-then-else expression will always return false", + ); + }, + (RetBool(true), RetBool(false)) => reduce(true, false), + (Bool(true), Bool(false)) => reduce(false, false), + (RetBool(false), RetBool(true)) => reduce(true, true), + (Bool(false), Bool(true)) => reduce(false, true), + _ => (), + } + } + } + } +} + +declare_lint_pass!(BoolComparison => [BOOL_COMPARISON]); + +impl<'tcx> LateLintPass<'tcx> for BoolComparison { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if e.span.from_expansion() { + return; + } + + if let ExprKind::Binary(Spanned { node, .. }, ..) = e.kind { + let ignore_case = None::<(fn(_) -> _, &str)>; + let ignore_no_literal = None::<(fn(_, _) -> _, &str)>; + match node { + BinOpKind::Eq => { + let true_case = Some((|h| h, "equality checks against true are unnecessary")); + let false_case = Some(( + |h: Sugg<'tcx>| !h, + "equality checks against false can be replaced by a negation", + )); + check_comparison(cx, e, true_case, false_case, true_case, false_case, ignore_no_literal); + }, + BinOpKind::Ne => { + let true_case = Some(( + |h: Sugg<'tcx>| !h, + "inequality checks against true can be replaced by a negation", + )); + let false_case = Some((|h| h, "inequality checks against false are unnecessary")); + check_comparison(cx, e, true_case, false_case, true_case, false_case, ignore_no_literal); + }, + BinOpKind::Lt => check_comparison( + cx, + e, + ignore_case, + Some((|h| h, "greater than checks against false are unnecessary")), + Some(( + |h: Sugg<'tcx>| !h, + "less than comparison against true can be replaced by a negation", + )), + ignore_case, + Some(( + |l: Sugg<'tcx>, r: Sugg<'tcx>| (!l).bit_and(&r), + "order comparisons between booleans can be simplified", + )), + ), + BinOpKind::Gt => check_comparison( + cx, + e, + Some(( + |h: Sugg<'tcx>| !h, + "less than comparison against true can be replaced by a negation", + )), + ignore_case, + ignore_case, + Some((|h| h, "greater than checks against false are unnecessary")), + Some(( + |l: Sugg<'tcx>, r: Sugg<'tcx>| l.bit_and(&(!r)), + "order comparisons between booleans can be simplified", + )), + ), + _ => (), + } + } + } +} + +struct ExpressionInfoWithSpan { + one_side_is_unary_not: bool, + left_span: Span, + right_span: Span, +} + +fn is_unary_not(e: &Expr<'_>) -> (bool, Span) { + if let ExprKind::Unary(UnOp::Not, operand) = e.kind { + return (true, operand.span); + } + (false, e.span) +} + +fn one_side_is_unary_not<'tcx>(left_side: &'tcx Expr<'_>, right_side: &'tcx Expr<'_>) -> ExpressionInfoWithSpan { + let left = is_unary_not(left_side); + let right = is_unary_not(right_side); + + ExpressionInfoWithSpan { + one_side_is_unary_not: left.0 != right.0, + left_span: left.1, + right_span: right.1, + } +} + +fn check_comparison<'a, 'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + left_true: Option<(impl FnOnce(Sugg<'a>) -> Sugg<'a>, &str)>, + left_false: Option<(impl FnOnce(Sugg<'a>) -> Sugg<'a>, &str)>, + right_true: Option<(impl FnOnce(Sugg<'a>) -> Sugg<'a>, &str)>, + right_false: Option<(impl FnOnce(Sugg<'a>) -> Sugg<'a>, &str)>, + no_literal: Option<(impl FnOnce(Sugg<'a>, Sugg<'a>) -> Sugg<'a>, &str)>, +) { + if let ExprKind::Binary(op, left_side, right_side) = e.kind { + let (l_ty, r_ty) = ( + cx.typeck_results().expr_ty(left_side), + cx.typeck_results().expr_ty(right_side), + ); + if is_expn_of(left_side.span, "cfg").is_some() || is_expn_of(right_side.span, "cfg").is_some() { + return; + } + if l_ty.is_bool() && r_ty.is_bool() { + let mut applicability = Applicability::MachineApplicable; + + if op.node == BinOpKind::Eq { + let expression_info = one_side_is_unary_not(left_side, right_side); + if expression_info.one_side_is_unary_not { + span_lint_and_sugg( + cx, + BOOL_COMPARISON, + e.span, + "this comparison might be written more concisely", + "try simplifying it as shown", + format!( + "{} != {}", + snippet_with_applicability(cx, expression_info.left_span, "..", &mut applicability), + snippet_with_applicability(cx, expression_info.right_span, "..", &mut applicability) + ), + applicability, + ); + } + } + + match (fetch_bool_expr(left_side), fetch_bool_expr(right_side)) { + (Some(true), None) => left_true.map_or((), |(h, m)| { + suggest_bool_comparison(cx, e, right_side, applicability, m, h); + }), + (None, Some(true)) => right_true.map_or((), |(h, m)| { + suggest_bool_comparison(cx, e, left_side, applicability, m, h); + }), + (Some(false), None) => left_false.map_or((), |(h, m)| { + suggest_bool_comparison(cx, e, right_side, applicability, m, h); + }), + (None, Some(false)) => right_false.map_or((), |(h, m)| { + suggest_bool_comparison(cx, e, left_side, applicability, m, h); + }), + (None, None) => no_literal.map_or((), |(h, m)| { + let left_side = Sugg::hir_with_applicability(cx, left_side, "..", &mut applicability); + let right_side = Sugg::hir_with_applicability(cx, right_side, "..", &mut applicability); + span_lint_and_sugg( + cx, + BOOL_COMPARISON, + e.span, + m, + "try simplifying it as shown", + h(left_side, right_side).to_string(), + applicability, + ); + }), + _ => (), + } + } + } +} + +fn suggest_bool_comparison<'a, 'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + expr: &Expr<'_>, + mut applicability: Applicability, + message: &str, + conv_hint: impl FnOnce(Sugg<'a>) -> Sugg<'a>, +) { + let hint = if expr.span.from_expansion() { + if applicability != Applicability::Unspecified { + applicability = Applicability::MaybeIncorrect; + } + Sugg::hir_with_macro_callsite(cx, expr, "..") + } else { + Sugg::hir_with_applicability(cx, expr, "..", &mut applicability) + }; + span_lint_and_sugg( + cx, + BOOL_COMPARISON, + e.span, + message, + "try simplifying it as shown", + conv_hint(hint).to_string(), + applicability, + ); +} + +enum Expression { + Bool(bool), + RetBool(bool), +} + +fn fetch_bool_block(expr: &Expr<'_>) -> Option<Expression> { + match peel_blocks_with_stmt(expr).kind { + ExprKind::Ret(Some(ret)) => Some(Expression::RetBool(fetch_bool_expr(ret)?)), + _ => Some(Expression::Bool(fetch_bool_expr(expr)?)), + } +} + +fn fetch_bool_expr(expr: &Expr<'_>) -> Option<bool> { + if let ExprKind::Lit(ref lit_ptr) = peel_blocks(expr).kind { + if let LitKind::Bool(value) = lit_ptr.node { + return Some(value); + } + } + None +} diff --git a/src/tools/clippy/clippy_lints/src/needless_borrowed_ref.rs b/src/tools/clippy/clippy_lints/src/needless_borrowed_ref.rs new file mode 100644 index 000000000..05c012b92 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_borrowed_ref.rs @@ -0,0 +1,87 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_with_applicability; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BindingAnnotation, Mutability, Node, Pat, PatKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for bindings that destructure a reference and borrow the inner + /// value with `&ref`. + /// + /// ### Why is this bad? + /// This pattern has no effect in almost all cases. + /// + /// ### Known problems + /// In some cases, `&ref` is needed to avoid a lifetime mismatch error. + /// Example: + /// ```rust + /// fn foo(a: &Option<String>, b: &Option<String>) { + /// match (a, b) { + /// (None, &ref c) | (&ref c, None) => (), + /// (&Some(ref c), _) => (), + /// }; + /// } + /// ``` + /// + /// ### Example + /// ```rust + /// let mut v = Vec::<String>::new(); + /// # #[allow(unused)] + /// v.iter_mut().filter(|&ref a| a.is_empty()); + /// ``` + /// + /// Use instead: + /// ```rust + /// let mut v = Vec::<String>::new(); + /// # #[allow(unused)] + /// v.iter_mut().filter(|a| a.is_empty()); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_BORROWED_REFERENCE, + complexity, + "destructuring a reference and borrowing the inner value" +} + +declare_lint_pass!(NeedlessBorrowedRef => [NEEDLESS_BORROWED_REFERENCE]); + +impl<'tcx> LateLintPass<'tcx> for NeedlessBorrowedRef { + fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) { + if pat.span.from_expansion() { + // OK, simple enough, lints doesn't check in macro. + return; + } + + if_chain! { + // Only lint immutable refs, because `&mut ref T` may be useful. + if let PatKind::Ref(sub_pat, Mutability::Not) = pat.kind; + + // Check sub_pat got a `ref` keyword (excluding `ref mut`). + if let PatKind::Binding(BindingAnnotation::Ref, .., spanned_name, _) = sub_pat.kind; + let parent_id = cx.tcx.hir().get_parent_node(pat.hir_id); + if let Some(parent_node) = cx.tcx.hir().find(parent_id); + then { + // do not recurse within patterns, as they may have other references + // XXXManishearth we can relax this constraint if we only check patterns + // with a single ref pattern inside them + if let Node::Pat(_) = parent_node { + return; + } + let mut applicability = Applicability::MachineApplicable; + span_lint_and_then(cx, NEEDLESS_BORROWED_REFERENCE, pat.span, + "this pattern takes a reference on something that is being de-referenced", + |diag| { + let hint = snippet_with_applicability(cx, spanned_name.span, "..", &mut applicability).into_owned(); + diag.span_suggestion( + pat.span, + "try removing the `&ref` part and just keep", + hint, + applicability, + ); + }); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_continue.rs b/src/tools/clippy/clippy_lints/src/needless_continue.rs new file mode 100644 index 000000000..98a3bce1f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_continue.rs @@ -0,0 +1,479 @@ +//! Checks for continue statements in loops that are redundant. +//! +//! For example, the lint would catch +//! +//! ```rust +//! let mut a = 1; +//! let x = true; +//! +//! while a < 5 { +//! a = 6; +//! if x { +//! // ... +//! } else { +//! continue; +//! } +//! println!("Hello, world"); +//! } +//! ``` +//! +//! And suggest something like this: +//! +//! ```rust +//! let mut a = 1; +//! let x = true; +//! +//! while a < 5 { +//! a = 6; +//! if x { +//! // ... +//! println!("Hello, world"); +//! } +//! } +//! ``` +//! +//! This lint is **warn** by default. +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::{indent_of, snippet, snippet_block}; +use rustc_ast::ast; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// The lint checks for `if`-statements appearing in loops + /// that contain a `continue` statement in either their main blocks or their + /// `else`-blocks, when omitting the `else`-block possibly with some + /// rearrangement of code can make the code easier to understand. + /// + /// ### Why is this bad? + /// Having explicit `else` blocks for `if` statements + /// containing `continue` in their THEN branch adds unnecessary branching and + /// nesting to the code. Having an else block containing just `continue` can + /// also be better written by grouping the statements following the whole `if` + /// statement within the THEN block and omitting the else block completely. + /// + /// ### Example + /// ```rust + /// # fn condition() -> bool { false } + /// # fn update_condition() {} + /// # let x = false; + /// while condition() { + /// update_condition(); + /// if x { + /// // ... + /// } else { + /// continue; + /// } + /// println!("Hello, world"); + /// } + /// ``` + /// + /// Could be rewritten as + /// + /// ```rust + /// # fn condition() -> bool { false } + /// # fn update_condition() {} + /// # let x = false; + /// while condition() { + /// update_condition(); + /// if x { + /// // ... + /// println!("Hello, world"); + /// } + /// } + /// ``` + /// + /// As another example, the following code + /// + /// ```rust + /// # fn waiting() -> bool { false } + /// loop { + /// if waiting() { + /// continue; + /// } else { + /// // Do something useful + /// } + /// # break; + /// } + /// ``` + /// Could be rewritten as + /// + /// ```rust + /// # fn waiting() -> bool { false } + /// loop { + /// if waiting() { + /// continue; + /// } + /// // Do something useful + /// # break; + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_CONTINUE, + pedantic, + "`continue` statements that can be replaced by a rearrangement of code" +} + +declare_lint_pass!(NeedlessContinue => [NEEDLESS_CONTINUE]); + +impl EarlyLintPass for NeedlessContinue { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) { + if !expr.span.from_expansion() { + check_and_warn(cx, expr); + } + } +} + +/* This lint has to mainly deal with two cases of needless continue + * statements. */ +// Case 1 [Continue inside else block]: +// +// loop { +// // region A +// if cond { +// // region B +// } else { +// continue; +// } +// // region C +// } +// +// This code can better be written as follows: +// +// loop { +// // region A +// if cond { +// // region B +// // region C +// } +// } +// +// Case 2 [Continue inside then block]: +// +// loop { +// // region A +// if cond { +// continue; +// // potentially more code here. +// } else { +// // region B +// } +// // region C +// } +// +// +// This snippet can be refactored to: +// +// loop { +// // region A +// if !cond { +// // region B +// // region C +// } +// } +// + +/// Given an expression, returns true if either of the following is true +/// +/// - The expression is a `continue` node. +/// - The expression node is a block with the first statement being a +/// `continue`. +fn needless_continue_in_else(else_expr: &ast::Expr, label: Option<&ast::Label>) -> bool { + match else_expr.kind { + ast::ExprKind::Block(ref else_block, _) => is_first_block_stmt_continue(else_block, label), + ast::ExprKind::Continue(l) => compare_labels(label, l.as_ref()), + _ => false, + } +} + +fn is_first_block_stmt_continue(block: &ast::Block, label: Option<&ast::Label>) -> bool { + block.stmts.get(0).map_or(false, |stmt| match stmt.kind { + ast::StmtKind::Semi(ref e) | ast::StmtKind::Expr(ref e) => { + if let ast::ExprKind::Continue(ref l) = e.kind { + compare_labels(label, l.as_ref()) + } else { + false + } + }, + _ => false, + }) +} + +/// If the `continue` has a label, check it matches the label of the loop. +fn compare_labels(loop_label: Option<&ast::Label>, continue_label: Option<&ast::Label>) -> bool { + match (loop_label, continue_label) { + // `loop { continue; }` or `'a loop { continue; }` + (_, None) => true, + // `loop { continue 'a; }` + (None, _) => false, + // `'a loop { continue 'a; }` or `'a loop { continue 'b; }` + (Some(x), Some(y)) => x.ident == y.ident, + } +} + +/// If `expr` is a loop expression (while/while let/for/loop), calls `func` with +/// the AST object representing the loop block of `expr`. +fn with_loop_block<F>(expr: &ast::Expr, mut func: F) +where + F: FnMut(&ast::Block, Option<&ast::Label>), +{ + if let ast::ExprKind::While(_, loop_block, label) + | ast::ExprKind::ForLoop(_, _, loop_block, label) + | ast::ExprKind::Loop(loop_block, label, ..) = &expr.kind + { + func(loop_block, label.as_ref()); + } +} + +/// If `stmt` is an if expression node with an `else` branch, calls func with +/// the +/// following: +/// +/// - The `if` expression itself, +/// - The `if` condition expression, +/// - The `then` block, and +/// - The `else` expression. +fn with_if_expr<F>(stmt: &ast::Stmt, mut func: F) +where + F: FnMut(&ast::Expr, &ast::Expr, &ast::Block, &ast::Expr), +{ + match stmt.kind { + ast::StmtKind::Semi(ref e) | ast::StmtKind::Expr(ref e) => { + if let ast::ExprKind::If(ref cond, ref if_block, Some(ref else_expr)) = e.kind { + func(e, cond, if_block, else_expr); + } + }, + _ => {}, + } +} + +/// A type to distinguish between the two distinct cases this lint handles. +#[derive(Copy, Clone, Debug)] +enum LintType { + ContinueInsideElseBlock, + ContinueInsideThenBlock, +} + +/// Data we pass around for construction of help messages. +struct LintData<'a> { + /// The `if` expression encountered in the above loop. + if_expr: &'a ast::Expr, + /// The condition expression for the above `if`. + if_cond: &'a ast::Expr, + /// The `then` block of the `if` statement. + if_block: &'a ast::Block, + /// The `else` block of the `if` statement. + /// Note that we only work with `if` exprs that have an `else` branch. + else_expr: &'a ast::Expr, + /// The 0-based index of the `if` statement in the containing loop block. + stmt_idx: usize, + /// The statements of the loop block. + loop_block: &'a ast::Block, +} + +const MSG_REDUNDANT_CONTINUE_EXPRESSION: &str = "this `continue` expression is redundant"; + +const MSG_REDUNDANT_ELSE_BLOCK: &str = "this `else` block is redundant"; + +const MSG_ELSE_BLOCK_NOT_NEEDED: &str = "there is no need for an explicit `else` block for this `if` \ + expression"; + +const DROP_ELSE_BLOCK_AND_MERGE_MSG: &str = "consider dropping the `else` clause and merging the code that \ + follows (in the loop) with the `if` block"; + +const DROP_ELSE_BLOCK_MSG: &str = "consider dropping the `else` clause"; + +const DROP_CONTINUE_EXPRESSION_MSG: &str = "consider dropping the `continue` expression"; + +fn emit_warning<'a>(cx: &EarlyContext<'_>, data: &'a LintData<'_>, header: &str, typ: LintType) { + // snip is the whole *help* message that appears after the warning. + // message is the warning message. + // expr is the expression which the lint warning message refers to. + let (snip, message, expr) = match typ { + LintType::ContinueInsideElseBlock => ( + suggestion_snippet_for_continue_inside_else(cx, data), + MSG_REDUNDANT_ELSE_BLOCK, + data.else_expr, + ), + LintType::ContinueInsideThenBlock => ( + suggestion_snippet_for_continue_inside_if(cx, data), + MSG_ELSE_BLOCK_NOT_NEEDED, + data.if_expr, + ), + }; + span_lint_and_help( + cx, + NEEDLESS_CONTINUE, + expr.span, + message, + None, + &format!("{}\n{}", header, snip), + ); +} + +fn suggestion_snippet_for_continue_inside_if<'a>(cx: &EarlyContext<'_>, data: &'a LintData<'_>) -> String { + let cond_code = snippet(cx, data.if_cond.span, ".."); + + let continue_code = snippet_block(cx, data.if_block.span, "..", Some(data.if_expr.span)); + + let else_code = snippet_block(cx, data.else_expr.span, "..", Some(data.if_expr.span)); + + let indent_if = indent_of(cx, data.if_expr.span).unwrap_or(0); + format!( + "{indent}if {} {}\n{indent}{}", + cond_code, + continue_code, + else_code, + indent = " ".repeat(indent_if), + ) +} + +fn suggestion_snippet_for_continue_inside_else<'a>(cx: &EarlyContext<'_>, data: &'a LintData<'_>) -> String { + let cond_code = snippet(cx, data.if_cond.span, ".."); + + // Region B + let block_code = erode_from_back(&snippet_block(cx, data.if_block.span, "..", Some(data.if_expr.span))); + + // Region C + // These is the code in the loop block that follows the if/else construction + // we are complaining about. We want to pull all of this code into the + // `then` block of the `if` statement. + let indent = span_of_first_expr_in_block(data.if_block) + .and_then(|span| indent_of(cx, span)) + .unwrap_or(0); + let to_annex = data.loop_block.stmts[data.stmt_idx + 1..] + .iter() + .map(|stmt| { + let span = cx.sess().source_map().stmt_span(stmt.span, data.loop_block.span); + let snip = snippet_block(cx, span, "..", None).into_owned(); + snip.lines() + .map(|line| format!("{}{}", " ".repeat(indent), line)) + .collect::<Vec<_>>() + .join("\n") + }) + .collect::<Vec<_>>() + .join("\n"); + + let indent_if = indent_of(cx, data.if_expr.span).unwrap_or(0); + format!( + "{indent_if}if {} {}\n{indent}// merged code follows:\n{}\n{indent_if}}}", + cond_code, + block_code, + to_annex, + indent = " ".repeat(indent), + indent_if = " ".repeat(indent_if), + ) +} + +fn check_and_warn<'a>(cx: &EarlyContext<'_>, expr: &'a ast::Expr) { + if_chain! { + if let ast::ExprKind::Loop(loop_block, ..) = &expr.kind; + if let Some(last_stmt) = loop_block.stmts.last(); + if let ast::StmtKind::Expr(inner_expr) | ast::StmtKind::Semi(inner_expr) = &last_stmt.kind; + if let ast::ExprKind::Continue(_) = inner_expr.kind; + then { + span_lint_and_help( + cx, + NEEDLESS_CONTINUE, + last_stmt.span, + MSG_REDUNDANT_CONTINUE_EXPRESSION, + None, + DROP_CONTINUE_EXPRESSION_MSG, + ); + } + } + with_loop_block(expr, |loop_block, label| { + for (i, stmt) in loop_block.stmts.iter().enumerate() { + with_if_expr(stmt, |if_expr, cond, then_block, else_expr| { + let data = &LintData { + stmt_idx: i, + if_expr, + if_cond: cond, + if_block: then_block, + else_expr, + loop_block, + }; + if needless_continue_in_else(else_expr, label) { + emit_warning( + cx, + data, + DROP_ELSE_BLOCK_AND_MERGE_MSG, + LintType::ContinueInsideElseBlock, + ); + } else if is_first_block_stmt_continue(then_block, label) { + emit_warning(cx, data, DROP_ELSE_BLOCK_MSG, LintType::ContinueInsideThenBlock); + } + }); + } + }); +} + +/// Eats at `s` from the end till a closing brace `}` is encountered, and then continues eating +/// till a non-whitespace character is found. e.g., the string. If no closing `}` is present, the +/// string will be preserved. +/// +/// ```rust +/// { +/// let x = 5; +/// } +/// ``` +/// +/// is transformed to +/// +/// ```text +/// { +/// let x = 5; +/// ``` +#[must_use] +fn erode_from_back(s: &str) -> String { + let mut ret = s.to_string(); + while ret.pop().map_or(false, |c| c != '}') {} + while let Some(c) = ret.pop() { + if !c.is_whitespace() { + ret.push(c); + break; + } + } + if ret.is_empty() { s.to_string() } else { ret } +} + +fn span_of_first_expr_in_block(block: &ast::Block) -> Option<Span> { + block.stmts.get(0).map(|stmt| stmt.span) +} + +#[cfg(test)] +mod test { + use super::erode_from_back; + + #[test] + #[rustfmt::skip] + fn test_erode_from_back() { + let input = "\ +{ + let x = 5; + let y = format!(\"{}\", 42); +}"; + + let expected = "\ +{ + let x = 5; + let y = format!(\"{}\", 42);"; + + let got = erode_from_back(input); + assert_eq!(expected, got); + } + + #[test] + #[rustfmt::skip] + fn test_erode_from_back_no_brace() { + let input = "\ +let x = 5; +let y = something(); +"; + let expected = input; + let got = erode_from_back(input); + assert_eq!(expected, got); + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_for_each.rs b/src/tools/clippy/clippy_lints/src/needless_for_each.rs new file mode 100644 index 000000000..10e188ecb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_for_each.rs @@ -0,0 +1,160 @@ +use rustc_errors::Applicability; +use rustc_hir::{ + intravisit::{walk_expr, Visitor}, + Closure, Expr, ExprKind, Stmt, StmtKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{source_map::Span, sym, Symbol}; + +use if_chain::if_chain; + +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::is_trait_method; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::has_iter_method; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `for_each` that would be more simply written as a + /// `for` loop. + /// + /// ### Why is this bad? + /// `for_each` may be used after applying iterator transformers like + /// `filter` for better readability and performance. It may also be used to fit a simple + /// operation on one line. + /// But when none of these apply, a simple `for` loop is more idiomatic. + /// + /// ### Example + /// ```rust + /// let v = vec![0, 1, 2]; + /// v.iter().for_each(|elem| { + /// println!("{}", elem); + /// }) + /// ``` + /// Use instead: + /// ```rust + /// let v = vec![0, 1, 2]; + /// for elem in v.iter() { + /// println!("{}", elem); + /// } + /// ``` + #[clippy::version = "1.53.0"] + pub NEEDLESS_FOR_EACH, + pedantic, + "using `for_each` where a `for` loop would be simpler" +} + +declare_lint_pass!(NeedlessForEach => [NEEDLESS_FOR_EACH]); + +impl<'tcx> LateLintPass<'tcx> for NeedlessForEach { + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + let expr = match stmt.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => expr, + _ => return, + }; + + if_chain! { + // Check the method name is `for_each`. + if let ExprKind::MethodCall(method_name, [for_each_recv, for_each_arg], _) = expr.kind; + if method_name.ident.name == Symbol::intern("for_each"); + // Check `for_each` is an associated function of `Iterator`. + if is_trait_method(cx, expr, sym::Iterator); + // Checks the receiver of `for_each` is also a method call. + if let ExprKind::MethodCall(_, [iter_recv], _) = for_each_recv.kind; + // Skip the lint if the call chain is too long. e.g. `v.field.iter().for_each()` or + // `v.foo().iter().for_each()` must be skipped. + if matches!( + iter_recv.kind, + ExprKind::Array(..) | ExprKind::Call(..) | ExprKind::Path(..) + ); + // Checks the type of the `iter` method receiver is NOT a user defined type. + if has_iter_method(cx, cx.typeck_results().expr_ty(iter_recv)).is_some(); + // Skip the lint if the body is not block because this is simpler than `for` loop. + // e.g. `v.iter().for_each(f)` is simpler and clearer than using `for` loop. + if let ExprKind::Closure(&Closure { body, .. }) = for_each_arg.kind; + let body = cx.tcx.hir().body(body); + if let ExprKind::Block(..) = body.value.kind; + then { + let mut ret_collector = RetCollector::default(); + ret_collector.visit_expr(&body.value); + + // Skip the lint if `return` is used in `Loop` in order not to suggest using `'label`. + if ret_collector.ret_in_loop { + return; + } + + let (mut applicability, ret_suggs) = if ret_collector.spans.is_empty() { + (Applicability::MachineApplicable, None) + } else { + ( + Applicability::MaybeIncorrect, + Some( + ret_collector + .spans + .into_iter() + .map(|span| (span, "continue".to_string())) + .collect(), + ), + ) + }; + + let sugg = format!( + "for {} in {} {}", + snippet_with_applicability(cx, body.params[0].pat.span, "..", &mut applicability), + snippet_with_applicability(cx, for_each_recv.span, "..", &mut applicability), + snippet_with_applicability(cx, body.value.span, "..", &mut applicability), + ); + + span_lint_and_then(cx, NEEDLESS_FOR_EACH, stmt.span, "needless use of `for_each`", |diag| { + diag.span_suggestion(stmt.span, "try", sugg, applicability); + if let Some(ret_suggs) = ret_suggs { + diag.multipart_suggestion("...and replace `return` with `continue`", ret_suggs, applicability); + } + }) + } + } + } +} + +/// This type plays two roles. +/// 1. Collect spans of `return` in the closure body. +/// 2. Detect use of `return` in `Loop` in the closure body. +/// +/// NOTE: The functionality of this type is similar to +/// [`clippy_utils::visitors::find_all_ret_expressions`], but we can't use +/// `find_all_ret_expressions` instead of this type. The reasons are: +/// 1. `find_all_ret_expressions` passes the argument of `ExprKind::Ret` to a callback, but what we +/// need here is `ExprKind::Ret` itself. +/// 2. We can't trace current loop depth with `find_all_ret_expressions`. +#[derive(Default)] +struct RetCollector { + spans: Vec<Span>, + ret_in_loop: bool, + loop_depth: u16, +} + +impl<'tcx> Visitor<'tcx> for RetCollector { + fn visit_expr(&mut self, expr: &Expr<'_>) { + match expr.kind { + ExprKind::Ret(..) => { + if self.loop_depth > 0 && !self.ret_in_loop { + self.ret_in_loop = true; + } + + self.spans.push(expr.span); + }, + + ExprKind::Loop(..) => { + self.loop_depth += 1; + walk_expr(self, expr); + self.loop_depth -= 1; + return; + }, + + _ => {}, + } + + walk_expr(self, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_late_init.rs b/src/tools/clippy/clippy_lints/src/needless_late_init.rs new file mode 100644 index 000000000..ff2999b1f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_late_init.rs @@ -0,0 +1,390 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::path_to_local; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::needs_ordered_drop; +use clippy_utils::visitors::{expr_visitor, expr_visitor_no_bodies, is_local_used}; +use rustc_errors::{Applicability, MultiSpan}; +use rustc_hir::intravisit::Visitor; +use rustc_hir::{ + BindingAnnotation, Block, Expr, ExprKind, HirId, Local, LocalSource, MatchSource, Node, Pat, PatKind, Stmt, + StmtKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for late initializations that can be replaced by a `let` statement + /// with an initializer. + /// + /// ### Why is this bad? + /// Assigning in the `let` statement is less repetitive. + /// + /// ### Example + /// ```rust + /// let a; + /// a = 1; + /// + /// let b; + /// match 3 { + /// 0 => b = "zero", + /// 1 => b = "one", + /// _ => b = "many", + /// } + /// + /// let c; + /// if true { + /// c = 1; + /// } else { + /// c = -1; + /// } + /// ``` + /// Use instead: + /// ```rust + /// let a = 1; + /// + /// let b = match 3 { + /// 0 => "zero", + /// 1 => "one", + /// _ => "many", + /// }; + /// + /// let c = if true { + /// 1 + /// } else { + /// -1 + /// }; + /// ``` + #[clippy::version = "1.59.0"] + pub NEEDLESS_LATE_INIT, + style, + "late initializations that can be replaced by a `let` statement with an initializer" +} +declare_lint_pass!(NeedlessLateInit => [NEEDLESS_LATE_INIT]); + +fn contains_assign_expr<'tcx>(cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) -> bool { + let mut seen = false; + expr_visitor(cx, |expr| { + if let ExprKind::Assign(..) = expr.kind { + seen = true; + } + + !seen + }) + .visit_stmt(stmt); + + seen +} + +fn contains_let(cond: &Expr<'_>) -> bool { + let mut seen = false; + expr_visitor_no_bodies(|expr| { + if let ExprKind::Let(_) = expr.kind { + seen = true; + } + + !seen + }) + .visit_expr(cond); + + seen +} + +fn stmt_needs_ordered_drop(cx: &LateContext<'_>, stmt: &Stmt<'_>) -> bool { + let StmtKind::Local(local) = stmt.kind else { return false }; + !local.pat.walk_short(|pat| { + if let PatKind::Binding(.., None) = pat.kind { + !needs_ordered_drop(cx, cx.typeck_results().pat_ty(pat)) + } else { + true + } + }) +} + +#[derive(Debug)] +struct LocalAssign { + lhs_id: HirId, + lhs_span: Span, + rhs_span: Span, + span: Span, +} + +impl LocalAssign { + fn from_expr(expr: &Expr<'_>, span: Span) -> Option<Self> { + if let ExprKind::Assign(lhs, rhs, _) = expr.kind { + if lhs.span.from_expansion() { + return None; + } + + Some(Self { + lhs_id: path_to_local(lhs)?, + lhs_span: lhs.span, + rhs_span: rhs.span.source_callsite(), + span, + }) + } else { + None + } + } + + fn new<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, binding_id: HirId) -> Option<LocalAssign> { + let assign = match expr.kind { + ExprKind::Block(Block { expr: Some(expr), .. }, _) => Self::from_expr(expr, expr.span), + ExprKind::Block(block, _) => { + if_chain! { + if let Some((last, other_stmts)) = block.stmts.split_last(); + if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = last.kind; + + let assign = Self::from_expr(expr, last.span)?; + + // avoid visiting if not needed + if assign.lhs_id == binding_id; + if other_stmts.iter().all(|stmt| !contains_assign_expr(cx, stmt)); + + then { + Some(assign) + } else { + None + } + } + }, + ExprKind::Assign(..) => Self::from_expr(expr, expr.span), + _ => None, + }?; + + if assign.lhs_id == binding_id { + Some(assign) + } else { + None + } + } +} + +fn assignment_suggestions<'tcx>( + cx: &LateContext<'tcx>, + binding_id: HirId, + exprs: impl IntoIterator<Item = &'tcx Expr<'tcx>>, +) -> Option<(Applicability, Vec<(Span, String)>)> { + let mut assignments = Vec::new(); + + for expr in exprs { + let ty = cx.typeck_results().expr_ty(expr); + + if ty.is_never() { + continue; + } + if !ty.is_unit() { + return None; + } + + let assign = LocalAssign::new(cx, expr, binding_id)?; + + assignments.push(assign); + } + + let suggestions = assignments + .iter() + .flat_map(|assignment| { + [ + assignment.span.until(assignment.rhs_span), + assignment.rhs_span.shrink_to_hi().with_hi(assignment.span.hi()), + ] + }) + .map(|span| (span, String::new())) + .collect::<Vec<(Span, String)>>(); + + match suggestions.len() { + // All of `exprs` are never types + // https://github.com/rust-lang/rust-clippy/issues/8911 + 0 => None, + 1 => Some((Applicability::MachineApplicable, suggestions)), + // multiple suggestions don't work with rustfix in multipart_suggest + // https://github.com/rust-lang/rustfix/issues/141 + _ => Some((Applicability::Unspecified, suggestions)), + } +} + +struct Usage<'tcx> { + stmt: &'tcx Stmt<'tcx>, + expr: &'tcx Expr<'tcx>, + needs_semi: bool, +} + +fn first_usage<'tcx>( + cx: &LateContext<'tcx>, + binding_id: HirId, + local_stmt_id: HirId, + block: &'tcx Block<'tcx>, +) -> Option<Usage<'tcx>> { + let significant_drop = needs_ordered_drop(cx, cx.typeck_results().node_type(binding_id)); + + block + .stmts + .iter() + .skip_while(|stmt| stmt.hir_id != local_stmt_id) + .skip(1) + .take_while(|stmt| !significant_drop || !stmt_needs_ordered_drop(cx, stmt)) + .find(|&stmt| is_local_used(cx, stmt, binding_id)) + .and_then(|stmt| match stmt.kind { + StmtKind::Expr(expr) => Some(Usage { + stmt, + expr, + needs_semi: true, + }), + StmtKind::Semi(expr) => Some(Usage { + stmt, + expr, + needs_semi: false, + }), + _ => None, + }) +} + +fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> { + let span = local.span.with_hi(match local.ty { + // let <pat>: <ty>; + // ~~~~~~~~~~~~~~~ + Some(ty) => ty.span.hi(), + // let <pat>; + // ~~~~~~~~~ + None => local.pat.span.hi(), + }); + + snippet_opt(cx, span) +} + +fn check<'tcx>( + cx: &LateContext<'tcx>, + local: &'tcx Local<'tcx>, + local_stmt: &'tcx Stmt<'tcx>, + block: &'tcx Block<'tcx>, + binding_id: HirId, +) -> Option<()> { + let usage = first_usage(cx, binding_id, local_stmt.hir_id, block)?; + let binding_name = cx.tcx.hir().opt_name(binding_id)?; + let let_snippet = local_snippet_without_semicolon(cx, local)?; + + match usage.expr.kind { + ExprKind::Assign(..) => { + let assign = LocalAssign::new(cx, usage.expr, binding_id)?; + let mut msg_span = MultiSpan::from_spans(vec![local_stmt.span, assign.span]); + msg_span.push_span_label(local_stmt.span, "created here"); + msg_span.push_span_label(assign.span, "initialised here"); + + span_lint_and_then( + cx, + NEEDLESS_LATE_INIT, + msg_span, + "unneeded late initialization", + |diag| { + diag.tool_only_span_suggestion( + local_stmt.span, + "remove the local", + "", + Applicability::MachineApplicable, + ); + + diag.span_suggestion( + assign.lhs_span, + &format!("declare `{}` here", binding_name), + let_snippet, + Applicability::MachineApplicable, + ); + }, + ); + }, + ExprKind::If(cond, then_expr, Some(else_expr)) if !contains_let(cond) => { + let (applicability, suggestions) = assignment_suggestions(cx, binding_id, [then_expr, else_expr])?; + + span_lint_and_then( + cx, + NEEDLESS_LATE_INIT, + local_stmt.span, + "unneeded late initialization", + |diag| { + diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); + + diag.span_suggestion_verbose( + usage.stmt.span.shrink_to_lo(), + &format!("declare `{}` here", binding_name), + format!("{} = ", let_snippet), + applicability, + ); + + diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability); + + if usage.needs_semi { + diag.span_suggestion( + usage.stmt.span.shrink_to_hi(), + "add a semicolon after the `if` expression", + ";", + applicability, + ); + } + }, + ); + }, + ExprKind::Match(_, arms, MatchSource::Normal) => { + let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?; + + span_lint_and_then( + cx, + NEEDLESS_LATE_INIT, + local_stmt.span, + "unneeded late initialization", + |diag| { + diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); + + diag.span_suggestion_verbose( + usage.stmt.span.shrink_to_lo(), + &format!("declare `{}` here", binding_name), + format!("{} = ", let_snippet), + applicability, + ); + + diag.multipart_suggestion( + "remove the assignments from the `match` arms", + suggestions, + applicability, + ); + + if usage.needs_semi { + diag.span_suggestion( + usage.stmt.span.shrink_to_hi(), + "add a semicolon after the `match` expression", + ";", + applicability, + ); + } + }, + ); + }, + _ => {}, + }; + + Some(()) +} + +impl<'tcx> LateLintPass<'tcx> for NeedlessLateInit { + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { + let mut parents = cx.tcx.hir().parent_iter(local.hir_id); + if_chain! { + if let Local { + init: None, + pat: &Pat { + kind: PatKind::Binding(BindingAnnotation::Unannotated, binding_id, _, None), + .. + }, + source: LocalSource::Normal, + .. + } = local; + if let Some((_, Node::Stmt(local_stmt))) = parents.next(); + if let Some((_, Node::Block(block))) = parents.next(); + + then { + check(cx, local, local_stmt, block, binding_id); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_parens_on_range_literals.rs b/src/tools/clippy/clippy_lints/src/needless_parens_on_range_literals.rs new file mode 100644 index 000000000..6e54b243c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_parens_on_range_literals.rs @@ -0,0 +1,87 @@ +use clippy_utils::{ + diagnostics::span_lint_and_then, + higher, + source::{snippet, snippet_with_applicability}, +}; + +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; + +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// The lint checks for parenthesis on literals in range statements that are + /// superfluous. + /// + /// ### Why is this bad? + /// Having superfluous parenthesis makes the code less readable + /// overhead when reading. + /// + /// ### Example + /// + /// ```rust + /// for i in (0)..10 { + /// println!("{i}"); + /// } + /// ``` + /// + /// Use instead: + /// + /// ```rust + /// for i in 0..10 { + /// println!("{i}"); + /// } + /// ``` + #[clippy::version = "1.63.0"] + pub NEEDLESS_PARENS_ON_RANGE_LITERALS, + style, + "needless parenthesis on range literals can be removed" +} + +declare_lint_pass!(NeedlessParensOnRangeLiterals => [NEEDLESS_PARENS_ON_RANGE_LITERALS]); + +fn snippet_enclosed_in_parenthesis(snippet: &str) -> bool { + snippet.starts_with('(') && snippet.ends_with(')') +} + +fn check_for_parens(cx: &LateContext<'_>, e: &Expr<'_>, is_start: bool) { + if is_start && + let ExprKind::Lit(ref literal) = e.kind && + let ast::LitKind::Float(_sym, ast::LitFloatType::Unsuffixed) = literal.node + { + // don't check floating point literals on the start expression of a range + return; + } + if_chain! { + if let ExprKind::Lit(ref literal) = e.kind; + // the indicator that parenthesis surround the literal is that the span of the expression and the literal differ + if (literal.span.data().hi - literal.span.data().lo) != (e.span.data().hi - e.span.data().lo); + // inspect the source code of the expression for parenthesis + if snippet_enclosed_in_parenthesis(&snippet(cx, e.span, "")); + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_then(cx, NEEDLESS_PARENS_ON_RANGE_LITERALS, e.span, + "needless parenthesis on range literals can be removed", + |diag| { + let suggestion = snippet_with_applicability(cx, literal.span, "_", &mut applicability); + diag.span_suggestion(e.span, "try", suggestion, applicability); + }); + } + } +} + +impl<'tcx> LateLintPass<'tcx> for NeedlessParensOnRangeLiterals { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let Some(higher::Range { start, end, .. }) = higher::Range::hir(expr) { + if let Some(start) = start { + check_for_parens(cx, start, true); + } + if let Some(end) = end { + check_for_parens(cx, end, false); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_pass_by_value.rs b/src/tools/clippy/clippy_lints/src/needless_pass_by_value.rs new file mode 100644 index 000000000..0cbef1c95 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_pass_by_value.rs @@ -0,0 +1,347 @@ +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::ptr::get_spans; +use clippy_utils::source::{snippet, snippet_opt}; +use clippy_utils::ty::{implements_trait, is_copy, is_type_diagnostic_item}; +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, Node, PatKind, QPath, TyKind}; +use rustc_hir::{HirIdMap, HirIdSet}; +use rustc_infer::infer::TyCtxtInferExt; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::mir::FakeReadCause; +use rustc_middle::ty::{self, TypeVisitable}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::kw; +use rustc_span::{sym, Span}; +use rustc_target::spec::abi::Abi; +use rustc_trait_selection::traits; +use rustc_trait_selection::traits::misc::can_type_implement_copy; +use rustc_typeck::expr_use_visitor as euv; +use std::borrow::Cow; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions taking arguments by value, but not + /// consuming them in its + /// body. + /// + /// ### Why is this bad? + /// Taking arguments by reference is more flexible and can + /// sometimes avoid + /// unnecessary allocations. + /// + /// ### Known problems + /// * This lint suggests taking an argument by reference, + /// however sometimes it is better to let users decide the argument type + /// (by using `Borrow` trait, for example), depending on how the function is used. + /// + /// ### Example + /// ```rust + /// fn foo(v: Vec<i32>) { + /// assert_eq!(v.len(), 42); + /// } + /// ``` + /// should be + /// ```rust + /// fn foo(v: &[i32]) { + /// assert_eq!(v.len(), 42); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_PASS_BY_VALUE, + pedantic, + "functions taking arguments by value, but not consuming them in its body" +} + +declare_lint_pass!(NeedlessPassByValue => [NEEDLESS_PASS_BY_VALUE]); + +macro_rules! need { + ($e: expr) => { + if let Some(x) = $e { + x + } else { + return; + } + }; +} + +impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { + #[expect(clippy::too_many_lines)] + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + span: Span, + hir_id: HirId, + ) { + if span.from_expansion() { + return; + } + + match kind { + FnKind::ItemFn(.., header) => { + let attrs = cx.tcx.hir().attrs(hir_id); + if header.abi != Abi::Rust || requires_exact_signature(attrs) { + return; + } + }, + FnKind::Method(..) => (), + FnKind::Closure => return, + } + + // Exclude non-inherent impls + if let Some(Node::Item(item)) = cx.tcx.hir().find(cx.tcx.hir().get_parent_node(hir_id)) { + if matches!( + item.kind, + ItemKind::Impl(Impl { of_trait: Some(_), .. }) | ItemKind::Trait(..) + ) { + return; + } + } + + // Allow `Borrow` or functions to be taken by value + let allowed_traits = [ + need!(cx.tcx.lang_items().fn_trait()), + need!(cx.tcx.lang_items().fn_once_trait()), + need!(cx.tcx.lang_items().fn_mut_trait()), + need!(get_trait_def_id(cx, &paths::RANGE_ARGUMENT_TRAIT)), + ]; + + let sized_trait = need!(cx.tcx.lang_items().sized_trait()); + + let fn_def_id = cx.tcx.hir().local_def_id(hir_id); + + let preds = traits::elaborate_predicates(cx.tcx, cx.param_env.caller_bounds().iter()) + .filter(|p| !p.is_global()) + .filter_map(|obligation| { + // Note that we do not want to deal with qualified predicates here. + match obligation.predicate.kind().no_bound_vars() { + Some(ty::PredicateKind::Trait(pred)) if pred.def_id() != sized_trait => Some(pred), + _ => None, + } + }) + .collect::<Vec<_>>(); + + // Collect moved variables and spans which will need dereferencings from the + // function body. + let MovedVariablesCtxt { + moved_vars, + spans_need_deref, + .. + } = { + let mut ctx = MovedVariablesCtxt::default(); + cx.tcx.infer_ctxt().enter(|infcx| { + euv::ExprUseVisitor::new(&mut ctx, &infcx, fn_def_id, cx.param_env, cx.typeck_results()) + .consume_body(body); + }); + ctx + }; + + let fn_sig = cx.tcx.fn_sig(fn_def_id); + let fn_sig = cx.tcx.erase_late_bound_regions(fn_sig); + + for (idx, ((input, &ty), arg)) in decl.inputs.iter().zip(fn_sig.inputs()).zip(body.params).enumerate() { + // All spans generated from a proc-macro invocation are the same... + if span == input.span { + return; + } + + // Ignore `self`s. + if idx == 0 { + if let PatKind::Binding(.., ident, _) = arg.pat.kind { + if ident.name == kw::SelfLower { + continue; + } + } + } + + // + // * Exclude a type that is specifically bounded by `Borrow`. + // * Exclude a type whose reference also fulfills its bound. (e.g., `std::convert::AsRef`, + // `serde::Serialize`) + let (implements_borrow_trait, all_borrowable_trait) = { + let preds = preds.iter().filter(|t| t.self_ty() == ty).collect::<Vec<_>>(); + + ( + preds.iter().any(|t| cx.tcx.is_diagnostic_item(sym::Borrow, t.def_id())), + !preds.is_empty() && { + let ty_empty_region = cx.tcx.mk_imm_ref(cx.tcx.lifetimes.re_root_empty, ty); + preds.iter().all(|t| { + let ty_params = t.trait_ref.substs.iter().skip(1).collect::<Vec<_>>(); + implements_trait(cx, ty_empty_region, t.def_id(), &ty_params) + }) + }, + ) + }; + + if_chain! { + if !is_self(arg); + if !ty.is_mutable_ptr(); + if !is_copy(cx, ty); + if !allowed_traits.iter().any(|&t| implements_trait(cx, ty, t, &[])); + if !implements_borrow_trait; + if !all_borrowable_trait; + + if let PatKind::Binding(mode, canonical_id, ..) = arg.pat.kind; + if !moved_vars.contains(&canonical_id); + then { + if mode == BindingAnnotation::Mutable || mode == BindingAnnotation::RefMut { + continue; + } + + // Dereference suggestion + let sugg = |diag: &mut Diagnostic| { + if let ty::Adt(def, ..) = ty.kind() { + if let Some(span) = cx.tcx.hir().span_if_local(def.did()) { + if can_type_implement_copy( + cx.tcx, + cx.param_env, + ty, + traits::ObligationCause::dummy_with_span(span), + ).is_ok() { + diag.span_help(span, "consider marking this type as `Copy`"); + } + } + } + + 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) = + get_spans(cx, Some(body.id()), idx, &[("clone", ".to_owned()")]); + if let TyKind::Path(QPath::Resolved(_, path)) = input.kind; + if let Some(elem_ty) = path.segments.iter() + .find(|seg| seg.ident.name == sym::Vec) + .and_then(|ps| ps.args.as_ref()) + .map(|params| params.args.iter().find_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }).unwrap()); + then { + let slice_ty = format!("&[{}]", snippet(cx, elem_ty.span, "_")); + diag.span_suggestion( + input.span, + "consider changing the type to", + slice_ty, + Applicability::Unspecified, + ); + + for (span, suggestion) in clone_spans { + diag.span_suggestion( + span, + snippet_opt(cx, span) + .map_or( + "change the call to".into(), + |x| Cow::from(format!("change `{}` to", x)), + ) + .as_ref(), + suggestion, + Applicability::Unspecified, + ); + } + + // cannot be destructured, no need for `*` suggestion + assert!(deref_span.is_none()); + return; + } + } + + if is_type_diagnostic_item(cx, ty, sym::String) { + if let Some(clone_spans) = + get_spans(cx, Some(body.id()), idx, &[("clone", ".to_string()"), ("as_str", "")]) { + diag.span_suggestion( + input.span, + "consider changing the type to", + "&str", + Applicability::Unspecified, + ); + + for (span, suggestion) in clone_spans { + diag.span_suggestion( + span, + snippet_opt(cx, span) + .map_or( + "change the call to".into(), + |x| Cow::from(format!("change `{}` to", x)) + ) + .as_ref(), + suggestion, + Applicability::Unspecified, + ); + } + + 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); + } + multispan_sugg(diag, "consider taking a reference instead", spans); + }; + + span_lint_and_then( + cx, + NEEDLESS_PASS_BY_VALUE, + input.span, + "this argument is passed by value, but not consumed in the function body", + sugg, + ); + } + } + } + } +} + +/// Functions marked with these attributes must have the exact signature. +fn requires_exact_signature(attrs: &[Attribute]) -> bool { + attrs.iter().any(|attr| { + [sym::proc_macro, sym::proc_macro_attribute, sym::proc_macro_derive] + .iter() + .any(|&allow| attr.has_name(allow)) + }) +} + +#[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 { + fn move_common(&mut self, cmt: &euv::PlaceWithHirId<'_>) { + if let euv::PlaceBase::Local(vid) = cmt.place.base { + self.moved_vars.insert(vid); + } + } +} + +impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt { + fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, _: HirId) { + self.move_common(cmt); + } + + fn borrow(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: HirId, _: ty::BorrowKind) {} + + fn mutate(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: HirId) {} + + fn fake_read(&mut self, _: &rustc_typeck::expr_use_visitor::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {} +} diff --git a/src/tools/clippy/clippy_lints/src/needless_question_mark.rs b/src/tools/clippy/clippy_lints/src/needless_question_mark.rs new file mode 100644 index 000000000..8f85b0059 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_question_mark.rs @@ -0,0 +1,143 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_lang_ctor; +use clippy_utils::source::snippet; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{OptionSome, ResultOk}; +use rustc_hir::{AsyncGeneratorKind, Block, Body, Expr, ExprKind, GeneratorKind, LangItem, MatchSource, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Suggests alternatives for useless applications of `?` in terminating expressions + /// + /// ### Why is this bad? + /// There's no reason to use `?` to short-circuit when execution of the body will end there anyway. + /// + /// ### Example + /// ```rust + /// struct TO { + /// magic: Option<usize>, + /// } + /// + /// fn f(to: TO) -> Option<usize> { + /// Some(to.magic?) + /// } + /// + /// struct TR { + /// magic: Result<usize, bool>, + /// } + /// + /// fn g(tr: Result<TR, bool>) -> Result<usize, bool> { + /// tr.and_then(|t| Ok(t.magic?)) + /// } + /// + /// ``` + /// Use instead: + /// ```rust + /// struct TO { + /// magic: Option<usize>, + /// } + /// + /// fn f(to: TO) -> Option<usize> { + /// to.magic + /// } + /// + /// struct TR { + /// magic: Result<usize, bool>, + /// } + /// + /// fn g(tr: Result<TR, bool>) -> Result<usize, bool> { + /// tr.and_then(|t| t.magic) + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub NEEDLESS_QUESTION_MARK, + complexity, + "Suggest `value.inner_option` instead of `Some(value.inner_option?)`. The same goes for `Result<T, E>`." +} + +declare_lint_pass!(NeedlessQuestionMark => [NEEDLESS_QUESTION_MARK]); + +impl LateLintPass<'_> for NeedlessQuestionMark { + /* + * The question mark operator is compatible with both Result<T, E> and Option<T>, + * from Rust 1.13 and 1.22 respectively. + */ + + /* + * What do we match: + * Expressions that look like this: + * Some(option?), Ok(result?) + * + * Where do we match: + * Last expression of a body + * Return statement + * A body's value (single line closure) + * + * What do we not match: + * Implicit calls to `from(..)` on the error value + */ + + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) { + if let ExprKind::Ret(Some(e)) = expr.kind { + check(cx, e); + } + } + + fn check_body(&mut self, cx: &LateContext<'_>, body: &'_ Body<'_>) { + if let Some(GeneratorKind::Async(AsyncGeneratorKind::Fn)) = body.generator_kind { + if let ExprKind::Block( + Block { + expr: + Some(Expr { + kind: ExprKind::DropTemps(async_body), + .. + }), + .. + }, + _, + ) = body.value.kind + { + if let ExprKind::Block(Block { expr: Some(expr), .. }, ..) = async_body.kind { + check(cx, expr); + } + } + } else { + check(cx, body.value.peel_blocks()); + } + } +} + +fn check(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::Call(path, [arg]) = &expr.kind; + if let ExprKind::Path(ref qpath) = &path.kind; + let sugg_remove = if is_lang_ctor(cx, qpath, OptionSome) { + "Some()" + } else if is_lang_ctor(cx, qpath, ResultOk) { + "Ok()" + } else { + return; + }; + if let ExprKind::Match(inner_expr_with_q, _, MatchSource::TryDesugar) = &arg.kind; + if let ExprKind::Call(called, [inner_expr]) = &inner_expr_with_q.kind; + if let ExprKind::Path(QPath::LangItem(LangItem::TryTraitBranch, ..)) = &called.kind; + if expr.span.ctxt() == inner_expr.span.ctxt(); + let expr_ty = cx.typeck_results().expr_ty(expr); + let inner_ty = cx.typeck_results().expr_ty(inner_expr); + if expr_ty == inner_ty; + then { + span_lint_and_sugg( + cx, + NEEDLESS_QUESTION_MARK, + expr.span, + "question mark operator is useless here", + &format!("try removing question mark and `{}`", sugg_remove), + format!("{}", snippet(cx, inner_expr.span, r#""...""#)), + Applicability::MachineApplicable, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/needless_update.rs b/src/tools/clippy/clippy_lints/src/needless_update.rs new file mode 100644 index 000000000..0bd29d177 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_update.rs @@ -0,0 +1,70 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for needlessly including a base struct on update + /// when all fields are changed anyway. + /// + /// This lint is not applied to structs marked with + /// [non_exhaustive](https://doc.rust-lang.org/reference/attributes/type_system.html). + /// + /// ### Why is this bad? + /// This will cost resources (because the base has to be + /// somewhere), and make the code less readable. + /// + /// ### Example + /// ```rust + /// # struct Point { + /// # x: i32, + /// # y: i32, + /// # z: i32, + /// # } + /// # let zero_point = Point { x: 0, y: 0, z: 0 }; + /// Point { + /// x: 1, + /// y: 1, + /// z: 1, + /// ..zero_point + /// }; + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// // Missing field `z` + /// Point { + /// x: 1, + /// y: 1, + /// ..zero_point + /// }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_UPDATE, + complexity, + "using `Foo { ..base }` when there are no missing fields" +} + +declare_lint_pass!(NeedlessUpdate => [NEEDLESS_UPDATE]); + +impl<'tcx> LateLintPass<'tcx> for NeedlessUpdate { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Struct(_, fields, Some(base)) = expr.kind { + let ty = cx.typeck_results().expr_ty(expr); + if let ty::Adt(def, _) = ty.kind() { + if fields.len() == def.non_enum_variant().fields.len() + && !def.variant(0_usize.into()).is_field_list_non_exhaustive() + { + span_lint( + cx, + NEEDLESS_UPDATE, + base.span, + "struct update has no effect, all the fields in the struct have already been specified", + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/neg_cmp_op_on_partial_ord.rs b/src/tools/clippy/clippy_lints/src/neg_cmp_op_on_partial_ord.rs new file mode 100644 index 000000000..a7e0e3578 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/neg_cmp_op_on_partial_ord.rs @@ -0,0 +1,90 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::implements_trait; +use clippy_utils::{self, get_trait_def_id, paths}; +use if_chain::if_chain; +use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp}; +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 the usage of negated comparison operators on types which only implement + /// `PartialOrd` (e.g., `f64`). + /// + /// ### Why is this bad? + /// These operators make it easy to forget that the underlying types actually allow not only three + /// potential Orderings (Less, Equal, Greater) but also a fourth one (Uncomparable). This is + /// especially easy to miss if the operator based comparison result is negated. + /// + /// ### Example + /// ```rust + /// let a = 1.0; + /// let b = f64::NAN; + /// + /// let not_less_or_equal = !(a <= b); + /// ``` + /// + /// Use instead: + /// ```rust + /// use std::cmp::Ordering; + /// # let a = 1.0; + /// # let b = f64::NAN; + /// + /// let _not_less_or_equal = match a.partial_cmp(&b) { + /// None | Some(Ordering::Greater) => true, + /// _ => false, + /// }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEG_CMP_OP_ON_PARTIAL_ORD, + complexity, + "The use of negated comparison operators on partially ordered types may produce confusing code." +} + +declare_lint_pass!(NoNegCompOpForPartialOrd => [NEG_CMP_OP_ON_PARTIAL_ORD]); + +impl<'tcx> LateLintPass<'tcx> for NoNegCompOpForPartialOrd { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + + if !in_external_macro(cx.sess(), expr.span); + if let ExprKind::Unary(UnOp::Not, inner) = expr.kind; + if let ExprKind::Binary(ref op, left, _) = inner.kind; + if let BinOpKind::Le | BinOpKind::Ge | BinOpKind::Lt | BinOpKind::Gt = op.node; + + then { + + let ty = cx.typeck_results().expr_ty(left); + + let implements_ord = { + if let Some(id) = get_trait_def_id(cx, &paths::ORD) { + implements_trait(cx, ty, id, &[]) + } else { + return; + } + }; + + let implements_partial_ord = { + if let Some(id) = cx.tcx.lang_items().partial_ord_trait() { + implements_trait(cx, ty, id, &[]) + } else { + return; + } + }; + + if implements_partial_ord && !implements_ord { + span_lint( + cx, + NEG_CMP_OP_ON_PARTIAL_ORD, + expr.span, + "the use of negated comparison operators on partially ordered \ + types produces code that is hard to read and refactor, please \ + consider using the `partial_cmp` method instead, to make it \ + clear that the two values could be incomparable" + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/neg_multiply.rs b/src/tools/clippy/clippy_lints/src/neg_multiply.rs new file mode 100644 index 000000000..b087cfb36 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/neg_multiply.rs @@ -0,0 +1,80 @@ +use clippy_utils::consts::{self, Constant}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::sugg::has_enclosing_paren; +use if_chain::if_chain; +use rustc_ast::util::parser::PREC_PREFIX; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for multiplication by -1 as a form of negation. + /// + /// ### Why is this bad? + /// It's more readable to just negate. + /// + /// ### Known problems + /// This only catches integers (for now). + /// + /// ### Example + /// ```rust,ignore + /// let a = x * -1; + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// let a = -x; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEG_MULTIPLY, + style, + "multiplying integers by `-1`" +} + +declare_lint_pass!(NegMultiply => [NEG_MULTIPLY]); + +impl<'tcx> LateLintPass<'tcx> for NegMultiply { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if let ExprKind::Binary(ref op, left, right) = e.kind { + if BinOpKind::Mul == op.node { + match (&left.kind, &right.kind) { + (&ExprKind::Unary(..), &ExprKind::Unary(..)) => {}, + (&ExprKind::Unary(UnOp::Neg, lit), _) => check_mul(cx, e.span, lit, right), + (_, &ExprKind::Unary(UnOp::Neg, lit)) => check_mul(cx, e.span, lit, left), + _ => {}, + } + } + } + } +} + +fn check_mul(cx: &LateContext<'_>, span: Span, lit: &Expr<'_>, exp: &Expr<'_>) { + if_chain! { + if let ExprKind::Lit(ref l) = lit.kind; + if consts::lit_to_mir_constant(&l.node, cx.typeck_results().expr_ty_opt(lit)) == Constant::Int(1); + if cx.typeck_results().expr_ty(exp).is_integral(); + + then { + let mut applicability = Applicability::MachineApplicable; + let snip = snippet_with_applicability(cx, exp.span, "..", &mut applicability); + let suggestion = if exp.precedence().order() < PREC_PREFIX && !has_enclosing_paren(&snip) { + format!("-({})", snip) + } else { + format!("-{}", snip) + }; + span_lint_and_sugg( + cx, + NEG_MULTIPLY, + span, + "this multiplication by -1 can be written more succinctly", + "consider using", + suggestion, + applicability, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/new_without_default.rs b/src/tools/clippy/clippy_lints/src/new_without_default.rs new file mode 100644 index 000000000..5c45ee6d9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/new_without_default.rs @@ -0,0 +1,169 @@ +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::return_ty; +use clippy_utils::source::snippet; +use clippy_utils::sugg::DiagnosticExt; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::HirIdSet; +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::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for public types with a `pub fn new() -> Self` method and no + /// implementation of + /// [`Default`](https://doc.rust-lang.org/std/default/trait.Default.html). + /// + /// ### Why is this bad? + /// The user might expect to be able to use + /// [`Default`](https://doc.rust-lang.org/std/default/trait.Default.html) as the + /// type can be constructed without arguments. + /// + /// ### Example + /// ```ignore + /// pub struct Foo(Bar); + /// + /// impl Foo { + /// pub fn new() -> Self { + /// Foo(Bar::new()) + /// } + /// } + /// ``` + /// + /// To fix the lint, add a `Default` implementation that delegates to `new`: + /// + /// ```ignore + /// pub struct Foo(Bar); + /// + /// impl Default for Foo { + /// fn default() -> Self { + /// Foo::new() + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEW_WITHOUT_DEFAULT, + style, + "`pub fn new() -> Self` method without `Default` implementation" +} + +#[derive(Clone, Default)] +pub struct NewWithoutDefault { + impling_types: Option<HirIdSet>, +} + +impl_lint_pass!(NewWithoutDefault => [NEW_WITHOUT_DEFAULT]); + +impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + if let hir::ItemKind::Impl(hir::Impl { + of_trait: None, + generics, + self_ty: impl_self_ty, + items, + .. + }) = item.kind + { + for assoc_item in *items { + if assoc_item.kind == (hir::AssocItemKind::Fn { has_self: false }) { + let impl_item = cx.tcx.hir().impl_item(assoc_item.id); + if in_external_macro(cx.sess(), impl_item.span) { + return; + } + if let hir::ImplItemKind::Fn(ref sig, _) = impl_item.kind { + let name = impl_item.ident.name; + let id = impl_item.hir_id(); + if sig.header.constness == hir::Constness::Const { + // can't be implemented by default + return; + } + if sig.header.unsafety == hir::Unsafety::Unsafe { + // can't be implemented for unsafe new + return; + } + if cx.tcx.is_doc_hidden(impl_item.def_id) { + // shouldn't be implemented when it is hidden in docs + return; + } + if !impl_item.generics.params.is_empty() { + // when the result of `new()` depends on a parameter we should not require + // an impl of `Default` + return; + } + if_chain! { + if sig.decl.inputs.is_empty(); + if name == sym::new; + if cx.access_levels.is_reachable(impl_item.def_id); + let self_def_id = cx.tcx.hir().get_parent_item(id); + let self_ty = cx.tcx.type_of(self_def_id); + if self_ty == return_ty(cx, id); + if let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default); + then { + if self.impling_types.is_none() { + let mut impls = HirIdSet::default(); + cx.tcx.for_each_impl(default_trait_id, |d| { + if let Some(ty_def) = cx.tcx.type_of(d).ty_adt_def() { + if let Some(local_def_id) = ty_def.did().as_local() { + impls.insert(cx.tcx.hir().local_def_id_to_hir_id(local_def_id)); + } + } + }); + self.impling_types = Some(impls); + } + + // Check if a Default implementation exists for the Self type, regardless of + // generics + if_chain! { + if let Some(ref impling_types) = self.impling_types; + if let Some(self_def) = cx.tcx.type_of(self_def_id).ty_adt_def(); + if let Some(self_local_did) = self_def.did().as_local(); + let self_id = cx.tcx.hir().local_def_id_to_hir_id(self_local_did); + if impling_types.contains(&self_id); + then { + return; + } + } + + let generics_sugg = snippet(cx, generics.span, ""); + let self_ty_fmt = self_ty.to_string(); + let self_type_snip = snippet(cx, impl_self_ty.span, &self_ty_fmt); + span_lint_hir_and_then( + cx, + NEW_WITHOUT_DEFAULT, + id, + impl_item.span, + &format!( + "you should consider adding a `Default` implementation for `{}`", + self_type_snip + ), + |diag| { + diag.suggest_prepend_item( + cx, + item.span, + "try adding this", + &create_new_without_default_suggest_msg(&self_type_snip, &generics_sugg), + Applicability::MaybeIncorrect, + ); + }, + ); + } + } + } + } + } + } + } +} + +fn create_new_without_default_suggest_msg(self_type_snip: &str, generics_sugg: &str) -> String { + #[rustfmt::skip] + format!( +"impl{} Default for {} {{ + fn default() -> Self {{ + Self::new() + }} +}}", generics_sugg, self_type_snip) +} diff --git a/src/tools/clippy/clippy_lints/src/no_effect.rs b/src/tools/clippy/clippy_lints/src/no_effect.rs new file mode 100644 index 000000000..819646bb6 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/no_effect.rs @@ -0,0 +1,277 @@ +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 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_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use std::ops::Deref; + +declare_clippy_lint! { + /// ### What it does + /// Checks for statements which have no effect. + /// + /// ### Why is this bad? + /// Unlike dead code, these statements are actually + /// executed. However, as they have no effect, all they do is make the code less + /// readable. + /// + /// ### Example + /// ```rust + /// 0; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NO_EFFECT, + complexity, + "statements with no effect" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for binding to underscore prefixed variable without side-effects. + /// + /// ### Why is this bad? + /// Unlike dead code, these bindings are actually + /// executed. However, as they have no effect and shouldn't be used further on, all they + /// do is make the code less readable. + /// + /// ### Known problems + /// Further usage of this variable is not checked, which can lead to false positives if it is + /// used later in the code. + /// + /// ### Example + /// ```rust,ignore + /// let _i_serve_no_purpose = 1; + /// ``` + #[clippy::version = "1.58.0"] + pub NO_EFFECT_UNDERSCORE_BINDING, + pedantic, + "binding to `_` prefixed variable with no side-effect" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for expression statements that can be reduced to a + /// sub-expression. + /// + /// ### Why is this bad? + /// Expressions by themselves often have no side-effects. + /// Having such expressions reduces readability. + /// + /// ### Example + /// ```rust,ignore + /// compute_array()[0]; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNNECESSARY_OPERATION, + complexity, + "outer expressions with no effect" +} + +declare_lint_pass!(NoEffect => [NO_EFFECT, UNNECESSARY_OPERATION, NO_EFFECT_UNDERSCORE_BINDING]); + +impl<'tcx> LateLintPass<'tcx> for NoEffect { + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + if check_no_effect(cx, stmt) { + return; + } + check_unnecessary_operation(cx, stmt); + } +} + +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"); + return true; + } + } else if let StmtKind::Local(local) = stmt.kind { + if_chain! { + if !is_lint_allowed(cx, NO_EFFECT_UNDERSCORE_BINDING, local.hir_id); + if let Some(init) = local.init; + if local.els.is_none(); + if !local.pat.span.from_expansion(); + if has_no_effect(cx, init); + if let PatKind::Binding(_, _, ident, _) = local.pat.kind; + if ident.name.to_ident_string().starts_with('_'); + then { + span_lint_hir( + cx, + NO_EFFECT_UNDERSCORE_BINDING, + init.hir_id, + stmt.span, + "binding to `_` prefixed variable with no side-effect" + ); + return true; + } + } + } + false +} + +fn has_no_effect(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + if expr.span.from_expansion() { + return false; + } + match peel_blocks(expr).kind { + ExprKind::Lit(..) | ExprKind::Closure { .. } => true, + ExprKind::Path(..) => !has_drop(cx, cx.typeck_results().expr_ty(expr)), + ExprKind::Index(a, b) | ExprKind::Binary(_, a, b) => has_no_effect(cx, a) && has_no_effect(cx, b), + ExprKind::Array(v) | ExprKind::Tup(v) => v.iter().all(|val| has_no_effect(cx, val)), + ExprKind::Repeat(inner, _) + | ExprKind::Cast(inner, _) + | ExprKind::Type(inner, _) + | ExprKind::Unary(_, inner) + | ExprKind::Field(inner, _) + | ExprKind::AddrOf(_, _, inner) + | ExprKind::Box(inner) => has_no_effect(cx, inner), + ExprKind::Struct(_, fields, ref base) => { + !has_drop(cx, cx.typeck_results().expr_ty(expr)) + && fields.iter().all(|field| has_no_effect(cx, field.expr)) + && base.as_ref().map_or(true, |base| has_no_effect(cx, base)) + }, + ExprKind::Call(callee, args) => { + if let ExprKind::Path(ref qpath) = callee.kind { + if cx.typeck_results().type_dependent_def(expr.hir_id).is_some() { + // type-dependent function call like `impl FnOnce for X` + return false; + } + let def_matched = matches!( + cx.qpath_res(qpath, callee.hir_id), + Res::Def(DefKind::Struct | DefKind::Variant | DefKind::Ctor(..), ..) + ); + if def_matched || is_range_literal(expr) { + !has_drop(cx, cx.typeck_results().expr_ty(expr)) && args.iter().all(|arg| has_no_effect(cx, arg)) + } else { + false + } + } else { + false + } + }, + _ => false, + } +} + +fn check_unnecessary_operation(cx: &LateContext<'_>, stmt: &Stmt<'_>) { + if_chain! { + if let StmtKind::Semi(expr) = stmt.kind; + if let Some(reduced) = reduce_expression(cx, expr); + if !&reduced.iter().any(|e| e.span.from_expansion()); + then { + if let ExprKind::Index(..) = &expr.kind { + let snippet = if let (Some(arr), Some(func)) = + (snippet_opt(cx, reduced[0].span), snippet_opt(cx, reduced[1].span)) + { + format!("assert!({}.len() > {});", &arr, &func) + } else { + return; + }; + span_lint_hir_and_then( + cx, + UNNECESSARY_OPERATION, + expr.hir_id, + stmt.span, + "unnecessary operation", + |diag| { + diag.span_suggestion( + stmt.span, + "statement can be written as", + snippet, + Applicability::MaybeIncorrect, + ); + }, + ); + } else { + let mut snippet = String::new(); + for e in reduced { + if let Some(snip) = snippet_opt(cx, e.span) { + snippet.push_str(&snip); + snippet.push(';'); + } else { + return; + } + } + span_lint_hir_and_then( + cx, + UNNECESSARY_OPERATION, + expr.hir_id, + stmt.span, + "unnecessary operation", + |diag| { + diag.span_suggestion( + stmt.span, + "statement can be reduced to", + snippet, + Applicability::MachineApplicable, + ); + }, + ); + } + } + } +} + +fn reduce_expression<'a>(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<Vec<&'a Expr<'a>>> { + if expr.span.from_expansion() { + return None; + } + match expr.kind { + ExprKind::Index(a, b) => Some(vec![a, b]), + ExprKind::Binary(ref binop, a, b) if binop.node != BinOpKind::And && binop.node != BinOpKind::Or => { + Some(vec![a, b]) + }, + ExprKind::Array(v) | ExprKind::Tup(v) => Some(v.iter().collect()), + ExprKind::Repeat(inner, _) + | ExprKind::Cast(inner, _) + | ExprKind::Type(inner, _) + | ExprKind::Unary(_, inner) + | ExprKind::Field(inner, _) + | ExprKind::AddrOf(_, _, inner) + | ExprKind::Box(inner) => reduce_expression(cx, inner).or_else(|| Some(vec![inner])), + ExprKind::Struct(_, fields, ref base) => { + if has_drop(cx, cx.typeck_results().expr_ty(expr)) { + None + } else { + Some(fields.iter().map(|f| &f.expr).chain(base).map(Deref::deref).collect()) + } + }, + ExprKind::Call(callee, args) => { + if let ExprKind::Path(ref qpath) = callee.kind { + if cx.typeck_results().type_dependent_def(expr.hir_id).is_some() { + // type-dependent function call like `impl FnOnce for X` + return None; + } + let res = cx.qpath_res(qpath, callee.hir_id); + match res { + Res::Def(DefKind::Struct | DefKind::Variant | DefKind::Ctor(..), ..) + if !has_drop(cx, cx.typeck_results().expr_ty(expr)) => + { + Some(args.iter().collect()) + }, + _ => None, + } + } else { + None + } + }, + ExprKind::Block(block, _) => { + if block.stmts.is_empty() { + block.expr.as_ref().and_then(|e| { + match block.rules { + BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) => None, + BlockCheckMode::DefaultBlock => Some(vec![&**e]), + // in case of compiler-inserted signaling blocks + BlockCheckMode::UnsafeBlock(_) => reduce_expression(cx, e), + } + }) + } else { + None + } + }, + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/non_copy_const.rs b/src/tools/clippy/clippy_lints/src/non_copy_const.rs new file mode 100644 index 000000000..72c86f28b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/non_copy_const.rs @@ -0,0 +1,449 @@ +//! Checks for uses of const which the type is not `Freeze` (`Cell`-free). +//! +//! This lint is **warn** by default. + +use std::ptr; + +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::in_constant; +use clippy_utils::macros::macro_backtrace; +use if_chain::if_chain; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::def_id::DefId; +use rustc_hir::{ + BodyId, Expr, ExprKind, HirId, Impl, ImplItem, ImplItemKind, Item, ItemKind, Node, TraitItem, TraitItemKind, UnOp, +}; +use rustc_lint::{LateContext, LateLintPass, Lint}; +use rustc_middle::mir; +use rustc_middle::mir::interpret::{ConstValue, ErrorHandled}; +use rustc_middle::ty::adjustment::Adjust; +use rustc_middle::ty::{self, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, InnerSpan, Span, DUMMY_SP}; +use rustc_typeck::hir_ty_to_ty; + +// FIXME: this is a correctness problem but there's no suitable +// warn-by-default category. +declare_clippy_lint! { + /// ### What it does + /// Checks for declaration of `const` items which is interior + /// mutable (e.g., contains a `Cell`, `Mutex`, `AtomicXxxx`, etc.). + /// + /// ### Why is this bad? + /// Consts are copied everywhere they are referenced, i.e., + /// every time you refer to the const a fresh instance of the `Cell` or `Mutex` + /// or `AtomicXxxx` will be created, which defeats the whole purpose of using + /// these types in the first place. + /// + /// The `const` should better be replaced by a `static` item if a global + /// variable is wanted, or replaced by a `const fn` if a constructor is wanted. + /// + /// ### Known problems + /// A "non-constant" const item is a legacy way to supply an + /// initialized value to downstream `static` items (e.g., the + /// `std::sync::ONCE_INIT` constant). In this case the use of `const` is legit, + /// and this lint should be suppressed. + /// + /// Even though the lint avoids triggering on a constant whose type has enums that have variants + /// with interior mutability, and its value uses non interior mutable variants (see + /// [#3962](https://github.com/rust-lang/rust-clippy/issues/3962) and + /// [#3825](https://github.com/rust-lang/rust-clippy/issues/3825) for examples); + /// it complains about associated constants without default values only based on its types; + /// which might not be preferable. + /// There're other enums plus associated constants cases that the lint cannot handle. + /// + /// Types that have underlying or potential interior mutability trigger the lint whether + /// the interior mutable field is used or not. See issues + /// [#5812](https://github.com/rust-lang/rust-clippy/issues/5812) and + /// + /// ### Example + /// ```rust + /// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + /// + /// const CONST_ATOM: AtomicUsize = AtomicUsize::new(12); + /// CONST_ATOM.store(6, SeqCst); // the content of the atomic is unchanged + /// assert_eq!(CONST_ATOM.load(SeqCst), 12); // because the CONST_ATOM in these lines are distinct + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + /// static STATIC_ATOM: AtomicUsize = AtomicUsize::new(15); + /// STATIC_ATOM.store(9, SeqCst); + /// assert_eq!(STATIC_ATOM.load(SeqCst), 9); // use a `static` item to refer to the same instance + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DECLARE_INTERIOR_MUTABLE_CONST, + style, + "declaring `const` with interior mutability" +} + +// FIXME: this is a correctness problem but there's no suitable +// warn-by-default category. +declare_clippy_lint! { + /// ### What it does + /// Checks if `const` items which is interior mutable (e.g., + /// contains a `Cell`, `Mutex`, `AtomicXxxx`, etc.) has been borrowed directly. + /// + /// ### Why is this bad? + /// Consts are copied everywhere they are referenced, i.e., + /// every time you refer to the const a fresh instance of the `Cell` or `Mutex` + /// or `AtomicXxxx` will be created, which defeats the whole purpose of using + /// these types in the first place. + /// + /// The `const` value should be stored inside a `static` item. + /// + /// ### Known problems + /// When an enum has variants with interior mutability, use of its non + /// interior mutable variants can generate false positives. See issue + /// [#3962](https://github.com/rust-lang/rust-clippy/issues/3962) + /// + /// Types that have underlying or potential interior mutability trigger the lint whether + /// the interior mutable field is used or not. See issues + /// [#5812](https://github.com/rust-lang/rust-clippy/issues/5812) and + /// [#3825](https://github.com/rust-lang/rust-clippy/issues/3825) + /// + /// ### Example + /// ```rust + /// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + /// const CONST_ATOM: AtomicUsize = AtomicUsize::new(12); + /// + /// CONST_ATOM.store(6, SeqCst); // the content of the atomic is unchanged + /// assert_eq!(CONST_ATOM.load(SeqCst), 12); // because the CONST_ATOM in these lines are distinct + /// ``` + /// + /// Use instead: + /// ```rust + /// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + /// const CONST_ATOM: AtomicUsize = AtomicUsize::new(12); + /// + /// static STATIC_ATOM: AtomicUsize = CONST_ATOM; + /// STATIC_ATOM.store(9, SeqCst); + /// assert_eq!(STATIC_ATOM.load(SeqCst), 9); // use a `static` item to refer to the same instance + /// ``` + #[clippy::version = "pre 1.29.0"] + pub BORROW_INTERIOR_MUTABLE_CONST, + style, + "referencing `const` with interior mutability" +} + +fn is_unfrozen<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + // Ignore types whose layout is unknown since `is_freeze` reports every generic types as `!Freeze`, + // making it indistinguishable from `UnsafeCell`. i.e. it isn't a tool to prove a type is + // 'unfrozen'. However, this code causes a false negative in which + // a type contains a layout-unknown type, but also an unsafe cell like `const CELL: Cell<T>`. + // Yet, it's better than `ty.has_type_flags(TypeFlags::HAS_TY_PARAM | TypeFlags::HAS_PROJECTION)` + // since it works when a pointer indirection involves (`Cell<*const T>`). + // Making up a `ParamEnv` where every generic params and assoc types are `Freeze`is another option; + // but I'm not sure whether it's a decent way, if possible. + cx.tcx.layout_of(cx.param_env.and(ty)).is_ok() && !ty.is_freeze(cx.tcx.at(DUMMY_SP), cx.param_env) +} + +fn is_value_unfrozen_raw<'tcx>( + cx: &LateContext<'tcx>, + result: Result<ConstValue<'tcx>, ErrorHandled>, + ty: Ty<'tcx>, +) -> bool { + fn inner<'tcx>(cx: &LateContext<'tcx>, val: mir::ConstantKind<'tcx>) -> bool { + match val.ty().kind() { + // the fact that we have to dig into every structs to search enums + // leads us to the point checking `UnsafeCell` directly is the only option. + ty::Adt(ty_def, ..) if ty_def.is_unsafe_cell() => true, + ty::Array(..) | ty::Adt(..) | ty::Tuple(..) => { + let val = cx.tcx.destructure_mir_constant(cx.param_env, val); + val.fields.iter().any(|field| inner(cx, *field)) + }, + _ => false, + } + } + result.map_or_else( + |err| { + // Consider `TooGeneric` cases as being unfrozen. + // This causes a false positive where an assoc const whose type is unfrozen + // 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`). + // 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 + // case (that actually removes another suboptimal behavior (I won't say 'false positive') where, + // similar to 2., but with the a frozen variant) (e.g. borrowing + // `borrow_interior_mutable_const::enums::AssocConsts::TO_BE_FROZEN_VARIANT`). + // I chose this way because unfrozen enums as assoc consts are rare (or, hopefully, none). + err == ErrorHandled::TooGeneric + }, + |val| inner(cx, mir::ConstantKind::from_value(val, ty)), + ) +} + +fn is_value_unfrozen_poly<'tcx>(cx: &LateContext<'tcx>, body_id: BodyId, ty: Ty<'tcx>) -> bool { + let result = cx.tcx.const_eval_poly(body_id.hir_id.owner.to_def_id()); + is_value_unfrozen_raw(cx, result, ty) +} + +fn is_value_unfrozen_expr<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId, def_id: DefId, ty: Ty<'tcx>) -> bool { + let substs = cx.typeck_results().node_substs(hir_id); + + let result = cx.tcx.const_eval_resolve( + cx.param_env, + ty::Unevaluated::new(ty::WithOptConstParam::unknown(def_id), substs), + None, + ); + is_value_unfrozen_raw(cx, result, ty) +} + +#[derive(Copy, Clone)] +enum Source { + Item { item: Span }, + Assoc { item: Span }, + Expr { expr: Span }, +} + +impl Source { + #[must_use] + fn lint(&self) -> (&'static Lint, &'static str, Span) { + match self { + Self::Item { item } | Self::Assoc { item, .. } => ( + DECLARE_INTERIOR_MUTABLE_CONST, + "a `const` item should never be interior mutable", + *item, + ), + Self::Expr { expr } => ( + BORROW_INTERIOR_MUTABLE_CONST, + "a `const` item with interior mutability should not be borrowed", + *expr, + ), + } + } +} + +fn lint(cx: &LateContext<'_>, source: Source) { + let (lint, msg, span) = source.lint(); + span_lint_and_then(cx, lint, span, msg, |diag| { + if span.from_expansion() { + return; // Don't give suggestions into macros. + } + match source { + Source::Item { .. } => { + let const_kw_span = span.from_inner(InnerSpan::new(0, 5)); + diag.span_label(const_kw_span, "make this a static item (maybe with lazy_static)"); + }, + Source::Assoc { .. } => (), + Source::Expr { .. } => { + diag.help("assign this const to a local or static variable, and use the variable here"); + }, + } + }); +} + +declare_lint_pass!(NonCopyConst => [DECLARE_INTERIOR_MUTABLE_CONST, BORROW_INTERIOR_MUTABLE_CONST]); + +impl<'tcx> LateLintPass<'tcx> for NonCopyConst { + fn check_item(&mut self, cx: &LateContext<'tcx>, it: &'tcx Item<'_>) { + if let ItemKind::Const(hir_ty, body_id) = it.kind { + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + if !ignored_macro(cx, it) && is_unfrozen(cx, ty) && is_value_unfrozen_poly(cx, body_id, ty) { + lint(cx, Source::Item { item: it.span }); + } + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, trait_item: &'tcx TraitItem<'_>) { + if let TraitItemKind::Const(hir_ty, body_id_opt) = &trait_item.kind { + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + + // Normalize assoc types because ones originated from generic params + // bounded other traits could have their bound. + let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty); + if is_unfrozen(cx, normalized) + // When there's no default value, lint it only according to its type; + // in other words, lint consts whose value *could* be unfrozen, not definitely is. + // This feels inconsistent with how the lint treats generic types, + // which avoids linting types which potentially become unfrozen. + // One could check whether an unfrozen type have a *frozen variant* + // (like `body_id_opt.map_or_else(|| !has_frozen_variant(...), ...)`), + // and do the same as the case of generic types at impl items. + // Note that it isn't sufficient to check if it has an enum + // since all of that enum's variants can be unfrozen: + // i.e. having an enum doesn't necessary mean a type has a frozen variant. + // And, implementing it isn't a trivial task; it'll probably end up + // re-implementing the trait predicate evaluation specific to `Freeze`. + && body_id_opt.map_or(true, |body_id| is_value_unfrozen_poly(cx, body_id, normalized)) + { + lint(cx, Source::Assoc { item: trait_item.span }); + } + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) { + if let ImplItemKind::Const(hir_ty, body_id) = &impl_item.kind { + let item_def_id = cx.tcx.hir().get_parent_item(impl_item.hir_id()); + let item = cx.tcx.hir().expect_item(item_def_id); + + match &item.kind { + ItemKind::Impl(Impl { + of_trait: Some(of_trait_ref), + .. + }) => { + if_chain! { + // Lint a trait impl item only when the definition is a generic type, + // assuming an assoc const is not meant to be an interior mutable type. + if let Some(of_trait_def_id) = of_trait_ref.trait_def_id(); + if let Some(of_assoc_item) = cx + .tcx + .associated_item(impl_item.def_id) + .trait_item_def_id; + if cx + .tcx + .layout_of(cx.tcx.param_env(of_trait_def_id).and( + // Normalize assoc types because ones originated from generic params + // bounded other traits could have their bound at the trait defs; + // and, in that case, the definition is *not* generic. + cx.tcx.normalize_erasing_regions( + cx.tcx.param_env(of_trait_def_id), + cx.tcx.type_of(of_assoc_item), + ), + )) + .is_err(); + // If there were a function like `has_frozen_variant` described above, + // we should use here as a frozen variant is a potential to be frozen + // similar to unknown layouts. + // e.g. `layout_of(...).is_err() || has_frozen_variant(...);` + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty); + if is_unfrozen(cx, normalized); + if is_value_unfrozen_poly(cx, *body_id, normalized); + then { + lint( + cx, + Source::Assoc { + item: impl_item.span, + }, + ); + } + } + }, + ItemKind::Impl(Impl { of_trait: None, .. }) => { + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + // Normalize assoc types originated from generic params. + let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty); + + if is_unfrozen(cx, ty) && is_value_unfrozen_poly(cx, *body_id, normalized) { + lint(cx, Source::Assoc { item: impl_item.span }); + } + }, + _ => (), + } + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Path(qpath) = &expr.kind { + // Only lint if we use the const item inside a function. + if in_constant(cx, expr.hir_id) { + return; + } + + // Make sure it is a const item. + let item_def_id = match cx.qpath_res(qpath, expr.hir_id) { + Res::Def(DefKind::Const | DefKind::AssocConst, did) => did, + _ => return, + }; + + // Climb up to resolve any field access and explicit referencing. + let mut cur_expr = expr; + let mut dereferenced_expr = expr; + let mut needs_check_adjustment = true; + loop { + let parent_id = cx.tcx.hir().get_parent_node(cur_expr.hir_id); + if parent_id == cur_expr.hir_id { + break; + } + if let Some(Node::Expr(parent_expr)) = cx.tcx.hir().find(parent_id) { + match &parent_expr.kind { + ExprKind::AddrOf(..) => { + // `&e` => `e` must be referenced. + needs_check_adjustment = false; + }, + ExprKind::Field(..) => { + needs_check_adjustment = true; + + // Check whether implicit dereferences happened; + // if so, no need to go further up + // because of the same reason as the `ExprKind::Unary` case. + if cx + .typeck_results() + .expr_adjustments(dereferenced_expr) + .iter() + .any(|adj| matches!(adj.kind, Adjust::Deref(_))) + { + break; + } + + dereferenced_expr = parent_expr; + }, + ExprKind::Index(e, _) if ptr::eq(&**e, cur_expr) => { + // `e[i]` => desugared to `*Index::index(&e, i)`, + // meaning `e` must be referenced. + // no need to go further up since a method call is involved now. + needs_check_adjustment = false; + break; + }, + ExprKind::Unary(UnOp::Deref, _) => { + // `*e` => desugared to `*Deref::deref(&e)`, + // meaning `e` must be referenced. + // no need to go further up since a method call is involved now. + needs_check_adjustment = false; + break; + }, + _ => break, + } + cur_expr = parent_expr; + } else { + break; + } + } + + let ty = if needs_check_adjustment { + let adjustments = cx.typeck_results().expr_adjustments(dereferenced_expr); + if let Some(i) = adjustments + .iter() + .position(|adj| matches!(adj.kind, Adjust::Borrow(_) | Adjust::Deref(_))) + { + if i == 0 { + cx.typeck_results().expr_ty(dereferenced_expr) + } else { + adjustments[i - 1].target + } + } else { + // No borrow adjustments means the entire const is moved. + return; + } + } else { + cx.typeck_results().expr_ty(dereferenced_expr) + }; + + if is_unfrozen(cx, ty) && is_value_unfrozen_expr(cx, expr.hir_id, item_def_id, ty) { + lint(cx, Source::Expr { expr: expr.span }); + } + } + } +} + +fn ignored_macro(cx: &LateContext<'_>, it: &rustc_hir::Item<'_>) -> bool { + macro_backtrace(it.span).any(|macro_call| { + matches!( + cx.tcx.get_diagnostic_name(macro_call.def_id), + Some(sym::thread_local_macro) + ) + }) +} diff --git a/src/tools/clippy/clippy_lints/src/non_expressive_names.rs b/src/tools/clippy/clippy_lints/src/non_expressive_names.rs new file mode 100644 index 000000000..b96af06b8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/non_expressive_names.rs @@ -0,0 +1,427 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; +use rustc_ast::ast::{ + self, Arm, AssocItem, AssocItemKind, Attribute, Block, FnDecl, Item, ItemKind, Local, Pat, PatKind, +}; +use rustc_ast::visit::{walk_block, walk_expr, walk_pat, Visitor}; +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::source_map::Span; +use rustc_span::sym; +use rustc_span::symbol::{Ident, Symbol}; +use std::cmp::Ordering; + +declare_clippy_lint! { + /// ### What it does + /// Checks for names that are very similar and thus confusing. + /// + /// ### Why is this bad? + /// It's hard to distinguish between names that differ only + /// by a single character. + /// + /// ### Example + /// ```ignore + /// let checked_exp = something; + /// let checked_expr = something_else; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SIMILAR_NAMES, + pedantic, + "similarly named items and bindings" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for too many variables whose name consists of a + /// single character. + /// + /// ### Why is this bad? + /// It's hard to memorize what a variable means without a + /// descriptive name. + /// + /// ### Example + /// ```ignore + /// let (a, b, c, d, e, f, g) = (...); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MANY_SINGLE_CHAR_NAMES, + pedantic, + "too many single character bindings" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks if you have variables whose name consists of just + /// underscores and digits. + /// + /// ### Why is this bad? + /// It's hard to memorize what a variable means without a + /// descriptive name. + /// + /// ### Example + /// ```rust + /// let _1 = 1; + /// let ___1 = 1; + /// let __1___2 = 11; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub JUST_UNDERSCORES_AND_DIGITS, + style, + "unclear name" +} + +#[derive(Copy, Clone)] +pub struct NonExpressiveNames { + pub single_char_binding_names_threshold: u64, +} + +impl_lint_pass!(NonExpressiveNames => [SIMILAR_NAMES, MANY_SINGLE_CHAR_NAMES, JUST_UNDERSCORES_AND_DIGITS]); + +struct ExistingName { + interned: Symbol, + span: Span, + len: usize, + exemptions: &'static [&'static str], +} + +struct SimilarNamesLocalVisitor<'a, 'tcx> { + names: Vec<ExistingName>, + cx: &'a EarlyContext<'tcx>, + lint: &'a NonExpressiveNames, + + /// A stack of scopes containing the single-character bindings in each scope. + single_char_names: Vec<Vec<Ident>>, +} + +impl<'a, 'tcx> SimilarNamesLocalVisitor<'a, 'tcx> { + fn check_single_char_names(&self) { + let num_single_char_names = self.single_char_names.iter().flatten().count(); + let threshold = self.lint.single_char_binding_names_threshold; + if num_single_char_names as u64 > threshold { + let span = self + .single_char_names + .iter() + .flatten() + .map(|ident| ident.span) + .collect::<Vec<_>>(); + span_lint( + self.cx, + MANY_SINGLE_CHAR_NAMES, + span, + &format!( + "{} bindings with single-character names in scope", + num_single_char_names + ), + ); + } + } +} + +// this list contains lists of names that are allowed to be similar +// the assumption is that no name is ever contained in multiple lists. +#[rustfmt::skip] +const ALLOWED_TO_BE_SIMILAR: &[&[&str]] = &[ + &["parsed", "parser"], + &["lhs", "rhs"], + &["tx", "rx"], + &["set", "get"], + &["args", "arms"], + &["qpath", "path"], + &["lit", "lint"], + &["wparam", "lparam"], + &["iter", "item"], +]; + +struct SimilarNamesNameVisitor<'a, 'tcx, 'b>(&'b mut SimilarNamesLocalVisitor<'a, 'tcx>); + +impl<'a, 'tcx, 'b> Visitor<'tcx> for SimilarNamesNameVisitor<'a, 'tcx, 'b> { + fn visit_pat(&mut self, pat: &'tcx Pat) { + match pat.kind { + PatKind::Ident(_, ident, _) => { + if !pat.span.from_expansion() { + self.check_ident(ident); + } + }, + PatKind::Struct(_, _, ref fields, _) => { + for field in fields { + if !field.is_shorthand { + self.visit_pat(&field.pat); + } + } + }, + // just go through the first pattern, as either all patterns + // bind the same bindings or rustc would have errored much earlier + PatKind::Or(ref pats) => self.visit_pat(&pats[0]), + _ => walk_pat(self, pat), + } + } +} + +#[must_use] +fn get_exemptions(interned_name: &str) -> Option<&'static [&'static str]> { + ALLOWED_TO_BE_SIMILAR + .iter() + .find(|&&list| allowed_to_be_similar(interned_name, list)) + .copied() +} + +#[must_use] +fn allowed_to_be_similar(interned_name: &str, list: &[&str]) -> bool { + list.iter() + .any(|&name| interned_name.starts_with(name) || interned_name.ends_with(name)) +} + +impl<'a, 'tcx, 'b> SimilarNamesNameVisitor<'a, 'tcx, 'b> { + fn check_short_ident(&mut self, ident: Ident) { + // Ignore shadowing + if self + .0 + .single_char_names + .iter() + .flatten() + .any(|id| id.name == ident.name) + { + return; + } + + if let Some(scope) = &mut self.0.single_char_names.last_mut() { + scope.push(ident); + } + } + + #[expect(clippy::too_many_lines)] + fn check_ident(&mut self, ident: Ident) { + let interned_name = ident.name.as_str(); + if interned_name.chars().any(char::is_uppercase) { + return; + } + if interned_name.chars().all(|c| c.is_ascii_digit() || c == '_') { + span_lint( + self.0.cx, + JUST_UNDERSCORES_AND_DIGITS, + ident.span, + "consider choosing a more descriptive name", + ); + return; + } + if interned_name.starts_with('_') { + // these bindings are typically unused or represent an ignored portion of a destructuring pattern + return; + } + let count = interned_name.chars().count(); + if count < 3 { + if count == 1 { + self.check_short_ident(ident); + } + return; + } + for existing_name in &self.0.names { + if allowed_to_be_similar(interned_name, existing_name.exemptions) { + continue; + } + match existing_name.len.cmp(&count) { + Ordering::Greater => { + if existing_name.len - count != 1 + || levenstein_not_1(interned_name, existing_name.interned.as_str()) + { + continue; + } + }, + Ordering::Less => { + if count - existing_name.len != 1 + || levenstein_not_1(existing_name.interned.as_str(), interned_name) + { + continue; + } + }, + Ordering::Equal => { + let mut interned_chars = interned_name.chars(); + let interned_str = existing_name.interned.as_str(); + let mut existing_chars = interned_str.chars(); + let first_i = interned_chars.next().expect("we know we have at least one char"); + let first_e = existing_chars.next().expect("we know we have at least one char"); + let eq_or_numeric = |(a, b): (char, char)| a == b || a.is_numeric() && b.is_numeric(); + + if eq_or_numeric((first_i, first_e)) { + let last_i = interned_chars.next_back().expect("we know we have at least two chars"); + let last_e = existing_chars.next_back().expect("we know we have at least two chars"); + if eq_or_numeric((last_i, last_e)) { + if interned_chars + .zip(existing_chars) + .filter(|&ie| !eq_or_numeric(ie)) + .count() + != 1 + { + continue; + } + } else { + let second_last_i = interned_chars + .next_back() + .expect("we know we have at least three chars"); + let second_last_e = existing_chars + .next_back() + .expect("we know we have at least three chars"); + if !eq_or_numeric((second_last_i, second_last_e)) + || second_last_i == '_' + || !interned_chars.zip(existing_chars).all(eq_or_numeric) + { + // allowed similarity foo_x, foo_y + // or too many chars differ (foo_x, boo_y) or (foox, booy) + continue; + } + } + } else { + let second_i = interned_chars.next().expect("we know we have at least two chars"); + let second_e = existing_chars.next().expect("we know we have at least two chars"); + if !eq_or_numeric((second_i, second_e)) + || second_i == '_' + || !interned_chars.zip(existing_chars).all(eq_or_numeric) + { + // allowed similarity x_foo, y_foo + // or too many chars differ (x_foo, y_boo) or (xfoo, yboo) + continue; + } + } + }, + } + span_lint_and_then( + self.0.cx, + SIMILAR_NAMES, + ident.span, + "binding's name is too similar to existing binding", + |diag| { + diag.span_note(existing_name.span, "existing binding defined here"); + }, + ); + return; + } + self.0.names.push(ExistingName { + exemptions: get_exemptions(interned_name).unwrap_or(&[]), + interned: ident.name, + span: ident.span, + len: count, + }); + } +} + +impl<'a, 'b> SimilarNamesLocalVisitor<'a, 'b> { + /// ensure scoping rules work + fn apply<F: for<'c> Fn(&'c mut Self)>(&mut self, f: F) { + let n = self.names.len(); + let single_char_count = self.single_char_names.len(); + f(self); + self.names.truncate(n); + self.single_char_names.truncate(single_char_count); + } +} + +impl<'a, 'tcx> Visitor<'tcx> for SimilarNamesLocalVisitor<'a, 'tcx> { + fn visit_local(&mut self, local: &'tcx Local) { + if let Some((init, els)) = &local.kind.init_else_opt() { + self.apply(|this| walk_expr(this, init)); + if let Some(els) = els { + self.apply(|this| walk_block(this, els)); + } + } + // add the pattern after the expression because the bindings aren't available + // yet in the init + // expression + SimilarNamesNameVisitor(self).visit_pat(&local.pat); + } + fn visit_block(&mut self, blk: &'tcx Block) { + self.single_char_names.push(vec![]); + + self.apply(|this| walk_block(this, blk)); + + self.check_single_char_names(); + self.single_char_names.pop(); + } + fn visit_arm(&mut self, arm: &'tcx Arm) { + self.single_char_names.push(vec![]); + + self.apply(|this| { + SimilarNamesNameVisitor(this).visit_pat(&arm.pat); + this.apply(|this| walk_expr(this, &arm.body)); + }); + + self.check_single_char_names(); + self.single_char_names.pop(); + } + fn visit_item(&mut self, _: &Item) { + // do not recurse into inner items + } +} + +impl EarlyLintPass for NonExpressiveNames { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if in_external_macro(cx.sess(), item.span) { + return; + } + + if let ItemKind::Fn(box ast::Fn { + ref sig, + body: Some(ref blk), + .. + }) = item.kind + { + do_check(self, cx, &item.attrs, &sig.decl, blk); + } + } + + fn check_impl_item(&mut self, cx: &EarlyContext<'_>, item: &AssocItem) { + if in_external_macro(cx.sess(), item.span) { + return; + } + + if let AssocItemKind::Fn(box ast::Fn { + ref sig, + body: Some(ref blk), + .. + }) = item.kind + { + do_check(self, cx, &item.attrs, &sig.decl, blk); + } + } +} + +fn do_check(lint: &mut NonExpressiveNames, cx: &EarlyContext<'_>, attrs: &[Attribute], decl: &FnDecl, blk: &Block) { + if !attrs.iter().any(|attr| attr.has_name(sym::test)) { + let mut visitor = SimilarNamesLocalVisitor { + names: Vec::new(), + cx, + lint, + single_char_names: vec![vec![]], + }; + + // initialize with function arguments + for arg in &decl.inputs { + SimilarNamesNameVisitor(&mut visitor).visit_pat(&arg.pat); + } + // walk all other bindings + walk_block(&mut visitor, blk); + + visitor.check_single_char_names(); + } +} + +/// Precondition: `a_name.chars().count() < b_name.chars().count()`. +#[must_use] +fn levenstein_not_1(a_name: &str, b_name: &str) -> bool { + debug_assert!(a_name.chars().count() < b_name.chars().count()); + let mut a_chars = a_name.chars(); + let mut b_chars = b_name.chars(); + while let (Some(a), Some(b)) = (a_chars.next(), b_chars.next()) { + if a == b { + continue; + } + if let Some(b2) = b_chars.next() { + // check if there's just one character inserted + return a != b2 || a_chars.ne(b_chars); + } + // tuple + // ntuple + return true; + } + // for item in items + true +} diff --git a/src/tools/clippy/clippy_lints/src/non_octal_unix_permissions.rs b/src/tools/clippy/clippy_lints/src/non_octal_unix_permissions.rs new file mode 100644 index 000000000..ed022b9d5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/non_octal_unix_permissions.rs @@ -0,0 +1,106 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{snippet_opt, snippet_with_applicability}; +use clippy_utils::ty::match_type; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for non-octal values used to set Unix file permissions. + /// + /// ### Why is this bad? + /// They will be converted into octal, creating potentially + /// unintended file permissions. + /// + /// ### Example + /// ```rust,ignore + /// use std::fs::OpenOptions; + /// use std::os::unix::fs::OpenOptionsExt; + /// + /// let mut options = OpenOptions::new(); + /// options.mode(644); + /// ``` + /// Use instead: + /// ```rust,ignore + /// use std::fs::OpenOptions; + /// use std::os::unix::fs::OpenOptionsExt; + /// + /// let mut options = OpenOptions::new(); + /// options.mode(0o644); + /// ``` + #[clippy::version = "1.53.0"] + pub NON_OCTAL_UNIX_PERMISSIONS, + correctness, + "use of non-octal value to set unix file permissions, which will be translated into octal" +} + +declare_lint_pass!(NonOctalUnixPermissions => [NON_OCTAL_UNIX_PERMISSIONS]); + +impl<'tcx> LateLintPass<'tcx> for NonOctalUnixPermissions { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + match &expr.kind { + ExprKind::MethodCall(path, [func, param], _) => { + let obj_ty = cx.typeck_results().expr_ty(func).peel_refs(); + + if_chain! { + if (path.ident.name == sym!(mode) + && (match_type(cx, obj_ty, &paths::OPEN_OPTIONS) + || match_type(cx, obj_ty, &paths::DIR_BUILDER))) + || (path.ident.name == sym!(set_mode) && match_type(cx, obj_ty, &paths::PERMISSIONS)); + if let ExprKind::Lit(_) = param.kind; + + then { + let snip = match snippet_opt(cx, param.span) { + Some(s) => s, + _ => return, + }; + + if !snip.starts_with("0o") { + show_error(cx, param); + } + } + } + }, + ExprKind::Call(func, [param]) => { + if_chain! { + if let ExprKind::Path(ref path) = func.kind; + if let Some(def_id) = cx.qpath_res(path, func.hir_id).opt_def_id(); + if match_def_path(cx, def_id, &paths::PERMISSIONS_FROM_MODE); + if let ExprKind::Lit(_) = param.kind; + + then { + let snip = match snippet_opt(cx, param.span) { + Some(s) => s, + _ => return, + }; + + if !snip.starts_with("0o") { + show_error(cx, param); + } + } + } + }, + _ => {}, + }; + } +} + +fn show_error(cx: &LateContext<'_>, param: &Expr<'_>) { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + NON_OCTAL_UNIX_PERMISSIONS, + param.span, + "using a non-octal value to set unix file permissions", + "consider using an octal literal instead", + format!( + "0o{}", + snippet_with_applicability(cx, param.span, "0o..", &mut applicability,), + ), + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/non_send_fields_in_send_ty.rs b/src/tools/clippy/clippy_lints/src/non_send_fields_in_send_ty.rs new file mode 100644 index 000000000..ddef7352d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/non_send_fields_in_send_ty.rs @@ -0,0 +1,251 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::ty::{implements_trait, is_copy}; +use clippy_utils::{is_lint_allowed, match_def_path, paths}; +use rustc_ast::ImplPolarity; +use rustc_hir::def_id::DefId; +use rustc_hir::{FieldDef, Item, ItemKind, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::{self, subst::GenericArgKind, Ty}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// This lint warns about a `Send` implementation for a type that + /// contains fields that are not safe to be sent across threads. + /// It tries to detect fields that can cause a soundness issue + /// when sent to another thread (e.g., `Rc`) while allowing `!Send` fields + /// that are expected to exist in a `Send` type, such as raw pointers. + /// + /// ### Why is this bad? + /// Sending the struct to another thread effectively sends all of its fields, + /// and the fields that do not implement `Send` can lead to soundness bugs + /// such as data races when accessed in a thread + /// that is different from the thread that created it. + /// + /// See: + /// * [*The Rustonomicon* about *Send and Sync*](https://doc.rust-lang.org/nomicon/send-and-sync.html) + /// * [The documentation of `Send`](https://doc.rust-lang.org/std/marker/trait.Send.html) + /// + /// ### Known Problems + /// This lint relies on heuristics to distinguish types that are actually + /// unsafe to be sent across threads and `!Send` types that are expected to + /// exist in `Send` type. Its rule can filter out basic cases such as + /// `Vec<*const T>`, but it's not perfect. Feel free to create an issue if + /// you have a suggestion on how this heuristic can be improved. + /// + /// ### Example + /// ```rust,ignore + /// struct ExampleStruct<T> { + /// rc_is_not_send: Rc<String>, + /// unbounded_generic_field: T, + /// } + /// + /// // This impl is unsound because it allows sending `!Send` types through `ExampleStruct` + /// unsafe impl<T> Send for ExampleStruct<T> {} + /// ``` + /// Use thread-safe types like [`std::sync::Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) + /// or specify correct bounds on generic type parameters (`T: Send`). + #[clippy::version = "1.57.0"] + pub NON_SEND_FIELDS_IN_SEND_TY, + nursery, + "there is a field that is not safe to be sent to another thread in a `Send` struct" +} + +#[derive(Copy, Clone)] +pub struct NonSendFieldInSendTy { + enable_raw_pointer_heuristic: bool, +} + +impl NonSendFieldInSendTy { + pub fn new(enable_raw_pointer_heuristic: bool) -> Self { + Self { + enable_raw_pointer_heuristic, + } + } +} + +impl_lint_pass!(NonSendFieldInSendTy => [NON_SEND_FIELDS_IN_SEND_TY]); + +impl<'tcx> LateLintPass<'tcx> for NonSendFieldInSendTy { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + let ty_allowed_in_send = if self.enable_raw_pointer_heuristic { + ty_allowed_with_raw_pointer_heuristic + } else { + ty_allowed_without_raw_pointer_heuristic + }; + + // Checks if we are in `Send` impl item. + // We start from `Send` impl instead of `check_field_def()` because + // single `AdtDef` may have multiple `Send` impls due to generic + // parameters, and the lint is much easier to implement in this way. + if_chain! { + if !in_external_macro(cx.tcx.sess, item.span); + if let Some(send_trait) = cx.tcx.get_diagnostic_item(sym::Send); + if let ItemKind::Impl(hir_impl) = &item.kind; + if let Some(trait_ref) = &hir_impl.of_trait; + if let Some(trait_id) = trait_ref.trait_def_id(); + if send_trait == trait_id; + if hir_impl.polarity == ImplPolarity::Positive; + if let Some(ty_trait_ref) = cx.tcx.impl_trait_ref(item.def_id); + if let self_ty = ty_trait_ref.self_ty(); + if let ty::Adt(adt_def, impl_trait_substs) = self_ty.kind(); + then { + let mut non_send_fields = Vec::new(); + + let hir_map = cx.tcx.hir(); + for variant in adt_def.variants() { + for field in &variant.fields { + if_chain! { + if let Some(field_hir_id) = field + .did + .as_local() + .map(|local_def_id| hir_map.local_def_id_to_hir_id(local_def_id)); + if !is_lint_allowed(cx, NON_SEND_FIELDS_IN_SEND_TY, field_hir_id); + if let field_ty = field.ty(cx.tcx, impl_trait_substs); + if !ty_allowed_in_send(cx, field_ty, send_trait); + if let Node::Field(field_def) = hir_map.get(field_hir_id); + then { + non_send_fields.push(NonSendField { + def: field_def, + ty: field_ty, + generic_params: collect_generic_params(field_ty), + }) + } + } + } + } + + if !non_send_fields.is_empty() { + span_lint_and_then( + cx, + NON_SEND_FIELDS_IN_SEND_TY, + item.span, + &format!( + "some fields in `{}` are not safe to be sent to another thread", + snippet(cx, hir_impl.self_ty.span, "Unknown") + ), + |diag| { + for field in non_send_fields { + diag.span_note( + field.def.span, + &format!("it is not safe to send field `{}` to another thread", field.def.ident.name), + ); + + match field.generic_params.len() { + 0 => diag.help("use a thread-safe type that implements `Send`"), + 1 if is_ty_param(field.ty) => diag.help(&format!("add `{}: Send` bound in `Send` impl", field.ty)), + _ => diag.help(&format!( + "add bounds on type parameter{} `{}` that satisfy `{}: Send`", + if field.generic_params.len() > 1 { "s" } else { "" }, + field.generic_params_string(), + snippet(cx, field.def.ty.span, "Unknown"), + )), + }; + } + }, + ); + } + } + } + } +} + +struct NonSendField<'tcx> { + def: &'tcx FieldDef<'tcx>, + ty: Ty<'tcx>, + generic_params: Vec<Ty<'tcx>>, +} + +impl<'tcx> NonSendField<'tcx> { + fn generic_params_string(&self) -> String { + self.generic_params + .iter() + .map(ToString::to_string) + .collect::<Vec<_>>() + .join(", ") + } +} + +/// Given a type, collect all of its generic parameters. +/// Example: `MyStruct<P, Box<Q, R>>` => `vec![P, Q, R]` +fn collect_generic_params(ty: Ty<'_>) -> Vec<Ty<'_>> { + ty.walk() + .filter_map(|inner| match inner.unpack() { + GenericArgKind::Type(inner_ty) => Some(inner_ty), + _ => None, + }) + .filter(|&inner_ty| is_ty_param(inner_ty)) + .collect() +} + +/// Be more strict when the heuristic is disabled +fn ty_allowed_without_raw_pointer_heuristic<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, send_trait: DefId) -> bool { + if implements_trait(cx, ty, send_trait, &[]) { + return true; + } + + if is_copy(cx, ty) && !contains_pointer_like(cx, ty) { + return true; + } + + false +} + +/// Heuristic to allow cases like `Vec<*const u8>` +fn ty_allowed_with_raw_pointer_heuristic<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, send_trait: DefId) -> bool { + if implements_trait(cx, ty, send_trait, &[]) || is_copy(cx, ty) { + return true; + } + + // The type is known to be `!Send` and `!Copy` + match ty.kind() { + ty::Tuple(fields) => fields + .iter() + .all(|ty| ty_allowed_with_raw_pointer_heuristic(cx, ty, send_trait)), + ty::Array(ty, _) | ty::Slice(ty) => ty_allowed_with_raw_pointer_heuristic(cx, *ty, send_trait), + ty::Adt(_, substs) => { + if contains_pointer_like(cx, ty) { + // descends only if ADT contains any raw pointers + substs.iter().all(|generic_arg| match generic_arg.unpack() { + GenericArgKind::Type(ty) => ty_allowed_with_raw_pointer_heuristic(cx, ty, send_trait), + // Lifetimes and const generics are not solid part of ADT and ignored + GenericArgKind::Lifetime(_) | GenericArgKind::Const(_) => true, + }) + } else { + false + } + }, + // Raw pointers are `!Send` but allowed by the heuristic + ty::RawPtr(_) => true, + _ => false, + } +} + +/// Checks if the type contains any pointer-like types in substs (including nested ones) +fn contains_pointer_like<'tcx>(cx: &LateContext<'tcx>, target_ty: Ty<'tcx>) -> bool { + for ty_node in target_ty.walk() { + if let GenericArgKind::Type(inner_ty) = ty_node.unpack() { + match inner_ty.kind() { + ty::RawPtr(_) => { + return true; + }, + ty::Adt(adt_def, _) => { + if match_def_path(cx, adt_def.did(), &paths::PTR_NON_NULL) { + return true; + } + }, + _ => (), + } + } + } + + false +} + +/// Returns `true` if the type is a type parameter such as `T`. +fn is_ty_param(target_ty: Ty<'_>) -> bool { + matches!(target_ty.kind(), ty::Param(_)) +} diff --git a/src/tools/clippy/clippy_lints/src/nonstandard_macro_braces.rs b/src/tools/clippy/clippy_lints/src/nonstandard_macro_braces.rs new file mode 100644 index 000000000..4722c0310 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/nonstandard_macro_braces.rs @@ -0,0 +1,282 @@ +use std::{ + fmt, + hash::{Hash, Hasher}, +}; + +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet_opt; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_hir::def_id::DefId; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::hygiene::{ExpnKind, MacroKind}; +use rustc_span::{Span, Symbol}; +use serde::{de, Deserialize}; + +declare_clippy_lint! { + /// ### What it does + /// Checks that common macros are used with consistent bracing. + /// + /// ### Why is this bad? + /// This is mostly a consistency lint although using () or [] + /// doesn't give you a semicolon in item position, which can be unexpected. + /// + /// ### Example + /// ```rust + /// vec!{1, 2, 3}; + /// ``` + /// Use instead: + /// ```rust + /// vec![1, 2, 3]; + /// ``` + #[clippy::version = "1.55.0"] + pub NONSTANDARD_MACRO_BRACES, + nursery, + "check consistent use of braces in macro" +} + +const BRACES: &[(&str, &str)] = &[("(", ")"), ("{", "}"), ("[", "]")]; + +/// The (name, (open brace, close brace), source snippet) +type MacroInfo<'a> = (Symbol, &'a (String, String), String); + +#[derive(Clone, Debug, Default)] +pub struct MacroBraces { + macro_braces: FxHashMap<String, (String, String)>, + done: FxHashSet<Span>, +} + +impl MacroBraces { + pub fn new(conf: &FxHashSet<MacroMatcher>) -> Self { + let macro_braces = macro_braces(conf.clone()); + Self { + macro_braces, + done: FxHashSet::default(), + } + } +} + +impl_lint_pass!(MacroBraces => [NONSTANDARD_MACRO_BRACES]); + +impl EarlyLintPass for MacroBraces { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { + if let Some((name, braces, snip)) = is_offending_macro(cx, item.span, self) { + let span = item.span.ctxt().outer_expn_data().call_site; + emit_help(cx, snip, braces, name, span); + self.done.insert(span); + } + } + + fn check_stmt(&mut self, cx: &EarlyContext<'_>, stmt: &ast::Stmt) { + if let Some((name, braces, snip)) = is_offending_macro(cx, stmt.span, self) { + let span = stmt.span.ctxt().outer_expn_data().call_site; + emit_help(cx, snip, braces, name, span); + self.done.insert(span); + } + } + + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) { + if let Some((name, braces, snip)) = is_offending_macro(cx, expr.span, self) { + let span = expr.span.ctxt().outer_expn_data().call_site; + emit_help(cx, snip, braces, name, span); + self.done.insert(span); + } + } + + fn check_ty(&mut self, cx: &EarlyContext<'_>, ty: &ast::Ty) { + if let Some((name, braces, snip)) = is_offending_macro(cx, ty.span, self) { + let span = ty.span.ctxt().outer_expn_data().call_site; + emit_help(cx, snip, braces, name, span); + self.done.insert(span); + } + } +} + +fn is_offending_macro<'a>(cx: &EarlyContext<'_>, span: Span, mac_braces: &'a MacroBraces) -> Option<MacroInfo<'a>> { + let unnested_or_local = || { + !span.ctxt().outer_expn_data().call_site.from_expansion() + || span + .macro_backtrace() + .last() + .map_or(false, |e| e.macro_def_id.map_or(false, DefId::is_local)) + }; + if_chain! { + if let ExpnKind::Macro(MacroKind::Bang, mac_name) = span.ctxt().outer_expn_data().kind; + let name = mac_name.as_str(); + if let Some(braces) = mac_braces.macro_braces.get(name); + if let Some(snip) = snippet_opt(cx, span.ctxt().outer_expn_data().call_site); + // we must check only invocation sites + // https://github.com/rust-lang/rust-clippy/issues/7422 + if snip.starts_with(&format!("{}!", name)); + if unnested_or_local(); + // make formatting consistent + let c = snip.replace(' ', ""); + if !c.starts_with(&format!("{}!{}", name, braces.0)); + if !mac_braces.done.contains(&span.ctxt().outer_expn_data().call_site); + then { + Some((mac_name, braces, snip)) + } else { + None + } + } +} + +fn emit_help(cx: &EarlyContext<'_>, snip: String, braces: &(String, String), name: Symbol, span: Span) { + let with_space = &format!("! {}", braces.0); + let without_space = &format!("!{}", braces.0); + let mut help = snip; + for b in BRACES.iter().filter(|b| b.0 != braces.0) { + help = help.replace(b.0, &braces.0).replace(b.1, &braces.1); + // Only `{` traditionally has space before the brace + if braces.0 != "{" && help.contains(with_space) { + help = help.replace(with_space, without_space); + } else if braces.0 == "{" && help.contains(without_space) { + help = help.replace(without_space, with_space); + } + } + span_lint_and_help( + cx, + NONSTANDARD_MACRO_BRACES, + span, + &format!("use of irregular braces for `{}!` macro", name), + Some(span), + &format!("consider writing `{}`", help), + ); +} + +fn macro_braces(conf: FxHashSet<MacroMatcher>) -> FxHashMap<String, (String, String)> { + let mut braces = vec![ + macro_matcher!( + name: "print", + braces: ("(", ")"), + ), + macro_matcher!( + name: "println", + braces: ("(", ")"), + ), + macro_matcher!( + name: "eprint", + braces: ("(", ")"), + ), + macro_matcher!( + name: "eprintln", + braces: ("(", ")"), + ), + macro_matcher!( + name: "write", + braces: ("(", ")"), + ), + macro_matcher!( + name: "writeln", + braces: ("(", ")"), + ), + macro_matcher!( + name: "format", + braces: ("(", ")"), + ), + macro_matcher!( + name: "format_args", + braces: ("(", ")"), + ), + macro_matcher!( + name: "vec", + braces: ("[", "]"), + ), + ] + .into_iter() + .collect::<FxHashMap<_, _>>(); + // We want users items to override any existing items + for it in conf { + braces.insert(it.name, it.braces); + } + braces +} + +macro_rules! macro_matcher { + (name: $name:expr, braces: ($open:expr, $close:expr) $(,)?) => { + ($name.to_owned(), ($open.to_owned(), $close.to_owned())) + }; +} +pub(crate) use macro_matcher; + +#[derive(Clone, Debug)] +pub struct MacroMatcher { + name: String, + braces: (String, String), +} + +impl Hash for MacroMatcher { + fn hash<H: Hasher>(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl PartialEq for MacroMatcher { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} +impl Eq for MacroMatcher {} + +impl<'de> Deserialize<'de> for MacroMatcher { + fn deserialize<D>(deser: D) -> Result<Self, D::Error> + where + D: de::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field { + Name, + Brace, + } + struct MacVisitor; + impl<'de> de::Visitor<'de> for MacVisitor { + type Value = MacroMatcher; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("struct MacroMatcher") + } + + fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> + where + V: de::MapAccess<'de>, + { + let mut name = None; + let mut brace: Option<&str> = None; + while let Some(key) = map.next_key()? { + match key { + Field::Name => { + if name.is_some() { + return Err(de::Error::duplicate_field("name")); + } + name = Some(map.next_value()?); + }, + Field::Brace => { + if brace.is_some() { + return Err(de::Error::duplicate_field("brace")); + } + brace = Some(map.next_value()?); + }, + } + } + let name = name.ok_or_else(|| de::Error::missing_field("name"))?; + let brace = brace.ok_or_else(|| de::Error::missing_field("brace"))?; + Ok(MacroMatcher { + name, + braces: BRACES + .iter() + .find(|b| b.0 == brace) + .map(|(o, c)| ((*o).to_owned(), (*c).to_owned())) + .ok_or_else(|| { + de::Error::custom(&format!("expected one of `(`, `{{`, `[` found `{}`", brace)) + })?, + }) + } + } + + const FIELDS: &[&str] = &["name", "brace"]; + deser.deserialize_struct("MacroMatcher", FIELDS, MacVisitor) + } +} diff --git a/src/tools/clippy/clippy_lints/src/octal_escapes.rs b/src/tools/clippy/clippy_lints/src/octal_escapes.rs new file mode 100644 index 000000000..6ad6837f0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/octal_escapes.rs @@ -0,0 +1,151 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use rustc_ast::ast::{Expr, ExprKind}; +use rustc_ast::token::{Lit, LitKind}; +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::Span; +use std::fmt::Write; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `\0` escapes in string and byte literals that look like octal + /// character escapes in C. + /// + /// ### Why is this bad? + /// + /// C and other languages support octal character escapes in strings, where + /// a backslash is followed by up to three octal digits. For example, `\033` + /// stands for the ASCII character 27 (ESC). Rust does not support this + /// notation, but has the escape code `\0` which stands for a null + /// byte/character, and any following digits do not form part of the escape + /// sequence. Therefore, `\033` is not a compiler error but the result may + /// be surprising. + /// + /// ### Known problems + /// The actual meaning can be the intended one. `\x00` can be used in these + /// cases to be unambiguous. + /// + /// The lint does not trigger for format strings in `print!()`, `write!()` + /// and friends since the string is already preprocessed when Clippy lints + /// can see it. + /// + /// ### Example + /// ```rust + /// let one = "\033[1m Bold? \033[0m"; // \033 intended as escape + /// let two = "\033\0"; // \033 intended as null-3-3 + /// ``` + /// + /// Use instead: + /// ```rust + /// let one = "\x1b[1mWill this be bold?\x1b[0m"; + /// let two = "\x0033\x00"; + /// ``` + #[clippy::version = "1.59.0"] + pub OCTAL_ESCAPES, + suspicious, + "string escape sequences looking like octal characters" +} + +declare_lint_pass!(OctalEscapes => [OCTAL_ESCAPES]); + +impl EarlyLintPass for OctalEscapes { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if let ExprKind::Lit(lit) = &expr.kind { + if matches!(lit.token.kind, LitKind::Str) { + check_lit(cx, &lit.token, lit.span, true); + } else if matches!(lit.token.kind, LitKind::ByteStr) { + check_lit(cx, &lit.token, lit.span, false); + } + } + } +} + +fn check_lit(cx: &EarlyContext<'_>, lit: &Lit, span: Span, is_string: bool) { + let contents = lit.symbol.as_str(); + let mut iter = contents.char_indices().peekable(); + let mut found = vec![]; + + // go through the string, looking for \0[0-7][0-7]? + while let Some((from, ch)) = iter.next() { + if ch == '\\' { + if let Some((_, '0')) = iter.next() { + // collect up to two further octal digits + if let Some((mut to, '0'..='7')) = iter.next() { + if let Some((_, '0'..='7')) = iter.peek() { + to += 1; + } + found.push((from, to + 1)); + } + } + } + } + + if found.is_empty() { + return; + } + + // construct two suggestion strings, one with \x escapes with octal meaning + // as in C, and one with \x00 for null bytes. + let mut suggest_1 = if is_string { "\"" } else { "b\"" }.to_string(); + let mut suggest_2 = suggest_1.clone(); + let mut index = 0; + for (from, to) in found { + suggest_1.push_str(&contents[index..from]); + suggest_2.push_str(&contents[index..from]); + + // construct a replacement escape + // the maximum value is \077, or \x3f, so u8 is sufficient here + if let Ok(n) = u8::from_str_radix(&contents[from + 1..to], 8) { + write!(suggest_1, "\\x{:02x}", n).unwrap(); + } + + // append the null byte as \x00 and the following digits literally + suggest_2.push_str("\\x00"); + suggest_2.push_str(&contents[from + 2..to]); + + index = to; + } + suggest_1.push_str(&contents[index..]); + suggest_1.push('"'); + suggest_2.push_str(&contents[index..]); + suggest_2.push('"'); + + span_lint_and_then( + cx, + OCTAL_ESCAPES, + span, + &format!( + "octal-looking escape in {} literal", + if is_string { "string" } else { "byte string" } + ), + |diag| { + diag.help(&format!( + "octal escapes are not supported, `\\0` is always a null {}", + if is_string { "character" } else { "byte" } + )); + // suggestion 1: equivalent hex escape + diag.span_suggestion( + span, + "if an octal escape was intended, use the hexadecimal representation instead", + suggest_1, + Applicability::MaybeIncorrect, + ); + // suggestion 2: unambiguous null byte + diag.span_suggestion( + span, + &format!( + "if the null {} is intended, disambiguate using", + if is_string { "character" } else { "byte" } + ), + suggest_2, + Applicability::MaybeIncorrect, + ); + }, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/only_used_in_recursion.rs b/src/tools/clippy/clippy_lints/src/only_used_in_recursion.rs new file mode 100644 index 000000000..413a740be --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/only_used_in_recursion.rs @@ -0,0 +1,660 @@ +use std::collections::VecDeque; + +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_lint_allowed; +use itertools::{izip, Itertools}; +use rustc_ast::{walk_list, Label, Mutability}; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_errors::Applicability; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::def_id::DefId; +use rustc_hir::definitions::{DefPathData, DisambiguatedDefPathData}; +use rustc_hir::intravisit::{walk_expr, walk_stmt, FnKind, Visitor}; +use rustc_hir::{ + Arm, Block, Body, Closure, Expr, ExprKind, Guard, HirId, ImplicitSelfKind, Let, Local, Pat, PatKind, Path, + PathSegment, QPath, Stmt, StmtKind, TyKind, UnOp, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_middle::ty::{Ty, TyCtxt, TypeckResults}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::kw; +use rustc_span::symbol::Ident; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for arguments that are only used in recursion with no side-effects. + /// + /// ### Why is this bad? + /// It could contain a useless calculation and can make function simpler. + /// + /// The arguments can be involved in calculations and assignments but as long as + /// the calculations have no side-effects (function calls or mutating dereference) + /// and the assigned variables are also only in recursion, it is useless. + /// + /// ### Known problems + /// Too many code paths in the linting code are currently untested and prone to produce false + /// positives or are prone to have performance implications. + /// + /// In some cases, this would not catch all useless arguments. + /// + /// ```rust + /// fn foo(a: usize, b: usize) -> usize { + /// let f = |x| x + 1; + /// + /// if a == 0 { + /// 1 + /// } else { + /// foo(a - 1, f(b)) + /// } + /// } + /// ``` + /// + /// For example, the argument `b` is only used in recursion, but the lint would not catch it. + /// + /// List of some examples that can not be caught: + /// - binary operation of non-primitive types + /// - closure usage + /// - some `break` relative operations + /// - struct pattern binding + /// + /// Also, when you recurse the function name with path segments, it is not possible to detect. + /// + /// ### Example + /// ```rust + /// fn f(a: usize, b: usize) -> usize { + /// if a == 0 { + /// 1 + /// } else { + /// f(a - 1, b + 1) + /// } + /// } + /// # fn main() { + /// # print!("{}", f(1, 1)); + /// # } + /// ``` + /// Use instead: + /// ```rust + /// fn f(a: usize) -> usize { + /// if a == 0 { + /// 1 + /// } else { + /// f(a - 1) + /// } + /// } + /// # fn main() { + /// # print!("{}", f(1)); + /// # } + /// ``` + #[clippy::version = "1.61.0"] + pub ONLY_USED_IN_RECURSION, + nursery, + "arguments that is only used in recursion can be removed" +} +declare_lint_pass!(OnlyUsedInRecursion => [ONLY_USED_IN_RECURSION]); + +impl<'tcx> LateLintPass<'tcx> for OnlyUsedInRecursion { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx rustc_hir::FnDecl<'tcx>, + body: &'tcx Body<'tcx>, + _: Span, + id: HirId, + ) { + if is_lint_allowed(cx, ONLY_USED_IN_RECURSION, id) { + return; + } + if let FnKind::ItemFn(ident, ..) | FnKind::Method(ident, ..) = kind { + let def_id = id.owner.to_def_id(); + let data = cx.tcx.def_path(def_id).data; + + if data.len() > 1 { + match data.get(data.len() - 2) { + Some(DisambiguatedDefPathData { + data: DefPathData::Impl, + disambiguator, + }) if *disambiguator != 0 => return, + _ => {}, + } + } + + let has_self = !matches!(decl.implicit_self, ImplicitSelfKind::None); + + let ty_res = cx.typeck_results(); + let param_span = body + .params + .iter() + .flat_map(|param| { + let mut v = Vec::new(); + param.pat.each_binding(|_, hir_id, span, ident| { + v.push((hir_id, span, ident)); + }); + v + }) + .skip(if has_self { 1 } else { 0 }) + .filter(|(_, _, ident)| !ident.name.as_str().starts_with('_')) + .collect_vec(); + + let params = body.params.iter().map(|param| param.pat).collect(); + + let mut visitor = SideEffectVisit { + graph: FxHashMap::default(), + has_side_effect: FxHashSet::default(), + ret_vars: Vec::new(), + contains_side_effect: false, + break_vars: FxHashMap::default(), + params, + fn_ident: ident, + fn_def_id: def_id, + is_method: matches!(kind, FnKind::Method(..)), + has_self, + ty_res, + tcx: cx.tcx, + visited_exprs: FxHashSet::default(), + }; + + visitor.visit_expr(&body.value); + let vars = std::mem::take(&mut visitor.ret_vars); + // this would set the return variables to side effect + visitor.add_side_effect(vars); + + let mut queue = visitor.has_side_effect.iter().copied().collect::<VecDeque<_>>(); + + // a simple BFS to check all the variables that have side effect + while let Some(id) = queue.pop_front() { + if let Some(next) = visitor.graph.get(&id) { + for i in next { + if !visitor.has_side_effect.contains(i) { + visitor.has_side_effect.insert(*i); + queue.push_back(*i); + } + } + } + } + + for (id, span, ident) in param_span { + // if the variable is not used in recursion, it would be marked as unused + if !visitor.has_side_effect.contains(&id) { + let mut queue = VecDeque::new(); + let mut visited = FxHashSet::default(); + + queue.push_back(id); + + // a simple BFS to check the graph can reach to itself + // if it can't, it means the variable is never used in recursion + while let Some(id) = queue.pop_front() { + if let Some(next) = visitor.graph.get(&id) { + for i in next { + if !visited.contains(i) { + visited.insert(id); + queue.push_back(*i); + } + } + } + } + + if visited.contains(&id) { + span_lint_and_sugg( + cx, + ONLY_USED_IN_RECURSION, + span, + "parameter is only used in recursion", + "if this is intentional, prefix with an underscore", + format!("_{}", ident.name.as_str()), + Applicability::MaybeIncorrect, + ); + } + } + } + } + } +} + +pub fn is_primitive(ty: Ty<'_>) -> bool { + let ty = ty.peel_refs(); + ty.is_primitive() || ty.is_str() +} + +pub fn is_array(ty: Ty<'_>) -> bool { + let ty = ty.peel_refs(); + ty.is_array() || ty.is_array_slice() +} + +/// This builds the graph of side effect. +/// The edge `a -> b` means if `a` has side effect, `b` will have side effect. +/// +/// There are some example in following code: +/// ```rust, ignore +/// let b = 1; +/// let a = b; // a -> b +/// let (c, d) = (a, b); // c -> b, d -> b +/// +/// let e = if a == 0 { // e -> a +/// c // e -> c +/// } else { +/// d // e -> d +/// }; +/// ``` +pub struct SideEffectVisit<'tcx> { + graph: FxHashMap<HirId, FxHashSet<HirId>>, + has_side_effect: FxHashSet<HirId>, + // bool for if the variable was dereferenced from mutable reference + ret_vars: Vec<(HirId, bool)>, + contains_side_effect: bool, + // break label + break_vars: FxHashMap<Ident, Vec<(HirId, bool)>>, + params: Vec<&'tcx Pat<'tcx>>, + fn_ident: Ident, + fn_def_id: DefId, + is_method: bool, + has_self: bool, + ty_res: &'tcx TypeckResults<'tcx>, + tcx: TyCtxt<'tcx>, + visited_exprs: FxHashSet<HirId>, +} + +impl<'tcx> Visitor<'tcx> for SideEffectVisit<'tcx> { + fn visit_stmt(&mut self, s: &'tcx Stmt<'tcx>) { + match s.kind { + StmtKind::Local(Local { + pat, init: Some(init), .. + }) => { + self.visit_pat_expr(pat, init, false); + }, + StmtKind::Item(_) | StmtKind::Expr(_) | StmtKind::Semi(_) => { + walk_stmt(self, s); + }, + StmtKind::Local(_) => {}, + } + self.ret_vars.clear(); + } + + fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) { + if !self.visited_exprs.insert(ex.hir_id) { + return; + } + match ex.kind { + ExprKind::Array(exprs) | ExprKind::Tup(exprs) => { + self.ret_vars = exprs + .iter() + .flat_map(|expr| { + self.visit_expr(expr); + std::mem::take(&mut self.ret_vars) + }) + .collect(); + }, + ExprKind::Call(callee, args) => self.visit_fn(callee, args), + ExprKind::MethodCall(path, args, _) => self.visit_method_call(path, args), + ExprKind::Binary(_, lhs, rhs) => { + self.visit_bin_op(lhs, rhs); + }, + ExprKind::Unary(op, expr) => self.visit_un_op(op, expr), + ExprKind::Let(Let { pat, init, .. }) => self.visit_pat_expr(pat, init, false), + ExprKind::If(bind, then_expr, else_expr) => { + self.visit_if(bind, then_expr, else_expr); + }, + ExprKind::Match(expr, arms, _) => self.visit_match(expr, arms), + // since analysing the closure is not easy, just set all variables in it to side-effect + ExprKind::Closure(&Closure { body, .. }) => { + let body = self.tcx.hir().body(body); + self.visit_body(body); + let vars = std::mem::take(&mut self.ret_vars); + self.add_side_effect(vars); + }, + ExprKind::Loop(block, label, _, _) | ExprKind::Block(block, label) => { + self.visit_block_label(block, label); + }, + ExprKind::Assign(bind, expr, _) => { + self.visit_assign(bind, expr); + }, + ExprKind::AssignOp(_, bind, expr) => { + self.visit_assign(bind, expr); + self.visit_bin_op(bind, expr); + }, + ExprKind::Field(expr, _) => { + self.visit_expr(expr); + if matches!(self.ty_res.expr_ty(expr).kind(), ty::Ref(_, _, Mutability::Mut)) { + self.ret_vars.iter_mut().for_each(|(_, b)| *b = true); + } + }, + ExprKind::Index(expr, index) => { + self.visit_expr(expr); + let mut vars = std::mem::take(&mut self.ret_vars); + self.visit_expr(index); + self.ret_vars.append(&mut vars); + + if !is_array(self.ty_res.expr_ty(expr)) { + self.add_side_effect(self.ret_vars.clone()); + } else if matches!(self.ty_res.expr_ty(expr).kind(), ty::Ref(_, _, Mutability::Mut)) { + self.ret_vars.iter_mut().for_each(|(_, b)| *b = true); + } + }, + ExprKind::Break(dest, Some(expr)) => { + self.visit_expr(expr); + if let Some(label) = dest.label { + self.break_vars + .entry(label.ident) + .or_insert(Vec::new()) + .append(&mut self.ret_vars); + } + self.contains_side_effect = true; + }, + ExprKind::Ret(Some(expr)) => { + self.visit_expr(expr); + let vars = std::mem::take(&mut self.ret_vars); + self.add_side_effect(vars); + self.contains_side_effect = true; + }, + ExprKind::Break(_, None) | ExprKind::Continue(_) | ExprKind::Ret(None) => { + self.contains_side_effect = true; + }, + ExprKind::Struct(_, exprs, expr) => { + let mut ret_vars = exprs + .iter() + .flat_map(|field| { + self.visit_expr(field.expr); + std::mem::take(&mut self.ret_vars) + }) + .collect(); + + walk_list!(self, visit_expr, expr); + self.ret_vars.append(&mut ret_vars); + }, + _ => walk_expr(self, ex), + } + } + + fn visit_path(&mut self, path: &'tcx Path<'tcx>, _id: HirId) { + if let Res::Local(id) = path.res { + self.ret_vars.push((id, false)); + } + } +} + +impl<'tcx> SideEffectVisit<'tcx> { + fn visit_assign(&mut self, lhs: &'tcx Expr<'tcx>, rhs: &'tcx Expr<'tcx>) { + // Just support array and tuple unwrapping for now. + // + // ex) `(a, b) = (c, d);` + // The graph would look like this: + // a -> c + // b -> d + // + // This would minimize the connection of the side-effect graph. + match (&lhs.kind, &rhs.kind) { + (ExprKind::Array(lhs), ExprKind::Array(rhs)) | (ExprKind::Tup(lhs), ExprKind::Tup(rhs)) => { + // if not, it is a compile error + debug_assert!(lhs.len() == rhs.len()); + izip!(*lhs, *rhs).for_each(|(lhs, rhs)| self.visit_assign(lhs, rhs)); + }, + // in other assigns, we have to connect all each other + // because they can be connected somehow + _ => { + self.visit_expr(lhs); + let lhs_vars = std::mem::take(&mut self.ret_vars); + self.visit_expr(rhs); + let rhs_vars = std::mem::take(&mut self.ret_vars); + self.connect_assign(&lhs_vars, &rhs_vars, false); + }, + } + } + + fn visit_block_label(&mut self, block: &'tcx Block<'tcx>, label: Option<Label>) { + self.visit_block(block); + let _ = label.and_then(|label| { + self.break_vars + .remove(&label.ident) + .map(|mut break_vars| self.ret_vars.append(&mut break_vars)) + }); + } + + fn visit_bin_op(&mut self, lhs: &'tcx Expr<'tcx>, rhs: &'tcx Expr<'tcx>) { + self.visit_expr(lhs); + let mut ret_vars = std::mem::take(&mut self.ret_vars); + self.visit_expr(rhs); + self.ret_vars.append(&mut ret_vars); + + // the binary operation between non primitive values are overloaded operators + // so they can have side-effects + if !is_primitive(self.ty_res.expr_ty(lhs)) || !is_primitive(self.ty_res.expr_ty(rhs)) { + self.ret_vars.iter().for_each(|id| { + self.has_side_effect.insert(id.0); + }); + self.contains_side_effect = true; + } + } + + fn visit_un_op(&mut self, op: UnOp, expr: &'tcx Expr<'tcx>) { + self.visit_expr(expr); + let ty = self.ty_res.expr_ty(expr); + // dereferencing a reference has no side-effect + if !is_primitive(ty) && !matches!((op, ty.kind()), (UnOp::Deref, ty::Ref(..))) { + self.add_side_effect(self.ret_vars.clone()); + } + + if matches!((op, ty.kind()), (UnOp::Deref, ty::Ref(_, _, Mutability::Mut))) { + self.ret_vars.iter_mut().for_each(|(_, b)| *b = true); + } + } + + fn visit_pat_expr(&mut self, pat: &'tcx Pat<'tcx>, expr: &'tcx Expr<'tcx>, connect_self: bool) { + match (&pat.kind, &expr.kind) { + (PatKind::Tuple(pats, _), ExprKind::Tup(exprs)) => { + self.ret_vars = izip!(*pats, *exprs) + .flat_map(|(pat, expr)| { + self.visit_pat_expr(pat, expr, connect_self); + std::mem::take(&mut self.ret_vars) + }) + .collect(); + }, + (PatKind::Slice(front_exprs, _, back_exprs), ExprKind::Array(exprs)) => { + let mut vars = izip!(*front_exprs, *exprs) + .flat_map(|(pat, expr)| { + self.visit_pat_expr(pat, expr, connect_self); + std::mem::take(&mut self.ret_vars) + }) + .collect(); + self.ret_vars = izip!(back_exprs.iter().rev(), exprs.iter().rev()) + .flat_map(|(pat, expr)| { + self.visit_pat_expr(pat, expr, connect_self); + std::mem::take(&mut self.ret_vars) + }) + .collect(); + self.ret_vars.append(&mut vars); + }, + _ => { + let mut lhs_vars = Vec::new(); + pat.each_binding(|_, id, _, _| lhs_vars.push((id, false))); + self.visit_expr(expr); + let rhs_vars = std::mem::take(&mut self.ret_vars); + self.connect_assign(&lhs_vars, &rhs_vars, connect_self); + self.ret_vars = rhs_vars; + }, + } + } + + fn visit_fn(&mut self, callee: &'tcx Expr<'tcx>, args: &'tcx [Expr<'tcx>]) { + self.visit_expr(callee); + let mut ret_vars = std::mem::take(&mut self.ret_vars); + self.add_side_effect(ret_vars.clone()); + + let mut is_recursive = false; + + if_chain! { + if !self.has_self; + if let ExprKind::Path(QPath::Resolved(_, path)) = callee.kind; + if let Res::Def(DefKind::Fn, def_id) = path.res; + if self.fn_def_id == def_id; + then { + is_recursive = true; + } + } + + if_chain! { + if !self.has_self && self.is_method; + if let ExprKind::Path(QPath::TypeRelative(ty, segment)) = callee.kind; + if segment.ident == self.fn_ident; + if let TyKind::Path(QPath::Resolved(_, path)) = ty.kind; + if let Res::SelfTy{ .. } = path.res; + then { + is_recursive = true; + } + } + + if is_recursive { + izip!(self.params.clone(), args).for_each(|(pat, expr)| { + self.visit_pat_expr(pat, expr, true); + self.ret_vars.clear(); + }); + } else { + // This would set arguments used in closure that does not have side-effect. + // Closure itself can be detected whether there is a side-effect, but the + // value of variable that is holding closure can change. + // So, we just check the variables. + self.ret_vars = args + .iter() + .flat_map(|expr| { + self.visit_expr(expr); + std::mem::take(&mut self.ret_vars) + }) + .collect_vec() + .into_iter() + .map(|id| { + self.has_side_effect.insert(id.0); + id + }) + .collect(); + self.contains_side_effect = true; + } + + self.ret_vars.append(&mut ret_vars); + } + + fn visit_method_call(&mut self, path: &'tcx PathSegment<'tcx>, args: &'tcx [Expr<'tcx>]) { + if_chain! { + if self.is_method; + if path.ident == self.fn_ident; + if let ExprKind::Path(QPath::Resolved(_, path)) = args.first().unwrap().kind; + if let Res::Local(..) = path.res; + let ident = path.segments.last().unwrap().ident; + if ident.name == kw::SelfLower; + then { + izip!(self.params.clone(), args.iter()) + .for_each(|(pat, expr)| { + self.visit_pat_expr(pat, expr, true); + self.ret_vars.clear(); + }); + } else { + self.ret_vars = args + .iter() + .flat_map(|expr| { + self.visit_expr(expr); + std::mem::take(&mut self.ret_vars) + }) + .collect_vec() + .into_iter() + .map(|a| { + self.has_side_effect.insert(a.0); + a + }) + .collect(); + self.contains_side_effect = true; + } + } + } + + fn visit_if(&mut self, bind: &'tcx Expr<'tcx>, then_expr: &'tcx Expr<'tcx>, else_expr: Option<&'tcx Expr<'tcx>>) { + let contains_side_effect = self.contains_side_effect; + self.contains_side_effect = false; + self.visit_expr(bind); + let mut vars = std::mem::take(&mut self.ret_vars); + self.visit_expr(then_expr); + let mut then_vars = std::mem::take(&mut self.ret_vars); + walk_list!(self, visit_expr, else_expr); + if self.contains_side_effect { + self.add_side_effect(vars.clone()); + } + self.contains_side_effect |= contains_side_effect; + self.ret_vars.append(&mut vars); + self.ret_vars.append(&mut then_vars); + } + + fn visit_match(&mut self, expr: &'tcx Expr<'tcx>, arms: &'tcx [Arm<'tcx>]) { + self.visit_expr(expr); + let mut expr_vars = std::mem::take(&mut self.ret_vars); + self.ret_vars = arms + .iter() + .flat_map(|arm| { + let contains_side_effect = self.contains_side_effect; + self.contains_side_effect = false; + // this would visit `expr` multiple times + // but couldn't think of a better way + self.visit_pat_expr(arm.pat, expr, false); + let mut vars = std::mem::take(&mut self.ret_vars); + let _ = arm.guard.as_ref().map(|guard| { + self.visit_expr(match guard { + Guard::If(expr) | Guard::IfLet(Let { init: expr, .. }) => expr, + }); + vars.append(&mut self.ret_vars); + }); + self.visit_expr(arm.body); + if self.contains_side_effect { + self.add_side_effect(vars.clone()); + self.add_side_effect(expr_vars.clone()); + } + self.contains_side_effect |= contains_side_effect; + vars.append(&mut self.ret_vars); + vars + }) + .collect(); + self.ret_vars.append(&mut expr_vars); + } + + fn connect_assign(&mut self, lhs: &[(HirId, bool)], rhs: &[(HirId, bool)], connect_self: bool) { + // if mutable dereference is on assignment it can have side-effect + // (this can lead to parameter mutable dereference and change the original value) + // too hard to detect whether this value is from parameter, so this would all + // check mutable dereference assignment to side effect + lhs.iter().filter(|(_, b)| *b).for_each(|(id, _)| { + self.has_side_effect.insert(*id); + self.contains_side_effect = true; + }); + + // there is no connection + if lhs.is_empty() || rhs.is_empty() { + return; + } + + // by connected rhs in cycle, the connections would decrease + // from `n * m` to `n + m` + // where `n` and `m` are length of `lhs` and `rhs`. + + // unwrap is possible since rhs is not empty + let rhs_first = rhs.first().unwrap(); + for (id, _) in lhs.iter() { + if connect_self || *id != rhs_first.0 { + self.graph + .entry(*id) + .or_insert_with(FxHashSet::default) + .insert(rhs_first.0); + } + } + + let rhs = rhs.iter(); + izip!(rhs.clone().cycle().skip(1), rhs).for_each(|(from, to)| { + if connect_self || from.0 != to.0 { + self.graph.entry(from.0).or_insert_with(FxHashSet::default).insert(to.0); + } + }); + } + + fn add_side_effect(&mut self, v: Vec<(HirId, bool)>) { + for (id, _) in v { + self.has_side_effect.insert(id); + self.contains_side_effect = true; + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/open_options.rs b/src/tools/clippy/clippy_lints/src/open_options.rs new file mode 100644 index 000000000..5a0b50420 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/open_options.rs @@ -0,0 +1,202 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::paths; +use clippy_utils::ty::match_type; +use rustc_ast::ast::LitKind; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::{Span, Spanned}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for duplicate open options as well as combinations + /// that make no sense. + /// + /// ### Why is this bad? + /// In the best case, the code will be harder to read than + /// necessary. I don't know the worst case. + /// + /// ### Example + /// ```rust + /// use std::fs::OpenOptions; + /// + /// OpenOptions::new().read(true).truncate(true); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NONSENSICAL_OPEN_OPTIONS, + correctness, + "nonsensical combination of options for opening a file" +} + +declare_lint_pass!(OpenOptions => [NONSENSICAL_OPEN_OPTIONS]); + +impl<'tcx> LateLintPass<'tcx> for OpenOptions { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if let ExprKind::MethodCall(path, [self_arg, ..], _) = &e.kind { + let obj_ty = cx.typeck_results().expr_ty(self_arg).peel_refs(); + if path.ident.name == sym!(open) && match_type(cx, obj_ty, &paths::OPEN_OPTIONS) { + let mut options = Vec::new(); + get_open_options(cx, self_arg, &mut options); + check_open_options(cx, &options, e.span); + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Argument { + True, + False, + Unknown, +} + +#[derive(Debug)] +enum OpenOption { + Write, + Read, + Truncate, + Create, + Append, +} + +fn get_open_options(cx: &LateContext<'_>, argument: &Expr<'_>, options: &mut Vec<(OpenOption, Argument)>) { + if let ExprKind::MethodCall(path, arguments, _) = argument.kind { + let obj_ty = cx.typeck_results().expr_ty(&arguments[0]).peel_refs(); + + // Only proceed if this is a call on some object of type std::fs::OpenOptions + if match_type(cx, obj_ty, &paths::OPEN_OPTIONS) && arguments.len() >= 2 { + let argument_option = match arguments[1].kind { + ExprKind::Lit(ref span) => { + if let Spanned { + node: LitKind::Bool(lit), + .. + } = *span + { + if lit { Argument::True } else { Argument::False } + } else { + // The function is called with a literal which is not a boolean literal. + // This is theoretically possible, but not very likely. + return; + } + }, + _ => Argument::Unknown, + }; + + match path.ident.as_str() { + "create" => { + options.push((OpenOption::Create, argument_option)); + }, + "append" => { + options.push((OpenOption::Append, argument_option)); + }, + "truncate" => { + options.push((OpenOption::Truncate, argument_option)); + }, + "read" => { + options.push((OpenOption::Read, argument_option)); + }, + "write" => { + options.push((OpenOption::Write, argument_option)); + }, + _ => (), + } + + get_open_options(cx, &arguments[0], options); + } + } +} + +fn check_open_options(cx: &LateContext<'_>, options: &[(OpenOption, Argument)], span: Span) { + let (mut create, mut append, mut truncate, mut read, mut write) = (false, false, false, false, false); + let (mut create_arg, mut append_arg, mut truncate_arg, mut read_arg, mut write_arg) = + (false, false, false, false, false); + // This code is almost duplicated (oh, the irony), but I haven't found a way to + // unify it. + + for option in options { + match *option { + (OpenOption::Create, arg) => { + if create { + span_lint( + cx, + NONSENSICAL_OPEN_OPTIONS, + span, + "the method `create` is called more than once", + ); + } else { + create = true; + } + create_arg = create_arg || (arg == Argument::True); + }, + (OpenOption::Append, arg) => { + if append { + span_lint( + cx, + NONSENSICAL_OPEN_OPTIONS, + span, + "the method `append` is called more than once", + ); + } else { + append = true; + } + append_arg = append_arg || (arg == Argument::True); + }, + (OpenOption::Truncate, arg) => { + if truncate { + span_lint( + cx, + NONSENSICAL_OPEN_OPTIONS, + span, + "the method `truncate` is called more than once", + ); + } else { + truncate = true; + } + truncate_arg = truncate_arg || (arg == Argument::True); + }, + (OpenOption::Read, arg) => { + if read { + span_lint( + cx, + NONSENSICAL_OPEN_OPTIONS, + span, + "the method `read` is called more than once", + ); + } else { + read = true; + } + read_arg = read_arg || (arg == Argument::True); + }, + (OpenOption::Write, arg) => { + if write { + span_lint( + cx, + NONSENSICAL_OPEN_OPTIONS, + span, + "the method `write` is called more than once", + ); + } else { + write = true; + } + write_arg = write_arg || (arg == Argument::True); + }, + } + } + + if read && truncate && read_arg && truncate_arg && !(write && write_arg) { + span_lint( + cx, + NONSENSICAL_OPEN_OPTIONS, + span, + "file opened with `truncate` and `read`", + ); + } + if append && truncate && append_arg && truncate_arg { + span_lint( + cx, + NONSENSICAL_OPEN_OPTIONS, + span, + "file opened with `append` and `truncate`", + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/absurd_extreme_comparisons.rs b/src/tools/clippy/clippy_lints/src/operators/absurd_extreme_comparisons.rs new file mode 100644 index 000000000..1ec4240af --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/absurd_extreme_comparisons.rs @@ -0,0 +1,142 @@ +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use clippy_utils::comparisons::{normalize_comparison, Rel}; +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_isize_or_usize; +use clippy_utils::{clip, int_bits, unsext}; + +use super::ABSURD_EXTREME_COMPARISONS; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + op: BinOpKind, + lhs: &'tcx Expr<'_>, + rhs: &'tcx Expr<'_>, +) { + if let Some((culprit, result)) = detect_absurd_comparison(cx, op, lhs, rhs) { + let msg = "this comparison involving the minimum or maximum element for this \ + type contains a case that is always true or always false"; + + let conclusion = match result { + AbsurdComparisonResult::AlwaysFalse => "this comparison is always false".to_owned(), + AbsurdComparisonResult::AlwaysTrue => "this comparison is always true".to_owned(), + AbsurdComparisonResult::InequalityImpossible => format!( + "the case where the two sides are not equal never occurs, consider using `{} == {}` \ + instead", + snippet(cx, lhs.span, "lhs"), + snippet(cx, rhs.span, "rhs") + ), + }; + + let help = format!( + "because `{}` is the {} value for this type, {}", + snippet(cx, culprit.expr.span, "x"), + match culprit.which { + ExtremeType::Minimum => "minimum", + ExtremeType::Maximum => "maximum", + }, + conclusion + ); + + span_lint_and_help(cx, ABSURD_EXTREME_COMPARISONS, expr.span, msg, None, &help); + } +} + +enum ExtremeType { + Minimum, + Maximum, +} + +struct ExtremeExpr<'a> { + which: ExtremeType, + expr: &'a Expr<'a>, +} + +enum AbsurdComparisonResult { + AlwaysFalse, + AlwaysTrue, + InequalityImpossible, +} + +fn is_cast_between_fixed_and_target<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { + if let ExprKind::Cast(cast_exp, _) = expr.kind { + let precast_ty = cx.typeck_results().expr_ty(cast_exp); + let cast_ty = cx.typeck_results().expr_ty(expr); + + return is_isize_or_usize(precast_ty) != is_isize_or_usize(cast_ty); + } + + false +} + +fn detect_absurd_comparison<'tcx>( + cx: &LateContext<'tcx>, + op: BinOpKind, + lhs: &'tcx Expr<'_>, + rhs: &'tcx Expr<'_>, +) -> Option<(ExtremeExpr<'tcx>, AbsurdComparisonResult)> { + use AbsurdComparisonResult::{AlwaysFalse, AlwaysTrue, InequalityImpossible}; + use ExtremeType::{Maximum, Minimum}; + // absurd comparison only makes sense on primitive types + // primitive types don't implement comparison operators with each other + if cx.typeck_results().expr_ty(lhs) != cx.typeck_results().expr_ty(rhs) { + return None; + } + + // comparisons between fix sized types and target sized types are considered unanalyzable + if is_cast_between_fixed_and_target(cx, lhs) || is_cast_between_fixed_and_target(cx, rhs) { + return None; + } + + let (rel, normalized_lhs, normalized_rhs) = normalize_comparison(op, lhs, rhs)?; + + let lx = detect_extreme_expr(cx, normalized_lhs); + let rx = detect_extreme_expr(cx, normalized_rhs); + + Some(match rel { + Rel::Lt => { + match (lx, rx) { + (Some(l @ ExtremeExpr { which: Maximum, .. }), _) => (l, AlwaysFalse), // max < x + (_, Some(r @ ExtremeExpr { which: Minimum, .. })) => (r, AlwaysFalse), // x < min + _ => return None, + } + }, + Rel::Le => { + match (lx, rx) { + (Some(l @ ExtremeExpr { which: Minimum, .. }), _) => (l, AlwaysTrue), // min <= x + (Some(l @ ExtremeExpr { which: Maximum, .. }), _) => (l, InequalityImpossible), // max <= x + (_, Some(r @ ExtremeExpr { which: Minimum, .. })) => (r, InequalityImpossible), // x <= min + (_, Some(r @ ExtremeExpr { which: Maximum, .. })) => (r, AlwaysTrue), // x <= max + _ => return None, + } + }, + Rel::Ne | Rel::Eq => return None, + }) +} + +fn detect_extreme_expr<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<ExtremeExpr<'tcx>> { + let ty = cx.typeck_results().expr_ty(expr); + + let cv = constant(cx, cx.typeck_results(), expr)?.0; + + let which = match (ty.kind(), cv) { + (&ty::Bool, Constant::Bool(false)) | (&ty::Uint(_), Constant::Int(0)) => ExtremeType::Minimum, + (&ty::Int(ity), Constant::Int(i)) if i == unsext(cx.tcx, i128::MIN >> (128 - int_bits(cx.tcx, ity)), ity) => { + ExtremeType::Minimum + }, + + (&ty::Bool, Constant::Bool(true)) => ExtremeType::Maximum, + (&ty::Int(ity), Constant::Int(i)) if i == unsext(cx.tcx, i128::MAX >> (128 - int_bits(cx.tcx, ity)), ity) => { + ExtremeType::Maximum + }, + (&ty::Uint(uty), Constant::Int(i)) if clip(cx.tcx, u128::MAX, uty) == i => ExtremeType::Maximum, + + _ => return None, + }; + Some(ExtremeExpr { which, expr }) +} diff --git a/src/tools/clippy/clippy_lints/src/operators/arithmetic.rs b/src/tools/clippy/clippy_lints/src/operators/arithmetic.rs new file mode 100644 index 000000000..800cf249f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/arithmetic.rs @@ -0,0 +1,119 @@ +#![allow( + // False positive + clippy::match_same_arms +)] + +use super::ARITHMETIC; +use clippy_utils::{consts::constant_simple, diagnostics::span_lint}; +use rustc_data_structures::fx::FxHashSet; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::impl_lint_pass; +use rustc_span::source_map::Span; + +const HARD_CODED_ALLOWED: &[&str] = &["std::num::Saturating", "std::string::String", "std::num::Wrapping"]; + +#[derive(Debug)] +pub struct Arithmetic { + allowed: FxHashSet<String>, + // Used to check whether expressions are constants, such as in enum discriminants and consts + const_span: Option<Span>, + expr_span: Option<Span>, +} + +impl_lint_pass!(Arithmetic => [ARITHMETIC]); + +impl Arithmetic { + #[must_use] + pub fn new(mut allowed: FxHashSet<String>) -> Self { + allowed.extend(HARD_CODED_ALLOWED.iter().copied().map(String::from)); + Self { + allowed, + const_span: None, + expr_span: None, + } + } + + /// Checks if the given `expr` has any of the inner `allowed` elements. + fn is_allowed_ty(&self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool { + self.allowed.contains( + cx.typeck_results() + .expr_ty(expr) + .to_string() + .split('<') + .next() + .unwrap_or_default(), + ) + } + + fn issue_lint(&mut self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) { + span_lint(cx, ARITHMETIC, expr.span, "arithmetic detected"); + self.expr_span = Some(expr.span); + } +} + +impl<'tcx> LateLintPass<'tcx> for Arithmetic { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if self.expr_span.is_some() { + return; + } + if let Some(span) = self.const_span && span.contains(expr.span) { + return; + } + match &expr.kind { + hir::ExprKind::Binary(op, lhs, rhs) | hir::ExprKind::AssignOp(op, lhs, rhs) => { + let ( + hir::BinOpKind::Add + | hir::BinOpKind::Sub + | hir::BinOpKind::Mul + | hir::BinOpKind::Div + | hir::BinOpKind::Rem + | hir::BinOpKind::Shl + | hir::BinOpKind::Shr + ) = op.node else { + return; + }; + if self.is_allowed_ty(cx, lhs) || self.is_allowed_ty(cx, rhs) { + return; + } + self.issue_lint(cx, expr); + }, + hir::ExprKind::Unary(hir::UnOp::Neg, _) => { + // CTFE already takes care of things like `-1` that do not overflow. + if constant_simple(cx, cx.typeck_results(), expr).is_none() { + self.issue_lint(cx, expr); + } + }, + _ => {}, + } + } + + fn check_body(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) { + let body_owner = cx.tcx.hir().body_owner_def_id(body.id()); + match cx.tcx.hir().body_owner_kind(body_owner) { + hir::BodyOwnerKind::Const | hir::BodyOwnerKind::Static(_) => { + let body_span = cx.tcx.def_span(body_owner); + if let Some(span) = self.const_span && span.contains(body_span) { + return; + } + self.const_span = Some(body_span); + }, + hir::BodyOwnerKind::Closure | hir::BodyOwnerKind::Fn => {}, + } + } + + fn check_body_post(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) { + let body_owner = cx.tcx.hir().body_owner(body.id()); + let body_span = cx.tcx.hir().span(body_owner); + if let Some(span) = self.const_span && span.contains(body_span) { + return; + } + self.const_span = None; + } + + fn check_expr_post(&mut self, _: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if Some(expr.span) == self.expr_span { + self.expr_span = None; + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/assign_op_pattern.rs b/src/tools/clippy/clippy_lints/src/operators/assign_op_pattern.rs new file mode 100644 index 000000000..945a09a64 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/assign_op_pattern.rs @@ -0,0 +1,181 @@ +use clippy_utils::binop_traits; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::implements_trait; +use clippy_utils::{eq_expr_value, trait_ref_of_method}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_lint::LateContext; +use rustc_middle::mir::FakeReadCause; +use rustc_middle::ty::BorrowKind; +use rustc_trait_selection::infer::TyCtxtInferExt; +use rustc_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, PlaceWithHirId}; + +use super::ASSIGN_OP_PATTERN; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + assignee: &'tcx hir::Expr<'_>, + e: &'tcx hir::Expr<'_>, +) { + if let hir::ExprKind::Binary(op, l, r) = &e.kind { + let lint = |assignee: &hir::Expr<'_>, rhs: &hir::Expr<'_>| { + let ty = cx.typeck_results().expr_ty(assignee); + let rty = cx.typeck_results().expr_ty(rhs); + if_chain! { + if let Some((_, lang_item)) = binop_traits(op.node); + if let Ok(trait_id) = cx.tcx.lang_items().require(lang_item); + let parent_fn = cx.tcx.hir().get_parent_item(e.hir_id); + if trait_ref_of_method(cx, parent_fn) + .map_or(true, |t| t.path.res.def_id() != trait_id); + if implements_trait(cx, ty, trait_id, &[rty.into()]); + then { + // Primitive types execute assign-ops right-to-left. Every other type is left-to-right. + if !(ty.is_primitive() && rty.is_primitive()) { + // TODO: This will have false negatives as it doesn't check if the borrows are + // actually live at the end of their respective expressions. + let mut_borrows = mut_borrows_in_expr(cx, assignee); + let imm_borrows = imm_borrows_in_expr(cx, rhs); + if mut_borrows.iter().any(|id| imm_borrows.contains(id)) { + return; + } + } + span_lint_and_then( + cx, + ASSIGN_OP_PATTERN, + expr.span, + "manual implementation of an assign operation", + |diag| { + if let (Some(snip_a), Some(snip_r)) = + (snippet_opt(cx, assignee.span), snippet_opt(cx, rhs.span)) + { + diag.span_suggestion( + expr.span, + "replace it with", + format!("{} {}= {}", snip_a, op.node.as_str(), snip_r), + Applicability::MachineApplicable, + ); + } + }, + ); + } + } + }; + + let mut visitor = ExprVisitor { + assignee, + counter: 0, + cx, + }; + + walk_expr(&mut visitor, e); + + if visitor.counter == 1 { + // a = a op b + if eq_expr_value(cx, assignee, l) { + lint(assignee, r); + } + // a = b commutative_op a + // Limited to primitive type as these ops are know to be commutative + if eq_expr_value(cx, assignee, r) && cx.typeck_results().expr_ty(assignee).is_primitive_ty() { + match op.node { + hir::BinOpKind::Add + | hir::BinOpKind::Mul + | hir::BinOpKind::And + | hir::BinOpKind::Or + | hir::BinOpKind::BitXor + | hir::BinOpKind::BitAnd + | hir::BinOpKind::BitOr => { + lint(assignee, l); + }, + _ => {}, + } + } + } + } +} + +struct ExprVisitor<'a, 'tcx> { + assignee: &'a hir::Expr<'a>, + counter: u8, + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> Visitor<'tcx> for ExprVisitor<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) { + if eq_expr_value(self.cx, self.assignee, expr) { + self.counter += 1; + } + + walk_expr(self, expr); + } +} + +fn imm_borrows_in_expr(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> hir::HirIdSet { + struct S(hir::HirIdSet); + impl Delegate<'_> for S { + fn borrow(&mut self, place: &PlaceWithHirId<'_>, _: hir::HirId, kind: BorrowKind) { + if matches!(kind, BorrowKind::ImmBorrow | BorrowKind::UniqueImmBorrow) { + self.0.insert(match place.place.base { + PlaceBase::Local(id) => id, + PlaceBase::Upvar(id) => id.var_path.hir_id, + _ => return, + }); + } + } + + fn consume(&mut self, _: &PlaceWithHirId<'_>, _: hir::HirId) {} + fn mutate(&mut self, _: &PlaceWithHirId<'_>, _: hir::HirId) {} + fn fake_read(&mut self, _: &PlaceWithHirId<'_>, _: FakeReadCause, _: hir::HirId) {} + fn copy(&mut self, _: &PlaceWithHirId<'_>, _: hir::HirId) {} + } + + let mut s = S(hir::HirIdSet::default()); + cx.tcx.infer_ctxt().enter(|infcx| { + let mut v = ExprUseVisitor::new( + &mut s, + &infcx, + cx.tcx.hir().body_owner_def_id(cx.enclosing_body.unwrap()), + cx.param_env, + cx.typeck_results(), + ); + v.consume_expr(e); + }); + s.0 +} + +fn mut_borrows_in_expr(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> hir::HirIdSet { + struct S(hir::HirIdSet); + impl Delegate<'_> for S { + fn borrow(&mut self, place: &PlaceWithHirId<'_>, _: hir::HirId, kind: BorrowKind) { + if matches!(kind, BorrowKind::MutBorrow) { + self.0.insert(match place.place.base { + PlaceBase::Local(id) => id, + PlaceBase::Upvar(id) => id.var_path.hir_id, + _ => return, + }); + } + } + + fn consume(&mut self, _: &PlaceWithHirId<'_>, _: hir::HirId) {} + fn mutate(&mut self, _: &PlaceWithHirId<'_>, _: hir::HirId) {} + fn fake_read(&mut self, _: &PlaceWithHirId<'_>, _: FakeReadCause, _: hir::HirId) {} + fn copy(&mut self, _: &PlaceWithHirId<'_>, _: hir::HirId) {} + } + + let mut s = S(hir::HirIdSet::default()); + cx.tcx.infer_ctxt().enter(|infcx| { + let mut v = ExprUseVisitor::new( + &mut s, + &infcx, + cx.tcx.hir().body_owner_def_id(cx.enclosing_body.unwrap()), + cx.param_env, + cx.typeck_results(), + ); + v.consume_expr(e); + }); + s.0 +} diff --git a/src/tools/clippy/clippy_lints/src/operators/bit_mask.rs b/src/tools/clippy/clippy_lints/src/operators/bit_mask.rs new file mode 100644 index 000000000..74387fbc8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/bit_mask.rs @@ -0,0 +1,197 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_span::source_map::Span; + +use super::{BAD_BIT_MASK, INEFFECTIVE_BIT_MASK}; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, +) { + if op.is_comparison() { + if let Some(cmp_opt) = fetch_int_literal(cx, right) { + check_compare(cx, left, op, cmp_opt, e.span); + } else if let Some(cmp_val) = fetch_int_literal(cx, left) { + check_compare(cx, right, invert_cmp(op), cmp_val, e.span); + } + } +} + +#[must_use] +fn invert_cmp(cmp: BinOpKind) -> BinOpKind { + match cmp { + BinOpKind::Eq => BinOpKind::Eq, + BinOpKind::Ne => BinOpKind::Ne, + BinOpKind::Lt => BinOpKind::Gt, + BinOpKind::Gt => BinOpKind::Lt, + BinOpKind::Le => BinOpKind::Ge, + BinOpKind::Ge => BinOpKind::Le, + _ => BinOpKind::Or, // Dummy + } +} + +fn check_compare(cx: &LateContext<'_>, bit_op: &Expr<'_>, cmp_op: BinOpKind, cmp_value: u128, span: Span) { + if let ExprKind::Binary(op, left, right) = &bit_op.kind { + if op.node != BinOpKind::BitAnd && op.node != BinOpKind::BitOr { + return; + } + fetch_int_literal(cx, right) + .or_else(|| fetch_int_literal(cx, left)) + .map_or((), |mask| check_bit_mask(cx, op.node, cmp_op, mask, cmp_value, span)); + } +} + +#[allow(clippy::too_many_lines)] +fn check_bit_mask( + cx: &LateContext<'_>, + bit_op: BinOpKind, + cmp_op: BinOpKind, + mask_value: u128, + cmp_value: u128, + span: Span, +) { + match cmp_op { + BinOpKind::Eq | BinOpKind::Ne => match bit_op { + BinOpKind::BitAnd => { + if mask_value & cmp_value != cmp_value { + if cmp_value != 0 { + span_lint( + cx, + BAD_BIT_MASK, + span, + &format!( + "incompatible bit mask: `_ & {}` can never be equal to `{}`", + mask_value, cmp_value + ), + ); + } + } else if mask_value == 0 { + span_lint(cx, BAD_BIT_MASK, span, "&-masking with zero"); + } + }, + BinOpKind::BitOr => { + if mask_value | cmp_value != cmp_value { + span_lint( + cx, + BAD_BIT_MASK, + span, + &format!( + "incompatible bit mask: `_ | {}` can never be equal to `{}`", + mask_value, cmp_value + ), + ); + } + }, + _ => (), + }, + BinOpKind::Lt | BinOpKind::Ge => match bit_op { + BinOpKind::BitAnd => { + if mask_value < cmp_value { + span_lint( + cx, + BAD_BIT_MASK, + span, + &format!( + "incompatible bit mask: `_ & {}` will always be lower than `{}`", + mask_value, cmp_value + ), + ); + } else if mask_value == 0 { + span_lint(cx, BAD_BIT_MASK, span, "&-masking with zero"); + } + }, + BinOpKind::BitOr => { + if mask_value >= cmp_value { + span_lint( + cx, + BAD_BIT_MASK, + span, + &format!( + "incompatible bit mask: `_ | {}` will never be lower than `{}`", + mask_value, cmp_value + ), + ); + } else { + check_ineffective_lt(cx, span, mask_value, cmp_value, "|"); + } + }, + BinOpKind::BitXor => check_ineffective_lt(cx, span, mask_value, cmp_value, "^"), + _ => (), + }, + BinOpKind::Le | BinOpKind::Gt => match bit_op { + BinOpKind::BitAnd => { + if mask_value <= cmp_value { + span_lint( + cx, + BAD_BIT_MASK, + span, + &format!( + "incompatible bit mask: `_ & {}` will never be higher than `{}`", + mask_value, cmp_value + ), + ); + } else if mask_value == 0 { + span_lint(cx, BAD_BIT_MASK, span, "&-masking with zero"); + } + }, + BinOpKind::BitOr => { + if mask_value > cmp_value { + span_lint( + cx, + BAD_BIT_MASK, + span, + &format!( + "incompatible bit mask: `_ | {}` will always be higher than `{}`", + mask_value, cmp_value + ), + ); + } else { + check_ineffective_gt(cx, span, mask_value, cmp_value, "|"); + } + }, + BinOpKind::BitXor => check_ineffective_gt(cx, span, mask_value, cmp_value, "^"), + _ => (), + }, + _ => (), + } +} + +fn check_ineffective_lt(cx: &LateContext<'_>, span: Span, m: u128, c: u128, op: &str) { + if c.is_power_of_two() && m < c { + span_lint( + cx, + INEFFECTIVE_BIT_MASK, + span, + &format!( + "ineffective bit mask: `x {} {}` compared to `{}`, is the same as x compared directly", + op, m, c + ), + ); + } +} + +fn check_ineffective_gt(cx: &LateContext<'_>, span: Span, m: u128, c: u128, op: &str) { + if (c + 1).is_power_of_two() && m <= c { + span_lint( + cx, + INEFFECTIVE_BIT_MASK, + span, + &format!( + "ineffective bit mask: `x {} {}` compared to `{}`, is the same as x compared directly", + op, m, c + ), + ); + } +} + +fn fetch_int_literal(cx: &LateContext<'_>, lit: &Expr<'_>) -> Option<u128> { + match constant(cx, cx.typeck_results(), lit)?.0 { + Constant::Int(n) => Some(n), + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/cmp_nan.rs b/src/tools/clippy/clippy_lints/src/operators/cmp_nan.rs new file mode 100644 index 000000000..786ae1552 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/cmp_nan.rs @@ -0,0 +1,30 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::in_constant; +use rustc_hir::{BinOpKind, Expr}; +use rustc_lint::LateContext; + +use super::CMP_NAN; + +pub(super) fn check(cx: &LateContext<'_>, e: &Expr<'_>, op: BinOpKind, lhs: &Expr<'_>, rhs: &Expr<'_>) { + if op.is_comparison() && !in_constant(cx, e.hir_id) && (is_nan(cx, lhs) || is_nan(cx, rhs)) { + span_lint( + cx, + CMP_NAN, + e.span, + "doomed comparison with `NAN`, use `{f32,f64}::is_nan()` instead", + ); + } +} + +fn is_nan(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + if let Some((value, _)) = constant(cx, cx.typeck_results(), e) { + match value { + Constant::F32(num) => num.is_nan(), + Constant::F64(num) => num.is_nan(), + _ => false, + } + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/cmp_owned.rs b/src/tools/clippy/clippy_lints/src/operators/cmp_owned.rs new file mode 100644 index 000000000..e1f9b5906 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/cmp_owned.rs @@ -0,0 +1,147 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::ty::{implements_trait, is_copy}; +use clippy_utils::{match_any_def_paths, path_def_id, paths}; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp}; +use rustc_lint::LateContext; +use rustc_middle::ty::Ty; +use rustc_span::symbol::sym; + +use super::CMP_OWNED; + +pub(super) fn check(cx: &LateContext<'_>, op: BinOpKind, lhs: &Expr<'_>, rhs: &Expr<'_>) { + if op.is_comparison() { + check_op(cx, lhs, rhs, true); + check_op(cx, rhs, lhs, false); + } +} + +#[derive(Default)] +struct EqImpl { + ty_eq_other: bool, + other_eq_ty: bool, +} +impl EqImpl { + fn is_implemented(&self) -> bool { + self.ty_eq_other || self.other_eq_ty + } +} + +fn symmetric_partial_eq<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, other: Ty<'tcx>) -> Option<EqImpl> { + cx.tcx.lang_items().eq_trait().map(|def_id| EqImpl { + ty_eq_other: implements_trait(cx, ty, def_id, &[other.into()]), + other_eq_ty: implements_trait(cx, other, def_id, &[ty.into()]), + }) +} + +fn check_op(cx: &LateContext<'_>, expr: &Expr<'_>, other: &Expr<'_>, left: bool) { + let typeck = cx.typeck_results(); + let (arg, arg_span) = match expr.kind { + ExprKind::MethodCall(.., [arg], _) + if typeck + .type_dependent_def_id(expr.hir_id) + .and_then(|id| cx.tcx.trait_of_item(id)) + .map_or(false, |id| { + matches!(cx.tcx.get_diagnostic_name(id), Some(sym::ToString | sym::ToOwned)) + }) => + { + (arg, arg.span) + }, + ExprKind::Call(path, [arg]) + if path_def_id(cx, path) + .and_then(|id| match_any_def_paths(cx, id, &[&paths::FROM_STR_METHOD, &paths::FROM_FROM])) + .map_or(false, |idx| match idx { + 0 => true, + 1 => !is_copy(cx, typeck.expr_ty(expr)), + _ => false, + }) => + { + (arg, arg.span) + }, + _ => return, + }; + + let arg_ty = typeck.expr_ty(arg); + let other_ty = typeck.expr_ty(other); + + let without_deref = symmetric_partial_eq(cx, arg_ty, other_ty).unwrap_or_default(); + let with_deref = arg_ty + .builtin_deref(true) + .and_then(|tam| symmetric_partial_eq(cx, tam.ty, other_ty)) + .unwrap_or_default(); + + if !with_deref.is_implemented() && !without_deref.is_implemented() { + return; + } + + let other_gets_derefed = matches!(other.kind, ExprKind::Unary(UnOp::Deref, _)); + + let lint_span = if other_gets_derefed { + expr.span.to(other.span) + } else { + expr.span + }; + + span_lint_and_then( + cx, + CMP_OWNED, + lint_span, + "this creates an owned instance just for comparison", + |diag| { + // This also catches `PartialEq` implementations that call `to_owned`. + if other_gets_derefed { + diag.span_label(lint_span, "try implementing the comparison without allocating"); + return; + } + + let arg_snip = snippet(cx, arg_span, ".."); + let expr_snip; + let eq_impl; + if with_deref.is_implemented() { + expr_snip = format!("*{}", arg_snip); + eq_impl = with_deref; + } else { + expr_snip = arg_snip.to_string(); + eq_impl = without_deref; + }; + + let span; + let hint; + if (eq_impl.ty_eq_other && left) || (eq_impl.other_eq_ty && !left) { + span = expr.span; + hint = expr_snip; + } else { + span = expr.span.to(other.span); + + let cmp_span = if other.span < expr.span { + other.span.between(expr.span) + } else { + expr.span.between(other.span) + }; + if eq_impl.ty_eq_other { + hint = format!( + "{}{}{}", + expr_snip, + snippet(cx, cmp_span, ".."), + snippet(cx, other.span, "..") + ); + } else { + hint = format!( + "{}{}{}", + snippet(cx, other.span, ".."), + snippet(cx, cmp_span, ".."), + expr_snip + ); + } + } + + diag.span_suggestion( + span, + "try", + hint, + Applicability::MachineApplicable, // snippet + ); + }, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/operators/double_comparison.rs b/src/tools/clippy/clippy_lints/src/operators/double_comparison.rs new file mode 100644 index 000000000..56a86d0ff --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/double_comparison.rs @@ -0,0 +1,54 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::eq_expr_value; +use clippy_utils::source::snippet_with_applicability; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_span::source_map::Span; + +use super::DOUBLE_COMPARISONS; + +#[expect(clippy::similar_names)] +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, op: BinOpKind, lhs: &'tcx Expr<'_>, rhs: &'tcx Expr<'_>, span: Span) { + let (lkind, llhs, lrhs, rkind, rlhs, rrhs) = match (&lhs.kind, &rhs.kind) { + (ExprKind::Binary(lb, llhs, lrhs), ExprKind::Binary(rb, rlhs, rrhs)) => { + (lb.node, llhs, lrhs, rb.node, rlhs, rrhs) + }, + _ => return, + }; + if !(eq_expr_value(cx, llhs, rlhs) && eq_expr_value(cx, lrhs, rrhs)) { + return; + } + macro_rules! lint_double_comparison { + ($op:tt) => {{ + let mut applicability = Applicability::MachineApplicable; + let lhs_str = snippet_with_applicability(cx, llhs.span, "", &mut applicability); + let rhs_str = snippet_with_applicability(cx, lrhs.span, "", &mut applicability); + let sugg = format!("{} {} {}", lhs_str, stringify!($op), rhs_str); + span_lint_and_sugg( + cx, + DOUBLE_COMPARISONS, + span, + "this binary expression can be simplified", + "try", + sugg, + applicability, + ); + }}; + } + match (op, lkind, rkind) { + (BinOpKind::Or, BinOpKind::Eq, BinOpKind::Lt) | (BinOpKind::Or, BinOpKind::Lt, BinOpKind::Eq) => { + lint_double_comparison!(<=); + }, + (BinOpKind::Or, BinOpKind::Eq, BinOpKind::Gt) | (BinOpKind::Or, BinOpKind::Gt, BinOpKind::Eq) => { + lint_double_comparison!(>=); + }, + (BinOpKind::Or, BinOpKind::Lt, BinOpKind::Gt) | (BinOpKind::Or, BinOpKind::Gt, BinOpKind::Lt) => { + lint_double_comparison!(!=); + }, + (BinOpKind::And, BinOpKind::Le, BinOpKind::Ge) | (BinOpKind::And, BinOpKind::Ge, BinOpKind::Le) => { + lint_double_comparison!(==); + }, + _ => (), + }; +} diff --git a/src/tools/clippy/clippy_lints/src/operators/duration_subsec.rs b/src/tools/clippy/clippy_lints/src/operators/duration_subsec.rs new file mode 100644 index 000000000..0d067d1e1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/duration_subsec.rs @@ -0,0 +1,44 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::DURATION_SUBSEC; + +pub(crate) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, +) { + if op == BinOpKind::Div + && let ExprKind::MethodCall(method_path, [self_arg], _) = left.kind + && is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(self_arg).peel_refs(), sym::Duration) + && let Some((Constant::Int(divisor), _)) = constant(cx, cx.typeck_results(), right) + { + let suggested_fn = match (method_path.ident.as_str(), divisor) { + ("subsec_micros", 1_000) | ("subsec_nanos", 1_000_000) => "subsec_millis", + ("subsec_nanos", 1_000) => "subsec_micros", + _ => return, + }; + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + DURATION_SUBSEC, + expr.span, + &format!("calling `{}()` is more concise than this calculation", suggested_fn), + "try", + format!( + "{}.{}()", + snippet_with_applicability(cx, self_arg.span, "_", &mut applicability), + suggested_fn + ), + applicability, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/eq_op.rs b/src/tools/clippy/clippy_lints/src/operators/eq_op.rs new file mode 100644 index 000000000..44cf0bb06 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/eq_op.rs @@ -0,0 +1,45 @@ +use clippy_utils::diagnostics::span_lint; +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}; +use rustc_lint::LateContext; + +use super::EQ_OP; + +pub(crate) fn check_assert<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if let Some((macro_call, macro_name)) + = first_node_macro_backtrace(cx, e).find_map(|macro_call| { + let name = cx.tcx.item_name(macro_call.def_id); + matches!(name.as_str(), "assert_eq" | "assert_ne" | "debug_assert_eq" | "debug_assert_ne") + .then(|| (macro_call, name)) + }) + && let Some((lhs, rhs, _)) = find_assert_eq_args(cx, e, macro_call.expn) + && eq_expr_value(cx, lhs, rhs) + && macro_call.is_local() + && !is_in_test_function(cx.tcx, e.hir_id) + { + span_lint( + cx, + EQ_OP, + lhs.span.to(rhs.span), + &format!("identical args used in this `{}!` macro call", macro_name), + ); + } +} + +pub(crate) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + 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( + cx, + EQ_OP, + e.span, + &format!("equal expressions as operands to `{}`", op.as_str()), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/erasing_op.rs b/src/tools/clippy/clippy_lints/src/operators/erasing_op.rs new file mode 100644 index 000000000..066e08f3b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/erasing_op.rs @@ -0,0 +1,53 @@ +use clippy_utils::consts::{constant_simple, Constant}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::same_type_and_consts; + +use rustc_hir::{BinOpKind, Expr}; +use rustc_lint::LateContext; +use rustc_middle::ty::TypeckResults; + +use super::ERASING_OP; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, +) { + let tck = cx.typeck_results(); + match op { + BinOpKind::Mul | BinOpKind::BitAnd => { + check_op(cx, tck, left, right, e); + check_op(cx, tck, right, left, e); + }, + BinOpKind::Div => check_op(cx, tck, left, right, e), + _ => (), + } +} + +fn different_types(tck: &TypeckResults<'_>, input: &Expr<'_>, output: &Expr<'_>) -> bool { + let input_ty = tck.expr_ty(input).peel_refs(); + let output_ty = tck.expr_ty(output).peel_refs(); + !same_type_and_consts(input_ty, output_ty) +} + +fn check_op<'tcx>( + cx: &LateContext<'tcx>, + tck: &TypeckResults<'tcx>, + op: &Expr<'tcx>, + other: &Expr<'tcx>, + parent: &Expr<'tcx>, +) { + if constant_simple(cx, tck, op) == Some(Constant::Int(0)) { + if different_types(tck, other, parent) { + return; + } + span_lint( + cx, + ERASING_OP, + parent.span, + "this operation will always return zero. This is likely not the intended outcome", + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/float_cmp.rs b/src/tools/clippy/clippy_lints/src/operators/float_cmp.rs new file mode 100644 index 000000000..0ef793443 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/float_cmp.rs @@ -0,0 +1,139 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::get_item_name; +use clippy_utils::sugg::Sugg; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::{FLOAT_CMP, FLOAT_CMP_CONST}; + +pub(crate) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, +) { + if (op == BinOpKind::Eq || op == BinOpKind::Ne) && (is_float(cx, left) || is_float(cx, right)) { + if is_allowed(cx, left) || is_allowed(cx, right) { + return; + } + + // Allow comparing the results of signum() + if is_signum(cx, left) && is_signum(cx, right) { + return; + } + + if let Some(name) = get_item_name(cx, expr) { + let name = name.as_str(); + if name == "eq" || name == "ne" || name == "is_nan" || name.starts_with("eq_") || name.ends_with("_eq") { + return; + } + } + let is_comparing_arrays = is_array(cx, left) || is_array(cx, right); + let (lint, msg) = get_lint_and_message( + is_named_constant(cx, left) || is_named_constant(cx, right), + is_comparing_arrays, + ); + span_lint_and_then(cx, lint, expr.span, msg, |diag| { + let lhs = Sugg::hir(cx, left, ".."); + let rhs = Sugg::hir(cx, right, ".."); + + if !is_comparing_arrays { + diag.span_suggestion( + expr.span, + "consider comparing them within some margin of error", + format!( + "({}).abs() {} error_margin", + lhs - rhs, + if op == BinOpKind::Eq { '<' } else { '>' } + ), + Applicability::HasPlaceholders, // snippet + ); + } + diag.note("`f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`"); + }); + } +} + +fn get_lint_and_message( + is_comparing_constants: bool, + is_comparing_arrays: bool, +) -> (&'static rustc_lint::Lint, &'static str) { + if is_comparing_constants { + ( + FLOAT_CMP_CONST, + if is_comparing_arrays { + "strict comparison of `f32` or `f64` constant arrays" + } else { + "strict comparison of `f32` or `f64` constant" + }, + ) + } else { + ( + FLOAT_CMP, + if is_comparing_arrays { + "strict comparison of `f32` or `f64` arrays" + } else { + "strict comparison of `f32` or `f64`" + }, + ) + } +} + +fn is_named_constant<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool { + if let Some((_, res)) = constant(cx, cx.typeck_results(), expr) { + res + } else { + false + } +} + +fn is_allowed<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool { + match constant(cx, cx.typeck_results(), expr) { + Some((Constant::F32(f), _)) => f == 0.0 || f.is_infinite(), + Some((Constant::F64(f), _)) => f == 0.0 || f.is_infinite(), + Some((Constant::Vec(vec), _)) => vec.iter().all(|f| match f { + Constant::F32(f) => *f == 0.0 || (*f).is_infinite(), + Constant::F64(f) => *f == 0.0 || (*f).is_infinite(), + _ => false, + }), + _ => false, + } +} + +// Return true if `expr` is the result of `signum()` invoked on a float value. +fn is_signum(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + // The negation of a signum is still a signum + if let ExprKind::Unary(UnOp::Neg, child_expr) = expr.kind { + return is_signum(cx, child_expr); + } + + if_chain! { + if let ExprKind::MethodCall(method_name, [ref self_arg, ..], _) = expr.kind; + if sym!(signum) == method_name.ident.name; + // Check that the receiver of the signum() is a float (expressions[0] is the receiver of + // the method call) + then { + return is_float(cx, self_arg); + } + } + false +} + +fn is_float(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let value = &cx.typeck_results().expr_ty(expr).peel_refs().kind(); + + if let ty::Array(arr_ty, _) = value { + return matches!(arr_ty.kind(), ty::Float(_)); + }; + + matches!(value, ty::Float(_)) +} + +fn is_array(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + matches!(&cx.typeck_results().expr_ty(expr).peel_refs().kind(), ty::Array(_, _)) +} diff --git a/src/tools/clippy/clippy_lints/src/operators/float_equality_without_abs.rs b/src/tools/clippy/clippy_lints/src/operators/float_equality_without_abs.rs new file mode 100644 index 000000000..a0a8b6aab --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/float_equality_without_abs.rs @@ -0,0 +1,71 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{match_def_path, paths, sugg}; +use if_chain::if_chain; +use rustc_ast::util::parser::AssocOp; +use rustc_errors::Applicability; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::source_map::Spanned; + +use super::FLOAT_EQUALITY_WITHOUT_ABS; + +pub(crate) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + op: BinOpKind, + lhs: &'tcx Expr<'_>, + rhs: &'tcx Expr<'_>, +) { + let (lhs, rhs) = match op { + BinOpKind::Lt => (lhs, rhs), + BinOpKind::Gt => (rhs, lhs), + _ => return, + }; + + if_chain! { + // left hand side is a subtraction + if let ExprKind::Binary( + Spanned { + node: BinOpKind::Sub, + .. + }, + val_l, + val_r, + ) = lhs.kind; + + // right hand side matches either f32::EPSILON or f64::EPSILON + if let ExprKind::Path(ref epsilon_path) = rhs.kind; + if let Res::Def(DefKind::AssocConst, def_id) = cx.qpath_res(epsilon_path, rhs.hir_id); + if match_def_path(cx, def_id, &paths::F32_EPSILON) || match_def_path(cx, def_id, &paths::F64_EPSILON); + + // values of the subtractions on the left hand side are of the type float + let t_val_l = cx.typeck_results().expr_ty(val_l); + let t_val_r = cx.typeck_results().expr_ty(val_r); + if let ty::Float(_) = t_val_l.kind(); + if let ty::Float(_) = t_val_r.kind(); + + then { + let sug_l = sugg::Sugg::hir(cx, val_l, ".."); + let sug_r = sugg::Sugg::hir(cx, val_r, ".."); + // format the suggestion + let suggestion = format!("{}.abs()", sugg::make_assoc(AssocOp::Subtract, &sug_l, &sug_r).maybe_par()); + // spans the lint + span_lint_and_then( + cx, + FLOAT_EQUALITY_WITHOUT_ABS, + expr.span, + "float equality check without `.abs()`", + | diag | { + diag.span_suggestion( + lhs.span, + "add `.abs()`", + suggestion, + Applicability::MaybeIncorrect, + ); + } + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/identity_op.rs b/src/tools/clippy/clippy_lints/src/operators/identity_op.rs new file mode 100644 index 000000000..b48d6c4e2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/identity_op.rs @@ -0,0 +1,148 @@ +use clippy_utils::consts::{constant_full_int, constant_simple, Constant, FullInt}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{clip, unsext}; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, Node}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::source_map::Span; + +use super::IDENTITY_OP; + +pub(crate) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, +) { + if !is_allowed(cx, op, left, right) { + match op { + BinOpKind::Add | BinOpKind::BitOr | BinOpKind::BitXor => { + check_op(cx, left, 0, expr.span, right.span, needs_parenthesis(cx, expr, right)); + check_op(cx, right, 0, expr.span, left.span, Parens::Unneeded); + }, + BinOpKind::Shl | BinOpKind::Shr | BinOpKind::Sub => { + check_op(cx, right, 0, expr.span, left.span, Parens::Unneeded); + }, + BinOpKind::Mul => { + check_op(cx, left, 1, expr.span, right.span, needs_parenthesis(cx, expr, right)); + check_op(cx, right, 1, expr.span, left.span, Parens::Unneeded); + }, + BinOpKind::Div => check_op(cx, right, 1, expr.span, left.span, Parens::Unneeded), + BinOpKind::BitAnd => { + check_op(cx, left, -1, expr.span, right.span, needs_parenthesis(cx, expr, right)); + check_op(cx, right, -1, expr.span, left.span, Parens::Unneeded); + }, + BinOpKind::Rem => check_remainder(cx, left, right, expr.span, left.span), + _ => (), + } + } +} + +#[derive(Copy, Clone)] +enum Parens { + Needed, + Unneeded, +} + +/// Checks if `left op right` needs parenthesis when reduced to `right` +/// e.g. `0 + if b { 1 } else { 2 } + if b { 3 } else { 4 }` cannot be reduced +/// to `if b { 1 } else { 2 } + if b { 3 } else { 4 }` where the `if` could be +/// interpreted as a statement +/// +/// See #8724 +fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, right: &Expr<'_>) -> Parens { + match right.kind { + ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) => { + // ensure we're checking against the leftmost expression of `right` + // + // ~~~ `lhs` + // 0 + {4} * 2 + // ~~~~~~~ `right` + return needs_parenthesis(cx, binary, lhs); + }, + ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) => {}, + _ => return Parens::Unneeded, + } + + let mut prev_id = binary.hir_id; + for (_, node) in cx.tcx.hir().parent_iter(binary.hir_id) { + if let Node::Expr(expr) = node + && let ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) = expr.kind + && lhs.hir_id == prev_id + { + // keep going until we find a node that encompasses left of `binary` + prev_id = expr.hir_id; + continue; + } + + match node { + Node::Block(_) | Node::Stmt(_) => break, + _ => return Parens::Unneeded, + }; + } + + Parens::Needed +} + +fn is_allowed(cx: &LateContext<'_>, cmp: BinOpKind, left: &Expr<'_>, right: &Expr<'_>) -> bool { + // This lint applies to integers + !cx.typeck_results().expr_ty(left).peel_refs().is_integral() + || !cx.typeck_results().expr_ty(right).peel_refs().is_integral() + // `1 << 0` is a common pattern in bit manipulation code + || (cmp == BinOpKind::Shl + && constant_simple(cx, cx.typeck_results(), right) == Some(Constant::Int(0)) + && constant_simple(cx, cx.typeck_results(), left) == Some(Constant::Int(1))) +} + +fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span: Span, arg: Span) { + let lhs_const = constant_full_int(cx, cx.typeck_results(), left); + let rhs_const = constant_full_int(cx, cx.typeck_results(), right); + if match (lhs_const, rhs_const) { + (Some(FullInt::S(lv)), Some(FullInt::S(rv))) => lv.abs() < rv.abs(), + (Some(FullInt::U(lv)), Some(FullInt::U(rv))) => lv < rv, + _ => return, + } { + span_ineffective_operation(cx, span, arg, Parens::Unneeded); + } +} + +fn check_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, parens: Parens) { + if let Some(Constant::Int(v)) = constant_simple(cx, cx.typeck_results(), e).map(Constant::peel_refs) { + let check = match *cx.typeck_results().expr_ty(e).peel_refs().kind() { + ty::Int(ity) => unsext(cx.tcx, -1_i128, ity), + ty::Uint(uty) => clip(cx.tcx, !0, uty), + _ => return, + }; + if match m { + 0 => v == 0, + -1 => v == check, + 1 => v == 1, + _ => unreachable!(), + } { + span_ineffective_operation(cx, span, arg, parens); + } + } +} + +fn span_ineffective_operation(cx: &LateContext<'_>, span: Span, arg: Span, parens: Parens) { + let mut applicability = Applicability::MachineApplicable; + let expr_snippet = snippet_with_applicability(cx, arg, "..", &mut applicability); + + let suggestion = match parens { + Parens::Needed => format!("({expr_snippet})"), + Parens::Unneeded => expr_snippet.into_owned(), + }; + + span_lint_and_sugg( + cx, + IDENTITY_OP, + span, + "this operation has no effect", + "consider reducing it to", + suggestion, + applicability, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/operators/integer_division.rs b/src/tools/clippy/clippy_lints/src/operators/integer_division.rs new file mode 100644 index 000000000..631d10f4a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/integer_division.rs @@ -0,0 +1,27 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir as hir; +use rustc_lint::LateContext; + +use super::INTEGER_DIVISION; + +pub(crate) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + op: hir::BinOpKind, + left: &'tcx hir::Expr<'_>, + right: &'tcx hir::Expr<'_>, +) { + if op == hir::BinOpKind::Div + && cx.typeck_results().expr_ty(left).is_integral() + && cx.typeck_results().expr_ty(right).is_integral() + { + span_lint_and_help( + cx, + INTEGER_DIVISION, + expr.span, + "integer division", + None, + "division of integers may cause loss of precision. consider using floats", + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/misrefactored_assign_op.rs b/src/tools/clippy/clippy_lints/src/operators/misrefactored_assign_op.rs new file mode 100644 index 000000000..0024384d9 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/misrefactored_assign_op.rs @@ -0,0 +1,84 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::eq_expr_value; +use clippy_utils::source::snippet_opt; +use clippy_utils::sugg; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; + +use super::MISREFACTORED_ASSIGN_OP; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + op: hir::BinOpKind, + lhs: &'tcx hir::Expr<'_>, + rhs: &'tcx hir::Expr<'_>, +) { + if let hir::ExprKind::Binary(binop, l, r) = &rhs.kind { + if op != binop.node { + return; + } + // lhs op= l op r + if eq_expr_value(cx, lhs, l) { + lint_misrefactored_assign_op(cx, expr, op, rhs, lhs, r); + } + // lhs op= l commutative_op r + if is_commutative(op) && eq_expr_value(cx, lhs, r) { + lint_misrefactored_assign_op(cx, expr, op, rhs, lhs, l); + } + } +} + +fn lint_misrefactored_assign_op( + cx: &LateContext<'_>, + expr: &hir::Expr<'_>, + op: hir::BinOpKind, + rhs: &hir::Expr<'_>, + assignee: &hir::Expr<'_>, + rhs_other: &hir::Expr<'_>, +) { + span_lint_and_then( + cx, + MISREFACTORED_ASSIGN_OP, + expr.span, + "variable appears on both sides of an assignment operation", + |diag| { + if let (Some(snip_a), Some(snip_r)) = (snippet_opt(cx, assignee.span), snippet_opt(cx, rhs_other.span)) { + let a = &sugg::Sugg::hir(cx, assignee, ".."); + let r = &sugg::Sugg::hir(cx, rhs, ".."); + let long = format!("{} = {}", snip_a, sugg::make_binop(op.into(), a, r)); + diag.span_suggestion( + expr.span, + &format!( + "did you mean `{} = {} {} {}` or `{}`? Consider replacing it with", + snip_a, + snip_a, + op.as_str(), + snip_r, + long + ), + format!("{} {}= {}", snip_a, op.as_str(), snip_r), + Applicability::MaybeIncorrect, + ); + diag.span_suggestion( + expr.span, + "or", + long, + Applicability::MaybeIncorrect, // snippet + ); + } + }, + ); +} + +#[must_use] +fn is_commutative(op: hir::BinOpKind) -> bool { + use rustc_hir::BinOpKind::{ + Add, And, BitAnd, BitOr, BitXor, Div, Eq, Ge, Gt, Le, Lt, Mul, Ne, Or, Rem, Shl, Shr, Sub, + }; + match op { + Add | Mul | And | Or | BitXor | BitAnd | BitOr | Eq | Ne => true, + Sub | Div | Rem | Shl | Shr | Lt | Le | Ge | Gt => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/mod.rs b/src/tools/clippy/clippy_lints/src/operators/mod.rs new file mode 100644 index 000000000..bb6d99406 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/mod.rs @@ -0,0 +1,888 @@ +mod absurd_extreme_comparisons; +mod assign_op_pattern; +mod bit_mask; +mod cmp_nan; +mod cmp_owned; +mod double_comparison; +mod duration_subsec; +mod eq_op; +mod erasing_op; +mod float_cmp; +mod float_equality_without_abs; +mod identity_op; +mod integer_division; +mod misrefactored_assign_op; +mod modulo_arithmetic; +mod modulo_one; +mod needless_bitwise_bool; +mod numeric_arithmetic; +mod op_ref; +mod ptr_eq; +mod self_assignment; +mod verbose_bit_mask; + +pub(crate) mod arithmetic; + +use rustc_hir::{Body, Expr, ExprKind, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons where one side of the relation is + /// either the minimum or maximum value for its type and warns if it involves a + /// case that is always true or always false. Only integer and boolean types are + /// checked. + /// + /// ### Why is this bad? + /// An expression like `min <= x` may misleadingly imply + /// that it is possible for `x` to be less than the minimum. Expressions like + /// `max < x` are probably mistakes. + /// + /// ### Known problems + /// For `usize` the size of the current compile target will + /// be assumed (e.g., 64 bits on 64 bit systems). This means code that uses such + /// a comparison to detect target pointer width will trigger this lint. One can + /// use `mem::sizeof` and compare its value or conditional compilation + /// attributes + /// like `#[cfg(target_pointer_width = "64")] ..` instead. + /// + /// ### Example + /// ```rust + /// let vec: Vec<isize> = Vec::new(); + /// if vec.len() <= 0 {} + /// if 100 > i32::MAX {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ABSURD_EXTREME_COMPARISONS, + correctness, + "a comparison with a maximum or minimum value that is always true or false" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for any kind of arithmetic operation of any type. + /// + /// Operators like `+`, `-`, `*` or `<<` are usually capable of overflowing according to the [Rust + /// Reference](https://doc.rust-lang.org/reference/expressions/operator-expr.html#overflow), + /// or can panic (`/`, `%`). Known safe built-in types like `Wrapping` or `Saturing` are filtered + /// away. + /// + /// ### Why is this bad? + /// Integer overflow will trigger a panic in debug builds or will wrap in + /// release mode. Division by zero will cause a panic in either mode. In some applications one + /// wants explicitly checked, wrapping or saturating arithmetic. + /// + /// #### Example + /// ```rust + /// # let a = 0; + /// a + 1; + /// ``` + /// + /// Third-party types also tend to overflow. + /// + /// #### Example + /// ```ignore,rust + /// use rust_decimal::Decimal; + /// let _n = Decimal::MAX + Decimal::MAX; + /// ``` + /// + /// ### Allowed types + /// Custom allowed types can be specified through the "arithmetic-allowed" filter. + #[clippy::version = "1.64.0"] + pub ARITHMETIC, + restriction, + "any arithmetic expression that could overflow or panic" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for integer arithmetic operations which could overflow or panic. + /// + /// Specifically, checks for any operators (`+`, `-`, `*`, `<<`, etc) which are capable + /// of overflowing according to the [Rust + /// Reference](https://doc.rust-lang.org/reference/expressions/operator-expr.html#overflow), + /// or which can panic (`/`, `%`). No bounds analysis or sophisticated reasoning is + /// attempted. + /// + /// ### Why is this bad? + /// Integer overflow will trigger a panic in debug builds or will wrap in + /// release mode. Division by zero will cause a panic in either mode. In some applications one + /// wants explicitly checked, wrapping or saturating arithmetic. + /// + /// ### Example + /// ```rust + /// # let a = 0; + /// a + 1; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INTEGER_ARITHMETIC, + restriction, + "any integer arithmetic expression which could overflow or panic" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for float arithmetic. + /// + /// ### Why is this bad? + /// For some embedded systems or kernel development, it + /// can be useful to rule out floating-point numbers. + /// + /// ### Example + /// ```rust + /// # let a = 0.0; + /// a + 1.0; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FLOAT_ARITHMETIC, + restriction, + "any floating-point arithmetic statement" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `a = a op b` or `a = b commutative_op a` + /// patterns. + /// + /// ### Why is this bad? + /// These can be written as the shorter `a op= b`. + /// + /// ### Known problems + /// While forbidden by the spec, `OpAssign` traits may have + /// implementations that differ from the regular `Op` impl. + /// + /// ### Example + /// ```rust + /// let mut a = 5; + /// let b = 0; + /// // ... + /// + /// a = a + b; + /// ``` + /// + /// Use instead: + /// ```rust + /// let mut a = 5; + /// let b = 0; + /// // ... + /// + /// a += b; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ASSIGN_OP_PATTERN, + style, + "assigning the result of an operation on a variable to that same variable" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `a op= a op b` or `a op= b op a` patterns. + /// + /// ### Why is this bad? + /// Most likely these are bugs where one meant to write `a + /// op= b`. + /// + /// ### Known problems + /// Clippy cannot know for sure if `a op= a op b` should have + /// been `a = a op a op b` or `a = a op b`/`a op= b`. Therefore, it suggests both. + /// If `a op= a op b` is really the correct behavior it should be + /// written as `a = a op a op b` as it's less confusing. + /// + /// ### Example + /// ```rust + /// let mut a = 5; + /// let b = 2; + /// // ... + /// a += a + b; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MISREFACTORED_ASSIGN_OP, + suspicious, + "having a variable on both sides of an assign op" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for incompatible bit masks in comparisons. + /// + /// The formula for detecting if an expression of the type `_ <bit_op> m + /// <cmp_op> c` (where `<bit_op>` is one of {`&`, `|`} and `<cmp_op>` is one of + /// {`!=`, `>=`, `>`, `!=`, `>=`, `>`}) can be determined from the following + /// table: + /// + /// |Comparison |Bit Op|Example |is always|Formula | + /// |------------|------|-------------|---------|----------------------| + /// |`==` or `!=`| `&` |`x & 2 == 3` |`false` |`c & m != c` | + /// |`<` or `>=`| `&` |`x & 2 < 3` |`true` |`m < c` | + /// |`>` or `<=`| `&` |`x & 1 > 1` |`false` |`m <= c` | + /// |`==` or `!=`| `\|` |`x \| 1 == 0`|`false` |`c \| m != c` | + /// |`<` or `>=`| `\|` |`x \| 1 < 1` |`false` |`m >= c` | + /// |`<=` or `>` | `\|` |`x \| 1 > 0` |`true` |`m > c` | + /// + /// ### Why is this bad? + /// If the bits that the comparison cares about are always + /// set to zero or one by the bit mask, the comparison is constant `true` or + /// `false` (depending on mask, compared value, and operators). + /// + /// So the code is actively misleading, and the only reason someone would write + /// this intentionally is to win an underhanded Rust contest or create a + /// test-case for this lint. + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// if (x & 1 == 2) { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub BAD_BIT_MASK, + correctness, + "expressions of the form `_ & mask == select` that will only ever return `true` or `false`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for bit masks in comparisons which can be removed + /// without changing the outcome. The basic structure can be seen in the + /// following table: + /// + /// |Comparison| Bit Op |Example |equals | + /// |----------|----------|------------|-------| + /// |`>` / `<=`|`\|` / `^`|`x \| 2 > 3`|`x > 3`| + /// |`<` / `>=`|`\|` / `^`|`x ^ 1 < 4` |`x < 4`| + /// + /// ### Why is this bad? + /// Not equally evil as [`bad_bit_mask`](#bad_bit_mask), + /// but still a bit misleading, because the bit mask is ineffective. + /// + /// ### Known problems + /// False negatives: This lint will only match instances + /// where we have figured out the math (which is for a power-of-two compared + /// value). This means things like `x | 1 >= 7` (which would be better written + /// as `x >= 6`) will not be reported (but bit masks like this are fairly + /// uncommon). + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// if (x | 1 > 3) { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INEFFECTIVE_BIT_MASK, + correctness, + "expressions where a bit mask will be rendered useless by a comparison, e.g., `(x | 1) > 2`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for bit masks that can be replaced by a call + /// to `trailing_zeros` + /// + /// ### Why is this bad? + /// `x.trailing_zeros() > 4` is much clearer than `x & 15 + /// == 0` + /// + /// ### Known problems + /// llvm generates better code for `x & 15 == 0` on x86 + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// if x & 0b1111 == 0 { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub VERBOSE_BIT_MASK, + pedantic, + "expressions where a bit mask is less readable than the corresponding method call" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for double comparisons that could be simplified to a single expression. + /// + /// + /// ### Why is this bad? + /// Readability. + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// # let y = 2; + /// if x == y || x < y {} + /// ``` + /// + /// Use instead: + /// + /// ```rust + /// # let x = 1; + /// # let y = 2; + /// if x <= y {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DOUBLE_COMPARISONS, + complexity, + "unnecessary double comparisons that can be simplified" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calculation of subsecond microseconds or milliseconds + /// from other `Duration` methods. + /// + /// ### Why is this bad? + /// It's more concise to call `Duration::subsec_micros()` or + /// `Duration::subsec_millis()` than to calculate them. + /// + /// ### Example + /// ```rust + /// # use std::time::Duration; + /// # let duration = Duration::new(5, 0); + /// let micros = duration.subsec_nanos() / 1_000; + /// let millis = duration.subsec_nanos() / 1_000_000; + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::time::Duration; + /// # let duration = Duration::new(5, 0); + /// let micros = duration.subsec_micros(); + /// let millis = duration.subsec_millis(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DURATION_SUBSEC, + complexity, + "checks for calculation of subsecond microseconds or milliseconds" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for equal operands to comparison, logical and + /// bitwise, difference and division binary operators (`==`, `>`, etc., `&&`, + /// `||`, `&`, `|`, `^`, `-` and `/`). + /// + /// ### Why is this bad? + /// This is usually just a typo or a copy and paste error. + /// + /// ### Known problems + /// False negatives: We had some false positives regarding + /// calls (notably [racer](https://github.com/phildawes/racer) had one instance + /// of `x.pop() && x.pop()`), so we removed matching any function or method + /// calls. We may introduce a list of known pure functions in the future. + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// if x + 1 == x + 1 {} + /// + /// // or + /// + /// # let a = 3; + /// # let b = 4; + /// assert_eq!(a, a); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub EQ_OP, + correctness, + "equal operands on both sides of a comparison or bitwise combination (e.g., `x == x`)" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for arguments to `==` which have their address + /// taken to satisfy a bound + /// and suggests to dereference the other argument instead + /// + /// ### Why is this bad? + /// It is more idiomatic to dereference the other argument. + /// + /// ### Example + /// ```rust,ignore + /// &x == y + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// x == *y + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OP_REF, + style, + "taking a reference to satisfy the type constraints on `==`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for erasing operations, e.g., `x * 0`. + /// + /// ### Why is this bad? + /// The whole expression can be replaced by zero. + /// This is most likely not the intended outcome and should probably be + /// corrected + /// + /// ### Example + /// ```rust + /// let x = 1; + /// 0 / x; + /// 0 * x; + /// x & 0; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ERASING_OP, + correctness, + "using erasing operations, e.g., `x * 0` or `y & 0`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for statements of the form `(a - b) < f32::EPSILON` or + /// `(a - b) < f64::EPSILON`. Notes the missing `.abs()`. + /// + /// ### Why is this bad? + /// The code without `.abs()` is more likely to have a bug. + /// + /// ### Known problems + /// If the user can ensure that b is larger than a, the `.abs()` is + /// technically unnecessary. However, it will make the code more robust and doesn't have any + /// large performance implications. If the abs call was deliberately left out for performance + /// reasons, it is probably better to state this explicitly in the code, which then can be done + /// with an allow. + /// + /// ### Example + /// ```rust + /// pub fn is_roughly_equal(a: f32, b: f32) -> bool { + /// (a - b) < f32::EPSILON + /// } + /// ``` + /// Use instead: + /// ```rust + /// pub fn is_roughly_equal(a: f32, b: f32) -> bool { + /// (a - b).abs() < f32::EPSILON + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub FLOAT_EQUALITY_WITHOUT_ABS, + suspicious, + "float equality check without `.abs()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for identity operations, e.g., `x + 0`. + /// + /// ### Why is this bad? + /// This code can be removed without changing the + /// meaning. So it just obscures what's going on. Delete it mercilessly. + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// x / 1 + 0 * 1 - 0 | 0; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub IDENTITY_OP, + complexity, + "using identity operations, e.g., `x + 0` or `y / 1`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for division of integers + /// + /// ### Why is this bad? + /// When outside of some very specific algorithms, + /// integer division is very often a mistake because it discards the + /// remainder. + /// + /// ### Example + /// ```rust + /// let x = 3 / 2; + /// println!("{}", x); + /// ``` + /// + /// Use instead: + /// ```rust + /// let x = 3f32 / 2f32; + /// println!("{}", x); + /// ``` + #[clippy::version = "1.37.0"] + pub INTEGER_DIVISION, + restriction, + "integer division may cause loss of precision" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons to NaN. + /// + /// ### Why is this bad? + /// NaN does not compare meaningfully to anything – not + /// even itself – so those comparisons are simply wrong. + /// + /// ### Example + /// ```rust + /// # let x = 1.0; + /// if x == f32::NAN { } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = 1.0f32; + /// if x.is_nan() { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CMP_NAN, + correctness, + "comparisons to `NAN`, which will always return false, probably not intended" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for conversions to owned values just for the sake + /// of a comparison. + /// + /// ### Why is this bad? + /// The comparison can operate on a reference, so creating + /// an owned value effectively throws it away directly afterwards, which is + /// needlessly consuming code and heap space. + /// + /// ### Example + /// ```rust + /// # let x = "foo"; + /// # let y = String::from("foo"); + /// if x.to_owned() == y {} + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = "foo"; + /// # let y = String::from("foo"); + /// if x == y {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CMP_OWNED, + perf, + "creating owned instances for comparing with others, e.g., `x == \"foo\".to_string()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for (in-)equality comparisons on floating-point + /// values (apart from zero), except in functions called `*eq*` (which probably + /// implement equality for a type involving floats). + /// + /// ### Why is this bad? + /// Floating point calculations are usually imprecise, so + /// asking if two values are *exactly* equal is asking for trouble. For a good + /// guide on what to do, see [the floating point + /// guide](http://www.floating-point-gui.de/errors/comparison). + /// + /// ### Example + /// ```rust + /// let x = 1.2331f64; + /// let y = 1.2332f64; + /// + /// if y == 1.23f64 { } + /// if y != x {} // where both are floats + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = 1.2331f64; + /// # let y = 1.2332f64; + /// let error_margin = f64::EPSILON; // Use an epsilon for comparison + /// // Or, if Rust <= 1.42, use `std::f64::EPSILON` constant instead. + /// // let error_margin = std::f64::EPSILON; + /// if (y - 1.23f64).abs() < error_margin { } + /// if (y - x).abs() > error_margin { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FLOAT_CMP, + pedantic, + "using `==` or `!=` on float values instead of comparing difference with an epsilon" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for (in-)equality comparisons on floating-point + /// value and constant, except in functions called `*eq*` (which probably + /// implement equality for a type involving floats). + /// + /// ### Why is this bad? + /// Floating point calculations are usually imprecise, so + /// asking if two values are *exactly* equal is asking for trouble. For a good + /// guide on what to do, see [the floating point + /// guide](http://www.floating-point-gui.de/errors/comparison). + /// + /// ### Example + /// ```rust + /// let x: f64 = 1.0; + /// const ONE: f64 = 1.00; + /// + /// if x == ONE { } // where both are floats + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x: f64 = 1.0; + /// # const ONE: f64 = 1.00; + /// let error_margin = f64::EPSILON; // Use an epsilon for comparison + /// // Or, if Rust <= 1.42, use `std::f64::EPSILON` constant instead. + /// // let error_margin = std::f64::EPSILON; + /// if (x - ONE).abs() < error_margin { } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub FLOAT_CMP_CONST, + restriction, + "using `==` or `!=` on float constants instead of comparing difference with an epsilon" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for getting the remainder of a division by one or minus + /// one. + /// + /// ### Why is this bad? + /// The result for a divisor of one can only ever be zero; for + /// minus one it can cause panic/overflow (if the left operand is the minimal value of + /// the respective integer type) or results in zero. No one will write such code + /// deliberately, unless trying to win an Underhanded Rust Contest. Even for that + /// contest, it's probably a bad idea. Use something more underhanded. + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// let a = x % 1; + /// let a = x % -1; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MODULO_ONE, + correctness, + "taking a number modulo +/-1, which can either panic/overflow or always returns 0" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for modulo arithmetic. + /// + /// ### Why is this bad? + /// The results of modulo (%) operation might differ + /// depending on the language, when negative numbers are involved. + /// If you interop with different languages it might be beneficial + /// to double check all places that use modulo arithmetic. + /// + /// For example, in Rust `17 % -3 = 2`, but in Python `17 % -3 = -1`. + /// + /// ### Example + /// ```rust + /// let x = -17 % 3; + /// ``` + #[clippy::version = "1.42.0"] + pub MODULO_ARITHMETIC, + restriction, + "any modulo arithmetic statement" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for uses of bitwise and/or operators between booleans, where performance may be improved by using + /// a lazy and. + /// + /// ### Why is this bad? + /// The bitwise operators do not support short-circuiting, so it may hinder code performance. + /// Additionally, boolean logic "masked" as bitwise logic is not caught by lints like `unnecessary_fold` + /// + /// ### Known problems + /// This lint evaluates only when the right side is determined to have no side effects. At this time, that + /// determination is quite conservative. + /// + /// ### Example + /// ```rust + /// let (x,y) = (true, false); + /// if x & !y {} // where both x and y are booleans + /// ``` + /// Use instead: + /// ```rust + /// let (x,y) = (true, false); + /// if x && !y {} + /// ``` + #[clippy::version = "1.54.0"] + pub NEEDLESS_BITWISE_BOOL, + pedantic, + "Boolean expressions that use bitwise rather than lazy operators" +} + +declare_clippy_lint! { + /// ### What it does + /// Use `std::ptr::eq` when applicable + /// + /// ### Why is this bad? + /// `ptr::eq` can be used to compare `&T` references + /// (which coerce to `*const T` implicitly) by their address rather than + /// comparing the values they point to. + /// + /// ### Example + /// ```rust + /// let a = &[1, 2, 3]; + /// let b = &[1, 2, 3]; + /// + /// assert!(a as *const _ as usize == b as *const _ as usize); + /// ``` + /// Use instead: + /// ```rust + /// let a = &[1, 2, 3]; + /// let b = &[1, 2, 3]; + /// + /// assert!(std::ptr::eq(a, b)); + /// ``` + #[clippy::version = "1.49.0"] + pub PTR_EQ, + style, + "use `std::ptr::eq` when comparing raw pointers" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for explicit self-assignments. + /// + /// ### Why is this bad? + /// Self-assignments are redundant and unlikely to be + /// intentional. + /// + /// ### Known problems + /// If expression contains any deref coercions or + /// indexing operations they are assumed not to have any side effects. + /// + /// ### Example + /// ```rust + /// struct Event { + /// x: i32, + /// } + /// + /// fn copy_position(a: &mut Event, b: &Event) { + /// a.x = a.x; + /// } + /// ``` + /// + /// Should be: + /// ```rust + /// struct Event { + /// x: i32, + /// } + /// + /// fn copy_position(a: &mut Event, b: &Event) { + /// a.x = b.x; + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub SELF_ASSIGNMENT, + correctness, + "explicit self-assignment" +} + +pub struct Operators { + arithmetic_context: numeric_arithmetic::Context, + verbose_bit_mask_threshold: u64, +} +impl_lint_pass!(Operators => [ + ABSURD_EXTREME_COMPARISONS, + ARITHMETIC, + INTEGER_ARITHMETIC, + FLOAT_ARITHMETIC, + ASSIGN_OP_PATTERN, + MISREFACTORED_ASSIGN_OP, + BAD_BIT_MASK, + INEFFECTIVE_BIT_MASK, + VERBOSE_BIT_MASK, + DOUBLE_COMPARISONS, + DURATION_SUBSEC, + EQ_OP, + OP_REF, + ERASING_OP, + FLOAT_EQUALITY_WITHOUT_ABS, + IDENTITY_OP, + INTEGER_DIVISION, + CMP_NAN, + CMP_OWNED, + FLOAT_CMP, + FLOAT_CMP_CONST, + MODULO_ONE, + MODULO_ARITHMETIC, + NEEDLESS_BITWISE_BOOL, + PTR_EQ, + SELF_ASSIGNMENT, +]); +impl Operators { + pub fn new(verbose_bit_mask_threshold: u64) -> Self { + Self { + arithmetic_context: numeric_arithmetic::Context::default(), + verbose_bit_mask_threshold, + } + } +} +impl<'tcx> LateLintPass<'tcx> for Operators { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + eq_op::check_assert(cx, e); + match e.kind { + ExprKind::Binary(op, lhs, rhs) => { + if !e.span.from_expansion() { + absurd_extreme_comparisons::check(cx, e, op.node, lhs, rhs); + if !(macro_with_not_op(lhs) || macro_with_not_op(rhs)) { + eq_op::check(cx, e, op.node, lhs, rhs); + op_ref::check(cx, e, op.node, lhs, rhs); + } + erasing_op::check(cx, e, op.node, lhs, rhs); + identity_op::check(cx, e, op.node, lhs, rhs); + needless_bitwise_bool::check(cx, e, op.node, lhs, rhs); + ptr_eq::check(cx, e, op.node, lhs, rhs); + } + self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs); + bit_mask::check(cx, e, op.node, lhs, rhs); + verbose_bit_mask::check(cx, e, op.node, lhs, rhs, self.verbose_bit_mask_threshold); + double_comparison::check(cx, op.node, lhs, rhs, e.span); + duration_subsec::check(cx, e, op.node, lhs, rhs); + float_equality_without_abs::check(cx, e, op.node, lhs, rhs); + integer_division::check(cx, e, op.node, lhs, rhs); + cmp_nan::check(cx, e, op.node, lhs, rhs); + cmp_owned::check(cx, op.node, lhs, rhs); + float_cmp::check(cx, e, op.node, lhs, rhs); + modulo_one::check(cx, e, op.node, rhs); + modulo_arithmetic::check(cx, e, op.node, lhs, rhs); + }, + ExprKind::AssignOp(op, lhs, rhs) => { + self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs); + misrefactored_assign_op::check(cx, e, op.node, lhs, rhs); + modulo_arithmetic::check(cx, e, op.node, lhs, rhs); + }, + ExprKind::Assign(lhs, rhs, _) => { + assign_op_pattern::check(cx, e, lhs, rhs); + self_assignment::check(cx, e, lhs, rhs); + }, + ExprKind::Unary(op, arg) => { + if op == UnOp::Neg { + self.arithmetic_context.check_negate(cx, e, arg); + } + }, + _ => (), + } + } + + fn check_expr_post(&mut self, _: &LateContext<'_>, e: &Expr<'_>) { + self.arithmetic_context.expr_post(e.hir_id); + } + + fn check_body(&mut self, cx: &LateContext<'tcx>, b: &'tcx Body<'_>) { + self.arithmetic_context.enter_body(cx, b); + } + + fn check_body_post(&mut self, cx: &LateContext<'tcx>, b: &'tcx Body<'_>) { + self.arithmetic_context.body_post(cx, b); + } +} + +fn macro_with_not_op(e: &Expr<'_>) -> bool { + if let ExprKind::Unary(_, e) = e.kind { + e.span.from_expansion() + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/modulo_arithmetic.rs b/src/tools/clippy/clippy_lints/src/operators/modulo_arithmetic.rs new file mode 100644 index 000000000..af4e74947 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/modulo_arithmetic.rs @@ -0,0 +1,126 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sext; +use if_chain::if_chain; +use rustc_hir::{BinOpKind, Expr}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use std::fmt::Display; + +use super::MODULO_ARITHMETIC; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + op: BinOpKind, + lhs: &'tcx Expr<'_>, + rhs: &'tcx Expr<'_>, +) { + if op == BinOpKind::Rem { + let lhs_operand = analyze_operand(lhs, cx, e); + let rhs_operand = analyze_operand(rhs, cx, e); + if_chain! { + if let Some(lhs_operand) = lhs_operand; + if let Some(rhs_operand) = rhs_operand; + then { + check_const_operands(cx, e, &lhs_operand, &rhs_operand); + } + else { + check_non_const_operands(cx, e, lhs); + } + } + }; +} + +struct OperandInfo { + string_representation: Option<String>, + is_negative: bool, + is_integral: bool, +} + +fn analyze_operand(operand: &Expr<'_>, cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<OperandInfo> { + match constant(cx, cx.typeck_results(), operand) { + Some((Constant::Int(v), _)) => match *cx.typeck_results().expr_ty(expr).kind() { + ty::Int(ity) => { + let value = sext(cx.tcx, v, ity); + return Some(OperandInfo { + string_representation: Some(value.to_string()), + is_negative: value < 0, + is_integral: true, + }); + }, + ty::Uint(_) => { + return Some(OperandInfo { + string_representation: None, + is_negative: false, + is_integral: true, + }); + }, + _ => {}, + }, + Some((Constant::F32(f), _)) => { + return Some(floating_point_operand_info(&f)); + }, + Some((Constant::F64(f), _)) => { + return Some(floating_point_operand_info(&f)); + }, + _ => {}, + } + None +} + +fn floating_point_operand_info<T: Display + PartialOrd + From<f32>>(f: &T) -> OperandInfo { + OperandInfo { + string_representation: Some(format!("{:.3}", *f)), + is_negative: *f < 0.0.into(), + is_integral: false, + } +} + +fn might_have_negative_value(t: Ty<'_>) -> bool { + t.is_signed() || t.is_floating_point() +} + +fn check_const_operands<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + lhs_operand: &OperandInfo, + rhs_operand: &OperandInfo, +) { + if lhs_operand.is_negative ^ rhs_operand.is_negative { + span_lint_and_then( + cx, + MODULO_ARITHMETIC, + expr.span, + &format!( + "you are using modulo operator on constants with different signs: `{} % {}`", + lhs_operand.string_representation.as_ref().unwrap(), + rhs_operand.string_representation.as_ref().unwrap() + ), + |diag| { + diag.note("double check for expected result especially when interoperating with different languages"); + if lhs_operand.is_integral { + diag.note("or consider using `rem_euclid` or similar function"); + } + }, + ); + } +} + +fn check_non_const_operands<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, operand: &Expr<'_>) { + let operand_type = cx.typeck_results().expr_ty(operand); + if might_have_negative_value(operand_type) { + span_lint_and_then( + cx, + MODULO_ARITHMETIC, + expr.span, + "you are using modulo operator on types that might have different signs", + |diag| { + diag.note("double check for expected result especially when interoperating with different languages"); + if operand_type.is_integral() { + diag.note("or consider using `rem_euclid` or similar function"); + } + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/modulo_one.rs b/src/tools/clippy/clippy_lints/src/operators/modulo_one.rs new file mode 100644 index 000000000..54eea1483 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/modulo_one.rs @@ -0,0 +1,26 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{is_integer_const, unsext}; +use rustc_hir::{BinOpKind, Expr}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::MODULO_ONE; + +pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, op: BinOpKind, right: &Expr<'_>) { + if op == BinOpKind::Rem { + if is_integer_const(cx, right, 1) { + span_lint(cx, MODULO_ONE, expr.span, "any number modulo 1 will be 0"); + } + + if let ty::Int(ity) = cx.typeck_results().expr_ty(right).kind() { + if is_integer_const(cx, right, unsext(cx.tcx, -1, *ity)) { + span_lint( + cx, + MODULO_ONE, + expr.span, + "any number modulo -1 will panic/overflow or result in 0", + ); + } + }; + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/needless_bitwise_bool.rs b/src/tools/clippy/clippy_lints/src/operators/needless_bitwise_bool.rs new file mode 100644 index 000000000..e902235a0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/needless_bitwise_bool.rs @@ -0,0 +1,36 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_opt; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; + +use super::NEEDLESS_BITWISE_BOOL; + +pub(super) fn check(cx: &LateContext<'_>, e: &Expr<'_>, op: BinOpKind, lhs: &Expr<'_>, rhs: &Expr<'_>) { + let op_str = match op { + BinOpKind::BitAnd => "&&", + BinOpKind::BitOr => "||", + _ => return, + }; + if matches!( + rhs.kind, + ExprKind::Call(..) | ExprKind::MethodCall(..) | ExprKind::Binary(..) | ExprKind::Unary(..) + ) && cx.typeck_results().expr_ty(e).is_bool() + && !rhs.can_have_side_effects() + { + span_lint_and_then( + cx, + NEEDLESS_BITWISE_BOOL, + e.span, + "use of bitwise operator instead of lazy operator between booleans", + |diag| { + if let Some(lhs_snip) = snippet_opt(cx, lhs.span) + && let Some(rhs_snip) = snippet_opt(cx, rhs.span) + { + let sugg = format!("{} {} {}", lhs_snip, op_str, rhs_snip); + diag.span_suggestion(e.span, "try", sugg, Applicability::MachineApplicable); + } + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/numeric_arithmetic.rs b/src/tools/clippy/clippy_lints/src/operators/numeric_arithmetic.rs new file mode 100644 index 000000000..b6097710d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/numeric_arithmetic.rs @@ -0,0 +1,128 @@ +use clippy_utils::consts::constant_simple; +use clippy_utils::diagnostics::span_lint; +use rustc_hir as hir; +use rustc_lint::LateContext; +use rustc_span::source_map::Span; + +use super::{FLOAT_ARITHMETIC, INTEGER_ARITHMETIC}; + +#[derive(Default)] +pub struct Context { + expr_id: Option<hir::HirId>, + /// This field is used to check whether expressions are constants, such as in enum discriminants + /// and consts + const_span: Option<Span>, +} +impl Context { + fn skip_expr(&mut self, e: &hir::Expr<'_>) -> bool { + self.expr_id.is_some() || self.const_span.map_or(false, |span| span.contains(e.span)) + } + + pub fn check_binary<'tcx>( + &mut self, + cx: &LateContext<'tcx>, + expr: &'tcx hir::Expr<'_>, + op: hir::BinOpKind, + l: &'tcx hir::Expr<'_>, + r: &'tcx hir::Expr<'_>, + ) { + if self.skip_expr(expr) { + return; + } + match op { + hir::BinOpKind::And + | hir::BinOpKind::Or + | hir::BinOpKind::BitAnd + | hir::BinOpKind::BitOr + | hir::BinOpKind::BitXor + | hir::BinOpKind::Eq + | hir::BinOpKind::Lt + | hir::BinOpKind::Le + | hir::BinOpKind::Ne + | hir::BinOpKind::Ge + | hir::BinOpKind::Gt => return, + _ => (), + } + + let (l_ty, r_ty) = (cx.typeck_results().expr_ty(l), cx.typeck_results().expr_ty(r)); + if l_ty.peel_refs().is_integral() && r_ty.peel_refs().is_integral() { + match op { + hir::BinOpKind::Div | hir::BinOpKind::Rem => match &r.kind { + hir::ExprKind::Lit(_lit) => (), + hir::ExprKind::Unary(hir::UnOp::Neg, expr) => { + if let hir::ExprKind::Lit(lit) = &expr.kind { + if let rustc_ast::ast::LitKind::Int(1, _) = lit.node { + span_lint(cx, INTEGER_ARITHMETIC, expr.span, "integer arithmetic detected"); + self.expr_id = Some(expr.hir_id); + } + } + }, + _ => { + span_lint(cx, INTEGER_ARITHMETIC, expr.span, "integer arithmetic detected"); + self.expr_id = Some(expr.hir_id); + }, + }, + _ => { + span_lint(cx, INTEGER_ARITHMETIC, expr.span, "integer arithmetic detected"); + self.expr_id = Some(expr.hir_id); + }, + } + } else if r_ty.peel_refs().is_floating_point() && r_ty.peel_refs().is_floating_point() { + span_lint(cx, FLOAT_ARITHMETIC, expr.span, "floating-point arithmetic detected"); + self.expr_id = Some(expr.hir_id); + } + } + + pub fn check_negate<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>, arg: &'tcx hir::Expr<'_>) { + if self.skip_expr(expr) { + return; + } + let ty = cx.typeck_results().expr_ty(arg); + if constant_simple(cx, cx.typeck_results(), expr).is_none() { + if ty.is_integral() { + span_lint(cx, INTEGER_ARITHMETIC, expr.span, "integer arithmetic detected"); + self.expr_id = Some(expr.hir_id); + } else if ty.is_floating_point() { + span_lint(cx, FLOAT_ARITHMETIC, expr.span, "floating-point arithmetic detected"); + self.expr_id = Some(expr.hir_id); + } + } + } + + pub fn expr_post(&mut self, id: hir::HirId) { + if Some(id) == self.expr_id { + self.expr_id = None; + } + } + + pub fn enter_body(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) { + let body_owner = cx.tcx.hir().body_owner(body.id()); + let body_owner_def_id = cx.tcx.hir().local_def_id(body_owner); + + match cx.tcx.hir().body_owner_kind(body_owner_def_id) { + hir::BodyOwnerKind::Static(_) | hir::BodyOwnerKind::Const => { + let body_span = cx.tcx.hir().span_with_body(body_owner); + + if let Some(span) = self.const_span { + if span.contains(body_span) { + return; + } + } + self.const_span = Some(body_span); + }, + hir::BodyOwnerKind::Fn | hir::BodyOwnerKind::Closure => (), + } + } + + pub fn body_post(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) { + let body_owner = cx.tcx.hir().body_owner(body.id()); + let body_span = cx.tcx.hir().span_with_body(body_owner); + + if let Some(span) = self.const_span { + if span.contains(body_span) { + return; + } + } + self.const_span = None; + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/op_ref.rs b/src/tools/clippy/clippy_lints/src/operators/op_ref.rs new file mode 100644 index 000000000..1805672e3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/op_ref.rs @@ -0,0 +1,218 @@ +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::get_enclosing_block; +use clippy_utils::source::snippet; +use clippy_utils::ty::{implements_trait, is_copy}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{def::Res, def_id::DefId, BinOpKind, BorrowKind, Expr, ExprKind, GenericArg, ItemKind, QPath, TyKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +use super::OP_REF; + +#[expect(clippy::similar_names, clippy::too_many_lines)] +pub(crate) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, +) { + let (trait_id, requires_ref) = match op { + BinOpKind::Add => (cx.tcx.lang_items().add_trait(), false), + BinOpKind::Sub => (cx.tcx.lang_items().sub_trait(), false), + BinOpKind::Mul => (cx.tcx.lang_items().mul_trait(), false), + BinOpKind::Div => (cx.tcx.lang_items().div_trait(), false), + BinOpKind::Rem => (cx.tcx.lang_items().rem_trait(), false), + // don't lint short circuiting ops + BinOpKind::And | BinOpKind::Or => return, + BinOpKind::BitXor => (cx.tcx.lang_items().bitxor_trait(), false), + BinOpKind::BitAnd => (cx.tcx.lang_items().bitand_trait(), false), + BinOpKind::BitOr => (cx.tcx.lang_items().bitor_trait(), false), + BinOpKind::Shl => (cx.tcx.lang_items().shl_trait(), false), + BinOpKind::Shr => (cx.tcx.lang_items().shr_trait(), false), + BinOpKind::Ne | BinOpKind::Eq => (cx.tcx.lang_items().eq_trait(), true), + BinOpKind::Lt | BinOpKind::Le | BinOpKind::Ge | BinOpKind::Gt => { + (cx.tcx.lang_items().partial_ord_trait(), true) + }, + }; + if let Some(trait_id) = trait_id { + match (&left.kind, &right.kind) { + // do not suggest to dereference literals + (&ExprKind::Lit(..), _) | (_, &ExprKind::Lit(..)) => {}, + // &foo == &bar + (&ExprKind::AddrOf(BorrowKind::Ref, _, l), &ExprKind::AddrOf(BorrowKind::Ref, _, r)) => { + let lty = cx.typeck_results().expr_ty(l); + let rty = cx.typeck_results().expr_ty(r); + let lcpy = is_copy(cx, lty); + let rcpy = is_copy(cx, rty); + if let Some((self_ty, other_ty)) = in_impl(cx, e, trait_id) { + if (are_equal(cx, rty, self_ty) && are_equal(cx, lty, other_ty)) + || (are_equal(cx, rty, other_ty) && are_equal(cx, lty, self_ty)) + { + return; // Don't lint + } + } + // either operator autorefs or both args are copyable + if (requires_ref || (lcpy && rcpy)) && implements_trait(cx, lty, trait_id, &[rty.into()]) { + span_lint_and_then( + cx, + OP_REF, + e.span, + "needlessly taken reference of both operands", + |diag| { + let lsnip = snippet(cx, l.span, "...").to_string(); + let rsnip = snippet(cx, r.span, "...").to_string(); + multispan_sugg( + diag, + "use the values directly", + vec![(left.span, lsnip), (right.span, rsnip)], + ); + }, + ); + } else if lcpy + && !rcpy + && implements_trait(cx, lty, trait_id, &[cx.typeck_results().expr_ty(right).into()]) + { + span_lint_and_then( + cx, + OP_REF, + e.span, + "needlessly taken reference of left operand", + |diag| { + let lsnip = snippet(cx, l.span, "...").to_string(); + diag.span_suggestion( + left.span, + "use the left value directly", + lsnip, + Applicability::MaybeIncorrect, // FIXME #2597 + ); + }, + ); + } else if !lcpy + && rcpy + && implements_trait(cx, cx.typeck_results().expr_ty(left), trait_id, &[rty.into()]) + { + span_lint_and_then( + cx, + OP_REF, + e.span, + "needlessly taken reference of right operand", + |diag| { + let rsnip = snippet(cx, r.span, "...").to_string(); + diag.span_suggestion( + right.span, + "use the right value directly", + rsnip, + Applicability::MaybeIncorrect, // FIXME #2597 + ); + }, + ); + } + }, + // &foo == bar + (&ExprKind::AddrOf(BorrowKind::Ref, _, l), _) => { + let lty = cx.typeck_results().expr_ty(l); + if let Some((self_ty, other_ty)) = in_impl(cx, e, trait_id) { + let rty = cx.typeck_results().expr_ty(right); + if (are_equal(cx, rty, self_ty) && are_equal(cx, lty, other_ty)) + || (are_equal(cx, rty, other_ty) && are_equal(cx, lty, self_ty)) + { + return; // Don't lint + } + } + let lcpy = is_copy(cx, lty); + if (requires_ref || lcpy) + && implements_trait(cx, lty, trait_id, &[cx.typeck_results().expr_ty(right).into()]) + { + span_lint_and_then( + cx, + OP_REF, + e.span, + "needlessly taken reference of left operand", + |diag| { + let lsnip = snippet(cx, l.span, "...").to_string(); + diag.span_suggestion( + left.span, + "use the left value directly", + lsnip, + Applicability::MaybeIncorrect, // FIXME #2597 + ); + }, + ); + } + }, + // foo == &bar + (_, &ExprKind::AddrOf(BorrowKind::Ref, _, r)) => { + let rty = cx.typeck_results().expr_ty(r); + if let Some((self_ty, other_ty)) = in_impl(cx, e, trait_id) { + let lty = cx.typeck_results().expr_ty(left); + if (are_equal(cx, rty, self_ty) && are_equal(cx, lty, other_ty)) + || (are_equal(cx, rty, other_ty) && are_equal(cx, lty, self_ty)) + { + return; // Don't lint + } + } + let rcpy = is_copy(cx, rty); + if (requires_ref || rcpy) + && implements_trait(cx, cx.typeck_results().expr_ty(left), trait_id, &[rty.into()]) + { + span_lint_and_then(cx, OP_REF, e.span, "taken reference of right operand", |diag| { + let rsnip = snippet(cx, r.span, "...").to_string(); + diag.span_suggestion( + right.span, + "use the right value directly", + rsnip, + Applicability::MaybeIncorrect, // FIXME #2597 + ); + }); + } + }, + _ => {}, + } + } +} + +fn in_impl<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + bin_op: DefId, +) -> Option<(&'tcx rustc_hir::Ty<'tcx>, &'tcx rustc_hir::Ty<'tcx>)> { + if_chain! { + if let Some(block) = get_enclosing_block(cx, e.hir_id); + if let Some(impl_def_id) = cx.tcx.impl_of_method(block.hir_id.owner.to_def_id()); + let item = cx.tcx.hir().expect_item(impl_def_id.expect_local()); + if let ItemKind::Impl(item) = &item.kind; + if let Some(of_trait) = &item.of_trait; + if let Some(seg) = of_trait.path.segments.last(); + if let Some(Res::Def(_, trait_id)) = seg.res; + if trait_id == bin_op; + if let Some(generic_args) = seg.args; + if let Some(GenericArg::Type(other_ty)) = generic_args.args.last(); + + then { + Some((item.self_ty, other_ty)) + } + else { + None + } + } +} + +fn are_equal<'tcx>(cx: &LateContext<'tcx>, middle_ty: Ty<'_>, hir_ty: &rustc_hir::Ty<'_>) -> bool { + if_chain! { + if let ty::Adt(adt_def, _) = middle_ty.kind(); + if let Some(local_did) = adt_def.did().as_local(); + let item = cx.tcx.hir().expect_item(local_did); + let middle_ty_id = item.def_id.to_def_id(); + if let TyKind::Path(QPath::Resolved(_, path)) = hir_ty.kind; + if let Res::Def(_, hir_ty_id) = path.res; + + then { + hir_ty_id == middle_ty_id + } + else { + false + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/ptr_eq.rs b/src/tools/clippy/clippy_lints/src/operators/ptr_eq.rs new file mode 100644 index 000000000..1aefc2741 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/ptr_eq.rs @@ -0,0 +1,65 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_opt; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; + +use super::PTR_EQ; + +static LINT_MSG: &str = "use `std::ptr::eq` when comparing raw pointers"; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, +) { + if BinOpKind::Eq == op { + let (left, right) = match (expr_as_cast_to_usize(cx, left), expr_as_cast_to_usize(cx, right)) { + (Some(lhs), Some(rhs)) => (lhs, rhs), + _ => (left, right), + }; + + if_chain! { + if let Some(left_var) = expr_as_cast_to_raw_pointer(cx, left); + if let Some(right_var) = expr_as_cast_to_raw_pointer(cx, right); + if let Some(left_snip) = snippet_opt(cx, left_var.span); + if let Some(right_snip) = snippet_opt(cx, right_var.span); + then { + span_lint_and_sugg( + cx, + PTR_EQ, + expr.span, + LINT_MSG, + "try", + format!("std::ptr::eq({}, {})", left_snip, right_snip), + Applicability::MachineApplicable, + ); + } + } + } +} + +// If the given expression is a cast to a usize, return the lhs of the cast +// E.g., `foo as *const _ as usize` returns `foo as *const _`. +fn expr_as_cast_to_usize<'tcx>(cx: &LateContext<'tcx>, cast_expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { + if cx.typeck_results().expr_ty(cast_expr) == cx.tcx.types.usize { + if let ExprKind::Cast(expr, _) = cast_expr.kind { + return Some(expr); + } + } + None +} + +// If the given expression is a cast to a `*const` pointer, return the lhs of the cast +// E.g., `foo as *const _` returns `foo`. +fn expr_as_cast_to_raw_pointer<'tcx>(cx: &LateContext<'tcx>, cast_expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { + if cx.typeck_results().expr_ty(cast_expr).is_unsafe_ptr() { + if let ExprKind::Cast(expr, _) = cast_expr.kind { + return Some(expr); + } + } + None +} diff --git a/src/tools/clippy/clippy_lints/src/operators/self_assignment.rs b/src/tools/clippy/clippy_lints/src/operators/self_assignment.rs new file mode 100644 index 000000000..9d6bec05b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/self_assignment.rs @@ -0,0 +1,20 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::eq_expr_value; +use clippy_utils::source::snippet; +use rustc_hir::Expr; +use rustc_lint::LateContext; + +use super::SELF_ASSIGNMENT; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>, lhs: &'tcx Expr<'_>, rhs: &'tcx Expr<'_>) { + if eq_expr_value(cx, lhs, rhs) { + let lhs = snippet(cx, lhs.span, "<lhs>"); + let rhs = snippet(cx, rhs.span, "<rhs>"); + span_lint( + cx, + SELF_ASSIGNMENT, + e.span, + &format!("self-assignment of `{}` to `{}`", rhs, lhs), + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/operators/verbose_bit_mask.rs b/src/tools/clippy/clippy_lints/src/operators/verbose_bit_mask.rs new file mode 100644 index 000000000..ff85fd554 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/operators/verbose_bit_mask.rs @@ -0,0 +1,44 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg::Sugg; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; + +use super::VERBOSE_BIT_MASK; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + op: BinOpKind, + left: &'tcx Expr<'_>, + right: &'tcx Expr<'_>, + threshold: u64, +) { + if BinOpKind::Eq == op + && let ExprKind::Binary(op1, left1, right1) = &left.kind + && BinOpKind::BitAnd == op1.node + && let ExprKind::Lit(lit) = &right1.kind + && let LitKind::Int(n, _) = lit.node + && let ExprKind::Lit(lit1) = &right.kind + && let LitKind::Int(0, _) = lit1.node + && n.leading_zeros() == n.count_zeros() + && n > u128::from(threshold) + { + span_lint_and_then( + cx, + VERBOSE_BIT_MASK, + e.span, + "bit mask could be simplified with a call to `trailing_zeros`", + |diag| { + let sugg = Sugg::hir(cx, left1, "...").maybe_par(); + diag.span_suggestion( + e.span, + "try", + format!("{}.trailing_zeros() >= {}", sugg, n.count_ones()), + Applicability::MaybeIncorrect, + ); + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/option_env_unwrap.rs b/src/tools/clippy/clippy_lints/src/option_env_unwrap.rs new file mode 100644 index 000000000..3f5286ba0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/option_env_unwrap.rs @@ -0,0 +1,56 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_direct_expn_of; +use if_chain::if_chain; +use rustc_ast::ast::{Expr, ExprKind}; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `option_env!(...).unwrap()` and + /// suggests usage of the `env!` macro. + /// + /// ### Why is this bad? + /// Unwrapping the result of `option_env!` will panic + /// at run-time if the environment variable doesn't exist, whereas `env!` + /// catches it at compile-time. + /// + /// ### Example + /// ```rust,no_run + /// let _ = option_env!("HOME").unwrap(); + /// ``` + /// + /// Is better expressed as: + /// + /// ```rust,no_run + /// let _ = env!("HOME"); + /// ``` + #[clippy::version = "1.43.0"] + pub OPTION_ENV_UNWRAP, + correctness, + "using `option_env!(...).unwrap()` to get environment variable" +} + +declare_lint_pass!(OptionEnvUnwrap => [OPTION_ENV_UNWRAP]); + +impl EarlyLintPass for OptionEnvUnwrap { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if_chain! { + if let ExprKind::MethodCall(path_segment, args, _) = &expr.kind; + if matches!(path_segment.ident.name, sym::expect | sym::unwrap); + if let ExprKind::Call(caller, _) = &args[0].kind; + if is_direct_expn_of(caller.span, "option_env").is_some(); + then { + span_lint_and_help( + cx, + OPTION_ENV_UNWRAP, + expr.span, + "this will panic at run-time if the environment variable doesn't exist at compile-time", + None, + "consider using the `env!` macro instead" + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/option_if_let_else.rs b/src/tools/clippy/clippy_lints/src/option_if_let_else.rs new file mode 100644 index 000000000..44f153cff --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/option_if_let_else.rs @@ -0,0 +1,186 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{ + can_move_expr_to_closure, eager_or_lazy, higher, in_constant, is_else_clause, is_lang_ctor, peel_blocks, + peel_hir_expr_while, CaptureKind, +}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::OptionSome; +use rustc_hir::{def::Res, BindingAnnotation, Expr, ExprKind, Mutability, PatKind, Path, QPath, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Lints usage of `if let Some(v) = ... { y } else { x }` which is more + /// idiomatically done with `Option::map_or` (if the else bit is a pure + /// expression) or `Option::map_or_else` (if the else bit is an impure + /// expression). + /// + /// ### Why is this bad? + /// Using the dedicated functions of the `Option` type is clearer and + /// more concise than an `if let` expression. + /// + /// ### Known problems + /// This lint uses a deliberately conservative metric for checking + /// if the inside of either body contains breaks or continues which will + /// cause it to not suggest a fix if either block contains a loop with + /// continues or breaks contained within the loop. + /// + /// ### Example + /// ```rust + /// # let optional: Option<u32> = Some(0); + /// # fn do_complicated_function() -> u32 { 5 }; + /// let _ = if let Some(foo) = optional { + /// foo + /// } else { + /// 5 + /// }; + /// let _ = if let Some(foo) = optional { + /// foo + /// } else { + /// let y = do_complicated_function(); + /// y*y + /// }; + /// ``` + /// + /// should be + /// + /// ```rust + /// # let optional: Option<u32> = Some(0); + /// # fn do_complicated_function() -> u32 { 5 }; + /// let _ = optional.map_or(5, |foo| foo); + /// let _ = optional.map_or_else(||{ + /// let y = do_complicated_function(); + /// y*y + /// }, |foo| foo); + /// ``` + #[clippy::version = "1.47.0"] + pub OPTION_IF_LET_ELSE, + nursery, + "reimplementation of Option::map_or" +} + +declare_lint_pass!(OptionIfLetElse => [OPTION_IF_LET_ELSE]); + +/// Returns true iff the given expression is the result of calling `Result::ok` +fn is_result_ok(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool { + if let ExprKind::MethodCall(path, &[ref receiver], _) = &expr.kind { + path.ident.name.as_str() == "ok" + && is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(receiver), sym::Result) + } else { + false + } +} + +/// A struct containing information about occurrences of the +/// `if let Some(..) = .. else` construct that this lint detects. +struct OptionIfLetElseOccurrence { + option: String, + method_sugg: String, + some_expr: String, + none_expr: String, +} + +fn format_option_in_sugg(cx: &LateContext<'_>, cond_expr: &Expr<'_>, as_ref: bool, as_mut: bool) -> String { + format!( + "{}{}", + Sugg::hir_with_macro_callsite(cx, cond_expr, "..").maybe_par(), + if as_mut { + ".as_mut()" + } else if as_ref { + ".as_ref()" + } else { + "" + } + ) +} + +/// If this expression is the option if let/else construct we're detecting, then +/// this function returns an `OptionIfLetElseOccurrence` struct with details if +/// this construct is found, or None if this construct is not found. +fn detect_option_if_let_else<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<OptionIfLetElseOccurrence> { + if_chain! { + if !expr.span.from_expansion(); // Don't lint macros, because it behaves weirdly + if !in_constant(cx, expr.hir_id); + if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else: Some(if_else) }) + = higher::IfLet::hir(cx, expr); + if !is_else_clause(cx.tcx, expr); + if !is_result_ok(cx, let_expr); // Don't lint on Result::ok because a different lint does it already + if let PatKind::TupleStruct(struct_qpath, [inner_pat], _) = &let_pat.kind; + if is_lang_ctor(cx, struct_qpath, OptionSome); + if let PatKind::Binding(bind_annotation, _, id, None) = &inner_pat.kind; + if let Some(some_captures) = can_move_expr_to_closure(cx, if_then); + if let Some(none_captures) = can_move_expr_to_closure(cx, if_else); + if some_captures + .iter() + .filter_map(|(id, &c)| none_captures.get(id).map(|&c2| (c, c2))) + .all(|(x, y)| x.is_imm_ref() && y.is_imm_ref()); + + then { + let capture_mut = if bind_annotation == &BindingAnnotation::Mutable { "mut " } else { "" }; + let some_body = peel_blocks(if_then); + let none_body = peel_blocks(if_else); + let method_sugg = if eager_or_lazy::switch_to_eager_eval(cx, none_body) { "map_or" } else { "map_or_else" }; + let capture_name = id.name.to_ident_string(); + let (as_ref, as_mut) = match &let_expr.kind { + ExprKind::AddrOf(_, Mutability::Not, _) => (true, false), + ExprKind::AddrOf(_, Mutability::Mut, _) => (false, true), + _ => (bind_annotation == &BindingAnnotation::Ref, bind_annotation == &BindingAnnotation::RefMut), + }; + let cond_expr = match let_expr.kind { + // Pointer dereferencing happens automatically, so we can omit it in the suggestion + ExprKind::Unary(UnOp::Deref, expr) | ExprKind::AddrOf(_, _, expr) => expr, + _ => let_expr, + }; + // Check if captures the closure will need conflict with borrows made in the scrutinee. + // TODO: check all the references made in the scrutinee expression. This will require interacting + // with the borrow checker. Currently only `<local>[.<field>]*` is checked for. + if as_ref || as_mut { + let e = peel_hir_expr_while(cond_expr, |e| match e.kind { + ExprKind::Field(e, _) | ExprKind::AddrOf(_, _, e) => Some(e), + _ => None, + }); + if let ExprKind::Path(QPath::Resolved(None, Path { res: Res::Local(local_id), .. })) = e.kind { + match some_captures.get(local_id) + .or_else(|| (method_sugg == "map_or_else").then_some(()).and_then(|_| none_captures.get(local_id))) + { + Some(CaptureKind::Value | CaptureKind::Ref(Mutability::Mut)) => return None, + Some(CaptureKind::Ref(Mutability::Not)) if as_mut => return None, + Some(CaptureKind::Ref(Mutability::Not)) | None => (), + } + } + } + Some(OptionIfLetElseOccurrence { + option: format_option_in_sugg(cx, cond_expr, as_ref, as_mut), + method_sugg: method_sugg.to_string(), + some_expr: format!("|{}{}| {}", capture_mut, capture_name, Sugg::hir_with_macro_callsite(cx, some_body, "..")), + none_expr: format!("{}{}", if method_sugg == "map_or" { "" } else { "|| " }, Sugg::hir_with_macro_callsite(cx, none_body, "..")), + }) + } else { + None + } + } +} + +impl<'tcx> LateLintPass<'tcx> for OptionIfLetElse { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + if let Some(detection) = detect_option_if_let_else(cx, expr) { + span_lint_and_sugg( + cx, + OPTION_IF_LET_ELSE, + expr.span, + format!("use Option::{} instead of an if let/else", detection.method_sugg).as_str(), + "try", + format!( + "{}.{}({}, {})", + detection.option, detection.method_sugg, detection.none_expr, detection.some_expr, + ), + Applicability::MaybeIncorrect, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/overflow_check_conditional.rs b/src/tools/clippy/clippy_lints/src/overflow_check_conditional.rs new file mode 100644 index 000000000..6dabbd480 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/overflow_check_conditional.rs @@ -0,0 +1,75 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::SpanlessEq; +use if_chain::if_chain; +use rustc_hir::{BinOpKind, Expr, ExprKind, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Detects classic underflow/overflow checks. + /// + /// ### Why is this bad? + /// Most classic C underflow/overflow checks will fail in + /// Rust. Users can use functions like `overflowing_*` and `wrapping_*` instead. + /// + /// ### Example + /// ```rust + /// # let a = 1; + /// # let b = 2; + /// a + b < a; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OVERFLOW_CHECK_CONDITIONAL, + complexity, + "overflow checks inspired by C which are likely to panic" +} + +declare_lint_pass!(OverflowCheckConditional => [OVERFLOW_CHECK_CONDITIONAL]); + +const OVERFLOW_MSG: &str = "you are trying to use classic C overflow conditions that will fail in Rust"; +const UNDERFLOW_MSG: &str = "you are trying to use classic C underflow conditions that will fail in Rust"; + +impl<'tcx> LateLintPass<'tcx> for OverflowCheckConditional { + // a + b < a, a > a + b, a < a - b, a - b > a + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let eq = |l, r| SpanlessEq::new(cx).eq_path_segment(l, r); + if_chain! { + if let ExprKind::Binary(ref op, first, second) = expr.kind; + if let ExprKind::Binary(ref op2, ident1, ident2) = first.kind; + if let ExprKind::Path(QPath::Resolved(_, path1)) = ident1.kind; + if let ExprKind::Path(QPath::Resolved(_, path2)) = ident2.kind; + if let ExprKind::Path(QPath::Resolved(_, path3)) = second.kind; + if eq(&path1.segments[0], &path3.segments[0]) || eq(&path2.segments[0], &path3.segments[0]); + if cx.typeck_results().expr_ty(ident1).is_integral(); + if cx.typeck_results().expr_ty(ident2).is_integral(); + then { + if op.node == BinOpKind::Lt && op2.node == BinOpKind::Add { + span_lint(cx, OVERFLOW_CHECK_CONDITIONAL, expr.span, OVERFLOW_MSG); + } + if op.node == BinOpKind::Gt && op2.node == BinOpKind::Sub { + span_lint(cx, OVERFLOW_CHECK_CONDITIONAL, expr.span, UNDERFLOW_MSG); + } + } + } + + if_chain! { + if let ExprKind::Binary(ref op, first, second) = expr.kind; + if let ExprKind::Binary(ref op2, ident1, ident2) = second.kind; + if let ExprKind::Path(QPath::Resolved(_, path1)) = ident1.kind; + if let ExprKind::Path(QPath::Resolved(_, path2)) = ident2.kind; + if let ExprKind::Path(QPath::Resolved(_, path3)) = first.kind; + if eq(&path1.segments[0], &path3.segments[0]) || eq(&path2.segments[0], &path3.segments[0]); + if cx.typeck_results().expr_ty(ident1).is_integral(); + if cx.typeck_results().expr_ty(ident2).is_integral(); + then { + if op.node == BinOpKind::Gt && op2.node == BinOpKind::Add { + span_lint(cx, OVERFLOW_CHECK_CONDITIONAL, expr.span, OVERFLOW_MSG); + } + if op.node == BinOpKind::Lt && op2.node == BinOpKind::Sub { + span_lint(cx, OVERFLOW_CHECK_CONDITIONAL, expr.span, UNDERFLOW_MSG); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/panic_in_result_fn.rs b/src/tools/clippy/clippy_lints/src/panic_in_result_fn.rs new file mode 100644 index 000000000..21acf003d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/panic_in_result_fn.rs @@ -0,0 +1,87 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::macros::root_macro_call_first_node; +use clippy_utils::return_ty; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::visitors::expr_visitor_no_bodies; +use rustc_hir as hir; +use rustc_hir::intravisit::{FnKind, Visitor}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `panic!`, `unimplemented!`, `todo!`, `unreachable!` or assertions in a function of type result. + /// + /// ### Why is this bad? + /// For some codebases, it is desirable for functions of type result to return an error instead of crashing. Hence panicking macros should be avoided. + /// + /// ### Known problems + /// Functions called from a function returning a `Result` may invoke a panicking macro. This is not checked. + /// + /// ### Example + /// ```rust + /// fn result_with_panic() -> Result<bool, String> + /// { + /// panic!("error"); + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn result_without_panic() -> Result<bool, String> { + /// Err(String::from("error")) + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub PANIC_IN_RESULT_FN, + restriction, + "functions of type `Result<..>` that contain `panic!()`, `todo!()`, `unreachable()`, `unimplemented()` or assertion" +} + +declare_lint_pass!(PanicInResultFn => [PANIC_IN_RESULT_FN]); + +impl<'tcx> LateLintPass<'tcx> for PanicInResultFn { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + fn_kind: FnKind<'tcx>, + _: &'tcx hir::FnDecl<'tcx>, + body: &'tcx hir::Body<'tcx>, + span: Span, + hir_id: hir::HirId, + ) { + if !matches!(fn_kind, FnKind::Closure) && is_type_diagnostic_item(cx, return_ty(cx, hir_id), sym::Result) { + lint_impl_body(cx, span, body); + } + } +} + +fn lint_impl_body<'tcx>(cx: &LateContext<'tcx>, impl_span: Span, body: &'tcx hir::Body<'tcx>) { + let mut panics = Vec::new(); + expr_visitor_no_bodies(|expr| { + let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return true }; + if matches!( + cx.tcx.item_name(macro_call.def_id).as_str(), + "unimplemented" | "unreachable" | "panic" | "todo" | "assert" | "assert_eq" | "assert_ne" + ) { + panics.push(macro_call.span); + return false; + } + true + }) + .visit_expr(&body.value); + if !panics.is_empty() { + span_lint_and_then( + cx, + PANIC_IN_RESULT_FN, + impl_span, + "used `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertion in a function that returns `Result`", + move |diag| { + diag.help( + "`unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertions should not be used in a function that returns `Result` as `Result` is expected to return an error instead of crashing", + ); + diag.span_note(panics, "return Err() instead of panicking"); + }, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/panic_unimplemented.rs b/src/tools/clippy/clippy_lints/src/panic_unimplemented.rs new file mode 100644 index 000000000..2f3007658 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/panic_unimplemented.rs @@ -0,0 +1,116 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::macros::{is_panic, root_macro_call_first_node}; +use rustc_hir::Expr; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `panic!`. + /// + /// ### Why is this bad? + /// `panic!` will stop the execution of the executable + /// + /// ### Example + /// ```no_run + /// panic!("even with a good reason"); + /// ``` + #[clippy::version = "1.40.0"] + pub PANIC, + restriction, + "usage of the `panic!` macro" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `unimplemented!`. + /// + /// ### Why is this bad? + /// This macro should not be present in production code + /// + /// ### Example + /// ```no_run + /// unimplemented!(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNIMPLEMENTED, + restriction, + "`unimplemented!` should not be present in production code" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `todo!`. + /// + /// ### Why is this bad? + /// This macro should not be present in production code + /// + /// ### Example + /// ```no_run + /// todo!(); + /// ``` + #[clippy::version = "1.40.0"] + pub TODO, + restriction, + "`todo!` should not be present in production code" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `unreachable!`. + /// + /// ### Why is this bad? + /// This macro can cause code to panic + /// + /// ### Example + /// ```no_run + /// unreachable!(); + /// ``` + #[clippy::version = "1.40.0"] + pub UNREACHABLE, + restriction, + "usage of the `unreachable!` macro" +} + +declare_lint_pass!(PanicUnimplemented => [UNIMPLEMENTED, UNREACHABLE, TODO, PANIC]); + +impl<'tcx> LateLintPass<'tcx> for PanicUnimplemented { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return }; + if is_panic(cx, macro_call.def_id) { + if cx.tcx.hir().is_inside_const_context(expr.hir_id) { + return; + } + + span_lint( + cx, + PANIC, + macro_call.span, + "`panic` should not be present in production code", + ); + return; + } + match cx.tcx.item_name(macro_call.def_id).as_str() { + "todo" => { + span_lint( + cx, + TODO, + macro_call.span, + "`todo` should not be present in production code", + ); + }, + "unimplemented" => { + span_lint( + cx, + UNIMPLEMENTED, + macro_call.span, + "`unimplemented` should not be present in production code", + ); + }, + "unreachable" => { + span_lint(cx, UNREACHABLE, macro_call.span, "usage of the `unreachable!` macro"); + }, + _ => {}, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/partialeq_ne_impl.rs b/src/tools/clippy/clippy_lints/src/partialeq_ne_impl.rs new file mode 100644 index 000000000..09ac514d0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/partialeq_ne_impl.rs @@ -0,0 +1,57 @@ +use clippy_utils::diagnostics::span_lint_hir; +use if_chain::if_chain; +use rustc_hir::{Impl, Item, ItemKind}; +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 manual re-implementations of `PartialEq::ne`. + /// + /// ### Why is this bad? + /// `PartialEq::ne` is required to always return the + /// negated result of `PartialEq::eq`, which is exactly what the default + /// implementation does. Therefore, there should never be any need to + /// re-implement it. + /// + /// ### Example + /// ```rust + /// struct Foo; + /// + /// impl PartialEq for Foo { + /// fn eq(&self, other: &Foo) -> bool { true } + /// fn ne(&self, other: &Foo) -> bool { !(self == other) } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub PARTIALEQ_NE_IMPL, + complexity, + "re-implementing `PartialEq::ne`" +} + +declare_lint_pass!(PartialEqNeImpl => [PARTIALEQ_NE_IMPL]); + +impl<'tcx> LateLintPass<'tcx> for PartialEqNeImpl { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if_chain! { + if let ItemKind::Impl(Impl { of_trait: Some(ref trait_ref), items: impl_items, .. }) = item.kind; + if !cx.tcx.has_attr(item.def_id.to_def_id(), sym::automatically_derived); + if let Some(eq_trait) = cx.tcx.lang_items().eq_trait(); + if trait_ref.path.res.def_id() == eq_trait; + then { + for impl_item in *impl_items { + if impl_item.ident.name == sym::ne { + span_lint_hir( + cx, + PARTIALEQ_NE_IMPL, + impl_item.id.hir_id(), + impl_item.span, + "re-implementing `PartialEq::ne` is unnecessary", + ); + } + } + } + }; + } +} diff --git a/src/tools/clippy/clippy_lints/src/pass_by_ref_or_value.rs b/src/tools/clippy/clippy_lints/src/pass_by_ref_or_value.rs new file mode 100644 index 000000000..5fa4fd748 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/pass_by_ref_or_value.rs @@ -0,0 +1,313 @@ +use std::cmp; +use std::iter; + +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::ty::{for_each_top_level_late_bound_region, is_copy}; +use clippy_utils::{is_self, is_self_ty}; +use core::ops::ControlFlow; +use if_chain::if_chain; +use rustc_ast::attr; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{BindingAnnotation, Body, FnDecl, HirId, Impl, ItemKind, MutTy, Mutability, Node, PatKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::adjustment::{Adjust, PointerCast}; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::{self, RegionKind}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::def_id::LocalDefId; +use rustc_span::{sym, Span}; +use rustc_target::spec::abi::Abi; +use rustc_target::spec::Target; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions taking arguments by reference, where + /// the argument type is `Copy` and small enough to be more efficient to always + /// pass by value. + /// + /// ### Why is this bad? + /// In many calling conventions instances of structs will + /// be passed through registers if they fit into two or less general purpose + /// registers. + /// + /// ### Known problems + /// This lint is target register size dependent, it is + /// limited to 32-bit to try and reduce portability problems between 32 and + /// 64-bit, but if you are compiling for 8 or 16-bit targets then the limit + /// will be different. + /// + /// The configuration option `trivial_copy_size_limit` can be set to override + /// this limit for a project. + /// + /// This lint attempts to allow passing arguments by reference if a reference + /// to that argument is returned. This is implemented by comparing the lifetime + /// of the argument and return value for equality. However, this can cause + /// false positives in cases involving multiple lifetimes that are bounded by + /// each other. + /// + /// Also, it does not take account of other similar cases where getting memory addresses + /// matters; namely, returning the pointer to the argument in question, + /// and passing the argument, as both references and pointers, + /// to a function that needs the memory address. For further details, refer to + /// [this issue](https://github.com/rust-lang/rust-clippy/issues/5953) + /// that explains a real case in which this false positive + /// led to an **undefined behavior** introduced with unsafe code. + /// + /// ### Example + /// + /// ```rust + /// fn foo(v: &u32) {} + /// ``` + /// + /// Use instead: + /// ```rust + /// fn foo(v: u32) {} + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRIVIALLY_COPY_PASS_BY_REF, + pedantic, + "functions taking small copyable arguments by reference" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions taking arguments by value, where + /// the argument type is `Copy` and large enough to be worth considering + /// passing by reference. Does not trigger if the function is being exported, + /// because that might induce API breakage, if the parameter is declared as mutable, + /// or if the argument is a `self`. + /// + /// ### Why is this bad? + /// Arguments passed by value might result in an unnecessary + /// shallow copy, taking up more space in the stack and requiring a call to + /// `memcpy`, which can be expensive. + /// + /// ### Example + /// ```rust + /// #[derive(Clone, Copy)] + /// struct TooLarge([u8; 2048]); + /// + /// fn foo(v: TooLarge) {} + /// ``` + /// + /// Use instead: + /// ```rust + /// # #[derive(Clone, Copy)] + /// # struct TooLarge([u8; 2048]); + /// fn foo(v: &TooLarge) {} + /// ``` + #[clippy::version = "1.49.0"] + pub LARGE_TYPES_PASSED_BY_VALUE, + pedantic, + "functions taking large arguments by value" +} + +#[derive(Copy, Clone)] +pub struct PassByRefOrValue { + ref_min_size: u64, + value_max_size: u64, + avoid_breaking_exported_api: bool, +} + +impl<'tcx> PassByRefOrValue { + pub fn new( + ref_min_size: Option<u64>, + value_max_size: u64, + avoid_breaking_exported_api: bool, + target: &Target, + ) -> Self { + let ref_min_size = ref_min_size.unwrap_or_else(|| { + let bit_width = u64::from(target.pointer_width); + // Cap the calculated bit width at 32-bits to reduce + // portability problems between 32 and 64-bit targets + let bit_width = cmp::min(bit_width, 32); + #[expect(clippy::integer_division)] + let byte_width = bit_width / 8; + // Use a limit of 2 times the register byte width + byte_width * 2 + }); + + Self { + ref_min_size, + value_max_size, + avoid_breaking_exported_api, + } + } + + fn check_poly_fn(&mut self, cx: &LateContext<'tcx>, def_id: LocalDefId, decl: &FnDecl<'_>, span: Option<Span>) { + if self.avoid_breaking_exported_api && cx.access_levels.is_exported(def_id) { + return; + } + + let fn_sig = cx.tcx.fn_sig(def_id); + let fn_body = cx.enclosing_body.map(|id| cx.tcx.hir().body(id)); + + // Gather all the lifetimes found in the output type which may affect whether + // `TRIVIALLY_COPY_PASS_BY_REF` should be linted. + let mut output_regions = FxHashSet::default(); + for_each_top_level_late_bound_region(fn_sig.skip_binder().output(), |region| -> ControlFlow<!> { + output_regions.insert(region); + ControlFlow::Continue(()) + }); + + for (index, (input, ty)) in iter::zip( + decl.inputs, + fn_sig.skip_binder().inputs().iter().map(|&ty| fn_sig.rebind(ty)), + ) + .enumerate() + { + // All spans generated from a proc-macro invocation are the same... + match span { + Some(s) if s == input.span => continue, + _ => (), + } + + match *ty.skip_binder().kind() { + ty::Ref(lt, ty, Mutability::Not) => { + match lt.kind() { + RegionKind::ReLateBound(index, region) + if index.as_u32() == 0 && output_regions.contains(®ion) => + { + continue; + }, + // Early bound regions on functions are either from the containing item, are bounded by another + // lifetime, or are used as a bound for a type or lifetime. + RegionKind::ReEarlyBound(..) => continue, + _ => (), + } + + let ty = cx.tcx.erase_late_bound_regions(fn_sig.rebind(ty)); + if is_copy(cx, ty) + && let Some(size) = cx.layout_of(ty).ok().map(|l| l.size.bytes()) + && size <= self.ref_min_size + && let hir::TyKind::Rptr(_, MutTy { ty: decl_ty, .. }) = input.kind + { + if let Some(typeck) = cx.maybe_typeck_results() { + // Don't lint if an unsafe pointer is created. + // TODO: Limit the check only to unsafe pointers to the argument (or part of the argument) + // which escape the current function. + if typeck.node_types().iter().any(|(_, &ty)| ty.is_unsafe_ptr()) + || typeck + .adjustments() + .iter() + .flat_map(|(_, a)| a) + .any(|a| matches!(a.kind, Adjust::Pointer(PointerCast::UnsafeFnPointer))) + { + continue; + } + } + let value_type = if fn_body.and_then(|body| body.params.get(index)).map_or(false, is_self) { + "self".into() + } else { + snippet(cx, decl_ty.span, "_").into() + }; + span_lint_and_sugg( + cx, + TRIVIALLY_COPY_PASS_BY_REF, + input.span, + &format!("this argument ({} byte) is passed by reference, but would be more efficient if passed by value (limit: {} byte)", size, self.ref_min_size), + "consider passing by value instead", + value_type, + Applicability::Unspecified, + ); + } + }, + + ty::Adt(_, _) | ty::Array(_, _) | ty::Tuple(_) => { + // if function has a body and parameter is annotated with mut, ignore + if let Some(param) = fn_body.and_then(|body| body.params.get(index)) { + match param.pat.kind { + PatKind::Binding(BindingAnnotation::Unannotated, _, _, _) => {}, + _ => continue, + } + } + let ty = cx.tcx.erase_late_bound_regions(ty); + + if_chain! { + if is_copy(cx, ty); + if !is_self_ty(input); + if let Some(size) = cx.layout_of(ty).ok().map(|l| l.size.bytes()); + if size > self.value_max_size; + then { + span_lint_and_sugg( + cx, + LARGE_TYPES_PASSED_BY_VALUE, + input.span, + &format!("this argument ({} byte) is passed by value, but might be more efficient if passed by reference (limit: {} byte)", size, self.value_max_size), + "consider passing by reference instead", + format!("&{}", snippet(cx, input.span, "_")), + Applicability::MaybeIncorrect, + ); + } + } + }, + + _ => {}, + } + } + } +} + +impl_lint_pass!(PassByRefOrValue => [TRIVIALLY_COPY_PASS_BY_REF, LARGE_TYPES_PASSED_BY_VALUE]); + +impl<'tcx> LateLintPass<'tcx> for PassByRefOrValue { + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) { + if item.span.from_expansion() { + return; + } + + if let hir::TraitItemKind::Fn(method_sig, _) = &item.kind { + self.check_poly_fn(cx, item.def_id, method_sig.decl, None); + } + } + + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + _body: &'tcx Body<'_>, + span: Span, + hir_id: HirId, + ) { + if span.from_expansion() { + return; + } + + match kind { + FnKind::ItemFn(.., header) => { + if header.abi != Abi::Rust { + return; + } + let attrs = cx.tcx.hir().attrs(hir_id); + for a in attrs { + if let Some(meta_items) = a.meta_item_list() { + if a.has_name(sym::proc_macro_derive) + || (a.has_name(sym::inline) && attr::list_contains_name(&meta_items, sym::always)) + { + return; + } + } + } + }, + FnKind::Method(..) => (), + FnKind::Closure => return, + } + + // Exclude non-inherent impls + if let Some(Node::Item(item)) = cx.tcx.hir().find(cx.tcx.hir().get_parent_node(hir_id)) { + if matches!( + item.kind, + ItemKind::Impl(Impl { of_trait: Some(_), .. }) | ItemKind::Trait(..) + ) { + return; + } + } + + self.check_poly_fn(cx, cx.tcx.hir().local_def_id(hir_id), decl, Some(span)); + } +} diff --git a/src/tools/clippy/clippy_lints/src/path_buf_push_overwrite.rs b/src/tools/clippy/clippy_lints/src/path_buf_push_overwrite.rs new file mode 100644 index 000000000..3f940ce61 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/path_buf_push_overwrite.rs @@ -0,0 +1,74 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; +use std::path::{Component, Path}; + +declare_clippy_lint! { + /// ### What it does + ///* Checks for [push](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push) + /// calls on `PathBuf` that can cause overwrites. + /// + /// ### Why is this bad? + /// Calling `push` with a root path at the start can overwrite the + /// previous defined path. + /// + /// ### Example + /// ```rust + /// use std::path::PathBuf; + /// + /// let mut x = PathBuf::from("/foo"); + /// x.push("/bar"); + /// assert_eq!(x, PathBuf::from("/bar")); + /// ``` + /// Could be written: + /// + /// ```rust + /// use std::path::PathBuf; + /// + /// let mut x = PathBuf::from("/foo"); + /// x.push("bar"); + /// assert_eq!(x, PathBuf::from("/foo/bar")); + /// ``` + #[clippy::version = "1.36.0"] + pub PATH_BUF_PUSH_OVERWRITE, + nursery, + "calling `push` with file system root on `PathBuf` can overwrite it" +} + +declare_lint_pass!(PathBufPushOverwrite => [PATH_BUF_PUSH_OVERWRITE]); + +impl<'tcx> LateLintPass<'tcx> for PathBufPushOverwrite { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::MethodCall(path, args, _) = expr.kind; + if path.ident.name == sym!(push); + if args.len() == 2; + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(&args[0]).peel_refs(), sym::PathBuf); + if let Some(get_index_arg) = args.get(1); + if let ExprKind::Lit(ref lit) = get_index_arg.kind; + if let LitKind::Str(ref path_lit, _) = lit.node; + if let pushed_path = Path::new(path_lit.as_str()); + if let Some(pushed_path_lit) = pushed_path.to_str(); + if pushed_path.has_root(); + if let Some(root) = pushed_path.components().next(); + if root == Component::RootDir; + then { + span_lint_and_sugg( + cx, + PATH_BUF_PUSH_OVERWRITE, + lit.span, + "calling `push` with '/' or '\\' (file system root) will overwrite the previous path definition", + "try", + format!("\"{}\"", pushed_path_lit.trim_start_matches(|c| c == '/' || c == '\\')), + Applicability::MachineApplicable, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/pattern_type_mismatch.rs b/src/tools/clippy/clippy_lints/src/pattern_type_mismatch.rs new file mode 100644 index 000000000..a4d265111 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/pattern_type_mismatch.rs @@ -0,0 +1,194 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{ + intravisit, Body, Expr, ExprKind, FnDecl, HirId, Let, LocalSource, Mutability, Pat, PatKind, Stmt, StmtKind, +}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for patterns that aren't exact representations of the types + /// they are applied to. + /// + /// To satisfy this lint, you will have to adjust either the expression that is matched + /// against or the pattern itself, as well as the bindings that are introduced by the + /// adjusted patterns. For matching you will have to either dereference the expression + /// with the `*` operator, or amend the patterns to explicitly match against `&<pattern>` + /// or `&mut <pattern>` depending on the reference mutability. For the bindings you need + /// to use the inverse. You can leave them as plain bindings if you wish for the value + /// to be copied, but you must use `ref mut <variable>` or `ref <variable>` to construct + /// a reference into the matched structure. + /// + /// If you are looking for a way to learn about ownership semantics in more detail, it + /// is recommended to look at IDE options available to you to highlight types, lifetimes + /// and reference semantics in your code. The available tooling would expose these things + /// in a general way even outside of the various pattern matching mechanics. Of course + /// this lint can still be used to highlight areas of interest and ensure a good understanding + /// of ownership semantics. + /// + /// ### Why is this bad? + /// It isn't bad in general. But in some contexts it can be desirable + /// because it increases ownership hints in the code, and will guard against some changes + /// in ownership. + /// + /// ### Example + /// This example shows the basic adjustments necessary to satisfy the lint. Note how + /// the matched expression is explicitly dereferenced with `*` and the `inner` variable + /// is bound to a shared borrow via `ref inner`. + /// + /// ```rust,ignore + /// // Bad + /// let value = &Some(Box::new(23)); + /// match value { + /// Some(inner) => println!("{}", inner), + /// None => println!("none"), + /// } + /// + /// // Good + /// let value = &Some(Box::new(23)); + /// match *value { + /// Some(ref inner) => println!("{}", inner), + /// None => println!("none"), + /// } + /// ``` + /// + /// The following example demonstrates one of the advantages of the more verbose style. + /// Note how the second version uses `ref mut a` to explicitly declare `a` a shared mutable + /// borrow, while `b` is simply taken by value. This ensures that the loop body cannot + /// accidentally modify the wrong part of the structure. + /// + /// ```rust,ignore + /// // Bad + /// let mut values = vec![(2, 3), (3, 4)]; + /// for (a, b) in &mut values { + /// *a += *b; + /// } + /// + /// // Good + /// let mut values = vec![(2, 3), (3, 4)]; + /// for &mut (ref mut a, b) in &mut values { + /// *a += b; + /// } + /// ``` + #[clippy::version = "1.47.0"] + pub PATTERN_TYPE_MISMATCH, + restriction, + "type of pattern does not match the expression type" +} + +declare_lint_pass!(PatternTypeMismatch => [PATTERN_TYPE_MISMATCH]); + +impl<'tcx> LateLintPass<'tcx> for PatternTypeMismatch { + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + if let StmtKind::Local(local) = stmt.kind { + if in_external_macro(cx.sess(), local.pat.span) { + return; + } + let deref_possible = match local.source { + LocalSource::Normal => DerefPossible::Possible, + _ => DerefPossible::Impossible, + }; + apply_lint(cx, local.pat, deref_possible); + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Match(_, arms, _) = expr.kind { + for arm in arms { + let pat = &arm.pat; + if apply_lint(cx, pat, DerefPossible::Possible) { + break; + } + } + } + if let ExprKind::Let(Let { pat, .. }) = expr.kind { + apply_lint(cx, pat, DerefPossible::Possible); + } + } + + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + _: intravisit::FnKind<'tcx>, + _: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + _: Span, + _: HirId, + ) { + for param in body.params { + apply_lint(cx, param.pat, DerefPossible::Impossible); + } + } +} + +#[derive(Debug, Clone, Copy)] +enum DerefPossible { + Possible, + Impossible, +} + +fn apply_lint<'tcx>(cx: &LateContext<'tcx>, pat: &Pat<'_>, deref_possible: DerefPossible) -> bool { + let maybe_mismatch = find_first_mismatch(cx, pat); + if let Some((span, mutability, level)) = maybe_mismatch { + span_lint_and_help( + cx, + PATTERN_TYPE_MISMATCH, + span, + "type of pattern does not match the expression type", + None, + &format!( + "{}explicitly match against a `{}` pattern and adjust the enclosed variable bindings", + match (deref_possible, level) { + (DerefPossible::Possible, Level::Top) => "use `*` to dereference the match expression or ", + _ => "", + }, + match mutability { + Mutability::Mut => "&mut _", + Mutability::Not => "&_", + }, + ), + ); + true + } else { + false + } +} + +#[derive(Debug, Copy, Clone)] +enum Level { + Top, + Lower, +} + +fn find_first_mismatch<'tcx>(cx: &LateContext<'tcx>, pat: &Pat<'_>) -> Option<(Span, Mutability, Level)> { + let mut result = None; + pat.walk(|p| { + if result.is_some() { + return false; + } + if in_external_macro(cx.sess(), p.span) { + return true; + } + let adjust_pat = match p.kind { + PatKind::Or([p, ..]) => p, + _ => p, + }; + if let Some(adjustments) = cx.typeck_results().pat_adjustments().get(adjust_pat.hir_id) { + if let [first, ..] = **adjustments { + if let ty::Ref(.., mutability) = *first.kind() { + let level = if p.hir_id == pat.hir_id { + Level::Top + } else { + Level::Lower + }; + result = Some((p.span, mutability, level)); + } + } + } + result.is_none() + }); + result +} diff --git a/src/tools/clippy/clippy_lints/src/precedence.rs b/src/tools/clippy/clippy_lints/src/precedence.rs new file mode 100644 index 000000000..cc0533c9f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/precedence.rs @@ -0,0 +1,161 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use if_chain::if_chain; +use rustc_ast::ast::{BinOpKind, Expr, ExprKind, LitKind, UnOp}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; + +const ALLOWED_ODD_FUNCTIONS: [&str; 14] = [ + "asin", + "asinh", + "atan", + "atanh", + "cbrt", + "fract", + "round", + "signum", + "sin", + "sinh", + "tan", + "tanh", + "to_degrees", + "to_radians", +]; + +declare_clippy_lint! { + /// ### What it does + /// Checks for operations where precedence may be unclear + /// and suggests to add parentheses. Currently it catches the following: + /// * mixed usage of arithmetic and bit shifting/combining operators without + /// parentheses + /// * a "negative" numeric literal (which is really a unary `-` followed by a + /// numeric literal) + /// followed by a method call + /// + /// ### Why is this bad? + /// Not everyone knows the precedence of those operators by + /// heart, so expressions like these may trip others trying to reason about the + /// code. + /// + /// ### Example + /// * `1 << 2 + 3` equals 32, while `(1 << 2) + 3` equals 7 + /// * `-1i32.abs()` equals -1, while `(-1i32).abs()` equals 1 + #[clippy::version = "pre 1.29.0"] + pub PRECEDENCE, + complexity, + "operations where precedence may be unclear" +} + +declare_lint_pass!(Precedence => [PRECEDENCE]); + +impl EarlyLintPass for Precedence { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if expr.span.from_expansion() { + return; + } + + if let ExprKind::Binary(Spanned { node: op, .. }, ref left, ref right) = expr.kind { + let span_sugg = |expr: &Expr, sugg, appl| { + span_lint_and_sugg( + cx, + PRECEDENCE, + expr.span, + "operator precedence can trip the unwary", + "consider parenthesizing your expression", + sugg, + appl, + ); + }; + + if !is_bit_op(op) { + return; + } + let mut applicability = Applicability::MachineApplicable; + match (is_arith_expr(left), is_arith_expr(right)) { + (true, true) => { + let sugg = format!( + "({}) {} ({})", + snippet_with_applicability(cx, left.span, "..", &mut applicability), + op.to_string(), + snippet_with_applicability(cx, right.span, "..", &mut applicability) + ); + span_sugg(expr, sugg, applicability); + }, + (true, false) => { + let sugg = format!( + "({}) {} {}", + snippet_with_applicability(cx, left.span, "..", &mut applicability), + op.to_string(), + snippet_with_applicability(cx, right.span, "..", &mut applicability) + ); + span_sugg(expr, sugg, applicability); + }, + (false, true) => { + let sugg = format!( + "{} {} ({})", + snippet_with_applicability(cx, left.span, "..", &mut applicability), + op.to_string(), + snippet_with_applicability(cx, right.span, "..", &mut applicability) + ); + span_sugg(expr, sugg, applicability); + }, + (false, false) => (), + } + } + + if let ExprKind::Unary(UnOp::Neg, operand) = &expr.kind { + let mut arg = operand; + + let mut all_odd = true; + while let ExprKind::MethodCall(path_segment, args, _) = &arg.kind { + let path_segment_str = path_segment.ident.name.as_str(); + all_odd &= ALLOWED_ODD_FUNCTIONS + .iter() + .any(|odd_function| **odd_function == *path_segment_str); + arg = args.first().expect("A method always has a receiver."); + } + + if_chain! { + if !all_odd; + if let ExprKind::Lit(lit) = &arg.kind; + if let LitKind::Int(..) | LitKind::Float(..) = &lit.kind; + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + PRECEDENCE, + expr.span, + "unary minus has lower precedence than method call", + "consider adding parentheses to clarify your intent", + format!( + "-({})", + snippet_with_applicability(cx, operand.span, "..", &mut applicability) + ), + applicability, + ); + } + } + } + } +} + +fn is_arith_expr(expr: &Expr) -> bool { + match expr.kind { + ExprKind::Binary(Spanned { node: op, .. }, _, _) => is_arith_op(op), + _ => false, + } +} + +#[must_use] +fn is_bit_op(op: BinOpKind) -> bool { + use rustc_ast::ast::BinOpKind::{BitAnd, BitOr, BitXor, Shl, Shr}; + matches!(op, BitXor | BitAnd | BitOr | Shl | Shr) +} + +#[must_use] +fn is_arith_op(op: BinOpKind) -> bool { + use rustc_ast::ast::BinOpKind::{Add, Div, Mul, Rem, Sub}; + matches!(op, Add | Sub | Mul | Div | Rem) +} diff --git a/src/tools/clippy/clippy_lints/src/ptr.rs b/src/tools/clippy/clippy_lints/src/ptr.rs new file mode 100644 index 000000000..3c5ea2d94 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/ptr.rs @@ -0,0 +1,684 @@ +//! Checks for usage of `&Vec[_]` and `&String`. + +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then, span_lint_hir_and_then}; +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 if_chain::if_chain; +use rustc_errors::{Applicability, MultiSpan}; +use rustc_hir::def_id::DefId; +use rustc_hir::hir_id::HirIdMap; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_hir::{ + self as hir, AnonConst, BinOpKind, BindingAnnotation, Body, Expr, ExprKind, FnRetTy, FnSig, GenericArg, + ImplItemKind, ItemKind, Lifetime, LifetimeName, Mutability, Node, Param, ParamName, PatKind, QPath, TraitFn, + TraitItem, TraitItemKind, TyKind, Unsafety, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter; +use rustc_middle::ty::{self, Ty}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::sym; +use rustc_span::symbol::Symbol; +use std::fmt; +use std::iter; + +declare_clippy_lint! { + /// ### What it does + /// This lint checks for function arguments of type `&String`, `&Vec`, + /// `&PathBuf`, and `Cow<_>`. It will also suggest you replace `.clone()` calls + /// with the appropriate `.to_owned()`/`to_string()` calls. + /// + /// ### Why is this bad? + /// Requiring the argument to be of the specific size + /// makes the function less useful for no benefit; slices in the form of `&[T]` + /// or `&str` usually suffice and can be obtained from other types, too. + /// + /// ### Known problems + /// There may be `fn(&Vec)`-typed references pointing to your function. + /// If you have them, you will get a compiler error after applying this lint's + /// suggestions. You then have the choice to undo your changes or change the + /// type of the reference. + /// + /// Note that if the function is part of your public interface, there may be + /// other crates referencing it, of which you may not be aware. Carefully + /// deprecate the function before applying the lint suggestions in this case. + /// + /// ### Example + /// ```ignore + /// fn foo(&Vec<u32>) { .. } + /// ``` + /// + /// Use instead: + /// ```ignore + /// fn foo(&[u32]) { .. } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub PTR_ARG, + style, + "fn arguments of the type `&Vec<...>` or `&String`, suggesting to use `&[...]` or `&str` instead, respectively" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint checks for equality comparisons with `ptr::null` + /// + /// ### Why is this bad? + /// It's easier and more readable to use the inherent + /// `.is_null()` + /// method instead + /// + /// ### Example + /// ```rust,ignore + /// use std::ptr; + /// + /// if x == ptr::null { + /// // .. + /// } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// if x.is_null() { + /// // .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CMP_NULL, + style, + "comparing a pointer to a null pointer, suggesting to use `.is_null()` instead" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint checks for functions that take immutable references and return + /// mutable ones. This will not trigger if no unsafe code exists as there + /// are multiple safe functions which will do this transformation + /// + /// To be on the conservative side, if there's at least one mutable + /// reference with the output lifetime, this lint will not trigger. + /// + /// ### Why is this bad? + /// Creating a mutable reference which can be repeatably derived from an + /// immutable reference is unsound as it allows creating multiple live + /// mutable references to the same object. + /// + /// This [error](https://github.com/rust-lang/rust/issues/39465) actually + /// lead to an interim Rust release 1.15.1. + /// + /// ### Known problems + /// This pattern is used by memory allocators to allow allocating multiple + /// objects while returning mutable references to each one. So long as + /// different mutable references are returned each time such a function may + /// be safe. + /// + /// ### Example + /// ```ignore + /// fn foo(&Foo) -> &mut Bar { .. } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MUT_FROM_REF, + correctness, + "fns that create mutable refs from immutable ref args" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint checks for invalid usages of `ptr::null`. + /// + /// ### Why is this bad? + /// This causes undefined behavior. + /// + /// ### Example + /// ```ignore + /// // Undefined behavior + /// unsafe { std::slice::from_raw_parts(ptr::null(), 0); } + /// ``` + /// + /// Use instead: + /// ```ignore + /// unsafe { std::slice::from_raw_parts(NonNull::dangling().as_ptr(), 0); } + /// ``` + #[clippy::version = "1.53.0"] + pub INVALID_NULL_PTR_USAGE, + correctness, + "invalid usage of a null pointer, suggesting `NonNull::dangling()` instead" +} + +declare_lint_pass!(Ptr => [PTR_ARG, CMP_NULL, MUT_FROM_REF, INVALID_NULL_PTR_USAGE]); + +impl<'tcx> LateLintPass<'tcx> for Ptr { + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) { + if let TraitItemKind::Fn(sig, trait_method) = &item.kind { + if matches!(trait_method, TraitFn::Provided(_)) { + // Handled by check body. + return; + } + + check_mut_from_ref(cx, sig, None); + for arg in check_fn_args( + cx, + cx.tcx.fn_sig(item.def_id).skip_binder().inputs(), + sig.decl.inputs, + &[], + ) + .filter(|arg| arg.mutability() == Mutability::Not) + { + span_lint_hir_and_then(cx, PTR_ARG, arg.emission_id, arg.span, &arg.build_msg(), |diag| { + diag.span_suggestion( + arg.span, + "change this to", + format!("{}{}", arg.ref_prefix, arg.deref_ty.display(cx)), + Applicability::Unspecified, + ); + }); + } + } + } + + fn check_body(&mut self, cx: &LateContext<'tcx>, body: &'tcx Body<'_>) { + let hir = cx.tcx.hir(); + let mut parents = hir.parent_iter(body.value.hir_id); + let (item_id, sig, is_trait_item) = match parents.next() { + Some((_, Node::Item(i))) => { + if let ItemKind::Fn(sig, ..) = &i.kind { + (i.def_id, sig, false) + } else { + return; + } + }, + Some((_, Node::ImplItem(i))) => { + if !matches!(parents.next(), + Some((_, Node::Item(i))) if matches!(&i.kind, ItemKind::Impl(i) if i.of_trait.is_none()) + ) { + return; + } + if let ImplItemKind::Fn(sig, _) = &i.kind { + (i.def_id, sig, false) + } else { + return; + } + }, + Some((_, Node::TraitItem(i))) => { + if let TraitItemKind::Fn(sig, _) = &i.kind { + (i.def_id, sig, true) + } else { + return; + } + }, + _ => return, + }; + + check_mut_from_ref(cx, sig, Some(body)); + let decl = sig.decl; + let sig = cx.tcx.fn_sig(item_id).skip_binder(); + let lint_args: Vec<_> = check_fn_args(cx, sig.inputs(), decl.inputs, body.params) + .filter(|arg| !is_trait_item || arg.mutability() == Mutability::Not) + .collect(); + let results = check_ptr_arg_usage(cx, body, &lint_args); + + for (result, args) in results.iter().zip(lint_args.iter()).filter(|(r, _)| !r.skip) { + span_lint_hir_and_then(cx, PTR_ARG, args.emission_id, args.span, &args.build_msg(), |diag| { + diag.multipart_suggestion( + "change this to", + iter::once((args.span, format!("{}{}", args.ref_prefix, args.deref_ty.display(cx)))) + .chain(result.replacements.iter().map(|r| { + ( + r.expr_span, + format!("{}{}", snippet_opt(cx, r.self_span).unwrap(), r.replacement), + ) + })) + .collect(), + Applicability::Unspecified, + ); + }); + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Binary(ref op, l, r) = expr.kind { + if (op.node == BinOpKind::Eq || op.node == BinOpKind::Ne) && (is_null_path(cx, l) || is_null_path(cx, r)) { + span_lint( + cx, + CMP_NULL, + expr.span, + "comparing with null is better expressed by the `.is_null()` method", + ); + } + } else { + check_invalid_ptr_usage(cx, expr); + } + } +} + +fn check_invalid_ptr_usage<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // (fn_path, arg_indices) - `arg_indices` are the `arg` positions where null would cause U.B. + const INVALID_NULL_PTR_USAGE_TABLE: [(&[&str], &[usize]); 16] = [ + (&paths::SLICE_FROM_RAW_PARTS, &[0]), + (&paths::SLICE_FROM_RAW_PARTS_MUT, &[0]), + (&paths::PTR_COPY, &[0, 1]), + (&paths::PTR_COPY_NONOVERLAPPING, &[0, 1]), + (&paths::PTR_READ, &[0]), + (&paths::PTR_READ_UNALIGNED, &[0]), + (&paths::PTR_READ_VOLATILE, &[0]), + (&paths::PTR_REPLACE, &[0]), + (&paths::PTR_SLICE_FROM_RAW_PARTS, &[0]), + (&paths::PTR_SLICE_FROM_RAW_PARTS_MUT, &[0]), + (&paths::PTR_SWAP, &[0, 1]), + (&paths::PTR_SWAP_NONOVERLAPPING, &[0, 1]), + (&paths::PTR_WRITE, &[0]), + (&paths::PTR_WRITE_UNALIGNED, &[0]), + (&paths::PTR_WRITE_VOLATILE, &[0]), + (&paths::PTR_WRITE_BYTES, &[0]), + ]; + + if_chain! { + if let ExprKind::Call(fun, args) = expr.kind; + if let ExprKind::Path(ref qpath) = fun.kind; + if let Some(fun_def_id) = cx.qpath_res(qpath, fun.hir_id).opt_def_id(); + let fun_def_path = cx.get_def_path(fun_def_id).into_iter().map(Symbol::to_ident_string).collect::<Vec<_>>(); + if let Some(&(_, arg_indices)) = INVALID_NULL_PTR_USAGE_TABLE + .iter() + .find(|&&(fn_path, _)| fn_path == fun_def_path); + then { + for &arg_idx in arg_indices { + if let Some(arg) = args.get(arg_idx).filter(|arg| is_null_path(cx, arg)) { + span_lint_and_sugg( + cx, + INVALID_NULL_PTR_USAGE, + arg.span, + "pointer must be non-null", + "change this to", + "core::ptr::NonNull::dangling().as_ptr()".to_string(), + Applicability::MachineApplicable, + ); + } + } + } + } +} + +#[derive(Default)] +struct PtrArgResult { + skip: bool, + replacements: Vec<PtrArgReplacement>, +} + +struct PtrArgReplacement { + expr_span: Span, + self_span: Span, + replacement: &'static str, +} + +struct PtrArg<'tcx> { + idx: usize, + emission_id: hir::HirId, + span: Span, + ty_did: DefId, + ty_name: Symbol, + method_renames: &'static [(&'static str, &'static str)], + ref_prefix: RefPrefix, + deref_ty: DerefTy<'tcx>, +} +impl PtrArg<'_> { + fn build_msg(&self) -> String { + format!( + "writing `&{}{}` instead of `&{}{}` involves a new object where a slice will do", + self.ref_prefix.mutability.prefix_str(), + self.ty_name, + self.ref_prefix.mutability.prefix_str(), + self.deref_ty.argless_str(), + ) + } + + fn mutability(&self) -> Mutability { + self.ref_prefix.mutability + } +} + +struct RefPrefix { + lt: LifetimeName, + mutability: Mutability, +} +impl fmt::Display for RefPrefix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use fmt::Write; + f.write_char('&')?; + match self.lt { + LifetimeName::Param(_, ParamName::Plain(name)) => { + name.fmt(f)?; + f.write_char(' ')?; + }, + LifetimeName::Infer => f.write_str("'_ ")?, + LifetimeName::Static => f.write_str("'static ")?, + _ => (), + } + f.write_str(self.mutability.prefix_str()) + } +} + +struct DerefTyDisplay<'a, 'tcx>(&'a LateContext<'tcx>, &'a DerefTy<'tcx>); +impl fmt::Display for DerefTyDisplay<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use std::fmt::Write; + match self.1 { + DerefTy::Str => f.write_str("str"), + DerefTy::Path => f.write_str("Path"), + DerefTy::Slice(hir_ty, ty) => { + f.write_char('[')?; + match hir_ty.and_then(|s| snippet_opt(self.0, s)) { + Some(s) => f.write_str(&s)?, + None => ty.fmt(f)?, + } + f.write_char(']') + }, + } + } +} + +enum DerefTy<'tcx> { + Str, + Path, + Slice(Option<Span>, Ty<'tcx>), +} +impl<'tcx> DerefTy<'tcx> { + fn argless_str(&self) -> &'static str { + match *self { + Self::Str => "str", + Self::Path => "Path", + Self::Slice(..) => "[_]", + } + } + + fn display<'a>(&'a self, cx: &'a LateContext<'tcx>) -> DerefTyDisplay<'a, 'tcx> { + DerefTyDisplay(cx, self) + } +} + +fn check_fn_args<'cx, 'tcx: 'cx>( + cx: &'cx LateContext<'tcx>, + tys: &'tcx [Ty<'tcx>], + hir_tys: &'tcx [hir::Ty<'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::Rptr(lt, ref ty) = hir_ty.kind; + if 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 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) => ( + [("clone", ".to_owned()")].as_slice(), + DerefTy::Slice( + name.args + .and_then(|args| args.args.first()) + .and_then(|arg| if let GenericArg::Type(ty) = arg { + Some(ty.span) + } else { + None + }), + substs.type_at(0), + ), + ), + Some(sym::String) => ( + [("clone", ".to_owned()"), ("as_str", "")].as_slice(), + DerefTy::Str, + ), + Some(sym::PathBuf) => ( + [("clone", ".to_path_buf()"), ("as_path", "")].as_slice(), + DerefTy::Path, + ), + Some(sym::Cow) if mutability == Mutability::Not => { + let ty_name = name.args + .and_then(|args| { + args.args.iter().find_map(|a| match a { + GenericArg::Type(x) => Some(x), + _ => 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!("&{}{}", mutability.prefix_str(), ty_name), + Applicability::Unspecified, + ); + } + ); + return None; + }, + _ => return None, + }; + return Some(PtrArg { + idx: i, + emission_id, + span: hir_ty.span, + ty_did: adt.did(), + ty_name: name.ident.name, + method_renames, + ref_prefix: RefPrefix { + lt: lt.name, + mutability, + }, + deref_ty, + }); + } + } + None + }) +} + +fn check_mut_from_ref<'tcx>(cx: &LateContext<'tcx>, sig: &FnSig<'_>, body: Option<&'tcx Body<'_>>) { + if let FnRetTy::Return(ty) = sig.decl.output + && let Some((out, Mutability::Mut, _)) = get_rptr_lm(ty) + { + let out_region = cx.tcx.named_region(out.hir_id); + let args: Option<Vec<_>> = sig + .decl + .inputs + .iter() + .filter_map(get_rptr_lm) + .filter(|&(lt, _, _)| cx.tcx.named_region(lt.hir_id) == out_region) + .map(|(_, mutability, span)| (mutability == Mutability::Not).then_some(span)) + .collect(); + if let Some(args) = args + && !args.is_empty() + && body.map_or(true, |body| { + sig.header.unsafety == Unsafety::Unsafe || contains_unsafe_block(cx, &body.value) + }) + { + span_lint_and_then( + cx, + MUT_FROM_REF, + ty.span, + "mutable borrow from immutable input(s)", + |diag| { + let ms = MultiSpan::from_spans(args); + diag.span_note(ms, "immutable borrow here"); + }, + ); + } + } +} + +#[expect(clippy::too_many_lines)] +fn check_ptr_arg_usage<'tcx>(cx: &LateContext<'tcx>, body: &'tcx Body<'_>, args: &[PtrArg<'tcx>]) -> Vec<PtrArgResult> { + struct V<'cx, 'tcx> { + cx: &'cx LateContext<'tcx>, + /// Map from a local id to which argument it came from (index into `Self::args` and + /// `Self::results`) + bindings: HirIdMap<usize>, + /// The arguments being checked. + args: &'cx [PtrArg<'tcx>], + /// The results for each argument (len should match args.len) + results: Vec<PtrArgResult>, + /// The number of arguments which can't be linted. Used to return early. + skip_count: usize, + } + impl<'tcx> Visitor<'tcx> for V<'_, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_anon_const(&mut self, _: &'tcx AnonConst) {} + + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + if self.skip_count == self.args.len() { + return; + } + + // Check if this is local we care about + let args_idx = match path_to_local(e).and_then(|id| self.bindings.get(&id)) { + Some(&i) => i, + None => return walk_expr(self, e), + }; + let args = &self.args[args_idx]; + let result = &mut self.results[args_idx]; + + // Helper function to handle early returns. + let mut set_skip_flag = || { + if !result.skip { + self.skip_count += 1; + } + result.skip = true; + }; + + match get_expr_use_or_unification_node(self.cx.tcx, e) { + Some((Node::Stmt(_), _)) => (), + Some((Node::Local(l), _)) => { + // Only trace simple bindings. e.g `let x = y;` + if let PatKind::Binding(BindingAnnotation::Unannotated, id, _, None) = l.pat.kind { + self.bindings.insert(id, args_idx); + } else { + set_skip_flag(); + } + }, + Some((Node::Expr(e), child_id)) => match e.kind { + ExprKind::Call(f, expr_args) => { + let i = expr_args.iter().position(|arg| arg.hir_id == child_id).unwrap_or(0); + if expr_sig(self.cx, f).and_then(|sig| sig.input(i)).map_or(true, |ty| { + match *ty.skip_binder().peel_refs().kind() { + ty::Param(_) => true, + ty::Adt(def, _) => def.did() == args.ty_did, + _ => false, + } + }) { + // Passed to a function taking the non-dereferenced type. + set_skip_flag(); + } + }, + ExprKind::MethodCall(name, expr_args @ [self_arg, ..], _) => { + let i = expr_args.iter().position(|arg| arg.hir_id == child_id).unwrap_or(0); + if i == 0 { + // Check if the method can be renamed. + let name = name.ident.as_str(); + if let Some((_, replacement)) = args.method_renames.iter().find(|&&(x, _)| x == name) { + result.replacements.push(PtrArgReplacement { + expr_span: e.span, + self_span: self_arg.span, + replacement, + }); + return; + } + } + + let id = if let Some(x) = self.cx.typeck_results().type_dependent_def_id(e.hir_id) { + x + } else { + set_skip_flag(); + return; + }; + + match *self.cx.tcx.fn_sig(id).skip_binder().inputs()[i].peel_refs().kind() { + ty::Param(_) => { + set_skip_flag(); + }, + // If the types match check for methods which exist on both types. e.g. `Vec::len` and + // `slice::len` + ty::Adt(def, _) if def.did() == args.ty_did => { + set_skip_flag(); + }, + _ => (), + } + }, + // Indexing is fine for currently supported types. + ExprKind::Index(e, _) if e.hir_id == child_id => (), + _ => set_skip_flag(), + }, + _ => set_skip_flag(), + } + } + } + + let mut skip_count = 0; + let mut results = args.iter().map(|_| PtrArgResult::default()).collect::<Vec<_>>(); + let mut v = V { + cx, + bindings: args + .iter() + .enumerate() + .filter_map(|(i, arg)| { + let param = &body.params[arg.idx]; + match param.pat.kind { + PatKind::Binding(BindingAnnotation::Unannotated, id, _, None) + if !is_lint_allowed(cx, PTR_ARG, param.hir_id) => + { + Some((id, i)) + }, + _ => { + skip_count += 1; + results[i].skip = true; + None + }, + } + }) + .collect(), + args, + results, + skip_count, + }; + v.visit_expr(&body.value); + v.results +} + +fn get_rptr_lm<'tcx>(ty: &'tcx hir::Ty<'tcx>) -> Option<(&'tcx Lifetime, Mutability, Span)> { + if let TyKind::Rptr(ref lt, ref m) = ty.kind { + Some((lt, m.mutbl, ty.span)) + } else { + None + } +} + +fn is_null_path(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + if let ExprKind::Call(pathexp, []) = expr.kind { + path_def_id(cx, pathexp).map_or(false, |id| { + matches!(cx.tcx.get_diagnostic_name(id), Some(sym::ptr_null | sym::ptr_null_mut)) + }) + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/ptr_offset_with_cast.rs b/src/tools/clippy/clippy_lints/src/ptr_offset_with_cast.rs new file mode 100644 index 000000000..b907f38af --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/ptr_offset_with_cast.rs @@ -0,0 +1,153 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; +use clippy_utils::source::snippet_opt; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; +use std::fmt; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of the `offset` pointer method with a `usize` casted to an + /// `isize`. + /// + /// ### Why is this bad? + /// If we’re always increasing the pointer address, we can avoid the numeric + /// cast by using the `add` method instead. + /// + /// ### Example + /// ```rust + /// let vec = vec![b'a', b'b', b'c']; + /// let ptr = vec.as_ptr(); + /// let offset = 1_usize; + /// + /// unsafe { + /// ptr.offset(offset as isize); + /// } + /// ``` + /// + /// Could be written: + /// + /// ```rust + /// let vec = vec![b'a', b'b', b'c']; + /// let ptr = vec.as_ptr(); + /// let offset = 1_usize; + /// + /// unsafe { + /// ptr.add(offset); + /// } + /// ``` + #[clippy::version = "1.30.0"] + pub PTR_OFFSET_WITH_CAST, + complexity, + "unneeded pointer offset cast" +} + +declare_lint_pass!(PtrOffsetWithCast => [PTR_OFFSET_WITH_CAST]); + +impl<'tcx> LateLintPass<'tcx> for PtrOffsetWithCast { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // Check if the expressions is a ptr.offset or ptr.wrapping_offset method call + let (receiver_expr, arg_expr, method) = match expr_as_ptr_offset_call(cx, expr) { + Some(call_arg) => call_arg, + None => return, + }; + + // Check if the argument to the method call is a cast from usize + let cast_lhs_expr = match expr_as_cast_from_usize(cx, arg_expr) { + Some(cast_lhs_expr) => cast_lhs_expr, + None => return, + }; + + let msg = format!("use of `{}` with a `usize` casted to an `isize`", method); + if let Some(sugg) = build_suggestion(cx, method, receiver_expr, cast_lhs_expr) { + span_lint_and_sugg( + cx, + PTR_OFFSET_WITH_CAST, + expr.span, + &msg, + "try", + sugg, + Applicability::MachineApplicable, + ); + } else { + span_lint(cx, PTR_OFFSET_WITH_CAST, expr.span, &msg); + } + } +} + +// If the given expression is a cast from a usize, return the lhs of the cast +fn expr_as_cast_from_usize<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { + if let ExprKind::Cast(cast_lhs_expr, _) = expr.kind { + if is_expr_ty_usize(cx, cast_lhs_expr) { + return Some(cast_lhs_expr); + } + } + None +} + +// If the given expression is a ptr::offset or ptr::wrapping_offset method call, return the +// receiver, the arg of the method call, and the method. +fn expr_as_ptr_offset_call<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, +) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>, Method)> { + if let ExprKind::MethodCall(path_segment, [arg_0, arg_1, ..], _) = &expr.kind { + if is_expr_ty_raw_ptr(cx, arg_0) { + if path_segment.ident.name == sym::offset { + return Some((arg_0, arg_1, Method::Offset)); + } + if path_segment.ident.name == sym!(wrapping_offset) { + return Some((arg_0, arg_1, Method::WrappingOffset)); + } + } + } + None +} + +// Is the type of the expression a usize? +fn is_expr_ty_usize<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> bool { + cx.typeck_results().expr_ty(expr) == cx.tcx.types.usize +} + +// Is the type of the expression a raw pointer? +fn is_expr_ty_raw_ptr<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> bool { + cx.typeck_results().expr_ty(expr).is_unsafe_ptr() +} + +fn build_suggestion<'tcx>( + cx: &LateContext<'tcx>, + method: Method, + receiver_expr: &Expr<'_>, + cast_lhs_expr: &Expr<'_>, +) -> Option<String> { + let receiver = snippet_opt(cx, receiver_expr.span)?; + let cast_lhs = snippet_opt(cx, cast_lhs_expr.span)?; + Some(format!("{}.{}({})", receiver, method.suggestion(), cast_lhs)) +} + +#[derive(Copy, Clone)] +enum Method { + Offset, + WrappingOffset, +} + +impl Method { + #[must_use] + fn suggestion(self) -> &'static str { + match self { + Self::Offset => "add", + Self::WrappingOffset => "wrapping_add", + } + } +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Offset => write!(f, "offset"), + Self::WrappingOffset => write!(f, "wrapping_offset"), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/pub_use.rs b/src/tools/clippy/clippy_lints/src/pub_use.rs new file mode 100644 index 000000000..9d2b0cedb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/pub_use.rs @@ -0,0 +1,56 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{Item, ItemKind, VisibilityKind}; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// + /// Restricts the usage of `pub use ...` + /// + /// ### Why is this bad? + /// + /// `pub use` is usually fine, but a project may wish to limit `pub use` instances to prevent + /// unintentional exports or to encourage placing exported items directly in public modules + /// + /// ### Example + /// ```rust + /// pub mod outer { + /// mod inner { + /// pub struct Test {} + /// } + /// pub use inner::Test; + /// } + /// + /// use outer::Test; + /// ``` + /// Use instead: + /// ```rust + /// pub mod outer { + /// pub struct Test {} + /// } + /// + /// use outer::Test; + /// ``` + #[clippy::version = "1.62.0"] + pub PUB_USE, + restriction, + "restricts the usage of `pub use`" +} +declare_lint_pass!(PubUse => [PUB_USE]); + +impl EarlyLintPass for PubUse { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if let ItemKind::Use(_) = item.kind && + let VisibilityKind::Public = item.vis.kind { + span_lint_and_help( + cx, + PUB_USE, + item.span, + "using `pub use`", + None, + "move the exported item to a public module instead", + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/question_mark.rs b/src/tools/clippy/clippy_lints/src/question_mark.rs new file mode 100644 index 000000000..fd0a53839 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/question_mark.rs @@ -0,0 +1,231 @@ +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, is_else_clause, is_lang_ctor, path_to_local, path_to_local_id, peel_blocks, + peel_blocks_with_stmt, +}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk}; +use rustc_hir::{BindingAnnotation, 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_span::{sym, symbol::Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for expressions that could be replaced by the question mark operator. + /// + /// ### Why is this bad? + /// Question mark usage is more idiomatic. + /// + /// ### Example + /// ```ignore + /// if option.is_none() { + /// return None; + /// } + /// ``` + /// + /// Could be written: + /// + /// ```ignore + /// option?; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub QUESTION_MARK, + style, + "checks for expressions that could be replaced by the question mark operator" +} + +declare_lint_pass!(QuestionMark => [QUESTION_MARK]); + +enum IfBlockType<'hir> { + /// An `if x.is_xxx() { a } else { b } ` expression. + /// + /// Contains: caller (x), caller_type, call_sym (is_xxx), if_then (a), if_else (b) + IfIs( + &'hir Expr<'hir>, + Ty<'hir>, + Symbol, + &'hir Expr<'hir>, + Option<&'hir Expr<'hir>>, + ), + /// An `if let Xxx(a) = b { c } else { d }` expression. + /// + /// Contains: let_pat_qpath (Xxx), let_pat_type, let_pat_sym (a), let_expr (b), if_then (c), + /// if_else (d) + IfLet( + &'hir QPath<'hir>, + Ty<'hir>, + Symbol, + &'hir Expr<'hir>, + &'hir Expr<'hir>, + Option<&'hir Expr<'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, args, _) = &cond.kind; + if let Some(caller) = args.get(0); + 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.at(caller.span), 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], None) = let_pat.kind; + if let PatKind::Binding(annot, bind_id, ident, _) = field.kind; + let caller_ty = cx.typeck_results().expr_ty(let_expr); + let if_block = IfBlockType::IfLet(path1, 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 by_ref = matches!(annot, BindingAnnotation::Ref | BindingAnnotation::RefMut); + let requires_semi = matches!(get_parent_node(cx.tcx, expr.hir_id), Some(Node::Stmt(_))); + let sugg = format!( + "{}{}?{}", + receiver_str, + if by_ref { ".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, _) => { + // If the block could be identified as `if x.is_none()/is_err()`, + // we then only need to check the if_then return to see if it is none/err. + is_type_diagnostic_item(cx, caller_ty, smbl) + && expr_return_none_or_err(smbl, cx, if_then, caller, None) + && match smbl { + sym::Option => call_sym == sym!(is_none), + sym::Result => call_sym == sym!(is_err), + _ => false, + } + }, + IfBlockType::IfLet(qpath, let_expr_ty, let_pat_sym, let_expr, if_then, if_else) => { + is_type_diagnostic_item(cx, let_expr_ty, smbl) + && match smbl { + sym::Option => { + // We only need to check `if let Some(x) = option` not `if let None = option`, + // because the later one will be suggested as `if option.is_none()` thus causing conflict. + is_lang_ctor(cx, qpath, OptionSome) + && if_else.is_some() + && expr_return_none_or_err(smbl, cx, if_else.unwrap(), let_expr, None) + }, + sym::Result => { + (is_lang_ctor(cx, qpath, ResultOk) + && if_else.is_some() + && expr_return_none_or_err(smbl, cx, if_else.unwrap(), let_expr, Some(let_pat_sym))) + || is_lang_ctor(cx, qpath, ResultErr) + && expr_return_none_or_err(smbl, cx, if_then, let_expr, Some(let_pat_sym)) + }, + _ => false, + } + }, + } +} + +fn expr_return_none_or_err( + smbl: Symbol, + cx: &LateContext<'_>, + expr: &Expr<'_>, + cond_expr: &Expr<'_>, + err_sym: Option<Symbol>, +) -> bool { + match peel_blocks_with_stmt(expr).kind { + ExprKind::Ret(Some(ret_expr)) => expr_return_none_or_err(smbl, cx, ret_expr, cond_expr, err_sym), + ExprKind::Path(ref qpath) => match smbl { + sym::Option => is_lang_ctor(cx, qpath, OptionNone), + sym::Result => path_to_local(expr).is_some() && path_to_local(expr) == path_to_local(cond_expr), + _ => false, + }, + ExprKind::Call(call_expr, args_expr) => { + if_chain! { + if smbl == sym::Result; + if let ExprKind::Path(QPath::Resolved(_, path)) = &call_expr.kind; + if let Some(segment) = path.segments.first(); + if let Some(err_sym) = err_sym; + if let Some(arg) = args_expr.first(); + if let ExprKind::Path(QPath::Resolved(_, arg_path)) = &arg.kind; + if let Some(PathSegment { ident, .. }) = arg_path.segments.first(); + then { + return segment.ident.name == sym::Err && err_sym == ident.name; + } + } + false + }, + _ => false, + } +} + +impl<'tcx> LateLintPass<'tcx> for QuestionMark { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + check_is_none_or_err_and_early_return(cx, expr); + check_if_let_some_or_err_and_early_return(cx, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/ranges.rs b/src/tools/clippy/clippy_lints/src/ranges.rs new file mode 100644 index 000000000..547d4da81 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/ranges.rs @@ -0,0 +1,598 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::{snippet, snippet_opt, snippet_with_applicability}; +use clippy_utils::sugg::Sugg; +use clippy_utils::{get_parent_expr, in_constant, is_integer_const, meets_msrv, msrvs, path_to_local}; +use clippy_utils::{higher, SpanlessEq}; +use if_chain::if_chain; +use rustc_ast::ast::RangeLimits; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind, HirId, PathSegment, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::{Span, Spanned}; +use rustc_span::sym; +use std::cmp::Ordering; + +declare_clippy_lint! { + /// ### What it does + /// Checks for zipping a collection with the range of + /// `0.._.len()`. + /// + /// ### Why is this bad? + /// The code is better expressed with `.enumerate()`. + /// + /// ### Example + /// ```rust + /// # let x = vec![1]; + /// let _ = x.iter().zip(0..x.len()); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = vec![1]; + /// let _ = x.iter().enumerate(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub RANGE_ZIP_WITH_LEN, + complexity, + "zipping iterator with a range when `enumerate()` would do" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for exclusive ranges where 1 is added to the + /// upper bound, e.g., `x..(y+1)`. + /// + /// ### Why is this bad? + /// The code is more readable with an inclusive range + /// like `x..=y`. + /// + /// ### Known problems + /// Will add unnecessary pair of parentheses when the + /// expression is not wrapped in a pair but starts with an opening parenthesis + /// and ends with a closing one. + /// I.e., `let _ = (f()+1)..(f()+1)` results in `let _ = ((f()+1)..=f())`. + /// + /// Also in many cases, inclusive ranges are still slower to run than + /// exclusive ranges, because they essentially add an extra branch that + /// LLVM may fail to hoist out of the loop. + /// + /// This will cause a warning that cannot be fixed if the consumer of the + /// range only accepts a specific range type, instead of the generic + /// `RangeBounds` trait + /// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)). + /// + /// ### Example + /// ```rust + /// # let x = 0; + /// # let y = 1; + /// for i in x..(y+1) { + /// // .. + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = 0; + /// # let y = 1; + /// for i in x..=y { + /// // .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub RANGE_PLUS_ONE, + pedantic, + "`x..(y+1)` reads better as `x..=y`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for inclusive ranges where 1 is subtracted from + /// the upper bound, e.g., `x..=(y-1)`. + /// + /// ### Why is this bad? + /// The code is more readable with an exclusive range + /// like `x..y`. + /// + /// ### Known problems + /// This will cause a warning that cannot be fixed if + /// the consumer of the range only accepts a specific range type, instead of + /// the generic `RangeBounds` trait + /// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)). + /// + /// ### Example + /// ```rust + /// # let x = 0; + /// # let y = 1; + /// for i in x..=(y-1) { + /// // .. + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = 0; + /// # let y = 1; + /// for i in x..y { + /// // .. + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub RANGE_MINUS_ONE, + pedantic, + "`x..=(y-1)` reads better as `x..y`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for range expressions `x..y` where both `x` and `y` + /// are constant and `x` is greater or equal to `y`. + /// + /// ### Why is this bad? + /// Empty ranges yield no values so iterating them is a no-op. + /// Moreover, trying to use a reversed range to index a slice will panic at run-time. + /// + /// ### Example + /// ```rust,no_run + /// fn main() { + /// (10..=0).for_each(|x| println!("{}", x)); + /// + /// let arr = [1, 2, 3, 4, 5]; + /// let sub = &arr[3..1]; + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn main() { + /// (0..=10).rev().for_each(|x| println!("{}", x)); + /// + /// let arr = [1, 2, 3, 4, 5]; + /// let sub = &arr[1..3]; + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub REVERSED_EMPTY_RANGES, + correctness, + "reversing the limits of range expressions, resulting in empty ranges" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for expressions like `x >= 3 && x < 8` that could + /// be more readably expressed as `(3..8).contains(x)`. + /// + /// ### Why is this bad? + /// `contains` expresses the intent better and has less + /// failure modes (such as fencepost errors or using `||` instead of `&&`). + /// + /// ### Example + /// ```rust + /// // given + /// let x = 6; + /// + /// assert!(x >= 3 && x < 8); + /// ``` + /// Use instead: + /// ```rust + ///# let x = 6; + /// assert!((3..8).contains(&x)); + /// ``` + #[clippy::version = "1.49.0"] + pub MANUAL_RANGE_CONTAINS, + style, + "manually reimplementing {`Range`, `RangeInclusive`}`::contains`" +} + +pub struct Ranges { + msrv: Option<RustcVersion>, +} + +impl Ranges { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(Ranges => [ + RANGE_ZIP_WITH_LEN, + RANGE_PLUS_ONE, + RANGE_MINUS_ONE, + REVERSED_EMPTY_RANGES, + MANUAL_RANGE_CONTAINS, +]); + +impl<'tcx> LateLintPass<'tcx> for Ranges { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + match expr.kind { + ExprKind::MethodCall(path, args, _) => { + check_range_zip_with_len(cx, path, args, expr.span); + }, + ExprKind::Binary(ref op, l, r) => { + if meets_msrv(self.msrv, msrvs::RANGE_CONTAINS) { + check_possible_range_contains(cx, op.node, l, r, expr, expr.span); + } + }, + _ => {}, + } + + check_exclusive_range_plus_one(cx, expr); + check_inclusive_range_minus_one(cx, expr); + check_reversed_empty_range(cx, expr); + } + extract_msrv_attr!(LateContext); +} + +fn check_possible_range_contains( + cx: &LateContext<'_>, + op: BinOpKind, + left: &Expr<'_>, + right: &Expr<'_>, + expr: &Expr<'_>, + span: Span, +) { + if in_constant(cx, expr.hir_id) { + return; + } + + let combine_and = match op { + BinOpKind::And | BinOpKind::BitAnd => true, + BinOpKind::Or | BinOpKind::BitOr => false, + _ => return, + }; + // value, name, order (higher/lower), inclusiveness + if let (Some(l), Some(r)) = (check_range_bounds(cx, left), check_range_bounds(cx, right)) { + // we only lint comparisons on the same name and with different + // direction + if l.id != r.id || l.ord == r.ord { + return; + } + let ord = Constant::partial_cmp(cx.tcx, cx.typeck_results().expr_ty(l.expr), &l.val, &r.val); + if combine_and && ord == Some(r.ord) { + // order lower bound and upper bound + let (l_span, u_span, l_inc, u_inc) = if r.ord == Ordering::Less { + (l.val_span, r.val_span, l.inc, r.inc) + } else { + (r.val_span, l.val_span, r.inc, l.inc) + }; + // we only lint inclusive lower bounds + if !l_inc { + return; + } + let (range_type, range_op) = if u_inc { + ("RangeInclusive", "..=") + } else { + ("Range", "..") + }; + let mut applicability = Applicability::MachineApplicable; + let name = snippet_with_applicability(cx, l.name_span, "_", &mut applicability); + let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability); + let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability); + let space = if lo.ends_with('.') { " " } else { "" }; + span_lint_and_sugg( + cx, + MANUAL_RANGE_CONTAINS, + span, + &format!("manual `{}::contains` implementation", range_type), + "use", + format!("({}{}{}{}).contains(&{})", lo, space, range_op, hi, name), + applicability, + ); + } else if !combine_and && ord == Some(l.ord) { + // `!_.contains(_)` + // order lower bound and upper bound + let (l_span, u_span, l_inc, u_inc) = if l.ord == Ordering::Less { + (l.val_span, r.val_span, l.inc, r.inc) + } else { + (r.val_span, l.val_span, r.inc, l.inc) + }; + if l_inc { + return; + } + let (range_type, range_op) = if u_inc { + ("Range", "..") + } else { + ("RangeInclusive", "..=") + }; + let mut applicability = Applicability::MachineApplicable; + let name = snippet_with_applicability(cx, l.name_span, "_", &mut applicability); + let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability); + let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability); + let space = if lo.ends_with('.') { " " } else { "" }; + span_lint_and_sugg( + cx, + MANUAL_RANGE_CONTAINS, + span, + &format!("manual `!{}::contains` implementation", range_type), + "use", + format!("!({}{}{}{}).contains(&{})", lo, space, range_op, hi, name), + applicability, + ); + } + } + + // If the LHS is the same operator, we have to recurse to get the "real" RHS, since they have + // the same operator precedence + if_chain! { + if let ExprKind::Binary(ref lhs_op, _left, new_lhs) = left.kind; + if op == lhs_op.node; + let new_span = Span::new(new_lhs.span.lo(), right.span.hi(), expr.span.ctxt(), expr.span.parent()); + if let Some(snip) = &snippet_opt(cx, new_span); + // Do not continue if we have mismatched number of parens, otherwise the suggestion is wrong + if snip.matches('(').count() == snip.matches(')').count(); + then { + check_possible_range_contains(cx, op, new_lhs, right, expr, new_span); + } + } +} + +struct RangeBounds<'a> { + val: Constant, + expr: &'a Expr<'a>, + id: HirId, + name_span: Span, + val_span: Span, + ord: Ordering, + inc: bool, +} + +// 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>> { + if let ExprKind::Binary(ref op, l, r) = ex.kind { + let (inclusive, ordering) = match op.node { + BinOpKind::Gt => (false, Ordering::Greater), + BinOpKind::Ge => (true, Ordering::Greater), + BinOpKind::Lt => (false, Ordering::Less), + BinOpKind::Le => (true, Ordering::Less), + _ => return None, + }; + if let Some(id) = path_to_local(l) { + if let Some((c, _)) = constant(cx, cx.typeck_results(), r) { + return Some(RangeBounds { + val: c, + expr: r, + id, + name_span: l.span, + val_span: r.span, + ord: ordering, + inc: inclusive, + }); + } + } else if let Some(id) = path_to_local(r) { + if let Some((c, _)) = constant(cx, cx.typeck_results(), l) { + return Some(RangeBounds { + val: c, + expr: l, + id, + name_span: r.span, + val_span: l.span, + ord: ordering.reverse(), + inc: inclusive, + }); + } + } + } + None +} + +fn check_range_zip_with_len(cx: &LateContext<'_>, path: &PathSegment<'_>, args: &[Expr<'_>], span: Span) { + if_chain! { + if path.ident.as_str() == "zip"; + if let [iter, zip_arg] = args; + // `.iter()` call + if let ExprKind::MethodCall(iter_path, iter_args, _) = iter.kind; + if iter_path.ident.name == sym::iter; + // range expression in `.zip()` call: `0..x.len()` + if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::Range::hir(zip_arg); + if is_integer_const(cx, start, 0); + // `.len()` call + if let ExprKind::MethodCall(len_path, len_args, _) = end.kind; + if len_path.ident.name == sym::len && len_args.len() == 1; + // `.iter()` and `.len()` called on same `Path` + if let ExprKind::Path(QPath::Resolved(_, iter_path)) = iter_args[0].kind; + if let ExprKind::Path(QPath::Resolved(_, len_path)) = len_args[0].kind; + if SpanlessEq::new(cx).eq_path_segments(&iter_path.segments, &len_path.segments); + then { + span_lint(cx, + RANGE_ZIP_WITH_LEN, + span, + &format!("it is more idiomatic to use `{}.iter().enumerate()`", + snippet(cx, iter_args[0].span, "_")) + ); + } + } +} + +// exclusive range plus one: `x..(y+1)` +fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let Some(higher::Range { + start, + end: Some(end), + limits: RangeLimits::HalfOpen + }) = higher::Range::hir(expr); + if let Some(y) = y_plus_one(cx, end); + then { + let span = if expr.span.from_expansion() { + expr.span + .ctxt() + .outer_expn_data() + .call_site + } else { + expr.span + }; + span_lint_and_then( + cx, + RANGE_PLUS_ONE, + span, + "an inclusive range would be more readable", + |diag| { + let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").maybe_par().to_string()); + let end = Sugg::hir(cx, y, "y").maybe_par(); + if let Some(is_wrapped) = &snippet_opt(cx, span) { + if is_wrapped.starts_with('(') && is_wrapped.ends_with(')') { + diag.span_suggestion( + span, + "use", + format!("({}..={})", start, end), + Applicability::MaybeIncorrect, + ); + } else { + diag.span_suggestion( + span, + "use", + format!("{}..={}", start, end), + Applicability::MachineApplicable, // snippet + ); + } + } + }, + ); + } + } +} + +// inclusive range minus one: `x..=(y-1)` +fn check_inclusive_range_minus_one(cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if let Some(higher::Range { start, end: Some(end), limits: RangeLimits::Closed }) = higher::Range::hir(expr); + if let Some(y) = y_minus_one(cx, end); + then { + span_lint_and_then( + cx, + RANGE_MINUS_ONE, + expr.span, + "an exclusive range would be more readable", + |diag| { + let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").maybe_par().to_string()); + let end = Sugg::hir(cx, y, "y").maybe_par(); + diag.span_suggestion( + expr.span, + "use", + format!("{}..{}", start, end), + Applicability::MachineApplicable, // snippet + ); + }, + ); + } + } +} + +fn check_reversed_empty_range(cx: &LateContext<'_>, expr: &Expr<'_>) { + fn inside_indexing_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + matches!( + get_parent_expr(cx, expr), + Some(Expr { + kind: ExprKind::Index(..), + .. + }) + ) + } + + fn is_for_loop_arg(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let mut cur_expr = expr; + while let Some(parent_expr) = get_parent_expr(cx, cur_expr) { + match higher::ForLoop::hir(parent_expr) { + Some(higher::ForLoop { arg, .. }) if arg.hir_id == expr.hir_id => return true, + _ => cur_expr = parent_expr, + } + } + + false + } + + fn is_empty_range(limits: RangeLimits, ordering: Ordering) -> bool { + match limits { + RangeLimits::HalfOpen => ordering != Ordering::Less, + RangeLimits::Closed => ordering == Ordering::Greater, + } + } + + if_chain! { + if let Some(higher::Range { start: Some(start), end: Some(end), limits }) = higher::Range::hir(expr); + let ty = cx.typeck_results().expr_ty(start); + if let ty::Int(_) | ty::Uint(_) = ty.kind(); + if let Some((start_idx, _)) = constant(cx, cx.typeck_results(), start); + if let Some((end_idx, _)) = constant(cx, cx.typeck_results(), end); + if let Some(ordering) = Constant::partial_cmp(cx.tcx, ty, &start_idx, &end_idx); + if is_empty_range(limits, ordering); + then { + if inside_indexing_expr(cx, expr) { + // Avoid linting `N..N` as it has proven to be useful, see #5689 and #5628 ... + if ordering != Ordering::Equal { + span_lint( + cx, + REVERSED_EMPTY_RANGES, + expr.span, + "this range is reversed and using it to index a slice will panic at run-time", + ); + } + // ... except in for loop arguments for backwards compatibility with `reverse_range_loop` + } else if ordering != Ordering::Equal || is_for_loop_arg(cx, expr) { + span_lint_and_then( + cx, + REVERSED_EMPTY_RANGES, + expr.span, + "this range is empty so it will yield no values", + |diag| { + if ordering != Ordering::Equal { + let start_snippet = snippet(cx, start.span, "_"); + let end_snippet = snippet(cx, end.span, "_"); + let dots = match limits { + RangeLimits::HalfOpen => "..", + RangeLimits::Closed => "..=" + }; + + diag.span_suggestion( + expr.span, + "consider using the following if you are attempting to iterate over this \ + range in reverse", + format!("({}{}{}).rev()", end_snippet, dots, start_snippet), + Applicability::MaybeIncorrect, + ); + } + }, + ); + } + } + } +} + +fn y_plus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> { + match expr.kind { + ExprKind::Binary( + Spanned { + node: BinOpKind::Add, .. + }, + lhs, + rhs, + ) => { + if is_integer_const(cx, lhs, 1) { + Some(rhs) + } else if is_integer_const(cx, rhs, 1) { + Some(lhs) + } else { + None + } + }, + _ => None, + } +} + +fn y_minus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> { + match expr.kind { + ExprKind::Binary( + Spanned { + node: BinOpKind::Sub, .. + }, + lhs, + rhs, + ) if is_integer_const(cx, rhs, 1) => Some(lhs), + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/rc_clone_in_vec_init.rs b/src/tools/clippy/clippy_lints/src/rc_clone_in_vec_init.rs new file mode 100644 index 000000000..8db8c4e9b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/rc_clone_in_vec_init.rs @@ -0,0 +1,139 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::higher::VecArgs; +use clippy_utils::last_path_segment; +use clippy_utils::macros::root_macro_call_first_node; +use clippy_utils::paths; +use clippy_utils::source::{indent_of, snippet}; +use clippy_utils::ty::match_type; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, QPath, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for reference-counted pointers (`Arc`, `Rc`, `rc::Weak`, and `sync::Weak`) + /// in `vec![elem; len]` + /// + /// ### Why is this bad? + /// This will create `elem` once and clone it `len` times - doing so with `Arc`/`Rc`/`Weak` + /// is a bit misleading, as it will create references to the same pointer, rather + /// than different instances. + /// + /// ### Example + /// ```rust + /// let v = vec![std::sync::Arc::new("some data".to_string()); 100]; + /// // or + /// let v = vec![std::rc::Rc::new("some data".to_string()); 100]; + /// ``` + /// Use instead: + /// ```rust + /// // Initialize each value separately: + /// let mut data = Vec::with_capacity(100); + /// for _ in 0..100 { + /// data.push(std::rc::Rc::new("some data".to_string())); + /// } + /// + /// // Or if you want clones of the same reference, + /// // Create the reference beforehand to clarify that + /// // it should be cloned for each value + /// let data = std::rc::Rc::new("some data".to_string()); + /// let v = vec![data; 100]; + /// ``` + #[clippy::version = "1.62.0"] + pub RC_CLONE_IN_VEC_INIT, + suspicious, + "initializing reference-counted pointer in `vec![elem; len]`" +} +declare_lint_pass!(RcCloneInVecInit => [RC_CLONE_IN_VEC_INIT]); + +impl LateLintPass<'_> for RcCloneInVecInit { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return; }; + let Some(VecArgs::Repeat(elem, len)) = VecArgs::hir(cx, expr) else { return; }; + let Some((symbol, func_span)) = ref_init(cx, elem) else { return; }; + + emit_lint(cx, symbol, macro_call.span, elem, len, func_span); + } +} + +fn loop_init_suggestion(elem: &str, len: &str, indent: &str) -> String { + format!( + r#"{{ +{indent} let mut v = Vec::with_capacity({len}); +{indent} (0..{len}).for_each(|_| v.push({elem})); +{indent} v +{indent}}}"# + ) +} + +fn extract_suggestion(elem: &str, len: &str, indent: &str) -> String { + format!( + "{{ +{indent} let data = {elem}; +{indent} vec![data; {len}] +{indent}}}" + ) +} + +fn emit_lint(cx: &LateContext<'_>, symbol: Symbol, lint_span: Span, elem: &Expr<'_>, len: &Expr<'_>, func_span: Span) { + let symbol_name = symbol.as_str(); + + span_lint_and_then( + cx, + RC_CLONE_IN_VEC_INIT, + lint_span, + "initializing a reference-counted pointer in `vec![elem; len]`", + |diag| { + let len_snippet = snippet(cx, len.span, ".."); + let elem_snippet = format!("{}(..)", snippet(cx, elem.span.with_hi(func_span.hi()), "..")); + let indentation = " ".repeat(indent_of(cx, lint_span).unwrap_or(0)); + let loop_init_suggestion = loop_init_suggestion(&elem_snippet, len_snippet.as_ref(), &indentation); + let extract_suggestion = extract_suggestion(&elem_snippet, len_snippet.as_ref(), &indentation); + + diag.note(format!("each element will point to the same `{symbol_name}` instance")); + diag.span_suggestion( + lint_span, + format!("consider initializing each `{symbol_name}` element individually"), + loop_init_suggestion, + Applicability::HasPlaceholders, + ); + diag.span_suggestion( + lint_span, + format!( + "or if this is intentional, consider extracting the `{symbol_name}` initialization to a variable" + ), + extract_suggestion, + Applicability::HasPlaceholders, + ); + }, + ); +} + +/// Checks whether the given `expr` is a call to `Arc::new`, `Rc::new`, or evaluates to a `Weak` +fn ref_init(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<(Symbol, Span)> { + if_chain! { + if let ExprKind::Call(func, _args) = expr.kind; + if let ExprKind::Path(ref func_path @ QPath::TypeRelative(ty, _)) = func.kind; + if let TyKind::Path(ref ty_path) = ty.kind; + if let Some(def_id) = cx.qpath_res(ty_path, ty.hir_id).opt_def_id(); + + then { + if last_path_segment(func_path).ident.name == sym::new + && let Some(symbol) = cx + .tcx + .get_diagnostic_name(def_id) + .filter(|symbol| symbol == &sym::Arc || symbol == &sym::Rc) { + return Some((symbol, func.span)); + } + + let ty_path = cx.typeck_results().expr_ty(expr); + if match_type(cx, ty_path, &paths::WEAK_RC) || match_type(cx, ty_path, &paths::WEAK_ARC) { + return Some((Symbol::intern("Weak"), func.span)); + } + } + } + + None +} diff --git a/src/tools/clippy/clippy_lints/src/read_zero_byte_vec.rs b/src/tools/clippy/clippy_lints/src/read_zero_byte_vec.rs new file mode 100644 index 000000000..9538a8104 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/read_zero_byte_vec.rs @@ -0,0 +1,142 @@ +use clippy_utils::{ + diagnostics::{span_lint, span_lint_and_sugg}, + higher::{get_vec_init_kind, VecInitKind}, + source::snippet, + visitors::expr_visitor_no_bodies, +}; +use hir::{intravisit::Visitor, ExprKind, Local, PatKind, PathSegment, QPath, StmtKind}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// This lint catches reads into a zero-length `Vec`. + /// Especially in the case of a call to `with_capacity`, this lint warns that read + /// gets the number of bytes from the `Vec`'s length, not its capacity. + /// + /// ### Why is this bad? + /// Reading zero bytes is almost certainly not the intended behavior. + /// + /// ### Known problems + /// In theory, a very unusual read implementation could assign some semantic meaning + /// to zero-byte reads. But it seems exceptionally unlikely that code intending to do + /// a zero-byte read would allocate a `Vec` for it. + /// + /// ### Example + /// ```rust + /// use std::io; + /// fn foo<F: io::Read>(mut f: F) { + /// let mut data = Vec::with_capacity(100); + /// f.read(&mut data).unwrap(); + /// } + /// ``` + /// Use instead: + /// ```rust + /// use std::io; + /// fn foo<F: io::Read>(mut f: F) { + /// let mut data = Vec::with_capacity(100); + /// data.resize(100, 0); + /// f.read(&mut data).unwrap(); + /// } + /// ``` + #[clippy::version = "1.63.0"] + pub READ_ZERO_BYTE_VEC, + correctness, + "checks for reads into a zero-length `Vec`" +} +declare_lint_pass!(ReadZeroByteVec => [READ_ZERO_BYTE_VEC]); + +impl<'tcx> LateLintPass<'tcx> for ReadZeroByteVec { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &hir::Block<'tcx>) { + for (idx, stmt) in block.stmts.iter().enumerate() { + if !stmt.span.from_expansion() + // matches `let v = Vec::new();` + && let StmtKind::Local(local) = stmt.kind + && let Local { pat, init: Some(init), .. } = local + && let PatKind::Binding(_, _, ident, _) = pat.kind + && let Some(vec_init_kind) = get_vec_init_kind(cx, init) + { + // finds use of `_.read(&mut v)` + let mut read_found = false; + let mut visitor = expr_visitor_no_bodies(|expr| { + if let ExprKind::MethodCall(path, [_self, arg], _) = expr.kind + && let PathSegment { ident: read_or_read_exact, .. } = *path + && matches!(read_or_read_exact.as_str(), "read" | "read_exact") + && let ExprKind::AddrOf(_, hir::Mutability::Mut, inner) = arg.kind + && let ExprKind::Path(QPath::Resolved(None, inner_path)) = inner.kind + && let [inner_seg] = inner_path.segments + && ident.name == inner_seg.ident.name + { + read_found = true; + } + !read_found + }); + + let next_stmt_span; + if idx == block.stmts.len() - 1 { + // case { .. stmt; expr } + if let Some(e) = block.expr { + visitor.visit_expr(e); + next_stmt_span = e.span; + } else { + return; + } + } else { + // case { .. stmt; stmt; .. } + let next_stmt = &block.stmts[idx + 1]; + visitor.visit_stmt(next_stmt); + next_stmt_span = next_stmt.span; + } + drop(visitor); + + if read_found && !next_stmt_span.from_expansion() { + let applicability = Applicability::MaybeIncorrect; + match vec_init_kind { + VecInitKind::WithConstCapacity(len) => { + span_lint_and_sugg( + cx, + READ_ZERO_BYTE_VEC, + next_stmt_span, + "reading zero byte data to `Vec`", + "try", + format!("{}.resize({}, 0); {}", + ident.as_str(), + len, + snippet(cx, next_stmt_span, "..") + ), + applicability, + ); + } + VecInitKind::WithExprCapacity(hir_id) => { + let e = cx.tcx.hir().expect_expr(hir_id); + span_lint_and_sugg( + cx, + READ_ZERO_BYTE_VEC, + next_stmt_span, + "reading zero byte data to `Vec`", + "try", + format!("{}.resize({}, 0); {}", + ident.as_str(), + snippet(cx, e.span, ".."), + snippet(cx, next_stmt_span, "..") + ), + applicability, + ); + } + _ => { + span_lint( + cx, + READ_ZERO_BYTE_VEC, + next_stmt_span, + "reading zero byte data to `Vec`", + ); + + } + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/redundant_clone.rs b/src/tools/clippy/clippy_lints/src/redundant_clone.rs new file mode 100644 index 000000000..eddca6045 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/redundant_clone.rs @@ -0,0 +1,776 @@ +use clippy_utils::diagnostics::{span_lint_hir, span_lint_hir_and_then}; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::{has_drop, is_copy, is_type_diagnostic_item, walk_ptrs_ty_depth}; +use clippy_utils::{fn_has_unsatisfiable_preds, match_def_path, paths}; +use if_chain::if_chain; +use rustc_data_structures::fx::FxHashMap; +use rustc_errors::Applicability; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{def_id, Body, FnDecl, HirId}; +use rustc_index::bit_set::{BitSet, HybridBitSet}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::mir::{ + self, traversal, + visit::{MutatingUseContext, NonMutatingUseContext, PlaceContext, Visitor as _}, + Mutability, +}; +use rustc_middle::ty::{self, visit::TypeVisitor, Ty}; +use rustc_mir_dataflow::{Analysis, AnalysisDomain, CallReturnPlaces, GenKill, GenKillAnalysis, ResultsCursor}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::{BytePos, Span}; +use rustc_span::sym; +use std::ops::ControlFlow; + +macro_rules! unwrap_or_continue { + ($x:expr) => { + match $x { + Some(x) => x, + None => continue, + } + }; +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for a redundant `clone()` (and its relatives) which clones an owned + /// value that is going to be dropped without further use. + /// + /// ### Why is this bad? + /// It is not always possible for the compiler to eliminate useless + /// allocations and deallocations generated by redundant `clone()`s. + /// + /// ### Known problems + /// False-negatives: analysis performed by this lint is conservative and limited. + /// + /// ### Example + /// ```rust + /// # use std::path::Path; + /// # #[derive(Clone)] + /// # struct Foo; + /// # impl Foo { + /// # fn new() -> Self { Foo {} } + /// # } + /// # fn call(x: Foo) {} + /// { + /// let x = Foo::new(); + /// call(x.clone()); + /// call(x.clone()); // this can just pass `x` + /// } + /// + /// ["lorem", "ipsum"].join(" ").to_string(); + /// + /// Path::new("/a/b").join("c").to_path_buf(); + /// ``` + #[clippy::version = "1.32.0"] + pub REDUNDANT_CLONE, + perf, + "`clone()` of an owned value that is going to be dropped immediately" +} + +declare_lint_pass!(RedundantClone => [REDUNDANT_CLONE]); + +impl<'tcx> LateLintPass<'tcx> for RedundantClone { + #[expect(clippy::too_many_lines)] + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + _: FnKind<'tcx>, + _: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + _: Span, + _: HirId, + ) { + let def_id = cx.tcx.hir().body_owner_def_id(body.id()); + + // Building MIR for `fn`s with unsatisfiable preds results in ICE. + if fn_has_unsatisfiable_preds(cx, def_id.to_def_id()) { + return; + } + + let mir = cx.tcx.optimized_mir(def_id.to_def_id()); + + let possible_origin = { + let mut vis = PossibleOriginVisitor::new(mir); + vis.visit_body(mir); + vis.into_map(cx) + }; + let maybe_storage_live_result = MaybeStorageLive + .into_engine(cx.tcx, mir) + .pass_name("redundant_clone") + .iterate_to_fixpoint() + .into_results_cursor(mir); + let mut possible_borrower = { + let mut vis = PossibleBorrowerVisitor::new(cx, mir, possible_origin); + vis.visit_body(mir); + vis.into_map(cx, maybe_storage_live_result) + }; + + for (bb, bbdata) in mir.basic_blocks().iter_enumerated() { + let terminator = bbdata.terminator(); + + if terminator.source_info.span.from_expansion() { + continue; + } + + // Give up on loops + if terminator.successors().any(|s| s == bb) { + continue; + } + + let (fn_def_id, arg, arg_ty, clone_ret) = + unwrap_or_continue!(is_call_with_ref_arg(cx, mir, &terminator.kind)); + + let from_borrow = match_def_path(cx, fn_def_id, &paths::CLONE_TRAIT_METHOD) + || match_def_path(cx, fn_def_id, &paths::TO_OWNED_METHOD) + || (match_def_path(cx, fn_def_id, &paths::TO_STRING_METHOD) + && is_type_diagnostic_item(cx, arg_ty, sym::String)); + + let from_deref = !from_borrow + && (match_def_path(cx, fn_def_id, &paths::PATH_TO_PATH_BUF) + || match_def_path(cx, fn_def_id, &paths::OS_STR_TO_OS_STRING)); + + if !from_borrow && !from_deref { + continue; + } + + if let ty::Adt(def, _) = arg_ty.kind() { + if def.is_manually_drop() { + continue; + } + } + + // `{ arg = &cloned; clone(move arg); }` or `{ arg = &cloned; to_path_buf(arg); }` + let (cloned, cannot_move_out) = unwrap_or_continue!(find_stmt_assigns_to(cx, mir, arg, from_borrow, bb)); + + let loc = mir::Location { + block: bb, + statement_index: bbdata.statements.len(), + }; + + // `Local` to be cloned, and a local of `clone` call's destination + let (local, ret_local) = if from_borrow { + // `res = clone(arg)` can be turned into `res = move arg;` + // if `arg` is the only borrow of `cloned` at this point. + + if cannot_move_out || !possible_borrower.only_borrowers(&[arg], cloned, loc) { + continue; + } + + (cloned, clone_ret) + } else { + // `arg` is a reference as it is `.deref()`ed in the previous block. + // Look into the predecessor block and find out the source of deref. + + let ps = &mir.basic_blocks.predecessors()[bb]; + if ps.len() != 1 { + continue; + } + let pred_terminator = mir[ps[0]].terminator(); + + // receiver of the `deref()` call + let (pred_arg, deref_clone_ret) = if_chain! { + if let Some((pred_fn_def_id, pred_arg, pred_arg_ty, res)) = + is_call_with_ref_arg(cx, mir, &pred_terminator.kind); + if res == cloned; + if cx.tcx.is_diagnostic_item(sym::deref_method, pred_fn_def_id); + if is_type_diagnostic_item(cx, pred_arg_ty, sym::PathBuf) + || is_type_diagnostic_item(cx, pred_arg_ty, sym::OsString); + then { + (pred_arg, res) + } else { + continue; + } + }; + + let (local, cannot_move_out) = + unwrap_or_continue!(find_stmt_assigns_to(cx, mir, pred_arg, true, ps[0])); + let loc = mir::Location { + block: bb, + statement_index: mir.basic_blocks()[bb].statements.len(), + }; + + // This can be turned into `res = move local` if `arg` and `cloned` are not borrowed + // at the last statement: + // + // ``` + // pred_arg = &local; + // cloned = deref(pred_arg); + // arg = &cloned; + // StorageDead(pred_arg); + // res = to_path_buf(cloned); + // ``` + if cannot_move_out || !possible_borrower.only_borrowers(&[arg, cloned], local, loc) { + continue; + } + + (local, deref_clone_ret) + }; + + let clone_usage = if local == ret_local { + CloneUsage { + cloned_used: false, + cloned_consume_or_mutate_loc: None, + clone_consumed_or_mutated: true, + } + } else { + let clone_usage = visit_clone_usage(local, ret_local, mir, bb); + if clone_usage.cloned_used && clone_usage.clone_consumed_or_mutated { + // cloned value is used, and the clone is modified or moved + continue; + } else if let Some(loc) = clone_usage.cloned_consume_or_mutate_loc { + // cloned value is mutated, and the clone is alive. + if possible_borrower.local_is_alive_at(ret_local, loc) { + continue; + } + } + clone_usage + }; + + let span = terminator.source_info.span; + let scope = terminator.source_info.scope; + let node = mir.source_scopes[scope] + .local_data + .as_ref() + .assert_crate_local() + .lint_root; + + if_chain! { + if let Some(snip) = snippet_opt(cx, span); + if let Some(dot) = snip.rfind('.'); + then { + let sugg_span = span.with_lo( + span.lo() + BytePos(u32::try_from(dot).unwrap()) + ); + let mut app = Applicability::MaybeIncorrect; + + let call_snip = &snip[dot + 1..]; + // Machine applicable when `call_snip` looks like `foobar()` + if let Some(call_snip) = call_snip.strip_suffix("()").map(str::trim) { + if call_snip.as_bytes().iter().all(|b| b.is_ascii_alphabetic() || *b == b'_') { + app = Applicability::MachineApplicable; + } + } + + span_lint_hir_and_then(cx, REDUNDANT_CLONE, node, sugg_span, "redundant clone", |diag| { + diag.span_suggestion( + sugg_span, + "remove this", + "", + app, + ); + if clone_usage.cloned_used { + diag.span_note( + span, + "cloned value is neither consumed nor mutated", + ); + } else { + diag.span_note( + span.with_hi(span.lo() + BytePos(u32::try_from(dot).unwrap())), + "this value is dropped without further use", + ); + } + }); + } else { + span_lint_hir(cx, REDUNDANT_CLONE, node, span, "redundant clone"); + } + } + } + } +} + +/// If `kind` is `y = func(x: &T)` where `T: !Copy`, returns `(DefId of func, x, T, y)`. +fn is_call_with_ref_arg<'tcx>( + cx: &LateContext<'tcx>, + mir: &'tcx mir::Body<'tcx>, + kind: &'tcx mir::TerminatorKind<'tcx>, +) -> Option<(def_id::DefId, mir::Local, Ty<'tcx>, mir::Local)> { + if_chain! { + if let mir::TerminatorKind::Call { func, args, destination, .. } = kind; + if args.len() == 1; + if let mir::Operand::Move(mir::Place { local, .. }) = &args[0]; + if let ty::FnDef(def_id, _) = *func.ty(mir, cx.tcx).kind(); + if let (inner_ty, 1) = walk_ptrs_ty_depth(args[0].ty(mir, cx.tcx)); + if !is_copy(cx, inner_ty); + then { + Some((def_id, *local, inner_ty, destination.as_local()?)) + } else { + None + } + } +} + +type CannotMoveOut = bool; + +/// Finds the first `to = (&)from`, and returns +/// ``Some((from, whether `from` cannot be moved out))``. +fn find_stmt_assigns_to<'tcx>( + cx: &LateContext<'tcx>, + mir: &mir::Body<'tcx>, + to_local: mir::Local, + by_ref: bool, + bb: mir::BasicBlock, +) -> Option<(mir::Local, CannotMoveOut)> { + let rvalue = mir.basic_blocks()[bb].statements.iter().rev().find_map(|stmt| { + if let mir::StatementKind::Assign(box (mir::Place { local, .. }, v)) = &stmt.kind { + return if *local == to_local { Some(v) } else { None }; + } + + None + })?; + + match (by_ref, rvalue) { + (true, mir::Rvalue::Ref(_, _, place)) | (false, mir::Rvalue::Use(mir::Operand::Copy(place))) => { + Some(base_local_and_movability(cx, mir, *place)) + }, + (false, mir::Rvalue::Ref(_, _, place)) => { + if let [mir::ProjectionElem::Deref] = place.as_ref().projection { + Some(base_local_and_movability(cx, mir, *place)) + } else { + None + } + }, + _ => None, + } +} + +/// Extracts and returns the undermost base `Local` of given `place`. Returns `place` itself +/// if it is already a `Local`. +/// +/// Also reports whether given `place` cannot be moved out. +fn base_local_and_movability<'tcx>( + cx: &LateContext<'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. + let mut field = false; + // If projection is a slice index then clone can be removed only if the + // underlying type implements Copy + let mut slice = false; + + let PlaceRef { local, mut projection } = place.as_ref(); + while let [base @ .., elem] = projection { + projection = base; + 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); + } + + (local, deref || field || slice) +} + +#[derive(Default)] +struct CloneUsage { + /// Whether the cloned value is used after the clone. + cloned_used: bool, + /// The first location where the cloned value is consumed or mutated, if any. + cloned_consume_or_mutate_loc: Option<mir::Location>, + /// Whether the clone value is mutated. + clone_consumed_or_mutated: bool, +} +fn visit_clone_usage(cloned: mir::Local, clone: mir::Local, mir: &mir::Body<'_>, bb: mir::BasicBlock) -> CloneUsage { + struct V { + cloned: mir::Local, + clone: mir::Local, + result: CloneUsage, + } + impl<'tcx> mir::visit::Visitor<'tcx> for V { + fn visit_basic_block_data(&mut self, block: mir::BasicBlock, data: &mir::BasicBlockData<'tcx>) { + let statements = &data.statements; + for (statement_index, statement) in statements.iter().enumerate() { + self.visit_statement(statement, mir::Location { block, statement_index }); + } + + self.visit_terminator( + data.terminator(), + mir::Location { + block, + statement_index: statements.len(), + }, + ); + } + + fn visit_place(&mut self, place: &mir::Place<'tcx>, ctx: PlaceContext, loc: mir::Location) { + let local = place.local; + + if local == self.cloned + && !matches!( + ctx, + PlaceContext::MutatingUse(MutatingUseContext::Drop) | PlaceContext::NonUse(_) + ) + { + self.result.cloned_used = true; + self.result.cloned_consume_or_mutate_loc = self.result.cloned_consume_or_mutate_loc.or_else(|| { + matches!( + ctx, + PlaceContext::NonMutatingUse(NonMutatingUseContext::Move) + | PlaceContext::MutatingUse(MutatingUseContext::Borrow) + ) + .then(|| loc) + }); + } else if local == self.clone { + match ctx { + PlaceContext::NonMutatingUse(NonMutatingUseContext::Move) + | PlaceContext::MutatingUse(MutatingUseContext::Borrow) => { + self.result.clone_consumed_or_mutated = true; + }, + _ => {}, + } + } + } + } + + let init = CloneUsage { + cloned_used: false, + cloned_consume_or_mutate_loc: None, + // Consider non-temporary clones consumed. + // TODO: Actually check for mutation of non-temporaries. + clone_consumed_or_mutated: mir.local_kind(clone) != mir::LocalKind::Temp, + }; + traversal::ReversePostorder::new(mir, bb) + .skip(1) + .fold(init, |usage, (tbb, tdata)| { + // Short-circuit + if (usage.cloned_used && usage.clone_consumed_or_mutated) || + // Give up on loops + tdata.terminator().successors().any(|s| s == bb) + { + return CloneUsage { + cloned_used: true, + clone_consumed_or_mutated: true, + ..usage + }; + } + + let mut v = V { + cloned, + clone, + result: usage, + }; + v.visit_basic_block_data(tbb, tdata); + v.result + }) +} + +/// Determines liveness of each local purely based on `StorageLive`/`Dead`. +#[derive(Copy, Clone)] +struct MaybeStorageLive; + +impl<'tcx> AnalysisDomain<'tcx> for MaybeStorageLive { + type Domain = BitSet<mir::Local>; + const NAME: &'static str = "maybe_storage_live"; + + fn bottom_value(&self, body: &mir::Body<'tcx>) -> Self::Domain { + // bottom = dead + BitSet::new_empty(body.local_decls.len()) + } + + fn initialize_start_block(&self, body: &mir::Body<'tcx>, state: &mut Self::Domain) { + for arg in body.args_iter() { + state.insert(arg); + } + } +} + +impl<'tcx> GenKillAnalysis<'tcx> for MaybeStorageLive { + type Idx = mir::Local; + + fn statement_effect(&self, trans: &mut impl GenKill<Self::Idx>, stmt: &mir::Statement<'tcx>, _: mir::Location) { + match stmt.kind { + mir::StatementKind::StorageLive(l) => trans.gen(l), + mir::StatementKind::StorageDead(l) => trans.kill(l), + _ => (), + } + } + + fn terminator_effect( + &self, + _trans: &mut impl GenKill<Self::Idx>, + _terminator: &mir::Terminator<'tcx>, + _loc: mir::Location, + ) { + } + + fn call_return_effect( + &self, + _trans: &mut impl GenKill<Self::Idx>, + _block: mir::BasicBlock, + _return_places: CallReturnPlaces<'_, 'tcx>, + ) { + // Nothing to do when a call returns successfully + } +} + +/// Collects the possible borrowers of each local. +/// For example, `b = &a; c = &a;` will make `b` and (transitively) `c` +/// possible borrowers of `a`. +struct PossibleBorrowerVisitor<'a, 'tcx> { + possible_borrower: TransitiveRelation, + body: &'a mir::Body<'tcx>, + cx: &'a LateContext<'tcx>, + possible_origin: FxHashMap<mir::Local, HybridBitSet<mir::Local>>, +} + +impl<'a, 'tcx> PossibleBorrowerVisitor<'a, 'tcx> { + fn new( + cx: &'a LateContext<'tcx>, + body: &'a mir::Body<'tcx>, + possible_origin: FxHashMap<mir::Local, HybridBitSet<mir::Local>>, + ) -> Self { + Self { + possible_borrower: TransitiveRelation::default(), + cx, + body, + possible_origin, + } + } + + fn into_map( + self, + cx: &LateContext<'tcx>, + maybe_live: ResultsCursor<'tcx, 'tcx, MaybeStorageLive>, + ) -> PossibleBorrowerMap<'a, 'tcx> { + let mut map = FxHashMap::default(); + for row in (1..self.body.local_decls.len()).map(mir::Local::from_usize) { + if is_copy(cx, self.body.local_decls[row].ty) { + continue; + } + + let mut borrowers = self.possible_borrower.reachable_from(row, self.body.local_decls.len()); + borrowers.remove(mir::Local::from_usize(0)); + if !borrowers.is_empty() { + map.insert(row, borrowers); + } + } + + let bs = BitSet::new_empty(self.body.local_decls.len()); + PossibleBorrowerMap { + map, + maybe_live, + bitset: (bs.clone(), bs), + } + } +} + +impl<'a, 'tcx> mir::visit::Visitor<'tcx> for PossibleBorrowerVisitor<'a, 'tcx> { + fn visit_assign(&mut self, place: &mir::Place<'tcx>, rvalue: &mir::Rvalue<'_>, _location: mir::Location) { + let lhs = place.local; + match rvalue { + mir::Rvalue::Ref(_, _, borrowed) => { + self.possible_borrower.add(borrowed.local, lhs); + }, + other => { + if ContainsRegion + .visit_ty(place.ty(&self.body.local_decls, self.cx.tcx).ty) + .is_continue() + { + return; + } + rvalue_locals(other, |rhs| { + if lhs != rhs { + self.possible_borrower.add(rhs, lhs); + } + }); + }, + } + } + + fn visit_terminator(&mut self, terminator: &mir::Terminator<'_>, _loc: mir::Location) { + if let mir::TerminatorKind::Call { + args, + destination: mir::Place { local: dest, .. }, + .. + } = &terminator.kind + { + // TODO add doc + // If the call returns something with lifetimes, + // let's conservatively assume the returned value contains lifetime of all the arguments. + // For example, given `let y: Foo<'a> = foo(x)`, `y` is considered to be a possible borrower of `x`. + + let mut immutable_borrowers = vec![]; + let mut mutable_borrowers = vec![]; + + for op in args { + match op { + mir::Operand::Copy(p) | mir::Operand::Move(p) => { + if let ty::Ref(_, _, Mutability::Mut) = self.body.local_decls[p.local].ty.kind() { + mutable_borrowers.push(p.local); + } else { + immutable_borrowers.push(p.local); + } + }, + mir::Operand::Constant(..) => (), + } + } + + let mut mutable_variables: Vec<mir::Local> = mutable_borrowers + .iter() + .filter_map(|r| self.possible_origin.get(r)) + .flat_map(HybridBitSet::iter) + .collect(); + + if ContainsRegion.visit_ty(self.body.local_decls[*dest].ty).is_break() { + mutable_variables.push(*dest); + } + + for y in mutable_variables { + for x in &immutable_borrowers { + self.possible_borrower.add(*x, y); + } + for x in &mutable_borrowers { + self.possible_borrower.add(*x, y); + } + } + } + } +} + +/// Collect possible borrowed for every `&mut` local. +/// For example, `_1 = &mut _2` generate _1: {_2,...} +/// Known Problems: not sure all borrowed are tracked +struct PossibleOriginVisitor<'a, 'tcx> { + possible_origin: TransitiveRelation, + body: &'a mir::Body<'tcx>, +} + +impl<'a, 'tcx> PossibleOriginVisitor<'a, 'tcx> { + fn new(body: &'a mir::Body<'tcx>) -> Self { + Self { + possible_origin: TransitiveRelation::default(), + body, + } + } + + fn into_map(self, cx: &LateContext<'tcx>) -> FxHashMap<mir::Local, HybridBitSet<mir::Local>> { + let mut map = FxHashMap::default(); + for row in (1..self.body.local_decls.len()).map(mir::Local::from_usize) { + if is_copy(cx, self.body.local_decls[row].ty) { + continue; + } + + let mut borrowers = self.possible_origin.reachable_from(row, self.body.local_decls.len()); + borrowers.remove(mir::Local::from_usize(0)); + if !borrowers.is_empty() { + map.insert(row, borrowers); + } + } + map + } +} + +impl<'a, 'tcx> mir::visit::Visitor<'tcx> for PossibleOriginVisitor<'a, 'tcx> { + fn visit_assign(&mut self, place: &mir::Place<'tcx>, rvalue: &mir::Rvalue<'_>, _location: mir::Location) { + let lhs = place.local; + match rvalue { + // Only consider `&mut`, which can modify origin place + mir::Rvalue::Ref(_, rustc_middle::mir::BorrowKind::Mut { .. }, borrowed) | + // _2: &mut _; + // _3 = move _2 + mir::Rvalue::Use(mir::Operand::Move(borrowed)) | + // _3 = move _2 as &mut _; + mir::Rvalue::Cast(_, mir::Operand::Move(borrowed), _) + => { + self.possible_origin.add(lhs, borrowed.local); + }, + _ => {}, + } + } +} + +struct ContainsRegion; + +impl TypeVisitor<'_> for ContainsRegion { + type BreakTy = (); + + fn visit_region(&mut self, _: ty::Region<'_>) -> ControlFlow<Self::BreakTy> { + ControlFlow::BREAK + } +} + +fn rvalue_locals(rvalue: &mir::Rvalue<'_>, mut visit: impl FnMut(mir::Local)) { + use rustc_middle::mir::Rvalue::{Aggregate, BinaryOp, Cast, CheckedBinaryOp, Repeat, UnaryOp, Use}; + + let mut visit_op = |op: &mir::Operand<'_>| match op { + mir::Operand::Copy(p) | mir::Operand::Move(p) => visit(p.local), + mir::Operand::Constant(..) => (), + }; + + match rvalue { + Use(op) | Repeat(op, _) | Cast(_, op, _) | UnaryOp(_, op) => visit_op(op), + Aggregate(_, ops) => ops.iter().for_each(visit_op), + BinaryOp(_, box (lhs, rhs)) | CheckedBinaryOp(_, box (lhs, rhs)) => { + visit_op(lhs); + visit_op(rhs); + }, + _ => (), + } +} + +/// Result of `PossibleBorrowerVisitor`. +struct PossibleBorrowerMap<'a, 'tcx> { + /// Mapping `Local -> its possible borrowers` + map: FxHashMap<mir::Local, HybridBitSet<mir::Local>>, + maybe_live: ResultsCursor<'a, 'tcx, MaybeStorageLive>, + // Caches to avoid allocation of `BitSet` on every query + bitset: (BitSet<mir::Local>, BitSet<mir::Local>), +} + +impl PossibleBorrowerMap<'_, '_> { + /// Returns true if the set of borrowers of `borrowed` living at `at` matches with `borrowers`. + fn only_borrowers(&mut self, borrowers: &[mir::Local], borrowed: mir::Local, at: mir::Location) -> bool { + self.maybe_live.seek_after_primary_effect(at); + + self.bitset.0.clear(); + let maybe_live = &mut self.maybe_live; + if let Some(bitset) = self.map.get(&borrowed) { + for b in bitset.iter().filter(move |b| maybe_live.contains(*b)) { + self.bitset.0.insert(b); + } + } else { + return false; + } + + self.bitset.1.clear(); + for b in borrowers { + self.bitset.1.insert(*b); + } + + self.bitset.0 == self.bitset.1 + } + + fn local_is_alive_at(&mut self, local: mir::Local, at: mir::Location) -> bool { + self.maybe_live.seek_after_primary_effect(at); + self.maybe_live.contains(local) + } +} + +#[derive(Default)] +struct TransitiveRelation { + relations: FxHashMap<mir::Local, Vec<mir::Local>>, +} +impl TransitiveRelation { + fn add(&mut self, a: mir::Local, b: mir::Local) { + self.relations.entry(a).or_default().push(b); + } + + fn reachable_from(&self, a: mir::Local, domain_size: usize) -> HybridBitSet<mir::Local> { + let mut seen = HybridBitSet::new_empty(domain_size); + let mut stack = vec![a]; + while let Some(u) = stack.pop() { + if let Some(edges) = self.relations.get(&u) { + for &v in edges { + if seen.insert(v) { + stack.push(v); + } + } + } + } + seen + } +} diff --git a/src/tools/clippy/clippy_lints/src/redundant_closure_call.rs b/src/tools/clippy/clippy_lints/src/redundant_closure_call.rs new file mode 100644 index 000000000..f5a93ceba --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/redundant_closure_call.rs @@ -0,0 +1,157 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; +use clippy_utils::source::snippet_with_applicability; +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_middle::hir::nested_filter; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Detects closures called in the same expression where they + /// are defined. + /// + /// ### Why is this bad? + /// It is unnecessarily adding to the expression's + /// complexity. + /// + /// ### Example + /// ```rust + /// let a = (|| 42)(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let a = 42; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub REDUNDANT_CLOSURE_CALL, + complexity, + "throwaway closures called in the expression they are defined" +} + +declare_lint_pass!(RedundantClosureCall => [REDUNDANT_CLOSURE_CALL]); + +// Used to find `return` statements or equivalents e.g., `?` +struct ReturnVisitor { + found_return: bool, +} + +impl ReturnVisitor { + #[must_use] + fn new() -> Self { + Self { found_return: false } + } +} + +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 { + self.found_return = true; + } + + ast_visit::walk_expr(self, ex); + } +} + +impl EarlyLintPass for RedundantClosureCall { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) { + 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(_, _, _, _, ref decl, ref block, _) = closure.kind; + then { + let mut visitor = ReturnVisitor::new(); + visitor.visit_expr(block); + 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 decl.inputs.is_empty() { + let mut app = Applicability::MachineApplicable; + let hint = + snippet_with_applicability(cx, block.span, "..", &mut app).into_owned(); + diag.span_suggestion(expr.span, "try doing something like", hint, app); + } + }, + ); + } + } + } + } +} + +impl<'tcx> LateLintPass<'tcx> for RedundantClosureCall { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) { + fn count_closure_usage<'a, 'tcx>( + cx: &'a LateContext<'tcx>, + block: &'tcx hir::Block<'_>, + path: &'tcx hir::Path<'tcx>, + ) -> usize { + struct ClosureUsageCount<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + path: &'tcx hir::Path<'tcx>, + count: usize, + } + impl<'a, 'tcx> hir_visit::Visitor<'tcx> for ClosureUsageCount<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) { + if_chain! { + if let hir::ExprKind::Call(closure, _) = expr.kind; + if let hir::ExprKind::Path(hir::QPath::Resolved(_, path)) = closure.kind; + if self.path.segments[0].ident == path.segments[0].ident; + if self.path.res == path.res; + then { + self.count += 1; + } + } + hir_visit::walk_expr(self, expr); + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + } + let mut closure_usage_count = ClosureUsageCount { cx, path, count: 0 }; + closure_usage_count.visit_block(block); + closure_usage_count.count + } + + for w in block.stmts.windows(2) { + if_chain! { + if let hir::StmtKind::Local(local) = w[0].kind; + if let Option::Some(t) = local.init; + if let hir::ExprKind::Closure { .. } = t.kind; + if let hir::PatKind::Binding(_, _, ident, _) = local.pat.kind; + if let hir::StmtKind::Semi(second) = w[1].kind; + if let hir::ExprKind::Assign(_, call, _) = second.kind; + if let hir::ExprKind::Call(closure, _) = call.kind; + if let hir::ExprKind::Path(hir::QPath::Resolved(_, path)) = closure.kind; + if ident == path.segments[0].ident; + if count_closure_usage(cx, block, path) == 1; + then { + span_lint( + cx, + REDUNDANT_CLOSURE_CALL, + second.span, + "closure called just once immediately after it was declared", + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/redundant_else.rs b/src/tools/clippy/clippy_lints/src/redundant_else.rs new file mode 100644 index 000000000..73088ce1a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/redundant_else.rs @@ -0,0 +1,138 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{Block, Expr, ExprKind, Stmt, StmtKind}; +use rustc_ast::visit::{walk_expr, Visitor}; +use rustc_lint::{EarlyContext, EarlyLintPass, 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 `else` blocks that can be removed without changing semantics. + /// + /// ### Why is this bad? + /// The `else` block adds unnecessary indentation and verbosity. + /// + /// ### Known problems + /// Some may prefer to keep the `else` block for clarity. + /// + /// ### Example + /// ```rust + /// fn my_func(count: u32) { + /// if count == 0 { + /// print!("Nothing to do"); + /// return; + /// } else { + /// print!("Moving on..."); + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn my_func(count: u32) { + /// if count == 0 { + /// print!("Nothing to do"); + /// return; + /// } + /// print!("Moving on..."); + /// } + /// ``` + #[clippy::version = "1.50.0"] + pub REDUNDANT_ELSE, + pedantic, + "`else` branch that can be removed without changing semantics" +} + +declare_lint_pass!(RedundantElse => [REDUNDANT_ELSE]); + +impl EarlyLintPass for RedundantElse { + fn check_stmt(&mut self, cx: &EarlyContext<'_>, stmt: &Stmt) { + if in_external_macro(cx.sess(), stmt.span) { + return; + } + // Only look at expressions that are a whole statement + let expr: &Expr = match &stmt.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => expr, + _ => return, + }; + // if else + let (mut then, mut els): (&Block, &Expr) = match &expr.kind { + ExprKind::If(_, then, Some(els)) => (then, els), + _ => return, + }; + loop { + if !BreakVisitor::default().check_block(then) { + // then block does not always break + return; + } + match &els.kind { + // else if else + ExprKind::If(_, next_then, Some(next_els)) => { + then = next_then; + els = next_els; + continue; + }, + // else if without else + ExprKind::If(..) => return, + // done + _ => break, + } + } + span_lint_and_help( + cx, + REDUNDANT_ELSE, + els.span, + "redundant else block", + None, + "remove the `else` block and move the contents out", + ); + } +} + +/// Call `check` functions to check if an expression always breaks control flow +#[derive(Default)] +struct BreakVisitor { + is_break: bool, +} + +impl<'ast> Visitor<'ast> for BreakVisitor { + fn visit_block(&mut self, block: &'ast Block) { + self.is_break = match block.stmts.as_slice() { + [.., last] => self.check_stmt(last), + _ => false, + }; + } + + fn visit_expr(&mut self, expr: &'ast Expr) { + self.is_break = match expr.kind { + ExprKind::Break(..) | ExprKind::Continue(..) | ExprKind::Ret(..) => true, + ExprKind::Match(_, ref arms) => arms.iter().all(|arm| self.check_expr(&arm.body)), + ExprKind::If(_, ref then, Some(ref els)) => self.check_block(then) && self.check_expr(els), + ExprKind::If(_, _, None) + // ignore loops for simplicity + | ExprKind::While(..) | ExprKind::ForLoop(..) | ExprKind::Loop(..) => false, + _ => { + walk_expr(self, expr); + return; + }, + }; + } +} + +impl BreakVisitor { + fn check<T>(&mut self, item: T, visit: fn(&mut Self, T)) -> bool { + visit(self, item); + std::mem::replace(&mut self.is_break, false) + } + + fn check_block(&mut self, block: &Block) -> bool { + self.check(block, Self::visit_block) + } + + fn check_expr(&mut self, expr: &Expr) -> bool { + self.check(expr, Self::visit_expr) + } + + fn check_stmt(&mut self, stmt: &Stmt) -> bool { + self.check(stmt, Self::visit_stmt) + } +} diff --git a/src/tools/clippy/clippy_lints/src/redundant_field_names.rs b/src/tools/clippy/clippy_lints/src/redundant_field_names.rs new file mode 100644 index 000000000..40b03068f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/redundant_field_names.rs @@ -0,0 +1,86 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::{meets_msrv, msrvs}; +use rustc_ast::ast::{Expr, ExprKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for fields in struct literals where shorthands + /// could be used. + /// + /// ### Why is this bad? + /// If the field and variable names are the same, + /// the field name is redundant. + /// + /// ### Example + /// ```rust + /// let bar: u8 = 123; + /// + /// struct Foo { + /// bar: u8, + /// } + /// + /// let foo = Foo { bar: bar }; + /// ``` + /// the last line can be simplified to + /// ```ignore + /// let foo = Foo { bar }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub REDUNDANT_FIELD_NAMES, + style, + "checks for fields in struct literals where shorthands could be used" +} + +pub struct RedundantFieldNames { + msrv: Option<RustcVersion>, +} + +impl RedundantFieldNames { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(RedundantFieldNames => [REDUNDANT_FIELD_NAMES]); + +impl EarlyLintPass for RedundantFieldNames { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if !meets_msrv(self.msrv, msrvs::FIELD_INIT_SHORTHAND) { + return; + } + + if in_external_macro(cx.sess(), expr.span) { + return; + } + if let ExprKind::Struct(ref se) = expr.kind { + for field in &se.fields { + if field.is_shorthand { + continue; + } + if let ExprKind::Path(None, path) = &field.expr.kind { + if path.segments.len() == 1 + && path.segments[0].ident == field.ident + && path.segments[0].args.is_none() + { + span_lint_and_sugg( + cx, + REDUNDANT_FIELD_NAMES, + field.span, + "redundant field names in struct initialization", + "replace it with", + field.ident.to_string(), + Applicability::MachineApplicable, + ); + } + } + } + } + } + extract_msrv_attr!(EarlyContext); +} diff --git a/src/tools/clippy/clippy_lints/src/redundant_pub_crate.rs b/src/tools/clippy/clippy_lints/src/redundant_pub_crate.rs new file mode 100644 index 000000000..323326381 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/redundant_pub_crate.rs @@ -0,0 +1,94 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use rustc_errors::Applicability; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::def_id::CRATE_DEF_ID; +use rustc_span::hygiene::MacroKind; + +declare_clippy_lint! { + /// ### What it does + /// Checks for items declared `pub(crate)` that are not crate visible because they + /// are inside a private module. + /// + /// ### Why is this bad? + /// Writing `pub(crate)` is misleading when it's redundant due to the parent + /// module's visibility. + /// + /// ### Example + /// ```rust + /// mod internal { + /// pub(crate) fn internal_fn() { } + /// } + /// ``` + /// This function is not visible outside the module and it can be declared with `pub` or + /// private visibility + /// ```rust + /// mod internal { + /// pub fn internal_fn() { } + /// } + /// ``` + #[clippy::version = "1.44.0"] + pub REDUNDANT_PUB_CRATE, + nursery, + "Using `pub(crate)` visibility on items that are not crate visible due to the visibility of the module that contains them." +} + +#[derive(Default)] +pub struct RedundantPubCrate { + is_exported: Vec<bool>, +} + +impl_lint_pass!(RedundantPubCrate => [REDUNDANT_PUB_CRATE]); + +impl<'tcx> LateLintPass<'tcx> for RedundantPubCrate { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if_chain! { + if cx.tcx.visibility(item.def_id) == ty::Visibility::Restricted(CRATE_DEF_ID.to_def_id()); + if !cx.access_levels.is_exported(item.def_id) && self.is_exported.last() == Some(&false); + if is_not_macro_export(item); + then { + let span = item.span.with_hi(item.ident.span.hi()); + let descr = cx.tcx.def_kind(item.def_id).descr(item.def_id.to_def_id()); + span_lint_and_then( + cx, + REDUNDANT_PUB_CRATE, + span, + &format!("pub(crate) {} inside private module", descr), + |diag| { + diag.span_suggestion( + item.vis_span, + "consider using", + "pub".to_string(), + Applicability::MachineApplicable, + ); + }, + ); + } + } + + if let ItemKind::Mod { .. } = item.kind { + self.is_exported.push(cx.access_levels.is_exported(item.def_id)); + } + } + + fn check_item_post(&mut self, _cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if let ItemKind::Mod { .. } = item.kind { + self.is_exported.pop().expect("unbalanced check_item/check_item_post"); + } + } +} + +fn is_not_macro_export<'tcx>(item: &'tcx Item<'tcx>) -> bool { + if let ItemKind::Use(path, _) = item.kind { + if let Res::Def(DefKind::Macro(MacroKind::Bang), _) = path.res { + return false; + } + } else if let ItemKind::Macro(..) = item.kind { + return false; + } + + true +} diff --git a/src/tools/clippy/clippy_lints/src/redundant_slicing.rs b/src/tools/clippy/clippy_lints/src/redundant_slicing.rs new file mode 100644 index 000000000..db6c97f37 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/redundant_slicing.rs @@ -0,0 +1,169 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::get_parent_expr; +use clippy_utils::source::snippet_with_context; +use clippy_utils::ty::{is_type_lang_item, peel_mid_ty_refs}; +use if_chain::if_chain; +use rustc_ast::util::parser::PREC_PREFIX; +use rustc_errors::Applicability; +use rustc_hir::{BorrowKind, Expr, ExprKind, LangItem, Mutability}; +use rustc_lint::{LateContext, LateLintPass, Lint}; +use rustc_middle::ty::adjustment::{Adjust, AutoBorrow, AutoBorrowMutability}; +use rustc_middle::ty::subst::GenericArg; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for redundant slicing expressions which use the full range, and + /// do not change the type. + /// + /// ### Why is this bad? + /// It unnecessarily adds complexity to the expression. + /// + /// ### Known problems + /// If the type being sliced has an implementation of `Index<RangeFull>` + /// that actually changes anything then it can't be removed. However, this would be surprising + /// to people reading the code and should have a note with it. + /// + /// ### Example + /// ```ignore + /// fn get_slice(x: &[u32]) -> &[u32] { + /// &x[..] + /// } + /// ``` + /// Use instead: + /// ```ignore + /// fn get_slice(x: &[u32]) -> &[u32] { + /// x + /// } + /// ``` + #[clippy::version = "1.51.0"] + pub REDUNDANT_SLICING, + complexity, + "redundant slicing of the whole range of a type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for slicing expressions which are equivalent to dereferencing the + /// value. + /// + /// ### Why is this bad? + /// Some people may prefer to dereference rather than slice. + /// + /// ### Example + /// ```rust + /// let vec = vec![1, 2, 3]; + /// let slice = &vec[..]; + /// ``` + /// Use instead: + /// ```rust + /// let vec = vec![1, 2, 3]; + /// let slice = &*vec; + /// ``` + #[clippy::version = "1.61.0"] + pub DEREF_BY_SLICING, + restriction, + "slicing instead of dereferencing" +} + +declare_lint_pass!(RedundantSlicing => [REDUNDANT_SLICING, DEREF_BY_SLICING]); + +static REDUNDANT_SLICING_LINT: (&Lint, &str) = (REDUNDANT_SLICING, "redundant slicing of the whole range"); +static DEREF_BY_SLICING_LINT: (&Lint, &str) = (DEREF_BY_SLICING, "slicing when dereferencing would work"); + +impl<'tcx> LateLintPass<'tcx> for RedundantSlicing { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if expr.span.from_expansion() { + return; + } + + let ctxt = expr.span.ctxt(); + if_chain! { + if let ExprKind::AddrOf(BorrowKind::Ref, mutability, addressee) = expr.kind; + if addressee.span.ctxt() == ctxt; + if let ExprKind::Index(indexed, range) = addressee.kind; + if is_type_lang_item(cx, cx.typeck_results().expr_ty_adjusted(range), LangItem::RangeFull); + then { + let (expr_ty, expr_ref_count) = peel_mid_ty_refs(cx.typeck_results().expr_ty(expr)); + let (indexed_ty, indexed_ref_count) = peel_mid_ty_refs(cx.typeck_results().expr_ty(indexed)); + let parent_expr = get_parent_expr(cx, expr); + let needs_parens_for_prefix = parent_expr.map_or(false, |parent| { + parent.precedence().order() > PREC_PREFIX + }); + let mut app = Applicability::MachineApplicable; + + let ((lint, msg), help, sugg) = if expr_ty == indexed_ty { + if expr_ref_count > indexed_ref_count { + // Indexing takes self by reference and can't return a reference to that + // reference as it's a local variable. The only way this could happen is if + // `self` contains a reference to the `Self` type. If this occurs then the + // lint no longer applies as it's essentially a field access, which is not + // redundant. + return; + } + let deref_count = indexed_ref_count - expr_ref_count; + + let (lint, reborrow_str, help_str) = if mutability == Mutability::Mut { + // The slice was used to reborrow the mutable reference. + (DEREF_BY_SLICING_LINT, "&mut *", "reborrow the original value instead") + } else if matches!( + parent_expr, + Some(Expr { + kind: ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, _), + .. + }) + ) || cx.typeck_results().expr_adjustments(expr).first().map_or(false, |a| { + matches!(a.kind, Adjust::Borrow(AutoBorrow::Ref(_, AutoBorrowMutability::Mut { .. }))) + }) { + // The slice was used to make a temporary reference. + (DEREF_BY_SLICING_LINT, "&*", "reborrow the original value instead") + } else if deref_count != 0 { + (DEREF_BY_SLICING_LINT, "", "dereference the original value instead") + } else { + (REDUNDANT_SLICING_LINT, "", "use the original value instead") + }; + + let snip = snippet_with_context(cx, indexed.span, ctxt, "..", &mut app).0; + let sugg = if (deref_count != 0 || !reborrow_str.is_empty()) && needs_parens_for_prefix { + format!("({}{}{})", reborrow_str, "*".repeat(deref_count), snip) + } else { + format!("{}{}{}", reborrow_str, "*".repeat(deref_count), snip) + }; + + (lint, help_str, sugg) + } else if let Some(target_id) = cx.tcx.lang_items().deref_target() { + if let Ok(deref_ty) = cx.tcx.try_normalize_erasing_regions( + cx.param_env, + cx.tcx.mk_projection(target_id, cx.tcx.mk_substs([GenericArg::from(indexed_ty)].into_iter())), + ) { + if deref_ty == expr_ty { + let snip = snippet_with_context(cx, indexed.span, ctxt, "..", &mut app).0; + let sugg = if needs_parens_for_prefix { + format!("(&{}{}*{})", mutability.prefix_str(), "*".repeat(indexed_ref_count), snip) + } else { + format!("&{}{}*{}", mutability.prefix_str(), "*".repeat(indexed_ref_count), snip) + }; + (DEREF_BY_SLICING_LINT, "dereference the original value instead", sugg) + } else { + return; + } + } else { + return; + } + } else { + return; + }; + + span_lint_and_sugg( + cx, + lint, + expr.span, + msg, + help, + sugg, + app, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/redundant_static_lifetimes.rs b/src/tools/clippy/clippy_lints/src/redundant_static_lifetimes.rs new file mode 100644 index 000000000..f8801f769 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/redundant_static_lifetimes.rs @@ -0,0 +1,117 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::{meets_msrv, msrvs}; +use rustc_ast::ast::{Item, ItemKind, Ty, TyKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for constants and statics with an explicit `'static` lifetime. + /// + /// ### Why is this bad? + /// Adding `'static` to every reference can create very + /// complicated types. + /// + /// ### Example + /// ```ignore + /// const FOO: &'static [(&'static str, &'static str, fn(&Bar) -> bool)] = + /// &[...] + /// static FOO: &'static [(&'static str, &'static str, fn(&Bar) -> bool)] = + /// &[...] + /// ``` + /// This code can be rewritten as + /// ```ignore + /// const FOO: &[(&str, &str, fn(&Bar) -> bool)] = &[...] + /// static FOO: &[(&str, &str, fn(&Bar) -> bool)] = &[...] + /// ``` + #[clippy::version = "1.37.0"] + pub REDUNDANT_STATIC_LIFETIMES, + style, + "Using explicit `'static` lifetime for constants or statics when elision rules would allow omitting them." +} + +pub struct RedundantStaticLifetimes { + msrv: Option<RustcVersion>, +} + +impl RedundantStaticLifetimes { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(RedundantStaticLifetimes => [REDUNDANT_STATIC_LIFETIMES]); + +impl RedundantStaticLifetimes { + // Recursively visit types + fn visit_type(&mut self, ty: &Ty, cx: &EarlyContext<'_>, reason: &str) { + match ty.kind { + // Be careful of nested structures (arrays and tuples) + TyKind::Array(ref ty, _) | TyKind::Slice(ref ty) => { + self.visit_type(ty, cx, reason); + }, + TyKind::Tup(ref tup) => { + for tup_ty in tup { + self.visit_type(tup_ty, cx, reason); + } + }, + // This is what we are looking for ! + TyKind::Rptr(ref optional_lifetime, ref borrow_type) => { + // Match the 'static lifetime + if let Some(lifetime) = *optional_lifetime { + match borrow_type.ty.kind { + TyKind::Path(..) | TyKind::Slice(..) | TyKind::Array(..) | TyKind::Tup(..) => { + if lifetime.ident.name == rustc_span::symbol::kw::StaticLifetime { + let snip = snippet(cx, borrow_type.ty.span, "<type>"); + let sugg = format!("&{}", snip); + span_lint_and_then( + cx, + REDUNDANT_STATIC_LIFETIMES, + lifetime.ident.span, + reason, + |diag| { + diag.span_suggestion( + ty.span, + "consider removing `'static`", + sugg, + Applicability::MachineApplicable, //snippet + ); + }, + ); + } + }, + _ => {}, + } + } + self.visit_type(&borrow_type.ty, cx, reason); + }, + _ => {}, + } + } +} + +impl EarlyLintPass for RedundantStaticLifetimes { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if !meets_msrv(self.msrv, msrvs::STATIC_IN_CONST) { + return; + } + + if !item.span.from_expansion() { + if let ItemKind::Const(_, ref var_type, _) = item.kind { + self.visit_type(var_type, cx, "constants have by default a `'static` lifetime"); + // Don't check associated consts because `'static` cannot be elided on those (issue + // #2438) + } + + if let ItemKind::Static(ref var_type, _, _) = item.kind { + self.visit_type(var_type, cx, "statics have by default a `'static` lifetime"); + } + } + } + + extract_msrv_attr!(EarlyContext); +} diff --git a/src/tools/clippy/clippy_lints/src/ref_option_ref.rs b/src/tools/clippy/clippy_lints/src/ref_option_ref.rs new file mode 100644 index 000000000..909d6971a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/ref_option_ref.rs @@ -0,0 +1,71 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::last_path_segment; +use clippy_utils::source::snippet; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{GenericArg, Mutability, Ty, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `&Option<&T>`. + /// + /// ### Why is this bad? + /// Since `&` is Copy, it's useless to have a + /// reference on `Option<&T>`. + /// + /// ### Known problems + /// It may be irrelevant to use this lint on + /// public API code as it will make a breaking change to apply it. + /// + /// ### Example + /// ```rust,ignore + /// let x: &Option<&u32> = &Some(&0u32); + /// ``` + /// Use instead: + /// ```rust,ignore + /// let x: Option<&u32> = Some(&0u32); + /// ``` + #[clippy::version = "1.49.0"] + pub REF_OPTION_REF, + pedantic, + "use `Option<&T>` instead of `&Option<&T>`" +} + +declare_lint_pass!(RefOptionRef => [REF_OPTION_REF]); + +impl<'tcx> LateLintPass<'tcx> for RefOptionRef { + fn check_ty(&mut self, cx: &LateContext<'tcx>, ty: &'tcx Ty<'tcx>) { + if_chain! { + if let TyKind::Rptr(_, ref mut_ty) = ty.kind; + if mut_ty.mutbl == Mutability::Not; + if let TyKind::Path(ref qpath) = &mut_ty.ty.kind; + let last = last_path_segment(qpath); + if let Some(res) = last.res; + if let Some(def_id) = res.opt_def_id(); + + if cx.tcx.is_diagnostic_item(sym::Option, def_id); + if let Some(params) = last_path_segment(qpath).args ; + if !params.parenthesized; + if let Some(inner_ty) = params.args.iter().find_map(|arg| match arg { + GenericArg::Type(inner_ty) => Some(inner_ty), + _ => None, + }); + if let TyKind::Rptr(_, _) = inner_ty.kind; + + then { + span_lint_and_sugg( + cx, + REF_OPTION_REF, + ty.span, + "since `&` implements the `Copy` trait, `&Option<&T>` can be simplified to `Option<&T>`", + "try", + format!("Option<{}>", &snippet(cx, inner_ty.span, "..")), + Applicability::MaybeIncorrect, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/reference.rs b/src/tools/clippy/clippy_lints/src/reference.rs new file mode 100644 index 000000000..a642e2da3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/reference.rs @@ -0,0 +1,105 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{snippet_opt, snippet_with_applicability}; +use if_chain::if_chain; +use rustc_ast::ast::{Expr, ExprKind, Mutability, UnOp}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::BytePos; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `*&` and `*&mut` in expressions. + /// + /// ### Why is this bad? + /// Immediately dereferencing a reference is no-op and + /// makes the code less clear. + /// + /// ### Known problems + /// Multiple dereference/addrof pairs are not handled so + /// the suggested fix for `x = **&&y` is `x = *&y`, which is still incorrect. + /// + /// ### Example + /// ```rust,ignore + /// let a = f(*&mut b); + /// let c = *&d; + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// let a = f(b); + /// let c = d; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub DEREF_ADDROF, + complexity, + "use of `*&` or `*&mut` in an expression" +} + +declare_lint_pass!(DerefAddrOf => [DEREF_ADDROF]); + +fn without_parens(mut e: &Expr) -> &Expr { + while let ExprKind::Paren(ref child_e) = e.kind { + e = child_e; + } + e +} + +impl EarlyLintPass for DerefAddrOf { + fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &Expr) { + if_chain! { + if let ExprKind::Unary(UnOp::Deref, ref deref_target) = e.kind; + if let ExprKind::AddrOf(_, ref mutability, ref addrof_target) = without_parens(deref_target).kind; + if deref_target.span.ctxt() == e.span.ctxt(); + if !addrof_target.span.from_expansion(); + then { + let mut applicability = Applicability::MachineApplicable; + let sugg = if e.span.from_expansion() { + if let Some(macro_source) = snippet_opt(cx, e.span) { + // Remove leading whitespace from the given span + // e.g: ` $visitor` turns into `$visitor` + let trim_leading_whitespaces = |span| { + snippet_opt(cx, span).and_then(|snip| { + #[expect(clippy::cast_possible_truncation)] + snip.find(|c: char| !c.is_whitespace()).map(|pos| { + span.lo() + BytePos(pos as u32) + }) + }).map_or(span, |start_no_whitespace| e.span.with_lo(start_no_whitespace)) + }; + + let mut generate_snippet = |pattern: &str| { + #[expect(clippy::cast_possible_truncation)] + macro_source.rfind(pattern).map(|pattern_pos| { + let rpos = pattern_pos + pattern.len(); + let span_after_ref = e.span.with_lo(BytePos(e.span.lo().0 + rpos as u32)); + let span = trim_leading_whitespaces(span_after_ref); + snippet_with_applicability(cx, span, "_", &mut applicability) + }) + }; + + if *mutability == Mutability::Mut { + generate_snippet("mut") + } else { + generate_snippet("&") + } + } else { + Some(snippet_with_applicability(cx, e.span, "_", &mut applicability)) + } + } else { + Some(snippet_with_applicability(cx, addrof_target.span, "_", &mut applicability)) + }; + if let Some(sugg) = sugg { + span_lint_and_sugg( + cx, + DEREF_ADDROF, + e.span, + "immediately dereferencing a reference", + "try this", + sugg.to_string(), + applicability, + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/regex.rs b/src/tools/clippy/clippy_lints/src/regex.rs new file mode 100644 index 000000000..f9a9b0691 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/regex.rs @@ -0,0 +1,208 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_ast::ast::{LitKind, StrStyle}; +use rustc_hir::{BorrowKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::{BytePos, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks [regex](https://crates.io/crates/regex) creation + /// (with `Regex::new`, `RegexBuilder::new`, or `RegexSet::new`) for correct + /// regex syntax. + /// + /// ### Why is this bad? + /// This will lead to a runtime panic. + /// + /// ### Example + /// ```ignore + /// Regex::new("(") + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INVALID_REGEX, + correctness, + "invalid regular expressions" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for trivial [regex](https://crates.io/crates/regex) + /// creation (with `Regex::new`, `RegexBuilder::new`, or `RegexSet::new`). + /// + /// ### Why is this bad? + /// Matching the regex can likely be replaced by `==` or + /// `str::starts_with`, `str::ends_with` or `std::contains` or other `str` + /// methods. + /// + /// ### Known problems + /// If the same regex is going to be applied to multiple + /// inputs, the precomputations done by `Regex` construction can give + /// significantly better performance than any of the `str`-based methods. + /// + /// ### Example + /// ```ignore + /// Regex::new("^foobar") + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRIVIAL_REGEX, + nursery, + "trivial regular expressions" +} + +declare_lint_pass!(Regex => [INVALID_REGEX, TRIVIAL_REGEX]); + +impl<'tcx> LateLintPass<'tcx> for Regex { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::Call(fun, args) = expr.kind; + if let ExprKind::Path(ref qpath) = fun.kind; + if args.len() == 1; + if let Some(def_id) = cx.qpath_res(qpath, fun.hir_id).opt_def_id(); + then { + if match_def_path(cx, def_id, &paths::REGEX_NEW) || + match_def_path(cx, def_id, &paths::REGEX_BUILDER_NEW) { + check_regex(cx, &args[0], true); + } else if match_def_path(cx, def_id, &paths::REGEX_BYTES_NEW) || + match_def_path(cx, def_id, &paths::REGEX_BYTES_BUILDER_NEW) { + check_regex(cx, &args[0], false); + } else if match_def_path(cx, def_id, &paths::REGEX_SET_NEW) { + check_set(cx, &args[0], true); + } else if match_def_path(cx, def_id, &paths::REGEX_BYTES_SET_NEW) { + check_set(cx, &args[0], false); + } + } + } + } +} + +#[must_use] +fn str_span(base: Span, c: regex_syntax::ast::Span, offset: u8) -> Span { + let offset = u32::from(offset); + let end = base.lo() + BytePos(u32::try_from(c.end.offset).expect("offset too large") + offset); + let start = base.lo() + BytePos(u32::try_from(c.start.offset).expect("offset too large") + offset); + assert!(start <= end); + Span::new(start, end, base.ctxt(), base.parent()) +} + +fn const_str<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> Option<String> { + constant(cx, cx.typeck_results(), e).and_then(|(c, _)| match c { + Constant::Str(s) => Some(s), + _ => None, + }) +} + +fn is_trivial_regex(s: ®ex_syntax::hir::Hir) -> Option<&'static str> { + use regex_syntax::hir::Anchor::{EndText, StartText}; + use regex_syntax::hir::HirKind::{Alternation, Anchor, Concat, Empty, Literal}; + + let is_literal = |e: &[regex_syntax::hir::Hir]| e.iter().all(|e| matches!(*e.kind(), Literal(_))); + + match *s.kind() { + Empty | Anchor(_) => Some("the regex is unlikely to be useful as it is"), + Literal(_) => Some("consider using `str::contains`"), + Alternation(ref exprs) => { + if exprs.iter().all(|e| e.kind().is_empty()) { + Some("the regex is unlikely to be useful as it is") + } else { + None + } + }, + Concat(ref exprs) => match (exprs[0].kind(), exprs[exprs.len() - 1].kind()) { + (&Anchor(StartText), &Anchor(EndText)) if exprs[1..(exprs.len() - 1)].is_empty() => { + Some("consider using `str::is_empty`") + }, + (&Anchor(StartText), &Anchor(EndText)) if is_literal(&exprs[1..(exprs.len() - 1)]) => { + Some("consider using `==` on `str`s") + }, + (&Anchor(StartText), &Literal(_)) if is_literal(&exprs[1..]) => Some("consider using `str::starts_with`"), + (&Literal(_), &Anchor(EndText)) if is_literal(&exprs[1..(exprs.len() - 1)]) => { + Some("consider using `str::ends_with`") + }, + _ if is_literal(exprs) => Some("consider using `str::contains`"), + _ => None, + }, + _ => None, + } +} + +fn check_set<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, utf8: bool) { + if_chain! { + if let ExprKind::AddrOf(BorrowKind::Ref, _, expr) = expr.kind; + if let ExprKind::Array(exprs) = expr.kind; + then { + for expr in exprs { + check_regex(cx, expr, utf8); + } + } + } +} + +fn check_regex<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, utf8: bool) { + let mut parser = regex_syntax::ParserBuilder::new() + .unicode(true) + .allow_invalid_utf8(!utf8) + .build(); + + if let ExprKind::Lit(ref lit) = expr.kind { + if let LitKind::Str(ref r, style) = lit.node { + let r = r.as_str(); + let offset = if let StrStyle::Raw(n) = style { 2 + n } else { 1 }; + match parser.parse(r) { + Ok(r) => { + if let Some(repl) = is_trivial_regex(&r) { + span_lint_and_help(cx, TRIVIAL_REGEX, expr.span, "trivial regex", None, repl); + } + }, + Err(regex_syntax::Error::Parse(e)) => { + span_lint( + cx, + INVALID_REGEX, + str_span(expr.span, *e.span(), offset), + &format!("regex syntax error: {}", e.kind()), + ); + }, + Err(regex_syntax::Error::Translate(e)) => { + span_lint( + cx, + INVALID_REGEX, + str_span(expr.span, *e.span(), offset), + &format!("regex syntax error: {}", e.kind()), + ); + }, + Err(e) => { + span_lint(cx, INVALID_REGEX, expr.span, &format!("regex syntax error: {}", e)); + }, + } + } + } else if let Some(r) = const_str(cx, expr) { + match parser.parse(&r) { + Ok(r) => { + if let Some(repl) = is_trivial_regex(&r) { + span_lint_and_help(cx, TRIVIAL_REGEX, expr.span, "trivial regex", None, repl); + } + }, + Err(regex_syntax::Error::Parse(e)) => { + span_lint( + cx, + INVALID_REGEX, + expr.span, + &format!("regex syntax error on position {}: {}", e.span().start.offset, e.kind()), + ); + }, + Err(regex_syntax::Error::Translate(e)) => { + span_lint( + cx, + INVALID_REGEX, + expr.span, + &format!("regex syntax error on position {}: {}", e.span().start.offset, e.kind()), + ); + }, + Err(e) => { + span_lint(cx, INVALID_REGEX, expr.span, &format!("regex syntax error: {}", e)); + }, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/renamed_lints.rs b/src/tools/clippy/clippy_lints/src/renamed_lints.rs new file mode 100644 index 000000000..ba03ef937 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/renamed_lints.rs @@ -0,0 +1,40 @@ +// This file is managed by `cargo dev rename_lint`. Prefer using that when possible. + +#[rustfmt::skip] +pub static RENAMED_LINTS: &[(&str, &str)] = &[ + ("clippy::block_in_if_condition_expr", "clippy::blocks_in_if_conditions"), + ("clippy::block_in_if_condition_stmt", "clippy::blocks_in_if_conditions"), + ("clippy::box_vec", "clippy::box_collection"), + ("clippy::const_static_lifetime", "clippy::redundant_static_lifetimes"), + ("clippy::cyclomatic_complexity", "clippy::cognitive_complexity"), + ("clippy::disallowed_method", "clippy::disallowed_methods"), + ("clippy::disallowed_type", "clippy::disallowed_types"), + ("clippy::eval_order_dependence", "clippy::mixed_read_write_in_expression"), + ("clippy::for_loop_over_option", "clippy::for_loops_over_fallibles"), + ("clippy::for_loop_over_result", "clippy::for_loops_over_fallibles"), + ("clippy::identity_conversion", "clippy::useless_conversion"), + ("clippy::if_let_some_result", "clippy::match_result_ok"), + ("clippy::new_without_default_derive", "clippy::new_without_default"), + ("clippy::option_and_then_some", "clippy::bind_instead_of_map"), + ("clippy::option_expect_used", "clippy::expect_used"), + ("clippy::option_map_unwrap_or", "clippy::map_unwrap_or"), + ("clippy::option_map_unwrap_or_else", "clippy::map_unwrap_or"), + ("clippy::option_unwrap_used", "clippy::unwrap_used"), + ("clippy::ref_in_deref", "clippy::needless_borrow"), + ("clippy::result_expect_used", "clippy::expect_used"), + ("clippy::result_map_unwrap_or_else", "clippy::map_unwrap_or"), + ("clippy::result_unwrap_used", "clippy::unwrap_used"), + ("clippy::single_char_push_str", "clippy::single_char_add_str"), + ("clippy::stutter", "clippy::module_name_repetitions"), + ("clippy::to_string_in_display", "clippy::recursive_format_impl"), + ("clippy::zero_width_space", "clippy::invisible_characters"), + ("clippy::drop_bounds", "drop_bounds"), + ("clippy::into_iter_on_array", "array_into_iter"), + ("clippy::invalid_atomic_ordering", "invalid_atomic_ordering"), + ("clippy::invalid_ref", "invalid_value"), + ("clippy::mem_discriminant_non_enum", "enum_intrinsics_non_enums"), + ("clippy::panic_params", "non_fmt_panics"), + ("clippy::temporary_cstring_as_ptr", "temporary_cstring_as_ptr"), + ("clippy::unknown_clippy_lints", "unknown_lints"), + ("clippy::unused_label", "unused_labels"), +]; diff --git a/src/tools/clippy/clippy_lints/src/repeat_once.rs b/src/tools/clippy/clippy_lints/src/repeat_once.rs new file mode 100644 index 000000000..898c70ace --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/repeat_once.rs @@ -0,0 +1,89 @@ +use clippy_utils::consts::{constant_context, Constant}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use clippy_utils::ty::is_type_diagnostic_item; +use if_chain::if_chain; +use rustc_errors::Applicability; +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 `.repeat(1)` and suggest the following method for each types. + /// - `.to_string()` for `str` + /// - `.clone()` for `String` + /// - `.to_vec()` for `slice` + /// + /// The lint will evaluate constant expressions and values as arguments of `.repeat(..)` and emit a message if + /// they are equivalent to `1`. (Related discussion in [rust-clippy#7306](https://github.com/rust-lang/rust-clippy/issues/7306)) + /// + /// ### Why is this bad? + /// For example, `String.repeat(1)` is equivalent to `.clone()`. If cloning + /// the string is the intention behind this, `clone()` should be used. + /// + /// ### Example + /// ```rust + /// fn main() { + /// let x = String::from("hello world").repeat(1); + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn main() { + /// let x = String::from("hello world").clone(); + /// } + /// ``` + #[clippy::version = "1.47.0"] + pub REPEAT_ONCE, + complexity, + "using `.repeat(1)` instead of `String.clone()`, `str.to_string()` or `slice.to_vec()` " +} + +declare_lint_pass!(RepeatOnce => [REPEAT_ONCE]); + +impl<'tcx> LateLintPass<'tcx> for RepeatOnce { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::MethodCall(path, [receiver, count], _) = &expr.kind; + if path.ident.name == sym!(repeat); + if constant_context(cx, cx.typeck_results()).expr(count) == Some(Constant::Int(1)); + if !receiver.span.from_expansion(); + then { + let ty = cx.typeck_results().expr_ty(receiver).peel_refs(); + if ty.is_str() { + span_lint_and_sugg( + cx, + REPEAT_ONCE, + expr.span, + "calling `repeat(1)` on str", + "consider using `.to_string()` instead", + format!("{}.to_string()", snippet(cx, receiver.span, r#""...""#)), + Applicability::MachineApplicable, + ); + } else if ty.builtin_index().is_some() { + span_lint_and_sugg( + cx, + REPEAT_ONCE, + expr.span, + "calling `repeat(1)` on slice", + "consider using `.to_vec()` instead", + format!("{}.to_vec()", snippet(cx, receiver.span, r#""...""#)), + Applicability::MachineApplicable, + ); + } else if is_type_diagnostic_item(cx, ty, sym::String) { + span_lint_and_sugg( + cx, + REPEAT_ONCE, + expr.span, + "calling `repeat(1)` on a string literal", + "consider using `.clone()` instead", + format!("{}.clone()", snippet(cx, receiver.span, r#""...""#)), + Applicability::MachineApplicable, + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/return_self_not_must_use.rs b/src/tools/clippy/clippy_lints/src/return_self_not_must_use.rs new file mode 100644 index 000000000..60be6bd33 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/return_self_not_must_use.rs @@ -0,0 +1,134 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::is_must_use_ty; +use clippy_utils::{nth_arg, return_ty}; +use rustc_hir::def_id::LocalDefId; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{Body, FnDecl, HirId, TraitItem, TraitItemKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// This lint warns when a method returning `Self` doesn't have the `#[must_use]` attribute. + /// + /// ### Why is this bad? + /// Methods returning `Self` often create new values, having the `#[must_use]` attribute + /// prevents users from "forgetting" to use the newly created value. + /// + /// The `#[must_use]` attribute can be added to the type itself to ensure that instances + /// are never forgotten. Functions returning a type marked with `#[must_use]` will not be + /// linted, as the usage is already enforced by the type attribute. + /// + /// ### Limitations + /// This lint is only applied on methods taking a `self` argument. It would be mostly noise + /// if it was added on constructors for example. + /// + /// ### Example + /// ```rust + /// pub struct Bar; + /// impl Bar { + /// // Missing attribute + /// pub fn bar(&self) -> Self { + /// Self + /// } + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// # { + /// // It's better to have the `#[must_use]` attribute on the method like this: + /// pub struct Bar; + /// impl Bar { + /// #[must_use] + /// pub fn bar(&self) -> Self { + /// Self + /// } + /// } + /// # } + /// + /// # { + /// // Or on the type definition like this: + /// #[must_use] + /// pub struct Bar; + /// impl Bar { + /// pub fn bar(&self) -> Self { + /// Self + /// } + /// } + /// # } + /// ``` + #[clippy::version = "1.59.0"] + pub RETURN_SELF_NOT_MUST_USE, + pedantic, + "missing `#[must_use]` annotation on a method returning `Self`" +} + +declare_lint_pass!(ReturnSelfNotMustUse => [RETURN_SELF_NOT_MUST_USE]); + +fn check_method(cx: &LateContext<'_>, decl: &FnDecl<'_>, fn_def: LocalDefId, span: Span, hir_id: HirId) { + if_chain! { + // If it comes from an external macro, better ignore it. + if !in_external_macro(cx.sess(), span); + if decl.implicit_self.has_implicit_self(); + // We only show this warning for public exported methods. + if cx.access_levels.is_exported(fn_def); + // We don't want to emit this lint if the `#[must_use]` attribute is already there. + if !cx.tcx.hir().attrs(hir_id).iter().any(|attr| attr.has_name(sym::must_use)); + if cx.tcx.visibility(fn_def.to_def_id()).is_public(); + let ret_ty = return_ty(cx, hir_id); + let self_arg = nth_arg(cx, hir_id, 0); + // If `Self` has the same type as the returned type, then we want to warn. + // + // For this check, we don't want to remove the reference on the returned type because if + // there is one, we shouldn't emit a warning! + if self_arg.peel_refs() == ret_ty; + // If `Self` is already marked as `#[must_use]`, no need for the attribute here. + if !is_must_use_ty(cx, ret_ty); + + then { + span_lint_and_help( + cx, + RETURN_SELF_NOT_MUST_USE, + span, + "missing `#[must_use]` attribute on a method returning `Self`", + None, + "consider adding the `#[must_use]` attribute to the method or directly to the `Self` type" + ); + } + } +} + +impl<'tcx> LateLintPass<'tcx> for ReturnSelfNotMustUse { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'tcx>, + _: &'tcx Body<'tcx>, + span: Span, + hir_id: HirId, + ) { + if_chain! { + // We are only interested in methods, not in functions or associated functions. + if matches!(kind, FnKind::Method(_, _)); + if let Some(fn_def) = cx.tcx.hir().opt_local_def_id(hir_id); + if let Some(impl_def) = cx.tcx.impl_of_method(fn_def.to_def_id()); + // We don't want this method to be te implementation of a trait because the + // `#[must_use]` should be put on the trait definition directly. + if cx.tcx.trait_id_of_impl(impl_def).is_none(); + + then { + check_method(cx, decl, fn_def, span, hir_id); + } + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'tcx>) { + if let TraitItemKind::Fn(ref sig, _) = item.kind { + check_method(cx, sig.decl, item.def_id, item.span, item.hir_id()); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/returns.rs b/src/tools/clippy/clippy_lints/src/returns.rs new file mode 100644 index 000000000..1d9a2abf7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/returns.rs @@ -0,0 +1,333 @@ +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::source::{snippet_opt, snippet_with_context}; +use clippy_utils::{fn_def_id, path_to_local_id}; +use if_chain::if_chain; +use rustc_ast::ast::Attribute; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_expr, FnKind, Visitor}; +use rustc_hir::{Block, Body, Expr, ExprKind, FnDecl, HirId, MatchSource, PatKind, StmtKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::subst::GenericArgKind; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `let`-bindings, which are subsequently + /// returned. + /// + /// ### Why is this bad? + /// It is just extraneous code. Remove it to make your code + /// more rusty. + /// + /// ### Example + /// ```rust + /// fn foo() -> String { + /// let x = String::new(); + /// x + /// } + /// ``` + /// instead, use + /// ``` + /// fn foo() -> String { + /// String::new() + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LET_AND_RETURN, + style, + "creating a let-binding and then immediately returning it like `let x = expr; x` at the end of a block" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for return statements at the end of a block. + /// + /// ### Why is this bad? + /// Removing the `return` and semicolon will make the code + /// more rusty. + /// + /// ### Example + /// ```rust + /// fn foo(x: usize) -> usize { + /// return x; + /// } + /// ``` + /// simplify to + /// ```rust + /// fn foo(x: usize) -> usize { + /// x + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NEEDLESS_RETURN, + style, + "using a return statement like `return expr;` where an expression would suffice" +} + +#[derive(PartialEq, Eq, Copy, Clone)] +enum RetReplacement { + Empty, + Block, + Unit, +} + +declare_lint_pass!(Return => [LET_AND_RETURN, NEEDLESS_RETURN]); + +impl<'tcx> LateLintPass<'tcx> for Return { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'_>) { + // we need both a let-binding stmt and an expr + if_chain! { + if let Some(retexpr) = block.expr; + if let Some(stmt) = block.stmts.iter().last(); + if let StmtKind::Local(local) = &stmt.kind; + if local.ty.is_none(); + if cx.tcx.hir().attrs(local.hir_id).is_empty(); + if let Some(initexpr) = &local.init; + if let PatKind::Binding(_, local_id, _, _) = local.pat.kind; + if path_to_local_id(retexpr, local_id); + if !last_statement_borrows(cx, initexpr); + if !in_external_macro(cx.sess(), initexpr.span); + if !in_external_macro(cx.sess(), retexpr.span); + if !local.span.from_expansion(); + then { + span_lint_hir_and_then( + cx, + LET_AND_RETURN, + retexpr.hir_id, + retexpr.span, + "returning the result of a `let` binding from a block", + |err| { + err.span_label(local.span, "unnecessary `let` binding"); + + if let Some(mut snippet) = snippet_opt(cx, initexpr.span) { + if !cx.typeck_results().expr_adjustments(retexpr).is_empty() { + snippet.push_str(" as _"); + } + err.multipart_suggestion( + "return the expression directly", + vec![ + (local.span, String::new()), + (retexpr.span, snippet), + ], + Applicability::MachineApplicable, + ); + } else { + err.span_help(initexpr.span, "this expression can be directly returned"); + } + }, + ); + } + } + } + + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + _: &'tcx FnDecl<'tcx>, + body: &'tcx Body<'tcx>, + _: Span, + _: HirId, + ) { + match kind { + FnKind::Closure => { + // when returning without value in closure, replace this `return` + // with an empty block to prevent invalid suggestion (see #6501) + let replacement = if let ExprKind::Ret(None) = &body.value.kind { + RetReplacement::Block + } else { + RetReplacement::Empty + }; + check_final_expr(cx, &body.value, Some(body.value.span), replacement); + }, + FnKind::ItemFn(..) | FnKind::Method(..) => { + if let ExprKind::Block(block, _) = body.value.kind { + check_block_return(cx, block); + } + }, + } + } +} + +fn attr_is_cfg(attr: &Attribute) -> bool { + attr.meta_item_list().is_some() && attr.has_name(sym::cfg) +} + +fn check_block_return<'tcx>(cx: &LateContext<'tcx>, block: &Block<'tcx>) { + if let Some(expr) = block.expr { + check_final_expr(cx, expr, Some(expr.span), RetReplacement::Empty); + } else if let Some(stmt) = block.stmts.iter().last() { + match stmt.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => { + check_final_expr(cx, expr, Some(stmt.span), RetReplacement::Empty); + }, + _ => (), + } + } +} + +fn check_final_expr<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'tcx>, + span: Option<Span>, + replacement: RetReplacement, +) { + match expr.kind { + // simple return is always "bad" + ExprKind::Ret(ref inner) => { + // allow `#[cfg(a)] return a; #[cfg(b)] return b;` + let attrs = cx.tcx.hir().attrs(expr.hir_id); + if !attrs.iter().any(attr_is_cfg) { + let borrows = inner.map_or(false, |inner| last_statement_borrows(cx, inner)); + if !borrows { + emit_return_lint( + cx, + inner.map_or(expr.hir_id, |inner| inner.hir_id), + span.expect("`else return` is not possible"), + inner.as_ref().map(|i| i.span), + replacement, + ); + } + } + }, + // a whole block? check it! + ExprKind::Block(block, _) => { + check_block_return(cx, block); + }, + ExprKind::If(_, then, else_clause_opt) => { + if let ExprKind::Block(ifblock, _) = then.kind { + check_block_return(cx, ifblock); + } + if let Some(else_clause) = else_clause_opt { + check_final_expr(cx, else_clause, None, RetReplacement::Empty); + } + }, + // a match expr, check all arms + // an if/if let expr, check both exprs + // note, if without else is going to be a type checking error anyways + // (except for unit type functions) so we don't match it + ExprKind::Match(_, arms, MatchSource::Normal) => { + for arm in arms.iter() { + check_final_expr(cx, arm.body, Some(arm.body.span), RetReplacement::Unit); + } + }, + ExprKind::DropTemps(expr) => check_final_expr(cx, expr, None, RetReplacement::Empty), + _ => (), + } +} + +fn emit_return_lint( + cx: &LateContext<'_>, + emission_place: HirId, + ret_span: Span, + inner_span: Option<Span>, + replacement: RetReplacement, +) { + if ret_span.from_expansion() { + return; + } + match inner_span { + Some(inner_span) => { + let mut applicability = Applicability::MachineApplicable; + span_lint_hir_and_then( + cx, + NEEDLESS_RETURN, + emission_place, + ret_span, + "unneeded `return` statement", + |diag| { + let (snippet, _) = snippet_with_context(cx, inner_span, ret_span.ctxt(), "..", &mut applicability); + diag.span_suggestion(ret_span, "remove `return`", snippet, applicability); + }, + ); + }, + None => match replacement { + RetReplacement::Empty => { + span_lint_hir_and_then( + cx, + NEEDLESS_RETURN, + emission_place, + ret_span, + "unneeded `return` statement", + |diag| { + diag.span_suggestion( + ret_span, + "remove `return`", + String::new(), + Applicability::MachineApplicable, + ); + }, + ); + }, + RetReplacement::Block => { + span_lint_hir_and_then( + cx, + NEEDLESS_RETURN, + emission_place, + ret_span, + "unneeded `return` statement", + |diag| { + diag.span_suggestion( + ret_span, + "replace `return` with an empty block", + "{}".to_string(), + Applicability::MachineApplicable, + ); + }, + ); + }, + RetReplacement::Unit => { + span_lint_hir_and_then( + cx, + NEEDLESS_RETURN, + emission_place, + ret_span, + "unneeded `return` statement", + |diag| { + diag.span_suggestion( + ret_span, + "replace `return` with a unit value", + "()".to_string(), + Applicability::MachineApplicable, + ); + }, + ); + }, + }, + } +} + +fn last_statement_borrows<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { + let mut visitor = BorrowVisitor { cx, borrows: false }; + walk_expr(&mut visitor, expr); + visitor.borrows +} + +struct BorrowVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + borrows: bool, +} + +impl<'tcx> Visitor<'tcx> for BorrowVisitor<'_, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + if self.borrows || expr.span.from_expansion() { + return; + } + + if let Some(def_id) = fn_def_id(self.cx, expr) { + self.borrows = self + .cx + .tcx + .fn_sig(def_id) + .output() + .skip_binder() + .walk() + .any(|arg| matches!(arg.unpack(), GenericArgKind::Lifetime(_))); + } + + walk_expr(self, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/same_name_method.rs b/src/tools/clippy/clippy_lints/src/same_name_method.rs new file mode 100644 index 000000000..20184d54b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/same_name_method.rs @@ -0,0 +1,166 @@ +use clippy_utils::diagnostics::span_lint_hir_and_then; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{HirId, Impl, ItemKind, Node, Path, QPath, TraitRef, TyKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::AssocKind; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::Symbol; +use rustc_span::Span; +use std::collections::{BTreeMap, BTreeSet}; + +declare_clippy_lint! { + /// ### What it does + /// It lints if a struct has two methods with the same name: + /// one from a trait, another not from trait. + /// + /// ### Why is this bad? + /// Confusing. + /// + /// ### Example + /// ```rust + /// trait T { + /// fn foo(&self) {} + /// } + /// + /// struct S; + /// + /// impl T for S { + /// fn foo(&self) {} + /// } + /// + /// impl S { + /// fn foo(&self) {} + /// } + /// ``` + #[clippy::version = "1.57.0"] + pub SAME_NAME_METHOD, + restriction, + "two method with same name" +} + +declare_lint_pass!(SameNameMethod => [SAME_NAME_METHOD]); + +struct ExistingName { + impl_methods: BTreeMap<Symbol, (Span, HirId)>, + trait_methods: BTreeMap<Symbol, Vec<Span>>, +} + +impl<'tcx> LateLintPass<'tcx> for SameNameMethod { + #[expect(clippy::too_many_lines)] + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + let mut map = FxHashMap::<Res, ExistingName>::default(); + + for id in cx.tcx.hir().items() { + if matches!(cx.tcx.def_kind(id.def_id), DefKind::Impl) + && let item = cx.tcx.hir().item(id) + && let ItemKind::Impl(Impl { + items, + of_trait, + self_ty, + .. + }) = &item.kind + && let TyKind::Path(QPath::Resolved(_, Path { res, .. })) = self_ty.kind + { + if !map.contains_key(res) { + map.insert( + *res, + ExistingName { + impl_methods: BTreeMap::new(), + trait_methods: BTreeMap::new(), + }, + ); + } + let existing_name = map.get_mut(res).unwrap(); + + match of_trait { + Some(trait_ref) => { + let mut methods_in_trait: BTreeSet<Symbol> = if_chain! { + if let Some(Node::TraitRef(TraitRef { path, .. })) = + cx.tcx.hir().find(trait_ref.hir_ref_id); + if let Res::Def(DefKind::Trait, did) = path.res; + then{ + // FIXME: if + // `rustc_middle::ty::assoc::AssocItems::items` is public, + // we can iterate its keys instead of `in_definition_order`, + // which's more efficient + cx.tcx + .associated_items(did) + .in_definition_order() + .filter(|assoc_item| { + matches!(assoc_item.kind, AssocKind::Fn) + }) + .map(|assoc_item| assoc_item.name) + .collect() + }else{ + BTreeSet::new() + } + }; + + let mut check_trait_method = |method_name: Symbol, trait_method_span: Span| { + if let Some((impl_span, hir_id)) = existing_name.impl_methods.get(&method_name) { + span_lint_hir_and_then( + cx, + SAME_NAME_METHOD, + *hir_id, + *impl_span, + "method's name is the same as an existing method in a trait", + |diag| { + diag.span_note( + trait_method_span, + &format!("existing `{}` defined here", method_name), + ); + }, + ); + } + if let Some(v) = existing_name.trait_methods.get_mut(&method_name) { + v.push(trait_method_span); + } else { + existing_name.trait_methods.insert(method_name, vec![trait_method_span]); + } + }; + + for impl_item_ref in (*items).iter().filter(|impl_item_ref| { + matches!(impl_item_ref.kind, rustc_hir::AssocItemKind::Fn { .. }) + }) { + let method_name = impl_item_ref.ident.name; + methods_in_trait.remove(&method_name); + check_trait_method(method_name, impl_item_ref.span); + } + + for method_name in methods_in_trait { + check_trait_method(method_name, item.span); + } + }, + None => { + for impl_item_ref in (*items).iter().filter(|impl_item_ref| { + matches!(impl_item_ref.kind, rustc_hir::AssocItemKind::Fn { .. }) + }) { + let method_name = impl_item_ref.ident.name; + let impl_span = impl_item_ref.span; + let hir_id = impl_item_ref.id.hir_id(); + if let Some(trait_spans) = existing_name.trait_methods.get(&method_name) { + span_lint_hir_and_then( + cx, + SAME_NAME_METHOD, + hir_id, + impl_span, + "method's name is the same as an existing method in a trait", + |diag| { + // TODO should we `span_note` on every trait? + // iterate on trait_spans? + diag.span_note( + trait_spans[0], + &format!("existing `{}` defined here", method_name), + ); + }, + ); + } + existing_name.impl_methods.insert(method_name, (impl_span, hir_id)); + } + }, + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/self_named_constructors.rs b/src/tools/clippy/clippy_lints/src/self_named_constructors.rs new file mode 100644 index 000000000..d07c26d7c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/self_named_constructors.rs @@ -0,0 +1,91 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::return_ty; +use clippy_utils::ty::{contains_adt_constructor, contains_ty}; +use rustc_hir::{Impl, ImplItem, ImplItemKind, ItemKind, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Warns when constructors have the same name as their types. + /// + /// ### Why is this bad? + /// Repeating the name of the type is redundant. + /// + /// ### Example + /// ```rust,ignore + /// struct Foo {} + /// + /// impl Foo { + /// pub fn foo() -> Foo { + /// Foo {} + /// } + /// } + /// ``` + /// Use instead: + /// ```rust,ignore + /// struct Foo {} + /// + /// impl Foo { + /// pub fn new() -> Foo { + /// Foo {} + /// } + /// } + /// ``` + #[clippy::version = "1.55.0"] + pub SELF_NAMED_CONSTRUCTORS, + style, + "method should not have the same name as the type it is implemented for" +} + +declare_lint_pass!(SelfNamedConstructors => [SELF_NAMED_CONSTRUCTORS]); + +impl<'tcx> LateLintPass<'tcx> for SelfNamedConstructors { + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) { + match impl_item.kind { + ImplItemKind::Fn(ref sig, _) => { + if sig.decl.implicit_self.has_implicit_self() { + return; + } + }, + _ => return, + } + + let parent = cx.tcx.hir().get_parent_item(impl_item.hir_id()); + let item = cx.tcx.hir().expect_item(parent); + let self_ty = cx.tcx.type_of(item.def_id); + let ret_ty = return_ty(cx, impl_item.hir_id()); + + // Do not check trait impls + if matches!(item.kind, ItemKind::Impl(Impl { of_trait: Some(_), .. })) { + return; + } + + // Ensure method is constructor-like + if let Some(self_adt) = self_ty.ty_adt_def() { + if !contains_adt_constructor(ret_ty, self_adt) { + return; + } + } else if !contains_ty(ret_ty, self_ty) { + return; + } + + if_chain! { + if let Some(self_def) = self_ty.ty_adt_def(); + if let Some(self_local_did) = self_def.did().as_local(); + let self_id = cx.tcx.hir().local_def_id_to_hir_id(self_local_did); + if let Some(Node::Item(x)) = cx.tcx.hir().find(self_id); + let type_name = x.ident.name.as_str().to_lowercase(); + if impl_item.ident.name.as_str() == type_name || impl_item.ident.name.as_str().replace('_', "") == type_name; + + then { + span_lint( + cx, + SELF_NAMED_CONSTRUCTORS, + impl_item.span, + &format!("constructor `{}` has the same name as the type", impl_item.ident.name), + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/semicolon_if_nothing_returned.rs b/src/tools/clippy/clippy_lints/src/semicolon_if_nothing_returned.rs new file mode 100644 index 000000000..729694da4 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/semicolon_if_nothing_returned.rs @@ -0,0 +1,70 @@ +use crate::rustc_lint::LintContext; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_macro_callsite; +use clippy_utils::sugg; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Block, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Looks for blocks of expressions and fires if the last expression returns + /// `()` but is not followed by a semicolon. + /// + /// ### Why is this bad? + /// The semicolon might be optional but when extending the block with new + /// code, it doesn't require a change in previous last line. + /// + /// ### Example + /// ```rust + /// fn main() { + /// println!("Hello world") + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn main() { + /// println!("Hello world"); + /// } + /// ``` + #[clippy::version = "1.52.0"] + pub SEMICOLON_IF_NOTHING_RETURNED, + pedantic, + "add a semicolon if nothing is returned" +} + +declare_lint_pass!(SemicolonIfNothingReturned => [SEMICOLON_IF_NOTHING_RETURNED]); + +impl<'tcx> LateLintPass<'tcx> for SemicolonIfNothingReturned { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) { + if_chain! { + if !block.span.from_expansion(); + if let Some(expr) = block.expr; + let t_expr = cx.typeck_results().expr_ty(expr); + if t_expr.is_unit(); + if let snippet = snippet_with_macro_callsite(cx, expr.span, "}"); + if !snippet.ends_with('}') && !snippet.ends_with(';'); + if cx.sess().source_map().is_multiline(block.span); + then { + // filter out the desugared `for` loop + if let ExprKind::DropTemps(..) = &expr.kind { + return; + } + + let sugg = sugg::Sugg::hir_with_macro_callsite(cx, expr, ".."); + let suggestion = format!("{0};", sugg); + span_lint_and_sugg( + cx, + SEMICOLON_IF_NOTHING_RETURNED, + expr.span.source_callsite(), + "consider adding a `;` to the last statement for consistent formatting", + "add a `;` here", + suggestion, + Applicability::MaybeIncorrect, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/serde_api.rs b/src/tools/clippy/clippy_lints/src/serde_api.rs new file mode 100644 index 000000000..fc1c2af92 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/serde_api.rs @@ -0,0 +1,60 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{get_trait_def_id, paths}; +use rustc_hir::{Impl, Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for mis-uses of the serde API. + /// + /// ### Why is this bad? + /// Serde is very finnicky about how its API should be + /// used, but the type system can't be used to enforce it (yet?). + /// + /// ### Example + /// Implementing `Visitor::visit_string` but not + /// `Visitor::visit_str`. + #[clippy::version = "pre 1.29.0"] + pub SERDE_API_MISUSE, + correctness, + "various things that will negatively affect your serde experience" +} + +declare_lint_pass!(SerdeApi => [SERDE_API_MISUSE]); + +impl<'tcx> LateLintPass<'tcx> for SerdeApi { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if let ItemKind::Impl(Impl { + of_trait: Some(ref trait_ref), + items, + .. + }) = item.kind + { + let did = trait_ref.path.res.def_id(); + if let Some(visit_did) = get_trait_def_id(cx, &paths::SERDE_DE_VISITOR) { + if did == visit_did { + let mut seen_str = None; + let mut seen_string = None; + for item in *items { + match item.ident.as_str() { + "visit_str" => seen_str = Some(item.span), + "visit_string" => seen_string = Some(item.span), + _ => {}, + } + } + if let Some(span) = seen_string { + if seen_str.is_none() { + span_lint( + cx, + SERDE_API_MISUSE, + span, + "you should not implement `visit_string` without also implementing `visit_str`", + ); + } + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/shadow.rs b/src/tools/clippy/clippy_lints/src/shadow.rs new file mode 100644 index 000000000..5dcdab5b8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/shadow.rs @@ -0,0 +1,252 @@ +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::source::snippet; +use clippy_utils::visitors::is_local_used; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::def::Res; +use rustc_hir::def_id::LocalDefId; +use rustc_hir::hir_id::ItemLocalId; +use rustc_hir::{Block, Body, BodyOwnerKind, Expr, ExprKind, HirId, Let, Node, Pat, PatKind, QPath, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{Span, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for bindings that shadow other bindings already in + /// scope, while just changing reference level or mutability. + /// + /// ### Why is this bad? + /// Not much, in fact it's a very common pattern in Rust + /// code. Still, some may opt to avoid it in their code base, they can set this + /// lint to `Warn`. + /// + /// ### Example + /// ```rust + /// # let x = 1; + /// let x = &x; + /// ``` + /// + /// Use instead: + /// ```rust + /// # let x = 1; + /// let y = &x; // use different variable name + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SHADOW_SAME, + restriction, + "rebinding a name to itself, e.g., `let mut x = &mut x`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for bindings that shadow other bindings already in + /// scope, while reusing the original value. + /// + /// ### Why is this bad? + /// Not too much, in fact it's a common pattern in Rust + /// code. Still, some argue that name shadowing like this hurts readability, + /// because a value may be bound to different things depending on position in + /// the code. + /// + /// ### Example + /// ```rust + /// let x = 2; + /// let x = x + 1; + /// ``` + /// use different variable name: + /// ```rust + /// let x = 2; + /// let y = x + 1; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SHADOW_REUSE, + restriction, + "rebinding a name to an expression that re-uses the original value, e.g., `let x = x + 1`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for bindings that shadow other bindings already in + /// scope, either without an initialization or with one that does not even use + /// the original value. + /// + /// ### Why is this bad? + /// Name shadowing can hurt readability, especially in + /// large code bases, because it is easy to lose track of the active binding at + /// any place in the code. This can be alleviated by either giving more specific + /// names to bindings or introducing more scopes to contain the bindings. + /// + /// ### Example + /// ```rust + /// # let y = 1; + /// # let z = 2; + /// let x = y; + /// let x = z; // shadows the earlier binding + /// ``` + /// + /// Use instead: + /// ```rust + /// # let y = 1; + /// # let z = 2; + /// let x = y; + /// let w = z; // use different variable name + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SHADOW_UNRELATED, + restriction, + "rebinding a name without even using the original value" +} + +#[derive(Default)] +pub(crate) struct Shadow { + bindings: Vec<(FxHashMap<Symbol, Vec<ItemLocalId>>, LocalDefId)>, +} + +impl_lint_pass!(Shadow => [SHADOW_SAME, SHADOW_REUSE, SHADOW_UNRELATED]); + +impl<'tcx> LateLintPass<'tcx> for Shadow { + fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) { + let (id, ident) = match pat.kind { + PatKind::Binding(_, hir_id, ident, _) => (hir_id, ident), + _ => return, + }; + + if pat.span.desugaring_kind().is_some() { + return; + } + + if ident.span.from_expansion() || ident.span.is_dummy() { + return; + } + + let HirId { owner, local_id } = id; + // get (or insert) the list of items for this owner and symbol + let (ref mut data, scope_owner) = *self.bindings.last_mut().unwrap(); + let items_with_name = data.entry(ident.name).or_default(); + + // check other bindings with the same name, most recently seen first + for &prev in items_with_name.iter().rev() { + if prev == local_id { + // repeated binding in an `Or` pattern + return; + } + + if is_shadow(cx, scope_owner, prev, local_id) { + let prev_hir_id = HirId { owner, local_id: prev }; + lint_shadow(cx, pat, prev_hir_id, ident.span); + // only lint against the "nearest" shadowed binding + break; + } + } + // store the binding + items_with_name.push(local_id); + } + + fn check_body(&mut self, cx: &LateContext<'_>, body: &Body<'_>) { + let hir = cx.tcx.hir(); + let owner_id = hir.body_owner_def_id(body.id()); + if !matches!(hir.body_owner_kind(owner_id), BodyOwnerKind::Closure) { + self.bindings.push((FxHashMap::default(), owner_id)); + } + } + + fn check_body_post(&mut self, cx: &LateContext<'_>, body: &Body<'_>) { + let hir = cx.tcx.hir(); + if !matches!( + hir.body_owner_kind(hir.body_owner_def_id(body.id())), + BodyOwnerKind::Closure + ) { + self.bindings.pop(); + } + } +} + +fn is_shadow(cx: &LateContext<'_>, owner: LocalDefId, first: ItemLocalId, second: ItemLocalId) -> bool { + let scope_tree = cx.tcx.region_scope_tree(owner.to_def_id()); + if let Some(first_scope) = scope_tree.var_scope(first) { + if let Some(second_scope) = scope_tree.var_scope(second) { + return scope_tree.is_subscope_of(second_scope, first_scope); + } + } + + false +} + +fn lint_shadow(cx: &LateContext<'_>, pat: &Pat<'_>, shadowed: HirId, span: Span) { + let (lint, msg) = match find_init(cx, pat.hir_id) { + Some(expr) if is_self_shadow(cx, pat, expr, shadowed) => { + let msg = format!( + "`{}` is shadowed by itself in `{}`", + snippet(cx, pat.span, "_"), + snippet(cx, expr.span, "..") + ); + (SHADOW_SAME, msg) + }, + Some(expr) if is_local_used(cx, expr, shadowed) => { + let msg = format!("`{}` is shadowed", snippet(cx, pat.span, "_")); + (SHADOW_REUSE, msg) + }, + _ => { + let msg = format!("`{}` shadows a previous, unrelated binding", snippet(cx, pat.span, "_")); + (SHADOW_UNRELATED, msg) + }, + }; + span_lint_and_note( + cx, + lint, + span, + &msg, + Some(cx.tcx.hir().span(shadowed)), + "previous binding is here", + ); +} + +/// Returns true if the expression is a simple transformation of a local binding such as `&x` +fn is_self_shadow(cx: &LateContext<'_>, pat: &Pat<'_>, mut expr: &Expr<'_>, hir_id: HirId) -> bool { + let hir = cx.tcx.hir(); + let is_direct_binding = hir + .parent_iter(pat.hir_id) + .map_while(|(_id, node)| match node { + Node::Pat(pat) => Some(pat), + _ => None, + }) + .all(|pat| matches!(pat.kind, PatKind::Ref(..) | PatKind::Or(_))); + if !is_direct_binding { + return false; + } + loop { + expr = match expr.kind { + ExprKind::Box(e) + | ExprKind::AddrOf(_, _, e) + | ExprKind::Block( + &Block { + stmts: [], + expr: Some(e), + .. + }, + _, + ) + | ExprKind::Unary(UnOp::Deref, e) => e, + ExprKind::Path(QPath::Resolved(None, path)) => break path.res == Res::Local(hir_id), + _ => break false, + } + } +} + +/// Finds the "init" expression for a pattern: `let <pat> = <init>;` (or `if let`) or +/// `match <init> { .., <pat> => .., .. }` +fn find_init<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId) -> Option<&'tcx Expr<'tcx>> { + for (_, node) in cx.tcx.hir().parent_iter(hir_id) { + let init = match node { + Node::Arm(_) | Node::Pat(_) => continue, + Node::Expr(expr) => match expr.kind { + ExprKind::Match(e, _, _) | ExprKind::Let(&Let { init: e, .. }) => Some(e), + _ => None, + }, + Node::Local(local) => local.init, + _ => None, + }; + return init; + } + None +} diff --git a/src/tools/clippy/clippy_lints/src/single_char_lifetime_names.rs b/src/tools/clippy/clippy_lints/src/single_char_lifetime_names.rs new file mode 100644 index 000000000..3dc995e2f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/single_char_lifetime_names.rs @@ -0,0 +1,63 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_ast::ast::{GenericParam, GenericParamKind}; +use rustc_lint::{EarlyContext, EarlyLintPass, 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 lifetimes with names which are one character + /// long. + /// + /// ### Why is this bad? + /// A single character is likely not enough to express the + /// purpose of a lifetime. Using a longer name can make code + /// easier to understand, especially for those who are new to + /// Rust. + /// + /// ### Known problems + /// Rust programmers and learning resources tend to use single + /// character lifetimes, so this lint is at odds with the + /// ecosystem at large. In addition, the lifetime's purpose may + /// be obvious or, rarely, expressible in one character. + /// + /// ### Example + /// ```rust + /// struct DiagnosticCtx<'a> { + /// source: &'a str, + /// } + /// ``` + /// Use instead: + /// ```rust + /// struct DiagnosticCtx<'src> { + /// source: &'src str, + /// } + /// ``` + #[clippy::version = "1.60.0"] + pub SINGLE_CHAR_LIFETIME_NAMES, + restriction, + "warns against single-character lifetime names" +} + +declare_lint_pass!(SingleCharLifetimeNames => [SINGLE_CHAR_LIFETIME_NAMES]); + +impl EarlyLintPass for SingleCharLifetimeNames { + fn check_generic_param(&mut self, ctx: &EarlyContext<'_>, param: &GenericParam) { + if in_external_macro(ctx.sess(), param.ident.span) { + return; + } + + if let GenericParamKind::Lifetime = param.kind { + if !param.is_placeholder && param.ident.as_str().len() <= 2 { + span_lint_and_help( + ctx, + SINGLE_CHAR_LIFETIME_NAMES, + param.ident.span, + "single-character lifetime names are likely uninformative", + None, + "use a more informative name", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs b/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs new file mode 100644 index 000000000..66b795130 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/single_component_path_imports.rs @@ -0,0 +1,175 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; +use rustc_ast::{ptr::P, Crate, Item, ItemKind, MacroDef, ModKind, UseTreeKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{edition::Edition, symbol::kw, Span, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checking for imports with single component use path. + /// + /// ### Why is this bad? + /// Import with single component use path such as `use cratename;` + /// is not necessary, and thus should be removed. + /// + /// ### Example + /// ```rust,ignore + /// use regex; + /// + /// fn main() { + /// regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); + /// } + /// ``` + /// Better as + /// ```rust,ignore + /// fn main() { + /// regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); + /// } + /// ``` + #[clippy::version = "1.43.0"] + pub SINGLE_COMPONENT_PATH_IMPORTS, + style, + "imports with single component path are redundant" +} + +declare_lint_pass!(SingleComponentPathImports => [SINGLE_COMPONENT_PATH_IMPORTS]); + +impl EarlyLintPass for SingleComponentPathImports { + fn check_crate(&mut self, cx: &EarlyContext<'_>, krate: &Crate) { + if cx.sess().opts.edition < Edition::Edition2018 { + return; + } + check_mod(cx, &krate.items); + } +} + +fn check_mod(cx: &EarlyContext<'_>, items: &[P<Item>]) { + // keep track of imports reused with `self` keyword, + // such as `self::crypto_hash` in the example below + // ```rust,ignore + // use self::crypto_hash::{Algorithm, Hasher}; + // ``` + let mut imports_reused_with_self = Vec::new(); + + // keep track of single use statements + // such as `crypto_hash` in the example below + // ```rust,ignore + // use crypto_hash; + // ``` + let mut single_use_usages = Vec::new(); + + // keep track of macros defined in the module as we don't want it to trigger on this (#7106) + // ```rust,ignore + // macro_rules! foo { () => {} }; + // pub(crate) use foo; + // ``` + let mut macros = Vec::new(); + + for item in items { + track_uses( + cx, + item, + &mut imports_reused_with_self, + &mut single_use_usages, + &mut macros, + ); + } + + for (name, span, can_suggest) in single_use_usages { + if !imports_reused_with_self.contains(&name) { + if can_suggest { + span_lint_and_sugg( + cx, + SINGLE_COMPONENT_PATH_IMPORTS, + span, + "this import is redundant", + "remove it entirely", + String::new(), + Applicability::MachineApplicable, + ); + } else { + span_lint_and_help( + cx, + SINGLE_COMPONENT_PATH_IMPORTS, + span, + "this import is redundant", + None, + "remove this import", + ); + } + } + } +} + +fn track_uses( + cx: &EarlyContext<'_>, + item: &Item, + imports_reused_with_self: &mut Vec<Symbol>, + single_use_usages: &mut Vec<(Symbol, Span, bool)>, + macros: &mut Vec<Symbol>, +) { + if item.span.from_expansion() || item.vis.kind.is_pub() { + return; + } + + match &item.kind { + ItemKind::Mod(_, ModKind::Loaded(ref items, ..)) => { + check_mod(cx, items); + }, + ItemKind::MacroDef(MacroDef { macro_rules: true, .. }) => { + macros.push(item.ident.name); + }, + ItemKind::Use(use_tree) => { + let segments = &use_tree.prefix.segments; + + // keep track of `use some_module;` usages + if segments.len() == 1 { + if let UseTreeKind::Simple(None, _, _) = use_tree.kind { + let name = segments[0].ident.name; + if !macros.contains(&name) { + single_use_usages.push((name, item.span, true)); + } + } + return; + } + + if segments.is_empty() { + // keep track of `use {some_module, some_other_module};` usages + if let UseTreeKind::Nested(trees) = &use_tree.kind { + for tree in trees { + let segments = &tree.0.prefix.segments; + if segments.len() == 1 { + if let UseTreeKind::Simple(None, _, _) = tree.0.kind { + let name = segments[0].ident.name; + if !macros.contains(&name) { + single_use_usages.push((name, tree.0.span, false)); + } + } + } + } + } + } else { + // keep track of `use self::some_module` usages + if segments[0].ident.name == kw::SelfLower { + // simple case such as `use self::module::SomeStruct` + if segments.len() > 1 { + imports_reused_with_self.push(segments[1].ident.name); + return; + } + + // nested case such as `use self::{module1::Struct1, module2::Struct2}` + if let UseTreeKind::Nested(trees) = &use_tree.kind { + for tree in trees { + let segments = &tree.0.prefix.segments; + if !segments.is_empty() { + imports_reused_with_self.push(segments[0].ident.name); + } + } + } + } + } + }, + _ => {}, + } +} diff --git a/src/tools/clippy/clippy_lints/src/size_of_in_element_count.rs b/src/tools/clippy/clippy_lints/src/size_of_in_element_count.rs new file mode 100644 index 000000000..bfb9f0d01 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/size_of_in_element_count.rs @@ -0,0 +1,154 @@ +//! Lint on use of `size_of` or `size_of_val` of T in an expression +//! expecting a count of T + +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_hir::BinOpKind; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, Ty, TypeAndMut}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Detects expressions where + /// `size_of::<T>` or `size_of_val::<T>` is used as a + /// count of elements of type `T` + /// + /// ### Why is this bad? + /// These functions expect a count + /// of `T` and not a number of bytes + /// + /// ### Example + /// ```rust,no_run + /// # use std::ptr::copy_nonoverlapping; + /// # use std::mem::size_of; + /// const SIZE: usize = 128; + /// let x = [2u8; SIZE]; + /// let mut y = [2u8; SIZE]; + /// unsafe { copy_nonoverlapping(x.as_ptr(), y.as_mut_ptr(), size_of::<u8>() * SIZE) }; + /// ``` + #[clippy::version = "1.50.0"] + pub SIZE_OF_IN_ELEMENT_COUNT, + correctness, + "using `size_of::<T>` or `size_of_val::<T>` where a count of elements of `T` is expected" +} + +declare_lint_pass!(SizeOfInElementCount => [SIZE_OF_IN_ELEMENT_COUNT]); + +fn get_size_of_ty<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, inverted: bool) -> Option<Ty<'tcx>> { + match expr.kind { + ExprKind::Call(count_func, _func_args) => { + if_chain! { + if !inverted; + if let ExprKind::Path(ref count_func_qpath) = count_func.kind; + if let Some(def_id) = cx.qpath_res(count_func_qpath, count_func.hir_id).opt_def_id(); + if matches!(cx.tcx.get_diagnostic_name(def_id), Some(sym::mem_size_of | sym::mem_size_of_val)); + then { + cx.typeck_results().node_substs(count_func.hir_id).types().next() + } else { + None + } + } + }, + ExprKind::Binary(op, left, right) if BinOpKind::Mul == op.node => { + get_size_of_ty(cx, left, inverted).or_else(|| get_size_of_ty(cx, right, inverted)) + }, + ExprKind::Binary(op, left, right) if BinOpKind::Div == op.node => { + get_size_of_ty(cx, left, inverted).or_else(|| get_size_of_ty(cx, right, !inverted)) + }, + ExprKind::Cast(expr, _) => get_size_of_ty(cx, expr, inverted), + _ => None, + } +} + +fn get_pointee_ty_and_count_expr<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, +) -> Option<(Ty<'tcx>, &'tcx Expr<'tcx>)> { + const FUNCTIONS: [&[&str]; 8] = [ + &paths::PTR_COPY_NONOVERLAPPING, + &paths::PTR_COPY, + &paths::PTR_WRITE_BYTES, + &paths::PTR_SWAP_NONOVERLAPPING, + &paths::PTR_SLICE_FROM_RAW_PARTS, + &paths::PTR_SLICE_FROM_RAW_PARTS_MUT, + &paths::SLICE_FROM_RAW_PARTS, + &paths::SLICE_FROM_RAW_PARTS_MUT, + ]; + const METHODS: [&str; 11] = [ + "write_bytes", + "copy_to", + "copy_from", + "copy_to_nonoverlapping", + "copy_from_nonoverlapping", + "add", + "wrapping_add", + "sub", + "wrapping_sub", + "offset", + "wrapping_offset", + ]; + + if_chain! { + // Find calls to ptr::{copy, copy_nonoverlapping} + // and ptr::{swap_nonoverlapping, write_bytes}, + if let ExprKind::Call(func, [.., count]) = expr.kind; + if let ExprKind::Path(ref func_qpath) = func.kind; + if let Some(def_id) = cx.qpath_res(func_qpath, func.hir_id).opt_def_id(); + if FUNCTIONS.iter().any(|func_path| match_def_path(cx, def_id, func_path)); + + // Get the pointee type + if let Some(pointee_ty) = cx.typeck_results().node_substs(func.hir_id).types().next(); + then { + return Some((pointee_ty, count)); + } + }; + if_chain! { + // Find calls to copy_{from,to}{,_nonoverlapping} and write_bytes methods + if let ExprKind::MethodCall(method_path, [ptr_self, .., count], _) = expr.kind; + let method_ident = method_path.ident.as_str(); + if METHODS.iter().any(|m| *m == method_ident); + + // Get the pointee type + if let ty::RawPtr(TypeAndMut { ty: pointee_ty, .. }) = + cx.typeck_results().expr_ty(ptr_self).kind(); + then { + return Some((*pointee_ty, count)); + } + }; + None +} + +impl<'tcx> LateLintPass<'tcx> for SizeOfInElementCount { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + const HELP_MSG: &str = "use a count of elements instead of a count of bytes\ + , it already gets multiplied by the size of the type"; + + const LINT_MSG: &str = "found a count of bytes \ + instead of a count of elements of `T`"; + + if_chain! { + // Find calls to functions with an element count parameter and get + // the pointee type and count parameter expression + if let Some((pointee_ty, count_expr)) = get_pointee_ty_and_count_expr(cx, expr); + + // Find a size_of call in the count parameter expression and + // check that it's the same type + if let Some(ty_used_for_size_of) = get_size_of_ty(cx, count_expr, false); + if pointee_ty == ty_used_for_size_of; + then { + span_lint_and_help( + cx, + SIZE_OF_IN_ELEMENT_COUNT, + count_expr.span, + LINT_MSG, + None, + HELP_MSG + ); + } + }; + } +} diff --git a/src/tools/clippy/clippy_lints/src/slow_vector_initialization.rs b/src/tools/clippy/clippy_lints/src/slow_vector_initialization.rs new file mode 100644 index 000000000..2c8aa17e8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/slow_vector_initialization.rs @@ -0,0 +1,312 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{get_enclosing_block, is_expr_path_def_path, path_to_local, path_to_local_id, paths, SpanlessEq}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_block, walk_expr, walk_stmt, Visitor}; +use rustc_hir::{BindingAnnotation, Block, Expr, ExprKind, HirId, PatKind, QPath, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks slow zero-filled vector initialization + /// + /// ### Why is this bad? + /// These structures are non-idiomatic and less efficient than simply using + /// `vec![0; len]`. + /// + /// ### Example + /// ```rust + /// # use core::iter::repeat; + /// # let len = 4; + /// let mut vec1 = Vec::with_capacity(len); + /// vec1.resize(len, 0); + /// + /// let mut vec1 = Vec::with_capacity(len); + /// vec1.resize(vec1.capacity(), 0); + /// + /// let mut vec2 = Vec::with_capacity(len); + /// vec2.extend(repeat(0).take(len)); + /// ``` + /// + /// Use instead: + /// ```rust + /// # let len = 4; + /// let mut vec1 = vec![0; len]; + /// let mut vec2 = vec![0; len]; + /// ``` + #[clippy::version = "1.32.0"] + pub SLOW_VECTOR_INITIALIZATION, + perf, + "slow vector initialization" +} + +declare_lint_pass!(SlowVectorInit => [SLOW_VECTOR_INITIALIZATION]); + +/// `VecAllocation` contains data regarding a vector allocated with `with_capacity` and then +/// assigned to a variable. For example, `let mut vec = Vec::with_capacity(0)` or +/// `vec = Vec::with_capacity(0)` +struct VecAllocation<'tcx> { + /// HirId of the variable + local_id: HirId, + + /// Reference to the expression which allocates the vector + allocation_expr: &'tcx Expr<'tcx>, + + /// Reference to the expression used as argument on `with_capacity` call. This is used + /// to only match slow zero-filling idioms of the same length than vector initialization. + len_expr: &'tcx Expr<'tcx>, +} + +/// Type of slow initialization +enum InitializationType<'tcx> { + /// Extend is a slow initialization with the form `vec.extend(repeat(0).take(..))` + Extend(&'tcx Expr<'tcx>), + + /// Resize is a slow initialization with the form `vec.resize(.., 0)` + Resize(&'tcx Expr<'tcx>), +} + +impl<'tcx> LateLintPass<'tcx> for SlowVectorInit { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // Matches initialization on reassignements. For example: `vec = Vec::with_capacity(100)` + if_chain! { + if let ExprKind::Assign(left, right, _) = expr.kind; + + // Extract variable + if let Some(local_id) = path_to_local(left); + + // Extract len argument + if let Some(len_arg) = Self::is_vec_with_capacity(cx, right); + + then { + let vi = VecAllocation { + local_id, + allocation_expr: right, + len_expr: len_arg, + }; + + Self::search_initialization(cx, vi, expr.hir_id); + } + } + } + + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + // Matches statements which initializes vectors. For example: `let mut vec = Vec::with_capacity(10)` + if_chain! { + if let StmtKind::Local(local) = stmt.kind; + if let PatKind::Binding(BindingAnnotation::Mutable, local_id, _, None) = local.pat.kind; + if let Some(init) = local.init; + if let Some(len_arg) = Self::is_vec_with_capacity(cx, init); + + then { + let vi = VecAllocation { + local_id, + allocation_expr: init, + len_expr: len_arg, + }; + + Self::search_initialization(cx, vi, stmt.hir_id); + } + } + } +} + +impl SlowVectorInit { + /// Checks if the given expression is `Vec::with_capacity(..)`. It will return the expression + /// of the first argument of `with_capacity` call if it matches or `None` if it does not. + fn is_vec_with_capacity<'tcx>(cx: &LateContext<'_>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { + if_chain! { + if let ExprKind::Call(func, [arg]) = expr.kind; + if let ExprKind::Path(QPath::TypeRelative(ty, name)) = func.kind; + if name.ident.as_str() == "with_capacity"; + if is_type_diagnostic_item(cx, cx.typeck_results().node_type(ty.hir_id), sym::Vec); + then { + Some(arg) + } else { + None + } + } + } + + /// Search initialization for the given vector + fn search_initialization<'tcx>(cx: &LateContext<'tcx>, vec_alloc: VecAllocation<'tcx>, parent_node: HirId) { + let enclosing_body = get_enclosing_block(cx, parent_node); + + if enclosing_body.is_none() { + return; + } + + let mut v = VectorInitializationVisitor { + cx, + vec_alloc, + slow_expression: None, + initialization_found: false, + }; + + v.visit_block(enclosing_body.unwrap()); + + if let Some(ref allocation_expr) = v.slow_expression { + Self::lint_initialization(cx, allocation_expr, &v.vec_alloc); + } + } + + fn lint_initialization<'tcx>( + cx: &LateContext<'tcx>, + initialization: &InitializationType<'tcx>, + vec_alloc: &VecAllocation<'_>, + ) { + match initialization { + InitializationType::Extend(e) | InitializationType::Resize(e) => { + Self::emit_lint(cx, e, vec_alloc, "slow zero-filling initialization"); + }, + }; + } + + fn emit_lint<'tcx>(cx: &LateContext<'tcx>, slow_fill: &Expr<'_>, vec_alloc: &VecAllocation<'_>, msg: &str) { + let len_expr = Sugg::hir(cx, vec_alloc.len_expr, "len"); + + span_lint_and_then(cx, SLOW_VECTOR_INITIALIZATION, slow_fill.span, msg, |diag| { + diag.span_suggestion( + vec_alloc.allocation_expr.span, + "consider replace allocation with", + format!("vec![0; {}]", len_expr), + Applicability::Unspecified, + ); + }); + } +} + +/// `VectorInitializationVisitor` searches for unsafe or slow vector initializations for the given +/// vector. +struct VectorInitializationVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + + /// Contains the information. + vec_alloc: VecAllocation<'tcx>, + + /// Contains the slow initialization expression, if one was found. + slow_expression: Option<InitializationType<'tcx>>, + + /// `true` if the initialization of the vector has been found on the visited block. + initialization_found: bool, +} + +impl<'a, 'tcx> VectorInitializationVisitor<'a, 'tcx> { + /// Checks if the given expression is extending a vector with `repeat(0).take(..)` + fn search_slow_extend_filling(&mut self, expr: &'tcx Expr<'_>) { + if_chain! { + if self.initialization_found; + if let ExprKind::MethodCall(path, [self_arg, extend_arg], _) = expr.kind; + if path_to_local_id(self_arg, self.vec_alloc.local_id); + if path.ident.name == sym!(extend); + if self.is_repeat_take(extend_arg); + + then { + self.slow_expression = Some(InitializationType::Extend(expr)); + } + } + } + + /// Checks if the given expression is resizing a vector with 0 + fn search_slow_resize_filling(&mut self, expr: &'tcx Expr<'_>) { + if self.initialization_found + && let ExprKind::MethodCall(path, [self_arg, len_arg, fill_arg], _) = expr.kind + && path_to_local_id(self_arg, self.vec_alloc.local_id) + && path.ident.name == sym!(resize) + // Check that is filled with 0 + && let ExprKind::Lit(ref lit) = fill_arg.kind + && let LitKind::Int(0, _) = lit.node { + // Check that len expression is equals to `with_capacity` expression + if SpanlessEq::new(self.cx).eq_expr(len_arg, self.vec_alloc.len_expr) { + self.slow_expression = Some(InitializationType::Resize(expr)); + } else if let ExprKind::MethodCall(path, _, _) = len_arg.kind && path.ident.as_str() == "capacity" { + self.slow_expression = Some(InitializationType::Resize(expr)); + } + } + } + + /// Returns `true` if give expression is `repeat(0).take(...)` + fn is_repeat_take(&self, expr: &Expr<'_>) -> bool { + if_chain! { + if let ExprKind::MethodCall(take_path, take_args, _) = expr.kind; + if take_path.ident.name == sym!(take); + + // Check that take is applied to `repeat(0)` + if let Some(repeat_expr) = take_args.get(0); + if self.is_repeat_zero(repeat_expr); + + if let Some(len_arg) = take_args.get(1); + + then { + // Check that len expression is equals to `with_capacity` expression + if SpanlessEq::new(self.cx).eq_expr(len_arg, self.vec_alloc.len_expr) { + return true; + } else if let ExprKind::MethodCall(path, _, _) = len_arg.kind && path.ident.as_str() == "capacity" { + return true; + } + } + } + + false + } + + /// Returns `true` if given expression is `repeat(0)` + fn is_repeat_zero(&self, expr: &Expr<'_>) -> bool { + if_chain! { + if let ExprKind::Call(fn_expr, [repeat_arg]) = expr.kind; + if is_expr_path_def_path(self.cx, fn_expr, &paths::ITER_REPEAT); + if let ExprKind::Lit(ref lit) = repeat_arg.kind; + if let LitKind::Int(0, _) = lit.node; + + then { + true + } else { + false + } + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for VectorInitializationVisitor<'a, 'tcx> { + fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) { + if self.initialization_found { + match stmt.kind { + StmtKind::Expr(expr) | StmtKind::Semi(expr) => { + self.search_slow_extend_filling(expr); + self.search_slow_resize_filling(expr); + }, + _ => (), + } + + self.initialization_found = false; + } else { + walk_stmt(self, stmt); + } + } + + fn visit_block(&mut self, block: &'tcx Block<'_>) { + if self.initialization_found { + if let Some(s) = block.stmts.get(0) { + self.visit_stmt(s); + } + + self.initialization_found = false; + } else { + walk_block(self, block); + } + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + // Skip all the expressions previous to the vector initialization + if self.vec_alloc.allocation_expr.hir_id == expr.hir_id { + self.initialization_found = true; + } + + walk_expr(self, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/stable_sort_primitive.rs b/src/tools/clippy/clippy_lints/src/stable_sort_primitive.rs new file mode 100644 index 000000000..a6c685df7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/stable_sort_primitive.rs @@ -0,0 +1,145 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{is_slice_of_primitives, sugg::Sugg}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// When sorting primitive values (integers, bools, chars, as well + /// as arrays, slices, and tuples of such items), it is typically better to + /// use an unstable sort than a stable sort. + /// + /// ### Why is this bad? + /// Typically, using a stable sort consumes more memory and cpu cycles. + /// Because values which compare equal are identical, preserving their + /// relative order (the guarantee that a stable sort provides) means + /// nothing, while the extra costs still apply. + /// + /// ### Known problems + /// + /// As pointed out in + /// [issue #8241](https://github.com/rust-lang/rust-clippy/issues/8241), + /// a stable sort can instead be significantly faster for certain scenarios + /// (eg. when a sorted vector is extended with new data and resorted). + /// + /// For more information and benchmarking results, please refer to the + /// issue linked above. + /// + /// ### Example + /// ```rust + /// let mut vec = vec![2, 1, 3]; + /// vec.sort(); + /// ``` + /// Use instead: + /// ```rust + /// let mut vec = vec![2, 1, 3]; + /// vec.sort_unstable(); + /// ``` + #[clippy::version = "1.47.0"] + pub STABLE_SORT_PRIMITIVE, + pedantic, + "use of sort() when sort_unstable() is equivalent" +} + +declare_lint_pass!(StableSortPrimitive => [STABLE_SORT_PRIMITIVE]); + +/// The three "kinds" of sorts +enum SortingKind { + Vanilla, + /* The other kinds of lint are currently commented out because they + * can map distinct values to equal ones. If the key function is + * provably one-to-one, or if the Cmp function conserves equality, + * then they could be linted on, but I don't know if we can check + * for that. */ + + /* ByKey, + * ByCmp, */ +} +impl SortingKind { + /// The name of the stable version of this kind of sort + fn stable_name(&self) -> &str { + match self { + SortingKind::Vanilla => "sort", + /* SortingKind::ByKey => "sort_by_key", + * SortingKind::ByCmp => "sort_by", */ + } + } + /// The name of the unstable version of this kind of sort + fn unstable_name(&self) -> &str { + match self { + SortingKind::Vanilla => "sort_unstable", + /* SortingKind::ByKey => "sort_unstable_by_key", + * SortingKind::ByCmp => "sort_unstable_by", */ + } + } + /// Takes the name of a function call and returns the kind of sort + /// that corresponds to that function name (or None if it isn't) + fn from_stable_name(name: &str) -> Option<SortingKind> { + match name { + "sort" => Some(SortingKind::Vanilla), + // "sort_by" => Some(SortingKind::ByCmp), + // "sort_by_key" => Some(SortingKind::ByKey), + _ => None, + } + } +} + +/// A detected instance of this lint +struct LintDetection { + slice_name: String, + method: SortingKind, + method_args: String, + slice_type: String, +} + +fn detect_stable_sort_primitive(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<LintDetection> { + if_chain! { + if let ExprKind::MethodCall(method_name, args, _) = &expr.kind; + if let Some(slice) = &args.get(0); + if let Some(method) = SortingKind::from_stable_name(method_name.ident.name.as_str()); + if let Some(slice_type) = is_slice_of_primitives(cx, slice); + then { + let args_str = args.iter().skip(1).map(|arg| Sugg::hir(cx, arg, "..").to_string()).collect::<Vec<String>>().join(", "); + Some(LintDetection { slice_name: Sugg::hir(cx, slice, "..").to_string(), method, method_args: args_str, slice_type }) + } else { + None + } + } +} + +impl LateLintPass<'_> for StableSortPrimitive { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if let Some(detection) = detect_stable_sort_primitive(cx, expr) { + span_lint_and_then( + cx, + STABLE_SORT_PRIMITIVE, + expr.span, + format!( + "used `{}` on primitive type `{}`", + detection.method.stable_name(), + detection.slice_type, + ) + .as_str(), + |diag| { + diag.span_suggestion( + expr.span, + "try", + format!( + "{}.{}({})", + detection.slice_name, + detection.method.unstable_name(), + detection.method_args, + ), + Applicability::MachineApplicable, + ); + diag.note( + "an unstable sort typically performs faster without any observable difference for this data type", + ); + }, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/std_instead_of_core.rs b/src/tools/clippy/clippy_lints/src/std_instead_of_core.rs new file mode 100644 index 000000000..ffd63cc68 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/std_instead_of_core.rs @@ -0,0 +1,148 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{def::Res, HirId, Path, PathSegment}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{sym, symbol::kw, Span}; + +declare_clippy_lint! { + /// ### What it does + /// + /// Finds items imported through `std` when available through `core`. + /// + /// ### Why is this bad? + /// + /// Crates which have `no_std` compatibility may wish to ensure types are imported from core to ensure + /// disabling `std` does not cause the crate to fail to compile. This lint is also useful for crates + /// migrating to become `no_std` compatible. + /// + /// ### Example + /// ```rust + /// use std::hash::Hasher; + /// ``` + /// Use instead: + /// ```rust + /// use core::hash::Hasher; + /// ``` + #[clippy::version = "1.64.0"] + pub STD_INSTEAD_OF_CORE, + restriction, + "type is imported from std when available in core" +} + +declare_clippy_lint! { + /// ### What it does + /// + /// Finds items imported through `std` when available through `alloc`. + /// + /// ### Why is this bad? + /// + /// Crates which have `no_std` compatibility and require alloc may wish to ensure types are imported from + /// alloc to ensure disabling `std` does not cause the crate to fail to compile. This lint is also useful + /// for crates migrating to become `no_std` compatible. + /// + /// ### Example + /// ```rust + /// use std::vec::Vec; + /// ``` + /// Use instead: + /// ```rust + /// # extern crate alloc; + /// use alloc::vec::Vec; + /// ``` + #[clippy::version = "1.64.0"] + pub STD_INSTEAD_OF_ALLOC, + restriction, + "type is imported from std when available in alloc" +} + +declare_clippy_lint! { + /// ### What it does + /// + /// Finds items imported through `alloc` when available through `core`. + /// + /// ### Why is this bad? + /// + /// Crates which have `no_std` compatibility and may optionally require alloc may wish to ensure types are + /// imported from core to ensure disabling `alloc` does not cause the crate to fail to compile. This lint + /// is also useful for crates migrating to become `no_std` compatible. + /// + /// ### Example + /// ```rust + /// # extern crate alloc; + /// use alloc::slice::from_ref; + /// ``` + /// Use instead: + /// ```rust + /// use core::slice::from_ref; + /// ``` + #[clippy::version = "1.64.0"] + pub ALLOC_INSTEAD_OF_CORE, + restriction, + "type is imported from alloc when available in core" +} + +#[derive(Default)] +pub struct StdReexports { + // Paths which can be either a module or a macro (e.g. `std::env`) will cause this check to happen + // twice. First for the mod, second for the macro. This is used to avoid the lint reporting for the macro + // when the path could be also be used to access the module. + prev_span: Span, +} +impl_lint_pass!(StdReexports => [STD_INSTEAD_OF_CORE, STD_INSTEAD_OF_ALLOC, ALLOC_INSTEAD_OF_CORE]); + +impl<'tcx> LateLintPass<'tcx> for StdReexports { + fn check_path(&mut self, cx: &LateContext<'tcx>, path: &Path<'tcx>, _: HirId) { + if let Res::Def(_, def_id) = path.res + && let Some(first_segment) = get_first_segment(path) + { + let (lint, msg, help) = match first_segment.ident.name { + sym::std => match cx.tcx.crate_name(def_id.krate) { + sym::core => ( + STD_INSTEAD_OF_CORE, + "used import from `std` instead of `core`", + "consider importing the item from `core`", + ), + sym::alloc => ( + STD_INSTEAD_OF_ALLOC, + "used import from `std` instead of `alloc`", + "consider importing the item from `alloc`", + ), + _ => { + self.prev_span = path.span; + return; + }, + }, + sym::alloc => { + if cx.tcx.crate_name(def_id.krate) == sym::core { + ( + ALLOC_INSTEAD_OF_CORE, + "used import from `alloc` instead of `core`", + "consider importing the item from `core`", + ) + } else { + self.prev_span = path.span; + return; + } + }, + _ => return, + }; + if path.span != self.prev_span { + span_lint_and_help(cx, lint, path.span, msg, None, help); + self.prev_span = path.span; + } + } + } +} + +/// Returns the first named segment of a [`Path`]. +/// +/// If this is a global path (such as `::std::fmt::Debug`), then the segment after [`kw::PathRoot`] +/// is returned. +fn get_first_segment<'tcx>(path: &Path<'tcx>) -> Option<&'tcx PathSegment<'tcx>> { + match path.segments { + // A global path will have PathRoot as the first segment. In this case, return the segment after. + [x, y, ..] if x.ident.name == kw::PathRoot => Some(y), + [x, ..] => Some(x), + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/strings.rs b/src/tools/clippy/clippy_lints/src/strings.rs new file mode 100644 index 000000000..22eb06b36 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/strings.rs @@ -0,0 +1,517 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg}; +use clippy_utils::source::{snippet, snippet_with_applicability}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{get_parent_expr, is_lint_allowed, match_function_call, method_calls, paths}; +use clippy_utils::{peel_blocks, SpanlessEq}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::def_id::DefId; +use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, LangItem, QPath}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for string appends of the form `x = x + y` (without + /// `let`!). + /// + /// ### Why is this bad? + /// It's not really bad, but some people think that the + /// `.push_str(_)` method is more readable. + /// + /// ### Example + /// ```rust + /// let mut x = "Hello".to_owned(); + /// x = x + ", World"; + /// + /// // More readable + /// x += ", World"; + /// x.push_str(", World"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub STRING_ADD_ASSIGN, + pedantic, + "using `x = x + ..` where x is a `String` instead of `push_str()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for all instances of `x + _` where `x` is of type + /// `String`, but only if [`string_add_assign`](#string_add_assign) does *not* + /// match. + /// + /// ### Why is this bad? + /// It's not bad in and of itself. However, this particular + /// `Add` implementation is asymmetric (the other operand need not be `String`, + /// but `x` does), while addition as mathematically defined is symmetric, also + /// the `String::push_str(_)` function is a perfectly good replacement. + /// Therefore, some dislike it and wish not to have it in their code. + /// + /// That said, other people think that string addition, having a long tradition + /// in other languages is actually fine, which is why we decided to make this + /// particular lint `allow` by default. + /// + /// ### Example + /// ```rust + /// let x = "Hello".to_owned(); + /// x + ", World"; + /// ``` + /// + /// Use instead: + /// ```rust + /// let mut x = "Hello".to_owned(); + /// x.push_str(", World"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub STRING_ADD, + restriction, + "using `x + ..` where x is a `String` instead of `push_str()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for the `as_bytes` method called on string literals + /// that contain only ASCII characters. + /// + /// ### Why is this bad? + /// Byte string literals (e.g., `b"foo"`) can be used + /// instead. They are shorter but less discoverable than `as_bytes()`. + /// + /// ### Known problems + /// `"str".as_bytes()` and the suggested replacement of `b"str"` are not + /// equivalent because they have different types. The former is `&[u8]` + /// while the latter is `&[u8; 3]`. That means in general they will have a + /// different set of methods and different trait implementations. + /// + /// ```compile_fail + /// fn f(v: Vec<u8>) {} + /// + /// f("...".as_bytes().to_owned()); // works + /// f(b"...".to_owned()); // does not work, because arg is [u8; 3] not Vec<u8> + /// + /// fn g(r: impl std::io::Read) {} + /// + /// g("...".as_bytes()); // works + /// g(b"..."); // does not work + /// ``` + /// + /// The actual equivalent of `"str".as_bytes()` with the same type is not + /// `b"str"` but `&b"str"[..]`, which is a great deal of punctuation and not + /// more readable than a function call. + /// + /// ### Example + /// ```rust + /// let bstr = "a byte string".as_bytes(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let bstr = b"a byte string"; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub STRING_LIT_AS_BYTES, + nursery, + "calling `as_bytes` on a string literal instead of using a byte string literal" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for slice operations on strings + /// + /// ### Why is this bad? + /// UTF-8 characters span multiple bytes, and it is easy to inadvertently confuse character + /// counts and string indices. This may lead to panics, and should warrant some test cases + /// containing wide UTF-8 characters. This lint is most useful in code that should avoid + /// panics at all costs. + /// + /// ### Known problems + /// Probably lots of false positives. If an index comes from a known valid position (e.g. + /// obtained via `char_indices` over the same string), it is totally OK. + /// + /// # Example + /// ```rust,should_panic + /// &"Ölkanne"[1..]; + /// ``` + #[clippy::version = "1.58.0"] + pub STRING_SLICE, + restriction, + "slicing a string" +} + +declare_lint_pass!(StringAdd => [STRING_ADD, STRING_ADD_ASSIGN, STRING_SLICE]); + +impl<'tcx> LateLintPass<'tcx> for StringAdd { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if in_external_macro(cx.sess(), e.span) { + return; + } + match e.kind { + ExprKind::Binary( + Spanned { + node: BinOpKind::Add, .. + }, + left, + _, + ) => { + if is_string(cx, left) { + if !is_lint_allowed(cx, STRING_ADD_ASSIGN, e.hir_id) { + let parent = get_parent_expr(cx, e); + if let Some(p) = parent { + if let ExprKind::Assign(target, _, _) = p.kind { + // avoid duplicate matches + if SpanlessEq::new(cx).eq_expr(target, left) { + return; + } + } + } + } + span_lint( + cx, + STRING_ADD, + e.span, + "you added something to a string. Consider using `String::push_str()` instead", + ); + } + }, + ExprKind::Assign(target, src, _) => { + if is_string(cx, target) && is_add(cx, src, target) { + span_lint( + cx, + STRING_ADD_ASSIGN, + e.span, + "you assigned the result of adding something to this string. Consider using \ + `String::push_str()` instead", + ); + } + }, + ExprKind::Index(target, _idx) => { + let e_ty = cx.typeck_results().expr_ty(target).peel_refs(); + if matches!(e_ty.kind(), ty::Str) || is_type_diagnostic_item(cx, e_ty, sym::String) { + span_lint( + cx, + STRING_SLICE, + e.span, + "indexing into a string may panic if the index is within a UTF-8 character", + ); + } + }, + _ => {}, + } + } +} + +fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { + is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), sym::String) +} + +fn is_add(cx: &LateContext<'_>, src: &Expr<'_>, target: &Expr<'_>) -> bool { + match peel_blocks(src).kind { + ExprKind::Binary( + Spanned { + node: BinOpKind::Add, .. + }, + left, + _, + ) => SpanlessEq::new(cx).eq_expr(target, left), + _ => false, + } +} + +declare_clippy_lint! { + /// ### What it does + /// Check if the string is transformed to byte array and casted back to string. + /// + /// ### Why is this bad? + /// It's unnecessary, the string can be used directly. + /// + /// ### Example + /// ```rust + /// std::str::from_utf8(&"Hello World!".as_bytes()[6..11]).unwrap(); + /// ``` + /// + /// Use instead: + /// ```rust + /// &"Hello World!"[6..11]; + /// ``` + #[clippy::version = "1.50.0"] + pub STRING_FROM_UTF8_AS_BYTES, + complexity, + "casting string slices to byte slices and back" +} + +// Max length a b"foo" string can take +const MAX_LENGTH_BYTE_STRING_LIT: usize = 32; + +declare_lint_pass!(StringLitAsBytes => [STRING_LIT_AS_BYTES, STRING_FROM_UTF8_AS_BYTES]); + +impl<'tcx> LateLintPass<'tcx> for StringLitAsBytes { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + use rustc_ast::LitKind; + + if_chain! { + // Find std::str::converts::from_utf8 + if let Some(args) = match_function_call(cx, e, &paths::STR_FROM_UTF8); + + // Find string::as_bytes + if let ExprKind::AddrOf(BorrowKind::Ref, _, args) = args[0].kind; + if let ExprKind::Index(left, right) = args.kind; + let (method_names, expressions, _) = method_calls(left, 1); + if method_names.len() == 1; + if expressions.len() == 1; + if expressions[0].len() == 1; + if method_names[0] == sym!(as_bytes); + + // Check for slicer + if let ExprKind::Struct(QPath::LangItem(LangItem::Range, ..), _, _) = right.kind; + + then { + let mut applicability = Applicability::MachineApplicable; + let string_expression = &expressions[0][0]; + + let snippet_app = snippet_with_applicability( + cx, + string_expression.span, "..", + &mut applicability, + ); + + span_lint_and_sugg( + cx, + STRING_FROM_UTF8_AS_BYTES, + e.span, + "calling a slice of `as_bytes()` with `from_utf8` should be not necessary", + "try", + format!("Some(&{}[{}])", snippet_app, snippet(cx, right.span, "..")), + applicability + ) + } + } + + if_chain! { + if let ExprKind::MethodCall(path, args, _) = &e.kind; + if path.ident.name == sym!(as_bytes); + if let ExprKind::Lit(lit) = &args[0].kind; + if let LitKind::Str(lit_content, _) = &lit.node; + then { + let callsite = snippet(cx, args[0].span.source_callsite(), r#""foo""#); + let mut applicability = Applicability::MachineApplicable; + if callsite.starts_with("include_str!") { + span_lint_and_sugg( + cx, + STRING_LIT_AS_BYTES, + e.span, + "calling `as_bytes()` on `include_str!(..)`", + "consider using `include_bytes!(..)` instead", + snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability).replacen( + "include_str", + "include_bytes", + 1, + ), + applicability, + ); + } else if lit_content.as_str().is_ascii() + && lit_content.as_str().len() <= MAX_LENGTH_BYTE_STRING_LIT + && !args[0].span.from_expansion() + { + span_lint_and_sugg( + cx, + STRING_LIT_AS_BYTES, + e.span, + "calling `as_bytes()` on a string literal", + "consider using a byte string literal instead", + format!( + "b{}", + snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability) + ), + applicability, + ); + } + } + } + + if_chain! { + if let ExprKind::MethodCall(path, [recv], _) = &e.kind; + if path.ident.name == sym!(into_bytes); + if let ExprKind::MethodCall(path, [recv], _) = &recv.kind; + if matches!(path.ident.name.as_str(), "to_owned" | "to_string"); + if let ExprKind::Lit(lit) = &recv.kind; + if let LitKind::Str(lit_content, _) = &lit.node; + + if lit_content.as_str().is_ascii(); + if lit_content.as_str().len() <= MAX_LENGTH_BYTE_STRING_LIT; + if !recv.span.from_expansion(); + then { + let mut applicability = Applicability::MachineApplicable; + + span_lint_and_sugg( + cx, + STRING_LIT_AS_BYTES, + e.span, + "calling `into_bytes()` on a string literal", + "consider using a byte string literal instead", + format!( + "b{}.to_vec()", + snippet_with_applicability(cx, recv.span, r#""..""#, &mut applicability) + ), + applicability, + ); + } + } + } +} + +declare_clippy_lint! { + /// ### What it does + /// This lint checks for `.to_string()` method calls on values of type `&str`. + /// + /// ### Why is this bad? + /// The `to_string` method is also used on other types to convert them to a string. + /// When called on a `&str` it turns the `&str` into the owned variant `String`, which can be better + /// expressed with `.to_owned()`. + /// + /// ### Example + /// ```rust + /// // example code where clippy issues a warning + /// let _ = "str".to_string(); + /// ``` + /// Use instead: + /// ```rust + /// // example code which does not raise clippy warning + /// let _ = "str".to_owned(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub STR_TO_STRING, + restriction, + "using `to_string()` on a `&str`, which should be `to_owned()`" +} + +declare_lint_pass!(StrToString => [STR_TO_STRING]); + +impl<'tcx> LateLintPass<'tcx> for StrToString { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::MethodCall(path, [self_arg, ..], _) = &expr.kind; + if path.ident.name == sym::to_string; + let ty = cx.typeck_results().expr_ty(self_arg); + if let ty::Ref(_, ty, ..) = ty.kind(); + if *ty.kind() == ty::Str; + then { + span_lint_and_help( + cx, + STR_TO_STRING, + expr.span, + "`to_string()` called on a `&str`", + None, + "consider using `.to_owned()`", + ); + } + } + } +} + +declare_clippy_lint! { + /// ### What it does + /// This lint checks for `.to_string()` method calls on values of type `String`. + /// + /// ### Why is this bad? + /// The `to_string` method is also used on other types to convert them to a string. + /// When called on a `String` it only clones the `String`, which can be better expressed with `.clone()`. + /// + /// ### Example + /// ```rust + /// // example code where clippy issues a warning + /// let msg = String::from("Hello World"); + /// let _ = msg.to_string(); + /// ``` + /// Use instead: + /// ```rust + /// // example code which does not raise clippy warning + /// let msg = String::from("Hello World"); + /// let _ = msg.clone(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub STRING_TO_STRING, + restriction, + "using `to_string()` on a `String`, which should be `clone()`" +} + +declare_lint_pass!(StringToString => [STRING_TO_STRING]); + +impl<'tcx> LateLintPass<'tcx> for StringToString { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) { + if_chain! { + if let ExprKind::MethodCall(path, [self_arg, ..], _) = &expr.kind; + if path.ident.name == sym::to_string; + let ty = cx.typeck_results().expr_ty(self_arg); + if is_type_diagnostic_item(cx, ty, sym::String); + then { + span_lint_and_help( + cx, + STRING_TO_STRING, + expr.span, + "`to_string()` called on a `String`", + None, + "consider using `.clone()`", + ); + } + } + } +} + +declare_clippy_lint! { + /// ### What it does + /// Warns about calling `str::trim` (or variants) before `str::split_whitespace`. + /// + /// ### Why is this bad? + /// `split_whitespace` already ignores leading and trailing whitespace. + /// + /// ### Example + /// ```rust + /// " A B C ".trim().split_whitespace(); + /// ``` + /// Use instead: + /// ```rust + /// " A B C ".split_whitespace(); + /// ``` + #[clippy::version = "1.62.0"] + pub TRIM_SPLIT_WHITESPACE, + style, + "using `str::trim()` or alike before `str::split_whitespace`" +} +declare_lint_pass!(TrimSplitWhitespace => [TRIM_SPLIT_WHITESPACE]); + +impl<'tcx> LateLintPass<'tcx> for TrimSplitWhitespace { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) { + let tyckres = cx.typeck_results(); + if_chain! { + if let ExprKind::MethodCall(path, [split_recv], split_ws_span) = expr.kind; + if path.ident.name == sym!(split_whitespace); + if let Some(split_ws_def_id) = tyckres.type_dependent_def_id(expr.hir_id); + if cx.tcx.is_diagnostic_item(sym::str_split_whitespace, split_ws_def_id); + if let ExprKind::MethodCall(path, [_trim_recv], trim_span) = split_recv.kind; + if let trim_fn_name @ ("trim" | "trim_start" | "trim_end") = path.ident.name.as_str(); + if let Some(trim_def_id) = tyckres.type_dependent_def_id(split_recv.hir_id); + if is_one_of_trim_diagnostic_items(cx, trim_def_id); + then { + span_lint_and_sugg( + cx, + TRIM_SPLIT_WHITESPACE, + trim_span.with_hi(split_ws_span.lo()), + &format!("found call to `str::{}` before `str::split_whitespace`", trim_fn_name), + &format!("remove `{}()`", trim_fn_name), + String::new(), + Applicability::MachineApplicable, + ); + } + } + } +} + +fn is_one_of_trim_diagnostic_items(cx: &LateContext<'_>, trim_def_id: DefId) -> bool { + cx.tcx.is_diagnostic_item(sym::str_trim, trim_def_id) + || cx.tcx.is_diagnostic_item(sym::str_trim_start, trim_def_id) + || cx.tcx.is_diagnostic_item(sym::str_trim_end, trim_def_id) +} diff --git a/src/tools/clippy/clippy_lints/src/strlen_on_c_strings.rs b/src/tools/clippy/clippy_lints/src/strlen_on_c_strings.rs new file mode 100644 index 000000000..7bc9cf742 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/strlen_on_c_strings.rs @@ -0,0 +1,88 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_context; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::visitors::is_expr_unsafe; +use clippy_utils::{get_parent_node, match_libc_symbol}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Block, BlockCheckMode, Expr, ExprKind, Node, UnsafeSource}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `libc::strlen` on a `CString` or `CStr` value, + /// and suggest calling `as_bytes().len()` or `to_bytes().len()` respectively instead. + /// + /// ### Why is this bad? + /// This avoids calling an unsafe `libc` function. + /// Currently, it also avoids calculating the length. + /// + /// ### Example + /// ```rust, ignore + /// use std::ffi::CString; + /// let cstring = CString::new("foo").expect("CString::new failed"); + /// let len = unsafe { libc::strlen(cstring.as_ptr()) }; + /// ``` + /// Use instead: + /// ```rust, no_run + /// use std::ffi::CString; + /// let cstring = CString::new("foo").expect("CString::new failed"); + /// let len = cstring.as_bytes().len(); + /// ``` + #[clippy::version = "1.55.0"] + pub STRLEN_ON_C_STRINGS, + complexity, + "using `libc::strlen` on a `CString` or `CStr` value, while `as_bytes().len()` or `to_bytes().len()` respectively can be used instead" +} + +declare_lint_pass!(StrlenOnCStrings => [STRLEN_ON_C_STRINGS]); + +impl<'tcx> LateLintPass<'tcx> for StrlenOnCStrings { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + if !expr.span.from_expansion(); + if let ExprKind::Call(func, [recv]) = expr.kind; + if let ExprKind::Path(path) = &func.kind; + if let Some(did) = cx.qpath_res(path, func.hir_id).opt_def_id(); + if match_libc_symbol(cx, did, "strlen"); + if let ExprKind::MethodCall(path, [self_arg], _) = recv.kind; + if !recv.span.from_expansion(); + if path.ident.name == sym::as_ptr; + then { + let ctxt = expr.span.ctxt(); + let span = match get_parent_node(cx.tcx, expr.hir_id) { + Some(Node::Block(&Block { + rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided), span, .. + })) + if span.ctxt() == ctxt && !is_expr_unsafe(cx, self_arg) => { + span + } + _ => expr.span, + }; + + let ty = cx.typeck_results().expr_ty(self_arg).peel_refs(); + let mut app = Applicability::MachineApplicable; + let val_name = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0; + let method_name = if is_type_diagnostic_item(cx, ty, sym::cstring_type) { + "as_bytes" + } else if is_type_diagnostic_item(cx, ty, sym::CStr) { + "to_bytes" + } else { + return; + }; + + span_lint_and_sugg( + cx, + STRLEN_ON_C_STRINGS, + span, + "using `libc::strlen` on a `CString` or `CStr` value", + "try this", + format!("{}.{}().len()", val_name, method_name), + app, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/suspicious_operation_groupings.rs b/src/tools/clippy/clippy_lints/src/suspicious_operation_groupings.rs new file mode 100644 index 000000000..fe8859905 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/suspicious_operation_groupings.rs @@ -0,0 +1,693 @@ +use clippy_utils::ast_utils::{eq_id, is_useless_with_eq_exprs, IdentIter}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use core::ops::{Add, AddAssign}; +use if_chain::if_chain; +use rustc_ast::ast::{BinOpKind, Expr, ExprKind, StmtKind}; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; +use rustc_span::symbol::Ident; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for unlikely usages of binary operators that are almost + /// certainly typos and/or copy/paste errors, given the other usages + /// of binary operators nearby. + /// + /// ### Why is this bad? + /// They are probably bugs and if they aren't then they look like bugs + /// and you should add a comment explaining why you are doing such an + /// odd set of operations. + /// + /// ### Known problems + /// There may be some false positives if you are trying to do something + /// unusual that happens to look like a typo. + /// + /// ### Example + /// ```rust + /// struct Vec3 { + /// x: f64, + /// y: f64, + /// z: f64, + /// } + /// + /// impl Eq for Vec3 {} + /// + /// impl PartialEq for Vec3 { + /// fn eq(&self, other: &Self) -> bool { + /// // This should trigger the lint because `self.x` is compared to `other.y` + /// self.x == other.y && self.y == other.y && self.z == other.z + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// # struct Vec3 { + /// # x: f64, + /// # y: f64, + /// # z: f64, + /// # } + /// // same as above except: + /// impl PartialEq for Vec3 { + /// fn eq(&self, other: &Self) -> bool { + /// // Note we now compare other.x to self.x + /// self.x == other.x && self.y == other.y && self.z == other.z + /// } + /// } + /// ``` + #[clippy::version = "1.50.0"] + pub SUSPICIOUS_OPERATION_GROUPINGS, + nursery, + "groupings of binary operations that look suspiciously like typos" +} + +declare_lint_pass!(SuspiciousOperationGroupings => [SUSPICIOUS_OPERATION_GROUPINGS]); + +impl EarlyLintPass for SuspiciousOperationGroupings { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if expr.span.from_expansion() { + return; + } + + if let Some(binops) = extract_related_binops(&expr.kind) { + check_binops(cx, &binops.iter().collect::<Vec<_>>()); + + let mut op_types = Vec::with_capacity(binops.len()); + // We could use a hashmap, etc. to avoid being O(n*m) here, but + // we want the lints to be emitted in a consistent order. Besides, + // m, (the number of distinct `BinOpKind`s in `binops`) + // will often be small, and does have an upper limit. + binops.iter().map(|b| b.op).for_each(|op| { + if !op_types.contains(&op) { + op_types.push(op); + } + }); + + for op_type in op_types { + let ops: Vec<_> = binops.iter().filter(|b| b.op == op_type).collect(); + + check_binops(cx, &ops); + } + } + } +} + +fn check_binops(cx: &EarlyContext<'_>, binops: &[&BinaryOp<'_>]) { + let binop_count = binops.len(); + if binop_count < 2 { + // Single binary operation expressions would likely be false + // positives. + return; + } + + let mut one_ident_difference_count = 0; + let mut no_difference_info = None; + let mut double_difference_info = None; + let mut expected_ident_loc = None; + + let mut paired_identifiers = FxHashSet::default(); + + for (i, BinaryOp { left, right, op, .. }) in binops.iter().enumerate() { + match ident_difference_expr(left, right) { + IdentDifference::NoDifference => { + if is_useless_with_eq_exprs(*op) { + // The `eq_op` lint should catch this in this case. + return; + } + + no_difference_info = Some(i); + }, + IdentDifference::Single(ident_loc) => { + one_ident_difference_count += 1; + if let Some(previous_expected) = expected_ident_loc { + if previous_expected != ident_loc { + // This expression doesn't match the form we're + // looking for. + return; + } + } else { + expected_ident_loc = Some(ident_loc); + } + + // If there was only a single difference, all other idents + // must have been the same, and thus were paired. + for id in skip_index(IdentIter::from(*left), ident_loc.index) { + paired_identifiers.insert(id); + } + }, + IdentDifference::Double(ident_loc1, ident_loc2) => { + double_difference_info = Some((i, ident_loc1, ident_loc2)); + }, + IdentDifference::Multiple | IdentDifference::NonIdent => { + // It's too hard to know whether this is a bug or not. + return; + }, + } + } + + let mut applicability = Applicability::MachineApplicable; + + if let Some(expected_loc) = expected_ident_loc { + match (no_difference_info, double_difference_info) { + (Some(i), None) => attempt_to_emit_no_difference_lint(cx, binops, i, expected_loc), + (None, Some((double_difference_index, ident_loc1, ident_loc2))) => { + if_chain! { + if one_ident_difference_count == binop_count - 1; + if let Some(binop) = binops.get(double_difference_index); + then { + let changed_loc = if ident_loc1 == expected_loc { + ident_loc2 + } else if ident_loc2 == expected_loc { + ident_loc1 + } else { + // This expression doesn't match the form we're + // looking for. + return; + }; + + if let Some(sugg) = ident_swap_sugg( + cx, + &paired_identifiers, + binop, + changed_loc, + &mut applicability, + ) { + emit_suggestion( + cx, + binop.span, + sugg, + applicability, + ); + } + } + } + }, + _ => {}, + } + } +} + +fn attempt_to_emit_no_difference_lint( + cx: &EarlyContext<'_>, + binops: &[&BinaryOp<'_>], + i: usize, + expected_loc: IdentLocation, +) { + if let Some(binop) = binops.get(i).copied() { + // We need to try and figure out which identifier we should + // suggest using instead. Since there could be multiple + // replacement candidates in a given expression, and we're + // just taking the first one, we may get some bad lint + // messages. + let mut applicability = Applicability::MaybeIncorrect; + + // We assume that the correct ident is one used elsewhere in + // the other binops, in a place that there was a single + // difference between idents before. + let old_left_ident = get_ident(binop.left, expected_loc); + let old_right_ident = get_ident(binop.right, expected_loc); + + for b in skip_index(binops.iter(), i) { + if_chain! { + if let (Some(old_ident), Some(new_ident)) = + (old_left_ident, get_ident(b.left, expected_loc)); + if old_ident != new_ident; + if let Some(sugg) = suggestion_with_swapped_ident( + cx, + binop.left, + expected_loc, + new_ident, + &mut applicability, + ); + then { + emit_suggestion( + cx, + binop.span, + replace_left_sugg(cx, binop, &sugg, &mut applicability), + applicability, + ); + return; + } + } + + if_chain! { + if let (Some(old_ident), Some(new_ident)) = + (old_right_ident, get_ident(b.right, expected_loc)); + if old_ident != new_ident; + if let Some(sugg) = suggestion_with_swapped_ident( + cx, + binop.right, + expected_loc, + new_ident, + &mut applicability, + ); + then { + emit_suggestion( + cx, + binop.span, + replace_right_sugg(cx, binop, &sugg, &mut applicability), + applicability, + ); + return; + } + } + } + } +} + +fn emit_suggestion(cx: &EarlyContext<'_>, span: Span, sugg: String, applicability: Applicability) { + span_lint_and_sugg( + cx, + SUSPICIOUS_OPERATION_GROUPINGS, + span, + "this sequence of operators looks suspiciously like a bug", + "did you mean", + sugg, + applicability, + ); +} + +fn ident_swap_sugg( + cx: &EarlyContext<'_>, + paired_identifiers: &FxHashSet<Ident>, + binop: &BinaryOp<'_>, + location: IdentLocation, + applicability: &mut Applicability, +) -> Option<String> { + let left_ident = get_ident(binop.left, location)?; + let right_ident = get_ident(binop.right, location)?; + + let sugg = match ( + paired_identifiers.contains(&left_ident), + paired_identifiers.contains(&right_ident), + ) { + (true, true) | (false, false) => { + // We don't have a good guess of what ident should be + // used instead, in these cases. + *applicability = Applicability::MaybeIncorrect; + + // We arbitrarily choose one side to suggest changing, + // since we don't have a better guess. If the user + // ends up duplicating a clause, the `logic_bug` lint + // should catch it. + + let right_suggestion = suggestion_with_swapped_ident(cx, binop.right, location, left_ident, applicability)?; + + replace_right_sugg(cx, binop, &right_suggestion, applicability) + }, + (false, true) => { + // We haven't seen a pair involving the left one, so + // it's probably what is wanted. + + let right_suggestion = suggestion_with_swapped_ident(cx, binop.right, location, left_ident, applicability)?; + + replace_right_sugg(cx, binop, &right_suggestion, applicability) + }, + (true, false) => { + // We haven't seen a pair involving the right one, so + // it's probably what is wanted. + let left_suggestion = suggestion_with_swapped_ident(cx, binop.left, location, right_ident, applicability)?; + + replace_left_sugg(cx, binop, &left_suggestion, applicability) + }, + }; + + Some(sugg) +} + +fn replace_left_sugg( + cx: &EarlyContext<'_>, + binop: &BinaryOp<'_>, + left_suggestion: &str, + applicability: &mut Applicability, +) -> String { + format!( + "{} {} {}", + left_suggestion, + binop.op.to_string(), + snippet_with_applicability(cx, binop.right.span, "..", applicability), + ) +} + +fn replace_right_sugg( + cx: &EarlyContext<'_>, + binop: &BinaryOp<'_>, + right_suggestion: &str, + applicability: &mut Applicability, +) -> String { + format!( + "{} {} {}", + snippet_with_applicability(cx, binop.left.span, "..", applicability), + binop.op.to_string(), + right_suggestion, + ) +} + +#[derive(Clone, Debug)] +struct BinaryOp<'exprs> { + op: BinOpKind, + span: Span, + left: &'exprs Expr, + right: &'exprs Expr, +} + +impl<'exprs> BinaryOp<'exprs> { + fn new(op: BinOpKind, span: Span, (left, right): (&'exprs Expr, &'exprs Expr)) -> Self { + Self { op, span, left, right } + } +} + +fn strip_non_ident_wrappers(expr: &Expr) -> &Expr { + let mut output = expr; + loop { + output = match &output.kind { + ExprKind::Paren(ref inner) | ExprKind::Unary(_, ref inner) => inner, + _ => { + return output; + }, + }; + } +} + +fn extract_related_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> { + append_opt_vecs(chained_binops(kind), if_statement_binops(kind)) +} + +fn if_statement_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> { + match kind { + ExprKind::If(ref condition, _, _) => chained_binops(&condition.kind), + ExprKind::Paren(ref e) => if_statement_binops(&e.kind), + ExprKind::Block(ref block, _) => { + let mut output = None; + for stmt in &block.stmts { + match stmt.kind { + StmtKind::Expr(ref e) | StmtKind::Semi(ref e) => { + output = append_opt_vecs(output, if_statement_binops(&e.kind)); + }, + _ => {}, + } + } + output + }, + _ => None, + } +} + +fn append_opt_vecs<A>(target_opt: Option<Vec<A>>, source_opt: Option<Vec<A>>) -> Option<Vec<A>> { + match (target_opt, source_opt) { + (Some(mut target), Some(source)) => { + target.reserve(source.len()); + for op in source { + target.push(op); + } + Some(target) + }, + (Some(v), None) | (None, Some(v)) => Some(v), + (None, None) => None, + } +} + +fn chained_binops(kind: &ExprKind) -> Option<Vec<BinaryOp<'_>>> { + match kind { + ExprKind::Binary(_, left_outer, right_outer) => chained_binops_helper(left_outer, right_outer), + ExprKind::Paren(ref e) | ExprKind::Unary(_, ref e) => chained_binops(&e.kind), + _ => None, + } +} + +fn chained_binops_helper<'expr>(left_outer: &'expr Expr, right_outer: &'expr Expr) -> Option<Vec<BinaryOp<'expr>>> { + match (&left_outer.kind, &right_outer.kind) { + ( + ExprKind::Paren(ref left_e) | ExprKind::Unary(_, ref left_e), + ExprKind::Paren(ref right_e) | ExprKind::Unary(_, ref right_e), + ) => chained_binops_helper(left_e, right_e), + (ExprKind::Paren(ref left_e) | ExprKind::Unary(_, ref left_e), _) => chained_binops_helper(left_e, right_outer), + (_, ExprKind::Paren(ref right_e) | ExprKind::Unary(_, ref right_e)) => { + chained_binops_helper(left_outer, right_e) + }, + ( + ExprKind::Binary(Spanned { node: left_op, .. }, ref left_left, ref left_right), + ExprKind::Binary(Spanned { node: right_op, .. }, ref right_left, ref right_right), + ) => match ( + chained_binops_helper(left_left, left_right), + chained_binops_helper(right_left, right_right), + ) { + (Some(mut left_ops), Some(right_ops)) => { + left_ops.reserve(right_ops.len()); + for op in right_ops { + left_ops.push(op); + } + Some(left_ops) + }, + (Some(mut left_ops), _) => { + left_ops.push(BinaryOp::new(*right_op, right_outer.span, (right_left, right_right))); + Some(left_ops) + }, + (_, Some(mut right_ops)) => { + right_ops.insert(0, BinaryOp::new(*left_op, left_outer.span, (left_left, left_right))); + Some(right_ops) + }, + (None, None) => Some(vec![ + BinaryOp::new(*left_op, left_outer.span, (left_left, left_right)), + BinaryOp::new(*right_op, right_outer.span, (right_left, right_right)), + ]), + }, + _ => None, + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +struct IdentLocation { + index: usize, +} + +impl Add for IdentLocation { + type Output = IdentLocation; + + fn add(self, other: Self) -> Self::Output { + Self { + index: self.index + other.index, + } + } +} + +impl AddAssign for IdentLocation { + fn add_assign(&mut self, other: Self) { + *self = *self + other; + } +} + +#[derive(Clone, Copy, Debug)] +enum IdentDifference { + NoDifference, + Single(IdentLocation), + Double(IdentLocation, IdentLocation), + Multiple, + NonIdent, +} + +impl Add for IdentDifference { + type Output = IdentDifference; + + fn add(self, other: Self) -> Self::Output { + match (self, other) { + (Self::NoDifference, output) | (output, Self::NoDifference) => output, + (Self::Multiple, _) + | (_, Self::Multiple) + | (Self::Double(_, _), Self::Single(_)) + | (Self::Single(_) | Self::Double(_, _), Self::Double(_, _)) => Self::Multiple, + (Self::NonIdent, _) | (_, Self::NonIdent) => Self::NonIdent, + (Self::Single(il1), Self::Single(il2)) => Self::Double(il1, il2), + } + } +} + +impl AddAssign for IdentDifference { + fn add_assign(&mut self, other: Self) { + *self = *self + other; + } +} + +impl IdentDifference { + /// Returns true if learning about more differences will not change the value + /// of this `IdentDifference`, and false otherwise. + fn is_complete(&self) -> bool { + match self { + Self::NoDifference | Self::Single(_) | Self::Double(_, _) => false, + Self::Multiple | Self::NonIdent => true, + } + } +} + +fn ident_difference_expr(left: &Expr, right: &Expr) -> IdentDifference { + ident_difference_expr_with_base_location(left, right, IdentLocation::default()).0 +} + +fn ident_difference_expr_with_base_location( + left: &Expr, + right: &Expr, + mut base: IdentLocation, +) -> (IdentDifference, IdentLocation) { + // Ideally, this function should not use IdentIter because it should return + // early if the expressions have any non-ident differences. We want that early + // return because if without that restriction the lint would lead to false + // positives. + // + // But, we cannot (easily?) use a `rustc_ast::visit::Visitor`, since we need + // the two expressions to be walked in lockstep. And without a `Visitor`, we'd + // have to do all the AST traversal ourselves, which is a lot of work, since to + // do it properly we'd need to be able to handle more or less every possible + // AST node since `Item`s can be written inside `Expr`s. + // + // In practice, it seems likely that expressions, above a certain size, that + // happen to use the exact same idents in the exact same order, and which are + // not structured the same, would be rare. Therefore it seems likely that if + // we do only the first layer of matching ourselves and eventually fallback on + // IdentIter, then the output of this function will be almost always be correct + // in practice. + // + // If it turns out that problematic cases are more prevalent than we assume, + // then we should be able to change this function to do the correct traversal, + // without needing to change the rest of the code. + + #![allow(clippy::enum_glob_use)] + use ExprKind::*; + + match ( + &strip_non_ident_wrappers(left).kind, + &strip_non_ident_wrappers(right).kind, + ) { + (Yield(_), Yield(_)) + | (Try(_), Try(_)) + | (Paren(_), Paren(_)) + | (Repeat(_, _), Repeat(_, _)) + | (Struct(_), Struct(_)) + | (MacCall(_), MacCall(_)) + | (InlineAsm(_), InlineAsm(_)) + | (Ret(_), Ret(_)) + | (Continue(_), Continue(_)) + | (Break(_, _), Break(_, _)) + | (AddrOf(_, _, _), AddrOf(_, _, _)) + | (Path(_, _), Path(_, _)) + | (Range(_, _, _), Range(_, _, _)) + | (Index(_, _), Index(_, _)) + | (Field(_, _), Field(_, _)) + | (AssignOp(_, _, _), AssignOp(_, _, _)) + | (Assign(_, _, _), Assign(_, _, _)) + | (TryBlock(_), TryBlock(_)) + | (Await(_), Await(_)) + | (Async(_, _, _), Async(_, _, _)) + | (Block(_, _), Block(_, _)) + | (Closure(_, _, _, _, _, _, _), Closure(_, _, _, _, _, _, _)) + | (Match(_, _), Match(_, _)) + | (Loop(_, _), Loop(_, _)) + | (ForLoop(_, _, _, _), ForLoop(_, _, _, _)) + | (While(_, _, _), While(_, _, _)) + | (If(_, _, _), If(_, _, _)) + | (Let(_, _, _), Let(_, _, _)) + | (Type(_, _), Type(_, _)) + | (Cast(_, _), Cast(_, _)) + | (Lit(_), Lit(_)) + | (Unary(_, _), Unary(_, _)) + | (Binary(_, _, _), Binary(_, _, _)) + | (Tup(_), Tup(_)) + | (MethodCall(_, _, _), MethodCall(_, _, _)) + | (Call(_, _), Call(_, _)) + | (ConstBlock(_), ConstBlock(_)) + | (Array(_), Array(_)) + | (Box(_), Box(_)) => { + // keep going + }, + _ => { + return (IdentDifference::NonIdent, base); + }, + } + + let mut difference = IdentDifference::NoDifference; + + for (left_attr, right_attr) in left.attrs.iter().zip(right.attrs.iter()) { + let (new_difference, new_base) = + ident_difference_via_ident_iter_with_base_location(left_attr, right_attr, base); + base = new_base; + difference += new_difference; + if difference.is_complete() { + return (difference, base); + } + } + + let (new_difference, new_base) = ident_difference_via_ident_iter_with_base_location(left, right, base); + base = new_base; + difference += new_difference; + + (difference, base) +} + +fn ident_difference_via_ident_iter_with_base_location<Iterable: Into<IdentIter>>( + left: Iterable, + right: Iterable, + mut base: IdentLocation, +) -> (IdentDifference, IdentLocation) { + // See the note in `ident_difference_expr_with_base_location` about `IdentIter` + let mut difference = IdentDifference::NoDifference; + + let mut left_iterator = left.into(); + let mut right_iterator = right.into(); + + loop { + match (left_iterator.next(), right_iterator.next()) { + (Some(left_ident), Some(right_ident)) => { + if !eq_id(left_ident, right_ident) { + difference += IdentDifference::Single(base); + if difference.is_complete() { + return (difference, base); + } + } + }, + (Some(_), None) | (None, Some(_)) => { + return (IdentDifference::NonIdent, base); + }, + (None, None) => { + return (difference, base); + }, + } + base += IdentLocation { index: 1 }; + } +} + +fn get_ident(expr: &Expr, location: IdentLocation) -> Option<Ident> { + IdentIter::from(expr).nth(location.index) +} + +fn suggestion_with_swapped_ident( + cx: &EarlyContext<'_>, + expr: &Expr, + location: IdentLocation, + new_ident: Ident, + applicability: &mut Applicability, +) -> Option<String> { + get_ident(expr, location).and_then(|current_ident| { + if eq_id(current_ident, new_ident) { + // We never want to suggest a non-change + return None; + } + + Some(format!( + "{}{}{}", + snippet_with_applicability(cx, expr.span.with_hi(current_ident.span.lo()), "..", applicability), + new_ident, + snippet_with_applicability(cx, expr.span.with_lo(current_ident.span.hi()), "..", applicability), + )) + }) +} + +fn skip_index<A, Iter>(iter: Iter, index: usize) -> impl Iterator<Item = A> +where + Iter: Iterator<Item = A>, +{ + iter.enumerate() + .filter_map(move |(i, a)| if i == index { None } else { Some(a) }) +} diff --git a/src/tools/clippy/clippy_lints/src/suspicious_trait_impl.rs b/src/tools/clippy/clippy_lints/src/suspicious_trait_impl.rs new file mode 100644 index 000000000..4294464db --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/suspicious_trait_impl.rs @@ -0,0 +1,116 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{binop_traits, trait_ref_of_method, BINOP_TRAITS, OP_ASSIGN_TRAITS}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_hir::intravisit::{walk_expr, Visitor}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Lints for suspicious operations in impls of arithmetic operators, e.g. + /// subtracting elements in an Add impl. + /// + /// ### Why is this bad? + /// This is probably a typo or copy-and-paste error and not intended. + /// + /// ### Example + /// ```ignore + /// impl Add for Foo { + /// type Output = Foo; + /// + /// fn add(self, other: Foo) -> Foo { + /// Foo(self.0 - other.0) + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SUSPICIOUS_ARITHMETIC_IMPL, + suspicious, + "suspicious use of operators in impl of arithmetic trait" +} + +declare_clippy_lint! { + /// ### What it does + /// Lints for suspicious operations in impls of OpAssign, e.g. + /// subtracting elements in an AddAssign impl. + /// + /// ### Why is this bad? + /// This is probably a typo or copy-and-paste error and not intended. + /// + /// ### Example + /// ```ignore + /// impl AddAssign for Foo { + /// fn add_assign(&mut self, other: Foo) { + /// *self = *self - other; + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SUSPICIOUS_OP_ASSIGN_IMPL, + suspicious, + "suspicious use of operators in impl of OpAssign trait" +} + +declare_lint_pass!(SuspiciousImpl => [SUSPICIOUS_ARITHMETIC_IMPL, SUSPICIOUS_OP_ASSIGN_IMPL]); + +impl<'tcx> LateLintPass<'tcx> for SuspiciousImpl { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if_chain! { + if let hir::ExprKind::Binary(binop, _, _) | hir::ExprKind::AssignOp(binop, ..) = expr.kind; + if let Some((binop_trait_lang, op_assign_trait_lang)) = binop_traits(binop.node); + if let Ok(binop_trait_id) = cx.tcx.lang_items().require(binop_trait_lang); + if let Ok(op_assign_trait_id) = cx.tcx.lang_items().require(op_assign_trait_lang); + + // Check for more than one binary operation in the implemented function + // Linting when multiple operations are involved can result in false positives + let parent_fn = cx.tcx.hir().get_parent_item(expr.hir_id); + if let hir::Node::ImplItem(impl_item) = cx.tcx.hir().get_by_def_id(parent_fn); + if let hir::ImplItemKind::Fn(_, body_id) = impl_item.kind; + let body = cx.tcx.hir().body(body_id); + let parent_fn = cx.tcx.hir().get_parent_item(expr.hir_id); + if let Some(trait_ref) = trait_ref_of_method(cx, parent_fn); + let trait_id = trait_ref.path.res.def_id(); + if ![binop_trait_id, op_assign_trait_id].contains(&trait_id); + if let Some(&(_, lint)) = [ + (&BINOP_TRAITS, SUSPICIOUS_ARITHMETIC_IMPL), + (&OP_ASSIGN_TRAITS, SUSPICIOUS_OP_ASSIGN_IMPL), + ] + .iter() + .find(|&(ts, _)| ts.iter().any(|&t| Ok(trait_id) == cx.tcx.lang_items().require(t))); + if count_binops(&body.value) == 1; + then { + span_lint( + cx, + lint, + binop.span, + &format!("suspicious use of `{}` in `{}` impl", binop.node.as_str(), cx.tcx.item_name(trait_id)), + ); + } + } + } +} + +fn count_binops(expr: &hir::Expr<'_>) -> u32 { + let mut visitor = BinaryExprVisitor::default(); + visitor.visit_expr(expr); + visitor.nb_binops +} + +#[derive(Default)] +struct BinaryExprVisitor { + nb_binops: u32, +} + +impl<'tcx> Visitor<'tcx> for BinaryExprVisitor { + fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) { + match expr.kind { + hir::ExprKind::Binary(..) + | hir::ExprKind::Unary(hir::UnOp::Not | hir::UnOp::Neg, _) + | hir::ExprKind::AssignOp(..) => self.nb_binops += 1, + _ => {}, + } + + walk_expr(self, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/swap.rs b/src/tools/clippy/clippy_lints/src/swap.rs new file mode 100644 index 000000000..1885f3ca4 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/swap.rs @@ -0,0 +1,258 @@ +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{can_mut_borrow_both, eq_expr_value, std_or_core}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Block, Expr, ExprKind, PatKind, QPath, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for manual swapping. + /// + /// ### Why is this bad? + /// The `std::mem::swap` function exposes the intent better + /// without deinitializing or copying either variable. + /// + /// ### Example + /// ```rust + /// let mut a = 42; + /// let mut b = 1337; + /// + /// let t = b; + /// b = a; + /// a = t; + /// ``` + /// Use std::mem::swap(): + /// ```rust + /// let mut a = 1; + /// let mut b = 2; + /// std::mem::swap(&mut a, &mut b); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MANUAL_SWAP, + complexity, + "manual swap of two variables" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `foo = bar; bar = foo` sequences. + /// + /// ### Why is this bad? + /// This looks like a failed attempt to swap. + /// + /// ### Example + /// ```rust + /// # let mut a = 1; + /// # let mut b = 2; + /// a = b; + /// b = a; + /// ``` + /// If swapping is intended, use `swap()` instead: + /// ```rust + /// # let mut a = 1; + /// # let mut b = 2; + /// std::mem::swap(&mut a, &mut b); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ALMOST_SWAPPED, + correctness, + "`foo = bar; bar = foo` sequence" +} + +declare_lint_pass!(Swap => [MANUAL_SWAP, ALMOST_SWAPPED]); + +impl<'tcx> LateLintPass<'tcx> for Swap { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'_>) { + check_manual_swap(cx, block); + check_suspicious_swap(cx, block); + check_xor_swap(cx, block); + } +} + +fn generate_swap_warning(cx: &LateContext<'_>, e1: &Expr<'_>, e2: &Expr<'_>, span: Span, is_xor_based: bool) { + let mut applicability = Applicability::MachineApplicable; + + if !can_mut_borrow_both(cx, e1, e2) { + if let ExprKind::Index(lhs1, idx1) = e1.kind { + if let ExprKind::Index(lhs2, idx2) = e2.kind { + if eq_expr_value(cx, lhs1, lhs2) { + let ty = cx.typeck_results().expr_ty(lhs1).peel_refs(); + + if matches!(ty.kind(), ty::Slice(_)) + || matches!(ty.kind(), ty::Array(_, _)) + || is_type_diagnostic_item(cx, ty, sym::Vec) + || is_type_diagnostic_item(cx, ty, sym::VecDeque) + { + let slice = Sugg::hir_with_applicability(cx, lhs1, "<slice>", &mut applicability); + span_lint_and_sugg( + cx, + MANUAL_SWAP, + span, + &format!("this looks like you are swapping elements of `{}` manually", slice), + "try", + format!( + "{}.swap({}, {})", + slice.maybe_par(), + snippet_with_applicability(cx, idx1.span, "..", &mut applicability), + snippet_with_applicability(cx, idx2.span, "..", &mut applicability), + ), + applicability, + ); + } + } + } + } + return; + } + + let first = Sugg::hir_with_applicability(cx, e1, "..", &mut applicability); + let second = Sugg::hir_with_applicability(cx, e2, "..", &mut applicability); + let Some(sugg) = std_or_core(cx) else { return }; + + span_lint_and_then( + cx, + MANUAL_SWAP, + span, + &format!("this looks like you are swapping `{}` and `{}` manually", first, second), + |diag| { + diag.span_suggestion( + span, + "try", + format!("{}::mem::swap({}, {})", sugg, first.mut_addr(), second.mut_addr()), + applicability, + ); + if !is_xor_based { + diag.note(&format!("or maybe you should use `{}::mem::replace`?", sugg)); + } + }, + ); +} + +/// Implementation of the `MANUAL_SWAP` lint. +fn check_manual_swap(cx: &LateContext<'_>, block: &Block<'_>) { + for w in block.stmts.windows(3) { + if_chain! { + // let t = foo(); + if let StmtKind::Local(tmp) = w[0].kind; + if let Some(tmp_init) = tmp.init; + if let PatKind::Binding(.., ident, None) = tmp.pat.kind; + + // foo() = bar(); + if let StmtKind::Semi(first) = w[1].kind; + if let ExprKind::Assign(lhs1, rhs1, _) = first.kind; + + // bar() = t; + if let StmtKind::Semi(second) = w[2].kind; + if let ExprKind::Assign(lhs2, rhs2, _) = second.kind; + if let ExprKind::Path(QPath::Resolved(None, rhs2)) = rhs2.kind; + if rhs2.segments.len() == 1; + + if ident.name == rhs2.segments[0].ident.name; + if eq_expr_value(cx, tmp_init, lhs1); + if eq_expr_value(cx, rhs1, lhs2); + then { + let span = w[0].span.to(second.span); + generate_swap_warning(cx, lhs1, lhs2, span, false); + } + } + } +} + +/// Implementation of the `ALMOST_SWAPPED` lint. +fn check_suspicious_swap(cx: &LateContext<'_>, block: &Block<'_>) { + for w in block.stmts.windows(2) { + if_chain! { + if let StmtKind::Semi(first) = w[0].kind; + if let StmtKind::Semi(second) = w[1].kind; + if first.span.ctxt() == second.span.ctxt(); + if let ExprKind::Assign(lhs0, rhs0, _) = first.kind; + if let ExprKind::Assign(lhs1, rhs1, _) = second.kind; + if eq_expr_value(cx, lhs0, rhs1); + if eq_expr_value(cx, lhs1, rhs0); + then { + let lhs0 = Sugg::hir_opt(cx, lhs0); + let rhs0 = Sugg::hir_opt(cx, rhs0); + let (what, lhs, rhs) = if let (Some(first), Some(second)) = (lhs0, rhs0) { + ( + format!(" `{}` and `{}`", first, second), + first.mut_addr().to_string(), + second.mut_addr().to_string(), + ) + } else { + (String::new(), String::new(), String::new()) + }; + + let span = first.span.to(second.span); + let Some(sugg) = std_or_core(cx) else { return }; + + span_lint_and_then(cx, + ALMOST_SWAPPED, + span, + &format!("this looks like you are trying to swap{}", what), + |diag| { + if !what.is_empty() { + diag.span_suggestion( + span, + "try", + format!( + "{}::mem::swap({}, {})", + sugg, + lhs, + rhs, + ), + Applicability::MaybeIncorrect, + ); + diag.note( + &format!("or maybe you should use `{}::mem::replace`?", sugg) + ); + } + }); + } + } + } +} + +/// Implementation of the xor case for `MANUAL_SWAP` lint. +fn check_xor_swap(cx: &LateContext<'_>, block: &Block<'_>) { + for window in block.stmts.windows(3) { + if_chain! { + if let Some((lhs0, rhs0)) = extract_sides_of_xor_assign(&window[0]); + if let Some((lhs1, rhs1)) = extract_sides_of_xor_assign(&window[1]); + if let Some((lhs2, rhs2)) = extract_sides_of_xor_assign(&window[2]); + if eq_expr_value(cx, lhs0, rhs1); + if eq_expr_value(cx, lhs2, rhs1); + if eq_expr_value(cx, lhs1, rhs0); + if eq_expr_value(cx, lhs1, rhs2); + then { + let span = window[0].span.to(window[2].span); + generate_swap_warning(cx, lhs0, rhs0, span, true); + } + }; + } +} + +/// Returns the lhs and rhs of an xor assignment statement. +fn extract_sides_of_xor_assign<'a, 'hir>(stmt: &'a Stmt<'hir>) -> Option<(&'a Expr<'hir>, &'a Expr<'hir>)> { + if let StmtKind::Semi(expr) = stmt.kind { + if let ExprKind::AssignOp( + Spanned { + node: BinOpKind::BitXor, + .. + }, + lhs, + rhs, + ) = expr.kind + { + return Some((lhs, rhs)); + } + } + None +} diff --git a/src/tools/clippy/clippy_lints/src/swap_ptr_to_ref.rs b/src/tools/clippy/clippy_lints/src/swap_ptr_to_ref.rs new file mode 100644 index 000000000..3cbbda80f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/swap_ptr_to_ref.rs @@ -0,0 +1,80 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_with_context; +use clippy_utils::{match_def_path, path_def_id, paths}; +use rustc_errors::Applicability; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{Span, SyntaxContext}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `core::mem::swap` where either parameter is derived from a pointer + /// + /// ### Why is this bad? + /// When at least one parameter to `swap` is derived from a pointer it may overlap with the + /// other. This would then lead to undefined behavior. + /// + /// ### Example + /// ```rust + /// unsafe fn swap(x: &[*mut u32], y: &[*mut u32]) { + /// for (&x, &y) in x.iter().zip(y) { + /// core::mem::swap(&mut *x, &mut *y); + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// unsafe fn swap(x: &[*mut u32], y: &[*mut u32]) { + /// for (&x, &y) in x.iter().zip(y) { + /// core::ptr::swap(x, y); + /// } + /// } + /// ``` + #[clippy::version = "1.63.0"] + pub SWAP_PTR_TO_REF, + suspicious, + "call to `mem::swap` using pointer derived references" +} +declare_lint_pass!(SwapPtrToRef => [SWAP_PTR_TO_REF]); + +impl LateLintPass<'_> for SwapPtrToRef { + fn check_expr(&mut self, cx: &LateContext<'_>, e: &Expr<'_>) { + if let ExprKind::Call(fn_expr, [arg1, arg2]) = e.kind + && let Some(fn_id) = path_def_id(cx, fn_expr) + && match_def_path(cx, fn_id, &paths::MEM_SWAP) + && let ctxt = e.span.ctxt() + && let (from_ptr1, arg1_span) = is_ptr_to_ref(cx, arg1, ctxt) + && let (from_ptr2, arg2_span) = is_ptr_to_ref(cx, arg2, ctxt) + && (from_ptr1 || from_ptr2) + { + span_lint_and_then( + cx, + SWAP_PTR_TO_REF, + e.span, + "call to `core::mem::swap` with a parameter derived from a raw pointer", + |diag| { + if !((from_ptr1 && arg1_span.is_none()) || (from_ptr2 && arg2_span.is_none())) { + let mut app = Applicability::MachineApplicable; + let snip1 = snippet_with_context(cx, arg1_span.unwrap_or(arg1.span), ctxt, "..", &mut app).0; + let snip2 = snippet_with_context(cx, arg2_span.unwrap_or(arg2.span), ctxt, "..", &mut app).0; + diag.span_suggestion(e.span, "use ptr::swap", format!("core::ptr::swap({}, {})", snip1, snip2), app); + } + } + ); + } + } +} + +/// Checks if the expression converts a mutable pointer to a mutable reference. If it is, also +/// returns the span of the pointer expression if it's suitable for making a suggestion. +fn is_ptr_to_ref(cx: &LateContext<'_>, e: &Expr<'_>, ctxt: SyntaxContext) -> (bool, Option<Span>) { + if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, borrowed_expr) = e.kind + && let ExprKind::Unary(UnOp::Deref, derefed_expr) = borrowed_expr.kind + && cx.typeck_results().expr_ty(derefed_expr).is_unsafe_ptr() + { + (true, (borrowed_expr.span.ctxt() == ctxt || derefed_expr.span.ctxt() == ctxt).then_some(derefed_expr.span)) + } else { + (false, None) + } +} diff --git a/src/tools/clippy/clippy_lints/src/tabs_in_doc_comments.rs b/src/tools/clippy/clippy_lints/src/tabs_in_doc_comments.rs new file mode 100644 index 000000000..e223aea29 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/tabs_in_doc_comments.rs @@ -0,0 +1,230 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::ast; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::{BytePos, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks doc comments for usage of tab characters. + /// + /// ### Why is this bad? + /// The rust style-guide promotes spaces instead of tabs for indentation. + /// To keep a consistent view on the source, also doc comments should not have tabs. + /// Also, explaining ascii-diagrams containing tabs can get displayed incorrectly when the + /// display settings of the author and reader differ. + /// + /// ### Example + /// ```rust + /// /// + /// /// Struct to hold two strings: + /// /// - first one + /// /// - second one + /// pub struct DoubleString { + /// /// + /// /// - First String: + /// /// - needs to be inside here + /// first_string: String, + /// /// + /// /// - Second String: + /// /// - needs to be inside here + /// second_string: String, + ///} + /// ``` + /// + /// Will be converted to: + /// ```rust + /// /// + /// /// Struct to hold two strings: + /// /// - first one + /// /// - second one + /// pub struct DoubleString { + /// /// + /// /// - First String: + /// /// - needs to be inside here + /// first_string: String, + /// /// + /// /// - Second String: + /// /// - needs to be inside here + /// second_string: String, + ///} + /// ``` + #[clippy::version = "1.41.0"] + pub TABS_IN_DOC_COMMENTS, + style, + "using tabs in doc comments is not recommended" +} + +declare_lint_pass!(TabsInDocComments => [TABS_IN_DOC_COMMENTS]); + +impl TabsInDocComments { + fn warn_if_tabs_in_doc(cx: &EarlyContext<'_>, attr: &ast::Attribute) { + if let ast::AttrKind::DocComment(_, comment) = attr.kind { + let comment = comment.as_str(); + + for (lo, hi) in get_chunks_of_tabs(comment) { + // +3 skips the opening delimiter + let new_span = Span::new( + attr.span.lo() + BytePos(3 + lo), + attr.span.lo() + BytePos(3 + hi), + attr.span.ctxt(), + attr.span.parent(), + ); + span_lint_and_sugg( + cx, + TABS_IN_DOC_COMMENTS, + new_span, + "using tabs in doc comments is not recommended", + "consider using four spaces per tab", + " ".repeat((hi - lo) as usize), + Applicability::MaybeIncorrect, + ); + } + } + } +} + +impl EarlyLintPass for TabsInDocComments { + fn check_attribute(&mut self, cx: &EarlyContext<'_>, attribute: &ast::Attribute) { + Self::warn_if_tabs_in_doc(cx, attribute); + } +} + +/// +/// scans the string for groups of tabs and returns the start(inclusive) and end positions +/// (exclusive) of all groups +/// e.g. "sd\tasd\t\taa" will be converted to [(2, 3), (6, 8)] as +/// 012 3456 7 89 +/// ^-^ ^---^ +fn get_chunks_of_tabs(the_str: &str) -> Vec<(u32, u32)> { + let line_length_way_to_long = "doc comment longer than 2^32 chars"; + let mut spans: Vec<(u32, u32)> = vec![]; + let mut current_start: u32 = 0; + + // tracker to decide if the last group of tabs is not closed by a non-tab character + let mut is_active = false; + + // Note that we specifically need the char _byte_ indices here, not the positional indexes + // within the char array to deal with multi-byte characters properly. `char_indices` does + // exactly that. It provides an iterator over tuples of the form `(byte position, char)`. + let char_indices: Vec<_> = the_str.char_indices().collect(); + + if let [(_, '\t')] = char_indices.as_slice() { + return vec![(0, 1)]; + } + + for entry in char_indices.windows(2) { + match entry { + [(_, '\t'), (_, '\t')] => { + // either string starts with double tab, then we have to set it active, + // otherwise is_active is true anyway + is_active = true; + }, + [(_, _), (index_b, '\t')] => { + // as ['\t', '\t'] is excluded, this has to be a start of a tab group, + // set indices accordingly + is_active = true; + current_start = u32::try_from(*index_b).unwrap(); + }, + [(_, '\t'), (index_b, _)] => { + // this now has to be an end of the group, hence we have to push a new tuple + is_active = false; + spans.push((current_start, u32::try_from(*index_b).unwrap())); + }, + _ => {}, + } + } + + // only possible when tabs are at the end, insert last group + if is_active { + spans.push(( + current_start, + u32::try_from(char_indices.last().unwrap().0 + 1).expect(line_length_way_to_long), + )); + } + + spans +} + +#[cfg(test)] +mod tests_for_get_chunks_of_tabs { + use super::get_chunks_of_tabs; + + #[test] + fn test_unicode_han_string() { + let res = get_chunks_of_tabs(" \u{4f4d}\t"); + + assert_eq!(res, vec![(4, 5)]); + } + + #[test] + fn test_empty_string() { + let res = get_chunks_of_tabs(""); + + assert_eq!(res, vec![]); + } + + #[test] + fn test_simple() { + let res = get_chunks_of_tabs("sd\t\t\taa"); + + assert_eq!(res, vec![(2, 5)]); + } + + #[test] + fn test_only_t() { + let res = get_chunks_of_tabs("\t\t"); + + assert_eq!(res, vec![(0, 2)]); + } + + #[test] + fn test_only_one_t() { + let res = get_chunks_of_tabs("\t"); + + assert_eq!(res, vec![(0, 1)]); + } + + #[test] + fn test_double() { + let res = get_chunks_of_tabs("sd\tasd\t\taa"); + + assert_eq!(res, vec![(2, 3), (6, 8)]); + } + + #[test] + fn test_start() { + let res = get_chunks_of_tabs("\t\taa"); + + assert_eq!(res, vec![(0, 2)]); + } + + #[test] + fn test_end() { + let res = get_chunks_of_tabs("aa\t\t"); + + assert_eq!(res, vec![(2, 4)]); + } + + #[test] + fn test_start_single() { + let res = get_chunks_of_tabs("\taa"); + + assert_eq!(res, vec![(0, 1)]); + } + + #[test] + fn test_end_single() { + let res = get_chunks_of_tabs("aa\t"); + + assert_eq!(res, vec![(2, 3)]); + } + + #[test] + fn test_no_tabs() { + let res = get_chunks_of_tabs("dsfs"); + + assert_eq!(res, vec![]); + } +} diff --git a/src/tools/clippy/clippy_lints/src/temporary_assignment.rs b/src/tools/clippy/clippy_lints/src/temporary_assignment.rs new file mode 100644 index 000000000..3766b8f8e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/temporary_assignment.rs @@ -0,0 +1,44 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::is_adjusted; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for construction of a structure or tuple just to + /// assign a value in it. + /// + /// ### Why is this bad? + /// Readability. If the structure is only created to be + /// updated, why not write the structure you want in the first place? + /// + /// ### Example + /// ```rust + /// (0, 0).0 = 1 + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TEMPORARY_ASSIGNMENT, + complexity, + "assignments to temporaries" +} + +fn is_temporary(expr: &Expr<'_>) -> bool { + matches!(&expr.kind, ExprKind::Struct(..) | ExprKind::Tup(..)) +} + +declare_lint_pass!(TemporaryAssignment => [TEMPORARY_ASSIGNMENT]); + +impl<'tcx> LateLintPass<'tcx> for TemporaryAssignment { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let ExprKind::Assign(target, ..) = &expr.kind { + let mut base = target; + while let ExprKind::Field(f, _) | ExprKind::Index(f, _) = &base.kind { + base = f; + } + if is_temporary(base) && !is_adjusted(cx, base) { + span_lint(cx, TEMPORARY_ASSIGNMENT, expr.span, "assignment to temporary"); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/to_digit_is_some.rs b/src/tools/clippy/clippy_lints/src/to_digit_is_some.rs new file mode 100644 index 000000000..aa6c01b3a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/to_digit_is_some.rs @@ -0,0 +1,99 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::match_def_path; +use clippy_utils::source::snippet_with_applicability; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `.to_digit(..).is_some()` on `char`s. + /// + /// ### Why is this bad? + /// This is a convoluted way of checking if a `char` is a digit. It's + /// more straight forward to use the dedicated `is_digit` method. + /// + /// ### Example + /// ```rust + /// # let c = 'c'; + /// # let radix = 10; + /// let is_digit = c.to_digit(radix).is_some(); + /// ``` + /// can be written as: + /// ``` + /// # let c = 'c'; + /// # let radix = 10; + /// let is_digit = c.is_digit(radix); + /// ``` + #[clippy::version = "1.41.0"] + pub TO_DIGIT_IS_SOME, + style, + "`char.is_digit()` is clearer" +} + +declare_lint_pass!(ToDigitIsSome => [TO_DIGIT_IS_SOME]); + +impl<'tcx> LateLintPass<'tcx> for ToDigitIsSome { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if_chain! { + if let hir::ExprKind::MethodCall(is_some_path, is_some_args, _) = &expr.kind; + if is_some_path.ident.name.as_str() == "is_some"; + if let [to_digit_expr] = &**is_some_args; + then { + let match_result = match &to_digit_expr.kind { + hir::ExprKind::MethodCall(to_digits_path, to_digit_args, _) => { + if_chain! { + if let [char_arg, radix_arg] = &**to_digit_args; + if to_digits_path.ident.name.as_str() == "to_digit"; + let char_arg_ty = cx.typeck_results().expr_ty_adjusted(char_arg); + if *char_arg_ty.kind() == ty::Char; + then { + Some((true, char_arg, radix_arg)) + } else { + None + } + } + } + hir::ExprKind::Call(to_digits_call, to_digit_args) => { + if_chain! { + if let [char_arg, radix_arg] = &**to_digit_args; + if let hir::ExprKind::Path(to_digits_path) = &to_digits_call.kind; + if let to_digits_call_res = cx.qpath_res(to_digits_path, to_digits_call.hir_id); + if let Some(to_digits_def_id) = to_digits_call_res.opt_def_id(); + if match_def_path(cx, to_digits_def_id, &["core", "char", "methods", "<impl char>", "to_digit"]); + then { + Some((false, char_arg, radix_arg)) + } else { + None + } + } + } + _ => None + }; + + if let Some((is_method_call, char_arg, radix_arg)) = match_result { + let mut applicability = Applicability::MachineApplicable; + let char_arg_snip = snippet_with_applicability(cx, char_arg.span, "_", &mut applicability); + let radix_snip = snippet_with_applicability(cx, radix_arg.span, "_", &mut applicability); + + span_lint_and_sugg( + cx, + TO_DIGIT_IS_SOME, + expr.span, + "use of `.to_digit(..).is_some()`", + "try this", + if is_method_call { + format!("{}.is_digit({})", char_arg_snip, radix_snip) + } else { + format!("char::is_digit({}, {})", char_arg_snip, radix_snip) + }, + applicability, + ); + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/trailing_empty_array.rs b/src/tools/clippy/clippy_lints/src/trailing_empty_array.rs new file mode 100644 index 000000000..58cc057a3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/trailing_empty_array.rs @@ -0,0 +1,78 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{HirId, Item, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::Const; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Displays a warning when a struct with a trailing zero-sized array is declared without a `repr` attribute. + /// + /// ### Why is this bad? + /// Zero-sized arrays aren't very useful in Rust itself, so such a struct is likely being created to pass to C code or in some other situation where control over memory layout matters (for example, in conjunction with manual allocation to make it easy to compute the offset of the array). Either way, `#[repr(C)]` (or another `repr` attribute) is needed. + /// + /// ### Example + /// ```rust + /// struct RarelyUseful { + /// some_field: u32, + /// last: [u32; 0], + /// } + /// ``` + /// + /// Use instead: + /// ```rust + /// #[repr(C)] + /// struct MoreOftenUseful { + /// some_field: usize, + /// last: [u32; 0], + /// } + /// ``` + #[clippy::version = "1.58.0"] + pub TRAILING_EMPTY_ARRAY, + nursery, + "struct with a trailing zero-sized array but without `#[repr(C)]` or another `repr` attribute" +} +declare_lint_pass!(TrailingEmptyArray => [TRAILING_EMPTY_ARRAY]); + +impl<'tcx> LateLintPass<'tcx> for TrailingEmptyArray { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if is_struct_with_trailing_zero_sized_array(cx, item) && !has_repr_attr(cx, item.hir_id()) { + span_lint_and_help( + cx, + TRAILING_EMPTY_ARRAY, + item.span, + "trailing zero-sized array in a struct which is not marked with a `repr` attribute", + None, + &format!( + "consider annotating `{}` with `#[repr(C)]` or another `repr` attribute", + cx.tcx.def_path_str(item.def_id.to_def_id()) + ), + ); + } + } +} + +fn is_struct_with_trailing_zero_sized_array(cx: &LateContext<'_>, item: &Item<'_>) -> bool { + if_chain! { + // First check if last field is an array + if let ItemKind::Struct(data, _) = &item.kind; + if let Some(last_field) = data.fields().last(); + if let rustc_hir::TyKind::Array(_, rustc_hir::ArrayLen::Body(length)) = last_field.ty.kind; + + // Then check if that that array zero-sized + let length_ldid = cx.tcx.hir().local_def_id(length.hir_id); + let length = Const::from_anon_const(cx.tcx, length_ldid); + let length = length.try_eval_usize(cx.tcx, cx.param_env); + if let Some(length) = length; + then { + length == 0 + } else { + false + } + } +} + +fn has_repr_attr(cx: &LateContext<'_>, hir_id: HirId) -> bool { + cx.tcx.hir().attrs(hir_id).iter().any(|attr| attr.has_name(sym::repr)) +} diff --git a/src/tools/clippy/clippy_lints/src/trait_bounds.rs b/src/tools/clippy/clippy_lints/src/trait_bounds.rs new file mode 100644 index 000000000..0a42a31fb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/trait_bounds.rs @@ -0,0 +1,376 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; +use clippy_utils::source::{snippet, snippet_opt, snippet_with_applicability}; +use clippy_utils::{SpanlessEq, SpanlessHash}; +use core::hash::{Hash, Hasher}; +use if_chain::if_chain; +use itertools::Itertools; +use rustc_data_structures::fx::FxHashMap; +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, + TraitBoundModifier, TraitItem, TraitRef, Ty, TyKind, WherePredicate, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::{BytePos, Span}; + +declare_clippy_lint! { + /// ### What it does + /// This lint warns about unnecessary type repetitions in trait bounds + /// + /// ### Why is this bad? + /// Repeating the type for every bound makes the code + /// less readable than combining the bounds + /// + /// ### Example + /// ```rust + /// pub fn foo<T>(t: T) where T: Copy, T: Clone {} + /// ``` + /// + /// Use instead: + /// ```rust + /// pub fn foo<T>(t: T) where T: Copy + Clone {} + /// ``` + #[clippy::version = "1.38.0"] + pub TYPE_REPETITION_IN_BOUNDS, + nursery, + "types are repeated unnecessary in trait bounds use `+` instead of using `T: _, T: _`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for cases where generics are being used and multiple + /// syntax specifications for trait bounds are used simultaneously. + /// + /// ### Why is this bad? + /// Duplicate bounds makes the code + /// less readable than specifying them only once. + /// + /// ### Example + /// ```rust + /// fn func<T: Clone + Default>(arg: T) where T: Clone + Default {} + /// ``` + /// + /// Use instead: + /// ```rust + /// # mod hidden { + /// fn func<T: Clone + Default>(arg: T) {} + /// # } + /// + /// // or + /// + /// fn func<T>(arg: T) where T: Clone + Default {} + /// ``` + /// + /// ```rust + /// fn foo<T: Default + Default>(bar: T) {} + /// ``` + /// Use instead: + /// ```rust + /// fn foo<T: Default>(bar: T) {} + /// ``` + /// + /// ```rust + /// fn foo<T>(bar: T) where T: Default + Default {} + /// ``` + /// Use instead: + /// ```rust + /// fn foo<T>(bar: T) where T: Default {} + /// ``` + #[clippy::version = "1.47.0"] + pub TRAIT_DUPLICATION_IN_BOUNDS, + nursery, + "check if the same trait bounds are specified more than once during a generic declaration" +} + +#[derive(Copy, Clone)] +pub struct TraitBounds { + max_trait_bounds: u64, +} + +impl TraitBounds { + #[must_use] + pub fn new(max_trait_bounds: u64) -> Self { + Self { max_trait_bounds } + } +} + +impl_lint_pass!(TraitBounds => [TYPE_REPETITION_IN_BOUNDS, TRAIT_DUPLICATION_IN_BOUNDS]); + +impl<'tcx> LateLintPass<'tcx> for TraitBounds { + fn check_generics(&mut self, cx: &LateContext<'tcx>, gen: &'tcx Generics<'_>) { + self.check_type_repetition(cx, gen); + check_trait_bound_duplication(cx, gen); + check_bounds_or_where_duplication(cx, gen); + } + + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + // special handling for self trait bounds as these are not considered generics + // ie. trait Foo: Display {} + if let Item { + kind: ItemKind::Trait(_, _, _, bounds, ..), + .. + } = item + { + rollup_traits(cx, bounds, "these bounds contain repeated elements"); + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'tcx>) { + let mut self_bounds_map = FxHashMap::default(); + + for predicate in item.generics.predicates { + if_chain! { + if let WherePredicate::BoundPredicate(ref bound_predicate) = predicate; + if bound_predicate.origin != PredicateOrigin::ImplTrait; + if !bound_predicate.span.from_expansion(); + if let TyKind::Path(QPath::Resolved(_, Path { segments, .. })) = bound_predicate.bounded_ty.kind; + if let Some(PathSegment { + res: Some(Res::SelfTy{ trait_: Some(def_id), alias_to: _ }), .. + }) = segments.first(); + if let Some( + Node::Item( + Item { + kind: ItemKind::Trait(_, _, _, self_bounds, _), + .. } + ) + ) = cx.tcx.hir().get_if_local(*def_id); + then { + if self_bounds_map.is_empty() { + for bound in self_bounds.iter() { + let Some((self_res, self_segments, _)) = get_trait_info_from_bound(bound) else { continue }; + self_bounds_map.insert(self_res, self_segments); + } + } + + bound_predicate + .bounds + .iter() + .filter_map(get_trait_info_from_bound) + .for_each(|(trait_item_res, trait_item_segments, span)| { + if let Some(self_segments) = self_bounds_map.get(&trait_item_res) { + if SpanlessEq::new(cx).eq_path_segments(self_segments, trait_item_segments) { + span_lint_and_help( + cx, + TRAIT_DUPLICATION_IN_BOUNDS, + span, + "this trait bound is already specified in trait declaration", + None, + "consider removing this trait bound", + ); + } + } + }); + } + } + } + } +} + +impl TraitBounds { + fn check_type_repetition<'tcx>(self, cx: &LateContext<'tcx>, gen: &'tcx Generics<'_>) { + struct SpanlessTy<'cx, 'tcx> { + ty: &'tcx Ty<'tcx>, + cx: &'cx LateContext<'tcx>, + } + impl PartialEq for SpanlessTy<'_, '_> { + fn eq(&self, other: &Self) -> bool { + let mut eq = SpanlessEq::new(self.cx); + eq.inter_expr().eq_ty(self.ty, other.ty) + } + } + impl Hash for SpanlessTy<'_, '_> { + fn hash<H: Hasher>(&self, h: &mut H) { + let mut t = SpanlessHash::new(self.cx); + t.hash_ty(self.ty); + h.write_u64(t.finish()); + } + } + impl Eq for SpanlessTy<'_, '_> {} + + if gen.span.from_expansion() { + return; + } + let mut map: UnhashMap<SpanlessTy<'_, '_>, Vec<&GenericBound<'_>>> = UnhashMap::default(); + let mut applicability = Applicability::MaybeIncorrect; + for bound in gen.predicates { + if_chain! { + if let WherePredicate::BoundPredicate(ref p) = bound; + 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<_>>() + ); + + then { + let trait_bounds = v + .iter() + .copied() + .chain(p.bounds.iter()) + .filter_map(get_trait_info_from_bound) + .map(|(_, _, span)| snippet_with_applicability(cx, span, "..", &mut applicability)) + .join(" + "); + let hint_string = format!( + "consider combining the bounds: `{}: {}`", + snippet(cx, p.bounded_ty.span, "_"), + trait_bounds, + ); + span_lint_and_help( + cx, + TYPE_REPETITION_IN_BOUNDS, + p.span, + "this type has already been used as a bound predicate", + None, + &hint_string, + ); + } + } + } + } +} + +fn check_trait_bound_duplication(cx: &LateContext<'_>, gen: &'_ Generics<'_>) { + if gen.span.from_expansion() || gen.params.is_empty() || gen.predicates.is_empty() { + return; + } + + let mut map = FxHashMap::<_, Vec<_>>::default(); + for predicate in gen.predicates { + if_chain! { + if let WherePredicate::BoundPredicate(ref bound_predicate) = predicate; + if bound_predicate.origin != PredicateOrigin::ImplTrait; + if !bound_predicate.span.from_expansion(); + if let TyKind::Path(QPath::Resolved(_, Path { segments, .. })) = bound_predicate.bounded_ty.kind; + if let Some(segment) = segments.first(); + then { + for (res_where, _, span_where) in bound_predicate.bounds.iter().filter_map(get_trait_info_from_bound) { + let trait_resolutions_direct = map.entry(segment.ident).or_default(); + if let Some((_, span_direct)) = trait_resolutions_direct + .iter() + .find(|(res_direct, _)| *res_direct == res_where) { + span_lint_and_help( + cx, + TRAIT_DUPLICATION_IN_BOUNDS, + *span_direct, + "this trait bound is already specified in the where clause", + None, + "consider removing this trait bound", + ); + } + else { + trait_resolutions_direct.push((res_where, span_where)); + } + } + } + } + } +} + +#[derive(PartialEq, Eq, Hash, Debug)] +struct ComparableTraitRef(Res, Vec<Res>); + +fn check_bounds_or_where_duplication(cx: &LateContext<'_>, gen: &'_ Generics<'_>) { + if gen.span.from_expansion() { + return; + } + + for predicate in gen.predicates { + if let WherePredicate::BoundPredicate(ref bound_predicate) = predicate { + let msg = if predicate.in_where_clause() { + "these where clauses contain repeated elements" + } else { + "these bounds contain repeated elements" + }; + rollup_traits(cx, bound_predicate.bounds, msg); + } + } +} + +fn get_trait_info_from_bound<'a>(bound: &'a GenericBound<'_>) -> Option<(Res, &'a [PathSegment<'a>], Span)> { + if let GenericBound::Trait(t, tbm) = bound { + let trait_path = t.trait_ref.path; + let trait_span = { + let path_span = trait_path.span; + if let TraitBoundModifier::Maybe = tbm { + path_span.with_lo(path_span.lo() - BytePos(1)) // include the `?` + } else { + path_span + } + }; + Some((trait_path.res, trait_path.segments, trait_span)) + } else { + None + } +} + +// FIXME: ComparableTraitRef does not support nested bounds needed for associated_type_bounds +fn into_comparable_trait_ref(trait_ref: &TraitRef<'_>) -> ComparableTraitRef { + ComparableTraitRef( + trait_ref.path.res, + trait_ref + .path + .segments + .iter() + .filter_map(|segment| { + // get trait bound type arguments + Some(segment.args?.args.iter().filter_map(|arg| { + if_chain! { + if let GenericArg::Type(ty) = arg; + if let TyKind::Path(QPath::Resolved(_, path)) = ty.kind; + then { return Some(path.res) } + } + None + })) + }) + .flatten() + .collect(), + ) +} + +fn rollup_traits(cx: &LateContext<'_>, bounds: &[GenericBound<'_>], msg: &str) { + let mut map = FxHashMap::default(); + let mut repeated_res = false; + + let only_comparable_trait_refs = |bound: &GenericBound<'_>| { + if let GenericBound::Trait(t, _) = bound { + Some((into_comparable_trait_ref(&t.trait_ref), t.span)) + } else { + None + } + }; + + for bound in bounds.iter().filter_map(only_comparable_trait_refs) { + let (comparable_bound, span_direct) = bound; + if map.insert(comparable_bound, span_direct).is_some() { + repeated_res = true; + } + } + + if_chain! { + if repeated_res; + if let [first_trait, .., last_trait] = bounds; + then { + let all_trait_span = first_trait.span().to(last_trait.span()); + + let mut traits = map.values() + .filter_map(|span| snippet_opt(cx, *span)) + .collect::<Vec<_>>(); + traits.sort_unstable(); + let traits = traits.join(" + "); + + span_lint_and_sugg( + cx, + TRAIT_DUPLICATION_IN_BOUNDS, + all_trait_span, + msg, + "try", + traits, + Applicability::MachineApplicable + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/crosspointer_transmute.rs b/src/tools/clippy/clippy_lints/src/transmute/crosspointer_transmute.rs new file mode 100644 index 000000000..25d0543c8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/crosspointer_transmute.rs @@ -0,0 +1,37 @@ +use super::CROSSPOINTER_TRANSMUTE; +use clippy_utils::diagnostics::span_lint; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +/// Checks for `crosspointer_transmute` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>, from_ty: Ty<'tcx>, to_ty: Ty<'tcx>) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::RawPtr(from_ptr), _) if from_ptr.ty == to_ty => { + span_lint( + cx, + CROSSPOINTER_TRANSMUTE, + e.span, + &format!( + "transmute from a type (`{}`) to the type that it points to (`{}`)", + from_ty, to_ty + ), + ); + true + }, + (_, ty::RawPtr(to_ptr)) if to_ptr.ty == from_ty => { + span_lint( + cx, + CROSSPOINTER_TRANSMUTE, + e.span, + &format!( + "transmute from a type (`{}`) to a pointer to that type (`{}`)", + from_ty, to_ty + ), + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/mod.rs b/src/tools/clippy/clippy_lints/src/transmute/mod.rs new file mode 100644 index 000000000..5f3e98144 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/mod.rs @@ -0,0 +1,460 @@ +mod crosspointer_transmute; +mod transmute_float_to_int; +mod transmute_int_to_bool; +mod transmute_int_to_char; +mod transmute_int_to_float; +mod transmute_num_to_bytes; +mod transmute_ptr_to_ptr; +mod transmute_ptr_to_ref; +mod transmute_ref_to_ref; +mod transmute_undefined_repr; +mod transmutes_expressible_as_ptr_casts; +mod unsound_collection_transmute; +mod useless_transmute; +mod utils; +mod wrong_transmute; + +use clippy_utils::in_constant; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes that can't ever be correct on any + /// architecture. + /// + /// ### Why is this bad? + /// It's basically guaranteed to be undefined behavior. + /// + /// ### Known problems + /// When accessing C, users might want to store pointer + /// sized objects in `extradata` arguments to save an allocation. + /// + /// ### Example + /// ```ignore + /// let ptr: *const T = core::intrinsics::transmute('x') + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WRONG_TRANSMUTE, + correctness, + "transmutes that are confusing at best, undefined behavior at worst and always useless" +} + +// FIXME: Move this to `complexity` again, after #5343 is fixed +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes to the original type of the object + /// and transmutes that could be a cast. + /// + /// ### Why is this bad? + /// Readability. The code tricks people into thinking that + /// something complex is going on. + /// + /// ### Example + /// ```rust,ignore + /// core::intrinsics::transmute(t); // where the result type is the same as `t`'s + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USELESS_TRANSMUTE, + complexity, + "transmutes that have the same to and from types or could be a cast/coercion" +} + +// FIXME: Merge this lint with USELESS_TRANSMUTE once that is out of the nursery. +declare_clippy_lint! { + /// ### What it does + ///Checks for transmutes that could be a pointer cast. + /// + /// ### Why is this bad? + /// Readability. The code tricks people into thinking that + /// something complex is going on. + /// + /// ### Example + /// + /// ```rust + /// # let p: *const [i32] = &[]; + /// unsafe { std::mem::transmute::<*const [i32], *const [u16]>(p) }; + /// ``` + /// Use instead: + /// ```rust + /// # let p: *const [i32] = &[]; + /// p as *const [u16]; + /// ``` + #[clippy::version = "1.47.0"] + pub TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS, + complexity, + "transmutes that could be a pointer cast" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes between a type `T` and `*T`. + /// + /// ### Why is this bad? + /// It's easy to mistakenly transmute between a type and a + /// pointer to that type. + /// + /// ### Example + /// ```rust,ignore + /// core::intrinsics::transmute(t) // where the result type is the same as + /// // `*t` or `&t`'s + /// ``` + #[clippy::version = "pre 1.29.0"] + pub CROSSPOINTER_TRANSMUTE, + complexity, + "transmutes that have to or from types that are a pointer to the other" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from a pointer to a reference. + /// + /// ### Why is this bad? + /// This can always be rewritten with `&` and `*`. + /// + /// ### Known problems + /// - `mem::transmute` in statics and constants is stable from Rust 1.46.0, + /// while dereferencing raw pointer is not stable yet. + /// If you need to do this in those places, + /// you would have to use `transmute` instead. + /// + /// ### Example + /// ```rust,ignore + /// unsafe { + /// let _: &T = std::mem::transmute(p); // where p: *const T + /// } + /// + /// // can be written: + /// let _: &T = &*p; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRANSMUTE_PTR_TO_REF, + complexity, + "transmutes from a pointer to a reference type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from an integer to a `char`. + /// + /// ### Why is this bad? + /// Not every integer is a Unicode scalar value. + /// + /// ### Known problems + /// - [`from_u32`] which this lint suggests using is slower than `transmute` + /// as it needs to validate the input. + /// If you are certain that the input is always a valid Unicode scalar value, + /// use [`from_u32_unchecked`] which is as fast as `transmute` + /// but has a semantically meaningful name. + /// - You might want to handle `None` returned from [`from_u32`] instead of calling `unwrap`. + /// + /// [`from_u32`]: https://doc.rust-lang.org/std/char/fn.from_u32.html + /// [`from_u32_unchecked`]: https://doc.rust-lang.org/std/char/fn.from_u32_unchecked.html + /// + /// ### Example + /// ```rust + /// let x = 1_u32; + /// unsafe { + /// let _: char = std::mem::transmute(x); // where x: u32 + /// } + /// + /// // should be: + /// let _ = std::char::from_u32(x).unwrap(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRANSMUTE_INT_TO_CHAR, + complexity, + "transmutes from an integer to a `char`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from a `&[u8]` to a `&str`. + /// + /// ### Why is this bad? + /// Not every byte slice is a valid UTF-8 string. + /// + /// ### Known problems + /// - [`from_utf8`] which this lint suggests using is slower than `transmute` + /// as it needs to validate the input. + /// If you are certain that the input is always a valid UTF-8, + /// use [`from_utf8_unchecked`] which is as fast as `transmute` + /// but has a semantically meaningful name. + /// - You might want to handle errors returned from [`from_utf8`] instead of calling `unwrap`. + /// + /// [`from_utf8`]: https://doc.rust-lang.org/std/str/fn.from_utf8.html + /// [`from_utf8_unchecked`]: https://doc.rust-lang.org/std/str/fn.from_utf8_unchecked.html + /// + /// ### Example + /// ```rust + /// let b: &[u8] = &[1_u8, 2_u8]; + /// unsafe { + /// let _: &str = std::mem::transmute(b); // where b: &[u8] + /// } + /// + /// // should be: + /// let _ = std::str::from_utf8(b).unwrap(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRANSMUTE_BYTES_TO_STR, + complexity, + "transmutes from a `&[u8]` to a `&str`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from an integer to a `bool`. + /// + /// ### Why is this bad? + /// This might result in an invalid in-memory representation of a `bool`. + /// + /// ### Example + /// ```rust + /// let x = 1_u8; + /// unsafe { + /// let _: bool = std::mem::transmute(x); // where x: u8 + /// } + /// + /// // should be: + /// let _: bool = x != 0; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRANSMUTE_INT_TO_BOOL, + complexity, + "transmutes from an integer to a `bool`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from an integer to a float. + /// + /// ### Why is this bad? + /// Transmutes are dangerous and error-prone, whereas `from_bits` is intuitive + /// and safe. + /// + /// ### Example + /// ```rust + /// unsafe { + /// let _: f32 = std::mem::transmute(1_u32); // where x: u32 + /// } + /// + /// // should be: + /// let _: f32 = f32::from_bits(1_u32); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRANSMUTE_INT_TO_FLOAT, + complexity, + "transmutes from an integer to a float" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from a float to an integer. + /// + /// ### Why is this bad? + /// Transmutes are dangerous and error-prone, whereas `to_bits` is intuitive + /// and safe. + /// + /// ### Example + /// ```rust + /// unsafe { + /// let _: u32 = std::mem::transmute(1f32); + /// } + /// + /// // should be: + /// let _: u32 = 1f32.to_bits(); + /// ``` + #[clippy::version = "1.41.0"] + pub TRANSMUTE_FLOAT_TO_INT, + complexity, + "transmutes from a float to an integer" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from a number to an array of `u8` + /// + /// ### Why this is bad? + /// Transmutes are dangerous and error-prone, whereas `to_ne_bytes` + /// is intuitive and safe. + /// + /// ### Example + /// ```rust + /// unsafe { + /// let x: [u8; 8] = std::mem::transmute(1i64); + /// } + /// + /// // should be + /// let x: [u8; 8] = 0i64.to_ne_bytes(); + /// ``` + #[clippy::version = "1.58.0"] + pub TRANSMUTE_NUM_TO_BYTES, + complexity, + "transmutes from a number to an array of `u8`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes from a pointer to a pointer, or + /// from a reference to a reference. + /// + /// ### Why is this bad? + /// Transmutes are dangerous, and these can instead be + /// written as casts. + /// + /// ### Example + /// ```rust + /// let ptr = &1u32 as *const u32; + /// unsafe { + /// // pointer-to-pointer transmute + /// let _: *const f32 = std::mem::transmute(ptr); + /// // ref-ref transmute + /// let _: &f32 = std::mem::transmute(&1u32); + /// } + /// // These can be respectively written: + /// let _ = ptr as *const f32; + /// let _ = unsafe{ &*(&1u32 as *const u32 as *const f32) }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TRANSMUTE_PTR_TO_PTR, + pedantic, + "transmutes from a pointer to a pointer / a reference to a reference" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes between collections whose + /// types have different ABI, size or alignment. + /// + /// ### Why is this bad? + /// This is undefined behavior. + /// + /// ### Known problems + /// Currently, we cannot know whether a type is a + /// collection, so we just lint the ones that come with `std`. + /// + /// ### Example + /// ```rust + /// // different size, therefore likely out-of-bounds memory access + /// // You absolutely do not want this in your code! + /// unsafe { + /// std::mem::transmute::<_, Vec<u32>>(vec![2_u16]) + /// }; + /// ``` + /// + /// You must always iterate, map and collect the values: + /// + /// ```rust + /// vec![2_u16].into_iter().map(u32::from).collect::<Vec<_>>(); + /// ``` + #[clippy::version = "1.40.0"] + pub UNSOUND_COLLECTION_TRANSMUTE, + correctness, + "transmute between collections of layout-incompatible types" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmutes between types which do not have a representation defined relative to + /// each other. + /// + /// ### Why is this bad? + /// The results of such a transmute are not defined. + /// + /// ### Known problems + /// This lint has had multiple problems in the past and was moved to `nursery`. See issue + /// [#8496](https://github.com/rust-lang/rust-clippy/issues/8496) for more details. + /// + /// ### Example + /// ```rust + /// struct Foo<T>(u32, T); + /// let _ = unsafe { core::mem::transmute::<Foo<u32>, Foo<i32>>(Foo(0u32, 0u32)) }; + /// ``` + /// Use instead: + /// ```rust + /// #[repr(C)] + /// struct Foo<T>(u32, T); + /// let _ = unsafe { core::mem::transmute::<Foo<u32>, Foo<i32>>(Foo(0u32, 0u32)) }; + /// ``` + #[clippy::version = "1.60.0"] + pub TRANSMUTE_UNDEFINED_REPR, + nursery, + "transmute to or from a type with an undefined representation" +} + +pub struct Transmute { + msrv: Option<RustcVersion>, +} +impl_lint_pass!(Transmute => [ + CROSSPOINTER_TRANSMUTE, + TRANSMUTE_PTR_TO_REF, + TRANSMUTE_PTR_TO_PTR, + USELESS_TRANSMUTE, + WRONG_TRANSMUTE, + TRANSMUTE_INT_TO_CHAR, + TRANSMUTE_BYTES_TO_STR, + TRANSMUTE_INT_TO_BOOL, + TRANSMUTE_INT_TO_FLOAT, + TRANSMUTE_FLOAT_TO_INT, + TRANSMUTE_NUM_TO_BYTES, + UNSOUND_COLLECTION_TRANSMUTE, + TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS, + TRANSMUTE_UNDEFINED_REPR, +]); +impl Transmute { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} +impl<'tcx> LateLintPass<'tcx> for Transmute { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::Call(path_expr, [arg]) = e.kind; + if let ExprKind::Path(QPath::Resolved(None, path)) = path_expr.kind; + if let Some(def_id) = path.res.opt_def_id(); + if cx.tcx.is_diagnostic_item(sym::transmute, def_id); + then { + // Avoid suggesting non-const operations in const contexts: + // - from/to bits (https://github.com/rust-lang/rust/issues/73736) + // - dereferencing raw pointers (https://github.com/rust-lang/rust/issues/51911) + // - char conversions (https://github.com/rust-lang/rust/issues/89259) + let const_context = in_constant(cx, e.hir_id); + + let from_ty = cx.typeck_results().expr_ty_adjusted(arg); + // Adjustments for `to_ty` happen after the call to `transmute`, so don't use them. + let to_ty = cx.typeck_results().expr_ty(e); + + // If useless_transmute is triggered, the other lints can be skipped. + if useless_transmute::check(cx, e, from_ty, to_ty, arg) { + return; + } + + let linted = wrong_transmute::check(cx, e, from_ty, to_ty) + | crosspointer_transmute::check(cx, e, from_ty, to_ty) + | transmute_ptr_to_ref::check(cx, e, from_ty, to_ty, arg, path, self.msrv) + | transmute_int_to_char::check(cx, e, from_ty, to_ty, arg, const_context) + | transmute_ref_to_ref::check(cx, e, from_ty, to_ty, arg, const_context) + | transmute_ptr_to_ptr::check(cx, e, from_ty, to_ty, arg) + | transmute_int_to_bool::check(cx, e, from_ty, to_ty, arg) + | transmute_int_to_float::check(cx, e, from_ty, to_ty, arg, const_context) + | transmute_float_to_int::check(cx, e, from_ty, to_ty, arg, const_context) + | transmute_num_to_bytes::check(cx, e, from_ty, to_ty, arg, const_context) + | ( + unsound_collection_transmute::check(cx, e, from_ty, to_ty) + || transmute_undefined_repr::check(cx, e, from_ty, to_ty) + ); + + if !linted { + transmutes_expressible_as_ptr_casts::check(cx, e, from_ty, to_ty, arg); + } + } + } + } + + extract_msrv_attr!(LateContext); +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_float_to_int.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_float_to_int.rs new file mode 100644 index 000000000..1bde977cf --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_float_to_int.rs @@ -0,0 +1,65 @@ +use super::TRANSMUTE_FLOAT_TO_INT; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg; +use if_chain::if_chain; +use rustc_ast as ast; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, UnOp}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +/// Checks for `transmute_float_to_int` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + mut arg: &'tcx Expr<'_>, + const_context: bool, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::Float(float_ty), ty::Int(_) | ty::Uint(_)) if !const_context => { + span_lint_and_then( + cx, + TRANSMUTE_FLOAT_TO_INT, + e.span, + &format!("transmute from a `{}` to a `{}`", from_ty, to_ty), + |diag| { + let mut sugg = sugg::Sugg::hir(cx, arg, ".."); + + if let ExprKind::Unary(UnOp::Neg, inner_expr) = &arg.kind { + arg = inner_expr; + } + + if_chain! { + // if the expression is a float literal and it is unsuffixed then + // add a suffix so the suggestion is valid and unambiguous + if let ExprKind::Lit(lit) = &arg.kind; + if let ast::LitKind::Float(_, ast::LitFloatType::Unsuffixed) = lit.node; + then { + let op = format!("{}{}", sugg, float_ty.name_str()).into(); + match sugg { + sugg::Sugg::MaybeParen(_) => sugg = sugg::Sugg::MaybeParen(op), + _ => sugg = sugg::Sugg::NonParen(op) + } + } + } + + sugg = sugg::Sugg::NonParen(format!("{}.to_bits()", sugg.maybe_par()).into()); + + // cast the result of `to_bits` if `to_ty` is signed + sugg = if let ty::Int(int_ty) = to_ty.kind() { + sugg.as_ty(int_ty.name_str().to_string()) + } else { + sugg + }; + + diag.span_suggestion(e.span, "consider using", sugg, Applicability::Unspecified); + }, + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_bool.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_bool.rs new file mode 100644 index 000000000..8c50b58ca --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_bool.rs @@ -0,0 +1,42 @@ +use super::TRANSMUTE_INT_TO_BOOL; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg; +use rustc_ast as ast; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use std::borrow::Cow; + +/// Checks for `transmute_int_to_bool` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::Int(ty::IntTy::I8) | ty::Uint(ty::UintTy::U8), ty::Bool) => { + span_lint_and_then( + cx, + TRANSMUTE_INT_TO_BOOL, + e.span, + &format!("transmute from a `{}` to a `bool`", from_ty), + |diag| { + let arg = sugg::Sugg::hir(cx, arg, ".."); + let zero = sugg::Sugg::NonParen(Cow::from("0")); + diag.span_suggestion( + e.span, + "consider using", + sugg::make_binop(ast::BinOpKind::Ne, &arg, &zero).to_string(), + Applicability::Unspecified, + ); + }, + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_char.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_char.rs new file mode 100644 index 000000000..9e1823c37 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_char.rs @@ -0,0 +1,46 @@ +use super::TRANSMUTE_INT_TO_CHAR; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg; +use rustc_ast as ast; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +/// Checks for `transmute_int_to_char` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, + const_context: bool, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::Int(ty::IntTy::I32) | ty::Uint(ty::UintTy::U32), &ty::Char) if !const_context => { + span_lint_and_then( + cx, + TRANSMUTE_INT_TO_CHAR, + e.span, + &format!("transmute from a `{}` to a `char`", from_ty), + |diag| { + let arg = sugg::Sugg::hir(cx, arg, ".."); + let arg = if let ty::Int(_) = from_ty.kind() { + arg.as_ty(ast::UintTy::U32.name_str()) + } else { + arg + }; + diag.span_suggestion( + e.span, + "consider using", + format!("std::char::from_u32({}).unwrap()", arg), + Applicability::Unspecified, + ); + }, + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_float.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_float.rs new file mode 100644 index 000000000..b8703052e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_int_to_float.rs @@ -0,0 +1,48 @@ +use super::TRANSMUTE_INT_TO_FLOAT; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +/// Checks for `transmute_int_to_float` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, + const_context: bool, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::Int(_) | ty::Uint(_), ty::Float(_)) if !const_context => { + span_lint_and_then( + cx, + TRANSMUTE_INT_TO_FLOAT, + e.span, + &format!("transmute from a `{}` to a `{}`", from_ty, to_ty), + |diag| { + let arg = sugg::Sugg::hir(cx, arg, ".."); + let arg = if let ty::Int(int_ty) = from_ty.kind() { + arg.as_ty(format!( + "u{}", + int_ty.bit_width().map_or_else(|| "size".to_string(), |v| v.to_string()) + )) + } else { + arg + }; + diag.span_suggestion( + e.span, + "consider using", + format!("{}::from_bits({})", to_ty, arg), + Applicability::Unspecified, + ); + }, + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_num_to_bytes.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_num_to_bytes.rs new file mode 100644 index 000000000..52d193d11 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_num_to_bytes.rs @@ -0,0 +1,49 @@ +use super::TRANSMUTE_NUM_TO_BYTES; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty, UintTy}; + +/// Checks for `transmute_int_to_float` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, + const_context: bool, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::Int(_) | ty::Uint(_) | ty::Float(_), ty::Array(arr_ty, _)) => { + if !matches!(arr_ty.kind(), ty::Uint(UintTy::U8)) { + return false; + } + if matches!(from_ty.kind(), ty::Float(_)) && const_context { + // TODO: Remove when const_float_bits_conv is stabilized + // rust#72447 + return false; + } + + span_lint_and_then( + cx, + TRANSMUTE_NUM_TO_BYTES, + e.span, + &format!("transmute from a `{}` to a `{}`", from_ty, to_ty), + |diag| { + let arg = sugg::Sugg::hir(cx, arg, ".."); + diag.span_suggestion( + e.span, + "consider using `to_ne_bytes()`", + format!("{}.to_ne_bytes()", arg), + Applicability::Unspecified, + ); + }, + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_ptr_to_ptr.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_ptr_to_ptr.rs new file mode 100644 index 000000000..31a9b69ca --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_ptr_to_ptr.rs @@ -0,0 +1,36 @@ +use super::TRANSMUTE_PTR_TO_PTR; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +/// Checks for `transmute_ptr_to_ptr` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::RawPtr(_), ty::RawPtr(to_ty)) => { + span_lint_and_then( + cx, + TRANSMUTE_PTR_TO_PTR, + e.span, + "transmute from a pointer to a pointer", + |diag| { + if let Some(arg) = sugg::Sugg::hir_opt(cx, arg) { + let sugg = arg.as_ty(cx.tcx.mk_ptr(*to_ty)); + diag.span_suggestion(e.span, "try", sugg, Applicability::Unspecified); + } + }, + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_ptr_to_ref.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_ptr_to_ref.rs new file mode 100644 index 000000000..5eb03275b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_ptr_to_ref.rs @@ -0,0 +1,84 @@ +use super::TRANSMUTE_PTR_TO_REF; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{meets_msrv, msrvs, sugg}; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, Expr, GenericArg, Mutability, Path, TyKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty, TypeVisitable}; +use rustc_semver::RustcVersion; + +/// Checks for `transmute_ptr_to_ref` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, + path: &'tcx Path<'_>, + msrv: Option<RustcVersion>, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::RawPtr(from_ptr_ty), ty::Ref(_, to_ref_ty, mutbl)) => { + span_lint_and_then( + cx, + TRANSMUTE_PTR_TO_REF, + e.span, + &format!( + "transmute from a pointer type (`{}`) to a reference type (`{}`)", + from_ty, to_ty + ), + |diag| { + let arg = sugg::Sugg::hir(cx, arg, ".."); + let (deref, cast) = if *mutbl == Mutability::Mut { + ("&mut *", "*mut") + } else { + ("&*", "*const") + }; + let mut app = Applicability::MachineApplicable; + + let sugg = if let Some(ty) = get_explicit_type(path) { + let ty_snip = snippet_with_applicability(cx, ty.span, "..", &mut app); + if meets_msrv(msrv, msrvs::POINTER_CAST) { + format!("{}{}.cast::<{}>()", deref, arg.maybe_par(), ty_snip) + } else if from_ptr_ty.has_erased_regions() { + sugg::make_unop(deref, arg.as_ty(format!("{} () as {} {}", cast, cast, ty_snip))) + .to_string() + } else { + sugg::make_unop(deref, arg.as_ty(format!("{} {}", cast, ty_snip))).to_string() + } + } else if from_ptr_ty.ty == *to_ref_ty { + if from_ptr_ty.has_erased_regions() { + if meets_msrv(msrv, msrvs::POINTER_CAST) { + format!("{}{}.cast::<{}>()", deref, arg.maybe_par(), to_ref_ty) + } else { + sugg::make_unop(deref, arg.as_ty(format!("{} () as {} {}", cast, cast, to_ref_ty))) + .to_string() + } + } else { + sugg::make_unop(deref, arg).to_string() + } + } else { + sugg::make_unop(deref, arg.as_ty(format!("{} {}", cast, to_ref_ty))).to_string() + }; + + diag.span_suggestion(e.span, "try", sugg, app); + }, + ); + true + }, + _ => false, + } +} + +/// Gets the type `Bar` in `…::transmute<Foo, &Bar>`. +fn get_explicit_type<'tcx>(path: &'tcx Path<'tcx>) -> Option<&'tcx hir::Ty<'tcx>> { + if let GenericArg::Type(ty) = path.segments.last()?.args?.args.get(1)? + && let TyKind::Rptr(_, ty) = &ty.kind + { + Some(ty.ty) + } else { + None + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_ref_to_ref.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_ref_to_ref.rs new file mode 100644 index 000000000..707a11d36 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_ref_to_ref.rs @@ -0,0 +1,89 @@ +use super::{TRANSMUTE_BYTES_TO_STR, TRANSMUTE_PTR_TO_PTR}; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::snippet; +use clippy_utils::sugg; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, Mutability}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +/// Checks for `transmute_bytes_to_str` and `transmute_ptr_to_ptr` lints. +/// Returns `true` if either one triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, + const_context: bool, +) -> bool { + let mut triggered = false; + + if let (ty::Ref(_, ty_from, from_mutbl), ty::Ref(_, ty_to, to_mutbl)) = (&from_ty.kind(), &to_ty.kind()) { + if_chain! { + if let (&ty::Slice(slice_ty), &ty::Str) = (&ty_from.kind(), &ty_to.kind()); + if let ty::Uint(ty::UintTy::U8) = slice_ty.kind(); + if from_mutbl == to_mutbl; + then { + let postfix = if *from_mutbl == Mutability::Mut { + "_mut" + } else { + "" + }; + + let snippet = snippet(cx, arg.span, ".."); + + span_lint_and_sugg( + cx, + TRANSMUTE_BYTES_TO_STR, + e.span, + &format!("transmute from a `{}` to a `{}`", from_ty, to_ty), + "consider using", + if const_context { + format!("std::str::from_utf8_unchecked{postfix}({snippet})") + } else { + format!("std::str::from_utf8{postfix}({snippet}).unwrap()") + }, + Applicability::MaybeIncorrect, + ); + triggered = true; + } else { + if (cx.tcx.erase_regions(from_ty) != cx.tcx.erase_regions(to_ty)) + && !const_context { + span_lint_and_then( + cx, + TRANSMUTE_PTR_TO_PTR, + e.span, + "transmute from a reference to a reference", + |diag| if let Some(arg) = sugg::Sugg::hir_opt(cx, arg) { + let ty_from_and_mut = ty::TypeAndMut { + ty: *ty_from, + mutbl: *from_mutbl + }; + let ty_to_and_mut = ty::TypeAndMut { ty: *ty_to, mutbl: *to_mutbl }; + let sugg_paren = arg + .as_ty(cx.tcx.mk_ptr(ty_from_and_mut)) + .as_ty(cx.tcx.mk_ptr(ty_to_and_mut)); + let sugg = if *to_mutbl == Mutability::Mut { + sugg_paren.mut_addr_deref() + } else { + sugg_paren.addr_deref() + }; + diag.span_suggestion( + e.span, + "try", + sugg, + Applicability::Unspecified, + ); + }, + ); + + triggered = true; + } + } + } + } + + triggered +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmute_undefined_repr.rs b/src/tools/clippy/clippy_lints/src/transmute/transmute_undefined_repr.rs new file mode 100644 index 000000000..20b348fc1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmute_undefined_repr.rs @@ -0,0 +1,372 @@ +use super::TRANSMUTE_UNDEFINED_REPR; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::ty::is_c_void; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::subst::{Subst, SubstsRef}; +use rustc_middle::ty::{self, IntTy, Ty, TypeAndMut, UintTy}; +use rustc_span::Span; + +#[allow(clippy::too_many_lines)] +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty_orig: Ty<'tcx>, + to_ty_orig: Ty<'tcx>, +) -> bool { + let mut from_ty = cx.tcx.erase_regions(from_ty_orig); + let mut to_ty = cx.tcx.erase_regions(to_ty_orig); + + while from_ty != to_ty { + match reduce_refs(cx, e.span, from_ty, to_ty) { + ReducedTys::FromFatPtr { + unsized_ty, + to_ty: to_sub_ty, + } => match reduce_ty(cx, to_sub_ty) { + ReducedTy::TypeErasure => break, + ReducedTy::UnorderedFields(ty) if is_size_pair(ty) => break, + ReducedTy::Ref(to_sub_ty) => { + from_ty = unsized_ty; + to_ty = to_sub_ty; + continue; + }, + _ => { + span_lint_and_then( + cx, + TRANSMUTE_UNDEFINED_REPR, + e.span, + &format!("transmute from `{}` which has an undefined layout", from_ty_orig), + |diag| { + if from_ty_orig.peel_refs() != unsized_ty { + diag.note(&format!("the contained type `&{}` has an undefined layout", unsized_ty)); + } + }, + ); + return true; + }, + }, + ReducedTys::ToFatPtr { + unsized_ty, + from_ty: from_sub_ty, + } => match reduce_ty(cx, from_sub_ty) { + ReducedTy::TypeErasure => break, + ReducedTy::UnorderedFields(ty) if is_size_pair(ty) => break, + ReducedTy::Ref(from_sub_ty) => { + from_ty = from_sub_ty; + to_ty = unsized_ty; + continue; + }, + _ => { + span_lint_and_then( + cx, + TRANSMUTE_UNDEFINED_REPR, + e.span, + &format!("transmute to `{}` which has an undefined layout", to_ty_orig), + |diag| { + if to_ty_orig.peel_refs() != unsized_ty { + diag.note(&format!("the contained type `&{}` has an undefined layout", unsized_ty)); + } + }, + ); + return true; + }, + }, + ReducedTys::ToPtr { + from_ty: from_sub_ty, + to_ty: to_sub_ty, + } => match reduce_ty(cx, from_sub_ty) { + ReducedTy::UnorderedFields(from_ty) => { + span_lint_and_then( + cx, + TRANSMUTE_UNDEFINED_REPR, + e.span, + &format!("transmute from `{}` which has an undefined layout", from_ty_orig), + |diag| { + if from_ty_orig.peel_refs() != from_ty { + diag.note(&format!("the contained type `{}` has an undefined layout", from_ty)); + } + }, + ); + return true; + }, + ReducedTy::Ref(from_sub_ty) => { + from_ty = from_sub_ty; + to_ty = to_sub_ty; + continue; + }, + _ => break, + }, + ReducedTys::FromPtr { + from_ty: from_sub_ty, + to_ty: to_sub_ty, + } => match reduce_ty(cx, to_sub_ty) { + ReducedTy::UnorderedFields(to_ty) => { + span_lint_and_then( + cx, + TRANSMUTE_UNDEFINED_REPR, + e.span, + &format!("transmute to `{}` which has an undefined layout", to_ty_orig), + |diag| { + if to_ty_orig.peel_refs() != to_ty { + diag.note(&format!("the contained type `{}` has an undefined layout", to_ty)); + } + }, + ); + return true; + }, + ReducedTy::Ref(to_sub_ty) => { + from_ty = from_sub_ty; + to_ty = to_sub_ty; + continue; + }, + _ => break, + }, + ReducedTys::Other { + from_ty: from_sub_ty, + to_ty: to_sub_ty, + } => match (reduce_ty(cx, from_sub_ty), reduce_ty(cx, to_sub_ty)) { + (ReducedTy::TypeErasure, _) | (_, ReducedTy::TypeErasure) => return false, + (ReducedTy::UnorderedFields(from_ty), ReducedTy::UnorderedFields(to_ty)) if from_ty != to_ty => { + let same_adt_did = if let (ty::Adt(from_def, from_subs), ty::Adt(to_def, to_subs)) + = (from_ty.kind(), to_ty.kind()) + && from_def == to_def + { + if same_except_params(from_subs, to_subs) { + return false; + } + Some(from_def.did()) + } else { + None + }; + span_lint_and_then( + cx, + TRANSMUTE_UNDEFINED_REPR, + e.span, + &format!( + "transmute from `{}` to `{}`, both of which have an undefined layout", + from_ty_orig, to_ty_orig + ), + |diag| { + if let Some(same_adt_did) = same_adt_did { + diag.note(&format!( + "two instances of the same generic type (`{}`) may have different layouts", + cx.tcx.item_name(same_adt_did) + )); + } else { + if from_ty_orig.peel_refs() != from_ty { + diag.note(&format!("the contained type `{}` has an undefined layout", from_ty)); + } + if to_ty_orig.peel_refs() != to_ty { + diag.note(&format!("the contained type `{}` has an undefined layout", to_ty)); + } + } + }, + ); + return true; + }, + ( + ReducedTy::UnorderedFields(from_ty), + ReducedTy::Other(_) | ReducedTy::OrderedFields(_) | ReducedTy::Ref(_), + ) => { + span_lint_and_then( + cx, + TRANSMUTE_UNDEFINED_REPR, + e.span, + &format!("transmute from `{}` which has an undefined layout", from_ty_orig), + |diag| { + if from_ty_orig.peel_refs() != from_ty { + diag.note(&format!("the contained type `{}` has an undefined layout", from_ty)); + } + }, + ); + return true; + }, + ( + ReducedTy::Other(_) | ReducedTy::OrderedFields(_) | ReducedTy::Ref(_), + ReducedTy::UnorderedFields(to_ty), + ) => { + span_lint_and_then( + cx, + TRANSMUTE_UNDEFINED_REPR, + e.span, + &format!("transmute into `{}` which has an undefined layout", to_ty_orig), + |diag| { + if to_ty_orig.peel_refs() != to_ty { + diag.note(&format!("the contained type `{}` has an undefined layout", to_ty)); + } + }, + ); + return true; + }, + (ReducedTy::Ref(from_sub_ty), ReducedTy::Ref(to_sub_ty)) => { + from_ty = from_sub_ty; + to_ty = to_sub_ty; + continue; + }, + ( + ReducedTy::OrderedFields(_) | ReducedTy::Ref(_) | ReducedTy::Other(_) | ReducedTy::Param, + ReducedTy::OrderedFields(_) | ReducedTy::Ref(_) | ReducedTy::Other(_) | ReducedTy::Param, + ) + | ( + ReducedTy::UnorderedFields(_) | ReducedTy::Param, + ReducedTy::UnorderedFields(_) | ReducedTy::Param, + ) => break, + }, + } + } + + false +} + +enum ReducedTys<'tcx> { + FromFatPtr { unsized_ty: Ty<'tcx>, to_ty: Ty<'tcx> }, + ToFatPtr { unsized_ty: Ty<'tcx>, from_ty: Ty<'tcx> }, + ToPtr { from_ty: Ty<'tcx>, to_ty: Ty<'tcx> }, + FromPtr { from_ty: Ty<'tcx>, to_ty: Ty<'tcx> }, + Other { from_ty: Ty<'tcx>, to_ty: Ty<'tcx> }, +} + +/// Remove references so long as both types are references. +fn reduce_refs<'tcx>( + cx: &LateContext<'tcx>, + span: Span, + mut from_ty: Ty<'tcx>, + mut to_ty: Ty<'tcx>, +) -> ReducedTys<'tcx> { + loop { + return match (from_ty.kind(), to_ty.kind()) { + ( + &(ty::Ref(_, from_sub_ty, _) | ty::RawPtr(TypeAndMut { ty: from_sub_ty, .. })), + &(ty::Ref(_, to_sub_ty, _) | ty::RawPtr(TypeAndMut { ty: to_sub_ty, .. })), + ) => { + from_ty = from_sub_ty; + to_ty = to_sub_ty; + continue; + }, + (&(ty::Ref(_, unsized_ty, _) | ty::RawPtr(TypeAndMut { ty: unsized_ty, .. })), _) + if !unsized_ty.is_sized(cx.tcx.at(span), cx.param_env) => + { + ReducedTys::FromFatPtr { unsized_ty, to_ty } + }, + (_, &(ty::Ref(_, unsized_ty, _) | ty::RawPtr(TypeAndMut { ty: unsized_ty, .. }))) + if !unsized_ty.is_sized(cx.tcx.at(span), cx.param_env) => + { + ReducedTys::ToFatPtr { unsized_ty, from_ty } + }, + (&(ty::Ref(_, from_ty, _) | ty::RawPtr(TypeAndMut { ty: from_ty, .. })), _) => { + ReducedTys::FromPtr { from_ty, to_ty } + }, + (_, &(ty::Ref(_, to_ty, _) | ty::RawPtr(TypeAndMut { ty: to_ty, .. }))) => { + ReducedTys::ToPtr { from_ty, to_ty } + }, + _ => ReducedTys::Other { from_ty, to_ty }, + }; + } +} + +enum ReducedTy<'tcx> { + /// The type can be used for type erasure. + TypeErasure, + /// The type is a struct containing either zero non-zero sized fields, or multiple non-zero + /// sized fields with a defined order. + OrderedFields(Ty<'tcx>), + /// The type is a struct containing multiple non-zero sized fields with no defined order. + UnorderedFields(Ty<'tcx>), + /// The type is a reference to the contained type. + Ref(Ty<'tcx>), + /// The type is a generic parameter. + Param, + /// Any other type. + Other(Ty<'tcx>), +} + +/// Reduce structs containing a single non-zero sized field to it's contained type. +fn reduce_ty<'tcx>(cx: &LateContext<'tcx>, mut ty: Ty<'tcx>) -> ReducedTy<'tcx> { + loop { + ty = cx.tcx.try_normalize_erasing_regions(cx.param_env, ty).unwrap_or(ty); + return match *ty.kind() { + ty::Array(sub_ty, _) if matches!(sub_ty.kind(), ty::Int(_) | ty::Uint(_)) => ReducedTy::TypeErasure, + ty::Array(sub_ty, _) | ty::Slice(sub_ty) => { + ty = sub_ty; + continue; + }, + ty::Tuple(args) if args.is_empty() => ReducedTy::TypeErasure, + ty::Tuple(args) => { + let mut iter = args.iter(); + let Some(sized_ty) = iter.find(|&ty| !is_zero_sized_ty(cx, ty)) else { + return ReducedTy::OrderedFields(ty); + }; + if iter.all(|ty| is_zero_sized_ty(cx, ty)) { + ty = sized_ty; + continue; + } + ReducedTy::UnorderedFields(ty) + }, + ty::Adt(def, substs) if def.is_struct() => { + let mut iter = def + .non_enum_variant() + .fields + .iter() + .map(|f| cx.tcx.bound_type_of(f.did).subst(cx.tcx, substs)); + let Some(sized_ty) = iter.find(|&ty| !is_zero_sized_ty(cx, ty)) else { + return ReducedTy::TypeErasure; + }; + if iter.all(|ty| is_zero_sized_ty(cx, ty)) { + ty = sized_ty; + continue; + } + if def.repr().inhibit_struct_field_reordering_opt() { + ReducedTy::OrderedFields(ty) + } else { + ReducedTy::UnorderedFields(ty) + } + }, + ty::Adt(def, _) if def.is_enum() && (def.variants().is_empty() || is_c_void(cx, ty)) => { + ReducedTy::TypeErasure + }, + // TODO: Check if the conversion to or from at least one of a union's fields is valid. + ty::Adt(def, _) if def.is_union() => ReducedTy::TypeErasure, + ty::Foreign(_) => ReducedTy::TypeErasure, + ty::Ref(_, ty, _) => ReducedTy::Ref(ty), + ty::RawPtr(ty) => ReducedTy::Ref(ty.ty), + ty::Param(_) => ReducedTy::Param, + _ => ReducedTy::Other(ty), + }; + } +} + +fn is_zero_sized_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + if_chain! { + if let Ok(ty) = cx.tcx.try_normalize_erasing_regions(cx.param_env, ty); + if let Ok(layout) = cx.tcx.layout_of(cx.param_env.and(ty)); + then { + layout.layout.size().bytes() == 0 + } else { + false + } + } +} + +fn is_size_pair(ty: Ty<'_>) -> bool { + if let ty::Tuple(tys) = *ty.kind() + && let [ty1, ty2] = &**tys + { + matches!(ty1.kind(), ty::Int(IntTy::Isize) | ty::Uint(UintTy::Usize)) + && matches!(ty2.kind(), ty::Int(IntTy::Isize) | ty::Uint(UintTy::Usize)) + } else { + false + } +} + +fn same_except_params<'tcx>(subs1: SubstsRef<'tcx>, subs2: SubstsRef<'tcx>) -> bool { + // TODO: check const parameters as well. Currently this will consider `Array<5>` the same as + // `Array<6>` + for (ty1, ty2) in subs1.types().zip(subs2.types()).filter(|(ty1, ty2)| ty1 != ty2) { + match (ty1.kind(), ty2.kind()) { + (ty::Param(_), _) | (_, ty::Param(_)) => (), + (ty::Adt(adt1, subs1), ty::Adt(adt2, subs2)) if adt1 == adt2 && same_except_params(subs1, subs2) => (), + _ => return false, + } + } + true +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/transmutes_expressible_as_ptr_casts.rs b/src/tools/clippy/clippy_lints/src/transmute/transmutes_expressible_as_ptr_casts.rs new file mode 100644 index 000000000..626d7cd46 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/transmutes_expressible_as_ptr_casts.rs @@ -0,0 +1,39 @@ +use super::utils::can_be_expressed_as_pointer_cast; +use super::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::sugg; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::Ty; + +/// Checks for `transmutes_expressible_as_ptr_casts` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, +) -> bool { + if can_be_expressed_as_pointer_cast(cx, e, from_ty, to_ty) { + span_lint_and_then( + cx, + TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS, + e.span, + &format!( + "transmute from `{}` to `{}` which could be expressed as a pointer cast instead", + from_ty, to_ty + ), + |diag| { + if let Some(arg) = sugg::Sugg::hir_opt(cx, arg) { + let sugg = arg.as_ty(&to_ty.to_string()).to_string(); + diag.span_suggestion(e.span, "try", sugg, Applicability::MachineApplicable); + } + }, + ); + true + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/unsound_collection_transmute.rs b/src/tools/clippy/clippy_lints/src/transmute/unsound_collection_transmute.rs new file mode 100644 index 000000000..831b0d450 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/unsound_collection_transmute.rs @@ -0,0 +1,52 @@ +use super::utils::is_layout_incompatible; +use super::UNSOUND_COLLECTION_TRANSMUTE; +use clippy_utils::diagnostics::span_lint; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::symbol::sym; + +/// Checks for `unsound_collection_transmute` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>, from_ty: Ty<'tcx>, to_ty: Ty<'tcx>) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::Adt(from_adt, from_substs), ty::Adt(to_adt, to_substs)) => { + if from_adt.did() != to_adt.did() { + return false; + } + if !matches!( + cx.tcx.get_diagnostic_name(to_adt.did()), + Some( + sym::BTreeMap + | sym::BTreeSet + | sym::BinaryHeap + | sym::HashMap + | sym::HashSet + | sym::Vec + | sym::VecDeque + ) + ) { + return false; + } + if from_substs + .types() + .zip(to_substs.types()) + .any(|(from_ty, to_ty)| is_layout_incompatible(cx, from_ty, to_ty)) + { + span_lint( + cx, + UNSOUND_COLLECTION_TRANSMUTE, + e.span, + &format!( + "transmute from `{}` to `{}` with mismatched layout is unsound", + from_ty, to_ty + ), + ); + true + } else { + false + } + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/useless_transmute.rs b/src/tools/clippy/clippy_lints/src/transmute/useless_transmute.rs new file mode 100644 index 000000000..8122cd716 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/useless_transmute.rs @@ -0,0 +1,72 @@ +use super::USELESS_TRANSMUTE; +use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; +use clippy_utils::sugg; +use rustc_errors::Applicability; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty, TypeVisitable}; + +/// Checks for `useless_transmute` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, + arg: &'tcx Expr<'_>, +) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + _ if from_ty == to_ty && !from_ty.has_erased_regions() => { + span_lint( + cx, + USELESS_TRANSMUTE, + e.span, + &format!("transmute from a type (`{}`) to itself", from_ty), + ); + true + }, + (ty::Ref(_, rty, rty_mutbl), ty::RawPtr(ptr_ty)) => { + // No way to give the correct suggestion here. Avoid linting for now. + if !rty.has_erased_regions() { + span_lint_and_then( + cx, + USELESS_TRANSMUTE, + e.span, + "transmute from a reference to a pointer", + |diag| { + if let Some(arg) = sugg::Sugg::hir_opt(cx, arg) { + let rty_and_mut = ty::TypeAndMut { + ty: *rty, + mutbl: *rty_mutbl, + }; + + let sugg = if *ptr_ty == rty_and_mut { + arg.as_ty(to_ty) + } else { + arg.as_ty(cx.tcx.mk_ptr(rty_and_mut)).as_ty(to_ty) + }; + + diag.span_suggestion(e.span, "try", sugg, Applicability::Unspecified); + } + }, + ); + } + true + }, + (ty::Int(_) | ty::Uint(_), ty::RawPtr(_)) => { + span_lint_and_then( + cx, + USELESS_TRANSMUTE, + e.span, + "transmute from an integer to a pointer", + |diag| { + if let Some(arg) = sugg::Sugg::hir_opt(cx, arg) { + diag.span_suggestion(e.span, "try", arg.as_ty(&to_ty.to_string()), Applicability::Unspecified); + } + }, + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/utils.rs b/src/tools/clippy/clippy_lints/src/transmute/utils.rs new file mode 100644 index 000000000..74927570b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/utils.rs @@ -0,0 +1,76 @@ +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{cast::CastKind, Ty}; +use rustc_span::DUMMY_SP; +use rustc_typeck::check::{cast::CastCheck, FnCtxt, Inherited}; + +// check if the component types of the transmuted collection and the result have different ABI, +// size or alignment +pub(super) fn is_layout_incompatible<'tcx>(cx: &LateContext<'tcx>, from: Ty<'tcx>, to: Ty<'tcx>) -> bool { + if let Ok(from) = cx.tcx.try_normalize_erasing_regions(cx.param_env, from) + && let Ok(to) = cx.tcx.try_normalize_erasing_regions(cx.param_env, to) + && let Ok(from_layout) = cx.tcx.layout_of(cx.param_env.and(from)) + && let Ok(to_layout) = cx.tcx.layout_of(cx.param_env.and(to)) + { + from_layout.size != to_layout.size || from_layout.align.abi != to_layout.align.abi + } else { + // no idea about layout, so don't lint + false + } +} + +/// Check if the type conversion can be expressed as a pointer cast, instead of +/// a transmute. In certain cases, including some invalid casts from array +/// references to pointers, this may cause additional errors to be emitted and/or +/// ICE error messages. This function will panic if that occurs. +pub(super) fn can_be_expressed_as_pointer_cast<'tcx>( + cx: &LateContext<'tcx>, + e: &'tcx Expr<'_>, + from_ty: Ty<'tcx>, + to_ty: Ty<'tcx>, +) -> bool { + use CastKind::{AddrPtrCast, ArrayPtrCast, FnPtrAddrCast, FnPtrPtrCast, PtrAddrCast, PtrPtrCast}; + matches!( + check_cast(cx, e, from_ty, to_ty), + Some(PtrPtrCast | PtrAddrCast | AddrPtrCast | ArrayPtrCast | FnPtrPtrCast | FnPtrAddrCast) + ) +} + +/// If a cast from `from_ty` to `to_ty` is valid, returns an Ok containing the kind of +/// the cast. In certain cases, including some invalid casts from array references +/// to pointers, this may cause additional errors to be emitted and/or ICE error +/// messages. This function will panic if that occurs. +fn check_cast<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>, from_ty: Ty<'tcx>, to_ty: Ty<'tcx>) -> Option<CastKind> { + let hir_id = e.hir_id; + let local_def_id = hir_id.owner; + + Inherited::build(cx.tcx, local_def_id).enter(|inherited| { + let fn_ctxt = FnCtxt::new(&inherited, cx.param_env, hir_id); + + // If we already have errors, we can't be sure we can pointer cast. + assert!( + !fn_ctxt.errors_reported_since_creation(), + "Newly created FnCtxt contained errors" + ); + + if let Ok(check) = CastCheck::new( + &fn_ctxt, e, from_ty, to_ty, + // We won't show any error to the user, so we don't care what the span is here. + DUMMY_SP, DUMMY_SP, + ) { + let res = check.do_check(&fn_ctxt); + + // do_check's documentation says that it might return Ok and create + // errors in the fcx instead of returning Err in some cases. Those cases + // should be filtered out before getting here. + assert!( + !fn_ctxt.errors_reported_since_creation(), + "`fn_ctxt` contained errors after cast check!" + ); + + res.ok() + } else { + None + } + }) +} diff --git a/src/tools/clippy/clippy_lints/src/transmute/wrong_transmute.rs b/src/tools/clippy/clippy_lints/src/transmute/wrong_transmute.rs new file mode 100644 index 000000000..2118f3d69 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmute/wrong_transmute.rs @@ -0,0 +1,22 @@ +use super::WRONG_TRANSMUTE; +use clippy_utils::diagnostics::span_lint; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +/// Checks for `wrong_transmute` lint. +/// Returns `true` if it's triggered, otherwise returns `false`. +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>, from_ty: Ty<'tcx>, to_ty: Ty<'tcx>) -> bool { + match (&from_ty.kind(), &to_ty.kind()) { + (ty::Float(_) | ty::Char, ty::Ref(..) | ty::RawPtr(_)) => { + span_lint( + cx, + WRONG_TRANSMUTE, + e.span, + &format!("transmute from a `{}` to a pointer", from_ty), + ); + true + }, + _ => false, + } +} diff --git a/src/tools/clippy/clippy_lints/src/transmuting_null.rs b/src/tools/clippy/clippy_lints/src/transmuting_null.rs new file mode 100644 index 000000000..7939dfedc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/transmuting_null.rs @@ -0,0 +1,89 @@ +use clippy_utils::consts::{constant_context, Constant}; +use clippy_utils::diagnostics::span_lint; +use clippy_utils::is_expr_diagnostic_item; +use if_chain::if_chain; +use rustc_ast::LitKind; +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}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for transmute calls which would receive a null pointer. + /// + /// ### Why is this bad? + /// Transmuting a null pointer is undefined behavior. + /// + /// ### Known problems + /// Not all cases can be detected at the moment of this writing. + /// For example, variables which hold a null pointer and are then fed to a `transmute` + /// call, aren't detectable yet. + /// + /// ### Example + /// ```rust + /// let null_ref: &u64 = unsafe { std::mem::transmute(0 as *const u64) }; + /// ``` + #[clippy::version = "1.35.0"] + pub TRANSMUTING_NULL, + correctness, + "transmutes from a null pointer to a reference, which is undefined behavior" +} + +declare_lint_pass!(TransmutingNull => [TRANSMUTING_NULL]); + +const LINT_MSG: &str = "transmuting a known null pointer into a reference"; + +impl<'tcx> LateLintPass<'tcx> for TransmutingNull { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if in_external_macro(cx.sess(), expr.span) { + return; + } + + if_chain! { + if let ExprKind::Call(func, [arg]) = expr.kind; + if is_expr_diagnostic_item(cx, func, sym::transmute); + + then { + // Catching transmute over constants that resolve to `null`. + let mut const_eval_context = constant_context(cx, cx.typeck_results()); + if_chain! { + if let ExprKind::Path(ref _qpath) = arg.kind; + if let Some(Constant::RawPtr(x)) = const_eval_context.expr(arg); + if x == 0; + then { + span_lint(cx, TRANSMUTING_NULL, expr.span, LINT_MSG) + } + } + + // Catching: + // `std::mem::transmute(0 as *const i32)` + if_chain! { + if let ExprKind::Cast(inner_expr, _cast_ty) = arg.kind; + if let ExprKind::Lit(ref lit) = inner_expr.kind; + if let LitKind::Int(0, _) = lit.node; + then { + span_lint(cx, TRANSMUTING_NULL, expr.span, LINT_MSG) + } + } + + // Catching: + // `std::mem::transmute(std::ptr::null::<i32>())` + if_chain! { + if let ExprKind::Call(func1, []) = arg.kind; + if is_expr_diagnostic_item(cx, func1, sym::ptr_null); + then { + span_lint(cx, TRANSMUTING_NULL, expr.span, LINT_MSG) + } + } + + // FIXME: + // Also catch transmutations of variables which are known nulls. + // To do this, MIR const propagation seems to be the better tool. + // Whenever MIR const prop routines are more developed, this will + // become available. As of this writing (25/03/19) it is not yet. + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/types/borrowed_box.rs b/src/tools/clippy/clippy_lints/src/types/borrowed_box.rs new file mode 100644 index 000000000..94945b2e1 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/borrowed_box.rs @@ -0,0 +1,115 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, GenericArg, GenericBounds, GenericParamKind}; +use rustc_hir::{HirId, Lifetime, MutTy, Mutability, Node, QPath, TyKind}; +use rustc_lint::LateContext; +use rustc_span::sym; + +use super::BORROWED_BOX; + +pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, lt: &Lifetime, mut_ty: &MutTy<'_>) -> bool { + match mut_ty.ty.kind { + TyKind::Path(ref qpath) => { + let hir_id = mut_ty.ty.hir_id; + let def = cx.qpath_res(qpath, hir_id); + if_chain! { + if let Some(def_id) = def.opt_def_id(); + if Some(def_id) == cx.tcx.lang_items().owned_box(); + if let QPath::Resolved(None, path) = *qpath; + if let [ref bx] = *path.segments; + if let Some(params) = bx.args; + if !params.parenthesized; + if let Some(inner) = params.args.iter().find_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }); + then { + if is_any_trait(cx, inner) { + // Ignore `Box<Any>` types; see issue #1884 for details. + return false; + } + + let ltopt = if lt.name.is_anonymous() { + String::new() + } else { + format!("{} ", lt.name.ident().as_str()) + }; + + if mut_ty.mutbl == Mutability::Mut { + // Ignore `&mut Box<T>` types; see issue #2907 for + // details. + return false; + } + + // When trait objects or opaque types have lifetime or auto-trait bounds, + // we need to add parentheses to avoid a syntax error due to its ambiguity. + // Originally reported as the issue #3128. + let inner_snippet = snippet(cx, inner.span, ".."); + let suggestion = match &inner.kind { + TyKind::TraitObject(bounds, lt_bound, _) if bounds.len() > 1 || !lt_bound.is_elided() => { + format!("&{}({})", ltopt, &inner_snippet) + }, + TyKind::Path(qpath) + if get_bounds_if_impl_trait(cx, qpath, inner.hir_id) + .map_or(false, |bounds| bounds.len() > 1) => + { + format!("&{}({})", ltopt, &inner_snippet) + }, + _ => format!("&{}{}", ltopt, &inner_snippet), + }; + span_lint_and_sugg( + cx, + BORROWED_BOX, + hir_ty.span, + "you seem to be trying to use `&Box<T>`. Consider using just `&T`", + "try", + suggestion, + // To make this `MachineApplicable`, at least one needs to check if it isn't a trait item + // because the trait impls of it will break otherwise; + // and there may be other cases that result in invalid code. + // For example, type coercion doesn't work nicely. + Applicability::Unspecified, + ); + return true; + } + }; + false + }, + _ => false, + } +} + +// Returns true if given type is `Any` trait. +fn is_any_trait(cx: &LateContext<'_>, t: &hir::Ty<'_>) -> bool { + if_chain! { + if let TyKind::TraitObject(traits, ..) = t.kind; + if !traits.is_empty(); + if let Some(trait_did) = traits[0].trait_ref.trait_def_id(); + // Only Send/Sync can be used as additional traits, so it is enough to + // check only the first trait. + if cx.tcx.is_diagnostic_item(sym::Any, trait_did); + then { + return true; + } + } + + false +} + +fn get_bounds_if_impl_trait<'tcx>(cx: &LateContext<'tcx>, qpath: &QPath<'_>, id: HirId) -> Option<GenericBounds<'tcx>> { + if_chain! { + if let Some(did) = cx.qpath_res(qpath, id).opt_def_id(); + if let Some(Node::GenericParam(generic_param)) = cx.tcx.hir().get_if_local(did); + if let GenericParamKind::Type { synthetic, .. } = generic_param.kind; + if synthetic; + if let Some(generics) = cx.tcx.hir().get_generics(id.owner); + if let Some(pred) = generics.bounds_for_param(did.expect_local()).next(); + then { + Some(pred.bounds) + } else { + None + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/types/box_collection.rs b/src/tools/clippy/clippy_lints/src/types/box_collection.rs new file mode 100644 index 000000000..ba51404d2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/box_collection.rs @@ -0,0 +1,54 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{path_def_id, qpath_generic_tys}; +use rustc_hir::{self as hir, def_id::DefId, QPath}; +use rustc_lint::LateContext; +use rustc_span::{sym, Symbol}; + +use super::BOX_COLLECTION; + +pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, qpath: &QPath<'_>, def_id: DefId) -> bool { + if_chain! { + if Some(def_id) == cx.tcx.lang_items().owned_box(); + if let Some(item_type) = get_std_collection(cx, qpath); + then { + let generic = match item_type { + sym::String => "", + _ => "<..>", + }; + + let box_content = format!("{outer}{generic}", outer = item_type); + span_lint_and_help( + cx, + BOX_COLLECTION, + hir_ty.span, + &format!( + "you seem to be trying to use `Box<{box_content}>`. Consider using just `{box_content}`"), + None, + &format!( + "`{box_content}` is already on the heap, `Box<{box_content}>` makes an extra allocation") + ); + true + } else { + false + } + } +} + +fn get_std_collection(cx: &LateContext<'_>, qpath: &QPath<'_>) -> Option<Symbol> { + let param = qpath_generic_tys(qpath).next()?; + let id = path_def_id(cx, param)?; + cx.tcx.get_diagnostic_name(id).filter(|&name| { + matches!( + name, + sym::HashMap + | sym::String + | sym::Vec + | sym::HashSet + | sym::VecDeque + | sym::LinkedList + | sym::BTreeMap + | sym::BTreeSet + | sym::BinaryHeap + ) + }) +} diff --git a/src/tools/clippy/clippy_lints/src/types/linked_list.rs b/src/tools/clippy/clippy_lints/src/types/linked_list.rs new file mode 100644 index 000000000..5fb708741 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/linked_list.rs @@ -0,0 +1,22 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{self as hir, def_id::DefId}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +use super::LINKEDLIST; + +pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, def_id: DefId) -> bool { + if cx.tcx.is_diagnostic_item(sym::LinkedList, def_id) { + span_lint_and_help( + cx, + LINKEDLIST, + hir_ty.span, + "you seem to be using a `LinkedList`! Perhaps you meant some other data structure?", + None, + "a `VecDeque` might work", + ); + true + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/types/mod.rs b/src/tools/clippy/clippy_lints/src/types/mod.rs new file mode 100644 index 000000000..353a6f6b8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/mod.rs @@ -0,0 +1,574 @@ +mod borrowed_box; +mod box_collection; +mod linked_list; +mod option_option; +mod rc_buffer; +mod rc_mutex; +mod redundant_allocation; +mod type_complexity; +mod utils; +mod vec_box; + +use rustc_hir as hir; +use rustc_hir::intravisit::FnKind; +use rustc_hir::{ + Body, FnDecl, FnRetTy, GenericArg, HirId, ImplItem, ImplItemKind, Item, ItemKind, Local, MutTy, QPath, TraitItem, + TraitItemKind, TyKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `Box<T>` where T is a collection such as Vec anywhere in the code. + /// Check the [Box documentation](https://doc.rust-lang.org/std/boxed/index.html) for more information. + /// + /// ### Why is this bad? + /// Collections already keeps their contents in a separate area on + /// the heap. So if you `Box` them, you just add another level of indirection + /// without any benefit whatsoever. + /// + /// ### Example + /// ```rust,ignore + /// struct X { + /// values: Box<Vec<Foo>>, + /// } + /// ``` + /// + /// Better: + /// + /// ```rust,ignore + /// struct X { + /// values: Vec<Foo>, + /// } + /// ``` + #[clippy::version = "1.57.0"] + pub BOX_COLLECTION, + perf, + "usage of `Box<Vec<T>>`, vector elements are already on the heap" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `Vec<Box<T>>` where T: Sized anywhere in the code. + /// Check the [Box documentation](https://doc.rust-lang.org/std/boxed/index.html) for more information. + /// + /// ### Why is this bad? + /// `Vec` already keeps its contents in a separate area on + /// the heap. So if you `Box` its contents, you just add another level of indirection. + /// + /// ### Known problems + /// Vec<Box<T: Sized>> makes sense if T is a large type (see [#3530](https://github.com/rust-lang/rust-clippy/issues/3530), + /// 1st comment). + /// + /// ### Example + /// ```rust + /// struct X { + /// values: Vec<Box<i32>>, + /// } + /// ``` + /// + /// Better: + /// + /// ```rust + /// struct X { + /// values: Vec<i32>, + /// } + /// ``` + #[clippy::version = "1.33.0"] + pub VEC_BOX, + complexity, + "usage of `Vec<Box<T>>` where T: Sized, vector elements are already on the heap" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `Option<Option<_>>` in function signatures and type + /// definitions + /// + /// ### Why is this bad? + /// `Option<_>` represents an optional value. `Option<Option<_>>` + /// represents an optional optional value which is logically the same thing as an optional + /// value but has an unneeded extra level of wrapping. + /// + /// If you have a case where `Some(Some(_))`, `Some(None)` and `None` are distinct cases, + /// consider a custom `enum` instead, with clear names for each case. + /// + /// ### Example + /// ```rust + /// fn get_data() -> Option<Option<u32>> { + /// None + /// } + /// ``` + /// + /// Better: + /// + /// ```rust + /// pub enum Contents { + /// Data(Vec<u8>), // Was Some(Some(Vec<u8>)) + /// NotYetFetched, // Was Some(None) + /// None, // Was None + /// } + /// + /// fn get_data() -> Contents { + /// Contents::None + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub OPTION_OPTION, + pedantic, + "usage of `Option<Option<T>>`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of any `LinkedList`, suggesting to use a + /// `Vec` or a `VecDeque` (formerly called `RingBuf`). + /// + /// ### Why is this bad? + /// Gankro says: + /// + /// > The TL;DR of `LinkedList` is that it's built on a massive amount of + /// pointers and indirection. + /// > It wastes memory, it has terrible cache locality, and is all-around slow. + /// `RingBuf`, while + /// > "only" amortized for push/pop, should be faster in the general case for + /// almost every possible + /// > workload, and isn't even amortized at all if you can predict the capacity + /// you need. + /// > + /// > `LinkedList`s are only really good if you're doing a lot of merging or + /// splitting of lists. + /// > This is because they can just mangle some pointers instead of actually + /// copying the data. Even + /// > if you're doing a lot of insertion in the middle of the list, `RingBuf` + /// can still be better + /// > because of how expensive it is to seek to the middle of a `LinkedList`. + /// + /// ### Known problems + /// False positives – the instances where using a + /// `LinkedList` makes sense are few and far between, but they can still happen. + /// + /// ### Example + /// ```rust + /// # use std::collections::LinkedList; + /// let x: LinkedList<usize> = LinkedList::new(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LINKEDLIST, + pedantic, + "usage of LinkedList, usually a vector is faster, or a more specialized data structure like a `VecDeque`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `&Box<T>` anywhere in the code. + /// Check the [Box documentation](https://doc.rust-lang.org/std/boxed/index.html) for more information. + /// + /// ### Why is this bad? + /// A `&Box<T>` parameter requires the function caller to box `T` first before passing it to a function. + /// Using `&T` defines a concrete type for the parameter and generalizes the function, this would also + /// auto-deref to `&T` at the function call site if passed a `&Box<T>`. + /// + /// ### Example + /// ```rust,ignore + /// fn foo(bar: &Box<T>) { ... } + /// ``` + /// + /// Better: + /// + /// ```rust,ignore + /// fn foo(bar: &T) { ... } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub BORROWED_BOX, + complexity, + "a borrow of a boxed type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of redundant allocations anywhere in the code. + /// + /// ### Why is this bad? + /// Expressions such as `Rc<&T>`, `Rc<Rc<T>>`, `Rc<Arc<T>>`, `Rc<Box<T>>`, `Arc<&T>`, `Arc<Rc<T>>`, + /// `Arc<Arc<T>>`, `Arc<Box<T>>`, `Box<&T>`, `Box<Rc<T>>`, `Box<Arc<T>>`, `Box<Box<T>>`, add an unnecessary level of indirection. + /// + /// ### Example + /// ```rust + /// # use std::rc::Rc; + /// fn foo(bar: Rc<&usize>) {} + /// ``` + /// + /// Better: + /// + /// ```rust + /// fn foo(bar: &usize) {} + /// ``` + #[clippy::version = "1.44.0"] + pub REDUNDANT_ALLOCATION, + perf, + "redundant allocation" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `Rc<T>` and `Arc<T>` when `T` is a mutable buffer type such as `String` or `Vec`. + /// + /// ### Why is this bad? + /// Expressions such as `Rc<String>` usually have no advantage over `Rc<str>`, since + /// it is larger and involves an extra level of indirection, and doesn't implement `Borrow<str>`. + /// + /// While mutating a buffer type would still be possible with `Rc::get_mut()`, it only + /// works if there are no additional references yet, which usually defeats the purpose of + /// enclosing it in a shared ownership type. Instead, additionally wrapping the inner + /// type with an interior mutable container (such as `RefCell` or `Mutex`) would normally + /// be used. + /// + /// ### Known problems + /// This pattern can be desirable to avoid the overhead of a `RefCell` or `Mutex` for + /// cases where mutation only happens before there are any additional references. + /// + /// ### Example + /// ```rust,ignore + /// # use std::rc::Rc; + /// fn foo(interned: Rc<String>) { ... } + /// ``` + /// + /// Better: + /// + /// ```rust,ignore + /// fn foo(interned: Rc<str>) { ... } + /// ``` + #[clippy::version = "1.48.0"] + pub RC_BUFFER, + restriction, + "shared ownership of a buffer type" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for types used in structs, parameters and `let` + /// declarations above a certain complexity threshold. + /// + /// ### Why is this bad? + /// Too complex types make the code less readable. Consider + /// using a `type` definition to simplify them. + /// + /// ### Example + /// ```rust + /// # use std::rc::Rc; + /// struct Foo { + /// inner: Rc<Vec<Vec<Box<(u32, u32, u32, u32)>>>>, + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub TYPE_COMPLEXITY, + complexity, + "usage of very complex types that might be better factored into `type` definitions" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `Rc<Mutex<T>>`. + /// + /// ### Why is this bad? + /// `Rc` is used in single thread and `Mutex` is used in multi thread. + /// Consider using `Rc<RefCell<T>>` in single thread or `Arc<Mutex<T>>` in multi thread. + /// + /// ### Known problems + /// Sometimes combining generic types can lead to the requirement that a + /// type use Rc in conjunction with Mutex. We must consider those cases false positives, but + /// alas they are quite hard to rule out. Luckily they are also rare. + /// + /// ### Example + /// ```rust,ignore + /// use std::rc::Rc; + /// use std::sync::Mutex; + /// fn foo(interned: Rc<Mutex<i32>>) { ... } + /// ``` + /// + /// Better: + /// + /// ```rust,ignore + /// use std::rc::Rc; + /// use std::cell::RefCell + /// fn foo(interned: Rc<RefCell<i32>>) { ... } + /// ``` + #[clippy::version = "1.55.0"] + pub RC_MUTEX, + restriction, + "usage of `Rc<Mutex<T>>`" +} + +pub struct Types { + vec_box_size_threshold: u64, + type_complexity_threshold: u64, + avoid_breaking_exported_api: bool, +} + +impl_lint_pass!(Types => [BOX_COLLECTION, VEC_BOX, OPTION_OPTION, LINKEDLIST, BORROWED_BOX, REDUNDANT_ALLOCATION, RC_BUFFER, RC_MUTEX, TYPE_COMPLEXITY]); + +impl<'tcx> LateLintPass<'tcx> for Types { + fn check_fn(&mut self, cx: &LateContext<'_>, _: FnKind<'_>, decl: &FnDecl<'_>, _: &Body<'_>, _: Span, id: HirId) { + let is_in_trait_impl = + if let Some(hir::Node::Item(item)) = cx.tcx.hir().find_by_def_id(cx.tcx.hir().get_parent_item(id)) { + matches!(item.kind, ItemKind::Impl(hir::Impl { of_trait: Some(_), .. })) + } else { + false + }; + + let is_exported = cx.access_levels.is_exported(cx.tcx.hir().local_def_id(id)); + + self.check_fn_decl( + cx, + decl, + CheckTyContext { + is_in_trait_impl, + is_exported, + ..CheckTyContext::default() + }, + ); + } + + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + let is_exported = cx.access_levels.is_exported(item.def_id); + + match item.kind { + ItemKind::Static(ty, _, _) | ItemKind::Const(ty, _) => self.check_ty( + cx, + ty, + CheckTyContext { + is_exported, + ..CheckTyContext::default() + }, + ), + // functions, enums, structs, impls and traits are covered + _ => (), + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) { + match item.kind { + ImplItemKind::Const(ty, _) => { + let is_in_trait_impl = if let Some(hir::Node::Item(item)) = + cx.tcx.hir().find_by_def_id(cx.tcx.hir().get_parent_item(item.hir_id())) + { + matches!(item.kind, ItemKind::Impl(hir::Impl { of_trait: Some(_), .. })) + } else { + false + }; + + self.check_ty( + cx, + ty, + CheckTyContext { + is_in_trait_impl, + ..CheckTyContext::default() + }, + ); + }, + // Methods are covered by check_fn. + // Type aliases are ignored because oftentimes it's impossible to + // make type alias declaration in trait simpler, see #1013 + ImplItemKind::Fn(..) | ImplItemKind::TyAlias(..) => (), + } + } + + fn check_field_def(&mut self, cx: &LateContext<'_>, field: &hir::FieldDef<'_>) { + let is_exported = cx.access_levels.is_exported(cx.tcx.hir().local_def_id(field.hir_id)); + + self.check_ty( + cx, + field.ty, + CheckTyContext { + is_exported, + ..CheckTyContext::default() + }, + ); + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &TraitItem<'_>) { + let is_exported = cx.access_levels.is_exported(item.def_id); + + let context = CheckTyContext { + is_exported, + ..CheckTyContext::default() + }; + + match item.kind { + TraitItemKind::Const(ty, _) | TraitItemKind::Type(_, Some(ty)) => { + self.check_ty(cx, ty, context); + }, + TraitItemKind::Fn(ref sig, _) => self.check_fn_decl(cx, sig.decl, context), + TraitItemKind::Type(..) => (), + } + } + + fn check_local(&mut self, cx: &LateContext<'_>, local: &Local<'_>) { + if let Some(ty) = local.ty { + self.check_ty( + cx, + ty, + CheckTyContext { + is_local: true, + ..CheckTyContext::default() + }, + ); + } + } +} + +impl Types { + pub fn new(vec_box_size_threshold: u64, type_complexity_threshold: u64, avoid_breaking_exported_api: bool) -> Self { + Self { + vec_box_size_threshold, + type_complexity_threshold, + avoid_breaking_exported_api, + } + } + + fn check_fn_decl(&mut self, cx: &LateContext<'_>, decl: &FnDecl<'_>, context: CheckTyContext) { + // Ignore functions in trait implementations as they are usually forced by the trait definition. + // + // FIXME: ideally we would like to warn *if the complicated type can be simplified*, but it's hard + // to check. + if context.is_in_trait_impl { + return; + } + + for input in decl.inputs { + self.check_ty(cx, input, context); + } + + if let FnRetTy::Return(ty) = decl.output { + self.check_ty(cx, ty, context); + } + } + + /// Recursively check for `TypePass` lints in the given type. Stop at the first + /// lint found. + /// + /// The parameter `is_local` distinguishes the context of the type. + fn check_ty(&mut self, cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, mut context: CheckTyContext) { + if hir_ty.span.from_expansion() { + return; + } + + // Skip trait implementations; see issue #605. + if context.is_in_trait_impl { + return; + } + + if !context.is_nested_call && type_complexity::check(cx, hir_ty, self.type_complexity_threshold) { + return; + } + + match hir_ty.kind { + TyKind::Path(ref qpath) if !context.is_local => { + let hir_id = hir_ty.hir_id; + let res = cx.qpath_res(qpath, hir_id); + if let Some(def_id) = res.opt_def_id() { + if self.is_type_change_allowed(context) { + // All lints that are being checked in this block are guarded by + // the `avoid_breaking_exported_api` configuration. When adding a + // new lint, please also add the name to the configuration documentation + // in `clippy_lints::utils::conf.rs` + + let mut triggered = false; + triggered |= box_collection::check(cx, hir_ty, qpath, def_id); + triggered |= redundant_allocation::check(cx, hir_ty, qpath, def_id); + triggered |= rc_buffer::check(cx, hir_ty, qpath, def_id); + triggered |= vec_box::check(cx, hir_ty, qpath, def_id, self.vec_box_size_threshold); + triggered |= option_option::check(cx, hir_ty, qpath, def_id); + triggered |= linked_list::check(cx, hir_ty, def_id); + triggered |= rc_mutex::check(cx, hir_ty, qpath, def_id); + + if triggered { + return; + } + } + } + match *qpath { + QPath::Resolved(Some(ty), p) => { + context.is_nested_call = true; + self.check_ty(cx, ty, context); + for ty in p.segments.iter().flat_map(|seg| { + seg.args + .as_ref() + .map_or_else(|| [].iter(), |params| params.args.iter()) + .filter_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }) + }) { + self.check_ty(cx, ty, context); + } + }, + QPath::Resolved(None, p) => { + context.is_nested_call = true; + for ty in p.segments.iter().flat_map(|seg| { + seg.args + .as_ref() + .map_or_else(|| [].iter(), |params| params.args.iter()) + .filter_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }) + }) { + self.check_ty(cx, ty, context); + } + }, + QPath::TypeRelative(ty, seg) => { + context.is_nested_call = true; + self.check_ty(cx, ty, context); + if let Some(params) = seg.args { + for ty in params.args.iter().filter_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }) { + self.check_ty(cx, ty, context); + } + } + }, + QPath::LangItem(..) => {}, + } + }, + TyKind::Rptr(ref lt, ref mut_ty) => { + context.is_nested_call = true; + if !borrowed_box::check(cx, hir_ty, lt, mut_ty) { + self.check_ty(cx, mut_ty.ty, context); + } + }, + TyKind::Slice(ty) | TyKind::Array(ty, _) | TyKind::Ptr(MutTy { ty, .. }) => { + context.is_nested_call = true; + self.check_ty(cx, ty, context); + }, + TyKind::Tup(tys) => { + context.is_nested_call = true; + for ty in tys { + self.check_ty(cx, ty, context); + } + }, + _ => {}, + } + } + + /// This function checks if the type is allowed to change in the current context + /// based on the `avoid_breaking_exported_api` configuration + fn is_type_change_allowed(&self, context: CheckTyContext) -> bool { + !(context.is_exported && self.avoid_breaking_exported_api) + } +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone, Copy, Default)] +struct CheckTyContext { + is_in_trait_impl: bool, + /// `true` for types on local variables. + is_local: bool, + /// `true` for types that are part of the public API. + is_exported: bool, + is_nested_call: bool, +} diff --git a/src/tools/clippy/clippy_lints/src/types/option_option.rs b/src/tools/clippy/clippy_lints/src/types/option_option.rs new file mode 100644 index 000000000..8767e3c30 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/option_option.rs @@ -0,0 +1,28 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::{path_def_id, qpath_generic_tys}; +use if_chain::if_chain; +use rustc_hir::{self as hir, def_id::DefId, QPath}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +use super::OPTION_OPTION; + +pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, qpath: &QPath<'_>, def_id: DefId) -> bool { + if_chain! { + if cx.tcx.is_diagnostic_item(sym::Option, def_id); + if let Some(arg) = qpath_generic_tys(qpath).next(); + if path_def_id(cx, arg) == Some(def_id); + then { + span_lint( + cx, + OPTION_OPTION, + hir_ty.span, + "consider using `Option<T>` instead of `Option<Option<T>>` or a custom \ + enum if you need to distinguish all 3 cases", + ); + true + } else { + false + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/types/rc_buffer.rs b/src/tools/clippy/clippy_lints/src/types/rc_buffer.rs new file mode 100644 index 000000000..4d72a29e8 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/rc_buffer.rs @@ -0,0 +1,106 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{path_def_id, qpath_generic_tys}; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, def_id::DefId, QPath, TyKind}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +use super::RC_BUFFER; + +pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, qpath: &QPath<'_>, def_id: DefId) -> bool { + if cx.tcx.is_diagnostic_item(sym::Rc, def_id) { + if let Some(alternate) = match_buffer_type(cx, qpath) { + span_lint_and_sugg( + cx, + RC_BUFFER, + hir_ty.span, + "usage of `Rc<T>` when T is a buffer type", + "try", + format!("Rc<{}>", alternate), + Applicability::MachineApplicable, + ); + } else { + let Some(ty) = qpath_generic_tys(qpath).next() else { return false }; + let Some(id) = path_def_id(cx, ty) else { return false }; + if !cx.tcx.is_diagnostic_item(sym::Vec, id) { + return false; + } + let qpath = match &ty.kind { + TyKind::Path(qpath) => qpath, + _ => return false, + }; + let inner_span = match qpath_generic_tys(qpath).next() { + Some(ty) => ty.span, + None => return false, + }; + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + RC_BUFFER, + hir_ty.span, + "usage of `Rc<T>` when T is a buffer type", + "try", + format!( + "Rc<[{}]>", + snippet_with_applicability(cx, inner_span, "..", &mut applicability) + ), + Applicability::MachineApplicable, + ); + return true; + } + } else if cx.tcx.is_diagnostic_item(sym::Arc, def_id) { + if let Some(alternate) = match_buffer_type(cx, qpath) { + span_lint_and_sugg( + cx, + RC_BUFFER, + hir_ty.span, + "usage of `Arc<T>` when T is a buffer type", + "try", + format!("Arc<{}>", alternate), + Applicability::MachineApplicable, + ); + } else if let Some(ty) = qpath_generic_tys(qpath).next() { + let Some(id) = path_def_id(cx, ty) else { return false }; + if !cx.tcx.is_diagnostic_item(sym::Vec, id) { + return false; + } + let qpath = match &ty.kind { + TyKind::Path(qpath) => qpath, + _ => return false, + }; + let inner_span = match qpath_generic_tys(qpath).next() { + Some(ty) => ty.span, + None => return false, + }; + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + RC_BUFFER, + hir_ty.span, + "usage of `Arc<T>` when T is a buffer type", + "try", + format!( + "Arc<[{}]>", + snippet_with_applicability(cx, inner_span, "..", &mut applicability) + ), + Applicability::MachineApplicable, + ); + return true; + } + } + + false +} + +fn match_buffer_type(cx: &LateContext<'_>, qpath: &QPath<'_>) -> Option<&'static str> { + let ty = qpath_generic_tys(qpath).next()?; + let id = path_def_id(cx, ty)?; + let path = match cx.tcx.get_diagnostic_name(id)? { + sym::String => "str", + sym::OsString => "std::ffi::OsStr", + sym::PathBuf => "std::path::Path", + _ => return None, + }; + Some(path) +} diff --git a/src/tools/clippy/clippy_lints/src/types/rc_mutex.rs b/src/tools/clippy/clippy_lints/src/types/rc_mutex.rs new file mode 100644 index 000000000..a75972cf3 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/rc_mutex.rs @@ -0,0 +1,30 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{path_def_id, qpath_generic_tys}; +use if_chain::if_chain; +use rustc_hir::{self as hir, def_id::DefId, QPath}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; + +use super::RC_MUTEX; + +pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, qpath: &QPath<'_>, def_id: DefId) -> bool { + if_chain! { + if cx.tcx.is_diagnostic_item(sym::Rc, def_id) ; + if let Some(arg) = qpath_generic_tys(qpath).next(); + if let Some(id) = path_def_id(cx, arg); + if cx.tcx.is_diagnostic_item(sym::Mutex, id); + then { + span_lint_and_help( + cx, + RC_MUTEX, + hir_ty.span, + "usage of `Rc<Mutex<_>>`", + None, + "consider using `Rc<RefCell<_>>` or `Arc<Mutex<_>>` instead", + ); + return true; + } + } + + false +} diff --git a/src/tools/clippy/clippy_lints/src/types/redundant_allocation.rs b/src/tools/clippy/clippy_lints/src/types/redundant_allocation.rs new file mode 100644 index 000000000..a1312fcda --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/redundant_allocation.rs @@ -0,0 +1,115 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::{snippet, snippet_with_applicability}; +use clippy_utils::{path_def_id, qpath_generic_tys}; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, def_id::DefId, QPath, TyKind}; +use rustc_lint::LateContext; +use rustc_span::symbol::sym; +use rustc_typeck::hir_ty_to_ty; + +use super::{utils, REDUNDANT_ALLOCATION}; + +pub(super) fn check(cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>, qpath: &QPath<'_>, def_id: DefId) -> bool { + let outer_sym = if Some(def_id) == cx.tcx.lang_items().owned_box() { + "Box" + } else if cx.tcx.is_diagnostic_item(sym::Rc, def_id) { + "Rc" + } else if cx.tcx.is_diagnostic_item(sym::Arc, def_id) { + "Arc" + } else { + return false; + }; + + if let Some(span) = utils::match_borrows_parameter(cx, qpath) { + let mut applicability = Applicability::MaybeIncorrect; + let generic_snippet = snippet_with_applicability(cx, span, "..", &mut applicability); + span_lint_and_then( + cx, + REDUNDANT_ALLOCATION, + hir_ty.span, + &format!("usage of `{}<{}>`", outer_sym, generic_snippet), + |diag| { + diag.span_suggestion(hir_ty.span, "try", format!("{}", generic_snippet), applicability); + diag.note(&format!( + "`{generic}` is already a pointer, `{outer}<{generic}>` allocates a pointer on the heap", + outer = outer_sym, + generic = generic_snippet + )); + }, + ); + return true; + } + + let Some(ty) = qpath_generic_tys(qpath).next() else { return false }; + let Some(id) = path_def_id(cx, ty) else { return false }; + let (inner_sym, ty) = match cx.tcx.get_diagnostic_name(id) { + Some(sym::Arc) => ("Arc", ty), + Some(sym::Rc) => ("Rc", ty), + _ if Some(id) == cx.tcx.lang_items().owned_box() => ("Box", ty), + _ => return false, + }; + + let inner_qpath = match &ty.kind { + TyKind::Path(inner_qpath) => inner_qpath, + _ => return false, + }; + let inner_span = match qpath_generic_tys(inner_qpath).next() { + Some(ty) => { + // Reallocation of a fat pointer causes it to become thin. `hir_ty_to_ty` is safe to use + // here because `mod.rs` guarantees this lint is only run on types outside of bodies and + // is not run on locals. + if !hir_ty_to_ty(cx.tcx, ty).is_sized(cx.tcx.at(ty.span), cx.param_env) { + return false; + } + ty.span + }, + None => return false, + }; + if inner_sym == outer_sym { + let mut applicability = Applicability::MaybeIncorrect; + let generic_snippet = snippet_with_applicability(cx, inner_span, "..", &mut applicability); + span_lint_and_then( + cx, + REDUNDANT_ALLOCATION, + hir_ty.span, + &format!("usage of `{}<{}<{}>>`", outer_sym, inner_sym, generic_snippet), + |diag| { + diag.span_suggestion( + hir_ty.span, + "try", + format!("{}<{}>", outer_sym, generic_snippet), + applicability, + ); + diag.note(&format!( + "`{inner}<{generic}>` is already on the heap, `{outer}<{inner}<{generic}>>` makes an extra allocation", + outer = outer_sym, + inner = inner_sym, + generic = generic_snippet + )); + }, + ); + } else { + let generic_snippet = snippet(cx, inner_span, ".."); + span_lint_and_then( + cx, + REDUNDANT_ALLOCATION, + hir_ty.span, + &format!("usage of `{}<{}<{}>>`", outer_sym, inner_sym, generic_snippet), + |diag| { + diag.note(&format!( + "`{inner}<{generic}>` is already on the heap, `{outer}<{inner}<{generic}>>` makes an extra allocation", + outer = outer_sym, + inner = inner_sym, + generic = generic_snippet + )); + diag.help(&format!( + "consider using just `{outer}<{generic}>` or `{inner}<{generic}>`", + outer = outer_sym, + inner = inner_sym, + generic = generic_snippet + )); + }, + ); + } + true +} diff --git a/src/tools/clippy/clippy_lints/src/types/type_complexity.rs b/src/tools/clippy/clippy_lints/src/types/type_complexity.rs new file mode 100644 index 000000000..5ca4023aa --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/type_complexity.rs @@ -0,0 +1,78 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_hir as hir; +use rustc_hir::intravisit::{walk_inf, walk_ty, Visitor}; +use rustc_hir::{GenericParamKind, TyKind}; +use rustc_lint::LateContext; +use rustc_target::spec::abi::Abi; + +use super::TYPE_COMPLEXITY; + +pub(super) fn check(cx: &LateContext<'_>, ty: &hir::Ty<'_>, type_complexity_threshold: u64) -> bool { + let score = { + let mut visitor = TypeComplexityVisitor { score: 0, nest: 1 }; + visitor.visit_ty(ty); + visitor.score + }; + + if score > type_complexity_threshold { + span_lint( + cx, + TYPE_COMPLEXITY, + ty.span, + "very complex type used. Consider factoring parts into `type` definitions", + ); + true + } else { + false + } +} + +/// Walks a type and assigns a complexity score to it. +struct TypeComplexityVisitor { + /// total complexity score of the type + score: u64, + /// current nesting level + nest: u64, +} + +impl<'tcx> Visitor<'tcx> for TypeComplexityVisitor { + fn visit_infer(&mut self, inf: &'tcx hir::InferArg) { + self.score += 1; + walk_inf(self, inf); + } + + fn visit_ty(&mut self, ty: &'tcx hir::Ty<'_>) { + let (add_score, sub_nest) = match ty.kind { + // _, &x and *x have only small overhead; don't mess with nesting level + TyKind::Infer | TyKind::Ptr(..) | TyKind::Rptr(..) => (1, 0), + + // the "normal" components of a type: named types, arrays/tuples + TyKind::Path(..) | TyKind::Slice(..) | TyKind::Tup(..) | TyKind::Array(..) => (10 * self.nest, 1), + + // function types bring a lot of overhead + TyKind::BareFn(bare) if bare.abi == Abi::Rust => (50 * self.nest, 1), + + TyKind::TraitObject(param_bounds, _, _) => { + let has_lifetime_parameters = param_bounds.iter().any(|bound| { + bound + .bound_generic_params + .iter() + .any(|gen| matches!(gen.kind, GenericParamKind::Lifetime { .. })) + }); + if has_lifetime_parameters { + // complex trait bounds like A<'a, 'b> + (50 * self.nest, 1) + } else { + // simple trait bounds like A + B + (20 * self.nest, 0) + } + }, + + _ => (0, 0), + }; + self.score += add_score; + self.nest += sub_nest; + walk_ty(self, ty); + self.nest -= sub_nest; + } +} diff --git a/src/tools/clippy/clippy_lints/src/types/utils.rs b/src/tools/clippy/clippy_lints/src/types/utils.rs new file mode 100644 index 000000000..0fa75f8f0 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/utils.rs @@ -0,0 +1,22 @@ +use clippy_utils::last_path_segment; +use if_chain::if_chain; +use rustc_hir::{GenericArg, QPath, TyKind}; +use rustc_lint::LateContext; +use rustc_span::source_map::Span; + +pub(super) fn match_borrows_parameter(_cx: &LateContext<'_>, qpath: &QPath<'_>) -> Option<Span> { + let last = last_path_segment(qpath); + if_chain! { + if let Some(params) = last.args; + if !params.parenthesized; + if let Some(ty) = params.args.iter().find_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }); + if let TyKind::Rptr(..) = ty.kind; + then { + return Some(ty.span); + } + } + None +} diff --git a/src/tools/clippy/clippy_lints/src/types/vec_box.rs b/src/tools/clippy/clippy_lints/src/types/vec_box.rs new file mode 100644 index 000000000..b2f536ca7 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/types/vec_box.rs @@ -0,0 +1,64 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::last_path_segment; +use clippy_utils::source::snippet; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, def_id::DefId, GenericArg, QPath, TyKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::layout::LayoutOf; +use rustc_middle::ty::TypeVisitable; +use rustc_span::symbol::sym; +use rustc_typeck::hir_ty_to_ty; + +use super::VEC_BOX; + +pub(super) fn check( + cx: &LateContext<'_>, + hir_ty: &hir::Ty<'_>, + qpath: &QPath<'_>, + def_id: DefId, + box_size_threshold: u64, +) -> bool { + if cx.tcx.is_diagnostic_item(sym::Vec, def_id) { + if_chain! { + // Get the _ part of Vec<_> + if let Some(last) = last_path_segment(qpath).args; + if let Some(ty) = last.args.iter().find_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }); + // ty is now _ at this point + if let TyKind::Path(ref ty_qpath) = ty.kind; + let res = cx.qpath_res(ty_qpath, ty.hir_id); + if let Some(def_id) = res.opt_def_id(); + if Some(def_id) == cx.tcx.lang_items().owned_box(); + // At this point, we know ty is Box<T>, now get T + if let Some(last) = last_path_segment(ty_qpath).args; + if let Some(boxed_ty) = last.args.iter().find_map(|arg| match arg { + GenericArg::Type(ty) => Some(ty), + _ => None, + }); + let ty_ty = hir_ty_to_ty(cx.tcx, boxed_ty); + if !ty_ty.has_escaping_bound_vars(); + if ty_ty.is_sized(cx.tcx.at(ty.span), cx.param_env); + if let Ok(ty_ty_size) = cx.layout_of(ty_ty).map(|l| l.size.bytes()); + if ty_ty_size <= box_size_threshold; + then { + span_lint_and_sugg( + cx, + VEC_BOX, + hir_ty.span, + "`Vec<T>` is already on the heap, the boxing is unnecessary", + "try", + format!("Vec<{}>", snippet(cx, boxed_ty.span, "..")), + Applicability::MachineApplicable, + ); + true + } else { + false + } + } + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs b/src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs new file mode 100644 index 000000000..d2e675a78 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/undocumented_unsafe_blocks.rs @@ -0,0 +1,358 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::source::walk_span_to_context; +use clippy_utils::{get_parent_node, is_lint_allowed}; +use rustc_data_structures::sync::Lrc; +use rustc_hir as hir; +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_span::{BytePos, Pos, Span, SyntaxContext}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `unsafe` blocks and impls without a `// SAFETY: ` comment + /// explaining why the unsafe operations performed inside + /// the block are safe. + /// + /// Note the comment must appear on the line(s) preceding the unsafe block + /// with nothing appearing in between. The following is ok: + /// ```ignore + /// foo( + /// // SAFETY: + /// // This is a valid safety comment + /// unsafe { *x } + /// ) + /// ``` + /// But neither of these are: + /// ```ignore + /// // SAFETY: + /// // This is not a valid safety comment + /// foo( + /// /* SAFETY: Neither is this */ unsafe { *x }, + /// ); + /// ``` + /// + /// ### Why is this bad? + /// Undocumented unsafe blocks and impls can make it difficult to + /// read and maintain code, as well as uncover unsoundness + /// and bugs. + /// + /// ### Example + /// ```rust + /// use std::ptr::NonNull; + /// let a = &mut 42; + /// + /// let ptr = unsafe { NonNull::new_unchecked(a) }; + /// ``` + /// Use instead: + /// ```rust + /// use std::ptr::NonNull; + /// let a = &mut 42; + /// + /// // SAFETY: references are guaranteed to be non-null. + /// let ptr = unsafe { NonNull::new_unchecked(a) }; + /// ``` + #[clippy::version = "1.58.0"] + pub UNDOCUMENTED_UNSAFE_BLOCKS, + restriction, + "creating an unsafe block without explaining why it is safe" +} + +declare_lint_pass!(UndocumentedUnsafeBlocks => [UNDOCUMENTED_UNSAFE_BLOCKS]); + +impl LateLintPass<'_> for UndocumentedUnsafeBlocks { + fn check_block(&mut self, cx: &LateContext<'_>, block: &'_ Block<'_>) { + if block.rules == BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) + && !in_external_macro(cx.tcx.sess, block.span) + && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, block.hir_id) + && !is_unsafe_from_proc_macro(cx, block.span) + && !block_has_safety_comment(cx, block) + { + let source_map = cx.tcx.sess.source_map(); + let span = if source_map.is_multiline(block.span) { + source_map.span_until_char(block.span, '\n') + } else { + block.span + }; + + span_lint_and_help( + cx, + UNDOCUMENTED_UNSAFE_BLOCKS, + span, + "unsafe block missing a safety comment", + None, + "consider adding a safety comment on the preceding line", + ); + } + } + + fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) { + if let hir::ItemKind::Impl(imple) = item.kind + && imple.unsafety == hir::Unsafety::Unsafe + && !in_external_macro(cx.tcx.sess, item.span) + && !is_lint_allowed(cx, UNDOCUMENTED_UNSAFE_BLOCKS, item.hir_id()) + && !is_unsafe_from_proc_macro(cx, item.span) + && !item_has_safety_comment(cx, item) + { + let source_map = cx.tcx.sess.source_map(); + let span = if source_map.is_multiline(item.span) { + source_map.span_until_char(item.span, '\n') + } else { + item.span + }; + + span_lint_and_help( + cx, + UNDOCUMENTED_UNSAFE_BLOCKS, + span, + "unsafe impl missing a safety comment", + None, + "consider adding a safety comment on the preceding line", + ); + } + } +} + +fn is_unsafe_from_proc_macro(cx: &LateContext<'_>, span: Span) -> bool { + let source_map = cx.sess().source_map(); + let file_pos = source_map.lookup_byte_offset(span.lo()); + file_pos + .sf + .src + .as_deref() + .and_then(|src| src.get(file_pos.pos.to_usize()..)) + .map_or(true, |src| !src.starts_with("unsafe")) +} + +/// Checks if the lines immediately preceding the block contain a safety comment. +fn block_has_safety_comment(cx: &LateContext<'_>, block: &hir::Block<'_>) -> bool { + // This intentionally ignores text before the start of a function so something like: + // ``` + // // SAFETY: reason + // fn foo() { unsafe { .. } } + // ``` + // won't work. This is to avoid dealing with where such a comment should be place relative to + // attributes and doc comments. + + span_from_macro_expansion_has_safety_comment(cx, block.span) || span_in_body_has_safety_comment(cx, block.span) +} + +/// Checks if the lines immediately preceding the item contain a safety comment. +#[allow(clippy::collapsible_match)] +fn item_has_safety_comment(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool { + if span_from_macro_expansion_has_safety_comment(cx, item.span) { + return true; + } + + if item.span.ctxt() == SyntaxContext::root() { + if let Some(parent_node) = get_parent_node(cx.tcx, item.hir_id()) { + let comment_start = match parent_node { + Node::Crate(parent_mod) => { + comment_start_before_impl_in_mod(cx, parent_mod, parent_mod.spans.inner_span, item) + }, + Node::Item(parent_item) => { + if let ItemKind::Mod(parent_mod) = &parent_item.kind { + comment_start_before_impl_in_mod(cx, parent_mod, parent_item.span, item) + } else { + // Doesn't support impls in this position. Pretend a comment was found. + return true; + } + }, + Node::Stmt(stmt) => { + if let Some(stmt_parent) = get_parent_node(cx.tcx, stmt.hir_id) { + match stmt_parent { + Node::Block(block) => walk_span_to_context(block.span, SyntaxContext::root()).map(Span::lo), + _ => { + // Doesn't support impls in this position. Pretend a comment was found. + return true; + }, + } + } else { + // Problem getting the parent node. Pretend a comment was found. + return true; + } + }, + _ => { + // Doesn't support impls in this position. Pretend a comment was found. + return true; + }, + }; + + let source_map = cx.sess().source_map(); + if let Some(comment_start) = comment_start + && let Ok(unsafe_line) = source_map.lookup_line(item.span.lo()) + && let Ok(comment_start_line) = source_map.lookup_line(comment_start) + && Lrc::ptr_eq(&unsafe_line.sf, &comment_start_line.sf) + && let Some(src) = unsafe_line.sf.src.as_deref() + { + unsafe_line.sf.lines(|lines| { + comment_start_line.line < unsafe_line.line && text_has_safety_comment( + src, + &lines[comment_start_line.line + 1..=unsafe_line.line], + unsafe_line.sf.start_pos.to_usize(), + ) + }) + } else { + // Problem getting source text. Pretend a comment was found. + true + } + } else { + // No parent node. Pretend a comment was found. + true + } + } else { + false + } +} + +fn comment_start_before_impl_in_mod( + cx: &LateContext<'_>, + parent_mod: &hir::Mod<'_>, + parent_mod_span: Span, + imple: &hir::Item<'_>, +) -> Option<BytePos> { + parent_mod.item_ids.iter().enumerate().find_map(|(idx, item_id)| { + if *item_id == imple.item_id() { + if idx == 0 { + // mod A { /* comment */ unsafe impl T {} ... } + // ^------------------------------------------^ returns the start of this span + // ^---------------------^ finally checks comments in this range + if let Some(sp) = walk_span_to_context(parent_mod_span, SyntaxContext::root()) { + return Some(sp.lo()); + } + } else { + // some_item /* comment */ unsafe impl T {} + // ^-------^ returns the end of this span + // ^---------------^ finally checks comments in this range + let prev_item = cx.tcx.hir().item(parent_mod.item_ids[idx - 1]); + if let Some(sp) = walk_span_to_context(prev_item.span, SyntaxContext::root()) { + return Some(sp.hi()); + } + } + } + None + }) +} + +fn span_from_macro_expansion_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool { + let source_map = cx.sess().source_map(); + let ctxt = span.ctxt(); + if ctxt == SyntaxContext::root() { + false + } else { + // From a macro expansion. Get the text from the start of the macro declaration to start of the + // unsafe block. + // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; } + // ^--------------------------------------------^ + if let Ok(unsafe_line) = source_map.lookup_line(span.lo()) + && let Ok(macro_line) = source_map.lookup_line(ctxt.outer_expn_data().def_site.lo()) + && Lrc::ptr_eq(&unsafe_line.sf, ¯o_line.sf) + && let Some(src) = unsafe_line.sf.src.as_deref() + { + unsafe_line.sf.lines(|lines| { + macro_line.line < unsafe_line.line && text_has_safety_comment( + src, + &lines[macro_line.line + 1..=unsafe_line.line], + unsafe_line.sf.start_pos.to_usize(), + ) + }) + } else { + // Problem getting source text. Pretend a comment was found. + true + } + } +} + +fn get_body_search_span(cx: &LateContext<'_>) -> Option<Span> { + let body = cx.enclosing_body?; + let map = cx.tcx.hir(); + let mut span = map.body(body).value.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(_) => (), + _ => break, + } + } + Some(span) +} + +fn span_in_body_has_safety_comment(cx: &LateContext<'_>, span: Span) -> bool { + let source_map = cx.sess().source_map(); + let ctxt = span.ctxt(); + if ctxt == SyntaxContext::root() + && let Some(search_span) = get_body_search_span(cx) + { + if let Ok(unsafe_line) = source_map.lookup_line(span.lo()) + && let Some(body_span) = walk_span_to_context(search_span, SyntaxContext::root()) + && let Ok(body_line) = source_map.lookup_line(body_span.lo()) + && Lrc::ptr_eq(&unsafe_line.sf, &body_line.sf) + && let Some(src) = unsafe_line.sf.src.as_deref() + { + // Get the text from the start of function body to the unsafe block. + // fn foo() { some_stuff; unsafe { stuff }; other_stuff; } + // ^-------------^ + unsafe_line.sf.lines(|lines| { + body_line.line < unsafe_line.line && text_has_safety_comment( + src, + &lines[body_line.line + 1..=unsafe_line.line], + unsafe_line.sf.start_pos.to_usize(), + ) + }) + } else { + // Problem getting source text. Pretend a comment was found. + true + } + } else { + false + } +} + +/// Checks if the given text has a safety comment for the immediately proceeding line. +fn text_has_safety_comment(src: &str, line_starts: &[BytePos], offset: usize) -> bool { + let mut lines = line_starts + .array_windows::<2>() + .rev() + .map_while(|[start, end]| { + let start = start.to_usize() - offset; + let end = end.to_usize() - offset; + src.get(start..end).map(|text| (start, text.trim_start())) + }) + .filter(|(_, text)| !text.is_empty()); + + let Some((line_start, line)) = lines.next() else { + return false; + }; + // Check for a sequence of line comments. + if line.starts_with("//") { + let mut line = line; + loop { + if line.to_ascii_uppercase().contains("SAFETY:") { + return true; + } + match lines.next() { + Some((_, x)) if x.starts_with("//") => line = x, + _ => return false, + } + } + } + // No line comments; look for the start of a block comment. + // This will only find them if they are at the start of a line. + let (mut line_start, mut line) = (line_start, line); + loop { + if line.starts_with("/*") { + let src = src[line_start..line_starts.last().unwrap().to_usize() - offset].trim_start(); + let mut tokens = tokenize(src); + return src[..tokens.next().unwrap().len as usize] + .to_ascii_uppercase() + .contains("SAFETY:") + && tokens.all(|t| t.kind == TokenKind::Whitespace); + } + match lines.next() { + Some(x) => (line_start, line) = x, + None => return false, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unicode.rs b/src/tools/clippy/clippy_lints/src/unicode.rs new file mode 100644 index 000000000..cc64d17be --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unicode.rs @@ -0,0 +1,142 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_lint_allowed; +use clippy_utils::source::snippet; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, HirId}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use unicode_normalization::UnicodeNormalization; + +declare_clippy_lint! { + /// ### What it does + /// Checks for invisible Unicode characters in the code. + /// + /// ### Why is this bad? + /// Having an invisible character in the code makes for all + /// sorts of April fools, but otherwise is very much frowned upon. + /// + /// ### Example + /// You don't see it, but there may be a zero-width space or soft hyphen + /// somewhere in this text. + #[clippy::version = "1.49.0"] + pub INVISIBLE_CHARACTERS, + correctness, + "using an invisible character in a string literal, which is confusing" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for non-ASCII characters in string and char literals. + /// + /// ### Why is this bad? + /// Yeah, we know, the 90's called and wanted their charset + /// back. Even so, there still are editors and other programs out there that + /// don't work well with Unicode. So if the code is meant to be used + /// internationally, on multiple operating systems, or has other portability + /// requirements, activating this lint could be useful. + /// + /// ### Example + /// ```rust + /// let x = String::from("€"); + /// ``` + /// + /// Use instead: + /// ```rust + /// let x = String::from("\u{20ac}"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub NON_ASCII_LITERAL, + restriction, + "using any literal non-ASCII chars in a string literal instead of using the `\\u` escape" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for string literals that contain Unicode in a form + /// that is not equal to its + /// [NFC-recomposition](http://www.unicode.org/reports/tr15/#Norm_Forms). + /// + /// ### Why is this bad? + /// If such a string is compared to another, the results + /// may be surprising. + /// + /// ### Example + /// You may not see it, but "à"" and "à"" aren't the same string. The + /// former when escaped is actually `"a\u{300}"` while the latter is `"\u{e0}"`. + #[clippy::version = "pre 1.29.0"] + pub UNICODE_NOT_NFC, + pedantic, + "using a Unicode literal not in NFC normal form (see [Unicode tr15](http://www.unicode.org/reports/tr15/) for further information)" +} + +declare_lint_pass!(Unicode => [INVISIBLE_CHARACTERS, NON_ASCII_LITERAL, UNICODE_NOT_NFC]); + +impl LateLintPass<'_> for Unicode { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) { + if let ExprKind::Lit(ref lit) = expr.kind { + if let LitKind::Str(_, _) | LitKind::Char(_) = lit.node { + check_str(cx, lit.span, expr.hir_id); + } + } + } +} + +fn escape<T: Iterator<Item = char>>(s: T) -> String { + let mut result = String::new(); + for c in s { + if c as u32 > 0x7F { + for d in c.escape_unicode() { + result.push(d); + } + } else { + result.push(c); + } + } + result +} + +fn check_str(cx: &LateContext<'_>, span: Span, id: HirId) { + let string = snippet(cx, span, ""); + if string.chars().any(|c| ['\u{200B}', '\u{ad}', '\u{2060}'].contains(&c)) { + span_lint_and_sugg( + cx, + INVISIBLE_CHARACTERS, + span, + "invisible character detected", + "consider replacing the string with", + string + .replace('\u{200B}', "\\u{200B}") + .replace('\u{ad}', "\\u{AD}") + .replace('\u{2060}', "\\u{2060}"), + Applicability::MachineApplicable, + ); + } + if string.chars().any(|c| c as u32 > 0x7F) { + span_lint_and_sugg( + cx, + NON_ASCII_LITERAL, + span, + "literal non-ASCII character detected", + "consider replacing the string with", + if is_lint_allowed(cx, UNICODE_NOT_NFC, id) { + escape(string.chars()) + } else { + escape(string.nfc()) + }, + Applicability::MachineApplicable, + ); + } + if is_lint_allowed(cx, NON_ASCII_LITERAL, id) && string.chars().zip(string.nfc()).any(|(a, b)| a != b) { + span_lint_and_sugg( + cx, + UNICODE_NOT_NFC, + span, + "non-NFC Unicode sequence detected", + "consider replacing the string with", + string.nfc().collect::<String>(), + Applicability::MachineApplicable, + ); + } +} diff --git a/src/tools/clippy/clippy_lints/src/uninit_vec.rs b/src/tools/clippy/clippy_lints/src/uninit_vec.rs new file mode 100644 index 000000000..9f4c5555f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/uninit_vec.rs @@ -0,0 +1,224 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; +use clippy_utils::higher::{get_vec_init_kind, VecInitKind}; +use clippy_utils::ty::{is_type_diagnostic_item, is_uninit_value_valid_for_ty}; +use clippy_utils::{is_lint_allowed, path_to_local_id, peel_hir_expr_while, SpanlessEq}; +use rustc_hir::{Block, Expr, ExprKind, HirId, PatKind, PathSegment, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span}; + +// TODO: add `ReadBuf` (RFC 2930) in "How to fix" once it is available in std +declare_clippy_lint! { + /// ### What it does + /// Checks for `set_len()` call that creates `Vec` with uninitialized elements. + /// This is commonly caused by calling `set_len()` right after allocating or + /// reserving a buffer with `new()`, `default()`, `with_capacity()`, or `reserve()`. + /// + /// ### Why is this bad? + /// It creates a `Vec` with uninitialized data, which leads to + /// undefined behavior with most safe operations. Notably, uninitialized + /// `Vec<u8>` must not be used with generic `Read`. + /// + /// Moreover, calling `set_len()` on a `Vec` created with `new()` or `default()` + /// creates out-of-bound values that lead to heap memory corruption when used. + /// + /// ### Known Problems + /// This lint only checks directly adjacent statements. + /// + /// ### Example + /// ```rust,ignore + /// let mut vec: Vec<u8> = Vec::with_capacity(1000); + /// unsafe { vec.set_len(1000); } + /// reader.read(&mut vec); // undefined behavior! + /// ``` + /// + /// ### How to fix? + /// 1. Use an initialized buffer: + /// ```rust,ignore + /// let mut vec: Vec<u8> = vec![0; 1000]; + /// reader.read(&mut vec); + /// ``` + /// 2. Wrap the content in `MaybeUninit`: + /// ```rust,ignore + /// let mut vec: Vec<MaybeUninit<T>> = Vec::with_capacity(1000); + /// vec.set_len(1000); // `MaybeUninit` can be uninitialized + /// ``` + /// 3. If you are on nightly, `Vec::spare_capacity_mut()` is available: + /// ```rust,ignore + /// let mut vec: Vec<u8> = Vec::with_capacity(1000); + /// let remaining = vec.spare_capacity_mut(); // `&mut [MaybeUninit<u8>]` + /// // perform initialization with `remaining` + /// vec.set_len(...); // Safe to call `set_len()` on initialized part + /// ``` + #[clippy::version = "1.58.0"] + pub UNINIT_VEC, + correctness, + "Vec with uninitialized data" +} + +declare_lint_pass!(UninitVec => [UNINIT_VEC]); + +// FIXME: update to a visitor-based implementation. +// Threads: https://github.com/rust-lang/rust-clippy/pull/7682#discussion_r710998368 +impl<'tcx> LateLintPass<'tcx> for UninitVec { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'_>) { + if !in_external_macro(cx.tcx.sess, block.span) { + for w in block.stmts.windows(2) { + if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = w[1].kind { + handle_uninit_vec_pair(cx, &w[0], expr); + } + } + + if let (Some(stmt), Some(expr)) = (block.stmts.last(), block.expr) { + handle_uninit_vec_pair(cx, stmt, expr); + } + } + } +} + +fn handle_uninit_vec_pair<'tcx>( + cx: &LateContext<'tcx>, + maybe_init_or_reserve: &'tcx Stmt<'tcx>, + maybe_set_len: &'tcx Expr<'tcx>, +) { + if_chain! { + if let Some(vec) = extract_init_or_reserve_target(cx, maybe_init_or_reserve); + if let Some((set_len_self, call_span)) = extract_set_len_self(cx, maybe_set_len); + if vec.location.eq_expr(cx, set_len_self); + if let ty::Ref(_, vec_ty, _) = cx.typeck_results().expr_ty_adjusted(set_len_self).kind(); + if let ty::Adt(_, substs) = vec_ty.kind(); + // `#[allow(...)]` attribute can be set on enclosing unsafe block of `set_len()` + if !is_lint_allowed(cx, UNINIT_VEC, maybe_set_len.hir_id); + then { + if vec.has_capacity() { + // with_capacity / reserve -> set_len + + // Check T of Vec<T> + if !is_uninit_value_valid_for_ty(cx, substs.type_at(0)) { + // FIXME: #7698, false positive of the internal lints + #[expect(clippy::collapsible_span_lint_calls)] + span_lint_and_then( + cx, + UNINIT_VEC, + vec![call_span, maybe_init_or_reserve.span], + "calling `set_len()` immediately after reserving a buffer creates uninitialized values", + |diag| { + diag.help("initialize the buffer or wrap the content in `MaybeUninit`"); + }, + ); + } + } else { + // new / default -> set_len + span_lint( + cx, + UNINIT_VEC, + vec![call_span, maybe_init_or_reserve.span], + "calling `set_len()` on empty `Vec` creates out-of-bound values", + ); + } + } + } +} + +/// The target `Vec` that is initialized or reserved +#[derive(Clone, Copy)] +struct TargetVec<'tcx> { + location: VecLocation<'tcx>, + /// `None` if `reserve()` + init_kind: Option<VecInitKind>, +} + +impl TargetVec<'_> { + pub fn has_capacity(self) -> bool { + !matches!(self.init_kind, Some(VecInitKind::New | VecInitKind::Default)) + } +} + +#[derive(Clone, Copy)] +enum VecLocation<'tcx> { + Local(HirId), + Expr(&'tcx Expr<'tcx>), +} + +impl<'tcx> VecLocation<'tcx> { + pub fn eq_expr(self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { + match self { + VecLocation::Local(hir_id) => path_to_local_id(expr, hir_id), + VecLocation::Expr(self_expr) => SpanlessEq::new(cx).eq_expr(self_expr, expr), + } + } +} + +/// Finds the target location where the result of `Vec` initialization is stored +/// or `self` expression for `Vec::reserve()`. +fn extract_init_or_reserve_target<'tcx>(cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) -> Option<TargetVec<'tcx>> { + match stmt.kind { + StmtKind::Local(local) => { + if_chain! { + if let Some(init_expr) = local.init; + if let PatKind::Binding(_, hir_id, _, None) = local.pat.kind; + if let Some(init_kind) = get_vec_init_kind(cx, init_expr); + then { + return Some(TargetVec { + location: VecLocation::Local(hir_id), + init_kind: Some(init_kind), + }) + } + } + }, + StmtKind::Expr(expr) | StmtKind::Semi(expr) => match expr.kind { + ExprKind::Assign(lhs, rhs, _span) => { + if let Some(init_kind) = get_vec_init_kind(cx, rhs) { + return Some(TargetVec { + location: VecLocation::Expr(lhs), + init_kind: Some(init_kind), + }); + } + }, + ExprKind::MethodCall(path, [self_expr, _], _) if is_reserve(cx, path, self_expr) => { + return Some(TargetVec { + location: VecLocation::Expr(self_expr), + init_kind: None, + }); + }, + _ => (), + }, + StmtKind::Item(_) => (), + } + None +} + +fn is_reserve(cx: &LateContext<'_>, path: &PathSegment<'_>, self_expr: &Expr<'_>) -> bool { + is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(self_expr).peel_refs(), sym::Vec) + && path.ident.name.as_str() == "reserve" +} + +/// Returns self if the expression is `Vec::set_len()` +fn extract_set_len_self<'tcx>(cx: &LateContext<'_>, expr: &'tcx Expr<'_>) -> Option<(&'tcx Expr<'tcx>, Span)> { + // peel unsafe blocks in `unsafe { vec.set_len() }` + let expr = peel_hir_expr_while(expr, |e| { + if let ExprKind::Block(block, _) = e.kind { + // Extract the first statement/expression + match (block.stmts.get(0).map(|stmt| &stmt.kind), block.expr) { + (None, Some(expr)) => Some(expr), + (Some(StmtKind::Expr(expr) | StmtKind::Semi(expr)), _) => Some(expr), + _ => None, + } + } else { + None + } + }); + match expr.kind { + ExprKind::MethodCall(path, [self_expr, _], _) => { + let self_type = cx.typeck_results().expr_ty(self_expr).peel_refs(); + if is_type_diagnostic_item(cx, self_type, sym::Vec) && path.ident.name.as_str() == "set_len" { + Some((self_expr, expr.span)) + } else { + None + } + }, + _ => None, + } +} diff --git a/src/tools/clippy/clippy_lints/src/unit_hash.rs b/src/tools/clippy/clippy_lints/src/unit_hash.rs new file mode 100644 index 000000000..88ca0cb20 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unit_hash.rs @@ -0,0 +1,78 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use rustc_errors::Applicability; +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 + /// Detects `().hash(_)`. + /// + /// ### Why is this bad? + /// Hashing a unit value doesn't do anything as the implementation of `Hash` for `()` is a no-op. + /// + /// ### Example + /// ```rust + /// # use std::hash::Hash; + /// # use std::collections::hash_map::DefaultHasher; + /// # enum Foo { Empty, WithValue(u8) } + /// # use Foo::*; + /// # let mut state = DefaultHasher::new(); + /// # let my_enum = Foo::Empty; + /// match my_enum { + /// Empty => ().hash(&mut state), + /// WithValue(x) => x.hash(&mut state), + /// } + /// ``` + /// Use instead: + /// ```rust + /// # use std::hash::Hash; + /// # use std::collections::hash_map::DefaultHasher; + /// # enum Foo { Empty, WithValue(u8) } + /// # use Foo::*; + /// # let mut state = DefaultHasher::new(); + /// # let my_enum = Foo::Empty; + /// match my_enum { + /// Empty => 0_u8.hash(&mut state), + /// WithValue(x) => x.hash(&mut state), + /// } + /// ``` + #[clippy::version = "1.58.0"] + pub UNIT_HASH, + correctness, + "hashing a unit value, which does nothing" +} +declare_lint_pass!(UnitHash => [UNIT_HASH]); + +impl<'tcx> LateLintPass<'tcx> for UnitHash { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if_chain! { + if let ExprKind::MethodCall(name_ident, args, _) = &expr.kind; + if name_ident.ident.name == sym::hash; + if let [recv, state_param] = args; + if cx.typeck_results().expr_ty(recv).is_unit(); + then { + span_lint_and_then( + cx, + UNIT_HASH, + expr.span, + "this call to `hash` on the unit type will do nothing", + |diag| { + diag.span_suggestion( + expr.span, + "remove the call to `hash` or consider using", + format!( + "0_u8.hash({})", + snippet(cx, state_param.span, ".."), + ), + Applicability::MaybeIncorrect, + ); + diag.note("the implementation of `Hash` for `()` is a no-op"); + } + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unit_return_expecting_ord.rs b/src/tools/clippy/clippy_lints/src/unit_return_expecting_ord.rs new file mode 100644 index 000000000..b0fce91ab --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unit_return_expecting_ord.rs @@ -0,0 +1,184 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +use clippy_utils::{get_trait_def_id, paths}; +use if_chain::if_chain; +use rustc_hir::def_id::DefId; +use rustc_hir::{Closure, Expr, ExprKind, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_middle::ty::{GenericPredicates, PredicateKind, ProjectionPredicate, TraitPredicate}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{BytePos, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions that expect closures of type + /// Fn(...) -> Ord where the implemented closure returns the unit type. + /// The lint also suggests to remove the semi-colon at the end of the statement if present. + /// + /// ### Why is this bad? + /// Likely, returning the unit type is unintentional, and + /// could simply be caused by an extra semi-colon. Since () implements Ord + /// it doesn't cause a compilation error. + /// This is the same reasoning behind the unit_cmp lint. + /// + /// ### Known problems + /// If returning unit is intentional, then there is no + /// way of specifying this without triggering needless_return lint + /// + /// ### Example + /// ```rust + /// let mut twins = vec!((1, 1), (2, 2)); + /// twins.sort_by_key(|x| { x.1; }); + /// ``` + #[clippy::version = "1.47.0"] + pub UNIT_RETURN_EXPECTING_ORD, + correctness, + "fn arguments of type Fn(...) -> Ord returning the unit type ()." +} + +declare_lint_pass!(UnitReturnExpectingOrd => [UNIT_RETURN_EXPECTING_ORD]); + +fn get_trait_predicates_for_trait_id<'tcx>( + cx: &LateContext<'tcx>, + generics: GenericPredicates<'tcx>, + trait_id: Option<DefId>, +) -> Vec<TraitPredicate<'tcx>> { + let mut preds = Vec::new(); + for (pred, _) in generics.predicates { + if_chain! { + if let PredicateKind::Trait(poly_trait_pred) = pred.kind().skip_binder(); + let trait_pred = cx.tcx.erase_late_bound_regions(pred.kind().rebind(poly_trait_pred)); + if let Some(trait_def_id) = trait_id; + if trait_def_id == trait_pred.trait_ref.def_id; + then { + preds.push(trait_pred); + } + } + } + preds +} + +fn get_projection_pred<'tcx>( + cx: &LateContext<'tcx>, + generics: GenericPredicates<'tcx>, + trait_pred: TraitPredicate<'tcx>, +) -> Option<ProjectionPredicate<'tcx>> { + generics.predicates.iter().find_map(|(proj_pred, _)| { + if let ty::PredicateKind::Projection(pred) = proj_pred.kind().skip_binder() { + let projection_pred = cx.tcx.erase_late_bound_regions(proj_pred.kind().rebind(pred)); + if projection_pred.projection_ty.substs == trait_pred.trait_ref.substs { + return Some(projection_pred); + } + } + None + }) +} + +fn get_args_to_check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Vec<(usize, String)> { + let mut args_to_check = Vec::new(); + if let Some(def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) { + let fn_sig = cx.tcx.fn_sig(def_id); + let generics = cx.tcx.predicates_of(def_id); + let fn_mut_preds = get_trait_predicates_for_trait_id(cx, generics, cx.tcx.lang_items().fn_mut_trait()); + let ord_preds = get_trait_predicates_for_trait_id(cx, generics, get_trait_def_id(cx, &paths::ORD)); + let partial_ord_preds = + get_trait_predicates_for_trait_id(cx, generics, cx.tcx.lang_items().partial_ord_trait()); + // Trying to call erase_late_bound_regions on fn_sig.inputs() gives the following error + // The trait `rustc::ty::TypeFoldable<'_>` is not implemented for + // `&[rustc_middle::ty::Ty<'_>]` + let inputs_output = cx.tcx.erase_late_bound_regions(fn_sig.inputs_and_output()); + inputs_output + .iter() + .rev() + .skip(1) + .rev() + .enumerate() + .for_each(|(i, inp)| { + for trait_pred in &fn_mut_preds { + if_chain! { + if trait_pred.self_ty() == inp; + if let Some(return_ty_pred) = get_projection_pred(cx, generics, *trait_pred); + then { + if ord_preds.iter().any(|ord| Some(ord.self_ty()) == return_ty_pred.term.ty()) { + args_to_check.push((i, "Ord".to_string())); + } else if partial_ord_preds.iter().any(|pord| { + pord.self_ty() == return_ty_pred.term.ty().unwrap() + }) { + args_to_check.push((i, "PartialOrd".to_string())); + } + } + } + } + }); + } + args_to_check +} + +fn check_arg<'tcx>(cx: &LateContext<'tcx>, arg: &'tcx Expr<'tcx>) -> Option<(Span, Option<Span>)> { + if_chain! { + if let ExprKind::Closure(&Closure { body, fn_decl_span, .. }) = arg.kind; + if let ty::Closure(_def_id, substs) = &cx.typeck_results().node_type(arg.hir_id).kind(); + let ret_ty = substs.as_closure().sig().output(); + let ty = cx.tcx.erase_late_bound_regions(ret_ty); + if ty.is_unit(); + then { + let body = cx.tcx.hir().body(body); + if_chain! { + if let ExprKind::Block(block, _) = body.value.kind; + if block.expr.is_none(); + if let Some(stmt) = block.stmts.last(); + if let StmtKind::Semi(_) = stmt.kind; + then { + let data = stmt.span.data(); + // Make a span out of the semicolon for the help message + Some((fn_decl_span, Some(data.with_lo(data.hi-BytePos(1))))) + } else { + Some((fn_decl_span, None)) + } + } + } else { + None + } + } +} + +impl<'tcx> LateLintPass<'tcx> for UnitReturnExpectingOrd { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if let ExprKind::MethodCall(_, args, _) = expr.kind { + let arg_indices = get_args_to_check(cx, expr); + for (i, trait_name) in arg_indices { + if i < args.len() { + match check_arg(cx, &args[i]) { + Some((span, None)) => { + span_lint( + cx, + UNIT_RETURN_EXPECTING_ORD, + span, + &format!( + "this closure returns \ + the unit type which also implements {}", + trait_name + ), + ); + }, + Some((span, Some(last_semi))) => { + span_lint_and_help( + cx, + UNIT_RETURN_EXPECTING_ORD, + span, + &format!( + "this closure returns \ + the unit type which also implements {}", + trait_name + ), + Some(last_semi), + "probably caused by this trailing semicolon", + ); + }, + None => {}, + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unit_types/let_unit_value.rs b/src/tools/clippy/clippy_lints/src/unit_types/let_unit_value.rs new file mode 100644 index 000000000..aec028d5c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unit_types/let_unit_value.rs @@ -0,0 +1,165 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::get_parent_node; +use clippy_utils::source::snippet_with_macro_callsite; +use clippy_utils::visitors::{for_each_local_assignment, for_each_value_source}; +use core::ops::ControlFlow; +use rustc_errors::Applicability; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{Expr, ExprKind, HirId, HirIdSet, Local, Node, PatKind, QPath, TyKind}; +use rustc_lint::{LateContext, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty; + +use super::LET_UNIT_VALUE; + +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, local: &'tcx Local<'_>) { + if let Some(init) = local.init + && !local.pat.span.from_expansion() + && !in_external_macro(cx.sess(), local.span) + && cx.typeck_results().pat_ty(local.pat).is_unit() + { + if (local.ty.map_or(false, |ty| !matches!(ty.kind, TyKind::Infer)) + || matches!(local.pat.kind, PatKind::Tuple([], None))) + && expr_needs_inferred_result(cx, init) + { + if !matches!(local.pat.kind, PatKind::Wild | PatKind::Tuple([], None)) { + span_lint_and_then( + cx, + LET_UNIT_VALUE, + local.span, + "this let-binding has unit value", + |diag| { + diag.span_suggestion( + local.pat.span, + "use a wild (`_`) binding", + "_", + Applicability::MaybeIncorrect, // snippet + ); + }, + ); + } + } else { + span_lint_and_then( + cx, + LET_UNIT_VALUE, + local.span, + "this let-binding has unit value", + |diag| { + if let Some(expr) = &local.init { + let snip = snippet_with_macro_callsite(cx, expr.span, "()"); + diag.span_suggestion( + local.span, + "omit the `let` binding", + format!("{snip};"), + Applicability::MachineApplicable, // snippet + ); + } + }, + ); + } + } +} + +/// Checks sub-expressions which create the value returned by the given expression for whether +/// return value inference is needed. This checks through locals to see if they also need inference +/// at this point. +/// +/// e.g. +/// ```rust,ignore +/// let bar = foo(); +/// let x: u32 = if true { baz() } else { bar }; +/// ``` +/// Here the sources of the value assigned to `x` would be `baz()`, and `foo()` via the +/// initialization of `bar`. If both `foo` and `baz` have a return type which require type +/// inference then this function would return `true`. +fn expr_needs_inferred_result<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> bool { + // The locals used for initialization which have yet to be checked. + let mut locals_to_check = Vec::new(); + // All the locals which have been added to `locals_to_check`. Needed to prevent cycles. + let mut seen_locals = HirIdSet::default(); + if !each_value_source_needs_inference(cx, e, &mut locals_to_check, &mut seen_locals) { + return false; + } + while let Some(id) = locals_to_check.pop() { + if let Some(Node::Local(l)) = get_parent_node(cx.tcx, id) { + if !l.ty.map_or(true, |ty| matches!(ty.kind, TyKind::Infer)) { + return false; + } + if let Some(e) = l.init { + if !each_value_source_needs_inference(cx, e, &mut locals_to_check, &mut seen_locals) { + return false; + } + } else if for_each_local_assignment(cx, id, |e| { + if each_value_source_needs_inference(cx, e, &mut locals_to_check, &mut seen_locals) { + ControlFlow::Continue(()) + } else { + ControlFlow::Break(()) + } + }) + .is_break() + { + return false; + } + } + } + + true +} + +fn each_value_source_needs_inference( + cx: &LateContext<'_>, + e: &Expr<'_>, + locals_to_check: &mut Vec<HirId>, + seen_locals: &mut HirIdSet, +) -> bool { + for_each_value_source(e, &mut |e| { + if needs_inferred_result_ty(cx, e, locals_to_check, seen_locals) { + ControlFlow::Continue(()) + } else { + ControlFlow::Break(()) + } + }) + .is_continue() +} + +fn needs_inferred_result_ty( + cx: &LateContext<'_>, + e: &Expr<'_>, + locals_to_check: &mut Vec<HirId>, + seen_locals: &mut HirIdSet, +) -> bool { + let (id, args) = match e.kind { + ExprKind::Call( + Expr { + kind: ExprKind::Path(ref path), + hir_id, + .. + }, + args, + ) => match cx.qpath_res(path, *hir_id) { + Res::Def(DefKind::AssocFn | DefKind::Fn, id) => (id, args), + _ => return false, + }, + ExprKind::MethodCall(_, args, _) => match cx.typeck_results().type_dependent_def_id(e.hir_id) { + Some(id) => (id, args), + None => return false, + }, + ExprKind::Path(QPath::Resolved(None, path)) => { + if let Res::Local(id) = path.res + && seen_locals.insert(id) + { + locals_to_check.push(id); + } + return true; + }, + _ => return false, + }; + let sig = cx.tcx.fn_sig(id).skip_binder(); + if let ty::Param(output_ty) = *sig.output().kind() { + sig.inputs().iter().zip(args).all(|(&ty, arg)| { + !ty.is_param(output_ty.index) || each_value_source_needs_inference(cx, arg, locals_to_check, seen_locals) + }) + } else { + false + } +} diff --git a/src/tools/clippy/clippy_lints/src/unit_types/mod.rs b/src/tools/clippy/clippy_lints/src/unit_types/mod.rs new file mode 100644 index 000000000..6aa86a57c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unit_types/mod.rs @@ -0,0 +1,110 @@ +mod let_unit_value; +mod unit_arg; +mod unit_cmp; +mod utils; + +use rustc_hir::{Expr, Local}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for binding a unit value. + /// + /// ### Why is this bad? + /// A unit value cannot usefully be used anywhere. So + /// binding one is kind of pointless. + /// + /// ### Example + /// ```rust + /// let x = { + /// 1; + /// }; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub LET_UNIT_VALUE, + style, + "creating a `let` binding to a value of unit type, which usually can't be used afterwards" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons to unit. This includes all binary + /// comparisons (like `==` and `<`) and asserts. + /// + /// ### Why is this bad? + /// Unit is always equal to itself, and thus is just a + /// clumsily written constant. Mostly this happens when someone accidentally + /// adds semicolons at the end of the operands. + /// + /// ### Example + /// ```rust + /// # fn foo() {}; + /// # fn bar() {}; + /// # fn baz() {}; + /// if { + /// foo(); + /// } == { + /// bar(); + /// } { + /// baz(); + /// } + /// ``` + /// is equal to + /// ```rust + /// # fn foo() {}; + /// # fn bar() {}; + /// # fn baz() {}; + /// { + /// foo(); + /// bar(); + /// baz(); + /// } + /// ``` + /// + /// For asserts: + /// ```rust + /// # fn foo() {}; + /// # fn bar() {}; + /// assert_eq!({ foo(); }, { bar(); }); + /// ``` + /// will always succeed + #[clippy::version = "pre 1.29.0"] + pub UNIT_CMP, + correctness, + "comparing unit values" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for passing a unit value as an argument to a function without using a + /// unit literal (`()`). + /// + /// ### Why is this bad? + /// This is likely the result of an accidental semicolon. + /// + /// ### Example + /// ```rust,ignore + /// foo({ + /// let a = bar(); + /// baz(a); + /// }) + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNIT_ARG, + complexity, + "passing unit to a function" +} + +declare_lint_pass!(UnitTypes => [LET_UNIT_VALUE, UNIT_CMP, UNIT_ARG]); + +impl<'tcx> LateLintPass<'tcx> for UnitTypes { + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { + let_unit_value::check(cx, local); + } + + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + unit_cmp::check(cx, expr); + unit_arg::check(cx, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/unit_types/unit_arg.rs b/src/tools/clippy/clippy_lints/src/unit_types/unit_arg.rs new file mode 100644 index 000000000..97d92f10e --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unit_types/unit_arg.rs @@ -0,0 +1,207 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::{indent_of, reindent_multiline, snippet_opt}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, Block, Expr, ExprKind, MatchSource, Node, StmtKind}; +use rustc_lint::LateContext; + +use super::{utils, UNIT_ARG}; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>) { + if expr.span.from_expansion() { + return; + } + + // apparently stuff in the desugaring of `?` can trigger this + // so check for that here + // only the calls to `Try::from_error` is marked as desugared, + // so we need to check both the current Expr and its parent. + if is_questionmark_desugar_marked_call(expr) { + return; + } + let map = &cx.tcx.hir(); + let opt_parent_node = map.find(map.get_parent_node(expr.hir_id)); + if_chain! { + if let Some(hir::Node::Expr(parent_expr)) = opt_parent_node; + if is_questionmark_desugar_marked_call(parent_expr); + then { + return; + } + } + + match expr.kind { + ExprKind::Call(_, args) | ExprKind::MethodCall(_, args, _) => { + let args_to_recover = args + .iter() + .filter(|arg| { + if cx.typeck_results().expr_ty(arg).is_unit() && !utils::is_unit_literal(arg) { + !matches!( + &arg.kind, + ExprKind::Match(.., MatchSource::TryDesugar) | ExprKind::Path(..) + ) + } else { + false + } + }) + .collect::<Vec<_>>(); + if !args_to_recover.is_empty() { + lint_unit_args(cx, expr, &args_to_recover); + } + }, + _ => (), + } +} + +fn is_questionmark_desugar_marked_call(expr: &Expr<'_>) -> bool { + use rustc_span::hygiene::DesugaringKind; + if let ExprKind::Call(callee, _) = expr.kind { + callee.span.is_desugaring(DesugaringKind::QuestionMark) + } else { + false + } +} + +fn lint_unit_args(cx: &LateContext<'_>, expr: &Expr<'_>, args_to_recover: &[&Expr<'_>]) { + let mut applicability = Applicability::MachineApplicable; + let (singular, plural) = if args_to_recover.len() > 1 { + ("", "s") + } else { + ("a ", "") + }; + span_lint_and_then( + cx, + UNIT_ARG, + expr.span, + &format!("passing {}unit value{} to a function", singular, plural), + |db| { + let mut or = ""; + args_to_recover + .iter() + .filter_map(|arg| { + if_chain! { + if let ExprKind::Block(block, _) = arg.kind; + if block.expr.is_none(); + if let Some(last_stmt) = block.stmts.iter().last(); + if let StmtKind::Semi(last_expr) = last_stmt.kind; + if let Some(snip) = snippet_opt(cx, last_expr.span); + then { + Some(( + last_stmt.span, + snip, + )) + } + else { + None + } + } + }) + .for_each(|(span, sugg)| { + db.span_suggestion( + span, + "remove the semicolon from the last statement in the block", + sugg, + Applicability::MaybeIncorrect, + ); + or = "or "; + applicability = Applicability::MaybeIncorrect; + }); + + let arg_snippets: Vec<String> = args_to_recover + .iter() + .filter_map(|arg| snippet_opt(cx, arg.span)) + .collect(); + let arg_snippets_without_empty_blocks: Vec<String> = args_to_recover + .iter() + .filter(|arg| !is_empty_block(arg)) + .filter_map(|arg| snippet_opt(cx, arg.span)) + .collect(); + + if let Some(call_snippet) = snippet_opt(cx, expr.span) { + let sugg = fmt_stmts_and_call( + cx, + expr, + &call_snippet, + &arg_snippets, + &arg_snippets_without_empty_blocks, + ); + + if arg_snippets_without_empty_blocks.is_empty() { + db.multipart_suggestion( + &format!("use {}unit literal{} instead", singular, plural), + args_to_recover + .iter() + .map(|arg| (arg.span, "()".to_string())) + .collect::<Vec<_>>(), + applicability, + ); + } else { + let plural = arg_snippets_without_empty_blocks.len() > 1; + let empty_or_s = if plural { "s" } else { "" }; + let it_or_them = if plural { "them" } else { "it" }; + db.span_suggestion( + expr.span, + &format!( + "{}move the expression{} in front of the call and replace {} with the unit literal `()`", + or, empty_or_s, it_or_them + ), + sugg, + applicability, + ); + } + } + }, + ); +} + +fn is_empty_block(expr: &Expr<'_>) -> bool { + matches!( + expr.kind, + ExprKind::Block( + Block { + stmts: &[], + expr: None, + .. + }, + _, + ) + ) +} + +fn fmt_stmts_and_call( + cx: &LateContext<'_>, + call_expr: &Expr<'_>, + call_snippet: &str, + args_snippets: &[impl AsRef<str>], + non_empty_block_args_snippets: &[impl AsRef<str>], +) -> String { + let call_expr_indent = indent_of(cx, call_expr.span).unwrap_or(0); + let call_snippet_with_replacements = args_snippets + .iter() + .fold(call_snippet.to_owned(), |acc, arg| acc.replacen(arg.as_ref(), "()", 1)); + + let mut stmts_and_call = non_empty_block_args_snippets + .iter() + .map(|it| it.as_ref().to_owned()) + .collect::<Vec<_>>(); + stmts_and_call.push(call_snippet_with_replacements); + stmts_and_call = stmts_and_call + .into_iter() + .map(|v| reindent_multiline(v.into(), true, Some(call_expr_indent)).into_owned()) + .collect(); + + let mut stmts_and_call_snippet = stmts_and_call.join(&format!("{}{}", ";\n", " ".repeat(call_expr_indent))); + // expr is not in a block statement or result expression position, wrap in a block + let parent_node = cx.tcx.hir().find(cx.tcx.hir().get_parent_node(call_expr.hir_id)); + if !matches!(parent_node, Some(Node::Block(_))) && !matches!(parent_node, Some(Node::Stmt(_))) { + let block_indent = call_expr_indent + 4; + stmts_and_call_snippet = + reindent_multiline(stmts_and_call_snippet.into(), true, Some(block_indent)).into_owned(); + stmts_and_call_snippet = format!( + "{{\n{}{}\n{}}}", + " ".repeat(block_indent), + &stmts_and_call_snippet, + " ".repeat(call_expr_indent) + ); + } + stmts_and_call_snippet +} diff --git a/src/tools/clippy/clippy_lints/src/unit_types/unit_cmp.rs b/src/tools/clippy/clippy_lints/src/unit_types/unit_cmp.rs new file mode 100644 index 000000000..1dd8895eb --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unit_types/unit_cmp.rs @@ -0,0 +1,50 @@ +use clippy_utils::diagnostics::span_lint; +use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node}; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::LateContext; + +use super::UNIT_CMP; + +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>) { + if expr.span.from_expansion() { + if let Some(macro_call) = root_macro_call_first_node(cx, expr) { + let macro_name = cx.tcx.item_name(macro_call.def_id); + let result = match macro_name.as_str() { + "assert_eq" | "debug_assert_eq" => "succeed", + "assert_ne" | "debug_assert_ne" => "fail", + _ => return, + }; + let Some ((left, _, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else { return }; + if !cx.typeck_results().expr_ty(left).is_unit() { + return; + } + span_lint( + cx, + UNIT_CMP, + macro_call.span, + &format!("`{}` of unit values detected. This will always {}", macro_name, result), + ); + } + return; + } + + if let ExprKind::Binary(ref cmp, left, _) = expr.kind { + let op = cmp.node; + if op.is_comparison() && cx.typeck_results().expr_ty(left).is_unit() { + let result = match op { + BinOpKind::Eq | BinOpKind::Le | BinOpKind::Ge => "true", + _ => "false", + }; + span_lint( + cx, + UNIT_CMP, + expr.span, + &format!( + "{}-comparison of unit values detected. This will always be {}", + op.as_str(), + result + ), + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unit_types/utils.rs b/src/tools/clippy/clippy_lints/src/unit_types/utils.rs new file mode 100644 index 000000000..9a3750b23 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unit_types/utils.rs @@ -0,0 +1,5 @@ +use rustc_hir::{Expr, ExprKind}; + +pub(super) fn is_unit_literal(expr: &Expr<'_>) -> bool { + matches!(expr.kind, ExprKind::Tup(slice) if slice.is_empty()) +} diff --git a/src/tools/clippy/clippy_lints/src/unnamed_address.rs b/src/tools/clippy/clippy_lints/src/unnamed_address.rs new file mode 100644 index 000000000..0bcafde65 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unnamed_address.rs @@ -0,0 +1,132 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons with an address of a function item. + /// + /// ### Why is this bad? + /// Function item address is not guaranteed to be unique and could vary + /// between different code generation units. Furthermore different function items could have + /// the same address after being merged together. + /// + /// ### Example + /// ```rust + /// type F = fn(); + /// fn a() {} + /// let f: F = a; + /// if f == a { + /// // ... + /// } + /// ``` + #[clippy::version = "1.44.0"] + pub FN_ADDRESS_COMPARISONS, + correctness, + "comparison with an address of a function item" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons with an address of a trait vtable. + /// + /// ### Why is this bad? + /// Comparing trait objects pointers compares an vtable addresses which + /// are not guaranteed to be unique and could vary between different code generation units. + /// Furthermore vtables for different types could have the same address after being merged + /// together. + /// + /// ### Example + /// ```rust,ignore + /// let a: Rc<dyn Trait> = ... + /// let b: Rc<dyn Trait> = ... + /// if Rc::ptr_eq(&a, &b) { + /// ... + /// } + /// ``` + #[clippy::version = "1.44.0"] + pub VTABLE_ADDRESS_COMPARISONS, + correctness, + "comparison with an address of a trait vtable" +} + +declare_lint_pass!(UnnamedAddress => [FN_ADDRESS_COMPARISONS, VTABLE_ADDRESS_COMPARISONS]); + +impl LateLintPass<'_> for UnnamedAddress { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + fn is_comparison(binop: BinOpKind) -> bool { + matches!( + binop, + BinOpKind::Eq | BinOpKind::Lt | BinOpKind::Le | BinOpKind::Ne | BinOpKind::Ge | BinOpKind::Gt + ) + } + + fn is_trait_ptr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + match cx.typeck_results().expr_ty_adjusted(expr).kind() { + ty::RawPtr(ty::TypeAndMut { ty, .. }) => ty.is_trait(), + _ => false, + } + } + + fn is_fn_def(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + matches!(cx.typeck_results().expr_ty(expr).kind(), ty::FnDef(..)) + } + + if_chain! { + if let ExprKind::Binary(binop, left, right) = expr.kind; + if is_comparison(binop.node); + if is_trait_ptr(cx, left) && is_trait_ptr(cx, right); + then { + span_lint_and_help( + cx, + VTABLE_ADDRESS_COMPARISONS, + expr.span, + "comparing trait object pointers compares a non-unique vtable address", + None, + "consider extracting and comparing data pointers only", + ); + } + } + + if_chain! { + if let ExprKind::Call(func, [ref _left, ref _right]) = expr.kind; + if let ExprKind::Path(ref func_qpath) = func.kind; + if let Some(def_id) = cx.qpath_res(func_qpath, func.hir_id).opt_def_id(); + if match_def_path(cx, def_id, &paths::PTR_EQ) || + match_def_path(cx, def_id, &paths::RC_PTR_EQ) || + match_def_path(cx, def_id, &paths::ARC_PTR_EQ); + let ty_param = cx.typeck_results().node_substs(func.hir_id).type_at(0); + if ty_param.is_trait(); + then { + span_lint_and_help( + cx, + VTABLE_ADDRESS_COMPARISONS, + expr.span, + "comparing trait object pointers compares a non-unique vtable address", + None, + "consider extracting and comparing data pointers only", + ); + } + } + + if_chain! { + if let ExprKind::Binary(binop, left, right) = expr.kind; + if is_comparison(binop.node); + if cx.typeck_results().expr_ty_adjusted(left).is_fn_ptr(); + if cx.typeck_results().expr_ty_adjusted(right).is_fn_ptr(); + if is_fn_def(cx, left) || is_fn_def(cx, right); + then { + span_lint( + cx, + FN_ADDRESS_COMPARISONS, + expr.span, + "comparing with a non-unique address of a function item", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unnecessary_owned_empty_strings.rs b/src/tools/clippy/clippy_lints/src/unnecessary_owned_empty_strings.rs new file mode 100644 index 000000000..8a4f4c0ad --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unnecessary_owned_empty_strings.rs @@ -0,0 +1,81 @@ +use clippy_utils::{diagnostics::span_lint_and_sugg, ty::is_type_diagnostic_item}; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// + /// Detects cases of owned empty strings being passed as an argument to a function expecting `&str` + /// + /// ### Why is this bad? + /// + /// This results in longer and less readable code + /// + /// ### Example + /// ```rust + /// vec!["1", "2", "3"].join(&String::new()); + /// ``` + /// Use instead: + /// ```rust + /// vec!["1", "2", "3"].join(""); + /// ``` + #[clippy::version = "1.62.0"] + pub UNNECESSARY_OWNED_EMPTY_STRINGS, + style, + "detects cases of references to owned empty strings being passed as an argument to a function expecting `&str`" +} +declare_lint_pass!(UnnecessaryOwnedEmptyStrings => [UNNECESSARY_OWNED_EMPTY_STRINGS]); + +impl<'tcx> LateLintPass<'tcx> for UnnecessaryOwnedEmptyStrings { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if_chain! { + if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, inner_expr) = expr.kind; + if let ExprKind::Call(fun, args) = inner_expr.kind; + if let ExprKind::Path(ref qpath) = fun.kind; + if let Some(fun_def_id) = cx.qpath_res(qpath, fun.hir_id).opt_def_id(); + if let ty::Ref(_, inner_str, _) = cx.typeck_results().expr_ty_adjusted(expr).kind(); + if inner_str.is_str(); + then { + if match_def_path(cx, fun_def_id, &paths::STRING_NEW) { + span_lint_and_sugg( + cx, + UNNECESSARY_OWNED_EMPTY_STRINGS, + expr.span, + "usage of `&String::new()` for a function expecting a `&str` argument", + "try", + "\"\"".to_owned(), + Applicability::MachineApplicable, + ); + } else { + if_chain! { + if match_def_path(cx, fun_def_id, &paths::FROM_FROM); + if let [.., last_arg] = args; + if let ExprKind::Lit(spanned) = &last_arg.kind; + if let LitKind::Str(symbol, _) = spanned.node; + if symbol.is_empty(); + let inner_expr_type = cx.typeck_results().expr_ty(inner_expr); + if is_type_diagnostic_item(cx, inner_expr_type, sym::String); + then { + span_lint_and_sugg( + cx, + UNNECESSARY_OWNED_EMPTY_STRINGS, + expr.span, + "usage of `&String::from(\"\")` for a function expecting a `&str` argument", + "try", + "\"\"".to_owned(), + Applicability::MachineApplicable, + ); + } + } + } + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unnecessary_self_imports.rs b/src/tools/clippy/clippy_lints/src/unnecessary_self_imports.rs new file mode 100644 index 000000000..839a4bdab --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unnecessary_self_imports.rs @@ -0,0 +1,70 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use if_chain::if_chain; +use rustc_ast::{Item, ItemKind, UseTreeKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::kw; + +declare_clippy_lint! { + /// ### What it does + /// Checks for imports ending in `::{self}`. + /// + /// ### Why is this bad? + /// In most cases, this can be written much more cleanly by omitting `::{self}`. + /// + /// ### Known problems + /// Removing `::{self}` will cause any non-module items at the same path to also be imported. + /// This might cause a naming conflict (https://github.com/rust-lang/rustfmt/issues/3568). This lint makes no attempt + /// to detect this scenario and that is why it is a restriction lint. + /// + /// ### Example + /// ```rust + /// use std::io::{self}; + /// ``` + /// Use instead: + /// ```rust + /// use std::io; + /// ``` + #[clippy::version = "1.53.0"] + pub UNNECESSARY_SELF_IMPORTS, + restriction, + "imports ending in `::{self}`, which can be omitted" +} + +declare_lint_pass!(UnnecessarySelfImports => [UNNECESSARY_SELF_IMPORTS]); + +impl EarlyLintPass for UnnecessarySelfImports { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if_chain! { + if let ItemKind::Use(use_tree) = &item.kind; + if let UseTreeKind::Nested(nodes) = &use_tree.kind; + if let [(self_tree, _)] = &**nodes; + if let [self_seg] = &*self_tree.prefix.segments; + if self_seg.ident.name == kw::SelfLower; + if let Some(last_segment) = use_tree.prefix.segments.last(); + + then { + span_lint_and_then( + cx, + UNNECESSARY_SELF_IMPORTS, + item.span, + "import ending with `::{self}`", + |diag| { + diag.span_suggestion( + last_segment.span().with_hi(item.span.hi()), + "consider omitting `::{self}`", + format!( + "{}{};", + last_segment.ident, + if let UseTreeKind::Simple(Some(alias), ..) = self_tree.kind { format!(" as {}", alias) } else { String::new() }, + ), + Applicability::MaybeIncorrect, + ); + diag.note("this will slightly change semantics; any non-module items at the same path will also be imported"); + }, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unnecessary_sort_by.rs b/src/tools/clippy/clippy_lints/src/unnecessary_sort_by.rs new file mode 100644 index 000000000..ea5aadbbc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unnecessary_sort_by.rs @@ -0,0 +1,258 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Closure, Expr, ExprKind, Mutability, Param, Pat, PatKind, Path, PathSegment, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, subst::GenericArgKind}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; +use rustc_span::symbol::Ident; +use std::iter; + +declare_clippy_lint! { + /// ### What it does + /// Detects uses of `Vec::sort_by` passing in a closure + /// which compares the two arguments, either directly or indirectly. + /// + /// ### Why is this bad? + /// It is more clear to use `Vec::sort_by_key` (or `Vec::sort` if + /// possible) than to use `Vec::sort_by` and a more complicated + /// closure. + /// + /// ### Known problems + /// If the suggested `Vec::sort_by_key` uses Reverse and it isn't already + /// imported by a use statement, then it will need to be added manually. + /// + /// ### Example + /// ```rust + /// # struct A; + /// # impl A { fn foo(&self) {} } + /// # let mut vec: Vec<A> = Vec::new(); + /// vec.sort_by(|a, b| a.foo().cmp(&b.foo())); + /// ``` + /// Use instead: + /// ```rust + /// # struct A; + /// # impl A { fn foo(&self) {} } + /// # let mut vec: Vec<A> = Vec::new(); + /// vec.sort_by_key(|a| a.foo()); + /// ``` + #[clippy::version = "1.46.0"] + pub UNNECESSARY_SORT_BY, + complexity, + "Use of `Vec::sort_by` when `Vec::sort_by_key` or `Vec::sort` would be clearer" +} + +declare_lint_pass!(UnnecessarySortBy => [UNNECESSARY_SORT_BY]); + +enum LintTrigger { + Sort(SortDetection), + SortByKey(SortByKeyDetection), +} + +struct SortDetection { + vec_name: String, + unstable: bool, +} + +struct SortByKeyDetection { + vec_name: String, + closure_arg: String, + closure_body: String, + reverse: bool, + unstable: bool, +} + +/// Detect if the two expressions are mirrored (identical, except one +/// contains a and the other replaces it with b) +fn mirrored_exprs(a_expr: &Expr<'_>, a_ident: &Ident, b_expr: &Expr<'_>, b_ident: &Ident) -> bool { + match (&a_expr.kind, &b_expr.kind) { + // Two boxes with mirrored contents + (ExprKind::Box(left_expr), ExprKind::Box(right_expr)) => { + mirrored_exprs(left_expr, a_ident, right_expr, b_ident) + }, + // Two arrays with mirrored contents + (ExprKind::Array(left_exprs), ExprKind::Array(right_exprs)) => { + iter::zip(*left_exprs, *right_exprs).all(|(left, right)| mirrored_exprs(left, a_ident, right, b_ident)) + }, + // The two exprs are function calls. + // Check to see that the function itself and its arguments are mirrored + (ExprKind::Call(left_expr, left_args), ExprKind::Call(right_expr, right_args)) => { + mirrored_exprs(left_expr, a_ident, right_expr, b_ident) + && iter::zip(*left_args, *right_args).all(|(left, right)| mirrored_exprs(left, a_ident, right, b_ident)) + }, + // The two exprs are method calls. + // Check to see that the function is the same and the arguments are mirrored + // This is enough because the receiver of the method is listed in the arguments + (ExprKind::MethodCall(left_segment, left_args, _), ExprKind::MethodCall(right_segment, right_args, _)) => { + left_segment.ident == right_segment.ident + && iter::zip(*left_args, *right_args).all(|(left, right)| mirrored_exprs(left, a_ident, right, b_ident)) + }, + // Two tuples with mirrored contents + (ExprKind::Tup(left_exprs), ExprKind::Tup(right_exprs)) => { + iter::zip(*left_exprs, *right_exprs).all(|(left, right)| mirrored_exprs(left, a_ident, right, b_ident)) + }, + // Two binary ops, which are the same operation and which have mirrored arguments + (ExprKind::Binary(left_op, left_left, left_right), ExprKind::Binary(right_op, right_left, right_right)) => { + left_op.node == right_op.node + && mirrored_exprs(left_left, a_ident, right_left, b_ident) + && mirrored_exprs(left_right, a_ident, right_right, b_ident) + }, + // Two unary ops, which are the same operation and which have the same argument + (ExprKind::Unary(left_op, left_expr), ExprKind::Unary(right_op, right_expr)) => { + left_op == right_op && mirrored_exprs(left_expr, a_ident, right_expr, b_ident) + }, + // The two exprs are literals of some kind + (ExprKind::Lit(left_lit), ExprKind::Lit(right_lit)) => left_lit.node == right_lit.node, + (ExprKind::Cast(left, _), ExprKind::Cast(right, _)) => mirrored_exprs(left, a_ident, right, b_ident), + (ExprKind::DropTemps(left_block), ExprKind::DropTemps(right_block)) => { + mirrored_exprs(left_block, a_ident, right_block, b_ident) + }, + (ExprKind::Field(left_expr, left_ident), ExprKind::Field(right_expr, right_ident)) => { + left_ident.name == right_ident.name && mirrored_exprs(left_expr, a_ident, right_expr, right_ident) + }, + // Two paths: either one is a and the other is b, or they're identical to each other + ( + ExprKind::Path(QPath::Resolved( + _, + Path { + segments: left_segments, + .. + }, + )), + ExprKind::Path(QPath::Resolved( + _, + Path { + segments: right_segments, + .. + }, + )), + ) => { + (iter::zip(*left_segments, *right_segments).all(|(left, right)| left.ident == right.ident) + && left_segments + .iter() + .all(|seg| &seg.ident != a_ident && &seg.ident != b_ident)) + || (left_segments.len() == 1 + && &left_segments[0].ident == a_ident + && right_segments.len() == 1 + && &right_segments[0].ident == b_ident) + }, + // Matching expressions, but one or both is borrowed + ( + ExprKind::AddrOf(left_kind, Mutability::Not, left_expr), + ExprKind::AddrOf(right_kind, Mutability::Not, right_expr), + ) => left_kind == right_kind && mirrored_exprs(left_expr, a_ident, right_expr, b_ident), + (_, ExprKind::AddrOf(_, Mutability::Not, right_expr)) => mirrored_exprs(a_expr, a_ident, right_expr, b_ident), + (ExprKind::AddrOf(_, Mutability::Not, left_expr), _) => mirrored_exprs(left_expr, a_ident, b_expr, b_ident), + _ => false, + } +} + +fn detect_lint(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<LintTrigger> { + if_chain! { + if let ExprKind::MethodCall(name_ident, args, _) = &expr.kind; + if let name = name_ident.ident.name.to_ident_string(); + if name == "sort_by" || name == "sort_unstable_by"; + if let [vec, Expr { kind: ExprKind::Closure(Closure { body: closure_body_id, .. }), .. }] = args; + if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(vec), sym::Vec); + if let closure_body = cx.tcx.hir().body(*closure_body_id); + if let &[ + Param { pat: Pat { kind: PatKind::Binding(_, _, left_ident, _), .. }, ..}, + Param { pat: Pat { kind: PatKind::Binding(_, _, right_ident, _), .. }, .. } + ] = &closure_body.params; + if let ExprKind::MethodCall(method_path, [ref left_expr, ref right_expr], _) = &closure_body.value.kind; + if method_path.ident.name == sym::cmp; + then { + let (closure_body, closure_arg, reverse) = if mirrored_exprs( + left_expr, + left_ident, + right_expr, + right_ident + ) { + (Sugg::hir(cx, left_expr, "..").to_string(), left_ident.name.to_string(), false) + } else if mirrored_exprs(left_expr, right_ident, right_expr, left_ident) { + (Sugg::hir(cx, left_expr, "..").to_string(), right_ident.name.to_string(), true) + } else { + return None; + }; + let vec_name = Sugg::hir(cx, &args[0], "..").to_string(); + let unstable = name == "sort_unstable_by"; + + if_chain! { + if let ExprKind::Path(QPath::Resolved(_, Path { + segments: [PathSegment { ident: left_name, .. }], .. + })) = &left_expr.kind; + if left_name == left_ident; + if cx.tcx.get_diagnostic_item(sym::Ord).map_or(false, |id| { + implements_trait(cx, cx.typeck_results().expr_ty(left_expr), id, &[]) + }); + then { + return Some(LintTrigger::Sort(SortDetection { vec_name, unstable })); + } + } + + if !expr_borrows(cx, left_expr) { + return Some(LintTrigger::SortByKey(SortByKeyDetection { + vec_name, + closure_arg, + closure_body, + reverse, + unstable, + })); + } + } + } + + None +} + +fn expr_borrows(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let ty = cx.typeck_results().expr_ty(expr); + matches!(ty.kind(), ty::Ref(..)) || ty.walk().any(|arg| matches!(arg.unpack(), GenericArgKind::Lifetime(_))) +} + +impl LateLintPass<'_> for UnnecessarySortBy { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + match detect_lint(cx, expr) { + Some(LintTrigger::SortByKey(trigger)) => span_lint_and_sugg( + cx, + UNNECESSARY_SORT_BY, + expr.span, + "use Vec::sort_by_key here instead", + "try", + format!( + "{}.sort{}_by_key(|{}| {})", + trigger.vec_name, + if trigger.unstable { "_unstable" } else { "" }, + trigger.closure_arg, + if trigger.reverse { + format!("std::cmp::Reverse({})", trigger.closure_body) + } else { + trigger.closure_body.to_string() + }, + ), + if trigger.reverse { + Applicability::MaybeIncorrect + } else { + Applicability::MachineApplicable + }, + ), + Some(LintTrigger::Sort(trigger)) => span_lint_and_sugg( + cx, + UNNECESSARY_SORT_BY, + expr.span, + "use Vec::sort here instead", + "try", + format!( + "{}.sort{}()", + trigger.vec_name, + if trigger.unstable { "_unstable" } else { "" }, + ), + Applicability::MachineApplicable, + ), + None => {}, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unnecessary_wraps.rs b/src/tools/clippy/clippy_lints/src/unnecessary_wraps.rs new file mode 100644 index 000000000..f4f5a4336 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unnecessary_wraps.rs @@ -0,0 +1,177 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::{contains_return, is_lang_ctor, return_ty, visitors::find_all_ret_expressions}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::intravisit::FnKind; +use rustc_hir::LangItem::{OptionSome, ResultOk}; +use rustc_hir::{Body, ExprKind, FnDecl, HirId, Impl, ItemKind, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::sym; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for private functions that only return `Ok` or `Some`. + /// + /// ### Why is this bad? + /// It is not meaningful to wrap values when no `None` or `Err` is returned. + /// + /// ### Known problems + /// There can be false positives if the function signature is designed to + /// fit some external requirement. + /// + /// ### Example + /// ```rust + /// fn get_cool_number(a: bool, b: bool) -> Option<i32> { + /// if a && b { + /// return Some(50); + /// } + /// if a { + /// Some(0) + /// } else { + /// Some(10) + /// } + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn get_cool_number(a: bool, b: bool) -> i32 { + /// if a && b { + /// return 50; + /// } + /// if a { + /// 0 + /// } else { + /// 10 + /// } + /// } + /// ``` + #[clippy::version = "1.50.0"] + pub UNNECESSARY_WRAPS, + pedantic, + "functions that only return `Ok` or `Some`" +} + +pub struct UnnecessaryWraps { + avoid_breaking_exported_api: bool, +} + +impl_lint_pass!(UnnecessaryWraps => [UNNECESSARY_WRAPS]); + +impl UnnecessaryWraps { + pub fn new(avoid_breaking_exported_api: bool) -> Self { + Self { + avoid_breaking_exported_api, + } + } +} + +impl<'tcx> LateLintPass<'tcx> for UnnecessaryWraps { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + fn_kind: FnKind<'tcx>, + fn_decl: &FnDecl<'tcx>, + body: &Body<'tcx>, + span: Span, + hir_id: HirId, + ) { + // Abort if public function/method or closure. + match fn_kind { + FnKind::ItemFn(..) | FnKind::Method(..) => { + let def_id = cx.tcx.hir().local_def_id(hir_id); + if self.avoid_breaking_exported_api && cx.access_levels.is_exported(def_id) { + return; + } + }, + FnKind::Closure => return, + } + + // Abort if the method is implementing a trait or of it a trait method. + if let Some(Node::Item(item)) = cx.tcx.hir().find(cx.tcx.hir().get_parent_node(hir_id)) { + if matches!( + item.kind, + ItemKind::Impl(Impl { of_trait: Some(_), .. }) | ItemKind::Trait(..) + ) { + return; + } + } + + // Get the wrapper and inner types, if can't, abort. + let (return_type_label, lang_item, inner_type) = if let ty::Adt(adt_def, subst) = return_ty(cx, hir_id).kind() { + if cx.tcx.is_diagnostic_item(sym::Option, adt_def.did()) { + ("Option", OptionSome, subst.type_at(0)) + } else if cx.tcx.is_diagnostic_item(sym::Result, adt_def.did()) { + ("Result", ResultOk, subst.type_at(0)) + } else { + return; + } + } else { + return; + }; + + // Check if all return expression respect the following condition and collect them. + let mut suggs = Vec::new(); + let can_sugg = find_all_ret_expressions(cx, &body.value, |ret_expr| { + if_chain! { + if !ret_expr.span.from_expansion(); + // Check if a function call. + if let ExprKind::Call(func, [arg]) = ret_expr.kind; + // Check if OPTION_SOME or RESULT_OK, depending on return type. + if let ExprKind::Path(qpath) = &func.kind; + if is_lang_ctor(cx, qpath, lang_item); + // Make sure the function argument does not contain a return expression. + if !contains_return(arg); + then { + suggs.push( + ( + ret_expr.span, + if inner_type.is_unit() { + "".to_string() + } else { + snippet(cx, arg.span.source_callsite(), "..").to_string() + } + ) + ); + true + } else { + false + } + } + }); + + if can_sugg && !suggs.is_empty() { + let (lint_msg, return_type_sugg_msg, return_type_sugg, body_sugg_msg) = if inner_type.is_unit() { + ( + "this function's return value is unnecessary".to_string(), + "remove the return type...".to_string(), + snippet(cx, fn_decl.output.span(), "..").to_string(), + "...and then remove returned values", + ) + } else { + ( + format!( + "this function's return value is unnecessarily wrapped by `{}`", + return_type_label + ), + format!("remove `{}` from the return type...", return_type_label), + inner_type.to_string(), + "...and then change returning expressions", + ) + }; + + span_lint_and_then(cx, UNNECESSARY_WRAPS, span, lint_msg.as_str(), |diag| { + diag.span_suggestion( + fn_decl.output.span(), + return_type_sugg_msg.as_str(), + return_type_sugg, + Applicability::MaybeIncorrect, + ); + diag.multipart_suggestion(body_sugg_msg, suggs, Applicability::MaybeIncorrect); + }); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unnested_or_patterns.rs b/src/tools/clippy/clippy_lints/src/unnested_or_patterns.rs new file mode 100644 index 000000000..04e2f301b --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unnested_or_patterns.rs @@ -0,0 +1,428 @@ +#![allow(clippy::wildcard_imports, clippy::enum_glob_use)] + +use clippy_utils::ast_utils::{eq_field_pat, eq_id, eq_maybe_qself, eq_pat, eq_path}; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{meets_msrv, msrvs, over}; +use rustc_ast::mut_visit::*; +use rustc_ast::ptr::P; +use rustc_ast::{self as ast, Mutability, Pat, PatKind, PatKind::*, DUMMY_NODE_ID}; +use rustc_ast_pretty::pprust; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::DUMMY_SP; + +use std::cell::Cell; +use std::mem; + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnested or-patterns, e.g., `Some(0) | Some(2)` and + /// suggests replacing the pattern with a nested one, `Some(0 | 2)`. + /// + /// Another way to think of this is that it rewrites patterns in + /// *disjunctive normal form (DNF)* into *conjunctive normal form (CNF)*. + /// + /// ### Why is this bad? + /// In the example above, `Some` is repeated, which unnecessarily complicates the pattern. + /// + /// ### Example + /// ```rust + /// fn main() { + /// if let Some(0) | Some(2) = Some(0) {} + /// } + /// ``` + /// Use instead: + /// ```rust + /// fn main() { + /// if let Some(0 | 2) = Some(0) {} + /// } + /// ``` + #[clippy::version = "1.46.0"] + pub UNNESTED_OR_PATTERNS, + pedantic, + "unnested or-patterns, e.g., `Foo(Bar) | Foo(Baz) instead of `Foo(Bar | Baz)`" +} + +#[derive(Clone, Copy)] +pub struct UnnestedOrPatterns { + msrv: Option<RustcVersion>, +} + +impl UnnestedOrPatterns { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(UnnestedOrPatterns => [UNNESTED_OR_PATTERNS]); + +impl EarlyLintPass for UnnestedOrPatterns { + fn check_arm(&mut self, cx: &EarlyContext<'_>, a: &ast::Arm) { + if meets_msrv(self.msrv, msrvs::OR_PATTERNS) { + lint_unnested_or_patterns(cx, &a.pat); + } + } + + fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &ast::Expr) { + if meets_msrv(self.msrv, msrvs::OR_PATTERNS) { + if let ast::ExprKind::Let(pat, _, _) = &e.kind { + lint_unnested_or_patterns(cx, pat); + } + } + } + + fn check_param(&mut self, cx: &EarlyContext<'_>, p: &ast::Param) { + if meets_msrv(self.msrv, msrvs::OR_PATTERNS) { + lint_unnested_or_patterns(cx, &p.pat); + } + } + + fn check_local(&mut self, cx: &EarlyContext<'_>, l: &ast::Local) { + if meets_msrv(self.msrv, msrvs::OR_PATTERNS) { + lint_unnested_or_patterns(cx, &l.pat); + } + } + + extract_msrv_attr!(EarlyContext); +} + +fn lint_unnested_or_patterns(cx: &EarlyContext<'_>, pat: &Pat) { + if let Ident(.., None) | Lit(_) | Wild | Path(..) | Range(..) | Rest | MacCall(_) = pat.kind { + // This is a leaf pattern, so cloning is unprofitable. + return; + } + + let mut pat = P(pat.clone()); + + // Nix all the paren patterns everywhere so that they aren't in our way. + remove_all_parens(&mut pat); + + // Transform all unnested or-patterns into nested ones, and if there were none, quit. + if !unnest_or_patterns(&mut pat) { + return; + } + + span_lint_and_then(cx, UNNESTED_OR_PATTERNS, pat.span, "unnested or-patterns", |db| { + insert_necessary_parens(&mut pat); + db.span_suggestion_verbose( + pat.span, + "nest the patterns", + pprust::pat_to_string(&pat), + Applicability::MachineApplicable, + ); + }); +} + +/// Remove all `(p)` patterns in `pat`. +fn remove_all_parens(pat: &mut P<Pat>) { + struct Visitor; + impl MutVisitor for Visitor { + fn visit_pat(&mut self, pat: &mut P<Pat>) { + noop_visit_pat(pat, self); + let inner = match &mut pat.kind { + Paren(i) => mem::replace(&mut i.kind, Wild), + _ => return, + }; + pat.kind = inner; + } + } + Visitor.visit_pat(pat); +} + +/// Insert parens where necessary according to Rust's precedence rules for patterns. +fn insert_necessary_parens(pat: &mut P<Pat>) { + struct Visitor; + impl MutVisitor for Visitor { + fn visit_pat(&mut self, pat: &mut P<Pat>) { + use ast::{BindingMode::*, Mutability::*}; + noop_visit_pat(pat, self); + let target = match &mut pat.kind { + // `i @ a | b`, `box a | b`, and `& mut? a | b`. + Ident(.., Some(p)) | Box(p) | Ref(p, _) if matches!(&p.kind, Or(ps) if ps.len() > 1) => p, + Ref(p, Not) if matches!(p.kind, Ident(ByValue(Mut), ..)) => p, // `&(mut x)` + _ => return, + }; + target.kind = Paren(P(take_pat(target))); + } + } + Visitor.visit_pat(pat); +} + +/// Unnest or-patterns `p0 | ... | p1` in the pattern `pat`. +/// For example, this would transform `Some(0) | FOO | Some(2)` into `Some(0 | 2) | FOO`. +fn unnest_or_patterns(pat: &mut P<Pat>) -> bool { + struct Visitor { + changed: bool, + } + impl MutVisitor for Visitor { + fn visit_pat(&mut self, p: &mut P<Pat>) { + // This is a bottom up transformation, so recurse first. + noop_visit_pat(p, self); + + // Don't have an or-pattern? Just quit early on. + let alternatives = match &mut p.kind { + Or(ps) => ps, + _ => return, + }; + + // Collapse or-patterns directly nested in or-patterns. + let mut idx = 0; + let mut this_level_changed = false; + while idx < alternatives.len() { + let inner = if let Or(ps) = &mut alternatives[idx].kind { + mem::take(ps) + } else { + idx += 1; + continue; + }; + this_level_changed = true; + alternatives.splice(idx..=idx, inner); + } + + // Focus on `p_n` and then try to transform all `p_i` where `i > n`. + let mut focus_idx = 0; + while focus_idx < alternatives.len() { + this_level_changed |= transform_with_focus_on_idx(alternatives, focus_idx); + focus_idx += 1; + } + self.changed |= this_level_changed; + + // Deal with `Some(Some(0)) | Some(Some(1))`. + if this_level_changed { + noop_visit_pat(p, self); + } + } + } + + let mut visitor = Visitor { changed: false }; + visitor.visit_pat(pat); + visitor.changed +} + +/// Match `$scrutinee` against `$pat` and extract `$then` from it. +/// Panics if there is no match. +macro_rules! always_pat { + ($scrutinee:expr, $pat:pat => $then:expr) => { + match $scrutinee { + $pat => $then, + _ => unreachable!(), + } + }; +} + +/// Focus on `focus_idx` in `alternatives`, +/// attempting to extend it with elements of the same constructor `C` +/// in `alternatives[focus_idx + 1..]`. +fn transform_with_focus_on_idx(alternatives: &mut Vec<P<Pat>>, focus_idx: usize) -> bool { + // Extract the kind; we'll need to make some changes in it. + let mut focus_kind = mem::replace(&mut alternatives[focus_idx].kind, PatKind::Wild); + // We'll focus on `alternatives[focus_idx]`, + // so we're draining from `alternatives[focus_idx + 1..]`. + let start = focus_idx + 1; + + // We're trying to find whatever kind (~"constructor") we found in `alternatives[start..]`. + let changed = match &mut focus_kind { + // These pattern forms are "leafs" and do not have sub-patterns. + // Therefore they are not some form of constructor `C`, + // with which a pattern `C(p_0)` may be formed, + // which we would want to join with other `C(p_j)`s. + Ident(.., None) | Lit(_) | Wild | Path(..) | Range(..) | Rest | MacCall(_) + // Skip immutable refs, as grouping them saves few characters, + // and almost always requires adding parens (increasing noisiness). + // In the case of only two patterns, replacement adds net characters. + | Ref(_, Mutability::Not) + // Dealt with elsewhere. + | Or(_) | Paren(_) => false, + // Transform `box x | ... | box y` into `box (x | y)`. + // + // The cases below until `Slice(...)` deal with *singleton* products. + // These patterns have the shape `C(p)`, and not e.g., `C(p0, ..., pn)`. + Box(target) => extend_with_matching( + target, start, alternatives, + |k| matches!(k, Box(_)), + |k| always_pat!(k, Box(p) => p), + ), + // Transform `&mut x | ... | &mut y` into `&mut (x | y)`. + Ref(target, Mutability::Mut) => extend_with_matching( + target, start, alternatives, + |k| matches!(k, Ref(_, Mutability::Mut)), + |k| always_pat!(k, Ref(p, _) => p), + ), + // Transform `b @ p0 | ... b @ p1` into `b @ (p0 | p1)`. + Ident(b1, i1, Some(target)) => extend_with_matching( + target, start, alternatives, + // Binding names must match. + |k| matches!(k, Ident(b2, i2, Some(_)) if b1 == b2 && eq_id(*i1, *i2)), + |k| always_pat!(k, Ident(_, _, Some(p)) => p), + ), + // Transform `[pre, x, post] | ... | [pre, y, post]` into `[pre, x | y, post]`. + Slice(ps1) => extend_with_matching_product( + ps1, start, alternatives, + |k, ps1, idx| matches!(k, Slice(ps2) if eq_pre_post(ps1, ps2, idx)), + |k| always_pat!(k, Slice(ps) => ps), + ), + // Transform `(pre, x, post) | ... | (pre, y, post)` into `(pre, x | y, post)`. + Tuple(ps1) => extend_with_matching_product( + ps1, start, alternatives, + |k, ps1, idx| matches!(k, Tuple(ps2) if eq_pre_post(ps1, ps2, idx)), + |k| always_pat!(k, Tuple(ps) => ps), + ), + // Transform `S(pre, x, post) | ... | S(pre, y, post)` into `S(pre, x | y, post)`. + TupleStruct(qself1, path1, ps1) => extend_with_matching_product( + ps1, start, alternatives, + |k, ps1, idx| matches!( + k, + TupleStruct(qself2, path2, ps2) + if eq_maybe_qself(qself1, qself2) && eq_path(path1, path2) && eq_pre_post(ps1, ps2, idx) + ), + |k| always_pat!(k, TupleStruct(_, _, ps) => ps), + ), + // Transform a record pattern `S { fp_0, ..., fp_n }`. + Struct(qself1, path1, fps1, rest1) => extend_with_struct_pat(qself1, path1, fps1, *rest1, start, alternatives), + }; + + alternatives[focus_idx].kind = focus_kind; + changed +} + +/// Here we focusing on a record pattern `S { fp_0, ..., fp_n }`. +/// In particular, for a record pattern, the order in which the field patterns is irrelevant. +/// So when we fixate on some `ident_k: pat_k`, we try to find `ident_k` in the other pattern +/// and check that all `fp_i` where `i ∈ ((0...n) \ k)` between two patterns are equal. +fn extend_with_struct_pat( + qself1: &Option<ast::QSelf>, + path1: &ast::Path, + fps1: &mut [ast::PatField], + rest1: bool, + start: usize, + alternatives: &mut Vec<P<Pat>>, +) -> bool { + (0..fps1.len()).any(|idx| { + let pos_in_2 = Cell::new(None); // The element `k`. + let tail_or = drain_matching( + start, + alternatives, + |k| { + matches!(k, Struct(qself2, path2, fps2, rest2) + if rest1 == *rest2 // If one struct pattern has `..` so must the other. + && eq_maybe_qself(qself1, qself2) + && eq_path(path1, path2) + && fps1.len() == fps2.len() + && fps1.iter().enumerate().all(|(idx_1, fp1)| { + if idx_1 == idx { + // In the case of `k`, we merely require identical field names + // so that we will transform into `ident_k: p1_k | p2_k`. + let pos = fps2.iter().position(|fp2| eq_id(fp1.ident, fp2.ident)); + pos_in_2.set(pos); + pos.is_some() + } else { + fps2.iter().any(|fp2| eq_field_pat(fp1, fp2)) + } + })) + }, + // Extract `p2_k`. + |k| always_pat!(k, Struct(_, _, mut fps, _) => fps.swap_remove(pos_in_2.take().unwrap()).pat), + ); + extend_with_tail_or(&mut fps1[idx].pat, tail_or) + }) +} + +/// Like `extend_with_matching` but for products with > 1 factor, e.g., `C(p_0, ..., p_n)`. +/// Here, the idea is that we fixate on some `p_k` in `C`, +/// allowing it to vary between two `targets` and `ps2` (returned by `extract`), +/// while also requiring `ps1[..n] ~ ps2[..n]` (pre) and `ps1[n + 1..] ~ ps2[n + 1..]` (post), +/// where `~` denotes semantic equality. +fn extend_with_matching_product( + targets: &mut [P<Pat>], + start: usize, + alternatives: &mut Vec<P<Pat>>, + predicate: impl Fn(&PatKind, &[P<Pat>], usize) -> bool, + extract: impl Fn(PatKind) -> Vec<P<Pat>>, +) -> bool { + (0..targets.len()).any(|idx| { + let tail_or = drain_matching( + start, + alternatives, + |k| predicate(k, targets, idx), + |k| extract(k).swap_remove(idx), + ); + extend_with_tail_or(&mut targets[idx], tail_or) + }) +} + +/// Extract the pattern from the given one and replace it with `Wild`. +/// This is meant for temporarily swapping out the pattern for manipulation. +fn take_pat(from: &mut Pat) -> Pat { + let dummy = Pat { + id: DUMMY_NODE_ID, + kind: Wild, + span: DUMMY_SP, + tokens: None, + }; + mem::replace(from, dummy) +} + +/// Extend `target` as an or-pattern with the alternatives +/// in `tail_or` if there are any and return if there were. +fn extend_with_tail_or(target: &mut Pat, tail_or: Vec<P<Pat>>) -> bool { + fn extend(target: &mut Pat, mut tail_or: Vec<P<Pat>>) { + match target { + // On an existing or-pattern in the target, append to it. + Pat { kind: Or(ps), .. } => ps.append(&mut tail_or), + // Otherwise convert the target to an or-pattern. + target => { + let mut init_or = vec![P(take_pat(target))]; + init_or.append(&mut tail_or); + target.kind = Or(init_or); + }, + } + } + + let changed = !tail_or.is_empty(); + if changed { + // Extend the target. + extend(target, tail_or); + } + changed +} + +// Extract all inner patterns in `alternatives` matching our `predicate`. +// Only elements beginning with `start` are considered for extraction. +fn drain_matching( + start: usize, + alternatives: &mut Vec<P<Pat>>, + predicate: impl Fn(&PatKind) -> bool, + extract: impl Fn(PatKind) -> P<Pat>, +) -> Vec<P<Pat>> { + let mut tail_or = vec![]; + let mut idx = 0; + for pat in alternatives.drain_filter(|p| { + // Check if we should extract, but only if `idx >= start`. + idx += 1; + idx > start && predicate(&p.kind) + }) { + tail_or.push(extract(pat.into_inner().kind)); + } + tail_or +} + +fn extend_with_matching( + target: &mut Pat, + start: usize, + alternatives: &mut Vec<P<Pat>>, + predicate: impl Fn(&PatKind) -> bool, + extract: impl Fn(PatKind) -> P<Pat>, +) -> bool { + extend_with_tail_or(target, drain_matching(start, alternatives, predicate, extract)) +} + +/// Are the patterns in `ps1` and `ps2` equal save for `ps1[idx]` compared to `ps2[idx]`? +fn eq_pre_post(ps1: &[P<Pat>], ps2: &[P<Pat>], idx: usize) -> bool { + ps1.len() == ps2.len() + && ps1[idx].is_rest() == ps2[idx].is_rest() // Avoid `[x, ..] | [x, 0]` => `[x, .. | 0]`. + && over(&ps1[..idx], &ps2[..idx], |l, r| eq_pat(l, r)) + && over(&ps1[idx + 1..], &ps2[idx + 1..], |l, r| eq_pat(l, r)) +} diff --git a/src/tools/clippy/clippy_lints/src/unsafe_removed_from_name.rs b/src/tools/clippy/clippy_lints/src/unsafe_removed_from_name.rs new file mode 100644 index 000000000..64f7a055c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unsafe_removed_from_name.rs @@ -0,0 +1,79 @@ +use clippy_utils::diagnostics::span_lint; +use rustc_ast::ast::{Item, ItemKind, UseTree, UseTreeKind}; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::symbol::Ident; + +declare_clippy_lint! { + /// ### What it does + /// Checks for imports that remove "unsafe" from an item's + /// name. + /// + /// ### Why is this bad? + /// Renaming makes it less clear which traits and + /// structures are unsafe. + /// + /// ### Example + /// ```rust,ignore + /// use std::cell::{UnsafeCell as TotallySafeCell}; + /// + /// extern crate crossbeam; + /// use crossbeam::{spawn_unsafe as spawn}; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNSAFE_REMOVED_FROM_NAME, + style, + "`unsafe` removed from API names on import" +} + +declare_lint_pass!(UnsafeNameRemoval => [UNSAFE_REMOVED_FROM_NAME]); + +impl EarlyLintPass for UnsafeNameRemoval { + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { + if let ItemKind::Use(ref use_tree) = item.kind { + check_use_tree(use_tree, cx, item.span); + } + } +} + +fn check_use_tree(use_tree: &UseTree, cx: &EarlyContext<'_>, span: Span) { + match use_tree.kind { + UseTreeKind::Simple(Some(new_name), ..) => { + let old_name = use_tree + .prefix + .segments + .last() + .expect("use paths cannot be empty") + .ident; + unsafe_to_safe_check(old_name, new_name, cx, span); + }, + UseTreeKind::Simple(None, ..) | UseTreeKind::Glob => {}, + UseTreeKind::Nested(ref nested_use_tree) => { + for &(ref use_tree, _) in nested_use_tree { + check_use_tree(use_tree, cx, span); + } + }, + } +} + +fn unsafe_to_safe_check(old_name: Ident, new_name: Ident, cx: &EarlyContext<'_>, span: Span) { + let old_str = old_name.name.as_str(); + let new_str = new_name.name.as_str(); + if contains_unsafe(old_str) && !contains_unsafe(new_str) { + span_lint( + cx, + UNSAFE_REMOVED_FROM_NAME, + span, + &format!( + "removed `unsafe` from the name of `{}` in use as `{}`", + old_str, new_str + ), + ); + } +} + +#[must_use] +fn contains_unsafe(name: &str) -> bool { + name.contains("Unsafe") || name.contains("unsafe") +} diff --git a/src/tools/clippy/clippy_lints/src/unused_async.rs b/src/tools/clippy/clippy_lints/src/unused_async.rs new file mode 100644 index 000000000..a832dfccc --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unused_async.rs @@ -0,0 +1,86 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::intravisit::{walk_expr, walk_fn, FnKind, Visitor}; +use rustc_hir::{Body, Expr, ExprKind, FnDecl, HirId, IsAsync, YieldSource}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions that are declared `async` but have no `.await`s inside of them. + /// + /// ### Why is this bad? + /// Async functions with no async code create overhead, both mentally and computationally. + /// Callers of async methods either need to be calling from an async function themselves or run it on an executor, both of which + /// causes runtime overhead and hassle for the caller. + /// + /// ### Example + /// ```rust + /// async fn get_random_number() -> i64 { + /// 4 // Chosen by fair dice roll. Guaranteed to be random. + /// } + /// let number_future = get_random_number(); + /// ``` + /// + /// Use instead: + /// ```rust + /// fn get_random_number_improved() -> i64 { + /// 4 // Chosen by fair dice roll. Guaranteed to be random. + /// } + /// let number_future = async { get_random_number_improved() }; + /// ``` + #[clippy::version = "1.54.0"] + pub UNUSED_ASYNC, + pedantic, + "finds async functions with no await statements" +} + +declare_lint_pass!(UnusedAsync => [UNUSED_ASYNC]); + +struct AsyncFnVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + found_await: bool, +} + +impl<'a, 'tcx> Visitor<'tcx> for AsyncFnVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) { + if let ExprKind::Yield(_, YieldSource::Await { .. }) = ex.kind { + self.found_await = true; + } + walk_expr(self, ex); + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +impl<'tcx> LateLintPass<'tcx> for UnusedAsync { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + fn_kind: FnKind<'tcx>, + fn_decl: &'tcx FnDecl<'tcx>, + body: &Body<'tcx>, + span: Span, + hir_id: HirId, + ) { + if !span.from_expansion() && fn_kind.asyncness() == IsAsync::Async { + let mut visitor = AsyncFnVisitor { cx, found_await: false }; + walk_fn(&mut visitor, fn_kind, fn_decl, body.id(), span, hir_id); + if !visitor.found_await { + span_lint_and_help( + cx, + UNUSED_ASYNC, + span, + "unused `async` for function with no await statements", + None, + "consider removing the `async` from this function", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unused_io_amount.rs b/src/tools/clippy/clippy_lints/src/unused_io_amount.rs new file mode 100644 index 000000000..323cf83ff --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unused_io_amount.rs @@ -0,0 +1,170 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; +use clippy_utils::{is_try, match_trait_method, paths}; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for unused written/read amount. + /// + /// ### Why is this bad? + /// `io::Write::write(_vectored)` and + /// `io::Read::read(_vectored)` are not guaranteed to + /// process the entire buffer. They return how many bytes were processed, which + /// might be smaller + /// than a given buffer's length. If you don't need to deal with + /// partial-write/read, use + /// `write_all`/`read_exact` instead. + /// + /// When working with asynchronous code (either with the `futures` + /// crate or with `tokio`), a similar issue exists for + /// `AsyncWriteExt::write()` and `AsyncReadExt::read()` : these + /// functions are also not guaranteed to process the entire + /// buffer. Your code should either handle partial-writes/reads, or + /// call the `write_all`/`read_exact` methods on those traits instead. + /// + /// ### Known problems + /// Detects only common patterns. + /// + /// ### Examples + /// ```rust,ignore + /// use std::io; + /// fn foo<W: io::Write>(w: &mut W) -> io::Result<()> { + /// // must be `w.write_all(b"foo")?;` + /// w.write(b"foo")?; + /// Ok(()) + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNUSED_IO_AMOUNT, + correctness, + "unused written/read amount" +} + +declare_lint_pass!(UnusedIoAmount => [UNUSED_IO_AMOUNT]); + +impl<'tcx> LateLintPass<'tcx> for UnusedIoAmount { + fn check_stmt(&mut self, cx: &LateContext<'_>, s: &hir::Stmt<'_>) { + let expr = match s.kind { + hir::StmtKind::Semi(expr) | hir::StmtKind::Expr(expr) => expr, + _ => return, + }; + + match expr.kind { + hir::ExprKind::Match(res, _, _) if is_try(cx, expr).is_some() => { + if let hir::ExprKind::Call(func, [ref arg_0, ..]) = res.kind { + if matches!( + func.kind, + hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::TryTraitBranch, ..)) + ) { + check_map_error(cx, arg_0, expr); + } + } else { + check_map_error(cx, res, expr); + } + }, + hir::ExprKind::MethodCall(path, [ref arg_0, ..], _) => match path.ident.as_str() { + "expect" | "unwrap" | "unwrap_or" | "unwrap_or_else" => { + check_map_error(cx, arg_0, expr); + }, + _ => (), + }, + _ => (), + } + } +} + +/// If `expr` is an (e).await, return the inner expression "e" that's being +/// waited on. Otherwise return None. +fn try_remove_await<'a>(expr: &'a hir::Expr<'a>) -> Option<&hir::Expr<'a>> { + if let hir::ExprKind::Match(expr, _, hir::MatchSource::AwaitDesugar) = expr.kind { + if let hir::ExprKind::Call(func, [ref arg_0, ..]) = expr.kind { + if matches!( + func.kind, + hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::IntoFutureIntoFuture, ..)) + ) { + return Some(arg_0); + } + } + } + + None +} + +fn check_map_error(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<'_>) { + let mut call = call; + while let hir::ExprKind::MethodCall(path, args, _) = call.kind { + if matches!(path.ident.as_str(), "or" | "or_else" | "ok") { + call = &args[0]; + } else { + break; + } + } + + if let Some(call) = try_remove_await(call) { + check_method_call(cx, call, expr, true); + } else { + check_method_call(cx, call, expr, false); + } +} + +fn check_method_call(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<'_>, is_await: bool) { + if let hir::ExprKind::MethodCall(path, _, _) = call.kind { + let symbol = path.ident.as_str(); + let read_trait = if is_await { + match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCREADEXT) + || match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCREADEXT) + } else { + match_trait_method(cx, call, &paths::IO_READ) + }; + let write_trait = if is_await { + match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCWRITEEXT) + || match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCWRITEEXT) + } else { + match_trait_method(cx, call, &paths::IO_WRITE) + }; + + match (read_trait, write_trait, symbol, is_await) { + (true, _, "read", false) => span_lint_and_help( + cx, + UNUSED_IO_AMOUNT, + expr.span, + "read amount is not handled", + None, + "use `Read::read_exact` instead, or handle partial reads", + ), + (true, _, "read", true) => span_lint_and_help( + cx, + UNUSED_IO_AMOUNT, + expr.span, + "read amount is not handled", + None, + "use `AsyncReadExt::read_exact` instead, or handle partial reads", + ), + (true, _, "read_vectored", _) => { + span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "read amount is not handled"); + }, + (_, true, "write", false) => span_lint_and_help( + cx, + UNUSED_IO_AMOUNT, + expr.span, + "written amount is not handled", + None, + "use `Write::write_all` instead, or handle partial writes", + ), + (_, true, "write", true) => span_lint_and_help( + cx, + UNUSED_IO_AMOUNT, + expr.span, + "written amount is not handled", + None, + "use `AsyncWriteExt::write_all` instead, or handle partial writes", + ), + (_, true, "write_vectored", _) => { + span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "written amount is not handled"); + }, + _ => (), + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unused_rounding.rs b/src/tools/clippy/clippy_lints/src/unused_rounding.rs new file mode 100644 index 000000000..306afe441 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unused_rounding.rs @@ -0,0 +1,69 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use rustc_ast::ast::{Expr, ExprKind, LitFloatType, LitKind}; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// + /// Detects cases where a whole-number literal float is being rounded, using + /// the `floor`, `ceil`, or `round` methods. + /// + /// ### Why is this bad? + /// + /// This is unnecessary and confusing to the reader. Doing this is probably a mistake. + /// + /// ### Example + /// ```rust + /// let x = 1f32.ceil(); + /// ``` + /// Use instead: + /// ```rust + /// let x = 1f32; + /// ``` + #[clippy::version = "1.62.0"] + pub UNUSED_ROUNDING, + nursery, + "Uselessly rounding a whole number floating-point literal" +} +declare_lint_pass!(UnusedRounding => [UNUSED_ROUNDING]); + +fn is_useless_rounding(expr: &Expr) -> Option<(&str, String)> { + if let ExprKind::MethodCall(name_ident, args, _) = &expr.kind + && let method_name = name_ident.ident.name.as_str() + && (method_name == "ceil" || method_name == "round" || method_name == "floor") + && !args.is_empty() + && let ExprKind::Lit(spanned) = &args[0].kind + && let LitKind::Float(symbol, ty) = spanned.kind { + let f = symbol.as_str().parse::<f64>().unwrap(); + let f_str = symbol.to_string() + if let LitFloatType::Suffixed(ty) = ty { + ty.name_str() + } else { + "" + }; + if f.fract() == 0.0 { + Some((method_name, f_str)) + } else { + None + } + } else { + None + } +} + +impl EarlyLintPass for UnusedRounding { + fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { + if let Some((method_name, float)) = is_useless_rounding(expr) { + span_lint_and_sugg( + cx, + UNUSED_ROUNDING, + expr.span, + &format!("used the `{}` method with a whole number float", method_name), + &format!("remove the `{}` method call", method_name), + float, + Applicability::MachineApplicable, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unused_self.rs b/src/tools/clippy/clippy_lints/src/unused_self.rs new file mode 100644 index 000000000..51c65d898 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unused_self.rs @@ -0,0 +1,80 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::visitors::is_local_used; +use if_chain::if_chain; +use rustc_hir::{Impl, ImplItem, ImplItemKind, ItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +declare_clippy_lint! { + /// ### What it does + /// Checks methods that contain a `self` argument but don't use it + /// + /// ### Why is this bad? + /// It may be clearer to define the method as an associated function instead + /// of an instance method if it doesn't require `self`. + /// + /// ### Example + /// ```rust,ignore + /// struct A; + /// impl A { + /// fn method(&self) {} + /// } + /// ``` + /// + /// Could be written: + /// + /// ```rust,ignore + /// struct A; + /// impl A { + /// fn method() {} + /// } + /// ``` + #[clippy::version = "1.40.0"] + pub UNUSED_SELF, + pedantic, + "methods that contain a `self` argument but don't use it" +} + +pub struct UnusedSelf { + avoid_breaking_exported_api: bool, +} + +impl_lint_pass!(UnusedSelf => [UNUSED_SELF]); + +impl UnusedSelf { + pub fn new(avoid_breaking_exported_api: bool) -> Self { + Self { + avoid_breaking_exported_api, + } + } +} + +impl<'tcx> LateLintPass<'tcx> for UnusedSelf { + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &ImplItem<'_>) { + if impl_item.span.from_expansion() { + return; + } + let parent = cx.tcx.hir().get_parent_item(impl_item.hir_id()); + let parent_item = cx.tcx.hir().expect_item(parent); + let assoc_item = cx.tcx.associated_item(impl_item.def_id); + if_chain! { + if let ItemKind::Impl(Impl { of_trait: None, .. }) = parent_item.kind; + if assoc_item.fn_has_self_parameter; + if let ImplItemKind::Fn(.., body_id) = &impl_item.kind; + if !cx.access_levels.is_exported(impl_item.def_id) || !self.avoid_breaking_exported_api; + let body = cx.tcx.hir().body(*body_id); + if let [self_param, ..] = body.params; + if !is_local_used(cx, body, self_param.pat.hir_id); + then { + span_lint_and_help( + cx, + UNUSED_SELF, + self_param.span, + "unused `self` argument", + None, + "consider refactoring to a associated function", + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/unused_unit.rs b/src/tools/clippy/clippy_lints/src/unused_unit.rs new file mode 100644 index 000000000..52585e595 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unused_unit.rs @@ -0,0 +1,148 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{position_before_rarrow, snippet_opt}; +use if_chain::if_chain; +use rustc_ast::ast; +use rustc_ast::visit::FnKind; +use rustc_errors::Applicability; +use rustc_lint::{EarlyContext, EarlyLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::BytePos; + +declare_clippy_lint! { + /// ### What it does + /// Checks for unit (`()`) expressions that can be removed. + /// + /// ### Why is this bad? + /// Such expressions add no value, but can make the code + /// less readable. Depending on formatting they can make a `break` or `return` + /// statement look like a function call. + /// + /// ### Example + /// ```rust + /// fn return_unit() -> () { + /// () + /// } + /// ``` + /// is equivalent to + /// ```rust + /// fn return_unit() {} + /// ``` + #[clippy::version = "1.31.0"] + pub UNUSED_UNIT, + style, + "needless unit expression" +} + +declare_lint_pass!(UnusedUnit => [UNUSED_UNIT]); + +impl EarlyLintPass for UnusedUnit { + fn check_fn(&mut self, cx: &EarlyContext<'_>, kind: FnKind<'_>, span: Span, _: ast::NodeId) { + if_chain! { + if let ast::FnRetTy::Ty(ref ty) = kind.decl().output; + if let ast::TyKind::Tup(ref vals) = ty.kind; + if vals.is_empty() && !ty.span.from_expansion() && get_def(span) == get_def(ty.span); + then { + lint_unneeded_unit_return(cx, ty, span); + } + } + } + + fn check_block(&mut self, cx: &EarlyContext<'_>, block: &ast::Block) { + if_chain! { + if let Some(stmt) = block.stmts.last(); + if let ast::StmtKind::Expr(ref expr) = stmt.kind; + if is_unit_expr(expr); + let ctxt = block.span.ctxt(); + if stmt.span.ctxt() == ctxt && expr.span.ctxt() == ctxt; + then { + let sp = expr.span; + span_lint_and_sugg( + cx, + UNUSED_UNIT, + sp, + "unneeded unit expression", + "remove the final `()`", + String::new(), + Applicability::MachineApplicable, + ); + } + } + } + + fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &ast::Expr) { + match e.kind { + ast::ExprKind::Ret(Some(ref expr)) | ast::ExprKind::Break(_, Some(ref expr)) => { + if is_unit_expr(expr) && !expr.span.from_expansion() { + span_lint_and_sugg( + cx, + UNUSED_UNIT, + expr.span, + "unneeded `()`", + "remove the `()`", + String::new(), + Applicability::MachineApplicable, + ); + } + }, + _ => (), + } + } + + fn check_poly_trait_ref(&mut self, cx: &EarlyContext<'_>, poly: &ast::PolyTraitRef, _: &ast::TraitBoundModifier) { + let segments = &poly.trait_ref.path.segments; + + if_chain! { + if segments.len() == 1; + if ["Fn", "FnMut", "FnOnce"].contains(&segments[0].ident.name.as_str()); + if let Some(args) = &segments[0].args; + if let ast::GenericArgs::Parenthesized(generic_args) = &**args; + if let ast::FnRetTy::Ty(ty) = &generic_args.output; + if ty.kind.is_unit(); + then { + lint_unneeded_unit_return(cx, ty, generic_args.span); + } + } + } +} + +// get the def site +#[must_use] +fn get_def(span: Span) -> Option<Span> { + if span.from_expansion() { + Some(span.ctxt().outer_expn_data().def_site) + } else { + None + } +} + +// is this expr a `()` unit? +fn is_unit_expr(expr: &ast::Expr) -> bool { + if let ast::ExprKind::Tup(ref vals) = expr.kind { + vals.is_empty() + } else { + false + } +} + +fn lint_unneeded_unit_return(cx: &EarlyContext<'_>, ty: &ast::Ty, span: Span) { + let (ret_span, appl) = + snippet_opt(cx, span.with_hi(ty.span.hi())).map_or((ty.span, Applicability::MaybeIncorrect), |fn_source| { + position_before_rarrow(&fn_source).map_or((ty.span, Applicability::MaybeIncorrect), |rpos| { + ( + #[expect(clippy::cast_possible_truncation)] + ty.span.with_lo(BytePos(span.lo().0 + rpos as u32)), + Applicability::MachineApplicable, + ) + }) + }); + span_lint_and_sugg( + cx, + UNUSED_UNIT, + ret_span, + "unneeded unit return type", + "remove the `-> ()`", + String::new(), + appl, + ); +} diff --git a/src/tools/clippy/clippy_lints/src/unwrap.rs b/src/tools/clippy/clippy_lints/src/unwrap.rs new file mode 100644 index 000000000..d3f9e5abf --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unwrap.rs @@ -0,0 +1,331 @@ +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::higher; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{path_to_local, usage::is_potentially_mutated}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_expr, walk_fn, FnKind, Visitor}; +use rustc_hir::{BinOpKind, Body, Expr, ExprKind, FnDecl, HirId, PathSegment, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::hir::nested_filter; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::Ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Span; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls of `unwrap[_err]()` that cannot fail. + /// + /// ### Why is this bad? + /// Using `if let` or `match` is more idiomatic. + /// + /// ### Example + /// ```rust + /// # let option = Some(0); + /// # fn do_something_with(_x: usize) {} + /// if option.is_some() { + /// do_something_with(option.unwrap()) + /// } + /// ``` + /// + /// Could be written: + /// + /// ```rust + /// # let option = Some(0); + /// # fn do_something_with(_x: usize) {} + /// if let Some(value) = option { + /// do_something_with(value) + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub UNNECESSARY_UNWRAP, + complexity, + "checks for calls of `unwrap[_err]()` that cannot fail" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls of `unwrap[_err]()` that will always fail. + /// + /// ### Why is this bad? + /// If panicking is desired, an explicit `panic!()` should be used. + /// + /// ### Known problems + /// This lint only checks `if` conditions not assignments. + /// So something like `let x: Option<()> = None; x.unwrap();` will not be recognized. + /// + /// ### Example + /// ```rust + /// # let option = Some(0); + /// # fn do_something_with(_x: usize) {} + /// if option.is_none() { + /// do_something_with(option.unwrap()) + /// } + /// ``` + /// + /// This code will always panic. The if condition should probably be inverted. + #[clippy::version = "pre 1.29.0"] + pub PANICKING_UNWRAP, + correctness, + "checks for calls of `unwrap[_err]()` that will always fail" +} + +/// Visitor that keeps track of which variables are unwrappable. +struct UnwrappableVariablesVisitor<'a, 'tcx> { + unwrappables: Vec<UnwrapInfo<'tcx>>, + cx: &'a LateContext<'tcx>, +} + +/// What kind of unwrappable this is. +#[derive(Copy, Clone, Debug)] +enum UnwrappableKind { + Option, + Result, +} + +impl UnwrappableKind { + fn success_variant_pattern(self) -> &'static str { + match self { + UnwrappableKind::Option => "Some(..)", + UnwrappableKind::Result => "Ok(..)", + } + } + + fn error_variant_pattern(self) -> &'static str { + match self { + UnwrappableKind::Option => "None", + UnwrappableKind::Result => "Err(..)", + } + } +} + +/// Contains information about whether a variable can be unwrapped. +#[derive(Copy, Clone, Debug)] +struct UnwrapInfo<'tcx> { + /// The variable that is checked + local_id: HirId, + /// The if itself + if_expr: &'tcx Expr<'tcx>, + /// The check, like `x.is_ok()` + check: &'tcx Expr<'tcx>, + /// The check's name, like `is_ok` + check_name: &'tcx PathSegment<'tcx>, + /// The branch where the check takes place, like `if x.is_ok() { .. }` + branch: &'tcx Expr<'tcx>, + /// Whether `is_some()` or `is_ok()` was called (as opposed to `is_err()` or `is_none()`). + safe_to_unwrap: bool, + /// What kind of unwrappable this is. + kind: UnwrappableKind, + /// If the check is the entire condition (`if x.is_ok()`) or only a part of it (`foo() && + /// x.is_ok()`) + is_entire_condition: bool, +} + +/// Collects the information about unwrappable variables from an if condition +/// The `invert` argument tells us whether the condition is negated. +fn collect_unwrap_info<'tcx>( + cx: &LateContext<'tcx>, + if_expr: &'tcx Expr<'_>, + expr: &'tcx Expr<'_>, + branch: &'tcx Expr<'_>, + invert: bool, + is_entire_condition: bool, +) -> Vec<UnwrapInfo<'tcx>> { + fn is_relevant_option_call(cx: &LateContext<'_>, ty: Ty<'_>, method_name: &str) -> bool { + is_type_diagnostic_item(cx, ty, sym::Option) && ["is_some", "is_none"].contains(&method_name) + } + + fn is_relevant_result_call(cx: &LateContext<'_>, ty: Ty<'_>, method_name: &str) -> bool { + is_type_diagnostic_item(cx, ty, sym::Result) && ["is_ok", "is_err"].contains(&method_name) + } + + if let ExprKind::Binary(op, left, right) = &expr.kind { + match (invert, op.node) { + (false, BinOpKind::And | BinOpKind::BitAnd) | (true, BinOpKind::Or | BinOpKind::BitOr) => { + let mut unwrap_info = collect_unwrap_info(cx, if_expr, left, branch, invert, false); + unwrap_info.append(&mut collect_unwrap_info(cx, if_expr, right, branch, invert, false)); + return unwrap_info; + }, + _ => (), + } + } else if let ExprKind::Unary(UnOp::Not, expr) = &expr.kind { + return collect_unwrap_info(cx, if_expr, expr, branch, !invert, false); + } else { + if_chain! { + if let ExprKind::MethodCall(method_name, args, _) = &expr.kind; + if let Some(local_id) = path_to_local(&args[0]); + let ty = cx.typeck_results().expr_ty(&args[0]); + let name = method_name.ident.as_str(); + if is_relevant_option_call(cx, ty, name) || is_relevant_result_call(cx, ty, name); + then { + assert!(args.len() == 1); + let unwrappable = match name { + "is_some" | "is_ok" => true, + "is_err" | "is_none" => false, + _ => unreachable!(), + }; + let safe_to_unwrap = unwrappable != invert; + let kind = if is_type_diagnostic_item(cx, ty, sym::Option) { + UnwrappableKind::Option + } else { + UnwrappableKind::Result + }; + + return vec![ + UnwrapInfo { + local_id, + if_expr, + check: expr, + check_name: method_name, + branch, + safe_to_unwrap, + kind, + is_entire_condition, + } + ] + } + } + } + Vec::new() +} + +impl<'a, 'tcx> UnwrappableVariablesVisitor<'a, 'tcx> { + fn visit_branch( + &mut self, + if_expr: &'tcx Expr<'_>, + cond: &'tcx Expr<'_>, + branch: &'tcx Expr<'_>, + else_branch: bool, + ) { + let prev_len = self.unwrappables.len(); + for unwrap_info in collect_unwrap_info(self.cx, if_expr, cond, branch, else_branch, true) { + if is_potentially_mutated(unwrap_info.local_id, cond, self.cx) + || is_potentially_mutated(unwrap_info.local_id, branch, self.cx) + { + // if the variable is mutated, we don't know whether it can be unwrapped: + continue; + } + self.unwrappables.push(unwrap_info); + } + walk_expr(self, branch); + self.unwrappables.truncate(prev_len); + } +} + +impl<'a, 'tcx> Visitor<'tcx> for UnwrappableVariablesVisitor<'a, 'tcx> { + type NestedFilter = nested_filter::OnlyBodies; + + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + // Shouldn't lint when `expr` is in macro. + if in_external_macro(self.cx.tcx.sess, expr.span) { + return; + } + if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr) { + walk_expr(self, cond); + self.visit_branch(expr, cond, then, false); + if let Some(else_inner) = r#else { + self.visit_branch(expr, cond, else_inner, true); + } + } else { + // find `unwrap[_err]()` calls: + if_chain! { + if let ExprKind::MethodCall(method_name, [self_arg, ..], _) = expr.kind; + if let Some(id) = path_to_local(self_arg); + if [sym::unwrap, sym::expect, sym!(unwrap_err)].contains(&method_name.ident.name); + let call_to_unwrap = [sym::unwrap, sym::expect].contains(&method_name.ident.name); + if let Some(unwrappable) = self.unwrappables.iter() + .find(|u| u.local_id == id); + // Span contexts should not differ with the conditional branch + let span_ctxt = expr.span.ctxt(); + if unwrappable.branch.span.ctxt() == span_ctxt; + if unwrappable.check.span.ctxt() == span_ctxt; + then { + if call_to_unwrap == unwrappable.safe_to_unwrap { + let is_entire_condition = unwrappable.is_entire_condition; + let unwrappable_variable_name = self.cx.tcx.hir().name(unwrappable.local_id); + let suggested_pattern = if call_to_unwrap { + unwrappable.kind.success_variant_pattern() + } else { + unwrappable.kind.error_variant_pattern() + }; + + span_lint_hir_and_then( + self.cx, + UNNECESSARY_UNWRAP, + expr.hir_id, + expr.span, + &format!( + "called `{}` on `{}` after checking its variant with `{}`", + method_name.ident.name, + unwrappable_variable_name, + unwrappable.check_name.ident.as_str(), + ), + |diag| { + if is_entire_condition { + diag.span_suggestion( + unwrappable.check.span.with_lo(unwrappable.if_expr.span.lo()), + "try", + format!( + "if let {} = {}", + suggested_pattern, + unwrappable_variable_name, + ), + // We don't track how the unwrapped value is used inside the + // block or suggest deleting the unwrap, so we can't offer a + // fixable solution. + Applicability::Unspecified, + ); + } else { + diag.span_label(unwrappable.check.span, "the check is happening here"); + diag.help("try using `if let` or `match`"); + } + }, + ); + } else { + span_lint_hir_and_then( + self.cx, + PANICKING_UNWRAP, + expr.hir_id, + expr.span, + &format!("this call to `{}()` will always panic", + method_name.ident.name), + |diag| { diag.span_label(unwrappable.check.span, "because of this check"); }, + ); + } + } + } + walk_expr(self, expr); + } + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +declare_lint_pass!(Unwrap => [PANICKING_UNWRAP, UNNECESSARY_UNWRAP]); + +impl<'tcx> LateLintPass<'tcx> for Unwrap { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + kind: FnKind<'tcx>, + decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + span: Span, + fn_id: HirId, + ) { + if span.from_expansion() { + return; + } + + let mut v = UnwrappableVariablesVisitor { + cx, + unwrappables: Vec::new(), + }; + + walk_fn(&mut v, kind, decl, body.id(), span, fn_id); + } +} diff --git a/src/tools/clippy/clippy_lints/src/unwrap_in_result.rs b/src/tools/clippy/clippy_lints/src/unwrap_in_result.rs new file mode 100644 index 000000000..b32be238c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/unwrap_in_result.rs @@ -0,0 +1,133 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{method_chain_args, return_ty}; +use if_chain::if_chain; +use rustc_hir as hir; +use rustc_hir::intravisit::{self, Visitor}; +use rustc_hir::{Expr, ImplItemKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for functions of type `Result` that contain `expect()` or `unwrap()` + /// + /// ### Why is this bad? + /// These functions promote recoverable errors to non-recoverable errors which may be undesirable in code bases which wish to avoid panics. + /// + /// ### Known problems + /// This can cause false positives in functions that handle both recoverable and non recoverable errors. + /// + /// ### Example + /// Before: + /// ```rust + /// fn divisible_by_3(i_str: String) -> Result<(), String> { + /// let i = i_str + /// .parse::<i32>() + /// .expect("cannot divide the input by three"); + /// + /// if i % 3 != 0 { + /// Err("Number is not divisible by 3")? + /// } + /// + /// Ok(()) + /// } + /// ``` + /// + /// After: + /// ```rust + /// fn divisible_by_3(i_str: String) -> Result<(), String> { + /// let i = i_str + /// .parse::<i32>() + /// .map_err(|e| format!("cannot divide the input by three: {}", e))?; + /// + /// if i % 3 != 0 { + /// Err("Number is not divisible by 3")? + /// } + /// + /// Ok(()) + /// } + /// ``` + #[clippy::version = "1.48.0"] + pub UNWRAP_IN_RESULT, + restriction, + "functions of type `Result<..>` or `Option`<...> that contain `expect()` or `unwrap()`" +} + +declare_lint_pass!(UnwrapInResult=> [UNWRAP_IN_RESULT]); + +impl<'tcx> LateLintPass<'tcx> for UnwrapInResult { + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx hir::ImplItem<'_>) { + if_chain! { + // first check if it's a method or function + if let hir::ImplItemKind::Fn(ref _signature, _) = impl_item.kind; + // checking if its return type is `result` or `option` + if is_type_diagnostic_item(cx, return_ty(cx, impl_item.hir_id()), sym::Result) + || is_type_diagnostic_item(cx, return_ty(cx, impl_item.hir_id()), sym::Option); + then { + lint_impl_body(cx, impl_item.span, impl_item); + } + } + } +} + +struct FindExpectUnwrap<'a, 'tcx> { + lcx: &'a LateContext<'tcx>, + typeck_results: &'tcx ty::TypeckResults<'tcx>, + result: Vec<Span>, +} + +impl<'a, 'tcx> Visitor<'tcx> for FindExpectUnwrap<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + // check for `expect` + if let Some(arglists) = method_chain_args(expr, &["expect"]) { + let receiver_ty = self.typeck_results.expr_ty(&arglists[0][0]).peel_refs(); + if is_type_diagnostic_item(self.lcx, receiver_ty, sym::Option) + || is_type_diagnostic_item(self.lcx, receiver_ty, sym::Result) + { + self.result.push(expr.span); + } + } + + // check for `unwrap` + if let Some(arglists) = method_chain_args(expr, &["unwrap"]) { + let receiver_ty = self.typeck_results.expr_ty(&arglists[0][0]).peel_refs(); + if is_type_diagnostic_item(self.lcx, receiver_ty, sym::Option) + || is_type_diagnostic_item(self.lcx, receiver_ty, sym::Result) + { + self.result.push(expr.span); + } + } + + // and check sub-expressions + intravisit::walk_expr(self, expr); + } +} + +fn lint_impl_body<'tcx>(cx: &LateContext<'tcx>, impl_span: Span, impl_item: &'tcx hir::ImplItem<'_>) { + if let ImplItemKind::Fn(_, body_id) = impl_item.kind { + let body = cx.tcx.hir().body(body_id); + let mut fpu = FindExpectUnwrap { + lcx: cx, + typeck_results: cx.tcx.typeck(impl_item.def_id), + result: Vec::new(), + }; + fpu.visit_expr(&body.value); + + // if we've found one, lint + if !fpu.result.is_empty() { + span_lint_and_then( + cx, + UNWRAP_IN_RESULT, + impl_span, + "used unwrap or expect in a function that returns result or option", + move |diag| { + diag.help("unwrap and expect should not be used in a function that returns result or option"); + diag.span_note(fpu.result, "potential non-recoverable error(s)"); + }, + ); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/upper_case_acronyms.rs b/src/tools/clippy/clippy_lints/src/upper_case_acronyms.rs new file mode 100644 index 000000000..02bf09ed5 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/upper_case_acronyms.rs @@ -0,0 +1,127 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use itertools::Itertools; +use rustc_errors::Applicability; +use rustc_hir::{Item, ItemKind}; +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::Ident; + +declare_clippy_lint! { + /// ### What it does + /// Checks for fully capitalized names and optionally names containing a capitalized acronym. + /// + /// ### Why is this bad? + /// In CamelCase, acronyms count as one word. + /// See [naming conventions](https://rust-lang.github.io/api-guidelines/naming.html#casing-conforms-to-rfc-430-c-case) + /// for more. + /// + /// By default, the lint only triggers on fully-capitalized names. + /// You can use the `upper-case-acronyms-aggressive: true` config option to enable linting + /// on all camel case names + /// + /// ### Known problems + /// When two acronyms are contiguous, the lint can't tell where + /// the first acronym ends and the second starts, so it suggests to lowercase all of + /// the letters in the second acronym. + /// + /// ### Example + /// ```rust + /// struct HTTPResponse; + /// ``` + /// Use instead: + /// ```rust + /// struct HttpResponse; + /// ``` + #[clippy::version = "1.51.0"] + pub UPPER_CASE_ACRONYMS, + style, + "capitalized acronyms are against the naming convention" +} + +#[derive(Default)] +pub struct UpperCaseAcronyms { + avoid_breaking_exported_api: bool, + upper_case_acronyms_aggressive: bool, +} + +impl UpperCaseAcronyms { + pub fn new(avoid_breaking_exported_api: bool, aggressive: bool) -> Self { + Self { + avoid_breaking_exported_api, + upper_case_acronyms_aggressive: aggressive, + } + } +} + +impl_lint_pass!(UpperCaseAcronyms => [UPPER_CASE_ACRONYMS]); + +fn correct_ident(ident: &str) -> String { + let ident = ident.chars().rev().collect::<String>(); + let fragments = ident + .split_inclusive(|x: char| !x.is_ascii_lowercase()) + .rev() + .map(|x| x.chars().rev().collect::<String>()); + + let mut ident = fragments.clone().next().unwrap(); + for (ref prev, ref curr) in fragments.tuple_windows() { + if [prev, curr] + .iter() + .all(|s| s.len() == 1 && s.chars().next().unwrap().is_ascii_uppercase()) + { + ident.push_str(&curr.to_ascii_lowercase()); + } else { + ident.push_str(curr); + } + } + ident +} + +fn check_ident(cx: &LateContext<'_>, ident: &Ident, be_aggressive: bool) { + let span = ident.span; + let ident = ident.as_str(); + let corrected = correct_ident(ident); + // warn if we have pure-uppercase idents + // assume that two-letter words are some kind of valid abbreviation like FP for false positive + // (and don't warn) + if (ident.chars().all(|c| c.is_ascii_uppercase()) && ident.len() > 2) + // otherwise, warn if we have SOmeTHING lIKE THIs but only warn with the aggressive + // upper-case-acronyms-aggressive config option enabled + || (be_aggressive && ident != corrected) + { + span_lint_and_sugg( + cx, + UPPER_CASE_ACRONYMS, + span, + &format!("name `{}` contains a capitalized acronym", ident), + "consider making the acronym lowercase, except the initial letter", + corrected, + Applicability::MaybeIncorrect, + ); + } +} + +impl LateLintPass<'_> for UpperCaseAcronyms { + fn check_item(&mut self, cx: &LateContext<'_>, it: &Item<'_>) { + // do not lint public items or in macros + if in_external_macro(cx.sess(), it.span) + || (self.avoid_breaking_exported_api && cx.access_levels.is_exported(it.def_id)) + { + return; + } + match it.kind { + ItemKind::TyAlias(..) | ItemKind::Struct(..) | ItemKind::Trait(..) => { + check_ident(cx, &it.ident, self.upper_case_acronyms_aggressive); + }, + ItemKind::Enum(ref enumdef, _) => { + // check enum variants separately because again we only want to lint on private enums and + // the fn check_variant does not know about the vis of the enum of its variants + enumdef + .variants + .iter() + .for_each(|variant| check_ident(cx, &variant.ident, self.upper_case_acronyms_aggressive)); + }, + _ => {}, + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/use_self.rs b/src/tools/clippy/clippy_lints/src/use_self.rs new file mode 100644 index 000000000..486ea5e5c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/use_self.rs @@ -0,0 +1,320 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::same_type_and_consts; +use clippy_utils::{meets_msrv, msrvs}; +use if_chain::if_chain; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_hir::{ + self as hir, + def::{CtorOf, DefKind, Res}, + def_id::LocalDefId, + intravisit::{walk_inf, walk_ty, Visitor}, + Expr, ExprKind, FnRetTy, FnSig, GenericArg, HirId, Impl, ImplItemKind, Item, ItemKind, Pat, PatKind, Path, QPath, + TyKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::Span; +use rustc_typeck::hir_ty_to_ty; + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary repetition of structure name when a + /// replacement with `Self` is applicable. + /// + /// ### Why is this bad? + /// Unnecessary repetition. Mixed use of `Self` and struct + /// name + /// feels inconsistent. + /// + /// ### Known problems + /// - Unaddressed false negative in fn bodies of trait implementations + /// - False positive with associated types in traits (#4140) + /// + /// ### Example + /// ```rust + /// struct Foo; + /// impl Foo { + /// fn new() -> Foo { + /// Foo {} + /// } + /// } + /// ``` + /// could be + /// ```rust + /// struct Foo; + /// impl Foo { + /// fn new() -> Self { + /// Self {} + /// } + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USE_SELF, + nursery, + "unnecessary structure name repetition whereas `Self` is applicable" +} + +#[derive(Default)] +pub struct UseSelf { + msrv: Option<RustcVersion>, + stack: Vec<StackItem>, +} + +impl UseSelf { + #[must_use] + pub fn new(msrv: Option<RustcVersion>) -> Self { + Self { + msrv, + ..Self::default() + } + } +} + +#[derive(Debug)] +enum StackItem { + Check { + impl_id: LocalDefId, + in_body: u32, + types_to_skip: FxHashSet<HirId>, + }, + NoCheck, +} + +impl_lint_pass!(UseSelf => [USE_SELF]); + +const SEGMENTS_MSG: &str = "segments should be composed of at least 1 element"; + +impl<'tcx> LateLintPass<'tcx> for UseSelf { + fn check_item(&mut self, _cx: &LateContext<'_>, item: &Item<'_>) { + if matches!(item.kind, ItemKind::OpaqueTy(_)) { + // skip over `ItemKind::OpaqueTy` in order to lint `foo() -> impl <..>` + return; + } + // We push the self types of `impl`s on a stack here. Only the top type on the stack is + // relevant for linting, since this is the self type of the `impl` we're currently in. To + // avoid linting on nested items, we push `StackItem::NoCheck` on the stack to signal, that + // we're in an `impl` or nested item, that we don't want to lint + let stack_item = if_chain! { + if let ItemKind::Impl(Impl { self_ty, .. }) = item.kind; + if let TyKind::Path(QPath::Resolved(_, item_path)) = self_ty.kind; + let parameters = &item_path.segments.last().expect(SEGMENTS_MSG).args; + if parameters.as_ref().map_or(true, |params| { + !params.parenthesized && !params.args.iter().any(|arg| matches!(arg, GenericArg::Lifetime(_))) + }); + then { + StackItem::Check { + impl_id: item.def_id, + in_body: 0, + types_to_skip: std::iter::once(self_ty.hir_id).collect(), + } + } else { + StackItem::NoCheck + } + }; + self.stack.push(stack_item); + } + + fn check_item_post(&mut self, _: &LateContext<'_>, item: &Item<'_>) { + if !matches!(item.kind, ItemKind::OpaqueTy(_)) { + self.stack.pop(); + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &hir::ImplItem<'_>) { + // We want to skip types in trait `impl`s that aren't declared as `Self` in the trait + // declaration. The collection of those types is all this method implementation does. + if_chain! { + if let ImplItemKind::Fn(FnSig { decl, .. }, ..) = impl_item.kind; + if let Some(&mut StackItem::Check { + impl_id, + ref mut types_to_skip, + .. + }) = self.stack.last_mut(); + if let Some(impl_trait_ref) = cx.tcx.impl_trait_ref(impl_id); + then { + // `self_ty` is the semantic self type of `impl <trait> for <type>`. This cannot be + // `Self`. + let self_ty = impl_trait_ref.self_ty(); + + // `trait_method_sig` is the signature of the function, how it is declared in the + // trait, not in the impl of the trait. + let trait_method = cx + .tcx + .associated_item(impl_item.def_id) + .trait_item_def_id + .expect("impl method matches a trait method"); + let trait_method_sig = cx.tcx.fn_sig(trait_method); + let trait_method_sig = cx.tcx.erase_late_bound_regions(trait_method_sig); + + // `impl_inputs_outputs` is an iterator over the types (`hir::Ty`) declared in the + // implementation of the trait. + let output_hir_ty = if let FnRetTy::Return(ty) = &decl.output { + Some(&**ty) + } else { + None + }; + let impl_inputs_outputs = decl.inputs.iter().chain(output_hir_ty); + + // `impl_hir_ty` (of type `hir::Ty`) represents the type written in the signature. + // + // `trait_sem_ty` (of type `ty::Ty`) is the semantic type for the signature in the + // trait declaration. This is used to check if `Self` was used in the trait + // declaration. + // + // If `any`where in the `trait_sem_ty` the `self_ty` was used verbatim (as opposed + // to `Self`), we want to skip linting that type and all subtypes of it. This + // avoids suggestions to e.g. replace `Vec<u8>` with `Vec<Self>`, in an `impl Trait + // for u8`, when the trait always uses `Vec<u8>`. + // + // See also https://github.com/rust-lang/rust-clippy/issues/2894. + for (impl_hir_ty, trait_sem_ty) in impl_inputs_outputs.zip(trait_method_sig.inputs_and_output) { + if trait_sem_ty.walk().any(|inner| inner == self_ty.into()) { + let mut visitor = SkipTyCollector::default(); + visitor.visit_ty(impl_hir_ty); + types_to_skip.extend(visitor.types_to_skip); + } + } + } + } + } + + fn check_body(&mut self, _: &LateContext<'_>, _: &hir::Body<'_>) { + // `hir_ty_to_ty` cannot be called in `Body`s or it will panic (sometimes). But in bodies + // we can use `cx.typeck_results.node_type(..)` to get the `ty::Ty` from a `hir::Ty`. + // However the `node_type()` method can *only* be called in bodies. + if let Some(&mut StackItem::Check { ref mut in_body, .. }) = self.stack.last_mut() { + *in_body = in_body.saturating_add(1); + } + } + + fn check_body_post(&mut self, _: &LateContext<'_>, _: &hir::Body<'_>) { + if let Some(&mut StackItem::Check { ref mut in_body, .. }) = self.stack.last_mut() { + *in_body = in_body.saturating_sub(1); + } + } + + fn check_ty(&mut self, cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>) { + if_chain! { + if !hir_ty.span.from_expansion(); + if meets_msrv(self.msrv, msrvs::TYPE_ALIAS_ENUM_VARIANTS); + if let Some(&StackItem::Check { + impl_id, + in_body, + ref types_to_skip, + }) = self.stack.last(); + if let TyKind::Path(QPath::Resolved(_, path)) = hir_ty.kind; + if !matches!(path.res, Res::SelfTy { .. } | Res::Def(DefKind::TyParam, _)); + if !types_to_skip.contains(&hir_ty.hir_id); + let ty = if in_body > 0 { + cx.typeck_results().node_type(hir_ty.hir_id) + } else { + hir_ty_to_ty(cx.tcx, hir_ty) + }; + if same_type_and_consts(ty, cx.tcx.type_of(impl_id)); + let hir = cx.tcx.hir(); + // prevents false positive on `#[derive(serde::Deserialize)]` + if !hir.span(hir.get_parent_node(hir_ty.hir_id)).in_derive_expansion(); + then { + span_lint(cx, hir_ty.span); + } + } + } + + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + if_chain! { + if !expr.span.from_expansion(); + if meets_msrv(self.msrv, msrvs::TYPE_ALIAS_ENUM_VARIANTS); + if let Some(&StackItem::Check { impl_id, .. }) = self.stack.last(); + if cx.typeck_results().expr_ty(expr) == cx.tcx.type_of(impl_id); + then {} else { return; } + } + match expr.kind { + ExprKind::Struct(QPath::Resolved(_, path), ..) => match path.res { + Res::SelfTy { .. } => (), + Res::Def(DefKind::Variant, _) => lint_path_to_variant(cx, path), + _ => span_lint(cx, path.span), + }, + // tuple struct instantiation (`Foo(arg)` or `Enum::Foo(arg)`) + ExprKind::Call(fun, _) => { + if let ExprKind::Path(QPath::Resolved(_, path)) = fun.kind { + if let Res::Def(DefKind::Ctor(ctor_of, _), ..) = path.res { + match ctor_of { + CtorOf::Variant => lint_path_to_variant(cx, path), + CtorOf::Struct => span_lint(cx, path.span), + } + } + } + }, + // unit enum variants (`Enum::A`) + ExprKind::Path(QPath::Resolved(_, path)) => lint_path_to_variant(cx, path), + _ => (), + } + } + + fn check_pat(&mut self, cx: &LateContext<'_>, pat: &Pat<'_>) { + if_chain! { + if !pat.span.from_expansion(); + if meets_msrv(self.msrv, msrvs::TYPE_ALIAS_ENUM_VARIANTS); + if let Some(&StackItem::Check { impl_id, .. }) = self.stack.last(); + // get the path from the pattern + if let PatKind::Path(QPath::Resolved(_, path)) + | PatKind::TupleStruct(QPath::Resolved(_, path), _, _) + | PatKind::Struct(QPath::Resolved(_, path), _, _) = pat.kind; + if cx.typeck_results().pat_ty(pat) == cx.tcx.type_of(impl_id); + then { + match path.res { + Res::Def(DefKind::Ctor(ctor_of, _), ..) => match ctor_of { + CtorOf::Variant => lint_path_to_variant(cx, path), + CtorOf::Struct => span_lint(cx, path.span), + }, + Res::Def(DefKind::Variant, ..) => lint_path_to_variant(cx, path), + Res::Def(DefKind::Struct, ..) => span_lint(cx, path.span), + _ => () + } + } + } + } + + extract_msrv_attr!(LateContext); +} + +#[derive(Default)] +struct SkipTyCollector { + types_to_skip: Vec<HirId>, +} + +impl<'tcx> Visitor<'tcx> for SkipTyCollector { + fn visit_infer(&mut self, inf: &hir::InferArg) { + self.types_to_skip.push(inf.hir_id); + + walk_inf(self, inf); + } + fn visit_ty(&mut self, hir_ty: &hir::Ty<'_>) { + self.types_to_skip.push(hir_ty.hir_id); + + walk_ty(self, hir_ty); + } +} + +fn span_lint(cx: &LateContext<'_>, span: Span) { + span_lint_and_sugg( + cx, + USE_SELF, + span, + "unnecessary structure name repetition", + "use the applicable keyword", + "Self".to_owned(), + Applicability::MachineApplicable, + ); +} + +fn lint_path_to_variant(cx: &LateContext<'_>, path: &Path<'_>) { + if let [.., self_seg, _variant] = path.segments { + let span = path + .span + .with_hi(self_seg.args().span_ext().unwrap_or(self_seg.ident.span).hi()); + span_lint(cx, span); + } +} diff --git a/src/tools/clippy/clippy_lints/src/useless_conversion.rs b/src/tools/clippy/clippy_lints/src/useless_conversion.rs new file mode 100644 index 000000000..fe29bf29d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/useless_conversion.rs @@ -0,0 +1,189 @@ +use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg}; +use clippy_utils::source::{snippet, snippet_with_macro_callsite}; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::{is_type_diagnostic_item, same_type_and_consts}; +use clippy_utils::{get_parent_expr, is_trait_method, match_def_path, paths}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, HirId, MatchSource}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `Into`, `TryInto`, `From`, `TryFrom`, or `IntoIter` calls + /// which uselessly convert to the same type. + /// + /// ### Why is this bad? + /// Redundant code. + /// + /// ### Example + /// ```rust + /// // format!() returns a `String` + /// let s: String = format!("hello").into(); + /// ``` + /// + /// Use instead: + /// ```rust + /// let s: String = format!("hello"); + /// ``` + #[clippy::version = "1.45.0"] + pub USELESS_CONVERSION, + complexity, + "calls to `Into`, `TryInto`, `From`, `TryFrom`, or `IntoIter` which perform useless conversions to the same type" +} + +#[derive(Default)] +pub struct UselessConversion { + try_desugar_arm: Vec<HirId>, +} + +impl_lint_pass!(UselessConversion => [USELESS_CONVERSION]); + +#[expect(clippy::too_many_lines)] +impl<'tcx> LateLintPass<'tcx> for UselessConversion { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if e.span.from_expansion() { + return; + } + + if Some(&e.hir_id) == self.try_desugar_arm.last() { + return; + } + + match e.kind { + ExprKind::Match(_, arms, MatchSource::TryDesugar) => { + let e = match arms[0].body.kind { + ExprKind::Ret(Some(e)) | ExprKind::Break(_, Some(e)) => e, + _ => return, + }; + if let ExprKind::Call(_, args) = e.kind { + self.try_desugar_arm.push(args[0].hir_id); + } + }, + + ExprKind::MethodCall(name, .., args, _) => { + if is_trait_method(cx, e, sym::Into) && name.ident.as_str() == "into" { + let a = cx.typeck_results().expr_ty(e); + let b = cx.typeck_results().expr_ty(&args[0]); + if same_type_and_consts(a, b) { + let sugg = snippet_with_macro_callsite(cx, args[0].span, "<expr>").to_string(); + span_lint_and_sugg( + cx, + USELESS_CONVERSION, + e.span, + &format!("useless conversion to the same type: `{}`", b), + "consider removing `.into()`", + sugg, + Applicability::MachineApplicable, // snippet + ); + } + } + if is_trait_method(cx, e, sym::IntoIterator) && name.ident.name == sym::into_iter { + if let Some(parent_expr) = get_parent_expr(cx, e) { + if let ExprKind::MethodCall(parent_name, ..) = parent_expr.kind { + if parent_name.ident.name != sym::into_iter { + return; + } + } + } + let a = cx.typeck_results().expr_ty(e); + let b = cx.typeck_results().expr_ty(&args[0]); + if same_type_and_consts(a, b) { + let sugg = snippet(cx, args[0].span, "<expr>").into_owned(); + span_lint_and_sugg( + cx, + USELESS_CONVERSION, + e.span, + &format!("useless conversion to the same type: `{}`", b), + "consider removing `.into_iter()`", + sugg, + Applicability::MachineApplicable, // snippet + ); + } + } + if_chain! { + if is_trait_method(cx, e, sym::TryInto) && name.ident.name == sym::try_into; + let a = cx.typeck_results().expr_ty(e); + let b = cx.typeck_results().expr_ty(&args[0]); + if is_type_diagnostic_item(cx, a, sym::Result); + if let ty::Adt(_, substs) = a.kind(); + if let Some(a_type) = substs.types().next(); + if same_type_and_consts(a_type, b); + + then { + span_lint_and_help( + cx, + USELESS_CONVERSION, + e.span, + &format!("useless conversion to the same type: `{}`", b), + None, + "consider removing `.try_into()`", + ); + } + } + }, + + ExprKind::Call(path, args) => { + if_chain! { + if args.len() == 1; + if let ExprKind::Path(ref qpath) = path.kind; + if let Some(def_id) = cx.qpath_res(qpath, path.hir_id).opt_def_id(); + then { + let a = cx.typeck_results().expr_ty(e); + let b = cx.typeck_results().expr_ty(&args[0]); + if_chain! { + if match_def_path(cx, def_id, &paths::TRY_FROM); + if is_type_diagnostic_item(cx, a, sym::Result); + if let ty::Adt(_, substs) = a.kind(); + if let Some(a_type) = substs.types().next(); + if same_type_and_consts(a_type, b); + + then { + let hint = format!("consider removing `{}()`", snippet(cx, path.span, "TryFrom::try_from")); + span_lint_and_help( + cx, + USELESS_CONVERSION, + e.span, + &format!("useless conversion to the same type: `{}`", b), + None, + &hint, + ); + } + } + + if_chain! { + if match_def_path(cx, def_id, &paths::FROM_FROM); + if same_type_and_consts(a, b); + + then { + let sugg = Sugg::hir_with_macro_callsite(cx, &args[0], "<expr>").maybe_par(); + let sugg_msg = + format!("consider removing `{}()`", snippet(cx, path.span, "From::from")); + span_lint_and_sugg( + cx, + USELESS_CONVERSION, + e.span, + &format!("useless conversion to the same type: `{}`", b), + &sugg_msg, + sugg.to_string(), + Applicability::MachineApplicable, // snippet + ); + } + } + } + } + }, + + _ => {}, + } + } + + fn check_expr_post(&mut self, _: &LateContext<'tcx>, e: &'tcx Expr<'_>) { + if Some(&e.hir_id) == self.try_desugar_arm.last() { + self.try_desugar_arm.pop(); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/utils/author.rs b/src/tools/clippy/clippy_lints/src/utils/author.rs new file mode 100644 index 000000000..c0726868f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/utils/author.rs @@ -0,0 +1,741 @@ +//! A group of attributes that can be attached to Rust code in order +//! to generate a clippy lint detecting said code automatically. + +use clippy_utils::{get_attr, higher}; +use rustc_ast::ast::{LitFloatType, LitKind}; +use rustc_ast::LitIntType; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir as hir; +use rustc_hir::{ArrayLen, Closure, ExprKind, FnRetTy, HirId, Lit, PatKind, QPath, StmtKind, TyKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::symbol::{Ident, Symbol}; +use std::fmt::{Display, Formatter, Write as _}; + +declare_clippy_lint! { + /// ### What it does + /// Generates clippy code that detects the offending pattern + /// + /// ### Example + /// ```rust,ignore + /// // ./tests/ui/my_lint.rs + /// fn foo() { + /// // detect the following pattern + /// #[clippy::author] + /// if x == 42 { + /// // but ignore everything from here on + /// #![clippy::author = "ignore"] + /// } + /// () + /// } + /// ``` + /// + /// Running `TESTNAME=ui/my_lint cargo uitest` will produce + /// a `./tests/ui/new_lint.stdout` file with the generated code: + /// + /// ```rust,ignore + /// // ./tests/ui/new_lint.stdout + /// if_chain! { + /// if let ExprKind::If(ref cond, ref then, None) = item.kind, + /// if let ExprKind::Binary(BinOp::Eq, ref left, ref right) = cond.kind, + /// if let ExprKind::Path(ref path) = left.kind, + /// if let ExprKind::Lit(ref lit) = right.kind, + /// if let LitKind::Int(42, _) = lit.node, + /// then { + /// // report your lint here + /// } + /// } + /// ``` + pub LINT_AUTHOR, + internal_warn, + "helper for writing lints" +} + +declare_lint_pass!(Author => [LINT_AUTHOR]); + +/// Writes a line of output with indentation added +macro_rules! out { + ($($t:tt)*) => { + println!(" {}", format_args!($($t)*)) + }; +} + +/// The variables passed in are replaced with `&Binding`s where the `value` field is set +/// to the original value of the variable. The `name` field is set to the name of the variable +/// (using `stringify!`) and is adjusted to avoid duplicate names. +/// Note that the `Binding` may be printed directly to output the `name`. +macro_rules! bind { + ($self:ident $(, $name:ident)+) => { + $(let $name = & $self.bind(stringify!($name), $name);)+ + }; +} + +/// Transforms the given `Option<T>` variables into `OptionPat<Binding<T>>`. +/// This displays as `Some($name)` or `None` when printed. The name of the inner binding +/// is set to the name of the variable passed to the macro. +macro_rules! opt_bind { + ($self:ident $(, $name:ident)+) => { + $(let $name = OptionPat::new($name.map(|o| $self.bind(stringify!($name), o)));)+ + }; +} + +/// Creates a `Binding` that accesses the field of an existing `Binding` +macro_rules! field { + ($binding:ident.$field:ident) => { + &Binding { + name: $binding.name.to_string() + stringify!(.$field), + value: $binding.value.$field, + } + }; +} + +fn prelude() { + println!("if_chain! {{"); +} + +fn done() { + println!(" then {{"); + println!(" // report your lint here"); + println!(" }}"); + println!("}}"); +} + +impl<'tcx> LateLintPass<'tcx> for Author { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + check_item(cx, item.hir_id()); + } + + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) { + check_item(cx, item.hir_id()); + } + + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) { + check_item(cx, item.hir_id()); + } + + fn check_arm(&mut self, cx: &LateContext<'tcx>, arm: &'tcx hir::Arm<'_>) { + check_node(cx, arm.hir_id, |v| { + v.arm(&v.bind("arm", arm)); + }); + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + check_node(cx, expr.hir_id, |v| { + v.expr(&v.bind("expr", expr)); + }); + } + + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx hir::Stmt<'_>) { + match stmt.kind { + StmtKind::Expr(e) | StmtKind::Semi(e) if has_attr(cx, e.hir_id) => return, + _ => {}, + } + check_node(cx, stmt.hir_id, |v| { + v.stmt(&v.bind("stmt", stmt)); + }); + } +} + +fn check_item(cx: &LateContext<'_>, hir_id: HirId) { + let hir = cx.tcx.hir(); + if let Some(body_id) = hir.maybe_body_owned_by(hir_id.expect_owner()) { + check_node(cx, hir_id, |v| { + v.expr(&v.bind("expr", &hir.body(body_id).value)); + }); + } +} + +fn check_node(cx: &LateContext<'_>, hir_id: HirId, f: impl Fn(&PrintVisitor<'_, '_>)) { + if has_attr(cx, hir_id) { + prelude(); + f(&PrintVisitor::new(cx)); + done(); + } +} + +struct Binding<T> { + name: String, + value: T, +} + +impl<T> Display for Binding<T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.name) + } +} + +struct OptionPat<T> { + pub opt: Option<T>, +} + +impl<T> OptionPat<T> { + fn new(opt: Option<T>) -> Self { + Self { opt } + } + + fn if_some(&self, f: impl Fn(&T)) { + if let Some(t) = &self.opt { + f(t); + } + } +} + +impl<T: Display> Display for OptionPat<T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.opt { + None => f.write_str("None"), + Some(node) => write!(f, "Some({node})"), + } + } +} + +struct PrintVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + /// Fields are the current index that needs to be appended to pattern + /// binding names + ids: std::cell::Cell<FxHashMap<&'static str, u32>>, +} + +#[allow(clippy::unused_self)] +impl<'a, 'tcx> PrintVisitor<'a, 'tcx> { + fn new(cx: &'a LateContext<'tcx>) -> Self { + Self { + cx, + ids: std::cell::Cell::default(), + } + } + + fn next(&self, s: &'static str) -> String { + let mut ids = self.ids.take(); + let out = match *ids.entry(s).and_modify(|n| *n += 1).or_default() { + // first usage of the name, use it as is + 0 => s.to_string(), + // append a number starting with 1 + n => format!("{s}{n}"), + }; + self.ids.set(ids); + out + } + + fn bind<T>(&self, name: &'static str, value: T) -> Binding<T> { + let name = self.next(name); + Binding { name, value } + } + + fn option<T: Copy>(&self, option: &Binding<Option<T>>, name: &'static str, f: impl Fn(&Binding<T>)) { + match option.value { + None => out!("if {option}.is_none();"), + Some(value) => { + let value = &self.bind(name, value); + out!("if let Some({value}) = {option};"); + f(value); + }, + } + } + + fn slice<T>(&self, slice: &Binding<&[T]>, f: impl Fn(&Binding<&T>)) { + if slice.value.is_empty() { + out!("if {slice}.is_empty();"); + } else { + out!("if {slice}.len() == {};", slice.value.len()); + for (i, value) in slice.value.iter().enumerate() { + let name = format!("{slice}[{i}]"); + f(&Binding { name, value }); + } + } + } + + fn destination(&self, destination: &Binding<hir::Destination>) { + self.option(field!(destination.label), "label", |label| { + self.ident(field!(label.ident)); + }); + } + + fn ident(&self, ident: &Binding<Ident>) { + out!("if {ident}.as_str() == {:?};", ident.value.as_str()); + } + + fn symbol(&self, symbol: &Binding<Symbol>) { + out!("if {symbol}.as_str() == {:?};", symbol.value.as_str()); + } + + fn qpath(&self, qpath: &Binding<&QPath<'_>>) { + if let QPath::LangItem(lang_item, ..) = *qpath.value { + out!("if matches!({qpath}, QPath::LangItem(LangItem::{lang_item:?}, _));"); + } else { + out!("if match_qpath({qpath}, &[{}]);", path_to_string(qpath.value)); + } + } + + fn lit(&self, lit: &Binding<&Lit>) { + let kind = |kind| out!("if let LitKind::{kind} = {lit}.node;"); + macro_rules! kind { + ($($t:tt)*) => (kind(format_args!($($t)*))); + } + + match lit.value.node { + LitKind::Bool(val) => kind!("Bool({val:?})"), + LitKind::Char(c) => kind!("Char({c:?})"), + LitKind::Err(val) => kind!("Err({val})"), + LitKind::Byte(b) => kind!("Byte({b})"), + LitKind::Int(i, suffix) => { + let int_ty = match suffix { + LitIntType::Signed(int_ty) => format!("LitIntType::Signed(IntTy::{int_ty:?})"), + LitIntType::Unsigned(uint_ty) => format!("LitIntType::Unsigned(UintTy::{uint_ty:?})"), + LitIntType::Unsuffixed => String::from("LitIntType::Unsuffixed"), + }; + kind!("Int({i}, {int_ty})"); + }, + LitKind::Float(_, suffix) => { + let float_ty = match suffix { + LitFloatType::Suffixed(suffix_ty) => format!("LitFloatType::Suffixed(FloatTy::{suffix_ty:?})"), + LitFloatType::Unsuffixed => String::from("LitFloatType::Unsuffixed"), + }; + kind!("Float(_, {float_ty})"); + }, + LitKind::ByteStr(ref vec) => { + bind!(self, vec); + kind!("ByteStr(ref {vec})"); + out!("if let [{:?}] = **{vec};", vec.value); + }, + LitKind::Str(s, _) => { + bind!(self, s); + kind!("Str({s}, _)"); + self.symbol(s); + }, + } + } + + fn arm(&self, arm: &Binding<&hir::Arm<'_>>) { + self.pat(field!(arm.pat)); + match arm.value.guard { + None => out!("if {arm}.guard.is_none();"), + Some(hir::Guard::If(expr)) => { + bind!(self, expr); + out!("if let Some(Guard::If({expr})) = {arm}.guard;"); + self.expr(expr); + }, + Some(hir::Guard::IfLet(let_expr)) => { + bind!(self, let_expr); + out!("if let Some(Guard::IfLet({let_expr}) = {arm}.guard;"); + self.pat(field!(let_expr.pat)); + self.expr(field!(let_expr.init)); + }, + } + self.expr(field!(arm.body)); + } + + #[allow(clippy::too_many_lines)] + fn expr(&self, expr: &Binding<&hir::Expr<'_>>) { + if let Some(higher::While { condition, body }) = higher::While::hir(expr.value) { + bind!(self, condition, body); + out!( + "if let Some(higher::While {{ condition: {condition}, body: {body} }}) \ + = higher::While::hir({expr});" + ); + self.expr(condition); + self.expr(body); + return; + } + + if let Some(higher::WhileLet { + let_pat, + let_expr, + if_then, + }) = higher::WhileLet::hir(expr.value) + { + bind!(self, let_pat, let_expr, if_then); + out!( + "if let Some(higher::WhileLet {{ let_pat: {let_pat}, let_expr: {let_expr}, if_then: {if_then} }}) \ + = higher::WhileLet::hir({expr});" + ); + self.pat(let_pat); + self.expr(let_expr); + self.expr(if_then); + return; + } + + if let Some(higher::ForLoop { pat, arg, body, .. }) = higher::ForLoop::hir(expr.value) { + bind!(self, pat, arg, body); + out!( + "if let Some(higher::ForLoop {{ pat: {pat}, arg: {arg}, body: {body}, .. }}) \ + = higher::ForLoop::hir({expr});" + ); + self.pat(pat); + self.expr(arg); + self.expr(body); + return; + } + + let kind = |kind| out!("if let ExprKind::{kind} = {expr}.kind;"); + macro_rules! kind { + ($($t:tt)*) => (kind(format_args!($($t)*))); + } + + match expr.value.kind { + ExprKind::Let(let_expr) => { + bind!(self, let_expr); + kind!("Let({let_expr})"); + self.pat(field!(let_expr.pat)); + // Does what ExprKind::Cast does, only adds a clause for the type + // if it's a path + if let Some(TyKind::Path(ref qpath)) = let_expr.value.ty.as_ref().map(|ty| &ty.kind) { + bind!(self, qpath); + out!("if let TyKind::Path(ref {qpath}) = {let_expr}.ty.kind;"); + self.qpath(qpath); + } + self.expr(field!(let_expr.init)); + }, + ExprKind::Box(inner) => { + bind!(self, inner); + kind!("Box({inner})"); + self.expr(inner); + }, + ExprKind::Array(elements) => { + bind!(self, elements); + kind!("Array({elements})"); + self.slice(elements, |e| self.expr(e)); + }, + ExprKind::Call(func, args) => { + bind!(self, func, args); + kind!("Call({func}, {args})"); + self.expr(func); + self.slice(args, |e| self.expr(e)); + }, + ExprKind::MethodCall(method_name, args, _) => { + bind!(self, method_name, args); + kind!("MethodCall({method_name}, {args}, _)"); + self.ident(field!(method_name.ident)); + self.slice(args, |e| self.expr(e)); + }, + ExprKind::Tup(elements) => { + bind!(self, elements); + kind!("Tup({elements})"); + self.slice(elements, |e| self.expr(e)); + }, + ExprKind::Binary(op, left, right) => { + bind!(self, op, left, right); + kind!("Binary({op}, {left}, {right})"); + out!("if BinOpKind::{:?} == {op}.node;", op.value.node); + self.expr(left); + self.expr(right); + }, + ExprKind::Unary(op, inner) => { + bind!(self, inner); + kind!("Unary(UnOp::{op:?}, {inner})"); + self.expr(inner); + }, + ExprKind::Lit(ref lit) => { + bind!(self, lit); + kind!("Lit(ref {lit})"); + self.lit(lit); + }, + ExprKind::Cast(expr, cast_ty) => { + bind!(self, expr, cast_ty); + kind!("Cast({expr}, {cast_ty})"); + if let TyKind::Path(ref qpath) = cast_ty.value.kind { + bind!(self, qpath); + out!("if let TyKind::Path(ref {qpath}) = {cast_ty}.kind;"); + self.qpath(qpath); + } + self.expr(expr); + }, + ExprKind::Type(expr, _ty) => { + bind!(self, expr); + kind!("Type({expr}, _)"); + self.expr(expr); + }, + ExprKind::Loop(body, label, des, _) => { + bind!(self, body); + opt_bind!(self, label); + kind!("Loop({body}, {label}, LoopSource::{des:?}, _)"); + self.block(body); + label.if_some(|l| self.ident(field!(l.ident))); + }, + ExprKind::If(cond, then, else_expr) => { + bind!(self, cond, then); + opt_bind!(self, else_expr); + kind!("If({cond}, {then}, {else_expr})"); + self.expr(cond); + self.expr(then); + else_expr.if_some(|e| self.expr(e)); + }, + ExprKind::Match(scrutinee, arms, des) => { + bind!(self, scrutinee, arms); + kind!("Match({scrutinee}, {arms}, MatchSource::{des:?})"); + self.expr(scrutinee); + self.slice(arms, |arm| self.arm(arm)); + }, + ExprKind::Closure(&Closure { + capture_clause, + fn_decl, + body: body_id, + movability, + .. + }) => { + let movability = OptionPat::new(movability.map(|m| format!("Movability::{m:?}"))); + + let ret_ty = match fn_decl.output { + FnRetTy::DefaultReturn(_) => "FnRetTy::DefaultReturn(_)", + FnRetTy::Return(_) => "FnRetTy::Return(_ty)", + }; + + bind!(self, fn_decl, body_id); + kind!("Closure(CaptureBy::{capture_clause:?}, {fn_decl}, {body_id}, _, {movability})"); + out!("if let {ret_ty} = {fn_decl}.output;"); + self.body(body_id); + }, + ExprKind::Yield(sub, source) => { + bind!(self, sub); + kind!("Yield(sub, YieldSource::{source:?})"); + self.expr(sub); + }, + ExprKind::Block(block, label) => { + bind!(self, block); + opt_bind!(self, label); + kind!("Block({block}, {label})"); + self.block(block); + label.if_some(|l| self.ident(field!(l.ident))); + }, + ExprKind::Assign(target, value, _) => { + bind!(self, target, value); + kind!("Assign({target}, {value}, _span)"); + self.expr(target); + self.expr(value); + }, + ExprKind::AssignOp(op, target, value) => { + bind!(self, op, target, value); + kind!("AssignOp({op}, {target}, {value})"); + out!("if BinOpKind::{:?} == {op}.node;", op.value.node); + self.expr(target); + self.expr(value); + }, + ExprKind::Field(object, field_name) => { + bind!(self, object, field_name); + kind!("Field({object}, {field_name})"); + self.ident(field_name); + self.expr(object); + }, + ExprKind::Index(object, index) => { + bind!(self, object, index); + kind!("Index({object}, {index})"); + self.expr(object); + self.expr(index); + }, + ExprKind::Path(ref qpath) => { + bind!(self, qpath); + kind!("Path(ref {qpath})"); + self.qpath(qpath); + }, + ExprKind::AddrOf(kind, mutability, inner) => { + bind!(self, inner); + kind!("AddrOf(BorrowKind::{kind:?}, Mutability::{mutability:?}, {inner})"); + self.expr(inner); + }, + ExprKind::Break(destination, value) => { + bind!(self, destination); + opt_bind!(self, value); + kind!("Break({destination}, {value})"); + self.destination(destination); + value.if_some(|e| self.expr(e)); + }, + ExprKind::Continue(destination) => { + bind!(self, destination); + kind!("Continue({destination})"); + self.destination(destination); + }, + ExprKind::Ret(value) => { + opt_bind!(self, value); + kind!("Ret({value})"); + value.if_some(|e| self.expr(e)); + }, + ExprKind::InlineAsm(_) => { + kind!("InlineAsm(_)"); + out!("// unimplemented: `ExprKind::InlineAsm` is not further destructured at the moment"); + }, + ExprKind::Struct(qpath, fields, base) => { + bind!(self, qpath, fields); + opt_bind!(self, base); + kind!("Struct({qpath}, {fields}, {base})"); + self.qpath(qpath); + self.slice(fields, |field| { + self.ident(field!(field.ident)); + self.expr(field!(field.expr)); + }); + base.if_some(|e| self.expr(e)); + }, + ExprKind::ConstBlock(_) => kind!("ConstBlock(_)"), + ExprKind::Repeat(value, length) => { + bind!(self, value, length); + kind!("Repeat({value}, {length})"); + self.expr(value); + match length.value { + ArrayLen::Infer(..) => out!("if let ArrayLen::Infer(..) = length;"), + ArrayLen::Body(anon_const) => { + bind!(self, anon_const); + out!("if let ArrayLen::Body({anon_const}) = {length};"); + self.body(field!(anon_const.body)); + }, + } + }, + ExprKind::Err => kind!("Err"), + ExprKind::DropTemps(expr) => { + bind!(self, expr); + kind!("DropTemps({expr})"); + self.expr(expr); + }, + } + } + + fn block(&self, block: &Binding<&hir::Block<'_>>) { + self.slice(field!(block.stmts), |stmt| self.stmt(stmt)); + self.option(field!(block.expr), "trailing_expr", |expr| { + self.expr(expr); + }); + } + + fn body(&self, body_id: &Binding<hir::BodyId>) { + let expr = &self.cx.tcx.hir().body(body_id.value).value; + bind!(self, expr); + out!("let {expr} = &cx.tcx.hir().body({body_id}).value;"); + self.expr(expr); + } + + fn pat(&self, pat: &Binding<&hir::Pat<'_>>) { + let kind = |kind| out!("if let PatKind::{kind} = {pat}.kind;"); + macro_rules! kind { + ($($t:tt)*) => (kind(format_args!($($t)*))); + } + + match pat.value.kind { + PatKind::Wild => kind!("Wild"), + PatKind::Binding(anno, .., name, sub) => { + bind!(self, name); + opt_bind!(self, sub); + kind!("Binding(BindingAnnotation::{anno:?}, _, {name}, {sub})"); + self.ident(name); + sub.if_some(|p| self.pat(p)); + }, + PatKind::Struct(ref qpath, fields, ignore) => { + bind!(self, qpath, fields); + kind!("Struct(ref {qpath}, {fields}, {ignore})"); + self.qpath(qpath); + self.slice(fields, |field| { + self.ident(field!(field.ident)); + self.pat(field!(field.pat)); + }); + }, + PatKind::Or(fields) => { + bind!(self, fields); + kind!("Or({fields})"); + self.slice(fields, |pat| self.pat(pat)); + }, + PatKind::TupleStruct(ref qpath, fields, skip_pos) => { + bind!(self, qpath, fields); + kind!("TupleStruct(ref {qpath}, {fields}, {skip_pos:?})"); + self.qpath(qpath); + self.slice(fields, |pat| self.pat(pat)); + }, + PatKind::Path(ref qpath) => { + bind!(self, qpath); + kind!("Path(ref {qpath})"); + self.qpath(qpath); + }, + PatKind::Tuple(fields, skip_pos) => { + bind!(self, fields); + kind!("Tuple({fields}, {skip_pos:?})"); + self.slice(fields, |field| self.pat(field)); + }, + PatKind::Box(pat) => { + bind!(self, pat); + kind!("Box({pat})"); + self.pat(pat); + }, + PatKind::Ref(pat, muta) => { + bind!(self, pat); + kind!("Ref({pat}, Mutability::{muta:?})"); + self.pat(pat); + }, + PatKind::Lit(lit_expr) => { + bind!(self, lit_expr); + kind!("Lit({lit_expr})"); + self.expr(lit_expr); + }, + PatKind::Range(start, end, end_kind) => { + opt_bind!(self, start, end); + kind!("Range({start}, {end}, RangeEnd::{end_kind:?})"); + start.if_some(|e| self.expr(e)); + end.if_some(|e| self.expr(e)); + }, + PatKind::Slice(start, middle, end) => { + bind!(self, start, end); + opt_bind!(self, middle); + kind!("Slice({start}, {middle}, {end})"); + middle.if_some(|p| self.pat(p)); + self.slice(start, |pat| self.pat(pat)); + self.slice(end, |pat| self.pat(pat)); + }, + } + } + + fn stmt(&self, stmt: &Binding<&hir::Stmt<'_>>) { + let kind = |kind| out!("if let StmtKind::{kind} = {stmt}.kind;"); + macro_rules! kind { + ($($t:tt)*) => (kind(format_args!($($t)*))); + } + + match stmt.value.kind { + StmtKind::Local(local) => { + bind!(self, local); + kind!("Local({local})"); + self.option(field!(local.init), "init", |init| { + self.expr(init); + }); + self.pat(field!(local.pat)); + }, + StmtKind::Item(_) => kind!("Item(item_id)"), + StmtKind::Expr(e) => { + bind!(self, e); + kind!("Expr({e})"); + self.expr(e); + }, + StmtKind::Semi(e) => { + bind!(self, e); + kind!("Semi({e})"); + self.expr(e); + }, + } + } +} + +fn has_attr(cx: &LateContext<'_>, hir_id: hir::HirId) -> bool { + let attrs = cx.tcx.hir().attrs(hir_id); + get_attr(cx.sess(), attrs, "author").count() > 0 +} + +fn path_to_string(path: &QPath<'_>) -> String { + fn inner(s: &mut String, path: &QPath<'_>) { + match *path { + QPath::Resolved(_, path) => { + for (i, segment) in path.segments.iter().enumerate() { + if i > 0 { + *s += ", "; + } + write!(s, "{:?}", segment.ident.as_str()).unwrap(); + } + }, + QPath::TypeRelative(ty, segment) => match &ty.kind { + hir::TyKind::Path(inner_path) => { + inner(s, inner_path); + *s += ", "; + write!(s, "{:?}", segment.ident.as_str()).unwrap(); + }, + other => write!(s, "/* unimplemented: {:?}*/", other).unwrap(), + }, + QPath::LangItem(..) => panic!("path_to_string: called for lang item qpath"), + } + } + let mut s = String::new(); + inner(&mut s, path); + s +} diff --git a/src/tools/clippy/clippy_lints/src/utils/conf.rs b/src/tools/clippy/clippy_lints/src/utils/conf.rs new file mode 100644 index 000000000..6e033b3be --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/utils/conf.rs @@ -0,0 +1,534 @@ +//! Read configurations files. + +#![allow(clippy::module_name_repetitions)] + +use serde::de::{Deserializer, IgnoredAny, IntoDeserializer, MapAccess, Visitor}; +use serde::Deserialize; +use std::error::Error; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::{cmp, env, fmt, fs, io, iter}; + +#[rustfmt::skip] +const DEFAULT_DOC_VALID_IDENTS: &[&str] = &[ + "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", + "DirectX", + "ECMAScript", + "GPLv2", "GPLv3", + "GitHub", "GitLab", + "IPv4", "IPv6", + "ClojureScript", "CoffeeScript", "JavaScript", "PureScript", "TypeScript", + "NaN", "NaNs", + "OAuth", "GraphQL", + "OCaml", + "OpenGL", "OpenMP", "OpenSSH", "OpenSSL", "OpenStreetMap", "OpenDNS", + "WebGL", + "TensorFlow", + "TrueType", + "iOS", "macOS", "FreeBSD", + "TeX", "LaTeX", "BibTeX", "BibLaTeX", + "MinGW", + "CamelCase", +]; +const DEFAULT_BLACKLISTED_NAMES: &[&str] = &["foo", "baz", "quux"]; + +/// Holds information used by `MISSING_ENFORCED_IMPORT_RENAMES` lint. +#[derive(Clone, Debug, Deserialize)] +pub struct Rename { + pub path: String, + pub rename: String, +} + +/// A single disallowed method, used by the `DISALLOWED_METHODS` lint. +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum DisallowedMethod { + Simple(String), + WithReason { path: String, reason: Option<String> }, +} + +impl DisallowedMethod { + pub fn path(&self) -> &str { + let (Self::Simple(path) | Self::WithReason { path, .. }) = self; + + path + } +} + +/// A single disallowed type, used by the `DISALLOWED_TYPES` lint. +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum DisallowedType { + Simple(String), + WithReason { path: String, reason: Option<String> }, +} + +/// Conf with parse errors +#[derive(Default)] +pub struct TryConf { + pub conf: Conf, + pub errors: Vec<Box<dyn Error>>, +} + +impl TryConf { + fn from_error(error: impl Error + 'static) -> Self { + Self { + conf: Conf::default(), + errors: vec![Box::new(error)], + } + } +} + +#[derive(Debug)] +struct ConfError(String); + +impl fmt::Display for ConfError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + <String as fmt::Display>::fmt(&self.0, f) + } +} + +impl Error for ConfError {} + +fn conf_error(s: String) -> Box<dyn Error> { + Box::new(ConfError(s)) +} + +macro_rules! define_Conf { + ($( + $(#[doc = $doc:literal])+ + $(#[conf_deprecated($dep:literal)])? + ($name:ident: $ty:ty = $default:expr), + )*) => { + /// Clippy lint configuration + pub struct Conf { + $($(#[doc = $doc])+ pub $name: $ty,)* + } + + mod defaults { + $(pub fn $name() -> $ty { $default })* + } + + impl Default for Conf { + fn default() -> Self { + Self { $($name: defaults::$name(),)* } + } + } + + impl<'de> Deserialize<'de> for TryConf { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> { + deserializer.deserialize_map(ConfVisitor) + } + } + + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "kebab-case")] + #[allow(non_camel_case_types)] + enum Field { $($name,)* third_party, } + + struct ConfVisitor; + + impl<'de> Visitor<'de> for ConfVisitor { + type Value = TryConf; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Conf") + } + + fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> where V: MapAccess<'de> { + let mut errors = Vec::new(); + $(let mut $name = None;)* + // could get `Field` here directly, but get `str` first for diagnostics + while let Some(name) = map.next_key::<&str>()? { + match Field::deserialize(name.into_deserializer())? { + $(Field::$name => { + $(errors.push(conf_error(format!("deprecated field `{}`. {}", name, $dep)));)? + match map.next_value() { + Err(e) => errors.push(conf_error(e.to_string())), + Ok(value) => match $name { + Some(_) => errors.push(conf_error(format!("duplicate field `{}`", name))), + None => $name = Some(value), + } + } + })* + // white-listed; ignore + Field::third_party => drop(map.next_value::<IgnoredAny>()) + } + } + let conf = Conf { $($name: $name.unwrap_or_else(defaults::$name),)* }; + Ok(TryConf { conf, errors }) + } + } + + #[cfg(feature = "internal")] + pub mod metadata { + use crate::utils::internal_lints::metadata_collector::ClippyConfiguration; + + macro_rules! wrap_option { + () => (None); + ($x:literal) => (Some($x)); + } + + pub(crate) fn get_configuration_metadata() -> Vec<ClippyConfiguration> { + vec![ + $( + { + let deprecation_reason = wrap_option!($($dep)?); + + ClippyConfiguration::new( + stringify!($name), + stringify!($ty), + format!("{:?}", super::defaults::$name()), + concat!($($doc, '\n',)*), + deprecation_reason, + ) + }, + )+ + ] + } + } + }; +} + +define_Conf! { + /// Lint: Arithmetic. + /// + /// Suppress checking of the passed type names. + (arithmetic_allowed: 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. + /// + /// 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. + /// + /// The minimum rust version that the project supports + (msrv: Option<String> = None), + /// Lint: BLACKLISTED_NAME. + /// + /// The list of blacklisted names to lint about. NB: `bar` is not here since it has legitimate uses. 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 configuraction will replace the default value. + (blacklisted_names: Vec<String> = super::DEFAULT_BLACKLISTED_NAMES.iter().map(ToString::to_string).collect()), + /// Lint: COGNITIVE_COMPLEXITY. + /// + /// The maximum cognitive complexity a function can have + (cognitive_complexity_threshold: u64 = 25), + /// DEPRECATED LINT: CYCLOMATIC_COMPLEXITY. + /// + /// Use the Cognitive Complexity lint instead. + #[conf_deprecated("Please use `cognitive-complexity-threshold` instead")] + (cyclomatic_complexity_threshold: Option<u64> = None), + /// Lint: DOC_MARKDOWN. + /// + /// The list of words this lint should not consider as identifiers needing ticks. 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 configuraction will replace the default value. For example: + /// * `doc-valid-idents = ["ClipPy"]` would replace the default list with `["ClipPy"]`. + /// * `doc-valid-idents = ["ClipPy", ".."]` would append `ClipPy` to the default list. + /// + /// Default list: + (doc_valid_idents: Vec<String> = super::DEFAULT_DOC_VALID_IDENTS.iter().map(ToString::to_string).collect()), + /// Lint: TOO_MANY_ARGUMENTS. + /// + /// The maximum number of argument a function or method can have + (too_many_arguments_threshold: u64 = 7), + /// Lint: TYPE_COMPLEXITY. + /// + /// The maximum complexity a type can have + (type_complexity_threshold: u64 = 250), + /// Lint: MANY_SINGLE_CHAR_NAMES. + /// + /// The maximum number of single char bindings a scope may have + (single_char_binding_names_threshold: u64 = 4), + /// Lint: BOXED_LOCAL, USELESS_VEC. + /// + /// The maximum size of objects (in bytes) that will be linted. Larger objects are ok on the heap + (too_large_for_stack: u64 = 200), + /// Lint: ENUM_VARIANT_NAMES. + /// + /// The minimum number of enum variants for the lints about variant names to trigger + (enum_variant_name_threshold: u64 = 3), + /// Lint: LARGE_ENUM_VARIANT. + /// + /// The maximum size of an enum's variant to avoid box suggestion + (enum_variant_size_threshold: u64 = 200), + /// Lint: VERBOSE_BIT_MASK. + /// + /// The maximum allowed size of a bit mask before suggesting to use 'trailing_zeros' + (verbose_bit_mask_threshold: u64 = 1), + /// Lint: DECIMAL_LITERAL_REPRESENTATION. + /// + /// The lower bound for linting decimal literals + (literal_representation_threshold: u64 = 16384), + /// Lint: TRIVIALLY_COPY_PASS_BY_REF. + /// + /// The maximum size (in bytes) to consider a `Copy` type for passing by value instead of by reference. + (trivial_copy_size_limit: Option<u64> = None), + /// Lint: LARGE_TYPE_PASS_BY_MOVE. + /// + /// The minimum size (in bytes) to consider a type for passing by reference instead of by value. + (pass_by_value_size_limit: u64 = 256), + /// Lint: TOO_MANY_LINES. + /// + /// The maximum number of lines a function or method can have + (too_many_lines_threshold: u64 = 100), + /// Lint: LARGE_STACK_ARRAYS, LARGE_CONST_ARRAYS. + /// + /// The maximum allowed size for arrays on the stack + (array_size_threshold: u64 = 512_000), + /// Lint: VEC_BOX. + /// + /// The size of the boxed type in bytes, where boxing in a `Vec` is allowed + (vec_box_size_threshold: u64 = 4096), + /// Lint: TYPE_REPETITION_IN_BOUNDS. + /// + /// The maximum number of bounds a trait can have to be linted + (max_trait_bounds: u64 = 3), + /// Lint: STRUCT_EXCESSIVE_BOOLS. + /// + /// The maximum number of bool fields a struct can have + (max_struct_bools: u64 = 3), + /// Lint: FN_PARAMS_EXCESSIVE_BOOLS. + /// + /// The maximum number of bool parameters a function can have + (max_fn_params_bools: u64 = 3), + /// Lint: WILDCARD_IMPORTS. + /// + /// Whether to allow certain wildcard imports (prelude, super in tests). + (warn_on_all_wildcard_imports: bool = false), + /// Lint: DISALLOWED_METHODS. + /// + /// The list of disallowed methods, written as fully qualified paths. + (disallowed_methods: Vec<crate::utils::conf::DisallowedMethod> = Vec::new()), + /// Lint: DISALLOWED_TYPES. + /// + /// The list of disallowed types, written as fully qualified paths. + (disallowed_types: Vec<crate::utils::conf::DisallowedType> = Vec::new()), + /// Lint: UNREADABLE_LITERAL. + /// + /// Should the fraction of a decimal be linted to include separators. + (unreadable_literal_lint_fractions: bool = true), + /// Lint: UPPER_CASE_ACRONYMS. + /// + /// Enables verbose mode. Triggers if there is more than one uppercase char next to each other + (upper_case_acronyms_aggressive: bool = false), + /// Lint: _CARGO_COMMON_METADATA. + /// + /// For internal testing only, ignores the current `publish` settings in the Cargo manifest. + (cargo_ignore_publish: bool = false), + /// Lint: NONSTANDARD_MACRO_BRACES. + /// + /// Enforce the named macros always use the braces specified. + /// + /// A `MacroMatcher` can be added like so `{ name = "macro_name", brace = "(" }`. If the macro + /// is could be used with a full path two `MacroMatcher`s have to be added one with the full path + /// `crate_name::macro_name` and one with just the macro name. + (standard_macro_braces: Vec<crate::nonstandard_macro_braces::MacroMatcher> = Vec::new()), + /// Lint: MISSING_ENFORCED_IMPORT_RENAMES. + /// + /// The list of imports to always rename, a fully qualified path followed by the rename. + (enforced_import_renames: Vec<crate::utils::conf::Rename> = Vec::new()), + /// Lint: DISALLOWED_SCRIPT_IDENTS. + /// + /// The list of unicode scripts allowed to be used in the scope. + (allowed_scripts: Vec<String> = ["Latin"].iter().map(ToString::to_string).collect()), + /// Lint: NON_SEND_FIELDS_IN_SEND_TY. + /// + /// Whether to apply the raw pointer heuristic to determine if a type is `Send`. + (enable_raw_pointer_heuristic_for_send: bool = true), + /// Lint: INDEX_REFUTABLE_SLICE. + /// + /// When Clippy suggests using a slice pattern, this is the maximum number of elements allowed in + /// the slice pattern that is suggested. If more elements would be necessary, the lint is suppressed. + /// For example, `[_, _, _, e, ..]` is a slice pattern with 4 elements. + (max_suggested_slice_pattern_length: u64 = 3), + /// Lint: AWAIT_HOLDING_INVALID_TYPE + (await_holding_invalid_types: Vec<crate::utils::conf::DisallowedType> = Vec::new()), + /// Lint: LARGE_INCLUDE_FILE. + /// + /// The maximum size of a file included via `include_bytes!()` or `include_str!()`, in bytes + (max_include_file_size: u64 = 1_000_000), + /// Lint: EXPECT_USED. + /// + /// Whether `expect` should be allowed in test functions + (allow_expect_in_tests: bool = false), + /// Lint: UNWRAP_USED. + /// + /// Whether `unwrap` should be allowed in test functions + (allow_unwrap_in_tests: bool = false), + /// Lint: DBG_MACRO. + /// + /// Whether `dbg!` should be allowed in test functions + (allow_dbg_in_tests: bool = false), +} + +/// Search for the configuration file. +pub fn lookup_conf_file() -> io::Result<Option<PathBuf>> { + /// Possible filename to search for. + const CONFIG_FILE_NAMES: [&str; 2] = [".clippy.toml", "clippy.toml"]; + + // Start looking for a config file in CLIPPY_CONF_DIR, or failing that, CARGO_MANIFEST_DIR. + // If neither of those exist, use ".". + let mut current = env::var_os("CLIPPY_CONF_DIR") + .or_else(|| env::var_os("CARGO_MANIFEST_DIR")) + .map_or_else(|| PathBuf::from("."), PathBuf::from); + + let mut found_config: Option<PathBuf> = None; + + loop { + for config_file_name in &CONFIG_FILE_NAMES { + if let Ok(config_file) = current.join(config_file_name).canonicalize() { + match fs::metadata(&config_file) { + Err(e) if e.kind() == io::ErrorKind::NotFound => {}, + Err(e) => return Err(e), + Ok(md) if md.is_dir() => {}, + Ok(_) => { + // warn if we happen to find two config files #8323 + if let Some(ref found_config_) = found_config { + eprintln!( + "Using config file `{}`\nWarning: `{}` will be ignored.", + found_config_.display(), + config_file.display(), + ); + } else { + found_config = Some(config_file); + } + }, + } + } + } + + if found_config.is_some() { + return Ok(found_config); + } + + // If the current directory has no parent, we're done searching. + if !current.pop() { + return Ok(None); + } + } +} + +/// Read the `toml` configuration file. +/// +/// In case of error, the function tries to continue as much as possible. +pub fn read(path: &Path) -> TryConf { + let content = match fs::read_to_string(path) { + Err(e) => return TryConf::from_error(e), + Ok(content) => content, + }; + match toml::from_str::<TryConf>(&content) { + 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.blacklisted_names, DEFAULT_BLACKLISTED_NAMES); + + conf + }, + Err(e) => TryConf::from_error(e), + } +} + +fn extend_vec_if_indicator_present(vec: &mut Vec<String>, default: &[&str]) { + if vec.contains(&"..".to_string()) { + vec.extend(default.iter().map(ToString::to_string)); + } +} + +const SEPARATOR_WIDTH: usize = 4; + +// Check whether the error is "unknown field" and, if so, list the available fields sorted and at +// least one per line, more if `CLIPPY_TERMINAL_WIDTH` is set and allows it. +pub fn format_error(error: Box<dyn Error>) -> String { + let s = error.to_string(); + + if_chain! { + if error.downcast::<toml::de::Error>().is_ok(); + if let Some((prefix, mut fields, suffix)) = parse_unknown_field_message(&s); + then { + use fmt::Write; + + fields.sort_unstable(); + + let (rows, column_widths) = calculate_dimensions(&fields); + + let mut msg = String::from(prefix); + for row in 0..rows { + write!(msg, "\n").unwrap(); + for (column, column_width) in column_widths.iter().copied().enumerate() { + let index = column * rows + row; + let field = fields.get(index).copied().unwrap_or_default(); + write!( + msg, + "{:separator_width$}{:field_width$}", + " ", + field, + separator_width = SEPARATOR_WIDTH, + field_width = column_width + ) + .unwrap(); + } + } + write!(msg, "\n{}", suffix).unwrap(); + msg + } else { + s + } + } +} + +// `parse_unknown_field_message` will become unnecessary if +// https://github.com/alexcrichton/toml-rs/pull/364 is merged. +fn parse_unknown_field_message(s: &str) -> Option<(&str, Vec<&str>, &str)> { + // An "unknown field" message has the following form: + // unknown field `UNKNOWN`, expected one of `FIELD0`, `FIELD1`, ..., `FIELDN` at line X column Y + // ^^ ^^^^ ^^ + if_chain! { + if s.starts_with("unknown field"); + let slices = s.split("`, `").collect::<Vec<_>>(); + let n = slices.len(); + if n >= 2; + if let Some((prefix, first_field)) = slices[0].rsplit_once(" `"); + if let Some((last_field, suffix)) = slices[n - 1].split_once("` "); + then { + let fields = iter::once(first_field) + .chain(slices[1..n - 1].iter().copied()) + .chain(iter::once(last_field)) + .collect::<Vec<_>>(); + Some((prefix, fields, suffix)) + } else { + None + } + } +} + +fn calculate_dimensions(fields: &[&str]) -> (usize, Vec<usize>) { + let columns = env::var("CLIPPY_TERMINAL_WIDTH") + .ok() + .and_then(|s| <usize as FromStr>::from_str(&s).ok()) + .map_or(1, |terminal_width| { + let max_field_width = fields.iter().map(|field| field.len()).max().unwrap(); + cmp::max(1, terminal_width / (SEPARATOR_WIDTH + max_field_width)) + }); + + let rows = (fields.len() + (columns - 1)) / columns; + + let column_widths = (0..columns) + .map(|column| { + if column < columns - 1 { + (0..rows) + .map(|row| { + let index = column * rows + row; + let field = fields.get(index).copied().unwrap_or_default(); + field.len() + }) + .max() + .unwrap() + } else { + // Avoid adding extra space to the last column. + 0 + } + }) + .collect::<Vec<_>>(); + + (rows, column_widths) +} diff --git a/src/tools/clippy/clippy_lints/src/utils/dump_hir.rs b/src/tools/clippy/clippy_lints/src/utils/dump_hir.rs new file mode 100644 index 000000000..01efc527a --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/utils/dump_hir.rs @@ -0,0 +1,55 @@ +use clippy_utils::get_attr; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// It formats the attached node with `{:#?}` and writes the result to the + /// standard output. This is intended for debugging. + /// + /// ### Examples + /// ```rs + /// #[clippy::dump] + /// use std::mem; + /// + /// #[clippy::dump] + /// fn foo(input: u32) -> u64 { + /// input as u64 + /// } + /// ``` + pub DUMP_HIR, + internal_warn, + "helper to dump info about code" +} + +declare_lint_pass!(DumpHir => [DUMP_HIR]); + +impl<'tcx> LateLintPass<'tcx> for DumpHir { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { + if has_attr(cx, item.hir_id()) { + println!("{item:#?}"); + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if has_attr(cx, expr.hir_id) { + println!("{expr:#?}"); + } + } + + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx hir::Stmt<'_>) { + match stmt.kind { + hir::StmtKind::Expr(e) | hir::StmtKind::Semi(e) if has_attr(cx, e.hir_id) => return, + _ => {}, + } + if has_attr(cx, stmt.hir_id) { + println!("{stmt:#?}"); + } + } +} + +fn has_attr(cx: &LateContext<'_>, hir_id: hir::HirId) -> bool { + let attrs = cx.tcx.hir().attrs(hir_id); + get_attr(cx.sess(), attrs, "dump").count() > 0 +} diff --git a/src/tools/clippy/clippy_lints/src/utils/internal_lints.rs b/src/tools/clippy/clippy_lints/src/utils/internal_lints.rs new file mode 100644 index 000000000..b30965329 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/utils/internal_lints.rs @@ -0,0 +1,1436 @@ +use crate::utils::internal_lints::metadata_collector::is_deprecated_lint; +use clippy_utils::consts::{constant_simple, Constant}; +use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::macros::root_macro_call_first_node; +use clippy_utils::source::snippet; +use clippy_utils::ty::match_type; +use clippy_utils::{ + def_path_res, higher, is_else_clause, is_expn_of, is_expr_path_def_path, is_lint_allowed, match_def_path, + method_calls, paths, peel_blocks_with_stmt, SpanlessEq, +}; +use if_chain::if_chain; +use rustc_ast as ast; +use rustc_ast::ast::{Crate, ItemKind, LitKind, ModKind, NodeId}; +use rustc_ast::visit::FnKind; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::def_id::DefId; +use rustc_hir::hir_id::CRATE_HIR_ID; +use rustc_hir::intravisit::Visitor; +use rustc_hir::{ + BinOpKind, Block, Closure, Expr, ExprKind, HirId, Item, Local, MutTy, Mutability, Node, Path, Stmt, StmtKind, Ty, + TyKind, UnOp, +}; +use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext}; +use rustc_middle::hir::nested_filter; +use rustc_middle::mir::interpret::ConstValue; +use rustc_middle::ty::{self, fast_reject::SimplifiedTypeGen, subst::GenericArgKind, FloatTy}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_lint_pass, declare_tool_lint, impl_lint_pass}; +use rustc_span::source_map::Spanned; +use rustc_span::symbol::Symbol; +use rustc_span::{sym, BytePos, Span}; +use rustc_typeck::hir_ty_to_ty; + +use std::borrow::{Borrow, Cow}; + +#[cfg(feature = "internal")] +pub mod metadata_collector; + +declare_clippy_lint! { + /// ### What it does + /// Checks for various things we like to keep tidy in clippy. + /// + /// ### Why is this bad? + /// We like to pretend we're an example of tidy code. + /// + /// ### Example + /// Wrong ordering of the util::paths constants. + pub CLIPPY_LINTS_INTERNAL, + internal, + "various things that will negatively affect your clippy experience" +} + +declare_clippy_lint! { + /// ### What it does + /// Ensures every lint is associated to a `LintPass`. + /// + /// ### Why is this bad? + /// The compiler only knows lints via a `LintPass`. Without + /// putting a lint to a `LintPass::get_lints()`'s return, the compiler will not + /// know the name of the lint. + /// + /// ### Known problems + /// Only checks for lints associated using the + /// `declare_lint_pass!`, `impl_lint_pass!`, and `lint_array!` macros. + /// + /// ### Example + /// ```rust,ignore + /// declare_lint! { pub LINT_1, ... } + /// declare_lint! { pub LINT_2, ... } + /// declare_lint! { pub FORGOTTEN_LINT, ... } + /// // ... + /// declare_lint_pass!(Pass => [LINT_1, LINT_2]); + /// // missing FORGOTTEN_LINT + /// ``` + pub LINT_WITHOUT_LINT_PASS, + internal, + "declaring a lint without associating it in a LintPass" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `cx.span_lint*` and suggests to use the `utils::*` + /// variant of the function. + /// + /// ### Why is this bad? + /// The `utils::*` variants also add a link to the Clippy documentation to the + /// warning/error messages. + /// + /// ### Example + /// ```rust,ignore + /// cx.span_lint(LINT_NAME, "message"); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// utils::span_lint(cx, LINT_NAME, "message"); + /// ``` + pub COMPILER_LINT_FUNCTIONS, + internal, + "usage of the lint functions of the compiler instead of the utils::* variant" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `cx.outer().expn_data()` and suggests to use + /// the `cx.outer_expn_data()` + /// + /// ### Why is this bad? + /// `cx.outer_expn_data()` is faster and more concise. + /// + /// ### Example + /// ```rust,ignore + /// expr.span.ctxt().outer().expn_data() + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// expr.span.ctxt().outer_expn_data() + /// ``` + pub OUTER_EXPN_EXPN_DATA, + internal, + "using `cx.outer_expn().expn_data()` instead of `cx.outer_expn_data()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Not an actual lint. This lint is only meant for testing our customized internal compiler + /// error message by calling `panic`. + /// + /// ### Why is this bad? + /// ICE in large quantities can damage your teeth + /// + /// ### Example + /// ```rust,ignore + /// 🍦🍦🍦🍦🍦 + /// ``` + pub PRODUCE_ICE, + internal, + "this message should not appear anywhere as we ICE before and don't emit the lint" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for cases of an auto-generated lint without an updated description, + /// i.e. `default lint description`. + /// + /// ### Why is this bad? + /// Indicates that the lint is not finished. + /// + /// ### Example + /// ```rust,ignore + /// declare_lint! { pub COOL_LINT, nursery, "default lint description" } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// declare_lint! { pub COOL_LINT, nursery, "a great new lint" } + /// ``` + pub DEFAULT_LINT, + internal, + "found 'default lint description' in a lint declaration" +} + +declare_clippy_lint! { + /// ### What it does + /// Lints `span_lint_and_then` function calls, where the + /// closure argument has only one statement and that statement is a method + /// call to `span_suggestion`, `span_help`, `span_note` (using the same + /// span), `help` or `note`. + /// + /// These usages of `span_lint_and_then` should be replaced with one of the + /// wrapper functions `span_lint_and_sugg`, span_lint_and_help`, or + /// `span_lint_and_note`. + /// + /// ### Why is this bad? + /// Using the wrapper `span_lint_and_*` functions, is more + /// convenient, readable and less error prone. + /// + /// ### Example + /// ```rust,ignore + /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| { + /// diag.span_suggestion( + /// expr.span, + /// help_msg, + /// sugg.to_string(), + /// Applicability::MachineApplicable, + /// ); + /// }); + /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| { + /// diag.span_help(expr.span, help_msg); + /// }); + /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| { + /// diag.help(help_msg); + /// }); + /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| { + /// diag.span_note(expr.span, note_msg); + /// }); + /// span_lint_and_then(cx, TEST_LINT, expr.span, lint_msg, |diag| { + /// diag.note(note_msg); + /// }); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// span_lint_and_sugg( + /// cx, + /// TEST_LINT, + /// expr.span, + /// lint_msg, + /// help_msg, + /// sugg.to_string(), + /// Applicability::MachineApplicable, + /// ); + /// span_lint_and_help(cx, TEST_LINT, expr.span, lint_msg, Some(expr.span), help_msg); + /// span_lint_and_help(cx, TEST_LINT, expr.span, lint_msg, None, help_msg); + /// span_lint_and_note(cx, TEST_LINT, expr.span, lint_msg, Some(expr.span), note_msg); + /// span_lint_and_note(cx, TEST_LINT, expr.span, lint_msg, None, note_msg); + /// ``` + pub COLLAPSIBLE_SPAN_LINT_CALLS, + internal, + "found collapsible `span_lint_and_then` calls" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `utils::match_type()` on a type diagnostic item + /// and suggests to use `utils::is_type_diagnostic_item()` instead. + /// + /// ### Why is this bad? + /// `utils::is_type_diagnostic_item()` does not require hardcoded paths. + /// + /// ### Example + /// ```rust,ignore + /// utils::match_type(cx, ty, &paths::VEC) + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// utils::is_type_diagnostic_item(cx, ty, sym::Vec) + /// ``` + pub MATCH_TYPE_ON_DIAGNOSTIC_ITEM, + internal, + "using `utils::match_type()` instead of `utils::is_type_diagnostic_item()`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks the paths module for invalid paths. + /// + /// ### Why is this bad? + /// It indicates a bug in the code. + /// + /// ### Example + /// None. + pub INVALID_PATHS, + internal, + "invalid path" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for interning symbols that have already been pre-interned and defined as constants. + /// + /// ### Why is this bad? + /// It's faster and easier to use the symbol constant. + /// + /// ### Example + /// ```rust,ignore + /// let _ = sym!(f32); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// let _ = sym::f32; + /// ``` + pub INTERNING_DEFINED_SYMBOL, + internal, + "interning a symbol that is pre-interned and defined as a constant" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary conversion from Symbol to a string. + /// + /// ### Why is this bad? + /// It's faster use symbols directly instead of strings. + /// + /// ### Example + /// ```rust,ignore + /// symbol.as_str() == "clippy"; + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// symbol == sym::clippy; + /// ``` + pub UNNECESSARY_SYMBOL_STR, + internal, + "unnecessary conversion between Symbol and string" +} + +declare_clippy_lint! { + /// Finds unidiomatic usage of `if_chain!` + pub IF_CHAIN_STYLE, + internal, + "non-idiomatic `if_chain!` usage" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for invalid `clippy::version` attributes. + /// + /// Valid values are: + /// * "pre 1.29.0" + /// * any valid semantic version + pub INVALID_CLIPPY_VERSION_ATTRIBUTE, + internal, + "found an invalid `clippy::version` attribute" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for declared clippy lints without the `clippy::version` attribute. + /// + pub MISSING_CLIPPY_VERSION_ATTRIBUTE, + internal, + "found clippy lint without `clippy::version` attribute" +} + +declare_clippy_lint! { + /// ### What it does + /// Check that the `extract_msrv_attr!` macro is used, when a lint has a MSRV. + /// + pub MISSING_MSRV_ATTR_IMPL, + internal, + "checking if all necessary steps were taken when adding a MSRV to a lint" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for cases of an auto-generated deprecated lint without an updated reason, + /// i.e. `"default deprecation note"`. + /// + /// ### Why is this bad? + /// Indicates that the documentation is incomplete. + /// + /// ### Example + /// ```rust,ignore + /// declare_deprecated_lint! { + /// /// ### What it does + /// /// Nothing. This lint has been deprecated. + /// /// + /// /// ### Deprecation reason + /// /// TODO + /// #[clippy::version = "1.63.0"] + /// pub COOL_LINT, + /// "default deprecation note" + /// } + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// declare_deprecated_lint! { + /// /// ### What it does + /// /// Nothing. This lint has been deprecated. + /// /// + /// /// ### Deprecation reason + /// /// This lint has been replaced by `cooler_lint` + /// #[clippy::version = "1.63.0"] + /// pub COOL_LINT, + /// "this lint has been replaced by `cooler_lint`" + /// } + /// ``` + pub DEFAULT_DEPRECATION_REASON, + internal, + "found 'default deprecation note' in a deprecated lint declaration" +} + +declare_lint_pass!(ClippyLintsInternal => [CLIPPY_LINTS_INTERNAL]); + +impl EarlyLintPass for ClippyLintsInternal { + fn check_crate(&mut self, cx: &EarlyContext<'_>, krate: &Crate) { + if let Some(utils) = krate.items.iter().find(|item| item.ident.name.as_str() == "utils") { + if let ItemKind::Mod(_, ModKind::Loaded(ref items, ..)) = utils.kind { + if let Some(paths) = items.iter().find(|item| item.ident.name.as_str() == "paths") { + if let ItemKind::Mod(_, ModKind::Loaded(ref items, ..)) = paths.kind { + let mut last_name: Option<&str> = None; + for item in items { + let name = item.ident.as_str(); + if let Some(last_name) = last_name { + if *last_name > *name { + span_lint( + cx, + CLIPPY_LINTS_INTERNAL, + item.span, + "this constant should be before the previous constant due to lexical \ + ordering", + ); + } + } + last_name = Some(name); + } + } + } + } + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct LintWithoutLintPass { + declared_lints: FxHashMap<Symbol, Span>, + registered_lints: FxHashSet<Symbol>, +} + +impl_lint_pass!(LintWithoutLintPass => [DEFAULT_LINT, LINT_WITHOUT_LINT_PASS, INVALID_CLIPPY_VERSION_ATTRIBUTE, MISSING_CLIPPY_VERSION_ATTRIBUTE, DEFAULT_DEPRECATION_REASON]); + +impl<'tcx> LateLintPass<'tcx> for LintWithoutLintPass { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if is_lint_allowed(cx, DEFAULT_LINT, item.hir_id()) + || is_lint_allowed(cx, DEFAULT_DEPRECATION_REASON, item.hir_id()) + { + return; + } + + if let hir::ItemKind::Static(ty, Mutability::Not, body_id) = item.kind { + let is_lint_ref_ty = is_lint_ref_type(cx, ty); + if is_deprecated_lint(cx, ty) || is_lint_ref_ty { + check_invalid_clippy_version_attribute(cx, item); + + let expr = &cx.tcx.hir().body(body_id).value; + let fields; + if is_lint_ref_ty { + if let ExprKind::AddrOf(_, _, inner_exp) = expr.kind + && let ExprKind::Struct(_, struct_fields, _) = inner_exp.kind { + fields = struct_fields; + } else { + return; + } + } else if let ExprKind::Struct(_, struct_fields, _) = expr.kind { + fields = struct_fields; + } else { + return; + } + + let field = fields + .iter() + .find(|f| f.ident.as_str() == "desc") + .expect("lints must have a description field"); + + if let ExprKind::Lit(Spanned { + node: LitKind::Str(ref sym, _), + .. + }) = field.expr.kind + { + let sym_str = sym.as_str(); + if is_lint_ref_ty { + if sym_str == "default lint description" { + span_lint( + cx, + DEFAULT_LINT, + item.span, + &format!("the lint `{}` has the default lint description", item.ident.name), + ); + } + + self.declared_lints.insert(item.ident.name, item.span); + } else if sym_str == "default deprecation note" { + span_lint( + cx, + DEFAULT_DEPRECATION_REASON, + item.span, + &format!("the lint `{}` has the default deprecation reason", item.ident.name), + ); + } + } + } + } else if let Some(macro_call) = root_macro_call_first_node(cx, item) { + if !matches!( + cx.tcx.item_name(macro_call.def_id).as_str(), + "impl_lint_pass" | "declare_lint_pass" + ) { + return; + } + if let hir::ItemKind::Impl(hir::Impl { + of_trait: None, + items: impl_item_refs, + .. + }) = item.kind + { + let mut collector = LintCollector { + output: &mut self.registered_lints, + cx, + }; + let body_id = cx.tcx.hir().body_owned_by( + impl_item_refs + .iter() + .find(|iiref| iiref.ident.as_str() == "get_lints") + .expect("LintPass needs to implement get_lints") + .id + .hir_id(), + ); + collector.visit_expr(&cx.tcx.hir().body(body_id).value); + } + } + } + + fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { + if is_lint_allowed(cx, LINT_WITHOUT_LINT_PASS, CRATE_HIR_ID) { + return; + } + + for (lint_name, &lint_span) in &self.declared_lints { + // When using the `declare_tool_lint!` macro, the original `lint_span`'s + // file points to "<rustc macros>". + // `compiletest-rs` thinks that's an error in a different file and + // just ignores it. This causes the test in compile-fail/lint_pass + // not able to capture the error. + // Therefore, we need to climb the macro expansion tree and find the + // actual span that invoked `declare_tool_lint!`: + let lint_span = lint_span.ctxt().outer_expn_data().call_site; + + if !self.registered_lints.contains(lint_name) { + span_lint( + cx, + LINT_WITHOUT_LINT_PASS, + lint_span, + &format!("the lint `{}` is not added to any `LintPass`", lint_name), + ); + } + } + } +} + +fn is_lint_ref_type<'tcx>(cx: &LateContext<'tcx>, ty: &Ty<'_>) -> bool { + if let TyKind::Rptr( + _, + MutTy { + ty: inner, + mutbl: Mutability::Not, + }, + ) = ty.kind + { + if let TyKind::Path(ref path) = inner.kind { + if let Res::Def(DefKind::Struct, def_id) = cx.qpath_res(path, inner.hir_id) { + return match_def_path(cx, def_id, &paths::LINT); + } + } + } + + false +} + +fn check_invalid_clippy_version_attribute(cx: &LateContext<'_>, item: &'_ Item<'_>) { + if let Some(value) = extract_clippy_version_value(cx, item) { + // The `sym!` macro doesn't work as it only expects a single token. + // It's better to keep it this way and have a direct `Symbol::intern` call here. + if value == Symbol::intern("pre 1.29.0") { + return; + } + + if RustcVersion::parse(value.as_str()).is_err() { + span_lint_and_help( + cx, + INVALID_CLIPPY_VERSION_ATTRIBUTE, + item.span, + "this item has an invalid `clippy::version` attribute", + None, + "please use a valid sematic version, see `doc/adding_lints.md`", + ); + } + } else { + span_lint_and_help( + cx, + MISSING_CLIPPY_VERSION_ATTRIBUTE, + item.span, + "this lint is missing the `clippy::version` attribute or version value", + None, + "please use a `clippy::version` attribute, see `doc/adding_lints.md`", + ); + } +} + +/// This function extracts the version value of a `clippy::version` attribute if the given value has +/// one +fn extract_clippy_version_value(cx: &LateContext<'_>, item: &'_ Item<'_>) -> Option<Symbol> { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + attrs.iter().find_map(|attr| { + if_chain! { + // Identify attribute + if let ast::AttrKind::Normal(ref attr_kind, _) = &attr.kind; + if let [tool_name, attr_name] = &attr_kind.path.segments[..]; + if tool_name.ident.name == sym::clippy; + if attr_name.ident.name == sym::version; + if let Some(version) = attr.value_str(); + then { + Some(version) + } else { + None + } + } + }) +} + +struct LintCollector<'a, 'tcx> { + output: &'a mut FxHashSet<Symbol>, + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> Visitor<'tcx> for LintCollector<'a, 'tcx> { + type NestedFilter = nested_filter::All; + + fn visit_path(&mut self, path: &'tcx Path<'_>, _: HirId) { + if path.segments.len() == 1 { + self.output.insert(path.segments[0].ident.name); + } + } + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } +} + +#[derive(Clone, Default)] +pub struct CompilerLintFunctions { + map: FxHashMap<&'static str, &'static str>, +} + +impl CompilerLintFunctions { + #[must_use] + pub fn new() -> Self { + let mut map = FxHashMap::default(); + map.insert("span_lint", "utils::span_lint"); + map.insert("struct_span_lint", "utils::span_lint"); + map.insert("lint", "utils::span_lint"); + map.insert("span_lint_note", "utils::span_lint_and_note"); + map.insert("span_lint_help", "utils::span_lint_and_help"); + Self { map } + } +} + +impl_lint_pass!(CompilerLintFunctions => [COMPILER_LINT_FUNCTIONS]); + +impl<'tcx> LateLintPass<'tcx> for CompilerLintFunctions { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if is_lint_allowed(cx, COMPILER_LINT_FUNCTIONS, expr.hir_id) { + return; + } + + if_chain! { + if let ExprKind::MethodCall(path, [self_arg, ..], _) = &expr.kind; + let fn_name = path.ident; + if let Some(sugg) = self.map.get(fn_name.as_str()); + let ty = cx.typeck_results().expr_ty(self_arg).peel_refs(); + if match_type(cx, ty, &paths::EARLY_CONTEXT) + || match_type(cx, ty, &paths::LATE_CONTEXT); + then { + span_lint_and_help( + cx, + COMPILER_LINT_FUNCTIONS, + path.ident.span, + "usage of a compiler lint function", + None, + &format!("please use the Clippy variant of this function: `{}`", sugg), + ); + } + } + } +} + +declare_lint_pass!(OuterExpnDataPass => [OUTER_EXPN_EXPN_DATA]); + +impl<'tcx> LateLintPass<'tcx> for OuterExpnDataPass { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if is_lint_allowed(cx, OUTER_EXPN_EXPN_DATA, expr.hir_id) { + return; + } + + let (method_names, arg_lists, spans) = method_calls(expr, 2); + let method_names: Vec<&str> = method_names.iter().map(Symbol::as_str).collect(); + if_chain! { + if let ["expn_data", "outer_expn"] = method_names.as_slice(); + let args = arg_lists[1]; + if args.len() == 1; + let self_arg = &args[0]; + let self_ty = cx.typeck_results().expr_ty(self_arg).peel_refs(); + if match_type(cx, self_ty, &paths::SYNTAX_CONTEXT); + then { + span_lint_and_sugg( + cx, + OUTER_EXPN_EXPN_DATA, + spans[1].with_hi(expr.span.hi()), + "usage of `outer_expn().expn_data()`", + "try", + "outer_expn_data()".to_string(), + Applicability::MachineApplicable, + ); + } + } + } +} + +declare_lint_pass!(ProduceIce => [PRODUCE_ICE]); + +impl EarlyLintPass for ProduceIce { + fn check_fn(&mut self, _: &EarlyContext<'_>, fn_kind: FnKind<'_>, _: Span, _: NodeId) { + assert!(!is_trigger_fn(fn_kind), "Would you like some help with that?"); + } +} + +fn is_trigger_fn(fn_kind: FnKind<'_>) -> bool { + match fn_kind { + FnKind::Fn(_, ident, ..) => ident.name.as_str() == "it_looks_like_you_are_trying_to_kill_clippy", + FnKind::Closure(..) => false, + } +} + +declare_lint_pass!(CollapsibleCalls => [COLLAPSIBLE_SPAN_LINT_CALLS]); + +impl<'tcx> LateLintPass<'tcx> for CollapsibleCalls { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if is_lint_allowed(cx, COLLAPSIBLE_SPAN_LINT_CALLS, expr.hir_id) { + return; + } + + if_chain! { + if let ExprKind::Call(func, and_then_args) = expr.kind; + if is_expr_path_def_path(cx, func, &["clippy_utils", "diagnostics", "span_lint_and_then"]); + if and_then_args.len() == 5; + if let ExprKind::Closure(&Closure { body, .. }) = &and_then_args[4].kind; + let body = cx.tcx.hir().body(body); + let only_expr = peel_blocks_with_stmt(&body.value); + if let ExprKind::MethodCall(ps, span_call_args, _) = &only_expr.kind; + if let ExprKind::Path(..) = span_call_args[0].kind; + then { + let and_then_snippets = get_and_then_snippets(cx, and_then_args); + let mut sle = SpanlessEq::new(cx).deny_side_effects(); + match ps.ident.as_str() { + "span_suggestion" if sle.eq_expr(&and_then_args[2], &span_call_args[1]) => { + suggest_suggestion(cx, expr, &and_then_snippets, &span_suggestion_snippets(cx, span_call_args)); + }, + "span_help" if sle.eq_expr(&and_then_args[2], &span_call_args[1]) => { + let help_snippet = snippet(cx, span_call_args[2].span, r#""...""#); + suggest_help(cx, expr, &and_then_snippets, help_snippet.borrow(), true); + }, + "span_note" if sle.eq_expr(&and_then_args[2], &span_call_args[1]) => { + let note_snippet = snippet(cx, span_call_args[2].span, r#""...""#); + suggest_note(cx, expr, &and_then_snippets, note_snippet.borrow(), true); + }, + "help" => { + let help_snippet = snippet(cx, span_call_args[1].span, r#""...""#); + suggest_help(cx, expr, &and_then_snippets, help_snippet.borrow(), false); + } + "note" => { + let note_snippet = snippet(cx, span_call_args[1].span, r#""...""#); + suggest_note(cx, expr, &and_then_snippets, note_snippet.borrow(), false); + } + _ => (), + } + } + } + } +} + +struct AndThenSnippets<'a> { + cx: Cow<'a, str>, + lint: Cow<'a, str>, + span: Cow<'a, str>, + msg: Cow<'a, str>, +} + +fn get_and_then_snippets<'a, 'hir>(cx: &LateContext<'_>, and_then_snippets: &'hir [Expr<'hir>]) -> AndThenSnippets<'a> { + let cx_snippet = snippet(cx, and_then_snippets[0].span, "cx"); + let lint_snippet = snippet(cx, and_then_snippets[1].span, ".."); + let span_snippet = snippet(cx, and_then_snippets[2].span, "span"); + let msg_snippet = snippet(cx, and_then_snippets[3].span, r#""...""#); + + AndThenSnippets { + cx: cx_snippet, + lint: lint_snippet, + span: span_snippet, + msg: msg_snippet, + } +} + +struct SpanSuggestionSnippets<'a> { + help: Cow<'a, str>, + sugg: Cow<'a, str>, + applicability: Cow<'a, str>, +} + +fn span_suggestion_snippets<'a, 'hir>( + cx: &LateContext<'_>, + span_call_args: &'hir [Expr<'hir>], +) -> SpanSuggestionSnippets<'a> { + let help_snippet = snippet(cx, span_call_args[2].span, r#""...""#); + let sugg_snippet = snippet(cx, span_call_args[3].span, ".."); + let applicability_snippet = snippet(cx, span_call_args[4].span, "Applicability::MachineApplicable"); + + SpanSuggestionSnippets { + help: help_snippet, + sugg: sugg_snippet, + applicability: applicability_snippet, + } +} + +fn suggest_suggestion( + cx: &LateContext<'_>, + expr: &Expr<'_>, + and_then_snippets: &AndThenSnippets<'_>, + span_suggestion_snippets: &SpanSuggestionSnippets<'_>, +) { + span_lint_and_sugg( + cx, + COLLAPSIBLE_SPAN_LINT_CALLS, + expr.span, + "this call is collapsible", + "collapse into", + format!( + "span_lint_and_sugg({}, {}, {}, {}, {}, {}, {})", + and_then_snippets.cx, + and_then_snippets.lint, + and_then_snippets.span, + and_then_snippets.msg, + span_suggestion_snippets.help, + span_suggestion_snippets.sugg, + span_suggestion_snippets.applicability + ), + Applicability::MachineApplicable, + ); +} + +fn suggest_help( + cx: &LateContext<'_>, + expr: &Expr<'_>, + and_then_snippets: &AndThenSnippets<'_>, + help: &str, + with_span: bool, +) { + let option_span = if with_span { + format!("Some({})", and_then_snippets.span) + } else { + "None".to_string() + }; + + span_lint_and_sugg( + cx, + COLLAPSIBLE_SPAN_LINT_CALLS, + expr.span, + "this call is collapsible", + "collapse into", + format!( + "span_lint_and_help({}, {}, {}, {}, {}, {})", + and_then_snippets.cx, + and_then_snippets.lint, + and_then_snippets.span, + and_then_snippets.msg, + &option_span, + help + ), + Applicability::MachineApplicable, + ); +} + +fn suggest_note( + cx: &LateContext<'_>, + expr: &Expr<'_>, + and_then_snippets: &AndThenSnippets<'_>, + note: &str, + with_span: bool, +) { + let note_span = if with_span { + format!("Some({})", and_then_snippets.span) + } else { + "None".to_string() + }; + + span_lint_and_sugg( + cx, + COLLAPSIBLE_SPAN_LINT_CALLS, + expr.span, + "this call is collapsible", + "collapse into", + format!( + "span_lint_and_note({}, {}, {}, {}, {}, {})", + and_then_snippets.cx, + and_then_snippets.lint, + and_then_snippets.span, + and_then_snippets.msg, + note_span, + note + ), + Applicability::MachineApplicable, + ); +} + +declare_lint_pass!(MatchTypeOnDiagItem => [MATCH_TYPE_ON_DIAGNOSTIC_ITEM]); + +impl<'tcx> LateLintPass<'tcx> for MatchTypeOnDiagItem { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + if is_lint_allowed(cx, MATCH_TYPE_ON_DIAGNOSTIC_ITEM, expr.hir_id) { + return; + } + + if_chain! { + // Check if this is a call to utils::match_type() + if let ExprKind::Call(fn_path, [context, ty, ty_path]) = expr.kind; + if is_expr_path_def_path(cx, fn_path, &["clippy_utils", "ty", "match_type"]); + // Extract the path to the matched type + if let Some(segments) = path_to_matched_type(cx, ty_path); + let segments: Vec<&str> = segments.iter().map(Symbol::as_str).collect(); + if let Some(ty_did) = def_path_res(cx, &segments[..]).opt_def_id(); + // Check if the matched type is a diagnostic item + if let Some(item_name) = cx.tcx.get_diagnostic_name(ty_did); + then { + // TODO: check paths constants from external crates. + let cx_snippet = snippet(cx, context.span, "_"); + let ty_snippet = snippet(cx, ty.span, "_"); + + span_lint_and_sugg( + cx, + MATCH_TYPE_ON_DIAGNOSTIC_ITEM, + expr.span, + "usage of `clippy_utils::ty::match_type()` on a type diagnostic item", + "try", + format!("clippy_utils::ty::is_type_diagnostic_item({}, {}, sym::{})", cx_snippet, ty_snippet, item_name), + Applicability::MaybeIncorrect, + ); + } + } + } +} + +fn path_to_matched_type(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> Option<Vec<Symbol>> { + use rustc_hir::ItemKind; + + match &expr.kind { + ExprKind::AddrOf(.., expr) => return path_to_matched_type(cx, expr), + ExprKind::Path(qpath) => match cx.qpath_res(qpath, expr.hir_id) { + Res::Local(hir_id) => { + let parent_id = cx.tcx.hir().get_parent_node(hir_id); + if let Some(Node::Local(local)) = cx.tcx.hir().find(parent_id) { + if let Some(init) = local.init { + return path_to_matched_type(cx, init); + } + } + }, + Res::Def(DefKind::Const | DefKind::Static(..), def_id) => { + if let Some(Node::Item(item)) = cx.tcx.hir().get_if_local(def_id) { + if let ItemKind::Const(.., body_id) | ItemKind::Static(.., body_id) = item.kind { + let body = cx.tcx.hir().body(body_id); + return path_to_matched_type(cx, &body.value); + } + } + }, + _ => {}, + }, + ExprKind::Array(exprs) => { + let segments: Vec<Symbol> = exprs + .iter() + .filter_map(|expr| { + if let ExprKind::Lit(lit) = &expr.kind { + if let LitKind::Str(sym, _) = lit.node { + return Some(sym); + } + } + + None + }) + .collect(); + + if segments.len() == exprs.len() { + return Some(segments); + } + }, + _ => {}, + } + + None +} + +// This is not a complete resolver for paths. It works on all the paths currently used in the paths +// module. That's all it does and all it needs to do. +pub fn check_path(cx: &LateContext<'_>, path: &[&str]) -> bool { + if def_path_res(cx, path) != Res::Err { + return true; + } + + // Some implementations can't be found by `path_to_res`, particularly inherent + // implementations of native types. Check lang items. + let path_syms: Vec<_> = path.iter().map(|p| Symbol::intern(p)).collect(); + let lang_items = cx.tcx.lang_items(); + // This list isn't complete, but good enough for our current list of paths. + let incoherent_impls = [ + SimplifiedTypeGen::FloatSimplifiedType(FloatTy::F32), + SimplifiedTypeGen::FloatSimplifiedType(FloatTy::F64), + SimplifiedTypeGen::SliceSimplifiedType, + SimplifiedTypeGen::StrSimplifiedType, + ] + .iter() + .flat_map(|&ty| cx.tcx.incoherent_impls(ty)); + for item_def_id in lang_items.items().iter().flatten().chain(incoherent_impls) { + let lang_item_path = cx.get_def_path(*item_def_id); + if path_syms.starts_with(&lang_item_path) { + if let [item] = &path_syms[lang_item_path.len()..] { + if matches!( + cx.tcx.def_kind(*item_def_id), + DefKind::Mod | DefKind::Enum | DefKind::Trait + ) { + for child in cx.tcx.module_children(*item_def_id) { + if child.ident.name == *item { + return true; + } + } + } else { + for child in cx.tcx.associated_item_def_ids(*item_def_id) { + if cx.tcx.item_name(*child) == *item { + return true; + } + } + } + } + } + } + + false +} + +declare_lint_pass!(InvalidPaths => [INVALID_PATHS]); + +impl<'tcx> LateLintPass<'tcx> for InvalidPaths { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + let local_def_id = &cx.tcx.parent_module(item.hir_id()); + let mod_name = &cx.tcx.item_name(local_def_id.to_def_id()); + if_chain! { + if mod_name.as_str() == "paths"; + if let hir::ItemKind::Const(ty, body_id) = item.kind; + let ty = hir_ty_to_ty(cx.tcx, ty); + if let ty::Array(el_ty, _) = &ty.kind(); + if let ty::Ref(_, el_ty, _) = &el_ty.kind(); + if el_ty.is_str(); + let body = cx.tcx.hir().body(body_id); + let typeck_results = cx.tcx.typeck_body(body_id); + if let Some(Constant::Vec(path)) = constant_simple(cx, typeck_results, &body.value); + let path: Vec<&str> = path.iter().map(|x| { + if let Constant::Str(s) = x { + s.as_str() + } else { + // We checked the type of the constant above + unreachable!() + } + }).collect(); + if !check_path(cx, &path[..]); + then { + span_lint(cx, INVALID_PATHS, item.span, "invalid path"); + } + } + } +} + +#[derive(Default)] +pub struct InterningDefinedSymbol { + // Maps the symbol value to the constant DefId. + symbol_map: FxHashMap<u32, DefId>, +} + +impl_lint_pass!(InterningDefinedSymbol => [INTERNING_DEFINED_SYMBOL, UNNECESSARY_SYMBOL_STR]); + +impl<'tcx> LateLintPass<'tcx> for InterningDefinedSymbol { + fn check_crate(&mut self, cx: &LateContext<'_>) { + if !self.symbol_map.is_empty() { + return; + } + + for &module in &[&paths::KW_MODULE, &paths::SYM_MODULE] { + if let Some(def_id) = def_path_res(cx, module).opt_def_id() { + for item in cx.tcx.module_children(def_id).iter() { + if_chain! { + if let Res::Def(DefKind::Const, item_def_id) = item.res; + let ty = cx.tcx.type_of(item_def_id); + if match_type(cx, ty, &paths::SYMBOL); + if let Ok(ConstValue::Scalar(value)) = cx.tcx.const_eval_poly(item_def_id); + if let Ok(value) = value.to_u32(); + then { + self.symbol_map.insert(value, item_def_id); + } + } + } + } + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + if let ExprKind::Call(func, [arg]) = &expr.kind; + if let ty::FnDef(def_id, _) = cx.typeck_results().expr_ty(func).kind(); + if match_def_path(cx, *def_id, &paths::SYMBOL_INTERN); + if let Some(Constant::Str(arg)) = constant_simple(cx, cx.typeck_results(), arg); + let value = Symbol::intern(&arg).as_u32(); + if let Some(&def_id) = self.symbol_map.get(&value); + then { + span_lint_and_sugg( + cx, + INTERNING_DEFINED_SYMBOL, + is_expn_of(expr.span, "sym").unwrap_or(expr.span), + "interning a defined symbol", + "try", + cx.tcx.def_path_str(def_id), + Applicability::MachineApplicable, + ); + } + } + if let ExprKind::Binary(op, left, right) = expr.kind { + if matches!(op.node, BinOpKind::Eq | BinOpKind::Ne) { + let data = [ + (left, self.symbol_str_expr(left, cx)), + (right, self.symbol_str_expr(right, cx)), + ]; + match data { + // both operands are a symbol string + [(_, Some(left)), (_, Some(right))] => { + span_lint_and_sugg( + cx, + UNNECESSARY_SYMBOL_STR, + expr.span, + "unnecessary `Symbol` to string conversion", + "try", + format!( + "{} {} {}", + left.as_symbol_snippet(cx), + op.node.as_str(), + right.as_symbol_snippet(cx), + ), + Applicability::MachineApplicable, + ); + }, + // one of the operands is a symbol string + [(expr, Some(symbol)), _] | [_, (expr, Some(symbol))] => { + // creating an owned string for comparison + if matches!(symbol, SymbolStrExpr::Expr { is_to_owned: true, .. }) { + span_lint_and_sugg( + cx, + UNNECESSARY_SYMBOL_STR, + expr.span, + "unnecessary string allocation", + "try", + format!("{}.as_str()", symbol.as_symbol_snippet(cx)), + Applicability::MachineApplicable, + ); + } + }, + // nothing found + [(_, None), (_, None)] => {}, + } + } + } + } +} + +impl InterningDefinedSymbol { + fn symbol_str_expr<'tcx>(&self, expr: &'tcx Expr<'tcx>, cx: &LateContext<'tcx>) -> Option<SymbolStrExpr<'tcx>> { + static IDENT_STR_PATHS: &[&[&str]] = &[&paths::IDENT_AS_STR, &paths::TO_STRING_METHOD]; + static SYMBOL_STR_PATHS: &[&[&str]] = &[ + &paths::SYMBOL_AS_STR, + &paths::SYMBOL_TO_IDENT_STRING, + &paths::TO_STRING_METHOD, + ]; + let call = if_chain! { + if let ExprKind::AddrOf(_, _, e) = expr.kind; + if let ExprKind::Unary(UnOp::Deref, e) = e.kind; + then { e } else { expr } + }; + if_chain! { + // is a method call + if let ExprKind::MethodCall(_, [item], _) = call.kind; + if let Some(did) = cx.typeck_results().type_dependent_def_id(call.hir_id); + let ty = cx.typeck_results().expr_ty(item); + // ...on either an Ident or a Symbol + if let Some(is_ident) = if match_type(cx, ty, &paths::SYMBOL) { + Some(false) + } else if match_type(cx, ty, &paths::IDENT) { + Some(true) + } else { + None + }; + // ...which converts it to a string + let paths = if is_ident { IDENT_STR_PATHS } else { SYMBOL_STR_PATHS }; + if let Some(path) = paths.iter().find(|path| match_def_path(cx, did, path)); + then { + let is_to_owned = path.last().unwrap().ends_with("string"); + return Some(SymbolStrExpr::Expr { + item, + is_ident, + is_to_owned, + }); + } + } + // is a string constant + if let Some(Constant::Str(s)) = constant_simple(cx, cx.typeck_results(), expr) { + let value = Symbol::intern(&s).as_u32(); + // ...which matches a symbol constant + if let Some(&def_id) = self.symbol_map.get(&value) { + return Some(SymbolStrExpr::Const(def_id)); + } + } + None + } +} + +enum SymbolStrExpr<'tcx> { + /// a string constant with a corresponding symbol constant + Const(DefId), + /// a "symbol to string" expression like `symbol.as_str()` + Expr { + /// part that evaluates to `Symbol` or `Ident` + item: &'tcx Expr<'tcx>, + is_ident: bool, + /// whether an owned `String` is created like `to_ident_string()` + is_to_owned: bool, + }, +} + +impl<'tcx> SymbolStrExpr<'tcx> { + /// Returns a snippet that evaluates to a `Symbol` and is const if possible + fn as_symbol_snippet(&self, cx: &LateContext<'_>) -> Cow<'tcx, str> { + match *self { + Self::Const(def_id) => cx.tcx.def_path_str(def_id).into(), + Self::Expr { item, is_ident, .. } => { + let mut snip = snippet(cx, item.span.source_callsite(), ".."); + if is_ident { + // get `Ident.name` + snip.to_mut().push_str(".name"); + } + snip + }, + } + } +} + +declare_lint_pass!(IfChainStyle => [IF_CHAIN_STYLE]); + +impl<'tcx> LateLintPass<'tcx> for IfChainStyle { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) { + let (local, after, if_chain_span) = if_chain! { + if let [Stmt { kind: StmtKind::Local(local), .. }, after @ ..] = block.stmts; + if let Some(if_chain_span) = is_expn_of(block.span, "if_chain"); + then { (local, after, if_chain_span) } else { return } + }; + if is_first_if_chain_expr(cx, block.hir_id, if_chain_span) { + span_lint( + cx, + IF_CHAIN_STYLE, + if_chain_local_span(cx, local, if_chain_span), + "`let` expression should be above the `if_chain!`", + ); + } else if local.span.ctxt() == block.span.ctxt() && is_if_chain_then(after, block.expr, if_chain_span) { + span_lint( + cx, + IF_CHAIN_STYLE, + if_chain_local_span(cx, local, if_chain_span), + "`let` expression should be inside `then { .. }`", + ); + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { + let (cond, then, els) = if let Some(higher::IfOrIfLet { cond, r#else, then }) = higher::IfOrIfLet::hir(expr) { + (cond, then, r#else.is_some()) + } else { + return; + }; + let then_block = match then.kind { + ExprKind::Block(block, _) => block, + _ => return, + }; + let if_chain_span = is_expn_of(expr.span, "if_chain"); + if !els { + check_nested_if_chains(cx, expr, then_block, if_chain_span); + } + let if_chain_span = match if_chain_span { + None => return, + Some(span) => span, + }; + // check for `if a && b;` + if_chain! { + if let ExprKind::Binary(op, _, _) = cond.kind; + if op.node == BinOpKind::And; + if cx.sess().source_map().is_multiline(cond.span); + then { + span_lint(cx, IF_CHAIN_STYLE, cond.span, "`if a && b;` should be `if a; if b;`"); + } + } + if is_first_if_chain_expr(cx, expr.hir_id, if_chain_span) + && is_if_chain_then(then_block.stmts, then_block.expr, if_chain_span) + { + span_lint(cx, IF_CHAIN_STYLE, expr.span, "`if_chain!` only has one `if`"); + } + } +} + +fn check_nested_if_chains( + cx: &LateContext<'_>, + if_expr: &Expr<'_>, + then_block: &Block<'_>, + if_chain_span: Option<Span>, +) { + #[rustfmt::skip] + let (head, tail) = match *then_block { + Block { stmts, expr: Some(tail), .. } => (stmts, tail), + Block { + stmts: &[ + ref head @ .., + Stmt { kind: StmtKind::Expr(tail) | StmtKind::Semi(tail), .. } + ], + .. + } => (head, tail), + _ => return, + }; + if_chain! { + if let Some(higher::IfOrIfLet { r#else: None, .. }) = higher::IfOrIfLet::hir(tail); + let sm = cx.sess().source_map(); + if head + .iter() + .all(|stmt| matches!(stmt.kind, StmtKind::Local(..)) && !sm.is_multiline(stmt.span)); + if if_chain_span.is_some() || !is_else_clause(cx.tcx, if_expr); + then {} else { return } + } + let (span, msg) = match (if_chain_span, is_expn_of(tail.span, "if_chain")) { + (None, Some(_)) => (if_expr.span, "this `if` can be part of the inner `if_chain!`"), + (Some(_), None) => (tail.span, "this `if` can be part of the outer `if_chain!`"), + (Some(a), Some(b)) if a != b => (b, "this `if_chain!` can be merged with the outer `if_chain!`"), + _ => return, + }; + span_lint_and_then(cx, IF_CHAIN_STYLE, span, msg, |diag| { + let (span, msg) = match head { + [] => return, + [stmt] => (stmt.span, "this `let` statement can also be in the `if_chain!`"), + [a, .., b] => ( + a.span.to(b.span), + "these `let` statements can also be in the `if_chain!`", + ), + }; + diag.span_help(span, msg); + }); +} + +fn is_first_if_chain_expr(cx: &LateContext<'_>, hir_id: HirId, if_chain_span: Span) -> bool { + cx.tcx + .hir() + .parent_iter(hir_id) + .find(|(_, node)| { + #[rustfmt::skip] + !matches!(node, Node::Expr(Expr { kind: ExprKind::Block(..), .. }) | Node::Stmt(_)) + }) + .map_or(false, |(id, _)| { + is_expn_of(cx.tcx.hir().span(id), "if_chain") != Some(if_chain_span) + }) +} + +/// Checks a trailing slice of statements and expression of a `Block` to see if they are part +/// of the `then {..}` portion of an `if_chain!` +fn is_if_chain_then(stmts: &[Stmt<'_>], expr: Option<&Expr<'_>>, if_chain_span: Span) -> bool { + let span = if let [stmt, ..] = stmts { + stmt.span + } else if let Some(expr) = expr { + expr.span + } else { + // empty `then {}` + return true; + }; + is_expn_of(span, "if_chain").map_or(true, |span| span != if_chain_span) +} + +/// Creates a `Span` for `let x = ..;` in an `if_chain!` call. +fn if_chain_local_span(cx: &LateContext<'_>, local: &Local<'_>, if_chain_span: Span) -> Span { + let mut span = local.pat.span; + if let Some(init) = local.init { + span = span.to(init.span); + } + span.adjust(if_chain_span.ctxt().outer_expn()); + let sm = cx.sess().source_map(); + let span = sm.span_extend_to_prev_str(span, "let", false, true).unwrap_or(span); + let span = sm.span_extend_to_next_char(span, ';', false); + Span::new( + span.lo() - BytePos(3), + span.hi() + BytePos(1), + span.ctxt(), + span.parent(), + ) +} + +declare_lint_pass!(MsrvAttrImpl => [MISSING_MSRV_ATTR_IMPL]); + +impl LateLintPass<'_> for MsrvAttrImpl { + fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) { + if_chain! { + if let hir::ItemKind::Impl(hir::Impl { + of_trait: Some(lint_pass_trait_ref), + self_ty, + items, + .. + }) = &item.kind; + if let Some(lint_pass_trait_def_id) = lint_pass_trait_ref.trait_def_id(); + let is_late_pass = match_def_path(cx, lint_pass_trait_def_id, &paths::LATE_LINT_PASS); + if is_late_pass || match_def_path(cx, lint_pass_trait_def_id, &paths::EARLY_LINT_PASS); + let self_ty = hir_ty_to_ty(cx.tcx, self_ty); + if let ty::Adt(self_ty_def, _) = self_ty.kind(); + if self_ty_def.is_struct(); + if self_ty_def.all_fields().any(|f| { + cx.tcx + .type_of(f.did) + .walk() + .filter(|t| matches!(t.unpack(), GenericArgKind::Type(_))) + .any(|t| match_type(cx, t.expect_ty(), &paths::RUSTC_VERSION)) + }); + if !items.iter().any(|item| item.ident.name == sym!(enter_lint_attrs)); + then { + let context = if is_late_pass { "LateContext" } else { "EarlyContext" }; + let lint_pass = if is_late_pass { "LateLintPass" } else { "EarlyLintPass" }; + let span = cx.sess().source_map().span_through_char(item.span, '{'); + span_lint_and_sugg( + cx, + MISSING_MSRV_ATTR_IMPL, + span, + &format!("`extract_msrv_attr!` macro missing from `{lint_pass}` implementation"), + &format!("add `extract_msrv_attr!({context})` to the `{lint_pass}` implementation"), + format!("{}\n extract_msrv_attr!({context});", snippet(cx, span, "..")), + Applicability::MachineApplicable, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/utils/internal_lints/metadata_collector.rs b/src/tools/clippy/clippy_lints/src/utils/internal_lints/metadata_collector.rs new file mode 100644 index 000000000..92934c16d --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/utils/internal_lints/metadata_collector.rs @@ -0,0 +1,1169 @@ +//! This lint is used to collect metadata about clippy lints. This metadata is exported as a json +//! file and then used to generate the [clippy lint list](https://rust-lang.github.io/rust-clippy/master/index.html) +//! +//! This module and therefore the entire lint is guarded by a feature flag called `internal` +//! +//! The module transforms all lint names to ascii lowercase to ensure that we don't have mismatches +//! during any comparison or mapping. (Please take care of this, it's not fun to spend time on such +//! a simple mistake) + +use crate::renamed_lints::RENAMED_LINTS; +use crate::utils::internal_lints::{extract_clippy_version_value, is_lint_ref_type}; + +use clippy_utils::diagnostics::span_lint; +use clippy_utils::ty::{match_type, walk_ptrs_ty_depth}; +use clippy_utils::{last_path_segment, match_def_path, match_function_call, match_path, paths}; +use if_chain::if_chain; +use rustc_ast as ast; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::{ + self as hir, def::DefKind, intravisit, intravisit::Visitor, Closure, ExprKind, Item, ItemKind, Mutability, QPath, +}; +use rustc_lint::{CheckLintNameResult, LateContext, LateLintPass, LintContext, LintId}; +use rustc_middle::hir::nested_filter; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::Ident; +use rustc_span::{sym, Loc, Span, Symbol}; +use serde::{ser::SerializeStruct, Serialize, Serializer}; +use std::collections::BinaryHeap; +use std::fmt; +use std::fmt::Write as _; +use std::fs::{self, OpenOptions}; +use std::io::prelude::*; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +/// This is the output file of the lint collector. +const OUTPUT_FILE: &str = "../util/gh-pages/lints.json"; +/// These lints are excluded from the export. +const BLACK_LISTED_LINTS: &[&str] = &["lint_author", "dump_hir", "internal_metadata_collector"]; +/// These groups will be ignored by the lint group matcher. This is useful for collections like +/// `clippy::all` +const IGNORED_LINT_GROUPS: [&str; 1] = ["clippy::all"]; +/// Lints within this group will be excluded from the collection. These groups +/// have to be defined without the `clippy::` prefix. +const EXCLUDED_LINT_GROUPS: [&str; 1] = ["internal"]; +/// Collected deprecated lint will be assigned to this group in the JSON output +const DEPRECATED_LINT_GROUP_STR: &str = "deprecated"; +/// This is the lint level for deprecated lints that will be displayed in the lint list +const DEPRECATED_LINT_LEVEL: &str = "none"; +/// This array holds Clippy's lint groups with their corresponding default lint level. The +/// lint level for deprecated lints is set in `DEPRECATED_LINT_LEVEL`. +const DEFAULT_LINT_LEVELS: &[(&str, &str)] = &[ + ("correctness", "deny"), + ("suspicious", "warn"), + ("restriction", "allow"), + ("style", "warn"), + ("pedantic", "allow"), + ("complexity", "warn"), + ("perf", "warn"), + ("cargo", "allow"), + ("nursery", "allow"), +]; +/// This prefix is in front of the lint groups in the lint store. The prefix will be trimmed +/// to only keep the actual lint group in the output. +const CLIPPY_LINT_GROUP_PREFIX: &str = "clippy::"; + +/// This template will be used to format the configuration section in the lint documentation. +/// The `configurations` parameter will be replaced with one or multiple formatted +/// `ClippyConfiguration` instances. See `CONFIGURATION_VALUE_TEMPLATE` for further customizations +macro_rules! CONFIGURATION_SECTION_TEMPLATE { + () => { + r#" +### Configuration +This lint has the following configuration variables: + +{configurations} +"# + }; +} +/// This template will be used to format an individual `ClippyConfiguration` instance in the +/// lint documentation. +/// +/// The format function will provide strings for the following parameters: `name`, `ty`, `doc` and +/// `default` +macro_rules! CONFIGURATION_VALUE_TEMPLATE { + () => { + "* `{name}`: `{ty}`: {doc} (defaults to `{default}`)\n" + }; +} + +macro_rules! RENAMES_SECTION_TEMPLATE { + () => { + r#" +### Past names + +{names} +"# + }; +} +macro_rules! RENAME_VALUE_TEMPLATE { + () => { + "* `{name}`\n" + }; +} + +const LINT_EMISSION_FUNCTIONS: [&[&str]; 7] = [ + &["clippy_utils", "diagnostics", "span_lint"], + &["clippy_utils", "diagnostics", "span_lint_and_help"], + &["clippy_utils", "diagnostics", "span_lint_and_note"], + &["clippy_utils", "diagnostics", "span_lint_hir"], + &["clippy_utils", "diagnostics", "span_lint_and_sugg"], + &["clippy_utils", "diagnostics", "span_lint_and_then"], + &["clippy_utils", "diagnostics", "span_lint_hir_and_then"], +]; +const SUGGESTION_DIAGNOSTIC_BUILDER_METHODS: [(&str, bool); 9] = [ + ("span_suggestion", false), + ("span_suggestion_short", false), + ("span_suggestion_verbose", false), + ("span_suggestion_hidden", false), + ("tool_only_span_suggestion", false), + ("multipart_suggestion", true), + ("multipart_suggestions", true), + ("tool_only_multipart_suggestion", true), + ("span_suggestions", true), +]; +const SUGGESTION_FUNCTIONS: [&[&str]; 2] = [ + &["clippy_utils", "diagnostics", "multispan_sugg"], + &["clippy_utils", "diagnostics", "multispan_sugg_with_applicability"], +]; +const DEPRECATED_LINT_TYPE: [&str; 3] = ["clippy_lints", "deprecated_lints", "ClippyDeprecatedLint"]; + +/// The index of the applicability name of `paths::APPLICABILITY_VALUES` +const APPLICABILITY_NAME_INDEX: usize = 2; +/// This applicability will be set for unresolved applicability values. +const APPLICABILITY_UNRESOLVED_STR: &str = "Unresolved"; +/// The version that will be displayed if none has been defined +const VERSION_DEFAULT_STR: &str = "Unknown"; + +declare_clippy_lint! { + /// ### What it does + /// Collects metadata about clippy lints for the website. + /// + /// This lint will be used to report problems of syntax parsing. You should hopefully never + /// see this but never say never I guess ^^ + /// + /// ### Why is this bad? + /// This is not a bad thing but definitely a hacky way to do it. See + /// issue [#4310](https://github.com/rust-lang/rust-clippy/issues/4310) for a discussion + /// about the implementation. + /// + /// ### Known problems + /// Hopefully none. It would be pretty uncool to have a problem here :) + /// + /// ### Example output + /// ```json,ignore + /// { + /// "id": "internal_metadata_collector", + /// "id_span": { + /// "path": "clippy_lints/src/utils/internal_lints/metadata_collector.rs", + /// "line": 1 + /// }, + /// "group": "clippy::internal", + /// "docs": " ### What it does\nCollects metadata about clippy lints for the website. [...] " + /// } + /// ``` + #[clippy::version = "1.56.0"] + pub INTERNAL_METADATA_COLLECTOR, + internal_warn, + "A busy bee collection metadata about lints" +} + +impl_lint_pass!(MetadataCollector => [INTERNAL_METADATA_COLLECTOR]); + +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone)] +pub struct MetadataCollector { + /// All collected lints + /// + /// We use a Heap here to have the lints added in alphabetic order in the export + lints: BinaryHeap<LintMetadata>, + applicability_info: FxHashMap<String, ApplicabilityInfo>, + config: Vec<ClippyConfiguration>, + clippy_project_root: PathBuf, +} + +impl MetadataCollector { + pub fn new() -> Self { + Self { + lints: BinaryHeap::<LintMetadata>::default(), + applicability_info: FxHashMap::<String, ApplicabilityInfo>::default(), + config: collect_configs(), + clippy_project_root: std::env::current_dir() + .expect("failed to get current dir") + .ancestors() + .nth(1) + .expect("failed to get project root") + .to_path_buf(), + } + } + + fn get_lint_configs(&self, lint_name: &str) -> Option<String> { + self.config + .iter() + .filter(|config| config.lints.iter().any(|lint| lint == lint_name)) + .map(ToString::to_string) + .reduce(|acc, x| acc + &x) + .map(|configurations| format!(CONFIGURATION_SECTION_TEMPLATE!(), configurations = configurations)) + } +} + +impl Drop for MetadataCollector { + /// You might ask: How hacky is this? + /// My answer: YES + fn drop(&mut self) { + // The metadata collector gets dropped twice, this makes sure that we only write + // when the list is full + if self.lints.is_empty() { + return; + } + + let mut applicability_info = std::mem::take(&mut self.applicability_info); + + // Mapping the final data + let mut lints = std::mem::take(&mut self.lints).into_sorted_vec(); + for x in &mut lints { + x.applicability = Some(applicability_info.remove(&x.id).unwrap_or_default()); + replace_produces(&x.id, &mut x.docs, &self.clippy_project_root); + } + + collect_renames(&mut lints); + + // Outputting + if Path::new(OUTPUT_FILE).exists() { + fs::remove_file(OUTPUT_FILE).unwrap(); + } + let mut file = OpenOptions::new().write(true).create(true).open(OUTPUT_FILE).unwrap(); + writeln!(file, "{}", serde_json::to_string_pretty(&lints).unwrap()).unwrap(); + } +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)] +struct LintMetadata { + id: String, + id_span: SerializableSpan, + group: String, + level: String, + docs: String, + version: String, + /// This field is only used in the output and will only be + /// mapped shortly before the actual output. + applicability: Option<ApplicabilityInfo>, +} + +impl LintMetadata { + fn new( + id: String, + id_span: SerializableSpan, + group: String, + level: &'static str, + version: String, + docs: String, + ) -> Self { + Self { + id, + id_span, + group, + level: level.to_string(), + version, + docs, + applicability: None, + } + } +} + +fn replace_produces(lint_name: &str, docs: &mut String, clippy_project_root: &Path) { + let mut doc_lines = docs.lines().map(ToString::to_string).collect::<Vec<_>>(); + let mut lines = doc_lines.iter_mut(); + + 'outer: loop { + // Find the start of the example + + // ```rust + loop { + match lines.next() { + Some(line) if line.trim_start().starts_with("```rust") => { + if line.contains("ignore") || line.contains("no_run") { + // A {{produces}} marker may have been put on a ignored code block by mistake, + // just seek to the end of the code block and continue checking. + if lines.any(|line| line.trim_start().starts_with("```")) { + continue; + } + + panic!("lint `{}` has an unterminated code block", lint_name) + } + + break; + }, + Some(line) if line.trim_start() == "{{produces}}" => { + panic!( + "lint `{}` has marker {{{{produces}}}} with an ignored or missing code block", + lint_name + ) + }, + Some(line) => { + let line = line.trim(); + // These are the two most common markers of the corrections section + if line.eq_ignore_ascii_case("Use instead:") || line.eq_ignore_ascii_case("Could be written as:") { + break 'outer; + } + }, + None => break 'outer, + } + } + + // Collect the example + let mut example = Vec::new(); + loop { + match lines.next() { + Some(line) if line.trim_start() == "```" => break, + Some(line) => example.push(line), + None => panic!("lint `{}` has an unterminated code block", lint_name), + } + } + + // Find the {{produces}} and attempt to generate the output + loop { + match lines.next() { + Some(line) if line.is_empty() => {}, + Some(line) if line.trim() == "{{produces}}" => { + let output = get_lint_output(lint_name, &example, clippy_project_root); + line.replace_range( + .., + &format!( + "<details>\ + <summary>Produces</summary>\n\ + \n\ + ```text\n\ + {}\n\ + ```\n\ + </details>", + output + ), + ); + + break; + }, + // No {{produces}}, we can move on to the next example + Some(_) => break, + None => break 'outer, + } + } + } + + *docs = cleanup_docs(&doc_lines); +} + +fn get_lint_output(lint_name: &str, example: &[&mut String], clippy_project_root: &Path) -> String { + let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("failed to create temp dir: {e}")); + let file = dir.path().join("lint_example.rs"); + + let mut source = String::new(); + let unhidden = example + .iter() + .map(|line| line.trim_start().strip_prefix("# ").unwrap_or(line)); + + // Get any attributes + let mut lines = unhidden.peekable(); + while let Some(line) = lines.peek() { + if line.starts_with("#!") { + source.push_str(line); + source.push('\n'); + lines.next(); + } else { + break; + } + } + + let needs_main = !example.iter().any(|line| line.contains("fn main")); + if needs_main { + source.push_str("fn main() {\n"); + } + + for line in lines { + source.push_str(line); + source.push('\n'); + } + + if needs_main { + source.push_str("}\n"); + } + + if let Err(e) = fs::write(&file, &source) { + panic!("failed to write to `{}`: {e}", file.as_path().to_string_lossy()); + } + + let prefixed_name = format!("{}{lint_name}", CLIPPY_LINT_GROUP_PREFIX); + + let mut cmd = Command::new("cargo"); + + cmd.current_dir(clippy_project_root) + .env("CARGO_INCREMENTAL", "0") + .env("CLIPPY_ARGS", "") + .env("CLIPPY_DISABLE_DOCS_LINKS", "1") + // We need to disable this to enable all lints + .env("ENABLE_METADATA_COLLECTION", "0") + .args(["run", "--bin", "clippy-driver"]) + .args(["--target-dir", "./clippy_lints/target"]) + .args(["--", "--error-format=json"]) + .args(["--edition", "2021"]) + .arg("-Cdebuginfo=0") + .args(["-A", "clippy::all"]) + .args(["-W", &prefixed_name]) + .args(["-L", "./target/debug"]) + .args(["-Z", "no-codegen"]); + + let output = cmd + .arg(file.as_path()) + .output() + .unwrap_or_else(|e| panic!("failed to run `{:?}`: {e}", cmd)); + + let tmp_file_path = file.to_string_lossy(); + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + let msgs = stderr + .lines() + .filter(|line| line.starts_with('{')) + .map(|line| serde_json::from_str(line).unwrap()) + .collect::<Vec<serde_json::Value>>(); + + let mut rendered = String::new(); + let iter = msgs + .iter() + .filter(|msg| matches!(&msg["code"]["code"], serde_json::Value::String(s) if s == &prefixed_name)); + + for message in iter { + let rendered_part = message["rendered"].as_str().expect("rendered field should exist"); + rendered.push_str(rendered_part); + } + + if rendered.is_empty() { + let rendered: Vec<&str> = msgs.iter().filter_map(|msg| msg["rendered"].as_str()).collect(); + let non_json: Vec<&str> = stderr.lines().filter(|line| !line.starts_with('{')).collect(); + panic!( + "did not find lint `{}` in output of example, got:\n{}\n{}", + lint_name, + non_json.join("\n"), + rendered.join("\n") + ); + } + + // The reader doesn't need to see `/tmp/.tmpfiy2Qd/lint_example.rs` :) + rendered.trim_end().replace(&*tmp_file_path, "lint_example.rs") +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)] +struct SerializableSpan { + path: String, + line: usize, +} + +impl fmt::Display for SerializableSpan { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.path.rsplit('/').next().unwrap_or_default(), self.line) + } +} + +impl SerializableSpan { + fn from_item(cx: &LateContext<'_>, item: &Item<'_>) -> Self { + Self::from_span(cx, item.ident.span) + } + + fn from_span(cx: &LateContext<'_>, span: Span) -> Self { + let loc: Loc = cx.sess().source_map().lookup_char_pos(span.lo()); + + Self { + path: format!("{}", loc.file.name.prefer_remapped()), + line: loc.line, + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +struct ApplicabilityInfo { + /// Indicates if any of the lint emissions uses multiple spans. This is related to + /// [rustfix#141](https://github.com/rust-lang/rustfix/issues/141) as such suggestions can + /// currently not be applied automatically. + is_multi_part_suggestion: bool, + applicability: Option<usize>, +} + +impl Serialize for ApplicabilityInfo { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut s = serializer.serialize_struct("ApplicabilityInfo", 2)?; + s.serialize_field("is_multi_part_suggestion", &self.is_multi_part_suggestion)?; + if let Some(index) = self.applicability { + s.serialize_field( + "applicability", + &paths::APPLICABILITY_VALUES[index][APPLICABILITY_NAME_INDEX], + )?; + } else { + s.serialize_field("applicability", APPLICABILITY_UNRESOLVED_STR)?; + } + s.end() + } +} + +// ================================================================== +// Configuration +// ================================================================== +#[derive(Debug, Clone, Default)] +pub struct ClippyConfiguration { + name: String, + config_type: &'static str, + default: String, + lints: Vec<String>, + doc: String, + #[allow(dead_code)] + deprecation_reason: Option<&'static str>, +} + +impl ClippyConfiguration { + pub fn new( + name: &'static str, + config_type: &'static str, + default: String, + doc_comment: &'static str, + deprecation_reason: Option<&'static str>, + ) -> Self { + let (lints, doc) = parse_config_field_doc(doc_comment) + .unwrap_or_else(|| (vec![], "[ERROR] MALFORMED DOC COMMENT".to_string())); + + Self { + name: to_kebab(name), + lints, + doc, + config_type, + default, + deprecation_reason, + } + } +} + +fn collect_configs() -> Vec<ClippyConfiguration> { + crate::utils::conf::metadata::get_configuration_metadata() +} + +/// This parses the field documentation of the config struct. +/// +/// ```rust, ignore +/// parse_config_field_doc(cx, "Lint: LINT_NAME_1, LINT_NAME_2. Papa penguin, papa penguin") +/// ``` +/// +/// Would yield: +/// ```rust, ignore +/// Some(["lint_name_1", "lint_name_2"], "Papa penguin, papa penguin") +/// ``` +fn parse_config_field_doc(doc_comment: &str) -> Option<(Vec<String>, String)> { + const DOC_START: &str = " Lint: "; + if_chain! { + if doc_comment.starts_with(DOC_START); + if let Some(split_pos) = doc_comment.find('.'); + then { + let mut doc_comment = doc_comment.to_string(); + let mut documentation = doc_comment.split_off(split_pos); + + // Extract lints + doc_comment.make_ascii_lowercase(); + let lints: Vec<String> = doc_comment.split_off(DOC_START.len()).split(", ").map(str::to_string).collect(); + + // Format documentation correctly + // split off leading `.` from lint name list and indent for correct formatting + documentation = documentation.trim_start_matches('.').trim().replace("\n ", "\n "); + + Some((lints, documentation)) + } else { + None + } + } +} + +/// Transforms a given `snake_case_string` to a tasty `kebab-case-string` +fn to_kebab(config_name: &str) -> String { + config_name.replace('_', "-") +} + +impl fmt::Display for ClippyConfiguration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + CONFIGURATION_VALUE_TEMPLATE!(), + name = self.name, + ty = self.config_type, + doc = self.doc, + default = self.default + ) + } +} + +// ================================================================== +// Lint pass +// ================================================================== +impl<'hir> LateLintPass<'hir> for MetadataCollector { + /// Collecting lint declarations like: + /// ```rust, ignore + /// declare_clippy_lint! { + /// /// ### What it does + /// /// Something IDK. + /// pub SOME_LINT, + /// internal, + /// "Who am I?" + /// } + /// ``` + fn check_item(&mut self, cx: &LateContext<'hir>, item: &'hir Item<'_>) { + if let ItemKind::Static(ty, Mutability::Not, _) = item.kind { + // Normal lint + if_chain! { + // item validation + if is_lint_ref_type(cx, ty); + // blacklist check + let lint_name = sym_to_string(item.ident.name).to_ascii_lowercase(); + if !BLACK_LISTED_LINTS.contains(&lint_name.as_str()); + // metadata extraction + if let Some((group, level)) = get_lint_group_and_level_or_lint(cx, &lint_name, item); + if let Some(mut raw_docs) = extract_attr_docs_or_lint(cx, item); + then { + if let Some(configuration_section) = self.get_lint_configs(&lint_name) { + raw_docs.push_str(&configuration_section); + } + let version = get_lint_version(cx, item); + + self.lints.push(LintMetadata::new( + lint_name, + SerializableSpan::from_item(cx, item), + group, + level, + version, + raw_docs, + )); + } + } + + if_chain! { + if is_deprecated_lint(cx, ty); + // blacklist check + let lint_name = sym_to_string(item.ident.name).to_ascii_lowercase(); + if !BLACK_LISTED_LINTS.contains(&lint_name.as_str()); + // Metadata the little we can get from a deprecated lint + if let Some(raw_docs) = extract_attr_docs_or_lint(cx, item); + then { + let version = get_lint_version(cx, item); + + self.lints.push(LintMetadata::new( + lint_name, + SerializableSpan::from_item(cx, item), + DEPRECATED_LINT_GROUP_STR.to_string(), + DEPRECATED_LINT_LEVEL, + version, + raw_docs, + )); + } + } + } + } + + /// Collecting constant applicability from the actual lint emissions + /// + /// Example: + /// ```rust, ignore + /// span_lint_and_sugg( + /// cx, + /// SOME_LINT, + /// item.span, + /// "Le lint message", + /// "Here comes help:", + /// "#![allow(clippy::all)]", + /// Applicability::MachineApplicable, // <-- Extracts this constant value + /// ); + /// ``` + fn check_expr(&mut self, cx: &LateContext<'hir>, expr: &'hir hir::Expr<'_>) { + if let Some(args) = match_lint_emission(cx, expr) { + let emission_info = extract_emission_info(cx, args); + if emission_info.is_empty() { + // See: + // - src/misc.rs:734:9 + // - src/methods/mod.rs:3545:13 + // - src/methods/mod.rs:3496:13 + // We are basically unable to resolve the lint name itself. + return; + } + + for (lint_name, applicability, is_multi_part) in emission_info { + let app_info = self.applicability_info.entry(lint_name).or_default(); + app_info.applicability = applicability; + app_info.is_multi_part_suggestion = is_multi_part; + } + } + } +} + +// ================================================================== +// Lint definition extraction +// ================================================================== +fn sym_to_string(sym: Symbol) -> String { + sym.as_str().to_string() +} + +fn extract_attr_docs_or_lint(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> { + extract_attr_docs(cx, item).or_else(|| { + lint_collection_error_item(cx, item, "could not collect the lint documentation"); + None + }) +} + +/// This function collects all documentation that has been added to an item using +/// `#[doc = r""]` attributes. Several attributes are aggravated using line breaks +/// +/// ```ignore +/// #[doc = r"Hello world!"] +/// #[doc = r"=^.^="] +/// struct SomeItem {} +/// ``` +/// +/// Would result in `Hello world!\n=^.^=\n` +fn extract_attr_docs(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> { + let attrs = cx.tcx.hir().attrs(item.hir_id()); + let mut lines = attrs.iter().filter_map(ast::Attribute::doc_str); + + if let Some(line) = lines.next() { + let raw_docs = lines.fold(String::from(line.as_str()) + "\n", |s, line| s + line.as_str() + "\n"); + return Some(raw_docs); + } + + None +} + +/// This function may modify the doc comment to ensure that the string can be displayed using a +/// markdown viewer in Clippy's lint list. The following modifications could be applied: +/// * Removal of leading space after a new line. (Important to display tables) +/// * Ensures that code blocks only contain language information +fn cleanup_docs(docs_collection: &Vec<String>) -> String { + let mut in_code_block = false; + let mut is_code_block_rust = false; + + let mut docs = String::new(); + for line in docs_collection { + // Rustdoc hides code lines starting with `# ` and this removes them from Clippy's lint list :) + if is_code_block_rust && line.trim_start().starts_with("# ") { + continue; + } + + // The line should be represented in the lint list, even if it's just an empty line + docs.push('\n'); + if let Some(info) = line.trim_start().strip_prefix("```") { + in_code_block = !in_code_block; + is_code_block_rust = false; + if in_code_block { + let lang = info + .trim() + .split(',') + // remove rustdoc directives + .find(|&s| !matches!(s, "" | "ignore" | "no_run" | "should_panic")) + // if no language is present, fill in "rust" + .unwrap_or("rust"); + docs.push_str("```"); + docs.push_str(lang); + + is_code_block_rust = lang == "rust"; + continue; + } + } + // This removes the leading space that the macro translation introduces + if let Some(stripped_doc) = line.strip_prefix(' ') { + docs.push_str(stripped_doc); + } else if !line.is_empty() { + docs.push_str(line); + } + } + + docs +} + +fn get_lint_version(cx: &LateContext<'_>, item: &Item<'_>) -> String { + extract_clippy_version_value(cx, item).map_or_else( + || VERSION_DEFAULT_STR.to_string(), + |version| version.as_str().to_string(), + ) +} + +fn get_lint_group_and_level_or_lint( + cx: &LateContext<'_>, + lint_name: &str, + item: &Item<'_>, +) -> Option<(String, &'static str)> { + let result = cx.lint_store.check_lint_name( + lint_name, + Some(sym::clippy), + &[Ident::with_dummy_span(sym::clippy)].into_iter().collect(), + ); + if let CheckLintNameResult::Tool(Ok(lint_lst)) = result { + if let Some(group) = get_lint_group(cx, lint_lst[0]) { + if EXCLUDED_LINT_GROUPS.contains(&group.as_str()) { + return None; + } + + if let Some(level) = get_lint_level_from_group(&group) { + Some((group, level)) + } else { + lint_collection_error_item( + cx, + item, + &format!("Unable to determine lint level for found group `{}`", group), + ); + None + } + } else { + lint_collection_error_item(cx, item, "Unable to determine lint group"); + None + } + } else { + lint_collection_error_item(cx, item, "Unable to find lint in lint_store"); + None + } +} + +fn get_lint_group(cx: &LateContext<'_>, lint_id: LintId) -> Option<String> { + for (group_name, lints, _) in cx.lint_store.get_lint_groups() { + if IGNORED_LINT_GROUPS.contains(&group_name) { + continue; + } + + if lints.iter().any(|group_lint| *group_lint == lint_id) { + let group = group_name.strip_prefix(CLIPPY_LINT_GROUP_PREFIX).unwrap_or(group_name); + return Some((*group).to_string()); + } + } + + None +} + +fn get_lint_level_from_group(lint_group: &str) -> Option<&'static str> { + DEFAULT_LINT_LEVELS + .iter() + .find_map(|(group_name, group_level)| (*group_name == lint_group).then_some(*group_level)) +} + +pub(super) fn is_deprecated_lint(cx: &LateContext<'_>, ty: &hir::Ty<'_>) -> bool { + if let hir::TyKind::Path(ref path) = ty.kind { + if let hir::def::Res::Def(DefKind::Struct, def_id) = cx.qpath_res(path, ty.hir_id) { + return match_def_path(cx, def_id, &DEPRECATED_LINT_TYPE); + } + } + + false +} + +fn collect_renames(lints: &mut Vec<LintMetadata>) { + for lint in lints { + let mut collected = String::new(); + let mut names = vec![lint.id.clone()]; + + loop { + if let Some(lint_name) = names.pop() { + for (k, v) in RENAMED_LINTS { + if_chain! { + if let Some(name) = v.strip_prefix(CLIPPY_LINT_GROUP_PREFIX); + if name == lint_name; + if let Some(past_name) = k.strip_prefix(CLIPPY_LINT_GROUP_PREFIX); + then { + write!(collected, RENAME_VALUE_TEMPLATE!(), name = past_name).unwrap(); + names.push(past_name.to_string()); + } + } + } + + continue; + } + + break; + } + + if !collected.is_empty() { + write!(&mut lint.docs, RENAMES_SECTION_TEMPLATE!(), names = collected).unwrap(); + } + } +} + +// ================================================================== +// Lint emission +// ================================================================== +fn lint_collection_error_item(cx: &LateContext<'_>, item: &Item<'_>, message: &str) { + span_lint( + cx, + INTERNAL_METADATA_COLLECTOR, + item.ident.span, + &format!("metadata collection error for `{}`: {}", item.ident.name, message), + ); +} + +// ================================================================== +// Applicability +// ================================================================== +/// This function checks if a given expression is equal to a simple lint emission function call. +/// It will return the function arguments if the emission matched any function. +fn match_lint_emission<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'_>) -> Option<&'hir [hir::Expr<'hir>]> { + LINT_EMISSION_FUNCTIONS + .iter() + .find_map(|emission_fn| match_function_call(cx, expr, emission_fn)) +} + +fn take_higher_applicability(a: Option<usize>, b: Option<usize>) -> Option<usize> { + a.map_or(b, |a| a.max(b.unwrap_or_default()).into()) +} + +fn extract_emission_info<'hir>( + cx: &LateContext<'hir>, + args: &'hir [hir::Expr<'hir>], +) -> Vec<(String, Option<usize>, bool)> { + let mut lints = Vec::new(); + let mut applicability = None; + let mut multi_part = false; + + for arg in args { + let (arg_ty, _) = walk_ptrs_ty_depth(cx.typeck_results().expr_ty(arg)); + + if match_type(cx, arg_ty, &paths::LINT) { + // If we found the lint arg, extract the lint name + let mut resolved_lints = resolve_lints(cx, arg); + lints.append(&mut resolved_lints); + } else if match_type(cx, arg_ty, &paths::APPLICABILITY) { + applicability = resolve_applicability(cx, arg); + } else if arg_ty.is_closure() { + multi_part |= check_is_multi_part(cx, arg); + applicability = applicability.or_else(|| resolve_applicability(cx, arg)); + } + } + + lints + .into_iter() + .map(|lint_name| (lint_name, applicability, multi_part)) + .collect() +} + +/// Resolves the possible lints that this expression could reference +fn resolve_lints<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Vec<String> { + let mut resolver = LintResolver::new(cx); + resolver.visit_expr(expr); + resolver.lints +} + +/// This function tries to resolve the linked applicability to the given expression. +fn resolve_applicability<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Option<usize> { + let mut resolver = ApplicabilityResolver::new(cx); + resolver.visit_expr(expr); + resolver.complete() +} + +fn check_is_multi_part<'hir>(cx: &LateContext<'hir>, closure_expr: &'hir hir::Expr<'hir>) -> bool { + if let ExprKind::Closure(&Closure { body, .. }) = closure_expr.kind { + let mut scanner = IsMultiSpanScanner::new(cx); + intravisit::walk_body(&mut scanner, cx.tcx.hir().body(body)); + return scanner.is_multi_part(); + } else if let Some(local) = get_parent_local(cx, closure_expr) { + if let Some(local_init) = local.init { + return check_is_multi_part(cx, local_init); + } + } + + false +} + +struct LintResolver<'a, 'hir> { + cx: &'a LateContext<'hir>, + lints: Vec<String>, +} + +impl<'a, 'hir> LintResolver<'a, 'hir> { + fn new(cx: &'a LateContext<'hir>) -> Self { + Self { + cx, + lints: Vec::<String>::default(), + } + } +} + +impl<'a, 'hir> intravisit::Visitor<'hir> for LintResolver<'a, 'hir> { + type NestedFilter = nested_filter::All; + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) { + if_chain! { + if let ExprKind::Path(qpath) = &expr.kind; + if let QPath::Resolved(_, path) = qpath; + + let (expr_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(expr)); + if match_type(self.cx, expr_ty, &paths::LINT); + then { + if let hir::def::Res::Def(DefKind::Static(..), _) = path.res { + let lint_name = last_path_segment(qpath).ident.name; + self.lints.push(sym_to_string(lint_name).to_ascii_lowercase()); + } else if let Some(local) = get_parent_local(self.cx, expr) { + if let Some(local_init) = local.init { + intravisit::walk_expr(self, local_init); + } + } + } + } + + intravisit::walk_expr(self, expr); + } +} + +/// This visitor finds the highest applicability value in the visited expressions +struct ApplicabilityResolver<'a, 'hir> { + cx: &'a LateContext<'hir>, + /// This is the index of highest `Applicability` for `paths::APPLICABILITY_VALUES` + applicability_index: Option<usize>, +} + +impl<'a, 'hir> ApplicabilityResolver<'a, 'hir> { + fn new(cx: &'a LateContext<'hir>) -> Self { + Self { + cx, + applicability_index: None, + } + } + + fn add_new_index(&mut self, new_index: usize) { + self.applicability_index = take_higher_applicability(self.applicability_index, Some(new_index)); + } + + fn complete(self) -> Option<usize> { + self.applicability_index + } +} + +impl<'a, 'hir> intravisit::Visitor<'hir> for ApplicabilityResolver<'a, 'hir> { + type NestedFilter = nested_filter::All; + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_path(&mut self, path: &'hir hir::Path<'hir>, _id: hir::HirId) { + for (index, enum_value) in paths::APPLICABILITY_VALUES.iter().enumerate() { + if match_path(path, enum_value) { + self.add_new_index(index); + return; + } + } + } + + fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) { + let (expr_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(expr)); + + if_chain! { + if match_type(self.cx, expr_ty, &paths::APPLICABILITY); + if let Some(local) = get_parent_local(self.cx, expr); + if let Some(local_init) = local.init; + then { + intravisit::walk_expr(self, local_init); + } + }; + + intravisit::walk_expr(self, expr); + } +} + +/// This returns the parent local node if the expression is a reference one +fn get_parent_local<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Option<&'hir hir::Local<'hir>> { + if let ExprKind::Path(QPath::Resolved(_, path)) = expr.kind { + if let hir::def::Res::Local(local_hir) = path.res { + return get_parent_local_hir_id(cx, local_hir); + } + } + + None +} + +fn get_parent_local_hir_id<'hir>(cx: &LateContext<'hir>, hir_id: hir::HirId) -> Option<&'hir hir::Local<'hir>> { + let map = cx.tcx.hir(); + + match map.find(map.get_parent_node(hir_id)) { + Some(hir::Node::Local(local)) => Some(local), + Some(hir::Node::Pat(pattern)) => get_parent_local_hir_id(cx, pattern.hir_id), + _ => None, + } +} + +/// This visitor finds the highest applicability value in the visited expressions +struct IsMultiSpanScanner<'a, 'hir> { + cx: &'a LateContext<'hir>, + suggestion_count: usize, +} + +impl<'a, 'hir> IsMultiSpanScanner<'a, 'hir> { + fn new(cx: &'a LateContext<'hir>) -> Self { + Self { + cx, + suggestion_count: 0, + } + } + + /// Add a new single expression suggestion to the counter + fn add_single_span_suggestion(&mut self) { + self.suggestion_count += 1; + } + + /// Signals that a suggestion with possible multiple spans was found + fn add_multi_part_suggestion(&mut self) { + self.suggestion_count += 2; + } + + /// Checks if the suggestions include multiple spans + fn is_multi_part(&self) -> bool { + self.suggestion_count > 1 + } +} + +impl<'a, 'hir> intravisit::Visitor<'hir> for IsMultiSpanScanner<'a, 'hir> { + type NestedFilter = nested_filter::All; + + fn nested_visit_map(&mut self) -> Self::Map { + self.cx.tcx.hir() + } + + fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) { + // Early return if the lint is already multi span + if self.is_multi_part() { + return; + } + + match &expr.kind { + ExprKind::Call(fn_expr, _args) => { + let found_function = SUGGESTION_FUNCTIONS + .iter() + .any(|func_path| match_function_call(self.cx, fn_expr, func_path).is_some()); + if found_function { + // These functions are all multi part suggestions + self.add_single_span_suggestion(); + } + }, + ExprKind::MethodCall(path, arg, _arg_span) => { + let (self_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(&arg[0])); + if match_type(self.cx, self_ty, &paths::DIAGNOSTIC_BUILDER) { + let called_method = path.ident.name.as_str().to_string(); + for (method_name, is_multi_part) in &SUGGESTION_DIAGNOSTIC_BUILDER_METHODS { + if *method_name == called_method { + if *is_multi_part { + self.add_multi_part_suggestion(); + } else { + self.add_single_span_suggestion(); + } + break; + } + } + } + }, + _ => {}, + } + + intravisit::walk_expr(self, expr); + } +} diff --git a/src/tools/clippy/clippy_lints/src/utils/mod.rs b/src/tools/clippy/clippy_lints/src/utils/mod.rs new file mode 100644 index 000000000..787e9fd98 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/utils/mod.rs @@ -0,0 +1,5 @@ +pub mod author; +pub mod conf; +pub mod dump_hir; +#[cfg(feature = "internal")] +pub mod internal_lints; diff --git a/src/tools/clippy/clippy_lints/src/vec.rs b/src/tools/clippy/clippy_lints/src/vec.rs new file mode 100644 index 000000000..297a80e57 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/vec.rs @@ -0,0 +1,164 @@ +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::higher; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_copy; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability}; +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; + +#[expect(clippy::module_name_repetitions)] +#[derive(Copy, Clone)] +pub struct UselessVec { + pub too_large_for_stack: u64, +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for usage of `&vec![..]` when using `&[..]` would + /// be possible. + /// + /// ### Why is this bad? + /// This is less efficient. + /// + /// ### Example + /// ```rust + /// fn foo(_x: &[u8]) {} + /// + /// foo(&vec![1, 2]); + /// ``` + /// + /// Use instead: + /// ```rust + /// # fn foo(_x: &[u8]) {} + /// foo(&[1, 2]); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USELESS_VEC, + perf, + "useless `vec!`" +} + +impl_lint_pass!(UselessVec => [USELESS_VEC]); + +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 `&[_]` + 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); + then { + self.check_vec_macro(cx, &vec_args, mutability, expr.span); + } + } + + // search for `for _ in vec![…]` + 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))); + 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); + } + } + } +} + +impl UselessVec { + fn check_vec_macro<'tcx>( + self, + cx: &LateContext<'tcx>, + vec_args: &higher::VecArgs<'tcx>, + mutability: Mutability, + span: Span, + ) { + 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) { + #[expect(clippy::cast_possible_truncation)] + if len_constant as u64 * size_of(cx, elem) > self.too_large_for_stack { + 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) + ) + }, + } + } else { + return; + } + }, + higher::VecArgs::Vec(args) => { + if let Some(last) = args.iter().last() { + if args.len() as u64 * size_of(cx, last) > self.too_large_for_stack { + return; + } + let span = args[0].span.to(last.span); + + match mutability { + Mutability::Mut => { + format!( + "&mut [{}]", + snippet_with_applicability(cx, span, "..", &mut applicability) + ) + }, + Mutability::Not => { + format!("&[{}]", snippet_with_applicability(cx, span, "..", &mut applicability)) + }, + } + } else { + match mutability { + Mutability::Mut => "&mut []".into(), + Mutability::Not => "&[]".into(), + } + } + }, + }; + + span_lint_and_sugg( + cx, + USELESS_VEC, + span, + "useless use of `vec!`", + "you can use a slice directly", + snippet, + applicability, + ); + } +} + +fn size_of(cx: &LateContext<'_>, expr: &Expr<'_>) -> u64 { + let ty = cx.typeck_results().expr_ty_adjusted(expr); + cx.layout_of(ty).map_or(0, |l| l.size.bytes()) +} + +/// Returns the item type of the vector (i.e., the `T` in `Vec<T>`). +fn vec_type(ty: Ty<'_>) -> Ty<'_> { + if let ty::Adt(_, substs) = ty.kind() { + substs.type_at(0) + } else { + panic!("The type of `vec!` is a not a struct?"); + } +} diff --git a/src/tools/clippy/clippy_lints/src/vec_init_then_push.rs b/src/tools/clippy/clippy_lints/src/vec_init_then_push.rs new file mode 100644 index 000000000..d77a21d66 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/vec_init_then_push.rs @@ -0,0 +1,225 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::higher::{get_vec_init_kind, VecInitKind}; +use clippy_utils::source::snippet; +use clippy_utils::visitors::for_each_local_use_after_expr; +use clippy_utils::{get_parent_expr, path_to_local_id}; +use core::ops::ControlFlow; +use rustc_errors::Applicability; +use rustc_hir::def::Res; +use rustc_hir::{ + BindingAnnotation, Block, Expr, ExprKind, HirId, Local, Mutability, PatKind, QPath, Stmt, StmtKind, UnOp, +}; +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, Symbol}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for calls to `push` immediately after creating a new `Vec`. + /// + /// If the `Vec` is created using `with_capacity` this will only lint if the capacity is a + /// constant and the number of pushes is greater than or equal to the initial capacity. + /// + /// If the `Vec` is extended after the initial sequence of pushes and it was default initialized + /// then this will only lint after there were at least four pushes. This number may change in + /// the future. + /// + /// ### Why is this bad? + /// The `vec![]` macro is both more performant and easier to read than + /// multiple `push` calls. + /// + /// ### Example + /// ```rust + /// let mut v = Vec::new(); + /// v.push(0); + /// ``` + /// Use instead: + /// ```rust + /// let v = vec![0]; + /// ``` + #[clippy::version = "1.51.0"] + pub VEC_INIT_THEN_PUSH, + perf, + "`push` immediately after `Vec` creation" +} + +impl_lint_pass!(VecInitThenPush => [VEC_INIT_THEN_PUSH]); + +#[derive(Default)] +pub struct VecInitThenPush { + searcher: Option<VecPushSearcher>, +} + +struct VecPushSearcher { + local_id: HirId, + init: VecInitKind, + lhs_is_let: bool, + let_ty_span: Option<Span>, + name: Symbol, + err_span: Span, + found: u128, + last_push_expr: HirId, +} +impl VecPushSearcher { + fn display_err(&self, cx: &LateContext<'_>) { + let required_pushes_before_extension = match self.init { + _ if self.found == 0 => return, + VecInitKind::WithConstCapacity(x) if x > self.found => return, + VecInitKind::WithConstCapacity(x) => x, + VecInitKind::WithExprCapacity(_) => return, + _ => 3, + }; + + let mut needs_mut = false; + let res = for_each_local_use_after_expr(cx, self.local_id, self.last_push_expr, |e| { + let Some(parent) = get_parent_expr(cx, e) else { + return ControlFlow::Continue(()) + }; + let adjusted_ty = cx.typeck_results().expr_ty_adjusted(e); + let adjusted_mut = adjusted_ty.ref_mutability().unwrap_or(Mutability::Not); + needs_mut |= adjusted_mut == Mutability::Mut; + match parent.kind { + ExprKind::AddrOf(_, Mutability::Mut, _) => { + needs_mut = true; + return ControlFlow::Break(true); + }, + ExprKind::Unary(UnOp::Deref, _) | ExprKind::Index(..) if !needs_mut => { + let mut last_place = parent; + while let Some(parent) = get_parent_expr(cx, last_place) { + if matches!(parent.kind, ExprKind::Unary(UnOp::Deref, _) | ExprKind::Field(..)) + || matches!(parent.kind, ExprKind::Index(e, _) if e.hir_id == last_place.hir_id) + { + last_place = parent; + } else { + break; + } + } + needs_mut |= cx.typeck_results().expr_ty_adjusted(last_place).ref_mutability() + == Some(Mutability::Mut) + || get_parent_expr(cx, last_place) + .map_or(false, |e| matches!(e.kind, ExprKind::AddrOf(_, Mutability::Mut, _))); + }, + ExprKind::MethodCall(_, [recv, ..], _) + if recv.hir_id == e.hir_id + && adjusted_mut == Mutability::Mut + && !adjusted_ty.peel_refs().is_slice() => + { + // No need to set `needs_mut` to true. The receiver will be either explicitly borrowed, or it will + // be implicitly borrowed via an adjustment. Both of these cases are already handled by this point. + return ControlFlow::Break(true); + }, + ExprKind::Assign(lhs, ..) if e.hir_id == lhs.hir_id => { + needs_mut = true; + return ControlFlow::Break(false); + }, + _ => (), + } + ControlFlow::Continue(()) + }); + + // Avoid allocating small `Vec`s when they'll be extended right after. + if res == ControlFlow::Break(true) && self.found <= required_pushes_before_extension { + return; + } + + let mut s = if self.lhs_is_let { + String::from("let ") + } else { + String::new() + }; + if needs_mut { + s.push_str("mut "); + } + s.push_str(self.name.as_str()); + if let Some(span) = self.let_ty_span { + s.push_str(": "); + s.push_str(&snippet(cx, span, "_")); + } + s.push_str(" = vec![..];"); + + span_lint_and_sugg( + cx, + VEC_INIT_THEN_PUSH, + self.err_span, + "calls to `push` immediately after creation", + "consider using the `vec![]` macro", + s, + Applicability::HasPlaceholders, + ); + } +} + +impl<'tcx> LateLintPass<'tcx> for VecInitThenPush { + fn check_block(&mut self, _: &LateContext<'tcx>, _: &'tcx Block<'tcx>) { + self.searcher = None; + } + + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { + if let Some(init_expr) = local.init + && let PatKind::Binding(BindingAnnotation::Mutable, id, name, None) = local.pat.kind + && !in_external_macro(cx.sess(), local.span) + && let Some(init) = get_vec_init_kind(cx, init_expr) + && !matches!(init, VecInitKind::WithExprCapacity(_)) + { + self.searcher = Some(VecPushSearcher { + local_id: id, + init, + lhs_is_let: true, + name: name.name, + let_ty_span: local.ty.map(|ty| ty.span), + err_span: local.span, + found: 0, + last_push_expr: init_expr.hir_id, + }); + } + } + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if self.searcher.is_none() + && let ExprKind::Assign(left, right, _) = expr.kind + && let ExprKind::Path(QPath::Resolved(None, path)) = left.kind + && let [name] = &path.segments + && let Res::Local(id) = path.res + && !in_external_macro(cx.sess(), expr.span) + && let Some(init) = get_vec_init_kind(cx, right) + && !matches!(init, VecInitKind::WithExprCapacity(_)) + { + self.searcher = Some(VecPushSearcher { + local_id: id, + init, + lhs_is_let: false, + let_ty_span: None, + name: name.ident.name, + err_span: expr.span, + found: 0, + last_push_expr: expr.hir_id, + }); + } + } + + fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) { + if let Some(searcher) = self.searcher.take() { + if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = stmt.kind + && let ExprKind::MethodCall(name, [self_arg, _], _) = expr.kind + && path_to_local_id(self_arg, searcher.local_id) + && name.ident.as_str() == "push" + { + self.searcher = Some(VecPushSearcher { + found: searcher.found + 1, + err_span: searcher.err_span.to(stmt.span), + last_push_expr: expr.hir_id, + .. searcher + }); + } else { + searcher.display_err(cx); + } + } + } + + fn check_block_post(&mut self, cx: &LateContext<'tcx>, _: &'tcx Block<'tcx>) { + if let Some(searcher) = self.searcher.take() { + searcher.display_err(cx); + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/vec_resize_to_zero.rs b/src/tools/clippy/clippy_lints/src/vec_resize_to_zero.rs new file mode 100644 index 000000000..0fee3e812 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/vec_resize_to_zero.rs @@ -0,0 +1,64 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{match_def_path, paths}; +use if_chain::if_chain; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::source_map::Spanned; + +declare_clippy_lint! { + /// ### What it does + /// Finds occurrences of `Vec::resize(0, an_int)` + /// + /// ### Why is this bad? + /// This is probably an argument inversion mistake. + /// + /// ### Example + /// ```rust + /// vec!(1, 2, 3, 4, 5).resize(0, 5) + /// ``` + /// + /// Use instead: + /// ```rust + /// vec!(1, 2, 3, 4, 5).clear() + /// ``` + #[clippy::version = "1.46.0"] + pub VEC_RESIZE_TO_ZERO, + correctness, + "emptying a vector with `resize(0, an_int)` instead of `clear()` is probably an argument inversion mistake" +} + +declare_lint_pass!(VecResizeToZero => [VEC_RESIZE_TO_ZERO]); + +impl<'tcx> LateLintPass<'tcx> for VecResizeToZero { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if_chain! { + if let hir::ExprKind::MethodCall(path_segment, args, _) = expr.kind; + if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); + if match_def_path(cx, method_def_id, &paths::VEC_RESIZE) && args.len() == 3; + if let ExprKind::Lit(Spanned { node: LitKind::Int(0, _), .. }) = args[1].kind; + if let ExprKind::Lit(Spanned { node: LitKind::Int(..), .. }) = args[2].kind; + then { + let method_call_span = expr.span.with_lo(path_segment.ident.span.lo()); + span_lint_and_then( + cx, + VEC_RESIZE_TO_ZERO, + expr.span, + "emptying a vector with `resize`", + |db| { + db.help("the arguments may be inverted..."); + db.span_suggestion( + method_call_span, + "...or you can empty the vector with", + "clear()".to_string(), + Applicability::MaybeIncorrect, + ); + }, + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/verbose_file_reads.rs b/src/tools/clippy/clippy_lints/src/verbose_file_reads.rs new file mode 100644 index 000000000..8e2ddd225 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/verbose_file_reads.rs @@ -0,0 +1,88 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::paths; +use clippy_utils::ty::match_type; +use if_chain::if_chain; +use rustc_hir::{Expr, ExprKind, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of File::read_to_end and File::read_to_string. + /// + /// ### Why is this bad? + /// `fs::{read, read_to_string}` provide the same functionality when `buf` is empty with fewer imports and no intermediate values. + /// See also: [fs::read docs](https://doc.rust-lang.org/std/fs/fn.read.html), [fs::read_to_string docs](https://doc.rust-lang.org/std/fs/fn.read_to_string.html) + /// + /// ### Example + /// ```rust,no_run + /// # use std::io::Read; + /// # use std::fs::File; + /// let mut f = File::open("foo.txt").unwrap(); + /// let mut bytes = Vec::new(); + /// f.read_to_end(&mut bytes).unwrap(); + /// ``` + /// Can be written more concisely as + /// ```rust,no_run + /// # use std::fs; + /// let mut bytes = fs::read("foo.txt").unwrap(); + /// ``` + #[clippy::version = "1.44.0"] + pub VERBOSE_FILE_READS, + restriction, + "use of `File::read_to_end` or `File::read_to_string`" +} + +declare_lint_pass!(VerboseFileReads => [VERBOSE_FILE_READS]); + +impl<'tcx> LateLintPass<'tcx> for VerboseFileReads { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if is_file_read_to_end(cx, expr) { + span_lint_and_help( + cx, + VERBOSE_FILE_READS, + expr.span, + "use of `File::read_to_end`", + None, + "consider using `fs::read` instead", + ); + } else if is_file_read_to_string(cx, expr) { + span_lint_and_help( + cx, + VERBOSE_FILE_READS, + expr.span, + "use of `File::read_to_string`", + None, + "consider using `fs::read_to_string` instead", + ); + } + } +} + +fn is_file_read_to_end<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { + if_chain! { + if let ExprKind::MethodCall(method_name, exprs, _) = expr.kind; + if method_name.ident.as_str() == "read_to_end"; + if let ExprKind::Path(QPath::Resolved(None, _)) = &exprs[0].kind; + let ty = cx.typeck_results().expr_ty(&exprs[0]); + if match_type(cx, ty, &paths::FILE); + then { + return true + } + } + false +} + +fn is_file_read_to_string<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { + if_chain! { + if let ExprKind::MethodCall(method_name, exprs, _) = expr.kind; + if method_name.ident.as_str() == "read_to_string"; + if let ExprKind::Path(QPath::Resolved(None, _)) = &exprs[0].kind; + let ty = cx.typeck_results().expr_ty(&exprs[0]); + if match_type(cx, ty, &paths::FILE); + then { + return true + } + } + false +} diff --git a/src/tools/clippy/clippy_lints/src/wildcard_imports.rs b/src/tools/clippy/clippy_lints/src/wildcard_imports.rs new file mode 100644 index 000000000..5418eca38 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/wildcard_imports.rs @@ -0,0 +1,222 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_test_module_or_function; +use clippy_utils::source::{snippet, snippet_with_applicability}; +use if_chain::if_chain; +use rustc_errors::Applicability; +use rustc_hir::{ + def::{DefKind, Res}, + Item, ItemKind, PathSegment, UseKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::kw; +use rustc_span::{sym, BytePos}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `use Enum::*`. + /// + /// ### Why is this bad? + /// It is usually better style to use the prefixed name of + /// an enumeration variant, rather than importing variants. + /// + /// ### Known problems + /// Old-style enumerations that prefix the variants are + /// still around. + /// + /// ### Example + /// ```rust + /// use std::cmp::Ordering::*; + /// + /// # fn foo(_: std::cmp::Ordering) {} + /// foo(Less); + /// ``` + /// + /// Use instead: + /// ```rust + /// use std::cmp::Ordering; + /// + /// # fn foo(_: Ordering) {} + /// foo(Ordering::Less) + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ENUM_GLOB_USE, + pedantic, + "use items that import all variants of an enum" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard imports `use _::*`. + /// + /// ### Why is this bad? + /// wildcard imports can pollute the namespace. This is especially bad if + /// you try to import something through a wildcard, that already has been imported by name from + /// a different source: + /// + /// ```rust,ignore + /// use crate1::foo; // Imports a function named foo + /// use crate2::*; // Has a function named foo + /// + /// foo(); // Calls crate1::foo + /// ``` + /// + /// This can lead to confusing error messages at best and to unexpected behavior at worst. + /// + /// ### Exceptions + /// Wildcard imports are allowed from modules named `prelude`. Many crates (including the standard library) + /// provide modules named "prelude" specifically designed for wildcard import. + /// + /// `use super::*` is allowed in test modules. This is defined as any module with "test" in the name. + /// + /// These exceptions can be disabled using the `warn-on-all-wildcard-imports` configuration flag. + /// + /// ### Known problems + /// If macros are imported through the wildcard, this macro is not included + /// by the suggestion and has to be added by hand. + /// + /// Applying the suggestion when explicit imports of the things imported with a glob import + /// exist, may result in `unused_imports` warnings. + /// + /// ### Example + /// ```rust,ignore + /// use crate1::*; + /// + /// foo(); + /// ``` + /// + /// Use instead: + /// ```rust,ignore + /// use crate1::foo; + /// + /// foo(); + /// ``` + #[clippy::version = "1.43.0"] + pub WILDCARD_IMPORTS, + pedantic, + "lint `use _::*` statements" +} + +#[derive(Default)] +pub struct WildcardImports { + warn_on_all: bool, + test_modules_deep: u32, +} + +impl WildcardImports { + pub fn new(warn_on_all: bool) -> Self { + Self { + warn_on_all, + test_modules_deep: 0, + } + } +} + +impl_lint_pass!(WildcardImports => [ENUM_GLOB_USE, WILDCARD_IMPORTS]); + +impl LateLintPass<'_> for WildcardImports { + fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if is_test_module_or_function(cx.tcx, item) { + self.test_modules_deep = self.test_modules_deep.saturating_add(1); + } + let module = cx.tcx.parent_module_from_def_id(item.def_id); + if cx.tcx.visibility(item.def_id) != ty::Visibility::Restricted(module.to_def_id()) { + return; + } + if_chain! { + if let ItemKind::Use(use_path, UseKind::Glob) = &item.kind; + if self.warn_on_all || !self.check_exceptions(item, use_path.segments); + let used_imports = cx.tcx.names_imported_by_glob_use(item.def_id); + if !used_imports.is_empty(); // Already handled by `unused_imports` + then { + let mut applicability = Applicability::MachineApplicable; + let import_source_snippet = snippet_with_applicability(cx, use_path.span, "..", &mut applicability); + let (span, braced_glob) = if import_source_snippet.is_empty() { + // This is a `_::{_, *}` import + // In this case `use_path.span` is empty and ends directly in front of the `*`, + // so we need to extend it by one byte. + ( + use_path.span.with_hi(use_path.span.hi() + BytePos(1)), + true, + ) + } else { + // In this case, the `use_path.span` ends right before the `::*`, so we need to + // extend it up to the `*`. Since it is hard to find the `*` in weird + // formattings like `use _ :: *;`, we extend it up to, but not including the + // `;`. In nested imports, like `use _::{inner::*, _}` there is no `;` and we + // can just use the end of the item span + let mut span = use_path.span.with_hi(item.span.hi()); + if snippet(cx, span, "").ends_with(';') { + span = use_path.span.with_hi(item.span.hi() - BytePos(1)); + } + ( + span, false, + ) + }; + + let imports_string = if used_imports.len() == 1 { + used_imports.iter().next().unwrap().to_string() + } else { + let mut imports = used_imports + .iter() + .map(ToString::to_string) + .collect::<Vec<_>>(); + imports.sort(); + if braced_glob { + imports.join(", ") + } else { + format!("{{{}}}", imports.join(", ")) + } + }; + + let sugg = if braced_glob { + imports_string + } else { + format!("{}::{}", import_source_snippet, imports_string) + }; + + let (lint, message) = if let Res::Def(DefKind::Enum, _) = use_path.res { + (ENUM_GLOB_USE, "usage of wildcard import for enum variants") + } else { + (WILDCARD_IMPORTS, "usage of wildcard import") + }; + + span_lint_and_sugg( + cx, + lint, + span, + message, + "try", + sugg, + applicability, + ); + } + } + } + + fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { + if is_test_module_or_function(cx.tcx, item) { + self.test_modules_deep = self.test_modules_deep.saturating_sub(1); + } + } +} + +impl WildcardImports { + fn check_exceptions(&self, item: &Item<'_>, segments: &[PathSegment<'_>]) -> bool { + item.span.from_expansion() + || is_prelude_import(segments) + || (is_super_only_import(segments) && self.test_modules_deep > 0) + } +} + +// Allow "...prelude::..::*" imports. +// Many crates have a prelude, and it is imported as a glob by design. +fn is_prelude_import(segments: &[PathSegment<'_>]) -> bool { + segments.iter().any(|ps| ps.ident.name == sym::prelude) +} + +// Allow "super::*" imports in tests. +fn is_super_only_import(segments: &[PathSegment<'_>]) -> bool { + segments.len() == 1 && segments[0].ident.name == kw::Super +} diff --git a/src/tools/clippy/clippy_lints/src/write.rs b/src/tools/clippy/clippy_lints/src/write.rs new file mode 100644 index 000000000..32718200c --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/write.rs @@ -0,0 +1,709 @@ +use std::borrow::Cow; +use std::iter; +use std::ops::{Deref, Range}; + +use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::{snippet_opt, snippet_with_applicability}; +use rustc_ast::ast::{Expr, ExprKind, Impl, Item, ItemKind, MacCall, Path, StrLit, StrStyle}; +use rustc_ast::token::{self, LitKind}; +use rustc_ast::tokenstream::TokenStream; +use rustc_errors::{Applicability, DiagnosticBuilder}; +use rustc_lexer::unescape::{self, EscapeError}; +use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; +use rustc_parse::parser; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::{kw, Symbol}; +use rustc_span::{sym, BytePos, InnerSpan, Span, DUMMY_SP}; + +declare_clippy_lint! { + /// ### What it does + /// This lint warns when you use `println!("")` to + /// print a newline. + /// + /// ### Why is this bad? + /// You should use `println!()`, which is simpler. + /// + /// ### Example + /// ```rust + /// println!(""); + /// ``` + /// + /// Use instead: + /// ```rust + /// println!(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub PRINTLN_EMPTY_STRING, + style, + "using `println!(\"\")` with an empty string" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint warns when you use `print!()` with a format + /// string that ends in a newline. + /// + /// ### Why is this bad? + /// You should use `println!()` instead, which appends the + /// newline. + /// + /// ### Example + /// ```rust + /// # let name = "World"; + /// print!("Hello {}!\n", name); + /// ``` + /// use println!() instead + /// ```rust + /// # let name = "World"; + /// println!("Hello {}!", name); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub PRINT_WITH_NEWLINE, + style, + "using `print!()` with a format string that ends in a single newline" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for printing on *stdout*. The purpose of this lint + /// is to catch debugging remnants. + /// + /// ### Why is this bad? + /// People often print on *stdout* while debugging an + /// application and might forget to remove those prints afterward. + /// + /// ### Known problems + /// * Only catches `print!` and `println!` calls. + /// * The lint level is unaffected by crate attributes. The level can still + /// be set for functions, modules and other items. To change the level for + /// the entire crate, please use command line flags. More information and a + /// configuration example can be found in [clippy#6610]. + /// + /// [clippy#6610]: https://github.com/rust-lang/rust-clippy/issues/6610#issuecomment-977120558 + /// + /// ### Example + /// ```rust + /// println!("Hello world!"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub PRINT_STDOUT, + restriction, + "printing on stdout" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for printing on *stderr*. The purpose of this lint + /// is to catch debugging remnants. + /// + /// ### Why is this bad? + /// People often print on *stderr* while debugging an + /// application and might forget to remove those prints afterward. + /// + /// ### Known problems + /// * Only catches `eprint!` and `eprintln!` calls. + /// * The lint level is unaffected by crate attributes. The level can still + /// be set for functions, modules and other items. To change the level for + /// the entire crate, please use command line flags. More information and a + /// configuration example can be found in [clippy#6610]. + /// + /// [clippy#6610]: https://github.com/rust-lang/rust-clippy/issues/6610#issuecomment-977120558 + /// + /// ### Example + /// ```rust + /// eprintln!("Hello world!"); + /// ``` + #[clippy::version = "1.50.0"] + pub PRINT_STDERR, + restriction, + "printing on stderr" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for use of `Debug` formatting. The purpose of this + /// lint is to catch debugging remnants. + /// + /// ### Why is this bad? + /// The purpose of the `Debug` trait is to facilitate + /// debugging Rust code. It should not be used in user-facing output. + /// + /// ### Example + /// ```rust + /// # let foo = "bar"; + /// println!("{:?}", foo); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USE_DEBUG, + restriction, + "use of `Debug`-based formatting" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint warns about the use of literals as `print!`/`println!` args. + /// + /// ### Why is this bad? + /// Using literals as `println!` args is inefficient + /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary + /// (i.e., just put the literal in the format string) + /// + /// ### Known problems + /// Will also warn with macro calls as arguments that expand to literals + /// -- e.g., `println!("{}", env!("FOO"))`. + /// + /// ### Example + /// ```rust + /// println!("{}", "foo"); + /// ``` + /// use the literal without formatting: + /// ```rust + /// println!("foo"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub PRINT_LITERAL, + style, + "printing a literal with a format string" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint warns when you use `writeln!(buf, "")` to + /// print a newline. + /// + /// ### Why is this bad? + /// You should use `writeln!(buf)`, which is simpler. + /// + /// ### Example + /// ```rust + /// # use std::fmt::Write; + /// # let mut buf = String::new(); + /// writeln!(buf, ""); + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::fmt::Write; + /// # let mut buf = String::new(); + /// writeln!(buf); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WRITELN_EMPTY_STRING, + style, + "using `writeln!(buf, \"\")` with an empty string" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint warns when you use `write!()` with a format + /// string that + /// ends in a newline. + /// + /// ### Why is this bad? + /// You should use `writeln!()` instead, which appends the + /// newline. + /// + /// ### Example + /// ```rust + /// # use std::fmt::Write; + /// # let mut buf = String::new(); + /// # let name = "World"; + /// write!(buf, "Hello {}!\n", name); + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::fmt::Write; + /// # let mut buf = String::new(); + /// # let name = "World"; + /// writeln!(buf, "Hello {}!", name); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WRITE_WITH_NEWLINE, + style, + "using `write!()` with a format string that ends in a single newline" +} + +declare_clippy_lint! { + /// ### What it does + /// This lint warns about the use of literals as `write!`/`writeln!` args. + /// + /// ### Why is this bad? + /// Using literals as `writeln!` args is inefficient + /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary + /// (i.e., just put the literal in the format string) + /// + /// ### Known problems + /// Will also warn with macro calls as arguments that expand to literals + /// -- e.g., `writeln!(buf, "{}", env!("FOO"))`. + /// + /// ### Example + /// ```rust + /// # use std::fmt::Write; + /// # let mut buf = String::new(); + /// writeln!(buf, "{}", "foo"); + /// ``` + /// + /// Use instead: + /// ```rust + /// # use std::fmt::Write; + /// # let mut buf = String::new(); + /// writeln!(buf, "foo"); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub WRITE_LITERAL, + style, + "writing a literal with a format string" +} + +#[derive(Default)] +pub struct Write { + in_debug_impl: bool, +} + +impl_lint_pass!(Write => [ + PRINT_WITH_NEWLINE, + PRINTLN_EMPTY_STRING, + PRINT_STDOUT, + PRINT_STDERR, + USE_DEBUG, + PRINT_LITERAL, + WRITE_WITH_NEWLINE, + WRITELN_EMPTY_STRING, + WRITE_LITERAL +]); + +impl EarlyLintPass for Write { + fn check_item(&mut self, _: &EarlyContext<'_>, item: &Item) { + if let ItemKind::Impl(box Impl { + of_trait: Some(trait_ref), + .. + }) = &item.kind + { + let trait_name = trait_ref + .path + .segments + .iter() + .last() + .expect("path has at least one segment") + .ident + .name; + if trait_name == sym::Debug { + self.in_debug_impl = true; + } + } + } + + fn check_item_post(&mut self, _: &EarlyContext<'_>, _: &Item) { + self.in_debug_impl = false; + } + + fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) { + fn is_build_script(cx: &EarlyContext<'_>) -> bool { + // Cargo sets the crate name for build scripts to `build_script_build` + cx.sess() + .opts + .crate_name + .as_ref() + .map_or(false, |crate_name| crate_name == "build_script_build") + } + + if mac.path == sym!(print) { + if !is_build_script(cx) { + span_lint(cx, PRINT_STDOUT, mac.span(), "use of `print!`"); + } + self.lint_print_with_newline(cx, mac); + } else if mac.path == sym!(println) { + if !is_build_script(cx) { + span_lint(cx, PRINT_STDOUT, mac.span(), "use of `println!`"); + } + self.lint_println_empty_string(cx, mac); + } else if mac.path == sym!(eprint) { + span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprint!`"); + self.lint_print_with_newline(cx, mac); + } else if mac.path == sym!(eprintln) { + span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprintln!`"); + self.lint_println_empty_string(cx, mac); + } else if mac.path == sym!(write) { + if let (Some(fmt_str), dest) = self.check_tts(cx, mac.args.inner_tokens(), true) { + if check_newlines(&fmt_str) { + let (nl_span, only_nl) = newline_span(&fmt_str); + let nl_span = match (dest, only_nl) { + // Special case of `write!(buf, "\n")`: Mark everything from the end of + // `buf` for removal so no trailing comma [`writeln!(buf, )`] remains. + (Some(dest_expr), true) => nl_span.with_lo(dest_expr.span.hi()), + _ => nl_span, + }; + span_lint_and_then( + cx, + WRITE_WITH_NEWLINE, + mac.span(), + "using `write!()` with a format string that ends in a single newline", + |err| { + err.multipart_suggestion( + "use `writeln!()` instead", + vec![(mac.path.span, String::from("writeln")), (nl_span, String::new())], + Applicability::MachineApplicable, + ); + }, + ); + } + } + } else if mac.path == sym!(writeln) { + if let (Some(fmt_str), expr) = self.check_tts(cx, mac.args.inner_tokens(), true) { + if fmt_str.symbol == kw::Empty { + let mut applicability = Applicability::MachineApplicable; + let suggestion = if let Some(e) = expr { + snippet_with_applicability(cx, e.span, "v", &mut applicability) + } else { + applicability = Applicability::HasPlaceholders; + Cow::Borrowed("v") + }; + + span_lint_and_sugg( + cx, + WRITELN_EMPTY_STRING, + mac.span(), + format!("using `writeln!({}, \"\")`", suggestion).as_str(), + "replace it with", + format!("writeln!({})", suggestion), + applicability, + ); + } + } + } + } +} + +/// Given a format string that ends in a newline and its span, calculates the span of the +/// newline, or the format string itself if the format string consists solely of a newline. +/// Return this and a boolean indicating whether it only consisted of a newline. +fn newline_span(fmtstr: &StrLit) -> (Span, bool) { + let sp = fmtstr.span; + let contents = fmtstr.symbol.as_str(); + + if contents == r"\n" { + return (sp, true); + } + + let newline_sp_hi = sp.hi() + - match fmtstr.style { + StrStyle::Cooked => BytePos(1), + StrStyle::Raw(hashes) => BytePos((1 + hashes).into()), + }; + + let newline_sp_len = if contents.ends_with('\n') { + BytePos(1) + } else if contents.ends_with(r"\n") { + BytePos(2) + } else { + panic!("expected format string to contain a newline"); + }; + + (sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi), false) +} + +/// Stores a list of replacement spans for each argument, but only if all the replacements used an +/// empty format string. +#[derive(Default)] +struct SimpleFormatArgs { + unnamed: Vec<Vec<Span>>, + named: Vec<(Symbol, Vec<Span>)>, +} +impl SimpleFormatArgs { + fn get_unnamed(&self) -> impl Iterator<Item = &[Span]> { + self.unnamed.iter().map(|x| match x.as_slice() { + // Ignore the dummy span added from out of order format arguments. + [DUMMY_SP] => &[], + x => x, + }) + } + + fn get_named(&self, n: &Path) -> &[Span] { + self.named.iter().find(|x| *n == x.0).map_or(&[], |x| x.1.as_slice()) + } + + fn push(&mut self, arg: rustc_parse_format::Argument<'_>, span: Span) { + use rustc_parse_format::{ + AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied, FormatSpec, + }; + + const SIMPLE: FormatSpec<'_> = FormatSpec { + fill: None, + align: AlignUnknown, + flags: 0, + precision: CountImplied, + precision_span: None, + width: CountImplied, + width_span: None, + ty: "", + ty_span: None, + }; + + match arg.position { + ArgumentIs(n) | ArgumentImplicitlyIs(n) => { + if self.unnamed.len() <= n { + // Use a dummy span to mark all unseen arguments. + self.unnamed.resize_with(n, || vec![DUMMY_SP]); + if arg.format == SIMPLE { + self.unnamed.push(vec![span]); + } else { + self.unnamed.push(Vec::new()); + } + } else { + let args = &mut self.unnamed[n]; + match (args.as_mut_slice(), arg.format == SIMPLE) { + // A non-empty format string has been seen already. + ([], _) => (), + // Replace the dummy span, if it exists. + ([dummy @ DUMMY_SP], true) => *dummy = span, + ([_, ..], true) => args.push(span), + ([_, ..], false) => *args = Vec::new(), + } + } + }, + ArgumentNamed(n) => { + let n = Symbol::intern(n); + if let Some(x) = self.named.iter_mut().find(|x| x.0 == n) { + match x.1.as_slice() { + // A non-empty format string has been seen already. + [] => (), + [_, ..] if arg.format == SIMPLE => x.1.push(span), + [_, ..] => x.1 = Vec::new(), + } + } else if arg.format == SIMPLE { + self.named.push((n, vec![span])); + } else { + self.named.push((n, Vec::new())); + } + }, + }; + } +} + +impl Write { + /// Parses a format string into a collection of spans for each argument. This only keeps track + /// of empty format arguments. Will also lint usages of debug format strings outside of debug + /// impls. + fn parse_fmt_string(&self, cx: &EarlyContext<'_>, str_lit: &StrLit) -> Option<SimpleFormatArgs> { + use rustc_parse_format::{ParseMode, Parser, Piece}; + + let str_sym = str_lit.symbol_unescaped.as_str(); + let style = match str_lit.style { + StrStyle::Cooked => None, + StrStyle::Raw(n) => Some(n as usize), + }; + + let mut parser = Parser::new(str_sym, style, snippet_opt(cx, str_lit.span), false, ParseMode::Format); + let mut args = SimpleFormatArgs::default(); + + while let Some(arg) = parser.next() { + let arg = match arg { + Piece::String(_) => continue, + Piece::NextArgument(arg) => arg, + }; + let span = parser + .arg_places + .last() + .map_or(DUMMY_SP, |&x| str_lit.span.from_inner(InnerSpan::new(x.start, x.end))); + + if !self.in_debug_impl && arg.format.ty == "?" { + // FIXME: modify rustc's fmt string parser to give us the current span + span_lint(cx, USE_DEBUG, span, "use of `Debug`-based formatting"); + } + + args.push(arg, span); + } + + parser.errors.is_empty().then_some(args) + } + + /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two + /// `Option`s. The first `Option` of the tuple is the macro's format string. It includes + /// the contents of the string, whether it's a raw string, and the span of the literal in the + /// source. The second `Option` in the tuple is, in the `write[ln]!` case, the expression the + /// `format_str` should be written to. + /// + /// Example: + /// + /// Calling this function on + /// ```rust + /// # use std::fmt::Write; + /// # let mut buf = String::new(); + /// # let something = "something"; + /// writeln!(buf, "string to write: {}", something); + /// ``` + /// will return + /// ```rust,ignore + /// (Some("string to write: {}"), Some(buf)) + /// ``` + fn check_tts<'a>(&self, cx: &EarlyContext<'a>, tts: TokenStream, is_write: bool) -> (Option<StrLit>, Option<Expr>) { + let mut parser = parser::Parser::new(&cx.sess().parse_sess, tts, false, None); + let expr = if is_write { + match parser + .parse_expr() + .map(rustc_ast::ptr::P::into_inner) + .map_err(DiagnosticBuilder::cancel) + { + // write!(e, ...) + Ok(p) if parser.eat(&token::Comma) => Some(p), + // write!(e) or error + e => return (None, e.ok()), + } + } else { + None + }; + + let fmtstr = match parser.parse_str_lit() { + Ok(fmtstr) => fmtstr, + Err(_) => return (None, expr), + }; + + let args = match self.parse_fmt_string(cx, &fmtstr) { + Some(args) => args, + None => return (Some(fmtstr), expr), + }; + + let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL }; + let mut unnamed_args = args.get_unnamed(); + loop { + if !parser.eat(&token::Comma) { + return (Some(fmtstr), expr); + } + + let comma_span = parser.prev_token.span; + let token_expr = if let Ok(expr) = parser.parse_expr().map_err(DiagnosticBuilder::cancel) { + expr + } else { + return (Some(fmtstr), None); + }; + let (fmt_spans, lit) = match &token_expr.kind { + ExprKind::Lit(lit) => (unnamed_args.next().unwrap_or(&[]), lit), + ExprKind::Assign(lhs, rhs, _) => match (&lhs.kind, &rhs.kind) { + (ExprKind::Path(_, p), ExprKind::Lit(lit)) => (args.get_named(p), lit), + _ => continue, + }, + _ => { + unnamed_args.next(); + continue; + }, + }; + + let replacement: String = match lit.token.kind { + LitKind::StrRaw(_) | LitKind::ByteStrRaw(_) if matches!(fmtstr.style, StrStyle::Raw(_)) => { + lit.token.symbol.as_str().replace('{', "{{").replace('}', "}}") + }, + LitKind::Str | LitKind::ByteStr if matches!(fmtstr.style, StrStyle::Cooked) => { + lit.token.symbol.as_str().replace('{', "{{").replace('}', "}}") + }, + LitKind::StrRaw(_) + | LitKind::Str + | LitKind::ByteStrRaw(_) + | LitKind::ByteStr + | LitKind::Integer + | LitKind::Float + | LitKind::Err => continue, + LitKind::Byte | LitKind::Char => match lit.token.symbol.as_str() { + "\"" if matches!(fmtstr.style, StrStyle::Cooked) => "\\\"", + "\"" if matches!(fmtstr.style, StrStyle::Raw(0)) => continue, + "\\\\" if matches!(fmtstr.style, StrStyle::Raw(_)) => "\\", + "\\'" => "'", + "{" => "{{", + "}" => "}}", + x if matches!(fmtstr.style, StrStyle::Raw(_)) && x.starts_with('\\') => continue, + x => x, + } + .into(), + LitKind::Bool => lit.token.symbol.as_str().deref().into(), + }; + + if !fmt_spans.is_empty() { + span_lint_and_then( + cx, + lint, + token_expr.span, + "literal with an empty format string", + |diag| { + diag.multipart_suggestion( + "try this", + iter::once((comma_span.to(token_expr.span), String::new())) + .chain(fmt_spans.iter().copied().zip(iter::repeat(replacement))) + .collect(), + Applicability::MachineApplicable, + ); + }, + ); + } + } + } + + fn lint_println_empty_string(&self, cx: &EarlyContext<'_>, mac: &MacCall) { + if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) { + if fmt_str.symbol == kw::Empty { + let name = mac.path.segments[0].ident.name; + span_lint_and_sugg( + cx, + PRINTLN_EMPTY_STRING, + mac.span(), + &format!("using `{}!(\"\")`", name), + "replace it with", + format!("{}!()", name), + Applicability::MachineApplicable, + ); + } + } + } + + fn lint_print_with_newline(&self, cx: &EarlyContext<'_>, mac: &MacCall) { + if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) { + if check_newlines(&fmt_str) { + let name = mac.path.segments[0].ident.name; + let suggested = format!("{}ln", name); + span_lint_and_then( + cx, + PRINT_WITH_NEWLINE, + mac.span(), + &format!("using `{}!()` with a format string that ends in a single newline", name), + |err| { + err.multipart_suggestion( + &format!("use `{}!` instead", suggested), + vec![(mac.path.span, suggested), (newline_span(&fmt_str).0, String::new())], + Applicability::MachineApplicable, + ); + }, + ); + } + } + } +} + +/// Checks if the format string contains a single newline that terminates it. +/// +/// Literal and escaped newlines are both checked (only literal for raw strings). +fn check_newlines(fmtstr: &StrLit) -> bool { + let mut has_internal_newline = false; + let mut last_was_cr = false; + let mut should_lint = false; + + let contents = fmtstr.symbol.as_str(); + + let mut cb = |r: Range<usize>, c: Result<char, EscapeError>| { + let c = c.unwrap(); + + if r.end == contents.len() && c == '\n' && !last_was_cr && !has_internal_newline { + should_lint = true; + } else { + last_was_cr = c == '\r'; + if c == '\n' { + has_internal_newline = true; + } + } + }; + + match fmtstr.style { + StrStyle::Cooked => unescape::unescape_literal(contents, unescape::Mode::Str, &mut cb), + StrStyle::Raw(_) => unescape::unescape_literal(contents, unescape::Mode::RawStr, &mut cb), + } + + should_lint +} diff --git a/src/tools/clippy/clippy_lints/src/zero_div_zero.rs b/src/tools/clippy/clippy_lints/src/zero_div_zero.rs new file mode 100644 index 000000000..50d3c079f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/zero_div_zero.rs @@ -0,0 +1,67 @@ +use clippy_utils::consts::{constant_simple, Constant}; +use clippy_utils::diagnostics::span_lint_and_help; +use if_chain::if_chain; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `0.0 / 0.0`. + /// + /// ### Why is this bad? + /// It's less readable than `f32::NAN` or `f64::NAN`. + /// + /// ### Example + /// ```rust + /// let nan = 0.0f32 / 0.0; + /// ``` + /// + /// Use instead: + /// ```rust + /// let nan = f32::NAN; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub ZERO_DIVIDED_BY_ZERO, + complexity, + "usage of `0.0 / 0.0` to obtain NaN instead of `f32::NAN` or `f64::NAN`" +} + +declare_lint_pass!(ZeroDiv => [ZERO_DIVIDED_BY_ZERO]); + +impl<'tcx> LateLintPass<'tcx> for ZeroDiv { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // check for instances of 0.0/0.0 + if_chain! { + if let ExprKind::Binary(ref op, left, right) = expr.kind; + if op.node == BinOpKind::Div; + // TODO - constant_simple does not fold many operations involving floats. + // That's probably fine for this lint - it's pretty unlikely that someone would + // do something like 0.0/(2.0 - 2.0), but it would be nice to warn on that case too. + if let Some(lhs_value) = constant_simple(cx, cx.typeck_results(), left); + if let Some(rhs_value) = constant_simple(cx, cx.typeck_results(), right); + if Constant::F32(0.0) == lhs_value || Constant::F64(0.0) == lhs_value; + if Constant::F32(0.0) == rhs_value || Constant::F64(0.0) == rhs_value; + then { + // since we're about to suggest a use of f32::NAN or f64::NAN, + // match the precision of the literals that are given. + let float_type = match (lhs_value, rhs_value) { + (Constant::F64(_), _) + | (_, Constant::F64(_)) => "f64", + _ => "f32" + }; + span_lint_and_help( + cx, + ZERO_DIVIDED_BY_ZERO, + expr.span, + "constant division of `0.0` with `0.0` will always result in NaN", + None, + &format!( + "consider using `{}::NAN` if you would like a constant representing NaN", + float_type, + ), + ); + } + } + } +} diff --git a/src/tools/clippy/clippy_lints/src/zero_sized_map_values.rs b/src/tools/clippy/clippy_lints/src/zero_sized_map_values.rs new file mode 100644 index 000000000..8dc43c0e2 --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/zero_sized_map_values.rs @@ -0,0 +1,94 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::ty::{is_normalizable, is_type_diagnostic_item}; +use if_chain::if_chain; +use rustc_hir::{self as hir, HirId, ItemKind, Node}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::layout::LayoutOf as _; +use rustc_middle::ty::{Adt, Ty, TypeVisitable}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::sym; +use rustc_typeck::hir_ty_to_ty; + +declare_clippy_lint! { + /// ### What it does + /// Checks for maps with zero-sized value types anywhere in the code. + /// + /// ### Why is this bad? + /// Since there is only a single value for a zero-sized type, a map + /// containing zero sized values is effectively a set. Using a set in that case improves + /// readability and communicates intent more clearly. + /// + /// ### Known problems + /// * A zero-sized type cannot be recovered later if it contains private fields. + /// * This lints the signature of public items + /// + /// ### Example + /// ```rust + /// # use std::collections::HashMap; + /// fn unique_words(text: &str) -> HashMap<&str, ()> { + /// todo!(); + /// } + /// ``` + /// Use instead: + /// ```rust + /// # use std::collections::HashSet; + /// fn unique_words(text: &str) -> HashSet<&str> { + /// todo!(); + /// } + /// ``` + #[clippy::version = "1.50.0"] + pub ZERO_SIZED_MAP_VALUES, + pedantic, + "usage of map with zero-sized value type" +} + +declare_lint_pass!(ZeroSizedMapValues => [ZERO_SIZED_MAP_VALUES]); + +impl LateLintPass<'_> for ZeroSizedMapValues { + fn check_ty(&mut self, cx: &LateContext<'_>, hir_ty: &hir::Ty<'_>) { + if_chain! { + if !hir_ty.span.from_expansion(); + if !in_trait_impl(cx, hir_ty.hir_id); + let ty = ty_from_hir_ty(cx, hir_ty); + if is_type_diagnostic_item(cx, ty, sym::HashMap) || is_type_diagnostic_item(cx, ty, sym::BTreeMap); + if let Adt(_, substs) = ty.kind(); + let ty = substs.type_at(1); + // Fixes https://github.com/rust-lang/rust-clippy/issues/7447 because of + // https://github.com/rust-lang/rust/blob/master/compiler/rustc_middle/src/ty/sty.rs#L968 + if !ty.has_escaping_bound_vars(); + // Do this to prevent `layout_of` crashing, being unable to fully normalize `ty`. + if is_normalizable(cx, cx.param_env, ty); + if let Ok(layout) = cx.layout_of(ty); + if layout.is_zst(); + then { + span_lint_and_help(cx, ZERO_SIZED_MAP_VALUES, hir_ty.span, "map with zero-sized value type", None, "consider using a set instead"); + } + } + } +} + +fn in_trait_impl(cx: &LateContext<'_>, hir_id: HirId) -> bool { + let parent_id = cx.tcx.hir().get_parent_item(hir_id); + let second_parent_id = cx + .tcx + .hir() + .get_parent_item(cx.tcx.hir().local_def_id_to_hir_id(parent_id)); + if let Some(Node::Item(item)) = cx.tcx.hir().find_by_def_id(second_parent_id) { + if let ItemKind::Impl(hir::Impl { of_trait: Some(_), .. }) = item.kind { + return true; + } + } + false +} + +fn ty_from_hir_ty<'tcx>(cx: &LateContext<'tcx>, hir_ty: &hir::Ty<'_>) -> Ty<'tcx> { + cx.maybe_typeck_results() + .and_then(|results| { + if results.hir_owner == hir_ty.hir_id.owner { + results.node_type_opt(hir_ty.hir_id) + } else { + None + } + }) + .unwrap_or_else(|| hir_ty_to_ty(cx.tcx, hir_ty)) +} |