Skip to content

Commit 8deb383

Browse files
committed
Fix false positives on broken link detection
1 parent 6ff53c7 commit 8deb383

File tree

4 files changed

+130
-17
lines changed

4 files changed

+130
-17
lines changed

clippy_lints/src/doc/broken_link.rs

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,115 @@
1-
use super::{DOC_BROKEN_LINK, Fragments};
21
use clippy_utils::diagnostics::span_lint;
2+
use rustc_ast::{AttrKind, AttrStyle, Attribute};
33
use rustc_lint::LateContext;
4-
use std::ops::Range;
5-
6-
// Check broken links in code docs.
7-
pub fn check(cx: &LateContext<'_>, _trimmed_text: &str, range: Range<usize>, fragments: Fragments<'_>, link: &str) {
8-
if let Some(span) = fragments.span(cx, range) {
9-
// Broken links are replaced with "fake" value by `fake_broken_link_callback` at `doc/mod.rs`.
10-
if link == "fake" {
11-
span_lint(cx, DOC_BROKEN_LINK, span, "possible broken doc link");
4+
use rustc_span::{BytePos, Span};
5+
6+
use super::DOC_BROKEN_LINK;
7+
8+
pub fn check(cx: &LateContext<'_>, attrs: &[Attribute]) {
9+
let broken_links: Vec<_> = BrokenLinkLoader::collect_spans_broken_link(attrs);
10+
11+
for span in broken_links {
12+
span_lint(cx, DOC_BROKEN_LINK, span, "possible broken doc link");
13+
}
14+
}
15+
16+
struct BrokenLinkLoader {
17+
spans_broken_link: Vec<Span>,
18+
active: bool,
19+
processing_title: bool,
20+
processing_link: bool,
21+
start_at: u32,
22+
}
23+
24+
impl BrokenLinkLoader {
25+
fn collect_spans_broken_link(attrs: &[Attribute]) -> Vec<Span> {
26+
let mut loader = BrokenLinkLoader {
27+
spans_broken_link: vec![],
28+
active: false,
29+
processing_title: false,
30+
processing_link: false,
31+
start_at: 0_u32,
32+
};
33+
loader.scan_attrs(attrs);
34+
loader.spans_broken_link
35+
}
36+
37+
fn scan_attrs(&mut self, attrs: &[Attribute]) -> Vec<(Span, String)> {
38+
let broken_links: Vec<(Span, String)> = vec![];
39+
40+
for attr in attrs {
41+
if let AttrKind::DocComment(_com_kind, sym) = attr.kind
42+
&& let AttrStyle::Outer = attr.style
43+
{
44+
self.scan_line(sym.as_str(), attr.span);
45+
}
1246
}
47+
48+
broken_links
49+
}
50+
51+
fn scan_line(&mut self, the_str: &str, attr_span: Span) {
52+
// Note that we specifically need the char _byte_ indices here, not the positional indexes
53+
// within the char array to deal with multi-byte characters properly. `char_indices` does
54+
// exactly that. It provides an iterator over tuples of the form `(byte position, char)`.
55+
let char_indices: Vec<_> = the_str.char_indices().collect();
56+
57+
let mut no_url_curr_line = true;
58+
59+
for (pos, c) in char_indices {
60+
if !self.active {
61+
if c == '[' {
62+
self.processing_title = true;
63+
self.active = true;
64+
self.start_at = attr_span.lo().0 + u32::try_from(pos).unwrap();
65+
}
66+
continue;
67+
}
68+
69+
if self.processing_title {
70+
if c == ']' {
71+
self.processing_title = false;
72+
}
73+
continue;
74+
}
75+
76+
if !self.processing_link {
77+
if c == '(' {
78+
self.processing_link = true;
79+
} else {
80+
// not a real link, start lookup over again
81+
self.reset_lookup();
82+
no_url_curr_line = true;
83+
}
84+
continue;
85+
}
86+
87+
if c == ')' {
88+
self.reset_lookup();
89+
no_url_curr_line = true;
90+
} else if no_url_curr_line && c != ' ' {
91+
no_url_curr_line = false;
92+
}
93+
}
94+
95+
// If it got to the end of the line and it still processing link part,
96+
// it means this is a broken link.
97+
if self.active && self.processing_link && !no_url_curr_line {
98+
let pos_end_line = u32::try_from(the_str.len()).unwrap() - 1;
99+
100+
// +3 skips the opening delimiter
101+
let start = BytePos(self.start_at + 3);
102+
let end = start + BytePos(pos_end_line);
103+
104+
let com_span = Span::new(start, end, attr_span.ctxt(), attr_span.parent());
105+
106+
self.spans_broken_link.push(com_span);
107+
}
108+
}
109+
110+
fn reset_lookup(&mut self) {
111+
self.processing_link = false;
112+
self.active = false;
113+
self.start_at = 0;
13114
}
14115
}

clippy_lints/src/doc/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ declare_clippy_lint! {
283283
/// /// [example of a good link](https://github.com/rust-lang/rust-clippy/)
284284
/// pub fn do_something() {}
285285
/// ```
286-
#[clippy::version = "1.82.0"]
286+
#[clippy::version = "1.84.0"]
287287
pub DOC_BROKEN_LINK,
288288
pedantic,
289289
"broken document link"
@@ -753,6 +753,9 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
753753
return Some(DocHeaders::default());
754754
}
755755

756+
// Run broken link checker before parsing the document.
757+
broken_link::check(cx, attrs);
758+
756759
let mut cb = fake_broken_link_callback;
757760

758761
// disable smart punctuation to pick up ['link'] more easily
@@ -958,9 +961,6 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
958961
} else {
959962
if in_link.is_some() {
960963
link_with_quotes::check(cx, trimmed_text, range.clone(), fragments);
961-
if let Some(link) = in_link.as_ref() {
962-
broken_link::check(cx, trimmed_text, range.clone(), fragments, link);
963-
}
964964
}
965965
if let Some(link) = in_link.as_ref()
966966
&& let Ok(url) = Url::parse(link)

tests/ui/doc_broken_link.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,15 @@ pub fn doc_invalid_link_broken_url_scheme_part() {}
3434
/// .fake/doc_invalid_link_broken_url_host_part)
3535
//~^^ ERROR: possible broken doc link
3636
pub fn doc_invalid_link_broken_url_host_part() {}
37+
38+
/// This might be considered a link false positive
39+
/// and should be ignored by this lint rule:
40+
/// Example of referencing some code with brackets [T].
41+
pub fn doc_ignore_link_false_positive_1() {}
42+
43+
/// This might be considered a link false positive
44+
/// and should be ignored by this lint rule:
45+
/// [`T`]. Continue text after brackets,
46+
/// then (something in
47+
/// parenthesis).
48+
pub fn doc_ignore_link_false_positive_2() {}

tests/ui/doc_broken_link.stderr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
error: possible broken doc link
2-
--> tests/ui/doc_broken_link.rs:27:6
2+
--> tests/ui/doc_broken_link.rs:27:5
33
|
44
LL | /// [doc invalid link broken url scheme part part](https://
5-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66
|
77
= note: `-D clippy::doc-broken-link` implied by `-D warnings`
88
= help: to override `-D warnings` add `#[allow(clippy::doc_broken_link)]`
99

1010
error: possible broken doc link
11-
--> tests/ui/doc_broken_link.rs:33:6
11+
--> tests/ui/doc_broken_link.rs:33:5
1212
|
1313
LL | /// [doc invalid link broken url host part](https://test
14-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1515

1616
error: aborting due to 2 previous errors
1717

0 commit comments

Comments
 (0)