Skip to content

Commit ade32c1

Browse files
committed
Reuse existing markdown parser in doc_link_code
1 parent 0a8cb5d commit ade32c1

File tree

5 files changed

+160
-97
lines changed

5 files changed

+160
-97
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::mem;
2+
use std::ops::Range;
3+
4+
use clippy_utils::diagnostics::span_lint_and_then;
5+
use rustc_errors::Applicability;
6+
use rustc_lint::LateContext;
7+
use rustc_resolve::rustdoc::pulldown_cmark::{Event, Tag, TagEnd};
8+
9+
use crate::doc::Fragments;
10+
11+
use super::DOC_LINK_CODE;
12+
13+
struct PendingLink {
14+
range: Range<usize>,
15+
seen_code: bool,
16+
}
17+
18+
#[derive(Default)]
19+
pub(super) struct LinkCode {
20+
start: Option<usize>,
21+
end: Option<usize>,
22+
includes_link: bool,
23+
pending_link: Option<PendingLink>,
24+
}
25+
26+
impl LinkCode {
27+
pub fn check(
28+
&mut self,
29+
cx: &LateContext<'_>,
30+
event: &Event<'_>,
31+
range: Range<usize>,
32+
doc: &str,
33+
fragments: Fragments<'_>,
34+
) {
35+
match event {
36+
Event::Start(Tag::Link { .. }) => {
37+
self.pending_link = Some(PendingLink {
38+
range,
39+
seen_code: false,
40+
});
41+
},
42+
Event::End(TagEnd::Link) => {
43+
if let Some(PendingLink { range, seen_code: true }) = self.pending_link.take() {
44+
if self.start.is_some() {
45+
self.end = Some(range.end);
46+
} else {
47+
self.start = Some(range.start);
48+
}
49+
self.includes_link = true;
50+
}
51+
},
52+
_ if let Some(pending_link) = &mut self.pending_link => {
53+
if matches!(event, Event::Code(_)) && !pending_link.seen_code {
54+
pending_link.seen_code = true;
55+
} else {
56+
self.consume(cx, fragments, doc);
57+
}
58+
},
59+
Event::Code(_) => {
60+
if self.start.is_some() {
61+
self.end = Some(range.end);
62+
} else {
63+
self.start = Some(range.start);
64+
}
65+
},
66+
_ => self.consume(cx, fragments, doc),
67+
}
68+
}
69+
70+
fn consume(&mut self, cx: &LateContext<'_>, fragments: Fragments<'_>, doc: &str) {
71+
if let LinkCode {
72+
start: Some(start),
73+
end: Some(end),
74+
includes_link: true,
75+
pending_link: _,
76+
} = mem::take(self)
77+
&& let Some(span) = fragments.span(cx, start..end)
78+
{
79+
span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {
80+
diag.span_suggestion_verbose(
81+
span,
82+
"wrap the entire group in `<code>` tags",
83+
format!("<code>{}</code>", doc[start..end].replace('`', "")),
84+
Applicability::MaybeIncorrect,
85+
);
86+
diag.help("separate code snippets will be shown with a gap");
87+
});
88+
}
89+
}
90+
}

clippy_lints/src/doc/mod.rs

Lines changed: 9 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ use rustc_span::Span;
2525
use std::ops::Range;
2626
use url::Url;
2727

28+
use doc_link_code::LinkCode;
29+
use doc_paragraphs_missing_punctuation::MissingPunctuation;
30+
2831
mod broken_link;
2932
mod doc_comment_double_space_linebreaks;
33+
mod doc_link_code;
3034
mod doc_paragraphs_missing_punctuation;
3135
mod doc_suspicious_footnotes;
3236
mod include_in_doc_without_cfg;
@@ -847,14 +851,6 @@ struct DocHeaders {
847851
/// back in the various late lint pass methods if they need the final doc headers, like "Safety" or
848852
/// "Panics" sections.
849853
fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[Attribute]) -> Option<DocHeaders> {
850-
// We don't want the parser to choke on intra doc links. Since we don't
851-
// actually care about rendering them, just pretend that all broken links
852-
// point to a fake address.
853-
#[expect(clippy::unnecessary_wraps)] // we're following a type signature
854-
fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> {
855-
Some(("fake".into(), "fake".into()))
856-
}
857-
858854
if suspicious_doc_comments::check(cx, attrs) || is_doc_hidden(attrs) {
859855
return None;
860856
}
@@ -889,32 +885,16 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
889885
return Some(DocHeaders::default());
890886
}
891887

892-
check_for_code_clusters(
893-
cx,
894-
pulldown_cmark::Parser::new_with_broken_link_callback(
895-
&doc,
896-
main_body_opts() - Options::ENABLE_SMART_PUNCTUATION,
897-
Some(&mut fake_broken_link_callback),
898-
)
899-
.into_offset_iter(),
900-
&doc,
901-
Fragments {
902-
doc: &doc,
903-
fragments: &fragments,
904-
},
905-
);
906-
907888
// NOTE: check_doc uses it own cb function,
908889
// to avoid causing duplicated diagnostics for the broken link checker.
909-
let mut full_fake_broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> {
890+
let mut broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> {
910891
broken_link::check(cx, &bl, &doc, &fragments);
911892
Some(("fake".into(), "fake".into()))
912893
};
913894

914895
// disable smart punctuation to pick up ['link'] more easily
915896
let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION;
916-
let parser =
917-
pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut full_fake_broken_link_callback));
897+
let parser = pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut broken_link_callback));
918898

