diff options
Diffstat (limited to 'clippy_lints/src')
51 files changed, 2396 insertions, 596 deletions
diff --git a/clippy_lints/src/attrs/allow_attributes_without_reason.rs b/clippy_lints/src/attrs/allow_attributes_without_reason.rs index 4ab97118df1..4c7e07478c1 100644 --- a/clippy_lints/src/attrs/allow_attributes_without_reason.rs +++ b/clippy_lints/src/attrs/allow_attributes_without_reason.rs @@ -26,7 +26,7 @@ pub(super) fn check<'cx>(cx: &LateContext<'cx>, name: Symbol, items: &[NestedMet cx, ALLOW_ATTRIBUTES_WITHOUT_REASON, attr.span, - format!("`{}` attribute without specifying a reason", name.as_str()), + format!("`{name}` attribute without specifying a reason"), |diag| { diag.help("try adding a reason at the end with `, reason = \"..\"`"); }, diff --git a/clippy_lints/src/attrs/duplicated_attributes.rs b/clippy_lints/src/attrs/duplicated_attributes.rs index 40a1c4e2884..199e07565b0 100644 --- a/clippy_lints/src/attrs/duplicated_attributes.rs +++ b/clippy_lints/src/attrs/duplicated_attributes.rs @@ -36,7 +36,7 @@ fn check_duplicated_attr( } let Some(ident) = attr.ident() else { return }; let name = ident.name; - if name == sym::doc || name == sym::cfg_attr || name == sym::rustc_on_unimplemented { + if name == sym::doc || name == sym::cfg_attr || name == sym::rustc_on_unimplemented || name == sym::reason { // FIXME: Would be nice to handle `cfg_attr` as well. Only problem is to check that cfg // conditions are the same. // `#[rustc_on_unimplemented]` contains duplicated subattributes, that's expected. diff --git a/clippy_lints/src/attrs/empty_line_after.rs b/clippy_lints/src/attrs/empty_line_after.rs deleted file mode 100644 index 7ff644b4c44..00000000000 --- a/clippy_lints/src/attrs/empty_line_after.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::{EMPTY_LINE_AFTER_DOC_COMMENTS, EMPTY_LINE_AFTER_OUTER_ATTR}; -use clippy_utils::diagnostics::span_lint; -use clippy_utils::source::{is_present_in_source, without_block_comments, SpanRangeExt}; -use rustc_ast::{AttrKind, AttrStyle}; -use rustc_lint::EarlyContext; -use rustc_span::Span; - -/// Check for empty lines after outer attributes. -/// -/// Attributes and documentation comments are both considered outer attributes -/// by the AST. However, the average user likely considers them to be different. -/// Checking for empty lines after each of these attributes is split into two different -/// lints but can share the same logic. -pub(super) fn check(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(..)) || matches!(attr.kind, AttrKind::DocComment(..))) - && 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) = end_of_attr_to_next_attr_or_item.get_source_text(cx) { - let lines = snippet.split('\n').collect::<Vec<_>>(); - let lines = without_block_comments(lines); - - if lines.iter().filter(|l| l.trim().is_empty()).count() > 2 { - let (lint_msg, lint_type) = match attr.kind { - AttrKind::DocComment(..) => ( - "found an empty line after a doc comment. \ - Perhaps you need to use `//!` to make a comment on a module, remove the empty line, or make a regular comment with `//`?", - EMPTY_LINE_AFTER_DOC_COMMENTS, - ), - AttrKind::Normal(..) => ( - "found an empty line after an outer attribute. \ - Perhaps you forgot to add a `!` to make it an inner attribute?", - EMPTY_LINE_AFTER_OUTER_ATTR, - ), - }; - - span_lint(cx, lint_type, begin_of_attr_to_item, lint_msg); - } - } - } - } -} diff --git a/clippy_lints/src/attrs/mod.rs b/clippy_lints/src/attrs/mod.rs index 3b14e9aee7f..c8fea25c9f2 100644 --- a/clippy_lints/src/attrs/mod.rs +++ b/clippy_lints/src/attrs/mod.rs @@ -4,7 +4,6 @@ mod blanket_clippy_restriction_lints; mod deprecated_cfg_attr; mod deprecated_semver; mod duplicated_attributes; -mod empty_line_after; mod inline_always; mod mixed_attributes_style; mod non_minimal_cfg; @@ -128,94 +127,6 @@ declare_clippy_lint! { 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 - /// ```no_run - /// #[allow(dead_code)] - /// - /// fn not_quite_good_code() { } - /// ``` - /// - /// Use instead: - /// ```no_run - /// // 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 empty lines after documentation comments. - /// - /// ### Why is this bad? - /// The documentation comment was most likely meant to be an inner attribute or regular comment. - /// If it was intended to be a documentation comment, then the empty line should be removed to - /// be more idiomatic. - /// - /// ### Known problems - /// Only detects empty lines immediately following the documentation. If the doc comment is followed - /// by an attribute and then an empty line, this lint will not trigger. Use `empty_line_after_outer_attr` - /// in combination with this lint to detect both cases. - /// - /// Does not detect empty lines after doc attributes (e.g. `#[doc = ""]`). - /// - /// ### Example - /// ```no_run - /// /// Some doc comment with a blank line after it. - /// - /// fn not_quite_good_code() { } - /// ``` - /// - /// Use instead: - /// ```no_run - /// /// Good (no blank line) - /// fn this_is_fine() { } - /// ``` - /// - /// ```no_run - /// // Good (convert to a regular comment) - /// - /// fn this_is_fine_too() { } - /// ``` - /// - /// ```no_run - /// //! Good (convert to a comment on an inner attribute) - /// - /// fn this_is_fine_as_well() { } - /// ``` - #[clippy::version = "1.70.0"] - pub EMPTY_LINE_AFTER_DOC_COMMENTS, - nursery, - "empty line after documentation comments" -} - -declare_clippy_lint! { - /// ### What it does /// Checks for `warn`/`deny`/`forbid` attributes targeting the whole clippy::restriction category. /// /// ### Why is this bad? @@ -601,18 +512,12 @@ impl EarlyAttributes { impl_lint_pass!(EarlyAttributes => [ DEPRECATED_CFG_ATTR, - EMPTY_LINE_AFTER_OUTER_ATTR, - EMPTY_LINE_AFTER_DOC_COMMENTS, NON_MINIMAL_CFG, DEPRECATED_CLIPPY_CFG_ATTR, UNNECESSARY_CLIPPY_CFG, ]); impl EarlyLintPass for EarlyAttributes { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &rustc_ast::Item) { - empty_line_after::check(cx, item); - } - fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &Attribute) { deprecated_cfg_attr::check(cx, attr, &self.msrv); deprecated_cfg_attr::check_clippy(cx, attr); diff --git a/clippy_lints/src/attrs/useless_attribute.rs b/clippy_lints/src/attrs/useless_attribute.rs index 67ba605a59f..1296b59f2e1 100644 --- a/clippy_lints/src/attrs/useless_attribute.rs +++ b/clippy_lints/src/attrs/useless_attribute.rs @@ -51,6 +51,7 @@ pub(super) fn check(cx: &LateContext<'_>, item: &Item<'_>, attrs: &[Attribute]) | "module_name_repetitions" | "single_component_path_imports" | "disallowed_types" + | "unused_trait_names" ) }) { return; diff --git a/clippy_lints/src/byte_char_slices.rs b/clippy_lints/src/byte_char_slices.rs index a9fe190f177..dd2620b0b9d 100644 --- a/clippy_lints/src/byte_char_slices.rs +++ b/clippy_lints/src/byte_char_slices.rs @@ -22,7 +22,7 @@ declare_clippy_lint! { /// ```ignore /// b"Hello" /// ``` - #[clippy::version = "1.68.0"] + #[clippy::version = "1.81.0"] pub BYTE_CHAR_SLICES, style, "hard to read byte char slice" diff --git a/clippy_lints/src/casts/mod.rs b/clippy_lints/src/casts/mod.rs index c31716fbcee..39f50a347c0 100644 --- a/clippy_lints/src/casts/mod.rs +++ b/clippy_lints/src/casts/mod.rs @@ -410,19 +410,27 @@ declare_clippy_lint! { /// ### Why is this bad? /// Though `as` casts between raw pointers are not terrible, `pointer::cast_mut` and /// `pointer::cast_const` are safer because they cannot accidentally cast the pointer to another - /// type. + /// type. Or, when null pointers are involved, `null()` and `null_mut()` can be used directly. /// /// ### Example /// ```no_run /// let ptr: *const u32 = &42_u32; /// let mut_ptr = ptr as *mut u32; /// let ptr = mut_ptr as *const u32; + /// let ptr1 = std::ptr::null::<u32>() as *mut u32; + /// let ptr2 = std::ptr::null_mut::<u32>() as *const u32; + /// let ptr3 = std::ptr::null::<u32>().cast_mut(); + /// let ptr4 = std::ptr::null_mut::<u32>().cast_const(); /// ``` /// Use instead: /// ```no_run /// let ptr: *const u32 = &42_u32; /// let mut_ptr = ptr.cast_mut(); /// let ptr = mut_ptr.cast_const(); + /// let ptr1 = std::ptr::null_mut::<u32>(); + /// let ptr2 = std::ptr::null::<u32>(); + /// let ptr3 = std::ptr::null_mut::<u32>(); + /// let ptr4 = std::ptr::null::<u32>(); /// ``` #[clippy::version = "1.72.0"] pub PTR_CAST_CONSTNESS, @@ -809,6 +817,7 @@ impl<'tcx> LateLintPass<'tcx> for Casts { char_lit_as_u8::check(cx, expr); ptr_as_ptr::check(cx, expr, &self.msrv); cast_slice_different_sizes::check(cx, expr, &self.msrv); + ptr_cast_constness::check_null_ptr_cast_method(cx, expr); } extract_msrv_attr!(LateContext); diff --git a/clippy_lints/src/casts/ptr_cast_constness.rs b/clippy_lints/src/casts/ptr_cast_constness.rs index 7513e18d408..7518dd2435a 100644 --- a/clippy_lints/src/casts/ptr_cast_constness.rs +++ b/clippy_lints/src/casts/ptr_cast_constness.rs @@ -1,10 +1,12 @@ use clippy_config::msrvs::{self, Msrv}; use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::std_or_core; use clippy_utils::sugg::Sugg; use rustc_errors::Applicability; -use rustc_hir::{Expr, Mutability}; +use rustc_hir::{Expr, ExprKind, Mutability, QPath}; use rustc_lint::LateContext; use rustc_middle::ty::{self, Ty, TypeVisitableExt}; +use rustc_span::sym; use super::PTR_CAST_CONSTNESS; @@ -16,8 +18,7 @@ pub(super) fn check<'tcx>( cast_to: Ty<'tcx>, msrv: &Msrv, ) { - if msrv.meets(msrvs::POINTER_CAST_CONSTNESS) - && let ty::RawPtr(from_ty, from_mutbl) = cast_from.kind() + if let ty::RawPtr(from_ty, from_mutbl) = cast_from.kind() && let ty::RawPtr(to_ty, to_mutbl) = cast_to.kind() && matches!( (from_mutbl, to_mutbl), @@ -26,20 +27,74 @@ pub(super) fn check<'tcx>( && from_ty == to_ty && !from_ty.has_erased_regions() { - let sugg = Sugg::hir(cx, cast_expr, "_"); - let constness = match *to_mutbl { - Mutability::Not => "const", - Mutability::Mut => "mut", - }; + if let ExprKind::Call(func, []) = cast_expr.kind + && let ExprKind::Path(QPath::Resolved(None, path)) = func.kind + && let Some(defid) = path.res.opt_def_id() + && let Some(prefix) = std_or_core(cx) + && let mut app = Applicability::MachineApplicable + && let sugg = format!("{}", Sugg::hir_with_applicability(cx, cast_expr, "_", &mut app)) + && let Some((_, after_lt)) = sugg.split_once("::<") + && let Some((source, target, target_func)) = match cx.tcx.get_diagnostic_name(defid) { + Some(sym::ptr_null) => Some(("const", "mutable", "null_mut")), + Some(sym::ptr_null_mut) => Some(("mutable", "const", "null")), + _ => None, + } + { + span_lint_and_sugg( + cx, + PTR_CAST_CONSTNESS, + expr.span, + format!("`as` casting to make a {source} null pointer into a {target} null pointer"), + format!("use `{target_func}()` directly instead"), + format!("{prefix}::ptr::{target_func}::<{after_lt}"), + app, + ); + return; + } + if msrv.meets(msrvs::POINTER_CAST_CONSTNESS) { + let sugg = Sugg::hir(cx, cast_expr, "_"); + let constness = match *to_mutbl { + Mutability::Not => "const", + Mutability::Mut => "mut", + }; + + span_lint_and_sugg( + cx, + PTR_CAST_CONSTNESS, + expr.span, + "`as` casting between raw pointers while changing only its constness", + format!("try `pointer::cast_{constness}`, a safer alternative"), + format!("{}.cast_{constness}()", sugg.maybe_par()), + Applicability::MachineApplicable, + ); + } + } +} + +pub(super) fn check_null_ptr_cast_method(cx: &LateContext<'_>, expr: &Expr<'_>) { + if let ExprKind::MethodCall(method, cast_expr, [], _) = expr.kind + && let ExprKind::Call(func, []) = cast_expr.kind + && let ExprKind::Path(QPath::Resolved(None, path)) = func.kind + && let Some(defid) = path.res.opt_def_id() + && let method = match (cx.tcx.get_diagnostic_name(defid), method.ident.as_str()) { + (Some(sym::ptr_null), "cast_mut") => "null_mut", + (Some(sym::ptr_null_mut), "cast_const") => "null", + _ => return, + } + && let Some(prefix) = std_or_core(cx) + && let mut app = Applicability::MachineApplicable + && let sugg = format!("{}", Sugg::hir_with_applicability(cx, cast_expr, "_", &mut app)) + && let Some((_, after_lt)) = sugg.split_once("::<") + { span_lint_and_sugg( cx, PTR_CAST_CONSTNESS, expr.span, - "`as` casting between raw pointers while changing only its constness", - format!("try `pointer::cast_{constness}`, a safer alternative"), - format!("{}.cast_{constness}()", sugg.maybe_par()), - Applicability::MachineApplicable, + "changing constness of a null pointer", + format!("use `{method}()` directly instead"), + format!("{prefix}::ptr::{method}::<{after_lt}"), + app, ); } } diff --git a/clippy_lints/src/cfg_not_test.rs b/clippy_lints/src/cfg_not_test.rs index d820c1e0720..884d15cabff 100644 --- a/clippy_lints/src/cfg_not_test.rs +++ b/clippy_lints/src/cfg_not_test.rs @@ -22,7 +22,7 @@ declare_clippy_lint! { /// # fn important_check() {} /// important_check(); /// ``` - #[clippy::version = "1.73.0"] + #[clippy::version = "1.81.0"] pub CFG_NOT_TEST, restriction, "enforce against excluding code from test builds" diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 60e51713173..2b229d2fe6a 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -49,8 +49,6 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::attrs::DEPRECATED_CLIPPY_CFG_ATTR_INFO, crate::attrs::DEPRECATED_SEMVER_INFO, crate::attrs::DUPLICATED_ATTRIBUTES_INFO, - crate::attrs::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO, - crate::attrs::EMPTY_LINE_AFTER_OUTER_ATTR_INFO, crate::attrs::INLINE_ALWAYS_INFO, crate::attrs::MIXED_ATTRIBUTES_STYLE_INFO, crate::attrs::NON_MINIMAL_CFG_INFO, @@ -138,6 +136,8 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::doc::DOC_LINK_WITH_QUOTES_INFO, crate::doc::DOC_MARKDOWN_INFO, crate::doc::EMPTY_DOCS_INFO, + crate::doc::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO, + crate::doc::EMPTY_LINE_AFTER_OUTER_ATTR_INFO, crate::doc::MISSING_ERRORS_DOC_INFO, crate::doc::MISSING_PANICS_DOC_INFO, crate::doc::MISSING_SAFETY_DOC_INFO, @@ -217,6 +217,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::implicit_return::IMPLICIT_RETURN_INFO, crate::implicit_saturating_add::IMPLICIT_SATURATING_ADD_INFO, crate::implicit_saturating_sub::IMPLICIT_SATURATING_SUB_INFO, + crate::implicit_saturating_sub::INVERTED_SATURATING_SUB_INFO, crate::implied_bounds_in_impls::IMPLIED_BOUNDS_IN_IMPLS_INFO, crate::incompatible_msrv::INCOMPATIBLE_MSRV_INFO, crate::inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR_INFO, @@ -300,10 +301,12 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::manual_async_fn::MANUAL_ASYNC_FN_INFO, crate::manual_bits::MANUAL_BITS_INFO, crate::manual_clamp::MANUAL_CLAMP_INFO, + crate::manual_div_ceil::MANUAL_DIV_CEIL_INFO, crate::manual_float_methods::MANUAL_IS_FINITE_INFO, crate::manual_float_methods::MANUAL_IS_INFINITE_INFO, crate::manual_hash_one::MANUAL_HASH_ONE_INFO, crate::manual_is_ascii_check::MANUAL_IS_ASCII_CHECK_INFO, + crate::manual_is_power_of_two::MANUAL_IS_POWER_OF_TWO_INFO, crate::manual_let_else::MANUAL_LET_ELSE_INFO, crate::manual_main_separator_str::MANUAL_MAIN_SEPARATOR_STR_INFO, crate::manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE_INFO, @@ -464,6 +467,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::methods::UNNECESSARY_FALLIBLE_CONVERSIONS_INFO, crate::methods::UNNECESSARY_FILTER_MAP_INFO, crate::methods::UNNECESSARY_FIND_MAP_INFO, + crate::methods::UNNECESSARY_FIRST_THEN_CHECK_INFO, crate::methods::UNNECESSARY_FOLD_INFO, crate::methods::UNNECESSARY_GET_THEN_CHECK_INFO, crate::methods::UNNECESSARY_JOIN_INFO, @@ -486,6 +490,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::misc::SHORT_CIRCUIT_STATEMENT_INFO, crate::misc::TOPLEVEL_REF_ARG_INFO, crate::misc::USED_UNDERSCORE_BINDING_INFO, + crate::misc::USED_UNDERSCORE_ITEMS_INFO, crate::misc_early::BUILTIN_TYPE_SHADOW_INFO, crate::misc_early::DOUBLE_NEG_INFO, crate::misc_early::DUPLICATE_UNDERSCORE_ARGUMENT_INFO, @@ -553,6 +558,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::non_expressive_names::SIMILAR_NAMES_INFO, crate::non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS_INFO, crate::non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY_INFO, + crate::non_zero_suggestions::NON_ZERO_SUGGESTIONS_INFO, crate::nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES_INFO, crate::octal_escapes::OCTAL_ESCAPES_INFO, crate::only_used_in_recursion::ONLY_USED_IN_RECURSION_INFO, @@ -598,6 +604,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::pathbuf_init_then_push::PATHBUF_INIT_THEN_PUSH_INFO, crate::pattern_type_mismatch::PATTERN_TYPE_MISMATCH_INFO, crate::permissions_set_readonly_false::PERMISSIONS_SET_READONLY_FALSE_INFO, + crate::pointers_in_nomem_asm_block::POINTERS_IN_NOMEM_ASM_BLOCK_INFO, crate::precedence::PRECEDENCE_INFO, crate::ptr::CMP_NULL_INFO, crate::ptr::INVALID_NULL_PTR_USAGE_INFO, @@ -741,6 +748,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::unused_result_ok::UNUSED_RESULT_OK_INFO, crate::unused_rounding::UNUSED_ROUNDING_INFO, crate::unused_self::UNUSED_SELF_INFO, + crate::unused_trait_names::UNUSED_TRAIT_NAMES_INFO, crate::unused_unit::UNUSED_UNIT_INFO, crate::unwrap::PANICKING_UNWRAP_INFO, crate::unwrap::UNNECESSARY_UNWRAP_INFO, @@ -767,4 +775,5 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::zero_div_zero::ZERO_DIVIDED_BY_ZERO_INFO, crate::zero_repeat_side_effects::ZERO_REPEAT_SIDE_EFFECTS_INFO, crate::zero_sized_map_values::ZERO_SIZED_MAP_VALUES_INFO, + crate::zombie_processes::ZOMBIE_PROCESSES_INFO, ]; diff --git a/clippy_lints/src/doc/empty_line_after.rs b/clippy_lints/src/doc/empty_line_after.rs new file mode 100644 index 00000000000..289debe0a6a --- /dev/null +++ b/clippy_lints/src/doc/empty_line_after.rs @@ -0,0 +1,329 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::{snippet_indent, SpanRangeExt}; +use clippy_utils::tokenize_with_text; +use itertools::Itertools; +use rustc_ast::token::CommentKind; +use rustc_ast::{AttrKind, AttrStyle, Attribute}; +use rustc_errors::{Applicability, Diag, SuggestionStyle}; +use rustc_hir::{ItemKind, Node}; +use rustc_lexer::TokenKind; +use rustc_lint::LateContext; +use rustc_span::{ExpnKind, InnerSpan, Span, SpanData}; + +use super::{EMPTY_LINE_AFTER_DOC_COMMENTS, EMPTY_LINE_AFTER_OUTER_ATTR}; + +#[derive(Debug, PartialEq, Clone, Copy)] +enum StopKind { + Attr, + Doc(CommentKind), +} + +impl StopKind { + fn is_doc(self) -> bool { + matches!(self, StopKind::Doc(_)) + } +} + +#[derive(Debug)] +struct Stop { + span: Span, + kind: StopKind, + first: usize, + last: usize, +} + +impl Stop { + fn convert_to_inner(&self) -> (Span, String) { + let inner = match self.kind { + // #|[...] + StopKind::Attr => InnerSpan::new(1, 1), + // /// or /** + // ^ ^ + StopKind::Doc(_) => InnerSpan::new(2, 3), + }; + (self.span.from_inner(inner), "!".into()) + } + + fn comment_out(&self, cx: &LateContext<'_>, suggestions: &mut Vec<(Span, String)>) { + match self.kind { + StopKind::Attr => { + if cx.tcx.sess.source_map().is_multiline(self.span) { + suggestions.extend([ + (self.span.shrink_to_lo(), "/* ".into()), + (self.span.shrink_to_hi(), " */".into()), + ]); + } else { + suggestions.push((self.span.shrink_to_lo(), "// ".into())); + } + }, + StopKind::Doc(CommentKind::Line) => suggestions.push((self.span.shrink_to_lo(), "// ".into())), + StopKind::Doc(CommentKind::Block) => { + // /** outer */ /*! inner */ + // ^ ^ + let asterisk = self.span.from_inner(InnerSpan::new(1, 2)); + suggestions.push((asterisk, String::new())); + }, + } + } + + fn from_attr(cx: &LateContext<'_>, attr: &Attribute) -> Option<Self> { + let SpanData { lo, hi, .. } = attr.span.data(); + let file = cx.tcx.sess.source_map().lookup_source_file(lo); + + Some(Self { + span: attr.span, + kind: match attr.kind { + AttrKind::Normal(_) => StopKind::Attr, + AttrKind::DocComment(comment_kind, _) => StopKind::Doc(comment_kind), + }, + first: file.lookup_line(file.relative_position(lo))?, + last: file.lookup_line(file.relative_position(hi))?, + }) + } +} + +/// Represents a set of attrs/doc comments separated by 1 or more empty lines +/// +/// ```ignore +/// /// chunk 1 docs +/// // not an empty line so also part of chunk 1 +/// #[chunk_1_attrs] // <-- prev_stop +/// +/// /* gap */ +/// +/// /// chunk 2 docs // <-- next_stop +/// #[chunk_2_attrs] +/// ``` +struct Gap<'a> { + /// The span of individual empty lines including the newline at the end of the line + empty_lines: Vec<Span>, + has_comment: bool, + next_stop: &'a Stop, + prev_stop: &'a Stop, + /// The chunk that includes [`prev_stop`](Self::prev_stop) + prev_chunk: &'a [Stop], +} + +impl<'a> Gap<'a> { + fn new(cx: &LateContext<'_>, prev_chunk: &'a [Stop], next_chunk: &'a [Stop]) -> Option<Self> { + let prev_stop = prev_chunk.last()?; + let next_stop = next_chunk.first()?; + let gap_span = prev_stop.span.between(next_stop.span); + let gap_snippet = gap_span.get_source_text(cx)?; + + let mut has_comment = false; + let mut empty_lines = Vec::new(); + + for (token, source, inner_span) in tokenize_with_text(&gap_snippet) { + match token { + TokenKind::BlockComment { + doc_style: None, + terminated: true, + } + | TokenKind::LineComment { doc_style: None } => has_comment = true, + TokenKind::Whitespace => { + let newlines = source.bytes().positions(|b| b == b'\n'); + empty_lines.extend( + newlines + .tuple_windows() + .map(|(a, b)| InnerSpan::new(inner_span.start + a + 1, inner_span.start + b)) + .map(|inner_span| gap_span.from_inner(inner_span)), + ); + }, + // Ignore cfg_attr'd out attributes as they may contain empty lines, could also be from macro + // shenanigans + _ => return None, + } + } + + (!empty_lines.is_empty()).then_some(Self { + empty_lines, + has_comment, + next_stop, + prev_stop, + prev_chunk, + }) + } +} + +/// If the node the attributes/docs apply to is the first in the module/crate suggest converting +/// them to inner attributes/docs +fn suggest_inner(cx: &LateContext<'_>, diag: &mut Diag<'_, ()>, kind: StopKind, gaps: &[Gap<'_>]) { + let Some(owner) = cx.last_node_with_lint_attrs.as_owner() else { + return; + }; + let parent_desc = match cx.tcx.parent_hir_node(owner.into()) { + Node::Item(item) + if let ItemKind::Mod(parent_mod) = item.kind + && let [first, ..] = parent_mod.item_ids + && first.owner_id == owner => + { + "parent module" + }, + Node::Crate(crate_mod) + if let Some(first) = crate_mod + .item_ids + .iter() + .map(|&id| cx.tcx.hir().item(id)) + // skip prelude imports + .find(|item| !matches!(item.span.ctxt().outer_expn_data().kind, ExpnKind::AstPass(_))) + && first.owner_id == owner => + { + "crate" + }, + _ => return, + }; + + diag.multipart_suggestion_verbose( + match kind { + StopKind::Attr => format!("if the attribute should apply to the {parent_desc} use an inner attribute"), + StopKind::Doc(_) => format!("if the comment should document the {parent_desc} use an inner doc comment"), + }, + gaps.iter() + .flat_map(|gap| gap.prev_chunk) + .map(Stop::convert_to_inner) + .collect(), + Applicability::MaybeIncorrect, + ); +} + +fn check_gaps(cx: &LateContext<'_>, gaps: &[Gap<'_>]) -> bool { + let Some(first_gap) = gaps.first() else { + return false; + }; + let empty_lines = || gaps.iter().flat_map(|gap| gap.empty_lines.iter().copied()); + let mut has_comment = false; + let mut has_attr = false; + for gap in gaps { + has_comment |= gap.has_comment; + if !has_attr { + has_attr = gap.prev_chunk.iter().any(|stop| stop.kind == StopKind::Attr); + } + } + let kind = first_gap.prev_stop.kind; + let (lint, kind_desc) = match kind { + StopKind::Attr => (EMPTY_LINE_AFTER_OUTER_ATTR, "outer attribute"), + StopKind::Doc(_) => (EMPTY_LINE_AFTER_DOC_COMMENTS, "doc comment"), + }; + let (lines, are, them) = if empty_lines().nth(1).is_some() { + ("lines", "are", "them") + } else { + ("line", "is", "it") + }; + span_lint_and_then( + cx, + lint, + first_gap.prev_stop.span.to(empty_lines().last().unwrap()), + format!("empty {lines} after {kind_desc}"), + |diag| { + if let Some(owner) = cx.last_node_with_lint_attrs.as_owner() { + let def_id = owner.to_def_id(); + let def_descr = cx.tcx.def_descr(def_id); + diag.span_label( + cx.tcx.def_span(def_id), + match kind { + StopKind::Attr => format!("the attribute applies to this {def_descr}"), + StopKind::Doc(_) => format!("the comment documents this {def_descr}"), + }, + ); + } + + diag.multipart_suggestion_with_style( + format!("if the empty {lines} {are} unintentional remove {them}"), + empty_lines().map(|empty_line| (empty_line, String::new())).collect(), + Applicability::MaybeIncorrect, + SuggestionStyle::HideCodeAlways, + ); + + if has_comment && kind.is_doc() { + // Likely doc comments that applied to some now commented out code + // + // /// Old docs for Foo + // // struct Foo; + + let mut suggestions = Vec::new(); + for stop in gaps.iter().flat_map(|gap| gap.prev_chunk) { + stop.comment_out(cx, &mut suggestions); + } + let name = match cx.tcx.hir().opt_name(cx.last_node_with_lint_attrs) { + Some(name) => format!("`{name}`"), + None => "this".into(), + }; + diag.multipart_suggestion_verbose( + format!("if the doc comment should not document {name} comment it out"), + suggestions, + Applicability::MaybeIncorrect, + ); + } else { + suggest_inner(cx, diag, kind, gaps); + } + + if kind == StopKind::Doc(CommentKind::Line) + && gaps + .iter() + .all(|gap| !gap.has_comment && gap.next_stop.kind == StopKind::Doc(CommentKind::Line)) + { + // Commentless empty gaps between line doc comments, possibly intended to be part of the markdown + + let indent = snippet_indent(cx, first_gap.prev_stop.span).unwrap_or_default(); + diag.multipart_suggestion_verbose( + format!("if the documentation should include the empty {lines} include {them} in the comment"), + empty_lines() + .map(|empty_line| (empty_line, format!("{indent}///"))) + .collect(), + Applicability::MaybeIncorrect, + ); + } + }, + ); + kind.is_doc() +} + +/// Returns `true` if [`EMPTY_LINE_AFTER_DOC_COMMENTS`] triggered, used to skip other doc comment +/// lints where they would be confusing +/// +/// [`EMPTY_LINE_AFTER_OUTER_ATTR`] is also here to share an implementation but does not return +/// `true` if it triggers +pub(super) fn check(cx: &LateContext<'_>, attrs: &[Attribute]) -> bool { + let mut outer = attrs + .iter() + .filter(|attr| attr.style == AttrStyle::Outer && !attr.span.from_expansion()) + .map(|attr| Stop::from_attr(cx, attr)) + .collect::<Option<Vec<_>>>() + .unwrap_or_default(); + + if outer.is_empty() { + return false; + } + + // Push a fake attribute Stop for the item itself so we check for gaps between the last outer + // attr/doc comment and the item they apply to + let span = cx.tcx.hir().span(cx.last_node_with_lint_attrs); + if !span.from_expansion() + && let Ok(line) = cx.tcx.sess.source_map().lookup_line(span.lo()) + { + outer.push(Stop { + span, + kind: StopKind::Attr, + first: line.line, + // last doesn't need to be accurate here, we don't compare it with anything + last: line.line, + }); + } + + let mut gaps = Vec::new(); + let mut last = 0; + for pos in outer + .array_windows() + .positions(|[a, b]| b.first.saturating_sub(a.last) > 1) + { + // we want to be after the first stop in the window + let pos = pos + 1; + if let Some(gap) = Gap::new(cx, &outer[last..pos], &outer[pos..]) { + last = pos; + gaps.push(gap); + } + } + + check_gaps(cx, &gaps) +} diff --git a/clippy_lints/src/doc/lazy_continuation.rs b/clippy_lints/src/doc/lazy_continuation.rs index 771bcac2441..f9e4a43c0e7 100644 --- a/clippy_lints/src/doc/lazy_continuation.rs +++ b/clippy_lints/src/doc/lazy_continuation.rs @@ -22,7 +22,6 @@ pub(super) fn check( range: Range<usize>, mut span: Span, containers: &[super::Container], - line_break_span: Span, ) { if doc[range.clone()].contains('\t') { // We don't do tab stops correctly. @@ -52,29 +51,6 @@ pub(super) fn check( "doc list item without indentation" }; span_lint_and_then(cx, DOC_LAZY_CONTINUATION, span, msg, |diag| { - let snippet = clippy_utils::source::snippet(cx, line_break_span, ""); - if snippet.chars().filter(|&c| c == '\n').count() > 1 - && let Some(doc_comment_start) = snippet.rfind('\n') - && let doc_comment = snippet[doc_comment_start..].trim() - && (doc_comment == "///" || doc_comment == "//!") - { - // suggest filling in a blank line - diag.span_suggestion_verbose( - line_break_span.shrink_to_lo(), - "if this should be its own paragraph, add a blank doc comment line", - format!("\n{doc_comment}"), - Applicability::MaybeIncorrect, - ); - if ccount > 0 || blockquote_level > 0 { - diag.help("if this not intended to be a quote at all, escape it with `\\>`"); - } else { - let indent = list_indentation - lcount; - diag.help(format!( - "if this is intended to be part of the list, indent {indent} spaces" - )); - } - return; - } if ccount == 0 && blockquote_level == 0 { // simpler suggestion style for indentation let indent = list_indentation - lcount; diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs index 790579b21c9..f2c87f0fd70 100644 --- a/clippy_lints/src/doc/mod.rs +++ b/clippy_lints/src/doc/mod.rs @@ -32,6 +32,7 @@ use rustc_span::{sym, Span}; use std::ops::Range; use url::Url; +mod empty_line_after; mod link_with_quotes; mod markdown; mod missing_headers; @@ -449,13 +450,88 @@ declare_clippy_lint! { /// /// and probably spanning a many rows. /// struct Foo {} /// ``` - #[clippy::version = "1.81.0"] + #[clippy::version = "1.82.0"] pub TOO_LONG_FIRST_DOC_PARAGRAPH, style, "ensure that the first line of a documentation paragraph isn't too long" } -#[derive(Clone)] +declare_clippy_lint! { + /// ### What it does + /// Checks for empty lines after outer attributes + /// + /// ### Why is this bad? + /// The attribute may have meant to be an inner attribute (`#![attr]`). If + /// it was meant to be an outer attribute (`#[attr]`) then the empty line + /// should be removed + /// + /// ### Example + /// ```no_run + /// #[allow(dead_code)] + /// + /// fn not_quite_good_code() {} + /// ``` + /// + /// Use instead: + /// ```no_run + /// // 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, + suspicious, + "empty line after outer attribute" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for empty lines after doc comments. + /// + /// ### Why is this bad? + /// The doc comment may have meant to be an inner doc comment, regular + /// comment or applied to some old code that is now commented out. If it was + /// intended to be a doc comment, then the empty line should be removed. + /// + /// ### Example + /// ```no_run + /// /// Some doc comment with a blank line after it. + /// + /// fn f() {} + /// + /// /// Docs for `old_code` + /// // fn old_code() {} + /// + /// fn new_code() {} + /// ``` + /// + /// Use instead: + /// ```no_run + /// //! Convert it to an inner doc comment + /// + /// // Or a regular comment + /// + /// /// Or remove the empty line + /// fn f() {} + /// + /// // /// Docs for `old_code` + /// // fn old_code() {} + /// + /// fn new_code() {} + /// ``` + #[clippy::version = "1.70.0"] + pub EMPTY_LINE_AFTER_DOC_COMMENTS, + suspicious, + "empty line after doc comments" +} + pub struct Documentation { valid_idents: FxHashSet<String>, check_private_items: bool, @@ -482,6 +558,8 @@ impl_lint_pass!(Documentation => [ SUSPICIOUS_DOC_COMMENTS, EMPTY_DOCS, DOC_LAZY_CONTINUATION, + EMPTY_LINE_AFTER_OUTER_ATTR, + EMPTY_LINE_AFTER_DOC_COMMENTS, TOO_LONG_FIRST_DOC_PARAGRAPH, ]); @@ -612,12 +690,10 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[ Some(("fake".into(), "fake".into())) } - if is_doc_hidden(attrs) { + if suspicious_doc_comments::check(cx, attrs) || empty_line_after::check(cx, attrs) || is_doc_hidden(attrs) { return None; } - suspicious_doc_comments::check(cx, attrs); - let (fragments, _) = attrs_to_doc_fragments( attrs.iter().filter_map(|attr| { if in_external_macro(cx.sess(), attr.span) { @@ -816,7 +892,6 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize range.end..next_range.start, Span::new(span.hi(), next_span.lo(), span.ctxt(), span.parent()), &containers[..], - span, ); } }, diff --git a/clippy_lints/src/doc/suspicious_doc_comments.rs b/clippy_lints/src/doc/suspicious_doc_comments.rs index d7ad30efec3..f6f942b10ca 100644 --- a/clippy_lints/src/doc/suspicious_doc_comments.rs +++ b/clippy_lints/src/doc/suspicious_doc_comments.rs @@ -7,7 +7,7 @@ use rustc_span::Span; use super::SUSPICIOUS_DOC_COMMENTS; -pub fn check(cx: &LateContext<'_>, attrs: &[Attribute]) { +pub fn check(cx: &LateContext<'_>, attrs: &[Attribute]) -> bool { let replacements: Vec<_> = collect_doc_replacements(attrs); if let Some((&(lo_span, _), &(hi_span, _))) = replacements.first().zip(replacements.last()) { @@ -24,6 +24,10 @@ pub fn check(cx: &LateContext<'_>, attrs: &[Attribute]) { ); }, ); + + true + } else { + false } } diff --git a/clippy_lints/src/doc/too_long_first_doc_paragraph.rs b/clippy_lints/src/doc/too_long_first_doc_paragraph.rs index 7bb3bb12f2c..0165d24c7df 100644 --- a/clippy_lints/src/doc/too_long_first_doc_paragraph.rs +++ b/clippy_lints/src/doc/too_long_first_doc_paragraph.rs @@ -79,10 +79,17 @@ pub(super) fn check( && let new_span = first_span.with_hi(second_span.lo()).with_lo(first_span.hi()) && let Some(snippet) = snippet_opt(cx, new_span) { + let Some(first) = snippet_opt(cx, first_span) else { + return; + }; + let Some(comment_form) = first.get(..3) else { + return; + }; + diag.span_suggestion( new_span, "add an empty line", - format!("{snippet}///\n"), + format!("{snippet}{comment_form}{snippet}"), Applicability::MachineApplicable, ); } diff --git a/clippy_lints/src/field_scoped_visibility_modifiers.rs b/clippy_lints/src/field_scoped_visibility_modifiers.rs index 95b8e882da7..ba2b37fbf11 100644 --- a/clippy_lints/src/field_scoped_visibility_modifiers.rs +++ b/clippy_lints/src/field_scoped_visibility_modifiers.rs @@ -41,7 +41,7 @@ declare_clippy_lint! { /// } /// } /// ``` - #[clippy::version = "1.78.0"] + #[clippy::version = "1.81.0"] pub FIELD_SCOPED_VISIBILITY_MODIFIERS, restriction, "checks for usage of a scoped visibility modifier, like `pub(crate)`, on fields" diff --git a/clippy_lints/src/from_over_into.rs b/clippy_lints/src/from_over_into.rs index b84bf7c821c..5e9098474dc 100644 --- a/clippy_lints/src/from_over_into.rs +++ b/clippy_lints/src/from_over_into.rs @@ -181,6 +181,9 @@ fn convert_to_from( let from = self_ty.span.get_source_text(cx)?; let into = target_ty.span.get_source_text(cx)?; + let return_type = matches!(sig.decl.output, FnRetTy::Return(_)) + .then_some(String::from("Self")) + .unwrap_or_default(); let mut suggestions = vec![ // impl Into<T> for U -> impl From<T> for U // ~~~~ ~~~~ @@ -197,13 +200,10 @@ fn convert_to_from( // fn into([mut] self) -> T -> fn into([mut] v: T) -> T // ~~~~ ~~~~ (self_ident.span, format!("val: {from}")), - ]; - - if let FnRetTy::Return(_) = sig.decl.output { // fn into(self) -> T -> fn into(self) -> Self // ~ ~~~~ - suggestions.push((sig.decl.output.span(), String::from("Self"))); - } + (sig.decl.output.span(), return_type), + ]; let mut finder = SelfFinder { cx, diff --git a/clippy_lints/src/if_then_some_else_none.rs b/clippy_lints/src/if_then_some_else_none.rs index 0bca53c1536..55f9625709b 100644 --- a/clippy_lints/src/if_then_some_else_none.rs +++ b/clippy_lints/src/if_then_some_else_none.rs @@ -105,6 +105,8 @@ impl<'tcx> LateLintPass<'tcx> for IfThenSomeElseNone { snippet_with_context(cx, first_stmt.span.until(then_arg.span), ctxt, "..", &mut app); let closure = if method_name == "then" { "|| " } else { "" }; format!("{closure} {{ {block_snippet}; {arg_snip} }}") + } else if method_name == "then" { + (std::borrow::Cow::Borrowed("|| ") + arg_snip).into_owned() } else { arg_snip.into_owned() }; diff --git a/clippy_lints/src/implicit_saturating_sub.rs b/clippy_lints/src/implicit_saturating_sub.rs index 127c73ed637..3536309d83c 100644 --- a/clippy_lints/src/implicit_saturating_sub.rs +++ b/clippy_lints/src/implicit_saturating_sub.rs @@ -1,11 +1,17 @@ -use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::{higher, is_integer_literal, peel_blocks_with_stmt, SpanlessEq}; +use clippy_config::msrvs::{self, Msrv}; +use clippy_config::Conf; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::snippet_opt; +use clippy_utils::{ + higher, is_in_const_context, is_integer_literal, path_to_local, peel_blocks, peel_blocks_with_stmt, SpanlessEq, +}; use rustc_ast::ast::LitKind; use rustc_data_structures::packed::Pu128; use rustc_errors::Applicability; -use rustc_hir::{BinOpKind, Expr, ExprKind, QPath}; +use rustc_hir::{BinOp, BinOpKind, Expr, ExprKind, HirId, QPath}; use rustc_lint::{LateContext, LateLintPass}; -use rustc_session::declare_lint_pass; +use rustc_session::impl_lint_pass; +use rustc_span::Span; declare_clippy_lint! { /// ### What it does @@ -39,7 +45,49 @@ declare_clippy_lint! { "Perform saturating subtraction instead of implicitly checking lower bound of data type" } -declare_lint_pass!(ImplicitSaturatingSub => [IMPLICIT_SATURATING_SUB]); +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons between integers, followed by subtracting the greater value from the + /// lower one. + /// + /// ### Why is this bad? + /// This could result in an underflow and is most likely not what the user wants. If this was + /// intended to be a saturated subtraction, consider using the `saturating_sub` method directly. + /// + /// ### Example + /// ```no_run + /// let a = 12u32; + /// let b = 13u32; + /// + /// let result = if a > b { b - a } else { 0 }; + /// ``` + /// + /// Use instead: + /// ```no_run + /// let a = 12u32; + /// let b = 13u32; + /// + /// let result = a.saturating_sub(b); + /// ``` + #[clippy::version = "1.44.0"] + pub INVERTED_SATURATING_SUB, + correctness, + "Check if a variable is smaller than another one and still subtract from it even if smaller" +} + +pub struct ImplicitSaturatingSub { + msrv: Msrv, +} + +impl_lint_pass!(ImplicitSaturatingSub => [IMPLICIT_SATURATING_SUB, INVERTED_SATURATING_SUB]); + +impl ImplicitSaturatingSub { + pub fn new(conf: &'static Conf) -> Self { + Self { + msrv: conf.msrv.clone(), + } + } +} impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { @@ -50,73 +98,260 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { // Check if the conditional expression is a binary operation && let ExprKind::Binary(ref cond_op, cond_left, cond_right) = cond.kind + { + check_with_condition(cx, expr, cond_op.node, cond_left, cond_right, then); + } else if let Some(higher::If { + cond, + then: if_block, + r#else: Some(else_block), + }) = higher::If::hir(expr) + && let ExprKind::Binary(ref cond_op, cond_left, cond_right) = cond.kind + { + check_manual_check( + cx, expr, cond_op, cond_left, cond_right, if_block, else_block, &self.msrv, + ); + } + } + + extract_msrv_attr!(LateContext); +} - // Ensure that the binary operator is >, !=, or < - && (BinOpKind::Ne == cond_op.node || BinOpKind::Gt == cond_op.node || BinOpKind::Lt == cond_op.node) +#[allow(clippy::too_many_arguments)] +fn check_manual_check<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'tcx>, + condition: &BinOp, + left_hand: &Expr<'tcx>, + right_hand: &Expr<'tcx>, + if_block: &Expr<'tcx>, + else_block: &Expr<'tcx>, + msrv: &Msrv, +) { + let ty = cx.typeck_results().expr_ty(left_hand); + if ty.is_numeric() && !ty.is_signed() { + match condition.node { + BinOpKind::Gt | BinOpKind::Ge => check_gt( + cx, + condition.span, + expr.span, + left_hand, + right_hand, + if_block, + else_block, + msrv, + ), + BinOpKind::Lt | BinOpKind::Le => check_gt( + cx, + condition.span, + expr.span, + right_hand, + left_hand, + if_block, + else_block, + msrv, + ), + _ => {}, + } + } +} - // Check if assign operation is done - && let Some(target) = subtracts_one(cx, then) +#[allow(clippy::too_many_arguments)] +fn check_gt( + cx: &LateContext<'_>, + condition_span: Span, + expr_span: Span, + big_var: &Expr<'_>, + little_var: &Expr<'_>, + if_block: &Expr<'_>, + else_block: &Expr<'_>, + msrv: &Msrv, +) { + if let Some(big_var) = Var::new(big_var) + && let Some(little_var) = Var::new(little_var) + { + check_subtraction( + cx, + condition_span, + expr_span, + big_var, + little_var, + if_block, + else_block, + msrv, + ); + } +} - // Extracting out the variable name - && let ExprKind::Path(QPath::Resolved(_, ares_path)) = target.kind +struct Var { + span: Span, + hir_id: HirId, +} + +impl Var { + fn new(expr: &Expr<'_>) -> Option<Self> { + path_to_local(expr).map(|hir_id| Self { + span: expr.span, + hir_id, + }) + } +} + +#[allow(clippy::too_many_arguments)] +fn check_subtraction( + cx: &LateContext<'_>, + condition_span: Span, + expr_span: Span, + big_var: Var, + little_var: Var, + if_block: &Expr<'_>, + else_block: &Expr<'_>, + msrv: &Msrv, +) { + let if_block = peel_blocks(if_block); + let else_block = peel_blocks(else_block); + if is_integer_literal(if_block, 0) { + // We need to check this case as well to prevent infinite recursion. + if is_integer_literal(else_block, 0) { + // Well, seems weird but who knows? + return; + } + // If the subtraction is done in the `else` block, then we need to also revert the two + // variables as it means that the check was reverted too. + check_subtraction( + cx, + condition_span, + expr_span, + little_var, + big_var, + else_block, + if_block, + msrv, + ); + return; + } + if is_integer_literal(else_block, 0) + && let ExprKind::Binary(op, left, right) = if_block.kind + && let BinOpKind::Sub = op.node + { + let local_left = path_to_local(left); + let local_right = path_to_local(right); + if Some(big_var.hir_id) == local_left && Some(little_var.hir_id) == local_right { + // This part of the condition is voluntarily split from the one before to ensure that + // if `snippet_opt` fails, it won't try the next conditions. + if let Some(big_var_snippet) = snippet_opt(cx, big_var.span) + && let Some(little_var_snippet) = snippet_opt(cx, little_var.span) + && (!is_in_const_context(cx) || msrv.meets(msrvs::SATURATING_SUB_CONST)) + { + span_lint_and_sugg( + cx, + IMPLICIT_SATURATING_SUB, + expr_span, + "manual arithmetic check found", + "replace it with", + format!("{big_var_snippet}.saturating_sub({little_var_snippet})"), + Applicability::MachineApplicable, + ); + } + } else if Some(little_var.hir_id) == local_left + && Some(big_var.hir_id) == local_right + && let Some(big_var_snippet) = snippet_opt(cx, big_var.span) + && let Some(little_var_snippet) = snippet_opt(cx, little_var.span) { - // 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; - } + span_lint_and_then( + cx, + INVERTED_SATURATING_SUB, + condition_span, + "inverted arithmetic check before subtraction", + |diag| { + diag.span_note( + if_block.span, + format!("this subtraction underflows when `{little_var_snippet} < {big_var_snippet}`"), + ); + diag.span_suggestion( + if_block.span, + "try replacing it with", + format!("{big_var_snippet} - {little_var_snippet}"), + Applicability::MaybeIncorrect, + ); + }, + ); + } + } +} + +fn check_with_condition<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'tcx>, + cond_op: BinOpKind, + cond_left: &Expr<'tcx>, + cond_right: &Expr<'tcx>, + then: &Expr<'tcx>, +) { + // Ensure that the binary operator is >, !=, or < + if (BinOpKind::Ne == cond_op || BinOpKind::Gt == cond_op || BinOpKind::Lt == cond_op) + + // Check if assign operation is done + && let Some(target) = subtracts_one(cx, then) + + // Extracting out the variable name + && let ExprKind::Path(QPath::Resolved(_, ares_path)) = target.kind + { + // 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 || BinOpKind::Ne == cond_op { + (cond_left, cond_right) } else { return; - }; - - // Check if the variable in the condition statement is an integer - if !cx.typeck_results().expr_ty(cond_var).is_integral() { + } + } else if SpanlessEq::new(cx).eq_expr(cond_right, target) { + if BinOpKind::Lt == cond_op || BinOpKind::Ne == cond_op { + (cond_right, cond_left) + } else { return; } + } else { + return; + }; - // Get the variable name - let var_name = ares_path.segments[0].ident.name.as_str(); - match cond_num_val.kind { - ExprKind::Lit(cond_lit) => { - // Check if the constant is zero - if let LitKind::Int(Pu128(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 name.ident.as_str() == "MIN" - && let Some(const_id) = cx.typeck_results().type_dependent_def_id(cond_num_val.hir_id) - && let Some(impl_id) = cx.tcx.impl_of_method(const_id) - && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl - && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() - { - print_lint_and_sugg(cx, var_name, expr); - } - }, - ExprKind::Call(func, []) => { - if let ExprKind::Path(QPath::TypeRelative(_, name)) = func.kind - && name.ident.as_str() == "min_value" - && let Some(func_id) = cx.typeck_results().type_dependent_def_id(func.hir_id) - && let Some(impl_id) = cx.tcx.impl_of_method(func_id) - && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl - && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() - { + // 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(cond_lit) => { + // Check if the constant is zero + if let LitKind::Int(Pu128(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 name.ident.as_str() == "MIN" + && let Some(const_id) = cx.typeck_results().type_dependent_def_id(cond_num_val.hir_id) + && let Some(impl_id) = cx.tcx.impl_of_method(const_id) + && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl + && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() + { + print_lint_and_sugg(cx, var_name, expr); + } + }, + ExprKind::Call(func, []) => { + if let ExprKind::Path(QPath::TypeRelative(_, name)) = func.kind + && name.ident.as_str() == "min_value" + && let Some(func_id) = cx.typeck_results().type_dependent_def_id(func.hir_id) + && let Some(impl_id) = cx.tcx.impl_of_method(func_id) + && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl + && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() + { + print_lint_and_sugg(cx, var_name, expr); + } + }, + _ => (), } } } diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 2ac06b360be..b21d3f8d09e 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -203,9 +203,11 @@ mod manual_assert; mod manual_async_fn; mod manual_bits; mod manual_clamp; +mod manual_div_ceil; mod manual_float_methods; mod manual_hash_one; mod manual_is_ascii_check; +mod manual_is_power_of_two; mod manual_let_else; mod manual_main_separator_str; mod manual_non_exhaustive; @@ -271,6 +273,7 @@ mod non_copy_const; mod non_expressive_names; mod non_octal_unix_permissions; mod non_send_fields_in_send_ty; +mod non_zero_suggestions; mod nonstandard_macro_braces; mod octal_escapes; mod only_used_in_recursion; @@ -287,6 +290,7 @@ mod pass_by_ref_or_value; mod pathbuf_init_then_push; mod pattern_type_mismatch; mod permissions_set_readonly_false; +mod pointers_in_nomem_asm_block; mod precedence; mod ptr; mod ptr_offset_with_cast; @@ -372,6 +376,7 @@ mod unused_peekable; mod unused_result_ok; mod unused_rounding; mod unused_self; +mod unused_trait_names; mod unused_unit; mod unwrap; mod unwrap_in_result; @@ -386,6 +391,7 @@ mod write; mod zero_div_zero; mod zero_repeat_side_effects; mod zero_sized_map_values; +mod zombie_processes; // end lints modules, do not remove this comment, it’s used in `update_lints` use clippy_config::{get_configuration_metadata, Conf}; @@ -623,7 +629,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { 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(move |_| Box::new(implicit_saturating_sub::ImplicitSaturatingSub::new(conf))); 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)); @@ -632,8 +638,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { let format_args = format_args_storage.clone(); store.register_late_pass(move |_| Box::new(methods::Methods::new(conf, format_args.clone()))); store.register_late_pass(move |_| Box::new(matches::Matches::new(conf))); - store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveStruct::new(conf))); - store.register_late_pass(move |_| Box::new(manual_non_exhaustive::ManualNonExhaustiveEnum::new(conf))); + store.register_late_pass(move |_| Box::new(manual_non_exhaustive::ManualNonExhaustive::new(conf))); store.register_late_pass(move |_| Box::new(manual_strip::ManualStrip::new(conf))); store.register_early_pass(move || Box::new(redundant_static_lifetimes::RedundantStaticLifetimes::new(conf))); store.register_early_pass(move || Box::new(redundant_field_names::RedundantFieldNames::new(conf))); @@ -933,5 +938,11 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { store.register_late_pass(|_| Box::new(set_contains_or_insert::SetContainsOrInsert)); store.register_early_pass(|| Box::new(byte_char_slices::ByteCharSlice)); store.register_early_pass(|| Box::new(cfg_not_test::CfgNotTest)); + store.register_late_pass(|_| Box::new(zombie_processes::ZombieProcesses)); + store.register_late_pass(|_| Box::new(pointers_in_nomem_asm_block::PointersInNomemAsmBlock)); + store.register_late_pass(move |_| Box::new(manual_div_ceil::ManualDivCeil::new(conf))); + store.register_late_pass(|_| Box::new(manual_is_power_of_two::ManualIsPowerOfTwo)); + store.register_late_pass(|_| Box::new(non_zero_suggestions::NonZeroSuggestions)); + store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf))); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/clippy_lints/src/loops/mod.rs b/clippy_lints/src/loops/mod.rs index 92ccc0cc0a1..5155fd76b25 100644 --- a/clippy_lints/src/loops/mod.rs +++ b/clippy_lints/src/loops/mod.rs @@ -188,22 +188,22 @@ declare_clippy_lint! { /// 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); + /// let y = Some(1); /// loop { /// let x = match y { /// Some(x) => x, /// None => break, /// }; - /// // .. do something with x + /// // .. /// } - /// // is easier written as + /// ``` + /// Use instead: + /// ```rust,no_run + /// let y = Some(1); /// while let Some(x) = y { - /// // .. do something with x + /// // .. /// }; /// ``` #[clippy::version = "pre 1.29.0"] diff --git a/clippy_lints/src/manual_div_ceil.rs b/clippy_lints/src/manual_div_ceil.rs new file mode 100644 index 00000000000..024c2547dc6 --- /dev/null +++ b/clippy_lints/src/manual_div_ceil.rs @@ -0,0 +1,158 @@ +use clippy_config::msrvs::{self, Msrv}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::sugg::Sugg; +use clippy_utils::SpanlessEq; +use rustc_ast::{BinOpKind, LitKind}; +use rustc_data_structures::packed::Pu128; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self}; +use rustc_session::impl_lint_pass; +use rustc_span::symbol::Symbol; + +use clippy_config::Conf; + +declare_clippy_lint! { + /// ### What it does + /// Checks for an expression like `(x + (y - 1)) / y` which is a common manual reimplementation + /// of `x.div_ceil(y)`. + /// + /// ### Why is this bad? + /// It's simpler, clearer and more readable. + /// + /// ### Example + /// ```no_run + /// let x: i32 = 7; + /// let y: i32 = 4; + /// let div = (x + (y - 1)) / y; + /// ``` + /// Use instead: + /// ```no_run + /// #![feature(int_roundings)] + /// let x: i32 = 7; + /// let y: i32 = 4; + /// let div = x.div_ceil(y); + /// ``` + #[clippy::version = "1.81.0"] + pub MANUAL_DIV_CEIL, + complexity, + "manually reimplementing `div_ceil`" +} + +pub struct ManualDivCeil { + msrv: Msrv, +} + +impl ManualDivCeil { + #[must_use] + pub fn new(conf: &'static Conf) -> Self { + Self { + msrv: conf.msrv.clone(), + } + } +} + +impl_lint_pass!(ManualDivCeil => [MANUAL_DIV_CEIL]); + +impl<'tcx> LateLintPass<'tcx> for ManualDivCeil { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) { + if !self.msrv.meets(msrvs::MANUAL_DIV_CEIL) { + return; + } + + let mut applicability = Applicability::MachineApplicable; + + if let ExprKind::Binary(div_op, div_lhs, div_rhs) = expr.kind + && div_op.node == BinOpKind::Div + && check_int_ty_and_feature(cx, div_lhs) + && check_int_ty_and_feature(cx, div_rhs) + && let ExprKind::Binary(inner_op, inner_lhs, inner_rhs) = div_lhs.kind + { + // (x + (y - 1)) / y + if let ExprKind::Binary(sub_op, sub_lhs, sub_rhs) = inner_rhs.kind + && inner_op.node == BinOpKind::Add + && sub_op.node == BinOpKind::Sub + && check_literal(sub_rhs) + && check_eq_expr(cx, sub_lhs, div_rhs) + { + build_suggestion(cx, expr, inner_lhs, div_rhs, &mut applicability); + return; + } + + // ((y - 1) + x) / y + if let ExprKind::Binary(sub_op, sub_lhs, sub_rhs) = inner_lhs.kind + && inner_op.node == BinOpKind::Add + && sub_op.node == BinOpKind::Sub + && check_literal(sub_rhs) + && check_eq_expr(cx, sub_lhs, div_rhs) + { + build_suggestion(cx, expr, inner_rhs, div_rhs, &mut applicability); + return; + } + + // (x + y - 1) / y + if let ExprKind::Binary(add_op, add_lhs, add_rhs) = inner_lhs.kind + && inner_op.node == BinOpKind::Sub + && add_op.node == BinOpKind::Add + && check_literal(inner_rhs) + && check_eq_expr(cx, add_rhs, div_rhs) + { + build_suggestion(cx, expr, add_lhs, div_rhs, &mut applicability); + } + } + } + + extract_msrv_attr!(LateContext); +} + +fn check_int_ty_and_feature(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let expr_ty = cx.typeck_results().expr_ty(expr); + match expr_ty.peel_refs().kind() { + ty::Uint(_) => true, + ty::Int(_) => cx + .tcx + .features() + .declared_features + .contains(&Symbol::intern("int_roundings")), + + _ => false, + } +} + +fn check_literal(expr: &Expr<'_>) -> bool { + if let ExprKind::Lit(lit) = expr.kind + && let LitKind::Int(Pu128(1), _) = lit.node + { + return true; + } + false +} + +fn check_eq_expr(cx: &LateContext<'_>, lhs: &Expr<'_>, rhs: &Expr<'_>) -> bool { + SpanlessEq::new(cx).eq_expr(lhs, rhs) +} + +fn build_suggestion( + cx: &LateContext<'_>, + expr: &Expr<'_>, + lhs: &Expr<'_>, + rhs: &Expr<'_>, + applicability: &mut Applicability, +) { + let dividend_sugg = Sugg::hir_with_applicability(cx, lhs, "..", applicability).maybe_par(); + let divisor_snippet = snippet_with_applicability(cx, rhs.span.source_callsite(), "..", applicability); + + let sugg = format!("{dividend_sugg}.div_ceil({divisor_snippet})"); + + span_lint_and_sugg( + cx, + MANUAL_DIV_CEIL, + expr.span, + "manually reimplementing `div_ceil`", + "consider using `.div_ceil()`", + sugg, + *applicability, + ); +} diff --git a/clippy_lints/src/manual_is_power_of_two.rs b/clippy_lints/src/manual_is_power_of_two.rs new file mode 100644 index 00000000000..d7879403ebb --- /dev/null +++ b/clippy_lints/src/manual_is_power_of_two.rs @@ -0,0 +1,142 @@ +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_data_structures::packed::Pu128; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::Uint; +use rustc_session::declare_lint_pass; + +declare_clippy_lint! { + /// ### What it does + /// Checks for expressions like `x.count_ones() == 1` or `x & (x - 1) == 0`, with x and unsigned integer, which are manual + /// reimplementations of `x.is_power_of_two()`. + /// ### Why is this bad? + /// Manual reimplementations of `is_power_of_two` increase code complexity for little benefit. + /// ### Example + /// ```no_run + /// let a: u32 = 4; + /// let result = a.count_ones() == 1; + /// ``` + /// Use instead: + /// ```no_run + /// let a: u32 = 4; + /// let result = a.is_power_of_two(); + /// ``` + #[clippy::version = "1.82.0"] + pub MANUAL_IS_POWER_OF_TWO, + complexity, + "manually reimplementing `is_power_of_two`" +} + +declare_lint_pass!(ManualIsPowerOfTwo => [MANUAL_IS_POWER_OF_TWO]); + +impl LateLintPass<'_> for ManualIsPowerOfTwo { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + let mut applicability = Applicability::MachineApplicable; + + if let ExprKind::Binary(bin_op, left, right) = expr.kind + && bin_op.node == BinOpKind::Eq + { + // a.count_ones() == 1 + if let ExprKind::MethodCall(method_name, reciever, _, _) = left.kind + && method_name.ident.as_str() == "count_ones" + && let &Uint(_) = cx.typeck_results().expr_ty(reciever).kind() + && check_lit(right, 1) + { + build_sugg(cx, expr, reciever, &mut applicability); + } + + // 1 == a.count_ones() + if let ExprKind::MethodCall(method_name, reciever, _, _) = right.kind + && method_name.ident.as_str() == "count_ones" + && let &Uint(_) = cx.typeck_results().expr_ty(reciever).kind() + && check_lit(left, 1) + { + build_sugg(cx, expr, reciever, &mut applicability); + } + + // a & (a - 1) == 0 + if let ExprKind::Binary(op1, left1, right1) = left.kind + && op1.node == BinOpKind::BitAnd + && let ExprKind::Binary(op2, left2, right2) = right1.kind + && op2.node == BinOpKind::Sub + && check_eq_expr(cx, left1, left2) + && let &Uint(_) = cx.typeck_results().expr_ty(left1).kind() + && check_lit(right2, 1) + && check_lit(right, 0) + { + build_sugg(cx, expr, left1, &mut applicability); + } + + // (a - 1) & a == 0; + if let ExprKind::Binary(op1, left1, right1) = left.kind + && op1.node == BinOpKind::BitAnd + && let ExprKind::Binary(op2, left2, right2) = left1.kind + && op2.node == BinOpKind::Sub + && check_eq_expr(cx, right1, left2) + && let &Uint(_) = cx.typeck_results().expr_ty(right1).kind() + && check_lit(right2, 1) + && check_lit(right, 0) + { + build_sugg(cx, expr, right1, &mut applicability); + } + + // 0 == a & (a - 1); + if let ExprKind::Binary(op1, left1, right1) = right.kind + && op1.node == BinOpKind::BitAnd + && let ExprKind::Binary(op2, left2, right2) = right1.kind + && op2.node == BinOpKind::Sub + && check_eq_expr(cx, left1, left2) + && let &Uint(_) = cx.typeck_results().expr_ty(left1).kind() + && check_lit(right2, 1) + && check_lit(left, 0) + { + build_sugg(cx, expr, left1, &mut applicability); + } + + // 0 == (a - 1) & a + if let ExprKind::Binary(op1, left1, right1) = right.kind + && op1.node == BinOpKind::BitAnd + && let ExprKind::Binary(op2, left2, right2) = left1.kind + && op2.node == BinOpKind::Sub + && check_eq_expr(cx, right1, left2) + && let &Uint(_) = cx.typeck_results().expr_ty(right1).kind() + && check_lit(right2, 1) + && check_lit(left, 0) + { + build_sugg(cx, expr, right1, &mut applicability); + } + } + } +} + +fn build_sugg(cx: &LateContext<'_>, expr: &Expr<'_>, reciever: &Expr<'_>, applicability: &mut Applicability) { + let snippet = snippet_with_applicability(cx, reciever.span, "..", applicability); + + span_lint_and_sugg( + cx, + MANUAL_IS_POWER_OF_TWO, + expr.span, + "manually reimplementing `is_power_of_two`", + "consider using `.is_power_of_two()`", + format!("{snippet}.is_power_of_two()"), + *applicability, + ); +} + +fn check_lit(expr: &Expr<'_>, expected_num: u128) -> bool { + if let ExprKind::Lit(lit) = expr.kind + && let LitKind::Int(Pu128(num), _) = lit.node + && num == expected_num + { + return true; + } + false +} + +fn check_eq_expr(cx: &LateContext<'_>, lhs: &Expr<'_>, rhs: &Expr<'_>) -> bool { + SpanlessEq::new(cx).eq_expr(lhs, rhs) +} diff --git a/clippy_lints/src/manual_non_exhaustive.rs b/clippy_lints/src/manual_non_exhaustive.rs index 6b1d90483cc..01f1d9c3beb 100644 --- a/clippy_lints/src/manual_non_exhaustive.rs +++ b/clippy_lints/src/manual_non_exhaustive.rs @@ -2,16 +2,16 @@ use clippy_config::msrvs::{self, Msrv}; use clippy_config::Conf; use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then}; use clippy_utils::is_doc_hidden; -use clippy_utils::source::SpanRangeExt; -use rustc_ast::ast::{self, VisibilityKind}; +use clippy_utils::source::snippet_indent; +use itertools::Itertools; use rustc_ast::attr; 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_hir::{Expr, ExprKind, Item, ItemKind, QPath, TyKind, VariantData}; +use rustc_lint::{LateContext, LateLintPass}; use rustc_session::impl_lint_pass; -use rustc_span::def_id::{DefId, LocalDefId}; +use rustc_span::def_id::LocalDefId; use rustc_span::{sym, Span}; declare_clippy_lint! { @@ -62,29 +62,13 @@ declare_clippy_lint! { "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]" } -#[expect(clippy::module_name_repetitions)] -pub struct ManualNonExhaustiveStruct { +pub struct ManualNonExhaustive { msrv: Msrv, -} - -impl ManualNonExhaustiveStruct { - pub fn new(conf: &'static Conf) -> Self { - Self { - msrv: conf.msrv.clone(), - } - } -} - -impl_lint_pass!(ManualNonExhaustiveStruct => [MANUAL_NON_EXHAUSTIVE]); - -#[expect(clippy::module_name_repetitions)] -pub struct ManualNonExhaustiveEnum { - msrv: Msrv, - constructed_enum_variants: FxHashSet<(DefId, DefId)>, + constructed_enum_variants: FxHashSet<LocalDefId>, potential_enums: Vec<(LocalDefId, LocalDefId, Span, Span)>, } -impl ManualNonExhaustiveEnum { +impl ManualNonExhaustive { pub fn new(conf: &'static Conf) -> Self { Self { msrv: conf.msrv.clone(), @@ -94,96 +78,78 @@ impl ManualNonExhaustiveEnum { } } -impl_lint_pass!(ManualNonExhaustiveEnum => [MANUAL_NON_EXHAUSTIVE]); - -impl EarlyLintPass for ManualNonExhaustiveStruct { - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { - 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, - } - && fields.len() > 1 - && self.msrv.meets(msrvs::NON_EXHAUSTIVE) - { - 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() - { - 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) = header_span.get_source_text(cx) - { - diag.span_suggestion( - header_span, - "add the attribute", - format!("#[non_exhaustive] {snippet}"), - Applicability::Unspecified, - ); - } - diag.span_help(field.span, "remove this field"); - }, - ); - } - } - } +impl_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]); - extract_msrv_attr!(EarlyContext); -} - -impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustiveEnum { - fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { - if !self.msrv.meets(msrvs::NON_EXHAUSTIVE) { +impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustive { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) { + if !self.msrv.meets(msrvs::NON_EXHAUSTIVE) || !cx.effective_visibilities.is_exported(item.owner_id.def_id) { return; } - if let hir::ItemKind::Enum(def, _) = &item.kind - && def.variants.len() > 1 - { - let mut iter = def.variants.iter().filter_map(|v| { - (matches!(v.data, hir::VariantData::Unit(_, _)) - && is_doc_hidden(cx.tcx.hir().attrs(v.hir_id)) - && !attr::contains_name(cx.tcx.hir().attrs(item.hir_id()), sym::non_exhaustive)) - .then_some((v.def_id, v.span)) - }); - if let Some((id, span)) = iter.next() - && iter.next().is_none() - { - self.potential_enums.push((item.owner_id.def_id, id, item.span, span)); - } + match item.kind { + ItemKind::Enum(def, _) if def.variants.len() > 1 => { + let iter = def.variants.iter().filter_map(|v| { + (matches!(v.data, VariantData::Unit(_, _)) && is_doc_hidden(cx.tcx.hir().attrs(v.hir_id))) + .then_some((v.def_id, v.span)) + }); + if let Ok((id, span)) = iter.exactly_one() + && !attr::contains_name(cx.tcx.hir().attrs(item.hir_id()), sym::non_exhaustive) + { + self.potential_enums.push((item.owner_id.def_id, id, item.span, span)); + } + }, + ItemKind::Struct(variant_data, _) => { + let fields = variant_data.fields(); + let private_fields = fields + .iter() + .filter(|field| !cx.effective_visibilities.is_exported(field.def_id)); + if fields.len() > 1 + && let Ok(field) = private_fields.exactly_one() + && let TyKind::Tup([]) = field.ty.kind + { + span_lint_and_then( + cx, + MANUAL_NON_EXHAUSTIVE, + item.span, + "this seems like a manual implementation of the non-exhaustive pattern", + |diag| { + if let Some(non_exhaustive) = + attr::find_by_name(cx.tcx.hir().attrs(item.hir_id()), sym::non_exhaustive) + { + diag.span_note(non_exhaustive.span, "the struct is already non-exhaustive"); + } else { + let indent = snippet_indent(cx, item.span).unwrap_or_default(); + diag.span_suggestion_verbose( + item.span.shrink_to_lo(), + "use the `#[non_exhaustive]` attribute instead", + format!("#[non_exhaustive]\n{indent}"), + Applicability::MaybeIncorrect, + ); + } + diag.span_help(field.span, "remove this field"); + }, + ); + } + }, + _ => {}, } } fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { if let ExprKind::Path(QPath::Resolved(None, p)) = &e.kind - && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = p.res + && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), ctor_id) = p.res + && let Some(local_ctor) = ctor_id.as_local() { - let variant_id = cx.tcx.parent(id); - let enum_id = cx.tcx.parent(variant_id); - - self.constructed_enum_variants.insert((enum_id, variant_id)); + let variant_id = cx.tcx.local_parent(local_ctor); + self.constructed_enum_variants.insert(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())) - }) + for &(enum_id, _, enum_span, variant_span) in self + .potential_enums + .iter() + .filter(|(_, variant_id, _, _)| !self.constructed_enum_variants.contains(variant_id)) { let hir_id = cx.tcx.local_def_id_to_hir_id(enum_id); span_lint_hir_and_then( @@ -193,15 +159,13 @@ impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustiveEnum { enum_span, "this seems like a manual implementation of the non-exhaustive pattern", |diag| { - let header_span = cx.sess().source_map().span_until_char(enum_span, '{'); - if let Some(snippet) = header_span.get_source_text(cx) { - diag.span_suggestion( - header_span, - "add the attribute", - format!("#[non_exhaustive] {snippet}"), - Applicability::Unspecified, - ); - } + let indent = snippet_indent(cx, enum_span).unwrap_or_default(); + diag.span_suggestion_verbose( + enum_span.shrink_to_lo(), + "use the `#[non_exhaustive]` attribute instead", + format!("#[non_exhaustive]\n{indent}"), + Applicability::MaybeIncorrect, + ); diag.span_help(variant_span, "remove this variant"); }, ); diff --git a/clippy_lints/src/manual_range_patterns.rs b/clippy_lints/src/manual_range_patterns.rs index 9afb2b5bc84..66d2ab6b24a 100644 --- a/clippy_lints/src/manual_range_patterns.rs +++ b/clippy_lints/src/manual_range_patterns.rs @@ -77,9 +77,10 @@ impl Num { impl LateLintPass<'_> for ManualRangePatterns { fn check_pat(&mut self, cx: &LateContext<'_>, pat: &'_ rustc_hir::Pat<'_>) { // a pattern like 1 | 2 seems fine, lint if there are at least 3 alternatives - // or at least one range + // or more then one range (exclude triggering on stylistic using OR with one element + // like described https://github.com/rust-lang/rust-clippy/issues/11825) if let PatKind::Or(pats) = pat.kind - && (pats.len() >= 3 || pats.iter().any(|p| matches!(p.kind, PatKind::Range(..)))) + && (pats.len() >= 3 || (pats.len() > 1 && pats.iter().any(|p| matches!(p.kind, PatKind::Range(..))))) && !in_external_macro(cx.sess(), pat.span) { let mut min = Num::dummy(i128::MAX); diff --git a/clippy_lints/src/matches/single_match.rs b/clippy_lints/src/matches/single_match.rs index b6930f7b9d1..e7cef5bdbd7 100644 --- a/clippy_lints/src/matches/single_match.rs +++ b/clippy_lints/src/matches/single_match.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::source::{expr_block, snippet, SpanRangeExt}; +use clippy_utils::source::{expr_block, snippet, snippet_block_with_context, SpanRangeExt}; use clippy_utils::ty::implements_trait; use clippy_utils::{ is_lint_allowed, is_unit_expr, peel_blocks, peel_hir_pat_refs, peel_middle_ty_refs, peel_n_hir_expr_refs, @@ -9,7 +9,7 @@ use rustc_arena::DroplessArena; use rustc_errors::Applicability; use rustc_hir::def::{DefKind, Res}; use rustc_hir::intravisit::{walk_pat, Visitor}; -use rustc_hir::{Arm, Expr, ExprKind, HirId, Pat, PatKind, QPath}; +use rustc_hir::{Arm, Expr, ExprKind, HirId, Node, Pat, PatKind, QPath, StmtKind}; use rustc_lint::LateContext; use rustc_middle::ty::{self, AdtDef, ParamEnv, TyCtxt, TypeckResults, VariantDef}; use rustc_span::{sym, Span}; @@ -91,6 +91,29 @@ fn report_single_pattern(cx: &LateContext<'_>, ex: &Expr<'_>, arm: &Arm<'_>, exp format!(" else {}", expr_block(cx, els, ctxt, "..", Some(expr.span), &mut app)) }); + if snippet(cx, ex.span, "..") == snippet(cx, arm.pat.span, "..") { + let msg = "this pattern is irrefutable, `match` is useless"; + let (sugg, help) = if is_unit_expr(arm.body) { + (String::new(), "`match` expression can be removed") + } else { + let mut sugg = snippet_block_with_context(cx, arm.body.span, ctxt, "..", Some(expr.span), &mut app) + .0 + .to_string(); + if let Node::Stmt(stmt) = cx.tcx.parent_hir_node(expr.hir_id) + && let StmtKind::Expr(_) = stmt.kind + && match arm.body.kind { + ExprKind::Block(block, _) => block.span.from_expansion(), + _ => true, + } + { + sugg.push(';'); + } + (sugg, "try") + }; + span_lint_and_sugg(cx, lint, expr.span, msg, help, sugg.to_string(), app); + return; + } + let (pat, pat_ref_count) = peel_hir_pat_refs(arm.pat); let (msg, sugg) = if let PatKind::Path(_) | PatKind::Lit(_) = pat.kind && let (ty, ty_ref_count) = peel_middle_ty_refs(cx.typeck_results().expr_ty(ex)) diff --git a/clippy_lints/src/methods/iter_kv_map.rs b/clippy_lints/src/methods/iter_kv_map.rs index 33de3b87abc..390dd24b505 100644 --- a/clippy_lints/src/methods/iter_kv_map.rs +++ b/clippy_lints/src/methods/iter_kv_map.rs @@ -14,7 +14,6 @@ use rustc_span::sym; /// - `hashmap.into_iter().map(|(_, v)| v)` /// /// on `HashMaps` and `BTreeMaps` in std - pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, map_type: &'tcx str, // iter / into_iter diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs index f61bb3a6bf4..098d3fca9b2 100644 --- a/clippy_lints/src/methods/mod.rs +++ b/clippy_lints/src/methods/mod.rs @@ -111,6 +111,7 @@ mod uninit_assumed_init; mod unit_hash; mod unnecessary_fallible_conversions; mod unnecessary_filter_map; +mod unnecessary_first_then_check; mod unnecessary_fold; mod unnecessary_get_then_check; mod unnecessary_iter_cloned; @@ -441,6 +442,17 @@ declare_clippy_lint! { /// } /// } /// ``` + /// + /// Use instead: + /// ```no_run + /// # struct X; + /// impl X { + /// fn as_str(&self) -> &'static str { + /// // .. + /// # "" + /// } + /// } + /// ``` #[clippy::version = "pre 1.29.0"] pub WRONG_SELF_CONVENTION, style, @@ -3964,7 +3976,7 @@ declare_clippy_lint! { /// ```no_run /// let _ = 0; /// ``` - #[clippy::version = "1.78.0"] + #[clippy::version = "1.81.0"] pub UNNECESSARY_MIN_OR_MAX, complexity, "using 'min()/max()' when there is no need for it" @@ -4025,7 +4037,7 @@ declare_clippy_lint! { /// ``` #[clippy::version = "1.78.0"] pub MANUAL_C_STR_LITERALS, - pedantic, + complexity, r#"creating a `CStr` through functions when `c""` literals can be used"# } @@ -4099,7 +4111,7 @@ declare_clippy_lint! { /// ```no_run /// "foo".is_ascii(); /// ``` - #[clippy::version = "1.80.0"] + #[clippy::version = "1.81.0"] pub NEEDLESS_CHARACTER_ITERATION, suspicious, "is_ascii() called on a char iterator" @@ -4126,6 +4138,34 @@ declare_clippy_lint! { "use of `map` returning the original item" } +declare_clippy_lint! { + /// ### What it does + /// Checks the usage of `.first().is_some()` or `.first().is_none()` to check if a slice is + /// empty. + /// + /// ### Why is this bad? + /// Using `.is_empty()` is shorter and better communicates the intention. + /// + /// ### Example + /// ```no_run + /// let v = vec![1, 2, 3]; + /// if v.first().is_none() { + /// // The vector is empty... + /// } + /// ``` + /// Use instead: + /// ```no_run + /// let v = vec![1, 2, 3]; + /// if v.is_empty() { + /// // The vector is empty... + /// } + /// ``` + #[clippy::version = "1.83.0"] + pub UNNECESSARY_FIRST_THEN_CHECK, + complexity, + "calling `.first().is_some()` or `.first().is_none()` instead of `.is_empty()`" +} + pub struct Methods { avoid_breaking_exported_api: bool, msrv: Msrv, @@ -4283,6 +4323,7 @@ impl_lint_pass!(Methods => [ UNNECESSARY_RESULT_MAP_OR_ELSE, MANUAL_C_STR_LITERALS, UNNECESSARY_GET_THEN_CHECK, + UNNECESSARY_FIRST_THEN_CHECK, NEEDLESS_CHARACTER_ITERATION, MANUAL_INSPECT, UNNECESSARY_MIN_OR_MAX, @@ -5055,6 +5096,9 @@ fn check_is_some_is_none(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, Some(("get", f_recv, [arg], _, _)) => { unnecessary_get_then_check::check(cx, call_span, recv, f_recv, arg, is_some); }, + Some(("first", f_recv, [], _, _)) => { + unnecessary_first_then_check::check(cx, call_span, recv, f_recv, is_some); + }, _ => {}, } } diff --git a/clippy_lints/src/methods/unnecessary_first_then_check.rs b/clippy_lints/src/methods/unnecessary_first_then_check.rs new file mode 100644 index 00000000000..7ae1bb54e60 --- /dev/null +++ b/clippy_lints/src/methods/unnecessary_first_then_check.rs @@ -0,0 +1,56 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::SpanRangeExt; + +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::LateContext; +use rustc_span::Span; + +use super::UNNECESSARY_FIRST_THEN_CHECK; + +pub(super) fn check( + cx: &LateContext<'_>, + call_span: Span, + first_call: &Expr<'_>, + first_caller: &Expr<'_>, + is_some: bool, +) { + if !cx + .typeck_results() + .expr_ty_adjusted(first_caller) + .peel_refs() + .is_slice() + { + return; + } + + let ExprKind::MethodCall(_, _, _, first_call_span) = first_call.kind else { + return; + }; + + let both_calls_span = first_call_span.with_hi(call_span.hi()); + if let Some(both_calls_snippet) = both_calls_span.get_source_text(cx) + && let Some(first_caller_snippet) = first_caller.span.get_source_text(cx) + { + let (sugg_span, suggestion) = if is_some { + ( + first_caller.span.with_hi(call_span.hi()), + format!("!{first_caller_snippet}.is_empty()"), + ) + } else { + (both_calls_span, "is_empty()".to_owned()) + }; + span_lint_and_sugg( + cx, + UNNECESSARY_FIRST_THEN_CHECK, + sugg_span, + format!( + "unnecessary use of `{both_calls_snippet}` to check if slice {}", + if is_some { "is not empty" } else { "is empty" } + ), + "replace this with", + suggestion, + Applicability::MaybeIncorrect, + ); + } +} diff --git a/clippy_lints/src/methods/unnecessary_fold.rs b/clippy_lints/src/methods/unnecessary_fold.rs index ccc8d17970e..380ea317b94 100644 --- a/clippy_lints/src/methods/unnecessary_fold.rs +++ b/clippy_lints/src/methods/unnecessary_fold.rs @@ -150,18 +150,20 @@ pub(super) fn check( }, ); }, - ast::LitKind::Int(Pu128(0), _) => check_fold_with_op( - cx, - expr, - acc, - fold_span, - hir::BinOpKind::Add, - Replacement { - has_args: false, - has_generic_return: needs_turbofish(cx, expr), - method_name: "sum", - }, - ), + ast::LitKind::Int(Pu128(0), _) => { + check_fold_with_op( + cx, + expr, + acc, + fold_span, + hir::BinOpKind::Add, + Replacement { + has_args: false, + has_generic_return: needs_turbofish(cx, expr), + method_name: "sum", + }, + ); + }, ast::LitKind::Int(Pu128(1), _) => { check_fold_with_op( cx, diff --git a/clippy_lints/src/methods/unnecessary_min_or_max.rs b/clippy_lints/src/methods/unnecessary_min_or_max.rs index 86c0a6322b6..ff5fa7d33e3 100644 --- a/clippy_lints/src/methods/unnecessary_min_or_max.rs +++ b/clippy_lints/src/methods/unnecessary_min_or_max.rs @@ -1,16 +1,15 @@ use std::cmp::Ordering; use super::UNNECESSARY_MIN_OR_MAX; -use clippy_utils::diagnostics::span_lint_and_sugg; - use clippy_utils::consts::{ConstEvalCtxt, Constant, ConstantSource, FullInt}; +use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::source::snippet; use rustc_errors::Applicability; use rustc_hir::Expr; use rustc_lint::LateContext; use rustc_middle::ty; -use rustc_span::Span; +use rustc_span::{sym, Span}; pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, @@ -21,26 +20,30 @@ pub(super) fn check<'tcx>( ) { let typeck_results = cx.typeck_results(); let ecx = ConstEvalCtxt::with_env(cx.tcx, cx.param_env, typeck_results); - if let Some((left, ConstantSource::Local | ConstantSource::CoreConstant)) = ecx.eval_with_source(recv) - && let Some((right, ConstantSource::Local | ConstantSource::CoreConstant)) = ecx.eval_with_source(arg) + if let Some(id) = typeck_results.type_dependent_def_id(expr.hir_id) + && (cx.tcx.is_diagnostic_item(sym::cmp_ord_min, id) || cx.tcx.is_diagnostic_item(sym::cmp_ord_max, id)) { - let Some(ord) = Constant::partial_cmp(cx.tcx, typeck_results.expr_ty(recv), &left, &right) else { - return; - }; + if let Some((left, ConstantSource::Local | ConstantSource::CoreConstant)) = ecx.eval_with_source(recv) + && let Some((right, ConstantSource::Local | ConstantSource::CoreConstant)) = ecx.eval_with_source(arg) + { + let Some(ord) = Constant::partial_cmp(cx.tcx, typeck_results.expr_ty(recv), &left, &right) else { + return; + }; - lint(cx, expr, name, recv.span, arg.span, ord); - } else if let Some(extrema) = detect_extrema(cx, recv) { - let ord = match extrema { - Extrema::Minimum => Ordering::Less, - Extrema::Maximum => Ordering::Greater, - }; - lint(cx, expr, name, recv.span, arg.span, ord); - } else if let Some(extrema) = detect_extrema(cx, arg) { - let ord = match extrema { - Extrema::Minimum => Ordering::Greater, - Extrema::Maximum => Ordering::Less, - }; - lint(cx, expr, name, recv.span, arg.span, ord); + lint(cx, expr, name, recv.span, arg.span, ord); + } else if let Some(extrema) = detect_extrema(cx, recv) { + let ord = match extrema { + Extrema::Minimum => Ordering::Less, + Extrema::Maximum => Ordering::Greater, + }; + lint(cx, expr, name, recv.span, arg.span, ord); + } else if let Some(extrema) = detect_extrema(cx, arg) { + let ord = match extrema { + Extrema::Minimum => Ordering::Greater, + Extrema::Maximum => Ordering::Less, + }; + lint(cx, expr, name, recv.span, arg.span, ord); + } } } diff --git a/clippy_lints/src/methods/unnecessary_sort_by.rs b/clippy_lints/src/methods/unnecessary_sort_by.rs index 6911da69b94..a92fe9e55a4 100644 --- a/clippy_lints/src/methods/unnecessary_sort_by.rs +++ b/clippy_lints/src/methods/unnecessary_sort_by.rs @@ -5,7 +5,8 @@ use clippy_utils::ty::implements_trait; use rustc_errors::Applicability; use rustc_hir::{Closure, Expr, ExprKind, Mutability, Param, Pat, PatKind, Path, PathSegment, QPath}; use rustc_lint::LateContext; -use rustc_middle::ty::{self, GenericArgKind}; +use rustc_middle::ty; +use rustc_middle::ty::GenericArgKind; use rustc_span::sym; use rustc_span::symbol::Ident; use std::iter; diff --git a/clippy_lints/src/methods/unnecessary_to_owned.rs b/clippy_lints/src/methods/unnecessary_to_owned.rs index bf7555a29e2..f62cf55e72a 100644 --- a/clippy_lints/src/methods/unnecessary_to_owned.rs +++ b/clippy_lints/src/methods/unnecessary_to_owned.rs @@ -717,7 +717,7 @@ fn check_if_applicable_to_argument<'tcx>(cx: &LateContext<'tcx>, arg: &Expr<'tcx // check that: // 1. This is a method with only one argument that doesn't come from a trait. // 2. That it has `Borrow` in its generic predicates. -// 3. `Self` is a std "map type" (ie `HashSet`, `HashMap`, BTreeSet`, `BTreeMap`). +// 3. `Self` is a std "map type" (ie `HashSet`, `HashMap`, `BTreeSet`, `BTreeMap`). fn check_borrow_predicate<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { if let ExprKind::MethodCall(_, caller, &[arg], _) = expr.kind && let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) diff --git a/clippy_lints/src/misc.rs b/clippy_lints/src/misc.rs index a9aafe7ed56..3e80e48a948 100644 --- a/clippy_lints/src/misc.rs +++ b/clippy_lints/src/misc.rs @@ -7,6 +7,7 @@ use clippy_utils::{ }; use rustc_errors::Applicability; use rustc_hir::def::Res; +use rustc_hir::def_id::LOCAL_CRATE; use rustc_hir::intravisit::FnKind; use rustc_hir::{ BinOpKind, BindingMode, Body, ByRef, Expr, ExprKind, FnDecl, Mutability, PatKind, QPath, Stmt, StmtKind, @@ -82,6 +83,45 @@ declare_clippy_lint! { declare_clippy_lint! { /// ### What it does + /// Checks for the use of item with a single leading + /// underscore. + /// + /// ### Why is this bad? + /// A single leading underscore is usually used to indicate + /// that a item will not be used. Using such a item breaks this + /// expectation. + /// + /// ### Example + /// ```no_run + /// fn _foo() {} + /// + /// struct _FooStruct {} + /// + /// fn main() { + /// _foo(); + /// let _ = _FooStruct{}; + /// } + /// ``` + /// + /// Use instead: + /// ```no_run + /// fn foo() {} + /// + /// struct FooStruct {} + /// + /// fn main() { + /// foo(); + /// let _ = FooStruct{}; + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub USED_UNDERSCORE_ITEMS, + pedantic, + "using a item which is prefixed with an underscore" +} + +declare_clippy_lint! { + /// ### What it does /// Checks for the use of short circuit boolean conditions as /// a /// statement. @@ -104,6 +144,7 @@ declare_clippy_lint! { declare_lint_pass!(LintPass => [ TOPLEVEL_REF_ARG, USED_UNDERSCORE_BINDING, + USED_UNDERSCORE_ITEMS, SHORT_CIRCUIT_STATEMENT, ]); @@ -205,51 +246,104 @@ impl<'tcx> LateLintPass<'tcx> for LintPass { { return; } - let (definition_hir_id, ident) = match expr.kind { - ExprKind::Path(ref qpath) => { - if let QPath::Resolved(None, path) = qpath - && let Res::Local(id) = path.res - && is_used(cx, expr) - { - (id, last_path_segment(qpath).ident) - } else { - return; - } - }, - ExprKind::Field(recv, ident) => { - if let Some(adt_def) = cx.typeck_results().expr_ty_adjusted(recv).ty_adt_def() - && let Some(field) = adt_def.all_fields().find(|field| field.name == ident.name) - && let Some(local_did) = field.did.as_local() - && !cx.tcx.type_of(field.did).skip_binder().is_phantom_data() - { - (cx.tcx.local_def_id_to_hir_id(local_did), ident) - } else { - return; - } + + used_underscore_binding(cx, expr); + used_underscore_items(cx, expr); + } +} + +fn used_underscore_items<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (def_id, ident) = match expr.kind { + ExprKind::Call(func, ..) => { + if let ExprKind::Path(QPath::Resolved(.., path)) = func.kind + && let Some(last_segment) = path.segments.last() + && let Res::Def(_, def_id) = last_segment.res + { + (def_id, last_segment.ident) + } else { + return; + } + }, + ExprKind::MethodCall(path, ..) => { + if let Some(def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) { + (def_id, path.ident) + } else { + return; + } + }, + ExprKind::Struct(QPath::Resolved(_, path), ..) => { + if let Some(last_segment) = path.segments.last() + && let Res::Def(_, def_id) = last_segment.res + { + (def_id, last_segment.ident) + } else { + return; + } + }, + _ => return, + }; + + let name = ident.name.as_str(); + let definition_span = cx.tcx.def_span(def_id); + if name.starts_with('_') + && !name.starts_with("__") + && !definition_span.from_expansion() + && def_id.krate == LOCAL_CRATE + { + span_lint_and_then( + cx, + USED_UNDERSCORE_ITEMS, + expr.span, + "used underscore-prefixed item".to_string(), + |diag| { + diag.span_note(definition_span, "item is defined here".to_string()); }, - _ => return, - }; + ); + } +} - let name = ident.name.as_str(); - if name.starts_with('_') - && !name.starts_with("__") - && let definition_span = cx.tcx.hir().span(definition_hir_id) - && !definition_span.from_expansion() - && !fulfill_or_allowed(cx, USED_UNDERSCORE_BINDING, [expr.hir_id, definition_hir_id]) - { - span_lint_and_then( - cx, - USED_UNDERSCORE_BINDING, - expr.span, - format!( - "used binding `{name}` which is prefixed with an underscore. A leading \ - underscore signals that a binding will not be used" - ), - |diag| { - diag.span_note(definition_span, format!("`{name}` is defined here")); - }, - ); - } +fn used_underscore_binding<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (definition_hir_id, ident) = match expr.kind { + ExprKind::Path(ref qpath) => { + if let QPath::Resolved(None, path) = qpath + && let Res::Local(id) = path.res + && is_used(cx, expr) + { + (id, last_path_segment(qpath).ident) + } else { + return; + } + }, + ExprKind::Field(recv, ident) => { + if let Some(adt_def) = cx.typeck_results().expr_ty_adjusted(recv).ty_adt_def() + && let Some(field) = adt_def.all_fields().find(|field| field.name == ident.name) + && let Some(local_did) = field.did.as_local() + && !cx.tcx.type_of(field.did).skip_binder().is_phantom_data() + { + (cx.tcx.local_def_id_to_hir_id(local_did), ident) + } else { + return; + } + }, + _ => return, + }; + + let name = ident.name.as_str(); + if name.starts_with('_') + && !name.starts_with("__") + && let definition_span = cx.tcx.hir().span(definition_hir_id) + && !definition_span.from_expansion() + && !fulfill_or_allowed(cx, USED_UNDERSCORE_BINDING, [expr.hir_id, definition_hir_id]) + { + span_lint_and_then( + cx, + USED_UNDERSCORE_BINDING, + expr.span, + "used underscore-prefixed binding".to_string(), + |diag| { + diag.span_note(definition_span, "binding is defined here".to_string()); + }, + ); } } diff --git a/clippy_lints/src/mixed_read_write_in_expression.rs b/clippy_lints/src/mixed_read_write_in_expression.rs index 0b3769ecb7c..0b7d97c32c5 100644 --- a/clippy_lints/src/mixed_read_write_in_expression.rs +++ b/clippy_lints/src/mixed_read_write_in_expression.rs @@ -1,4 +1,5 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; +use clippy_utils::macros::root_macro_call_first_node; use clippy_utils::{get_parent_expr, path_to_local, path_to_local_id}; use rustc_hir::intravisit::{walk_expr, Visitor}; use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, LetStmt, Node, Stmt, StmtKind}; @@ -134,6 +135,11 @@ impl<'a, 'tcx> DivergenceVisitor<'a, 'tcx> { } fn report_diverging_sub_expr(&mut self, e: &Expr<'_>) { + if let Some(macro_call) = root_macro_call_first_node(self.cx, e) { + if self.cx.tcx.item_name(macro_call.def_id).as_str() == "todo" { + return; + } + } span_lint(self.cx, DIVERGING_SUB_EXPRESSION, e.span, "sub-expression diverges"); } } diff --git a/clippy_lints/src/mutable_debug_assertion.rs b/clippy_lints/src/mutable_debug_assertion.rs index e92ba93942e..563ce2d82ea 100644 --- a/clippy_lints/src/mutable_debug_assertion.rs +++ b/clippy_lints/src/mutable_debug_assertion.rs @@ -96,10 +96,6 @@ impl<'a, 'tcx> Visitor<'tcx> for MutArgVisitor<'a, 'tcx> { 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 diff --git a/clippy_lints/src/needless_maybe_sized.rs b/clippy_lints/src/needless_maybe_sized.rs index 62e41088f37..bb44ff37b20 100644 --- a/clippy_lints/src/needless_maybe_sized.rs +++ b/clippy_lints/src/needless_maybe_sized.rs @@ -26,7 +26,7 @@ declare_clippy_lint! { /// /// // or choose alternative bounds for `T` so that it can be unsized /// ``` - #[clippy::version = "1.79.0"] + #[clippy::version = "1.81.0"] pub NEEDLESS_MAYBE_SIZED, suspicious, "a `?Sized` bound that is unusable due to a `Sized` requirement" diff --git a/clippy_lints/src/needless_pass_by_value.rs b/clippy_lints/src/needless_pass_by_value.rs index addb4b1aee8..887070bcf9a 100644 --- a/clippy_lints/src/needless_pass_by_value.rs +++ b/clippy_lints/src/needless_pass_by_value.rs @@ -129,7 +129,7 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { }) .collect::<Vec<_>>(); - // Collect moved variables and spans which will need dereferencings from the + // Collect moved variables and spans which will need dereferencing from the // function body. let MovedVariablesCtxt { moved_vars } = { let mut ctx = MovedVariablesCtxt::default(); @@ -148,12 +148,13 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { return; } - // Ignore `self`s. - if idx == 0 { - if let PatKind::Binding(.., ident, _) = arg.pat.kind { - if ident.name == kw::SelfLower { - continue; - } + // Ignore `self`s and params whose variable name starts with an underscore + if let PatKind::Binding(.., ident, _) = arg.pat.kind { + if idx == 0 && ident.name == kw::SelfLower { + continue; + } + if ident.name.as_str().starts_with('_') { + continue; } } @@ -207,7 +208,7 @@ impl<'tcx> LateLintPass<'tcx> for NeedlessPassByValue { ) .is_ok() { - diag.span_help(span, "consider marking this type as `Copy`"); + diag.span_help(span, "or consider marking this type as `Copy`"); } } } diff --git a/clippy_lints/src/non_zero_suggestions.rs b/clippy_lints/src/non_zero_suggestions.rs new file mode 100644 index 00000000000..90a9f2e994b --- /dev/null +++ b/clippy_lints/src/non_zero_suggestions.rs @@ -0,0 +1,143 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet; +use rustc_ast::ast::BinOpKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{self, Ty}; +use rustc_session::declare_lint_pass; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Checks for conversions from `NonZero` types to regular integer types, + /// and suggests using `NonZero` types for the target as well. + /// + /// ### Why is this bad? + /// Converting from `NonZero` types to regular integer types and then back to `NonZero` + /// types is less efficient and loses the type-safety guarantees provided by `NonZero` types. + /// Using `NonZero` types consistently can lead to more optimized code and prevent + /// certain classes of errors related to zero values. + /// + /// ### Example + /// ```no_run + /// use std::num::{NonZeroU32, NonZeroU64}; + /// + /// fn example(x: u64, y: NonZeroU32) { + /// // Bad: Converting NonZeroU32 to u64 unnecessarily + /// let r1 = x / u64::from(y.get()); + /// let r2 = x % u64::from(y.get()); + /// } + /// ``` + /// Use instead: + /// ```no_run + /// use std::num::{NonZeroU32, NonZeroU64}; + /// + /// fn example(x: u64, y: NonZeroU32) { + /// // Good: Preserving the NonZero property + /// let r1 = x / NonZeroU64::from(y); + /// let r2 = x % NonZeroU64::from(y); + /// } + /// ``` + #[clippy::version = "1.81.0"] + pub NON_ZERO_SUGGESTIONS, + restriction, + "suggests using `NonZero#` from `u#` or `i#` for more efficient and type-safe conversions" +} + +declare_lint_pass!(NonZeroSuggestions => [NON_ZERO_SUGGESTIONS]); + +impl<'tcx> LateLintPass<'tcx> for NonZeroSuggestions { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if let ExprKind::Binary(op, _, rhs) = expr.kind + && matches!(op.node, BinOpKind::Div | BinOpKind::Rem) + { + check_non_zero_conversion(cx, rhs, Applicability::MachineApplicable); + } else { + // Check if the parent expression is a binary operation + let parent_is_binary = cx.tcx.hir().parent_iter(expr.hir_id).any(|(_, node)| { + matches!(node, rustc_hir::Node::Expr(parent_expr) if matches!(parent_expr.kind, ExprKind::Binary(..))) + }); + + if !parent_is_binary { + check_non_zero_conversion(cx, expr, Applicability::MaybeIncorrect); + } + } + } +} + +fn check_non_zero_conversion(cx: &LateContext<'_>, expr: &Expr<'_>, applicability: Applicability) { + // Check if the expression is a function call with one argument + if let ExprKind::Call(func, [arg]) = expr.kind + && let ExprKind::Path(qpath) = &func.kind + && let Some(def_id) = cx.qpath_res(qpath, func.hir_id).opt_def_id() + && let ExprKind::MethodCall(rcv_path, receiver, _, _) = &arg.kind + && rcv_path.ident.name.as_str() == "get" + { + let fn_name = cx.tcx.item_name(def_id); + let target_ty = cx.typeck_results().expr_ty(expr); + let receiver_ty = cx.typeck_results().expr_ty(receiver); + + // Check if the receiver type is a NonZero type + if let ty::Adt(adt_def, _) = receiver_ty.kind() + && adt_def.is_struct() + && cx.tcx.get_diagnostic_name(adt_def.did()) == Some(sym::NonZero) + { + if let Some(target_non_zero_type) = get_target_non_zero_type(target_ty) { + let arg_snippet = get_arg_snippet(cx, arg, rcv_path); + suggest_non_zero_conversion(cx, expr, fn_name, target_non_zero_type, &arg_snippet, applicability); + } + } + } +} + +fn get_arg_snippet(cx: &LateContext<'_>, arg: &Expr<'_>, rcv_path: &rustc_hir::PathSegment<'_>) -> String { + let arg_snippet = snippet(cx, arg.span, ".."); + if let Some(index) = arg_snippet.rfind(&format!(".{}", rcv_path.ident.name)) { + arg_snippet[..index].trim().to_string() + } else { + arg_snippet.to_string() + } +} + +fn suggest_non_zero_conversion( + cx: &LateContext<'_>, + expr: &Expr<'_>, + fn_name: rustc_span::Symbol, + target_non_zero_type: &str, + arg_snippet: &str, + applicability: Applicability, +) { + let suggestion = format!("{target_non_zero_type}::{fn_name}({arg_snippet})"); + span_lint_and_sugg( + cx, + NON_ZERO_SUGGESTIONS, + expr.span, + format!("consider using `{target_non_zero_type}::{fn_name}()` for more efficient and type-safe conversion"), + "replace with", + suggestion, + applicability, + ); +} + +fn get_target_non_zero_type(ty: Ty<'_>) -> Option<&'static str> { + match ty.kind() { + ty::Uint(uint_ty) => Some(match uint_ty { + ty::UintTy::U8 => "NonZeroU8", + ty::UintTy::U16 => "NonZeroU16", + ty::UintTy::U32 => "NonZeroU32", + ty::UintTy::U64 => "NonZeroU64", + ty::UintTy::U128 => "NonZeroU128", + ty::UintTy::Usize => "NonZeroUsize", + }), + ty::Int(int_ty) => Some(match int_ty { + ty::IntTy::I8 => "NonZeroI8", + ty::IntTy::I16 => "NonZeroI16", + ty::IntTy::I32 => "NonZeroI32", + ty::IntTy::I64 => "NonZeroI64", + ty::IntTy::I128 => "NonZeroI128", + ty::IntTy::Isize => "NonZeroIsize", + }), + _ => None, + } +} diff --git a/clippy_lints/src/panic_in_result_fn.rs b/clippy_lints/src/panic_in_result_fn.rs index 381975199d2..fa15d5e4f9f 100644 --- a/clippy_lints/src/panic_in_result_fn.rs +++ b/clippy_lints/src/panic_in_result_fn.rs @@ -1,8 +1,8 @@ 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::{for_each_expr, Descend}; +use clippy_utils::{is_inside_always_const_context, return_ty}; use core::ops::ControlFlow; use rustc_hir as hir; use rustc_hir::intravisit::FnKind; @@ -68,10 +68,12 @@ fn lint_impl_body<'tcx>(cx: &LateContext<'tcx>, impl_span: Span, body: &'tcx hir let Some(macro_call) = root_macro_call_first_node(cx, e) else { return ControlFlow::Continue(Descend::Yes); }; - if matches!( - cx.tcx.item_name(macro_call.def_id).as_str(), - "panic" | "assert" | "assert_eq" | "assert_ne" - ) { + if !is_inside_always_const_context(cx.tcx, e.hir_id) + && matches!( + cx.tcx.item_name(macro_call.def_id).as_str(), + "panic" | "assert" | "assert_eq" | "assert_ne" + ) + { panics.push(macro_call.span); ControlFlow::Continue(Descend::No) } else { diff --git a/clippy_lints/src/panic_unimplemented.rs b/clippy_lints/src/panic_unimplemented.rs index 4eefd0065f6..fa5b02a5a41 100644 --- a/clippy_lints/src/panic_unimplemented.rs +++ b/clippy_lints/src/panic_unimplemented.rs @@ -1,8 +1,9 @@ use clippy_config::Conf; use clippy_utils::diagnostics::span_lint; -use clippy_utils::is_in_test; use clippy_utils::macros::{is_panic, root_macro_call_first_node}; -use rustc_hir::Expr; +use clippy_utils::{is_in_test, match_def_path, paths}; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{Expr, ExprKind, QPath}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::impl_lint_pass; @@ -95,10 +96,49 @@ impl_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 let Some(macro_call) = root_macro_call_first_node(cx, expr) { + if is_panic(cx, macro_call.def_id) { + if cx.tcx.hir().is_inside_const_context(expr.hir_id) + || self.allow_panic_in_tests && is_in_test(cx.tcx, 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"); + }, + _ => {}, + } + } else if let ExprKind::Call(func, [_]) = expr.kind + && let ExprKind::Path(QPath::Resolved(None, expr_path)) = func.kind + && let Res::Def(DefKind::Fn, def_id) = expr_path.res + && match_def_path(cx, def_id, &paths::PANIC_ANY) + { if cx.tcx.hir().is_inside_const_context(expr.hir_id) || self.allow_panic_in_tests && is_in_test(cx.tcx, expr.hir_id) { @@ -108,32 +148,10 @@ impl<'tcx> LateLintPass<'tcx> for PanicUnimplemented { span_lint( cx, PANIC, - macro_call.span, - "`panic` should not be present in production code", + expr.span, + "`panic_any` 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/clippy_lints/src/pointers_in_nomem_asm_block.rs b/clippy_lints/src/pointers_in_nomem_asm_block.rs new file mode 100644 index 00000000000..385f634a150 --- /dev/null +++ b/clippy_lints/src/pointers_in_nomem_asm_block.rs @@ -0,0 +1,88 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use rustc_ast::InlineAsmOptions; +use rustc_hir::{Expr, ExprKind, InlineAsm, InlineAsmOperand}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::declare_lint_pass; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks if any pointer is being passed to an asm! block with `nomem` option. + /// + /// ### Why is this bad? + /// `nomem` forbids any reads or writes to memory and passing a pointer suggests + /// that either of those will happen. + /// + /// ### Example + /// ```no_run + /// fn f(p: *mut u32) { + /// unsafe { core::arch::asm!("mov [{p}], 42", p = in(reg) p, options(nomem, nostack)); } + /// } + /// ``` + /// Use instead: + /// ```no_run + /// fn f(p: *mut u32) { + /// unsafe { core::arch::asm!("mov [{p}], 42", p = in(reg) p, options(nostack)); } + /// } + /// ``` + #[clippy::version = "1.81.0"] + pub POINTERS_IN_NOMEM_ASM_BLOCK, + suspicious, + "pointers in nomem asm block" +} + +declare_lint_pass!(PointersInNomemAsmBlock => [POINTERS_IN_NOMEM_ASM_BLOCK]); + +impl<'tcx> LateLintPass<'tcx> for PointersInNomemAsmBlock { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { + if let ExprKind::InlineAsm(asm) = &expr.kind { + check_asm(cx, asm); + } + } +} + +fn check_asm(cx: &LateContext<'_>, asm: &InlineAsm<'_>) { + if !asm.options.contains(InlineAsmOptions::NOMEM) { + return; + } + + let spans = asm + .operands + .iter() + .filter(|(op, _span)| has_in_operand_pointer(cx, op)) + .map(|(_op, span)| *span) + .collect::<Vec<Span>>(); + + if spans.is_empty() { + return; + } + + span_lint_and_then( + cx, + POINTERS_IN_NOMEM_ASM_BLOCK, + spans, + "passing pointers to nomem asm block", + additional_notes, + ); +} + +fn has_in_operand_pointer(cx: &LateContext<'_>, asm_op: &InlineAsmOperand<'_>) -> bool { + let asm_in_expr = match asm_op { + InlineAsmOperand::SymStatic { .. } + | InlineAsmOperand::Out { .. } + | InlineAsmOperand::Const { .. } + | InlineAsmOperand::SymFn { .. } + | InlineAsmOperand::Label { .. } => return false, + InlineAsmOperand::SplitInOut { in_expr, .. } => in_expr, + InlineAsmOperand::In { expr, .. } | InlineAsmOperand::InOut { expr, .. } => expr, + }; + + // This checks for raw ptrs, refs and function pointers - the last one + // also technically counts as reading memory. + cx.typeck_results().expr_ty(asm_in_expr).is_any_ptr() +} + +fn additional_notes(diag: &mut rustc_errors::Diag<'_, ()>) { + diag.note("`nomem` means that no memory write or read happens inside the asm! block"); + diag.note("if this is intentional and no pointers are read or written to, consider allowing the lint"); +} diff --git a/clippy_lints/src/ptr.rs b/clippy_lints/src/ptr.rs index 125f694996c..cd8e921347c 100644 --- a/clippy_lints/src/ptr.rs +++ b/clippy_lints/src/ptr.rs @@ -1,11 +1,9 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then, span_lint_hir_and_then}; use clippy_utils::source::SpanRangeExt; -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}; use hir::LifetimeName; use rustc_errors::{Applicability, MultiSpan}; -use rustc_hir::def_id::DefId; use rustc_hir::hir_id::{HirId, HirIdMap}; use rustc_hir::intravisit::{walk_expr, Visitor}; use rustc_hir::{ @@ -34,7 +32,7 @@ declare_clippy_lint! { /// with the appropriate `.to_owned()`/`to_string()` calls. /// /// ### Why is this bad? - /// Requiring the argument to be of the specific size + /// Requiring the argument to be of the specific type /// 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. /// @@ -323,7 +321,6 @@ struct PtrArg<'tcx> { idx: usize, emission_id: HirId, span: Span, - ty_did: DefId, ty_name: Symbol, method_renames: &'static [(&'static str, &'static str)], ref_prefix: RefPrefix, @@ -411,7 +408,6 @@ impl<'tcx> DerefTy<'tcx> { } } -#[expect(clippy::too_many_lines)] fn check_fn_args<'cx, 'tcx: 'cx>( cx: &'cx LateContext<'tcx>, fn_sig: ty::FnSig<'tcx>, @@ -514,7 +510,6 @@ fn check_fn_args<'cx, 'tcx: 'cx>( idx: i, emission_id, span: hir_ty.span, - ty_did: adt.did(), ty_name: name.ident.name, method_renames, ref_prefix: RefPrefix { lt: *lt, mutability }, @@ -610,65 +605,50 @@ fn check_ptr_arg_usage<'tcx>(cx: &LateContext<'tcx>, body: &Body<'tcx>, args: &[ 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::Dynamic(preds, _, _) => !matches_preds(self.cx, args.deref_ty.ty(self.cx), preds), - 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, self_arg, expr_args, _) => { - let i = iter::once(self_arg) - .chain(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; - } + Some((Node::Expr(use_expr), child_id)) => { + if let ExprKind::Index(e, ..) = use_expr.kind + && e.hir_id == child_id + { + // Indexing works with both owned and its dereferenced type + return; + } + + if let ExprKind::MethodCall(name, receiver, ..) = use_expr.kind + && receiver.hir_id == child_id + { + let name = name.ident.as_str(); + + // Check if the method can be renamed. + if let Some((_, replacement)) = args.method_renames.iter().find(|&&(x, _)| x == name) { + result.replacements.push(PtrArgReplacement { + expr_span: use_expr.span, + self_span: receiver.span, + replacement, + }); + return; } - let Some(id) = self.cx.typeck_results().type_dependent_def_id(e.hir_id) else { - set_skip_flag(); + // Some methods exist on both `[T]` and `Vec<T>`, such as `len`, where the receiver type + // doesn't coerce to a slice and our adjusted type check below isn't enough, + // but it would still be valid to call with a slice + if is_allowed_vec_method(self.cx, use_expr) { return; - }; - - match *self.cx.tcx.fn_sig(id).instantiate_identity().skip_binder().inputs()[i] - .peel_refs() - .kind() - { - ty::Dynamic(preds, _, _) if !matches_preds(self.cx, args.deref_ty.ty(self.cx), preds) => { - set_skip_flag(); - }, - 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 && !is_allowed_vec_method(self.cx, e) => { - set_skip_flag(); - }, - _ => (), } - }, - // Indexing is fine for currently supported types. - ExprKind::Index(e, _, _) if e.hir_id == child_id => (), - _ => set_skip_flag(), + } + + let deref_ty = args.deref_ty.ty(self.cx); + let adjusted_ty = self.cx.typeck_results().expr_ty_adjusted(e).peel_refs(); + if adjusted_ty == deref_ty { + return; + } + + if let ty::Dynamic(preds, ..) = adjusted_ty.kind() + && matches_preds(self.cx, deref_ty, preds) + { + return; + } + + set_skip_flag(); }, _ => set_skip_flag(), } diff --git a/clippy_lints/src/returns.rs b/clippy_lints/src/returns.rs index 5ce091b7a7a..1e709649695 100644 --- a/clippy_lints/src/returns.rs +++ b/clippy_lints/src/returns.rs @@ -1,7 +1,7 @@ use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then}; use clippy_utils::source::{snippet_with_context, SpanRangeExt}; use clippy_utils::sugg::has_enclosing_paren; -use clippy_utils::visitors::{for_each_expr, Descend}; +use clippy_utils::visitors::{for_each_expr, for_each_unconsumed_temporary, Descend}; use clippy_utils::{ binary_expr_needs_parentheses, fn_def_id, is_from_proc_macro, is_inside_let_else, is_res_lang_ctor, path_res, path_to_local_id, span_contains_cfg, span_find_starting_semi, @@ -389,10 +389,24 @@ fn check_final_expr<'tcx>( } }; - let borrows = inner.map_or(false, |inner| last_statement_borrows(cx, inner)); - if borrows { - return; + if let Some(inner) = inner { + if for_each_unconsumed_temporary(cx, inner, |temporary_ty| { + if temporary_ty.has_significant_drop(cx.tcx, cx.param_env) + && temporary_ty + .walk() + .any(|arg| matches!(arg.unpack(), GenericArgKind::Lifetime(re) if !re.is_static())) + { + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }) + .is_break() + { + return; + } } + if ret_span.from_expansion() { return; } diff --git a/clippy_lints/src/set_contains_or_insert.rs b/clippy_lints/src/set_contains_or_insert.rs index e6fe7649397..bc2a71c42b9 100644 --- a/clippy_lints/src/set_contains_or_insert.rs +++ b/clippy_lints/src/set_contains_or_insert.rs @@ -42,7 +42,7 @@ declare_clippy_lint! { /// println!("inserted {value:?}"); /// } /// ``` - #[clippy::version = "1.80.0"] + #[clippy::version = "1.81.0"] pub SET_CONTAINS_OR_INSERT, nursery, "call to `<set>::contains` followed by `<set>::insert`" diff --git a/clippy_lints/src/string_patterns.rs b/clippy_lints/src/string_patterns.rs index 7e211d64da1..8af50ca87d6 100644 --- a/clippy_lints/src/string_patterns.rs +++ b/clippy_lints/src/string_patterns.rs @@ -33,7 +33,7 @@ declare_clippy_lint! { /// ```no_run /// "Hello World!".trim_end_matches(['.', ',', '!', '?']); /// ``` - #[clippy::version = "1.80.0"] + #[clippy::version = "1.81.0"] pub MANUAL_PATTERN_CHAR_COMPARISON, style, "manual char comparison in string patterns" diff --git a/clippy_lints/src/transmute/mod.rs b/clippy_lints/src/transmute/mod.rs index da7906b1183..ced53a808f9 100644 --- a/clippy_lints/src/transmute/mod.rs +++ b/clippy_lints/src/transmute/mod.rs @@ -527,24 +527,44 @@ declare_clippy_lint! { /// Checks if transmute calls have all generics specified. /// /// ### Why is this bad? - /// If not set, some unexpected output type could be retrieved instead of the expected one, - /// potentially leading to invalid code. + /// If not, one or more unexpected types could be used during `transmute()`, potentially leading + /// to Undefined Behavior or other problems. /// - /// This is particularly dangerous in case a seemingly innocent/unrelated change can cause type - /// inference to start inferring a different type. E.g. the transmute is the tail expression of - /// an `if` branch, and a different branches type changes, causing the transmute to silently - /// have a different type, instead of a proper error. + /// This is particularly dangerous in case a seemingly innocent/unrelated change causes type + /// inference to result in a different type. For example, if `transmute()` is the tail + /// expression of an `if`-branch, and the `else`-branch type changes, the compiler may silently + /// infer a different type to be returned by `transmute()`. That is because the compiler is + /// free to change the inference of a type as long as that inference is technically correct, + /// regardless of the programmer's unknown expectation. + /// + /// Both type-parameters, the input- and the output-type, to any `transmute()` should + /// be given explicitly: Setting the input-type explicitly avoids confusion about what the + /// argument's type actually is. Setting the output-type explicitly avoids type-inference + /// to infer a technically correct yet unexpected type. /// /// ### Example /// ```no_run /// # unsafe { + /// // Avoid "naked" calls to `transmute()`! /// let x: i32 = std::mem::transmute([1u16, 2u16]); + /// + /// // `first_answers` is intended to transmute a slice of bool to a slice of u8. + /// // But the programmer forgot to index the first element of the outer slice, + /// // so we are actually transmuting from "pointers to slices" instead of + /// // transmuting from "a slice of bool", causing a nonsensical result. + /// let the_answers: &[&[bool]] = &[&[true, false, true]]; + /// let first_answers: &[u8] = std::mem::transmute(the_answers); /// # } /// ``` /// Use instead: /// ```no_run /// # unsafe { /// let x = std::mem::transmute::<[u16; 2], i32>([1u16, 2u16]); + /// + /// // The explicit type parameters on `transmute()` makes the intention clear, + /// // and cause a type-error if the actual types don't match our expectation. + /// let the_answers: &[&[bool]] = &[&[true, false, true]]; + /// let first_answers: &[u8] = std::mem::transmute::<&[bool], &[u8]>(the_answers[0]); /// # } /// ``` #[clippy::version = "1.79.0"] diff --git a/clippy_lints/src/types/mod.rs b/clippy_lints/src/types/mod.rs index 3a14927802b..120d5ffbbd3 100644 --- a/clippy_lints/src/types/mod.rs +++ b/clippy_lints/src/types/mod.rs @@ -261,8 +261,59 @@ declare_clippy_lint! { /// ### Example /// ```no_run /// # use std::rc::Rc; - /// struct Foo { - /// inner: Rc<Vec<Vec<Box<(u32, u32, u32, u32)>>>>, + /// struct PointMatrixContainer { + /// matrix: Rc<Vec<Vec<Box<(u32, u32, u32, u32)>>>>, + /// } + /// + /// fn main() { + /// let point_matrix: Vec<Vec<Box<(u32, u32, u32, u32)>>> = vec![ + /// vec![ + /// Box::new((1, 2, 3, 4)), + /// Box::new((5, 6, 7, 8)), + /// ], + /// vec![ + /// Box::new((9, 10, 11, 12)), + /// ], + /// ]; + /// + /// let shared_point_matrix: Rc<Vec<Vec<Box<(u32, u32, u32, u32)>>>> = Rc::new(point_matrix); + /// + /// let container = PointMatrixContainer { + /// matrix: shared_point_matrix, + /// }; + /// + /// // ... + /// } + /// ``` + /// Use instead: + /// ### Example + /// ```no_run + /// # use std::rc::Rc; + /// type PointMatrix = Vec<Vec<Box<(u32, u32, u32, u32)>>>; + /// type SharedPointMatrix = Rc<PointMatrix>; + /// + /// struct PointMatrixContainer { + /// matrix: SharedPointMatrix, + /// } + /// + /// fn main() { + /// let point_matrix: PointMatrix = vec![ + /// vec![ + /// Box::new((1, 2, 3, 4)), + /// Box::new((5, 6, 7, 8)), + /// ], + /// vec![ + /// Box::new((9, 10, 11, 12)), + /// ], + /// ]; + /// + /// let shared_point_matrix: SharedPointMatrix = Rc::new(point_matrix); + /// + /// let container = PointMatrixContainer { + /// matrix: shared_point_matrix, + /// }; + /// + /// // ... /// } /// ``` #[clippy::version = "pre 1.29.0"] diff --git a/clippy_lints/src/unused_trait_names.rs b/clippy_lints/src/unused_trait_names.rs new file mode 100644 index 00000000000..c1cf58dcfce --- /dev/null +++ b/clippy_lints/src/unused_trait_names.rs @@ -0,0 +1,94 @@ +use clippy_config::msrvs::{self, Msrv}; +use clippy_config::Conf; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::is_from_proc_macro; +use clippy_utils::source::snippet_opt; +use rustc_errors::Applicability; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::{Item, ItemKind, UseKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext as _}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::Visibility; +use rustc_session::impl_lint_pass; +use rustc_span::symbol::kw; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `use Trait` where the Trait is only used for its methods and not referenced by a path directly. + /// + /// ### Why is this bad? + /// Traits imported that aren't used directly can be imported anonymously with `use Trait as _`. + /// It is more explicit, avoids polluting the current scope with unused names and can be useful to show which imports are required for traits. + /// + /// ### Example + /// ```no_run + /// use std::fmt::Write; + /// + /// fn main() { + /// let mut s = String::new(); + /// let _ = write!(s, "hello, world!"); + /// println!("{s}"); + /// } + /// ``` + /// Use instead: + /// ```no_run + /// use std::fmt::Write as _; + /// + /// fn main() { + /// let mut s = String::new(); + /// let _ = write!(s, "hello, world!"); + /// println!("{s}"); + /// } + /// ``` + #[clippy::version = "1.83.0"] + pub UNUSED_TRAIT_NAMES, + restriction, + "use items that import a trait but only use it anonymously" +} + +pub struct UnusedTraitNames { + msrv: Msrv, +} + +impl UnusedTraitNames { + pub fn new(conf: &'static Conf) -> Self { + Self { + msrv: conf.msrv.clone(), + } + } +} + +impl_lint_pass!(UnusedTraitNames => [UNUSED_TRAIT_NAMES]); + +impl<'tcx> LateLintPass<'tcx> for UnusedTraitNames { + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { + if self.msrv.meets(msrvs::UNDERSCORE_IMPORTS) + && !in_external_macro(cx.sess(), item.span) + && let ItemKind::Use(path, UseKind::Single) = item.kind + // Ignore imports that already use Underscore + && item.ident.name != kw::Underscore + // Only check traits + && let Some(Res::Def(DefKind::Trait, _)) = path.res.first() + && cx.tcx.maybe_unused_trait_imports(()).contains(&item.owner_id.def_id) + // Only check this import if it is visible to its module only (no pub, pub(crate), ...) + && let module = cx.tcx.parent_module_from_def_id(item.owner_id.def_id) + && cx.tcx.visibility(item.owner_id.def_id) == Visibility::Restricted(module.to_def_id()) + && let Some(last_segment) = path.segments.last() + && let Some(snip) = snippet_opt(cx, last_segment.ident.span) + && !is_from_proc_macro(cx, &last_segment.ident) + { + let complete_span = last_segment.ident.span.to(item.ident.span); + span_lint_and_sugg( + cx, + UNUSED_TRAIT_NAMES, + complete_span, + "importing trait that is only used anonymously", + "use", + format!("{snip} as _"), + Applicability::MachineApplicable, + ); + } + } + + extract_msrv_attr!(LateContext); +} diff --git a/clippy_lints/src/useless_conversion.rs b/clippy_lints/src/useless_conversion.rs index 2f7d54e73ed..1890b3fdd40 100644 --- a/clippy_lints/src/useless_conversion.rs +++ b/clippy_lints/src/useless_conversion.rs @@ -217,12 +217,12 @@ impl<'tcx> LateLintPass<'tcx> for UselessConversion { }, ExprKind::MethodCall(.., args, _) => { cx.typeck_results().type_dependent_def_id(parent.hir_id).map(|did| { - return ( + ( did, args, cx.typeck_results().node_args(parent.hir_id), MethodOrFunction::Method, - ); + ) }) }, _ => None, diff --git a/clippy_lints/src/zombie_processes.rs b/clippy_lints/src/zombie_processes.rs new file mode 100644 index 00000000000..eda3d7820c1 --- /dev/null +++ b/clippy_lints/src/zombie_processes.rs @@ -0,0 +1,334 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, path_to_local_id, paths}; +use rustc_ast::Mutability; +use rustc_errors::Applicability; +use rustc_hir::intravisit::{walk_block, walk_expr, walk_local, Visitor}; +use rustc_hir::{Expr, ExprKind, HirId, LetStmt, Node, PatKind, Stmt, StmtKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::declare_lint_pass; +use rustc_span::sym; +use std::ops::ControlFlow; +use ControlFlow::{Break, Continue}; + +declare_clippy_lint! { + /// ### What it does + /// Looks for code that spawns a process but never calls `wait()` on the child. + /// + /// ### Why is this bad? + /// As explained in the [standard library documentation](https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning), + /// calling `wait()` is necessary on Unix platforms to properly release all OS resources associated with the process. + /// Not doing so will effectively leak process IDs and/or other limited global resources, + /// which can eventually lead to resource exhaustion, so it's recommended to call `wait()` in long-running applications. + /// Such processes are called "zombie processes". + /// + /// ### Example + /// ```rust + /// use std::process::Command; + /// + /// let _child = Command::new("ls").spawn().expect("failed to execute child"); + /// ``` + /// Use instead: + /// ```rust + /// use std::process::Command; + /// + /// let mut child = Command::new("ls").spawn().expect("failed to execute child"); + /// child.wait().expect("failed to wait on child"); + /// ``` + #[clippy::version = "1.74.0"] + pub ZOMBIE_PROCESSES, + suspicious, + "not waiting on a spawned child process" +} +declare_lint_pass!(ZombieProcesses => [ZOMBIE_PROCESSES]); + +impl<'tcx> LateLintPass<'tcx> for ZombieProcesses { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if let ExprKind::Call(..) | ExprKind::MethodCall(..) = expr.kind + && let Some(child_adt) = cx.typeck_results().expr_ty(expr).ty_adt_def() + && match_def_path(cx, child_adt.did(), &paths::CHILD) + { + match cx.tcx.parent_hir_node(expr.hir_id) { + Node::LetStmt(local) + if let PatKind::Binding(_, local_id, ..) = local.pat.kind + && let Some(enclosing_block) = get_enclosing_block(cx, expr.hir_id) => + { + let mut vis = WaitFinder::WalkUpTo(cx, local_id); + + // If it does have a `wait()` call, we're done. Don't lint. + if let Break(BreakReason::WaitFound) = walk_block(&mut vis, enclosing_block) { + return; + } + + // Don't emit a suggestion since the binding is used later + check(cx, expr, false); + }, + Node::LetStmt(&LetStmt { pat, .. }) if let PatKind::Wild = pat.kind => { + // `let _ = child;`, also dropped immediately without `wait()`ing + check(cx, expr, true); + }, + Node::Stmt(&Stmt { + kind: StmtKind::Semi(_), + .. + }) => { + // Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();` + check(cx, expr, true); + }, + _ => {}, + } + } + } +} + +enum BreakReason { + WaitFound, + EarlyReturn, +} + +/// A visitor responsible for finding a `wait()` call on a local variable. +/// +/// Conditional `wait()` calls are assumed to not call wait: +/// ```ignore +/// let mut c = Command::new("").spawn().unwrap(); +/// if true { +/// c.wait(); +/// } +/// ``` +/// +/// Note that this visitor does NOT explicitly look for `wait()` calls directly, but rather does the +/// inverse -- checking if all uses of the local are either: +/// - a field access (`child.{stderr,stdin,stdout}`) +/// - calling `id` or `kill` +/// - no use at all (e.g. `let _x = child;`) +/// - taking a shared reference (`&`), `wait()` can't go through that +/// +/// None of these are sufficient to prevent zombie processes. +/// Doing it like this means more FNs, but FNs are better than FPs. +/// +/// `return` expressions, conditional or not, short-circuit the visitor because +/// if a `wait()` call hadn't been found at that point, it might never reach one at a later point: +/// ```ignore +/// let mut c = Command::new("").spawn().unwrap(); +/// if true { +/// return; // Break(BreakReason::EarlyReturn) +/// } +/// c.wait(); // this might not be reachable +/// ``` +enum WaitFinder<'a, 'tcx> { + WalkUpTo(&'a LateContext<'tcx>, HirId), + Found(&'a LateContext<'tcx>, HirId), +} + +impl<'a, 'tcx> Visitor<'tcx> for WaitFinder<'a, 'tcx> { + type Result = ControlFlow<BreakReason>; + + fn visit_local(&mut self, l: &'tcx LetStmt<'tcx>) -> Self::Result { + if let Self::WalkUpTo(cx, local_id) = *self + && let PatKind::Binding(_, pat_id, ..) = l.pat.kind + && local_id == pat_id + { + *self = Self::Found(cx, local_id); + } + + walk_local(self, l) + } + + fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) -> Self::Result { + let Self::Found(cx, local_id) = *self else { + return walk_expr(self, ex); + }; + + if path_to_local_id(ex, local_id) { + match cx.tcx.parent_hir_node(ex.hir_id) { + Node::Stmt(Stmt { + kind: StmtKind::Semi(_), + .. + }) => {}, + Node::Expr(expr) if let ExprKind::Field(..) = expr.kind => {}, + Node::Expr(expr) if let ExprKind::AddrOf(_, Mutability::Not, _) = expr.kind => {}, + Node::Expr(expr) + if let Some(fn_did) = fn_def_id(cx, expr) + && match_any_def_paths(cx, fn_did, &[&paths::CHILD_ID, &paths::CHILD_KILL]).is_some() => {}, + + // Conservatively assume that all other kinds of nodes call `.wait()` somehow. + _ => return Break(BreakReason::WaitFound), + } + } else { + match ex.kind { + ExprKind::Ret(..) => return Break(BreakReason::EarlyReturn), + ExprKind::If(cond, then, None) => { + walk_expr(self, cond)?; + + // A `wait()` call in an if expression with no else is not enough: + // + // let c = spawn(); + // if true { + // c.wait(); + // } + // + // This might not call wait(). However, early returns are propagated, + // because they might lead to a later wait() not being called. + if let Break(BreakReason::EarlyReturn) = walk_expr(self, then) { + return Break(BreakReason::EarlyReturn); + } + + return Continue(()); + }, + + ExprKind::If(cond, then, Some(else_)) => { + walk_expr(self, cond)?; + + #[expect(clippy::unnested_or_patterns)] + match (walk_expr(self, then), walk_expr(self, else_)) { + (Continue(()), Continue(())) + + // `wait()` in one branch but nothing in the other does not count + | (Continue(()), Break(BreakReason::WaitFound)) + | (Break(BreakReason::WaitFound), Continue(())) => {}, + + // `wait()` in both branches is ok + (Break(BreakReason::WaitFound), Break(BreakReason::WaitFound)) => { + return Break(BreakReason::WaitFound); + }, + + // Propagate early returns in either branch + (Break(BreakReason::EarlyReturn), _) | (_, Break(BreakReason::EarlyReturn)) => { + return Break(BreakReason::EarlyReturn); + }, + } + + return Continue(()); + }, + _ => {}, + } + } + + walk_expr(self, ex) + } +} + +/// This function has shared logic between the different kinds of nodes that can trigger the lint. +/// +/// In particular, `let <binding> = <expr that spawns child>;` requires some custom additional logic +/// such as checking that the binding is not used in certain ways, which isn't necessary for +/// `let _ = <expr that spawns child>;`. +/// +/// This checks if the program doesn't unconditionally exit after the spawn expression. +fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, emit_suggestion: bool) { + let Some(block) = get_enclosing_block(cx, spawn_expr.hir_id) else { + return; + }; + + let mut vis = ExitPointFinder { + cx, + state: ExitPointState::WalkUpTo(spawn_expr.hir_id), + }; + if let Break(ExitCallFound) = vis.visit_block(block) { + // Visitor found an unconditional `exit()` call, so don't lint. + return; + } + + span_lint_and_then( + cx, + ZOMBIE_PROCESSES, + spawn_expr.span, + "spawned process is never `wait()`ed on", + |diag| { + if emit_suggestion { + diag.span_suggestion( + spawn_expr.span.shrink_to_hi(), + "try", + ".wait()", + Applicability::MaybeIncorrect, + ); + } else { + diag.note("consider calling `.wait()`"); + } + + diag.note("not doing so might leave behind zombie processes") + .note("see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning"); + }, + ); +} + +/// Checks if the given expression exits the process. +fn is_exit_expression(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + fn_def_id(cx, expr).is_some_and(|fn_did| { + cx.tcx.is_diagnostic_item(sym::process_exit, fn_did) || match_def_path(cx, fn_did, &paths::ABORT) + }) +} + +#[derive(Debug)] +enum ExitPointState { + /// Still walking up to the expression that initiated the visitor. + WalkUpTo(HirId), + /// We're inside of a control flow construct (e.g. `if`, `match`, `loop`) + /// Within this, we shouldn't accept any `exit()` calls in here, but we can leave all of these + /// constructs later and still continue looking for an `exit()` call afterwards. Example: + /// ```ignore + /// Command::new("").spawn().unwrap(); + /// + /// if true { // depth=1 + /// if true { // depth=2 + /// match () { // depth=3 + /// () => loop { // depth=4 + /// + /// std::process::exit(); + /// ^^^^^^^^^^^^^^^^^^^^^ conditional exit call, ignored + /// + /// } // depth=3 + /// } // depth=2 + /// } // depth=1 + /// } // depth=0 + /// + /// std::process::exit(); + /// ^^^^^^^^^^^^^^^^^^^^^ this exit call is accepted because we're now unconditionally calling it + /// ``` + /// We can only get into this state from `NoExit`. + InControlFlow { depth: u32 }, + /// No exit call found yet, but looking for one. + NoExit, +} + +fn expr_enters_control_flow(expr: &Expr<'_>) -> bool { + matches!(expr.kind, ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Loop(..)) +} + +struct ExitPointFinder<'a, 'tcx> { + state: ExitPointState, + cx: &'a LateContext<'tcx>, +} + +struct ExitCallFound; + +impl<'a, 'tcx> Visitor<'tcx> for ExitPointFinder<'a, 'tcx> { + type Result = ControlFlow<ExitCallFound>; + + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) -> Self::Result { + match self.state { + ExitPointState::WalkUpTo(id) if expr.hir_id == id => { + self.state = ExitPointState::NoExit; + walk_expr(self, expr) + }, + ExitPointState::NoExit if expr_enters_control_flow(expr) => { + self.state = ExitPointState::InControlFlow { depth: 1 }; + walk_expr(self, expr)?; + if let ExitPointState::InControlFlow { .. } = self.state { + self.state = ExitPointState::NoExit; + } + Continue(()) + }, + ExitPointState::NoExit if is_exit_expression(self.cx, expr) => Break(ExitCallFound), + ExitPointState::InControlFlow { ref mut depth } if expr_enters_control_flow(expr) => { + *depth += 1; + walk_expr(self, expr)?; + match self.state { + ExitPointState::InControlFlow { depth: 1 } => self.state = ExitPointState::NoExit, + ExitPointState::InControlFlow { ref mut depth } => *depth -= 1, + _ => {}, + } + Continue(()) + }, + _ => Continue(()), + } + } +} |
