Skip to content

Commit 0a8cb5d

Browse files
committed
Reuse existing markdown parser in doc_paragraphs_missing_punctuation
1 parent 432dad4 commit 0a8cb5d

File tree

2 files changed

+68
-83
lines changed

2 files changed

+68
-83
lines changed
Lines changed: 64 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,31 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
12
use rustc_errors::Applicability;
23
use rustc_lint::LateContext;
3-
use rustc_resolve::rustdoc::main_body_opts;
4-
5-
use rustc_resolve::rustdoc::pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
4+
use rustc_resolve::rustdoc::pulldown_cmark::{Event, Tag, TagEnd};
5+
use rustc_span::Span;
6+
use std::ops::Range;
67

78
use super::{DOC_PARAGRAPHS_MISSING_PUNCTUATION, Fragments};
89

9-
const MSG: &str = "doc paragraphs should end with a terminal punctuation mark";
10-
const PUNCTUATION_SUGGESTION: char = '.';
11-
12-
pub fn check(cx: &LateContext<'_>, doc: &str, fragments: Fragments<'_>) {
13-
for missing_punctuation in is_missing_punctuation(doc) {
14-
match missing_punctuation {
15-
MissingPunctuation::Fixable(offset) => {
16-
// This ignores `#[doc]` attributes, which we do not handle.
17-
if let Some(span) = fragments.span(cx, offset..offset) {
18-
clippy_utils::diagnostics::span_lint_and_sugg(
19-
cx,
20-
DOC_PARAGRAPHS_MISSING_PUNCTUATION,
21-
span,
22-
MSG,
23-
"end the paragraph with some punctuation",
24-
PUNCTUATION_SUGGESTION.to_string(),
25-
Applicability::MaybeIncorrect,
26-
);
27-
}
28-
},
29-
MissingPunctuation::Unfixable(offset) => {
30-
// This ignores `#[doc]` attributes, which we do not handle.
31-
if let Some(span) = fragments.span(cx, offset..offset) {
32-
clippy_utils::diagnostics::span_lint_and_help(
33-
cx,
34-
DOC_PARAGRAPHS_MISSING_PUNCTUATION,
35-
span,
36-
MSG,
37-
None,
38-
"end the paragraph with some punctuation",
39-
);
40-
}
41-
},
42-
}
43-
}
10+
#[derive(Default)]
11+
pub(super) struct MissingPunctuation {
12+
no_report_depth: u32,
13+
current_paragraph: Option<Position>,
4414
}
4515

46-
#[must_use]
47-
/// If punctuation is missing, returns the offset where new punctuation should be inserted.
48-
fn is_missing_punctuation(doc_string: &str) -> Vec<MissingPunctuation> {
49-
// The colon is not exactly a terminal punctuation mark, but this is required for paragraphs that
50-
// introduce a table or a list for example.
51-
const TERMINAL_PUNCTUATION_MARKS: &[char] = &['.', '?', '!', '…', ':'];
16+
impl MissingPunctuation {
17+
pub fn check(
18+
&mut self,
19+
cx: &LateContext<'_>,
20+
event: &Event<'_>,
21+
range: Range<usize>,
22+
doc: &str,
23+
fragments: Fragments<'_>,
24+
) {
25+
// The colon is not exactly a terminal punctuation mark, but this is required for paragraphs that
26+
// introduce a table or a list for example.
27+
const TERMINAL_PUNCTUATION_MARKS: &[char] = &['.', '?', '!', '…', ':'];
5228

53-
let mut no_report_depth = 0;
54-
let mut missing_punctuation = Vec::new();
55-
let mut current_paragraph = None;
56-
57-
for (event, offset) in
58-
Parser::new_ext(doc_string, main_body_opts() - Options::ENABLE_SMART_PUNCTUATION).into_offset_iter()
59-
{
6029
match event {
6130
Event::Start(
6231
Tag::CodeBlock(..)
@@ -66,61 +35,82 @@ fn is_missing_punctuation(doc_string: &str) -> Vec<MissingPunctuation> {
6635
| Tag::List(..)
6736
| Tag::Table(_),
6837
) => {
69-
no_report_depth += 1;
38+
self.no_report_depth += 1;
7039
},
7140
Event::End(TagEnd::FootnoteDefinition) => {
72-
no_report_depth -= 1;
41+
self.no_report_depth -= 1;
7342
},
7443
Event::End(
7544
TagEnd::CodeBlock | TagEnd::Heading(_) | TagEnd::HtmlBlock | TagEnd::List(_) | TagEnd::Table,
7645
) => {
77-
no_report_depth -= 1;
78-
current_paragraph = None;
46+
self.no_report_depth -= 1;
47+
self.current_paragraph = None;
7948
},
8049
Event::InlineHtml(_) | Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image) => {
81-
current_paragraph = None;
50+
self.current_paragraph = None;
8251
},
8352
Event::End(TagEnd::Paragraph) => {
84-
if let Some(mp) = current_paragraph {
85-
missing_punctuation.push(mp);
53+
if let Some(position) = self.current_paragraph
54+
&& let Some(span) = position.span(cx, fragments)
55+
{
56+
span_lint_and_then(
57+
cx,
58+
DOC_PARAGRAPHS_MISSING_PUNCTUATION,
59+
span,
60+
"doc paragraphs should end with a terminal punctuation mark",
61+
|diag| {
62+
if matches!(position, Position::Fixable(_)) {
63+
diag.span_suggestion(
64+
span,
65+
"end the paragraph with some punctuation",
66+
'.',
67+
Applicability::MaybeIncorrect,
68+
);
69+
} else {
70+
diag.help("end the paragraph with some punctuation");
71+
}
72+
},
73+
);
8674
}
8775
},
8876
Event::Code(..) | Event::Start(Tag::Link { .. }) | Event::End(TagEnd::Link)
89-
if no_report_depth == 0 && !offset.is_empty() =>
77+
if self.no_report_depth == 0 && !range.is_empty() =>
9078
{
91-
if doc_string[..offset.end]
92-
.trim_end()
93-
.ends_with(TERMINAL_PUNCTUATION_MARKS)
94-
{
95-
current_paragraph = None;
79+
if doc[..range.end].trim_end().ends_with(TERMINAL_PUNCTUATION_MARKS) {
80+
self.current_paragraph = None;
9681
} else {
97-
current_paragraph = Some(MissingPunctuation::Fixable(offset.end));
82+
self.current_paragraph = Some(Position::Fixable(range.end));
9883
}
9984
},
100-
Event::Text(..) if no_report_depth == 0 && !offset.is_empty() => {
101-
let trimmed = doc_string[..offset.end].trim_end();
85+
Event::Text(..) if self.no_report_depth == 0 && !range.is_empty() => {
86+
let trimmed = doc[..range.end].trim_end();
10287
if trimmed.ends_with(TERMINAL_PUNCTUATION_MARKS) {
103-
current_paragraph = None;
88+
self.current_paragraph = None;
10489
} else if let Some(t) = trimmed.strip_suffix(|c| c == ')' || c == '"') {
10590
if t.ends_with(TERMINAL_PUNCTUATION_MARKS) {
10691
// Avoid false positives.
107-
current_paragraph = None;
92+
self.current_paragraph = None;
10893
} else {
109-
current_paragraph = Some(MissingPunctuation::Unfixable(offset.end));
94+
self.current_paragraph = Some(Position::Unfixable(range.end));
11095
}
11196
} else {
112-
current_paragraph = Some(MissingPunctuation::Fixable(offset.end));
97+
self.current_paragraph = Some(Position::Fixable(range.end));
11398
}
11499
},
115100
_ => {},
116101
}
117102
}
118-
119-
missing_punctuation
120103
}
121104

122105
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
123-
enum MissingPunctuation {
106+
enum Position {
124107
Fixable(usize),
125108
Unfixable(usize),
126109
}
110+
111+
impl Position {
112+
fn span(self, cx: &LateContext<'_>, fragments: Fragments<'_>) -> Option<Span> {
113+
let (Position::Fixable(pos) | Position::Unfixable(pos)) = self;
114+
fragments.span(cx, pos..pos)
115+
}
116+
}

clippy_lints/src/doc/mod.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -904,15 +904,6 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
904904
},
905905
);
906906

907-
doc_paragraphs_missing_punctuation::check(
908-
cx,
909-
&doc,
910-
Fragments {
911-
doc: &doc,
912-
fragments: &fragments,
913-
},
914-
);
915-
916907
// NOTE: check_doc uses it own cb function,
917908
// to avoid causing duplicated diagnostics for the broken link checker.
918909
let mut full_fake_broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> {
@@ -1079,6 +1070,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
10791070
fragments: Fragments<'_>,
10801071
attrs: &[Attribute],
10811072
) -> DocHeaders {
1073+
let mut missing_punctuation = doc_paragraphs_missing_punctuation::MissingPunctuation::default();
1074+
10821075
// true if a safety header was found
10831076
let mut headers = DocHeaders::default();
10841077
let mut code = None;
@@ -1098,6 +1091,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
10981091
let mut events = events.peekable();
10991092

11001093
while let Some((event, range)) = events.next() {
1094+
missing_punctuation.check(cx, &event, range.clone(), doc, fragments);
1095+
11011096
match event {
11021097
Html(tag) | InlineHtml(tag) => {
11031098
if tag.starts_with("<code") {

0 commit comments

Comments
 (0)