919899
Some(check_doc(
920900
cx,
@@ -934,65 +914,6 @@ enum Container {
934914
List(usize),
935915
}
936916

937-
/// Scan the documentation for code links that are back-to-back with code spans.
938-
///
939-
/// This is done separately from the rest of the docs, because that makes it easier to produce
940-
/// the correct messages.
941-
fn check_for_code_clusters<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
942-
cx: &LateContext<'_>,
943-
events: Events,
944-
doc: &str,
945-
fragments: Fragments<'_>,
946-
) {
947-
let mut events = events.peekable();
948-
let mut code_starts_at = None;
949-
let mut code_ends_at = None;
950-
let mut code_includes_link = false;
951-
while let Some((event, range)) = events.next() {
952-
match event {
953-
Start(Link { .. }) if matches!(events.peek(), Some((Code(_), _range))) => {
954-
if code_starts_at.is_some() {
955-
code_ends_at = Some(range.end);
956-
} else {
957-
code_starts_at = Some(range.start);
958-
}
959-
code_includes_link = true;
960-
// skip the nested "code", because we're already handling it here
961-
let _ = events.next();
962-
},
963-
Code(_) => {
964-
if code_starts_at.is_some() {
965-
code_ends_at = Some(range.end);
966-
} else {
967-
code_starts_at = Some(range.start);
968-
}
969-
},
970-
End(TagEnd::Link) => {},
971-
_ => {
972-
if let Some(start) = code_starts_at
973-
&& let Some(end) = code_ends_at
974-
&& code_includes_link
975-
&& let Some(span) = fragments.span(cx, start..end)
976-
{
977-
span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {
978-
let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));
979-
diag.span_suggestion_verbose(
980-
span,
981-
"wrap the entire group in `<code>` tags",
982-
sugg,
983-
Applicability::MaybeIncorrect,
984-
);
985-
diag.help("separate code snippets will be shown with a gap");
986-
});
987-
}
988-
code_includes_link = false;
989-
code_starts_at = None;
990-
code_ends_at = None;
991-
},
992-
}
993-
}
994-
}
995-
996917
#[derive(Clone, Copy)]
997918
#[expect(clippy::struct_excessive_bools)]
998919
struct CodeTags {
@@ -1070,7 +991,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
1070991
fragments: Fragments<'_>,
1071992
attrs: &[Attribute],
1072993
) -> DocHeaders {
1073-
let mut missing_punctuation = doc_paragraphs_missing_punctuation::MissingPunctuation::default();
994+
let mut missing_punctuation = MissingPunctuation::default();
995+
let mut link_code = LinkCode::default();
1074996

1075997
// true if a safety header was found
1076998
let mut headers = DocHeaders::default();
@@ -1092,6 +1014,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
10921014

10931015
while let Some((event, range)) = events.next() {
10941016
missing_punctuation.check(cx, &event, range.clone(), doc, fragments);
1017+
link_code.check(cx, &event, range.clone(), doc, fragments);
10951018

10961019
match event {
10971020
Html(tag) | InlineHtml(tag) => {

tests/ui/doc/link_adjacent.fixed

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
//! Neither is this: [`first`](x) `second`
1010
//!
1111
//! Neither is this: [first](x)`second`
12+
//!
13+
//! Neither is this: `first`[](x)
1214
//!
1315
//! This is: <code>[first](x)second</code>
1416
//~^ ERROR: adjacent
@@ -24,6 +26,9 @@
2426
//!
2527
//! So is this <code>[first](x)second[third](x)</code>
2628
//~^ ERROR: adjacent
29+
//!
30+
//! <code>first[second](x)</code>[third](x)
31+
//~^ ERROR: adjacent
2732

2833
/// Test case for code links that are adjacent to code text.
2934
///
@@ -35,6 +40,8 @@
3540
///
3641
/// Neither is this: [first](x)`second` arst
3742
///
43+
/// Neither is this: `first`[](x) arst
44+
///
3845
/// This is: <code>[first](x)second</code> arst
3946
//~^ ERROR: adjacent
4047
///
@@ -49,4 +56,7 @@
4956
///
5057
/// So is this <code>[first](x)second[third](x)</code> arst
5158
//~^ ERROR: adjacent
59+
///
60+
/// <code>first[second](x)</code>[third](x) arst
61+
//~^ ERROR: adjacent
5262
pub struct WithTrailing;

tests/ui/doc/link_adjacent.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
//!
1111
//! Neither is this: [first](x)`second`
1212
//!
13+
//! Neither is this: `first`[](x)
14+
//!
15+
//! Neither is this: `first`[`second` not code](x)
16+
//!
1317
//! This is: [`first`](x)`second`
1418
//~^ ERROR: adjacent
1519
//!
@@ -24,6 +28,9 @@
2428
//!
2529
//! So is this [`first`](x)`second`[`third`](x)
2630
//~^ ERROR: adjacent
31+
//!
32+
//! `first`[`second`](x)[third](x)
33+
//~^ ERROR: adjacent
2734

2835
/// Test case for code links that are adjacent to code text.
2936
///
@@ -35,6 +42,10 @@
3542
///
3643
/// Neither is this: [first](x)`second` arst
3744
///
45+
/// Neither is this: `first`[](x) arst
46+
///
47+
/// Neither is this: `first`[`second` not code](x) arst
48+
///
3849
/// This is: [`first`](x)`second` arst
3950
//~^ ERROR: adjacent
4051
///
@@ -49,4 +60,7 @@
4960
///
5061
/// So is this [`first`](x)`second`[`third`](x) arst
5162
//~^ ERROR: adjacent
63+
///
64+
/// `first`[`second`](x)[third](x) arst
65+
//~^ ERROR: adjacent
5266
pub struct WithTrailing;

0 commit comments

Comments
 (0)