|
1 | | -use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet_opt}; |
| 1 | +use clippy_utils::diagnostics::span_lint_and_then; |
2 | 2 | use rustc_errors::Applicability; |
3 | 3 | use rustc_hir::Item; |
4 | 4 | use rustc_lint::{LateContext, LateLintPass, LintContext}; |
5 | 5 | use rustc_session::{declare_lint_pass, declare_tool_lint}; |
6 | | -use rustc_span::{Span, SyntaxContext}; |
| 6 | +use rustc_span::Span; |
7 | 7 |
|
8 | 8 | declare_clippy_lint! { |
9 | 9 | /// ### What it does |
@@ -40,43 +40,60 @@ impl<'tcx> LateLintPass<'tcx> for FourForwardSlashes { |
40 | 40 | if item.span.from_expansion() { |
41 | 41 | return; |
42 | 42 | } |
43 | | - let src = cx.sess().source_map(); |
44 | | - let item_and_attrs_span = cx |
| 43 | + let sm = cx.sess().source_map(); |
| 44 | + let mut span = cx |
45 | 45 | .tcx |
46 | 46 | .hir() |
47 | 47 | .attrs(item.hir_id()) |
48 | 48 | .iter() |
49 | 49 | .fold(item.span.shrink_to_lo(), |span, attr| span.to(attr.span)); |
50 | | - let (Some(file), _, _, end_line, _) = src.span_to_location_info(item_and_attrs_span) else { |
| 50 | + let (Some(file), _, _, end_line, _) = sm.span_to_location_info(span) else { |
51 | 51 | return; |
52 | 52 | }; |
| 53 | + let mut bad_comments = vec![]; |
53 | 54 | for line in (0..end_line.saturating_sub(1)).rev() { |
54 | | - let Some(contents) = file.get_line(line) else { |
55 | | - continue; |
| 55 | + let Some(contents) = file.get_line(line).map(|c| c.trim().to_owned()) else { |
| 56 | + return; |
56 | 57 | }; |
57 | | - let contents = contents.trim(); |
58 | | - if contents.is_empty() { |
| 58 | + // Keep searching until we find the next item |
| 59 | + if !contents.is_empty() && !contents.starts_with("//") && !contents.starts_with("#[") { |
59 | 60 | break; |
60 | 61 | } |
61 | | - if contents.starts_with("////") { |
| 62 | + |
| 63 | + if contents.starts_with("////") && !matches!(contents.chars().nth(4), Some('/' | '!')) { |
62 | 64 | let bounds = file.line_bounds(line); |
63 | | - let span = Span::new(bounds.start, bounds.end, SyntaxContext::root(), None); |
| 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 | + } |
64 | 70 |
|
65 | | - if snippet_opt(cx, span).is_some_and(|s| s.trim().starts_with("////")) { |
66 | | - span_lint_and_sugg( |
67 | | - cx, |
68 | | - FOUR_FORWARD_SLASHES, |
69 | | - span, |
70 | | - "comment with 4 forward slashes (`////`). This looks like a doc comment, but it isn't", |
71 | | - "make this a doc comment by removing one `/`", |
72 | | - // It's a little unfortunate but the span includes the `\n` yet the contents |
73 | | - // do not, so we must add it back. If some codebase uses `\r\n` instead they |
74 | | - // will need normalization but it should be fine |
75 | | - contents.replacen("////", "///", 1) + "\n", |
| 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(), |
76 | 93 | Applicability::MachineApplicable, |
77 | 94 | ); |
78 | | - } |
79 | | - } |
| 95 | + }, |
| 96 | + ); |
80 | 97 | } |
81 | 98 | } |
82 | 99 | } |
0 commit comments