|
| 1 | +use clippy_utils::diagnostics::span_lint_and_then; |
| 2 | +use rustc_errors::Applicability; |
| 3 | +use rustc_hir::Item; |
| 4 | +use rustc_lint::{LateContext, LateLintPass, LintContext}; |
| 5 | +use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| 6 | +use rustc_span::Span; |
| 7 | + |
| 8 | +declare_clippy_lint! { |
| 9 | + /// ### What it does |
| 10 | + /// Checks for outer doc comments written with 4 forward slashes (`////`). |
| 11 | + /// |
| 12 | + /// ### Why is this bad? |
| 13 | + /// This is (probably) a typo, and results in it not being a doc comment; just a regular |
| 14 | + /// comment. |
| 15 | + /// |
| 16 | + /// ### Example |
| 17 | + /// ```rust |
| 18 | + /// //// My amazing data structure |
| 19 | + /// pub struct Foo { |
| 20 | + /// // ... |
| 21 | + /// } |
| 22 | + /// ``` |
| 23 | + /// |
| 24 | + /// Use instead: |
| 25 | + /// ```rust |
| 26 | + /// /// My amazing data structure |
| 27 | + /// pub struct Foo { |
| 28 | + /// // ... |
| 29 | + /// } |
| 30 | + /// ``` |
| 31 | + #[clippy::version = "1.72.0"] |
| 32 | + pub FOUR_FORWARD_SLASHES, |
| 33 | + suspicious, |
| 34 | + "comments with 4 forward slashes (`////`) likely intended to be doc comments (`///`)" |
| 35 | +} |
| 36 | +declare_lint_pass!(FourForwardSlashes => [FOUR_FORWARD_SLASHES]); |
| 37 | + |
| 38 | +impl<'tcx> LateLintPass<'tcx> for FourForwardSlashes { |
| 39 | + fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) { |
| 40 | + if item.span.from_expansion() { |
| 41 | + return; |
| 42 | + } |
| 43 | + let sm = cx.sess().source_map(); |
| 44 | + let mut span = cx |
| 45 | + .tcx |
| 46 | + .hir() |
| 47 | + .attrs(item.hir_id()) |
| 48 | + .iter() |
| 49 | + .fold(item.span.shrink_to_lo(), |span, attr| span.to(attr.span)); |
| 50 | + let (Some(file), _, _, end_line, _) = sm.span_to_location_info(span) else { |
| 51 | + return; |
| 52 | + }; |
| 53 | + let mut bad_comments = vec![]; |
| 54 | + for line in (0..end_line.saturating_sub(1)).rev() { |
| 55 | + let Some(contents) = file.get_line(line).map(|c| c.trim().to_owned()) else { |
| 56 | + return; |
| 57 | + }; |
| 58 | + // Keep searching until we find the next item |
| 59 | + if !contents.is_empty() && !contents.starts_with("//") && !contents.starts_with("#[") { |
| 60 | + break; |
| 61 | + } |
| 62 | + |
| 63 | + if contents.starts_with("////") && !matches!(contents.chars().nth(4), Some('/' | '!')) { |
| 64 | + let bounds = file.line_bounds(line); |
| 65 | + let line_span = Span::with_root_ctxt(bounds.start, bounds.end); |
| 66 | + span = line_span.to(span); |
| 67 | + bad_comments.push((line_span, contents)); |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + if !bad_comments.is_empty() { |
| 72 | + span_lint_and_then( |
| 73 | + cx, |
| 74 | + FOUR_FORWARD_SLASHES, |
| 75 | + span, |
| 76 | + "this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't", |
| 77 | + |diag| { |
| 78 | + let msg = if bad_comments.len() == 1 { |
| 79 | + "make this a doc comment by removing one `/`" |
| 80 | + } else { |
| 81 | + "turn these into doc comments by removing one `/`" |
| 82 | + }; |
| 83 | + |
| 84 | + diag.multipart_suggestion( |
| 85 | + msg, |
| 86 | + bad_comments |
| 87 | + .into_iter() |
| 88 | + // It's a little unfortunate but the span includes the `\n` yet the contents |
| 89 | + // do not, so we must add it back. If some codebase uses `\r\n` instead they |
| 90 | + // will need normalization but it should be fine |
| 91 | + .map(|(span, c)| (span, c.replacen("////", "///", 1) + "\n")) |
| 92 | + .collect(), |
| 93 | + Applicability::MachineApplicable, |
| 94 | + ); |
| 95 | + }, |
| 96 | + ); |
| 97 | + } |
| 98 | + } |
| 99 | +} |
0 commit comments