use clippy_utils::diagnostics::span_lint_and_then; use rustc_errors::Applicability; use rustc_hir::Item; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::Span; declare_clippy_lint! { /// ### What it does /// Checks for outer doc comments written with 4 forward slashes (`////`). /// /// ### Why is this bad? /// This is (probably) a typo, and results in it not being a doc comment; just a regular /// comment. /// /// ### Example /// ```rust /// //// My amazing data structure /// pub struct Foo { /// // ... /// } /// ``` /// /// Use instead: /// ```rust /// /// My amazing data structure /// pub struct Foo { /// // ... /// } /// ``` #[clippy::version = "1.72.0"] pub FOUR_FORWARD_SLASHES, suspicious, "comments with 4 forward slashes (`////`) likely intended to be doc comments (`///`)" } declare_lint_pass!(FourForwardSlashes => [FOUR_FORWARD_SLASHES]); impl<'tcx> LateLintPass<'tcx> for FourForwardSlashes { fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { if item.span.from_expansion() { return; } let sm = cx.sess().source_map(); let mut span = cx .tcx .hir() .attrs(item.hir_id()) .iter() .fold(item.span.shrink_to_lo(), |span, attr| span.to(attr.span)); let (Some(file), _, _, end_line, _) = sm.span_to_location_info(span) else { return; }; let mut bad_comments = vec![]; for line in (0..end_line.saturating_sub(1)).rev() { let Some(contents) = file.get_line(line).map(|c| c.trim().to_owned()) else { return; }; // Keep searching until we find the next item if !contents.is_empty() && !contents.starts_with("//") && !contents.starts_with("#[") { break; } if contents.starts_with("////") && !matches!(contents.chars().nth(4), Some('/' | '!')) { let bounds = file.line_bounds(line); let line_span = Span::with_root_ctxt(bounds.start, bounds.end); span = line_span.to(span); bad_comments.push((line_span, contents)); } } if !bad_comments.is_empty() { span_lint_and_then( cx, FOUR_FORWARD_SLASHES, span, "this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't", |diag| { let msg = if bad_comments.len() == 1 { "make this a doc comment by removing one `/`" } else { "turn these into doc comments by removing one `/`" }; diag.multipart_suggestion( msg, bad_comments .into_iter() // It's a little unfortunate but the span includes the `\n` yet the contents // do not, so we must add it back. If some codebase uses `\r\n` instead they // will need normalization but it should be fine .map(|(span, c)| (span, c.replacen("////", "///", 1) + "\n")) .collect(), Applicability::MachineApplicable, ); }, ); } } }