]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/passes/bare_urls.rs
Rollup merge of #84484 - jyn514:check-tools, r=Mark-Simulacrum
[rust.git] / src / librustdoc / passes / bare_urls.rs
1 use super::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, Parser, Tag};
8 use regex::Regex;
9 use rustc_errors::Applicability;
10 use std::lazy::SyncLazy;
11 use std::mem;
12
13 crate const CHECK_BARE_URLS: Pass = Pass {
14     name: "check-bare-urls",
15     run: check_bare_urls,
16     description: "detects URLs that are not hyperlinks",
17 };
18
19 static URL_REGEX: SyncLazy<Regex> = SyncLazy::new(|| {
20     Regex::new(concat!(
21         r"https?://",                          // url scheme
22         r"([-a-zA-Z0-9@:%._\+~#=]{2,256}\.)+", // one or more subdomains
23         r"[a-zA-Z]{2,63}",                     // root domain
24         r"\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)"      // optional query or url fragments
25     ))
26     .expect("failed to build regex")
27 });
28
29 struct BareUrlsLinter<'a, 'tcx> {
30     cx: &'a mut DocContext<'tcx>,
31 }
32
33 impl<'a, 'tcx> BareUrlsLinter<'a, 'tcx> {
34     fn find_raw_urls(
35         &self,
36         text: &str,
37         range: Range<usize>,
38         f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
39     ) {
40         trace!("looking for raw urls in {}", text);
41         // For now, we only check "full" URLs (meaning, starting with "http://" or "https://").
42         for match_ in URL_REGEX.find_iter(&text) {
43             let url = match_.as_str();
44             let url_range = match_.range();
45             f(
46                 self.cx,
47                 "this URL is not a hyperlink",
48                 url,
49                 Range { start: range.start + url_range.start, end: range.start + url_range.end },
50             );
51         }
52     }
53 }
54
55 crate fn check_bare_urls(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
56     BareUrlsLinter { cx }.fold_crate(krate)
57 }
58
59 impl<'a, 'tcx> DocFolder for BareUrlsLinter<'a, 'tcx> {
60     fn fold_item(&mut self, item: Item) -> Option<Item> {
61         let hir_id = match DocContext::as_local_hir_id(self.cx.tcx, item.def_id) {
62             Some(hir_id) => hir_id,
63             None => {
64                 // If non-local, no need to check anything.
65                 return Some(self.fold_item_recur(item));
66             }
67         };
68         let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
69         if !dox.is_empty() {
70             let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range<usize>| {
71                 let sp = super::source_span_for_markdown_range(cx.tcx, &dox, &range, &item.attrs)
72                     .unwrap_or_else(|| item.attr_span(cx.tcx));
73                 cx.tcx.struct_span_lint_hir(crate::lint::BARE_URLS, hir_id, sp, |lint| {
74                     lint.build(msg)
75                         .note("bare URLs are not automatically turned into clickable links")
76                         .span_suggestion(
77                             sp,
78                             "use an automatic link instead",
79                             format!("<{}>", url),
80                             Applicability::MachineApplicable,
81                         )
82                         .emit()
83                 });
84             };
85
86             let mut p = Parser::new_ext(&dox, opts()).into_offset_iter();
87
88             while let Some((event, range)) = p.next() {
89                 match event {
90                     Event::Text(s) => self.find_raw_urls(&s, range, &report_diag),
91                     // We don't want to check the text inside code blocks or links.
92                     Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link(..))) => {
93                         while let Some((event, _)) = p.next() {
94                             match event {
95                                 Event::End(end)
96                                     if mem::discriminant(&end) == mem::discriminant(&tag) =>
97                                 {
98                                     break;
99                                 }
100                                 _ => {}
101                             }
102                         }
103                     }
104                     _ => {}
105                 }
106             }
107         }
108
109         Some(self.fold_item_recur(item))
110     }
111 }