Skip to content

Commit 6db16ff

Browse files
Rollup merge of #77856 - GuillaumeGomez:automatic-links-lint, r=jyn514
Add automatic_links lint Fixes #77501. r? @jyn514
2 parents 6c5dbcb + d7272ea commit 6db16ff

File tree

14 files changed

+373
-4
lines changed

14 files changed

+373
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4233,6 +4233,7 @@ dependencies = [
42334233
"itertools 0.9.0",
42344234
"minifier",
42354235
"pulldown-cmark 0.8.0",
4236+
"regex",
42364237
"rustc-rayon",
42374238
"serde",
42384239
"serde_json",

compiler/rustc_lint/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ use rustc_hir::def_id::LocalDefId;
6363
use rustc_middle::ty::query::Providers;
6464
use rustc_middle::ty::TyCtxt;
6565
use rustc_session::lint::builtin::{
66-
BARE_TRAIT_OBJECTS, BROKEN_INTRA_DOC_LINKS, ELIDED_LIFETIMES_IN_PATHS,
66+
AUTOMATIC_LINKS, BARE_TRAIT_OBJECTS, BROKEN_INTRA_DOC_LINKS, ELIDED_LIFETIMES_IN_PATHS,
6767
EXPLICIT_OUTLIVES_REQUIREMENTS, INVALID_CODEBLOCK_ATTRIBUTES, INVALID_HTML_TAGS,
6868
MISSING_DOC_CODE_EXAMPLES, PRIVATE_DOC_TESTS,
6969
};
@@ -307,6 +307,7 @@ fn register_builtins(store: &mut LintStore, no_interleave_lints: bool) {
307307

308308
add_lint_group!(
309309
"rustdoc",
310+
AUTOMATIC_LINKS,
310311
BROKEN_INTRA_DOC_LINKS,
311312
PRIVATE_INTRA_DOC_LINKS,
312313
INVALID_CODEBLOCK_ATTRIBUTES,

compiler/rustc_session/src/lint/builtin.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,17 @@ declare_lint! {
18911891
"detects invalid HTML tags in doc comments"
18921892
}
18931893

1894+
declare_lint! {
1895+
/// The `automatic_links` lint detects when a URL could be written using
1896+
/// only angle brackets. This is a `rustdoc` only lint, see the
1897+
/// documentation in the [rustdoc book].
1898+
///
1899+
/// [rustdoc book]: ../../../rustdoc/lints.html#automatic_links
1900+
pub AUTOMATIC_LINKS,
1901+
Warn,
1902+
"detects URLs that could be written using only angle brackets"
1903+
}
1904+
18941905
declare_lint! {
18951906
/// The `where_clauses_object_safety` lint detects for [object safety] of
18961907
/// [where clauses].
@@ -2711,6 +2722,7 @@ declare_lint_pass! {
27112722
MISSING_DOC_CODE_EXAMPLES,
27122723
INVALID_HTML_TAGS,
27132724
PRIVATE_DOC_TESTS,
2725+
AUTOMATIC_LINKS,
27142726
WHERE_CLAUSES_OBJECT_SAFETY,
27152727
PROC_MACRO_DERIVE_RESOLUTION_FALLBACK,
27162728
MACRO_USE_EXTERN_CRATE,

library/core/src/intrinsics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//! This includes changes in the stability of the constness.
1010
//!
1111
//! In order to make an intrinsic usable at compile-time, one needs to copy the implementation
12-
//! from https://github.com/rust-lang/miri/blob/master/src/shims/intrinsics.rs to
12+
//! from <https://github.com/rust-lang/miri/blob/master/src/shims/intrinsics.rs> to
1313
//! `compiler/rustc_mir/src/interpret/intrinsics.rs` and add a
1414
//! `#[rustc_const_unstable(feature = "foo", issue = "01234")]` to the intrinsic.
1515
//!

library/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ pub mod primitive;
284284
unused_imports,
285285
unsafe_op_in_unsafe_fn
286286
)]
287+
#[cfg_attr(not(bootstrap), allow(automatic_links))]
287288
// FIXME: This annotation should be moved into rust-lang/stdarch after clashing_extern_declarations is
288289
// merged. It currently cannot because bootstrap fails as the lint hasn't been defined yet.
289290
#[allow(clashing_extern_declarations)]

library/core/src/num/dec2flt/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
//!
3434
//! Primarily, this module and its children implement the algorithms described in:
3535
//! "How to Read Floating Point Numbers Accurately" by William D. Clinger,
36-
//! available online: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4152
36+
//! available online: <http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4152>
3737
//!
3838
//! In addition, there are numerous helper functions that are used in the paper but not available
3939
//! in Rust (or at least in core). Our version is additionally complicated by the need to handle

library/core/src/slice/sort.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Slice sorting
22
//!
33
//! This module contains a sorting algorithm based on Orson Peters' pattern-defeating quicksort,
4-
//! published at: https://github.com/orlp/pdqsort
4+
//! published at: <https://github.com/orlp/pdqsort>
55
//!
66
//! Unstable sorting is compatible with libcore because it doesn't allocate memory, unlike our
77
//! stable sorting implementation.

src/doc/rustdoc/src/lints.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,40 @@ warning: unclosed HTML tag `h1`
285285
286286
warning: 2 warnings emitted
287287
```
288+
289+
## automatic_links
290+
291+
This lint is **nightly-only** and **warns by default**. It detects links which
292+
could use the "automatic" link syntax. For example:
293+
294+
```rust
295+
/// http://hello.rs
296+
/// [http://a.com](http://a.com)
297+
/// [http://b.com]
298+
///
299+
/// [http://b.com]: http://b.com
300+
pub fn foo() {}
301+
```
302+
303+
Which will give:
304+
305+
```text
306+
warning: this URL is not a hyperlink
307+
--> foo.rs:3:5
308+
|
309+
3 | /// http://hello.rs
310+
| ^^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://hello.rs>`
311+
|
312+
313+
warning: unneeded long form for URL
314+
--> foo.rs:4:5
315+
|
316+
4 | /// [http://a.com](http://a.com)
317+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://a.com>`
318+
319+
warning: unneeded long form for URL
320+
--> foo.rs:5:5
321+
|
322+
5 | /// [http://b.com]
323+
| ^^^^^^^^^^^^^^ help: use an automatic link instead: `<http://b.com>`
324+
```

src/librustdoc/Cargo.toml

Lines changed: 1 addition & 0 deletions
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/core.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,13 @@ pub fn run_core(
330330
let invalid_codeblock_attributes_name = rustc_lint::builtin::INVALID_CODEBLOCK_ATTRIBUTES.name;
331331
let invalid_html_tags = rustc_lint::builtin::INVALID_HTML_TAGS.name;
332332
let renamed_and_removed_lints = rustc_lint::builtin::RENAMED_AND_REMOVED_LINTS.name;
333+
let automatic_links = rustc_lint::builtin::AUTOMATIC_LINKS.name;
333334
let unknown_lints = rustc_lint::builtin::UNKNOWN_LINTS.name;
334335

335336
// In addition to those specific lints, we also need to allow those given through
336337
// command line, otherwise they'll get ignored and we don't want that.
337338
let lints_to_show = vec![
339+
automatic_links.to_owned(),
338340
intra_link_resolution_failure_name.to_owned(),
339341
missing_docs.to_owned(),
340342
missing_doc_example.to_owned(),
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use super::{span_of_attrs, Pass};
2+
use crate::clean::*;
3+
use crate::core::DocContext;
4+
use crate::fold::DocFolder;
5+
use crate::html::markdown::opts;
6+
use core::ops::Range;
7+
use pulldown_cmark::{Event, LinkType, Parser, Tag};
8+
use regex::Regex;
9+
use rustc_errors::Applicability;
10+
use rustc_feature::UnstableFeatures;
11+
use rustc_session::lint;
12+
13+
pub const CHECK_AUTOMATIC_LINKS: Pass = Pass {
14+
name: "check-automatic-links",
15+
run: check_automatic_links,
16+
description: "detects URLS that could be written using angle brackets",
17+
};
18+
19+
const URL_REGEX: &str = concat!(
20+
r"https?://", // url scheme
21+
r"([-a-zA-Z0-9@:%._\+~#=]{2,256}\.)+", // one or more subdomains
22+
r"[a-zA-Z]{2,4}", // root domain
23+
r"\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)" // optional query or url fragments
24+
);
25+
26+
struct AutomaticLinksLinter<'a, 'tcx> {
27+
cx: &'a DocContext<'tcx>,
28+
regex: Regex,
29+
}
30+
31+
impl<'a, 'tcx> AutomaticLinksLinter<'a, 'tcx> {
32+
fn new(cx: &'a DocContext<'tcx>) -> Self {
33+
AutomaticLinksLinter { cx, regex: Regex::new(URL_REGEX).expect("failed to build regex") }
34+
}
35+
36+
fn find_raw_urls(
37+
&self,
38+
text: &str,
39+
range: Range<usize>,
40+
f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
41+
) {
42+
// For now, we only check "full" URLs (meaning, starting with "http://" or "https://").
43+
for match_ in self.regex.find_iter(&text) {
44+
let url = match_.as_str();
45+
let url_range = match_.range();
46+
f(
47+
self.cx,
48+
"this URL is not a hyperlink",
49+
url,
50+
Range { start: range.start + url_range.start, end: range.start + url_range.end },
51+
);
52+
}
53+
}
54+
}
55+
56+
pub fn check_automatic_links(krate: Crate, cx: &DocContext<'_>) -> Crate {
57+
if !UnstableFeatures::from_environment().is_nightly_build() {
58+
krate
59+
} else {
60+
let mut coll = AutomaticLinksLinter::new(cx);
61+
62+
coll.fold_crate(krate)
63+
}
64+
}
65+
66+
impl<'a, 'tcx> DocFolder for AutomaticLinksLinter<'a, 'tcx> {
67+
fn fold_item(&mut self, item: Item) -> Option<Item> {
68+
let hir_id = match self.cx.as_local_hir_id(item.def_id) {
69+
Some(hir_id) => hir_id,
70+
None => {
71+
// If non-local, no need to check anything.
72+
return self.fold_item_recur(item);
73+
}
74+
};
75+
let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
76+
if !dox.is_empty() {
77+
let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range<usize>| {
78+
let sp = super::source_span_for_markdown_range(cx, &dox, &range, &item.attrs)
79+
.or_else(|| span_of_attrs(&item.attrs))
80+
.unwrap_or(item.source.span());
81+
cx.tcx.struct_span_lint_hir(lint::builtin::AUTOMATIC_LINKS, hir_id, sp, |lint| {
82+
lint.build(msg)
83+
.span_suggestion(
84+
sp,
85+
"use an automatic link instead",
86+
format!("<{}>", url),
87+
Applicability::MachineApplicable,
88+
)
89+
.emit()
90+
});
91+
};
92+
93+
let p = Parser::new_ext(&dox, opts()).into_offset_iter();
94+
95+
let mut title = String::new();
96+
let mut in_link = false;
97+
let mut ignore = false;
98+
99+
for (event, range) in p {
100+
match event {
101+
Event::Start(Tag::Link(kind, _, _)) => {
102+
in_link = true;
103+
ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
104+
}
105+
Event::End(Tag::Link(_, url, _)) => {
106+
in_link = false;
107+
// NOTE: links cannot be nested, so we don't need to check `kind`
108+
if url.as_ref() == title && !ignore {
109+
report_diag(self.cx, "unneeded long form for URL", &url, range);
110+
}
111+
title.clear();
112+
ignore = false;
113+
}
114+
Event::Text(s) if in_link => {
115+
if !ignore {
116+
title.push_str(&s);
117+
}
118+
}
119+
Event::Text(s) => self.find_raw_urls(&s, range, &report_diag),
120+
_ => {}
121+
}
122+
}
123+
}
124+
125+
self.fold_item_recur(item)
126+
}
127+
}

src/librustdoc/passes/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use crate::core::DocContext;
1111
mod stripper;
1212
pub use stripper::*;
1313

14+
mod automatic_links;
15+
pub use self::automatic_links::CHECK_AUTOMATIC_LINKS;
16+
1417
mod collapse_docs;
1518
pub use self::collapse_docs::COLLAPSE_DOCS;
1619

@@ -90,6 +93,7 @@ pub const PASSES: &[Pass] = &[
9093
COLLECT_TRAIT_IMPLS,
9194
CALCULATE_DOC_COVERAGE,
9295
CHECK_INVALID_HTML_TAGS,
96+
CHECK_AUTOMATIC_LINKS,
9397
];
9498

9599
/// The list of passes run by default.
@@ -105,6 +109,7 @@ pub const DEFAULT_PASSES: &[ConditionalPass] = &[
105109
ConditionalPass::always(CHECK_CODE_BLOCK_SYNTAX),
106110
ConditionalPass::always(CHECK_INVALID_HTML_TAGS),
107111
ConditionalPass::always(PROPAGATE_DOC_CFG),
112+
ConditionalPass::always(CHECK_AUTOMATIC_LINKS),
108113
];
109114

110115
/// The list of default passes run when `--doc-coverage` is passed to rustdoc.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#![deny(automatic_links)]
2+
3+
/// [http://a.com](http://a.com)
4+
//~^ ERROR unneeded long form for URL
5+
/// [http://b.com]
6+
//~^ ERROR unneeded long form for URL
7+
///
8+
/// [http://b.com]: http://b.com
9+
///
10+
/// [http://c.com][http://c.com]
11+
pub fn a() {}
12+
13+
/// https://somewhere.com
14+
//~^ ERROR this URL is not a hyperlink
15+
/// https://somewhere.com/a
16+
//~^ ERROR this URL is not a hyperlink
17+
/// https://www.somewhere.com
18+
//~^ ERROR this URL is not a hyperlink
19+
/// https://www.somewhere.com/a
20+
//~^ ERROR this URL is not a hyperlink
21+
/// https://subdomain.example.com
22+
//~^ ERROR not a hyperlink
23+
/// https://somewhere.com?
24+
//~^ ERROR this URL is not a hyperlink
25+
/// https://somewhere.com/a?
26+
//~^ ERROR this URL is not a hyperlink
27+
/// https://somewhere.com?hello=12
28+
//~^ ERROR this URL is not a hyperlink
29+
/// https://somewhere.com/a?hello=12
30+
//~^ ERROR this URL is not a hyperlink
31+
/// https://example.com?hello=12#xyz
32+
//~^ ERROR this URL is not a hyperlink
33+
/// https://example.com/a?hello=12#xyz
34+
//~^ ERROR this URL is not a hyperlink
35+
/// https://example.com#xyz
36+
//~^ ERROR this URL is not a hyperlink
37+
/// https://example.com/a#xyz
38+
//~^ ERROR this URL is not a hyperlink
39+
/// https://somewhere.com?hello=12&bye=11
40+
//~^ ERROR this URL is not a hyperlink
41+
/// https://somewhere.com/a?hello=12&bye=11
42+
//~^ ERROR this URL is not a hyperlink
43+
/// https://somewhere.com?hello=12&bye=11#xyz
44+
//~^ ERROR this URL is not a hyperlink
45+
/// hey! https://somewhere.com/a?hello=12&bye=11#xyz
46+
//~^ ERROR this URL is not a hyperlink
47+
pub fn c() {}
48+
49+
/// <https://somewhere.com>
50+
/// [a](http://a.com)
51+
/// [b]
52+
///
53+
/// [b]: http://b.com
54+
pub fn everything_is_fine_here() {}
55+
56+
#[allow(automatic_links)]
57+
pub mod foo {
58+
/// https://somewhere.com/a?hello=12&bye=11#xyz
59+
pub fn bar() {}
60+
}

0 commit comments

Comments
 (0)