Skip to content

Commit 6fdd98d

Browse files
Extend automatic_links lint to take into account URLs without link syntax
1 parent 3649620 commit 6fdd98d

File tree

6 files changed

+94
-45
lines changed

6 files changed

+94
-45
lines changed

Cargo.lock

+1
Original file line numberDiff line numberDiff line change
@@ -4203,6 +4203,7 @@ dependencies = [
42034203
"itertools 0.9.0",
42044204
"minifier",
42054205
"pulldown-cmark 0.8.0",
4206+
"regex",
42064207
"rustc-rayon",
42074208
"serde",
42084209
"serde_json",

src/doc/rustdoc/src/lints.md

+13-9
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ which could use the "automatic" link syntax. For example:
294294
```rust
295295
#![warn(automatic_links)]
296296

297+
/// http://hello.rs
297298
/// [http://a.com](http://a.com)
298299
/// [http://b.com]
299300
///
@@ -304,24 +305,27 @@ pub fn foo() {}
304305
Which will give:
305306

306307
```text
307-
error: Unneeded long form for URL
308+
warning: won't be a link as is
308309
--> foo.rs:3:5
309310
|
310-
3 | /// [http://a.com](http://a.com)
311-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
311+
3 | /// http://hello.rs
312+
| ^^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://hello.rs>`
312313
|
313314
note: the lint level is defined here
314315
--> foo.rs:1:9
315316
|
316-
1 | #![deny(automatic_links)]
317+
1 | #![warn(automatic_links)]
317318
| ^^^^^^^^^^^^^^^
318-
= help: Try with `<http://a.com>` instead
319319
320-
error: Unneeded long form for URL
320+
warning: unneeded long form for URL
321+
--> foo.rs:4:5
322+
|
323+
4 | /// [http://a.com](http://a.com)
324+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://a.com>`
325+
326+
warning: unneeded long form for URL
321327
--> foo.rs:5:5
322328
|
323329
5 | /// [http://b.com]
324-
| ^^^^^^^^^^^^^^
325-
|
326-
= help: Try with `<http://b.com>` instead
330+
| ^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://b.com>`
327331
```

src/librustdoc/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ serde_json = "1.0"
1616
smallvec = "1.0"
1717
tempfile = "3"
1818
itertools = "0.9"
19+
regex = "1"
1920

2021
[dev-dependencies]
2122
expect-test = "1.0"

src/librustdoc/passes/automatic_links.rs

+62-27
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,55 @@ use crate::clean::*;
33
use crate::core::DocContext;
44
use crate::fold::DocFolder;
55
use crate::html::markdown::opts;
6-
use pulldown_cmark::{Event, Parser, Tag};
6+
use core::ops::Range;
7+
use pulldown_cmark::{Event, LinkType, Parser, Tag};
8+
use regex::Regex;
9+
use rustc_errors::Applicability;
710
use rustc_feature::UnstableFeatures;
811
use rustc_session::lint;
912

1013
pub const CHECK_AUTOMATIC_LINKS: Pass = Pass {
1114
name: "check-automatic-links",
1215
run: check_automatic_links,
13-
description: "detects URLS/email addresses that could be written using brackets",
16+
description: "detects URLS/email addresses that could be written using angle brackets",
1417
};
1518

19+
const URL_REGEX: &str =
20+
r"https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)";
21+
1622
struct AutomaticLinksLinter<'a, 'tcx> {
1723
cx: &'a DocContext<'tcx>,
24+
regex: Regex,
1825
}
1926

2027
impl<'a, 'tcx> AutomaticLinksLinter<'a, 'tcx> {
2128
fn new(cx: &'a DocContext<'tcx>) -> Self {
22-
AutomaticLinksLinter { cx }
29+
AutomaticLinksLinter { cx, regex: Regex::new(URL_REGEX).expect("failed to build regex") }
30+
}
31+
32+
fn find_raw_urls(
33+
&self,
34+
text: &str,
35+
range: Range<usize>,
36+
f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
37+
) {
38+
for (pos, c) in text.char_indices() {
39+
// For now, we only check "full" URLs.
40+
if c == 'h' {
41+
let text = &text[pos..];
42+
if text.starts_with("http://") || text.starts_with("https://") {
43+
if let Some(m) = self.regex.find(text) {
44+
let url = &text[..m.end()];
45+
f(
46+
self.cx,
47+
"won't be a link as is",
48+
url,
49+
Range { start: range.start + pos, end: range.start + pos + m.end() },
50+
)
51+
}
52+
}
53+
}
54+
}
2355
}
2456
}
2557

@@ -44,45 +76,48 @@ impl<'a, 'tcx> DocFolder for AutomaticLinksLinter<'a, 'tcx> {
4476
};
4577
let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
4678
if !dox.is_empty() {
47-
let cx = &self.cx;
79+
let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range<usize>| {
80+
let sp = super::source_span_for_markdown_range(cx, &dox, &range, &item.attrs)
81+
.or_else(|| span_of_attrs(&item.attrs))
82+
.unwrap_or(item.source.span());
83+
cx.tcx.struct_span_lint_hir(lint::builtin::AUTOMATIC_LINKS, hir_id, sp, |lint| {
84+
lint.build(msg)
85+
.span_suggestion(
86+
sp,
87+
"use an automatic link instead",
88+
format!("<{}>", url),
89+
Applicability::MachineApplicable,
90+
)
91+
.emit()
92+
});
93+
};
4894

4995
let p = Parser::new_ext(&dox, opts()).into_offset_iter();
5096

5197
let mut title = String::new();
5298
let mut in_link = false;
99+
let mut ignore = false;
53100

54101
for (event, range) in p {
55102
match event {
56-
Event::Start(Tag::Link(..)) => in_link = true,
103+
Event::Start(Tag::Link(kind, _, _)) => {
104+
in_link = true;
105+
ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
106+
}
57107
Event::End(Tag::Link(_, url, _)) => {
58108
in_link = false;
59-
if url.as_ref() != title {
60-
continue;
109+
if url.as_ref() == title && !ignore {
110+
report_diag(self.cx, "unneeded long form for URL", &url, range);
61111
}
62-
let sp = match super::source_span_for_markdown_range(
63-
cx,
64-
&dox,
65-
&range,
66-
&item.attrs,
67-
) {
68-
Some(sp) => sp,
69-
None => span_of_attrs(&item.attrs).unwrap_or(item.source.span()),
70-
};
71-
cx.tcx.struct_span_lint_hir(
72-
lint::builtin::AUTOMATIC_LINKS,
73-
hir_id,
74-
sp,
75-
|lint| {
76-
lint.build("Unneeded long form for URL")
77-
.help(&format!("Try with `<{}>` instead", url))
78-
.emit()
79-
},
80-
);
81112
title.clear();
113+
ignore = false;
82114
}
83115
Event::Text(s) if in_link => {
84-
title.push_str(&s);
116+
if !ignore {
117+
title.push_str(&s);
118+
}
85119
}
120+
Event::Text(s) => self.find_raw_urls(&s, range, &report_diag),
86121
_ => {}
87122
}
88123
}

src/test/rustdoc-ui/automatic-links.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
#![deny(automatic_links)]
22

33
/// [http://a.com](http://a.com)
4-
//~^ ERROR Unneeded long form for URL
4+
//~^ ERROR unneeded long form for URL
55
/// [http://b.com]
6-
//~^ ERROR Unneeded long form for URL
6+
//~^ ERROR unneeded long form for URL
77
///
88
/// [http://b.com]: http://b.com
99
///
1010
/// [http://c.com][http://c.com]
1111
pub fn a() {}
1212

13+
/// https://somewhere.com?hello=12
14+
//~^ ERROR won't be a link as is
15+
pub fn c() {}
16+
17+
/// <https://somewhere.com>
1318
/// [a](http://a.com)
1419
/// [b]
1520
///
+10-7
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
error: Unneeded long form for URL
1+
error: unneeded long form for URL
22
--> $DIR/automatic-links.rs:3:5
33
|
44
LL | /// [http://a.com](http://a.com)
5-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://a.com>`
66
|
77
note: the lint level is defined here
88
--> $DIR/automatic-links.rs:1:9
99
|
1010
LL | #![deny(automatic_links)]
1111
| ^^^^^^^^^^^^^^^
12-
= help: Try with `<http://a.com>` instead
1312

14-
error: Unneeded long form for URL
13+
error: unneeded long form for URL
1514
--> $DIR/automatic-links.rs:5:5
1615
|
1716
LL | /// [http://b.com]
18-
| ^^^^^^^^^^^^^^
17+
| ^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://b.com>`
18+
19+
error: won't be a link as is
20+
--> $DIR/automatic-links.rs:13:5
1921
|
20-
= help: Try with `<http://b.com>` instead
22+
LL | /// https://somewhere.com?hello=12
23+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `<https://somewhere.com?hello=12>`
2124

22-
error: aborting due to 2 previous errors
25+
error: aborting due to 3 previous errors
2326

0 commit comments

Comments
 (0)