Skip to content

Commit 82d3214

Browse files
Add new comment_within_doc lint
1 parent 3d7188d commit 82d3214

File tree

4 files changed

+139
-0
lines changed

4 files changed

+139
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5779,6 +5779,7 @@ Released 2018-09-13
57795779
[`collapsible_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_match
57805780
[`collapsible_str_replace`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_str_replace
57815781
[`collection_is_never_read`]: https://rust-lang.github.io/rust-clippy/master/index.html#collection_is_never_read
5782+
[`comment_within_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#comment_within_doc
57825783
[`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
57835784
[`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
57845785
[`confusing_method_to_numeric_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#confusing_method_to_numeric_cast

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
112112
crate::disallowed_names::DISALLOWED_NAMES_INFO,
113113
crate::disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS_INFO,
114114
crate::disallowed_types::DISALLOWED_TYPES_INFO,
115+
crate::doc::COMMENT_WITHIN_DOC_INFO,
115116
crate::doc::DOC_BROKEN_LINK_INFO,
116117
crate::doc::DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS_INFO,
117118
crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
}

clippy_lints/src/doc/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use std::ops::Range;
2525
use url::Url;
2626

2727
mod broken_link;
28+
mod comment_within_doc;
2829
mod doc_comment_double_space_linebreaks;
2930
mod doc_suspicious_footnotes;
3031
mod include_in_doc_without_cfg;
@@ -668,6 +669,31 @@ declare_clippy_lint! {
668669
"looks like a link or footnote ref, but with no definition"
669670
}
670671

672+
declare_clippy_lint! {
673+
/// ### What it does
674+
/// Checks if a code comment is surrounded by doc comments.
675+
///
676+
/// ### Why is this bad?
677+
/// This is likely a typo, making the documentation miss a line.
678+
///
679+
/// ### Example
680+
/// ```no_run
681+
/// //! Doc
682+
/// // oups
683+
/// //! doc
684+
/// ```
685+
/// Use instead:
686+
/// ```no_run
687+
/// //! Doc
688+
/// //! oups
689+
/// //! doc
690+
/// ```
691+
#[clippy::version = "1.90.0"]
692+
pub COMMENT_WITHIN_DOC,
693+
pedantic,
694+
"code comment surrounded by doc comments"
695+
}
696+
671697
pub struct Documentation {
672698
valid_idents: FxHashSet<String>,
673699
check_private_items: bool,
@@ -702,11 +728,13 @@ impl_lint_pass!(Documentation => [
702728
DOC_INCLUDE_WITHOUT_CFG,
703729
DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,
704730
DOC_SUSPICIOUS_FOOTNOTES,
731+
COMMENT_WITHIN_DOC,
705732
]);
706733

707734
impl EarlyLintPass for Documentation {
708735
fn check_attributes(&mut self, cx: &EarlyContext<'_>, attrs: &[rustc_ast::Attribute]) {
709736
include_in_doc_without_cfg::check(cx, attrs);
737+
comment_within_doc::check(cx, attrs);
710738
}
711739
}
712740

0 commit comments

Comments
 (0)