|
| 1 | +use rustc_ast::token::CommentKind; |
| 2 | +use rustc_ast::{AttrKind, AttrStyle}; |
| 3 | +use rustc_errors::Applicability; |
| 4 | +use rustc_lexer::{TokenKind, tokenize}; |
| 5 | +use rustc_lint::{EarlyContext, LintContext}; |
| 6 | +use rustc_span::source_map::SourceMap; |
| 7 | +use rustc_span::{BytePos, Span}; |
| 8 | + |
| 9 | +use clippy_utils::diagnostics::span_lint_and_then; |
| 10 | + |
| 11 | +use super::COMMENT_WITHIN_DOC; |
| 12 | + |
| 13 | +struct AttrInfo { |
| 14 | + line: usize, |
| 15 | + is_outer: bool, |
| 16 | + span: Span, |
| 17 | + file_span_pos: BytePos, |
| 18 | +} |
| 19 | + |
| 20 | +impl AttrInfo { |
| 21 | + fn new(source_map: &SourceMap, attr: &rustc_ast::Attribute) -> Option<Self> { |
| 22 | + let span_info = source_map.span_to_lines(attr.span).ok()?; |
| 23 | + // If we cannot get the line for any reason, no point in building this item. |
| 24 | + let line = span_info.lines.last()?.line_index; |
| 25 | + Some(Self { |
| 26 | + line, |
| 27 | + is_outer: attr.style == AttrStyle::Outer, |
| 28 | + span: attr.span, |
| 29 | + file_span_pos: span_info.file.start_pos, |
| 30 | + }) |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +// Returns a `Vec` of `TokenKind` if the span only contains comments, otherwise returns `None`. |
| 35 | +fn snippet_contains_only_comments(snippet: &str) -> Option<Vec<TokenKind>> { |
| 36 | + let mut tokens = Vec::new(); |
| 37 | + for token in tokenize(snippet) { |
| 38 | + match token.kind { |
| 39 | + TokenKind::Whitespace => {}, |
| 40 | + TokenKind::BlockComment { .. } | TokenKind::LineComment { .. } => tokens.push(token.kind), |
| 41 | + _ => return None, |
| 42 | + } |
| 43 | + } |
| 44 | + Some(tokens) |
| 45 | +} |
| 46 | + |
| 47 | +pub(super) fn check(cx: &EarlyContext<'_>, attrs: &[rustc_ast::Attribute]) { |
| 48 | + let mut stored_prev_attr = None; |
| 49 | + let source_map = cx.sess().source_map(); |
| 50 | + for attr in attrs |
| 51 | + .iter() |
| 52 | + // We ignore `#[doc = "..."]` and `/** */` attributes. |
| 53 | + .filter(|attr| matches!(attr.kind, AttrKind::DocComment(CommentKind::Line, _))) |
| 54 | + { |
| 55 | + let Some(attr) = AttrInfo::new(source_map, attr) else { |
| 56 | + stored_prev_attr = None; |
| 57 | + continue; |
| 58 | + }; |
| 59 | + let Some(prev_attr) = stored_prev_attr else { |
| 60 | + stored_prev_attr = Some(attr); |
| 61 | + continue; |
| 62 | + }; |
| 63 | + // First we check if they are from the same file and if they are the same kind of doc |
| 64 | + // comments. |
| 65 | + if attr.file_span_pos != prev_attr.file_span_pos || attr.is_outer != prev_attr.is_outer { |
| 66 | + stored_prev_attr = Some(attr); |
| 67 | + continue; |
| 68 | + } |
| 69 | + let Some(nb_lines) = attr.line.checked_sub(prev_attr.line + 1) else { |
| 70 | + continue; |
| 71 | + }; |
| 72 | + // Then we check if they follow each other. |
| 73 | + if nb_lines == 0 || nb_lines > 1 { |
| 74 | + // If there is no line between them or there are more than 1, we skip this check. |
| 75 | + stored_prev_attr = Some(attr); |
| 76 | + continue; |
| 77 | + } |
| 78 | + let span_between = prev_attr.span.between(attr.span); |
| 79 | + // If there is one line between the two doc comments and this line contains a line code |
| 80 | + // comment, then we lint. |
| 81 | + if nb_lines == 1 |
| 82 | + && let Ok(snippet) = source_map.span_to_snippet(span_between) |
| 83 | + && let Some(comments) = snippet_contains_only_comments(&snippet) |
| 84 | + && let &[TokenKind::LineComment { .. }] = comments.as_slice() |
| 85 | + { |
| 86 | + let offset_begin = snippet.len() - snippet.trim_start().len(); |
| 87 | + let offset_end = snippet.len() - snippet.trim_end().len(); |
| 88 | + let span = span_between |
| 89 | + .with_lo(span_between.lo() + BytePos(offset_begin.try_into().unwrap())) |
| 90 | + .with_hi(span_between.hi() - BytePos(offset_end.try_into().unwrap())); |
| 91 | + let comment_kind = if attr.is_outer { '/' } else { '!' }; |
| 92 | + span_lint_and_then( |
| 93 | + cx, |
| 94 | + COMMENT_WITHIN_DOC, |
| 95 | + vec![prev_attr.span, span, attr.span], |
| 96 | + "code comment surrounded by doc comments", |
| 97 | + |diag| { |
| 98 | + diag.span_suggestion( |
| 99 | + span.with_hi(span.lo() + BytePos(2)), |
| 100 | + "did you mean to make it a doc comment?", |
| 101 | + format!("//{comment_kind}"), |
| 102 | + Applicability::MaybeIncorrect, |
| 103 | + ); |
| 104 | + }, |
| 105 | + ); |
| 106 | + } |
| 107 | + stored_prev_attr = Some(attr); |
| 108 | + } |
| 109 | +} |
0 commit comments