]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/passes/non_autolinks.rs
remove unused return types such as empty Results or Options that would always be...
[rust.git] / src / librustdoc / passes / non_autolinks.rs
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_session::lint;
11
12 crate const CHECK_NON_AUTOLINKS: Pass = Pass {
13     name: "check-non-autolinks",
14     run: check_non_autolinks,
15     description: "detects URLS that could be written using angle brackets",
16 };
17
18 const URL_REGEX: &str = concat!(
19     r"https?://",                          // url scheme
20     r"([-a-zA-Z0-9@:%._\+~#=]{2,256}\.)+", // one or more subdomains
21     r"[a-zA-Z]{2,63}",                     // root domain
22     r"\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)"      // optional query or url fragments
23 );
24
25 struct NonAutolinksLinter<'a, 'tcx> {
26     cx: &'a DocContext<'tcx>,
27     regex: Regex,
28 }
29
30 impl<'a, 'tcx> NonAutolinksLinter<'a, 'tcx> {
31     fn new(cx: &'a DocContext<'tcx>) -> Self {
32         Self { cx, regex: Regex::new(URL_REGEX).expect("failed to build regex") }
33     }
34
35     fn find_raw_urls(
36         &self,
37         text: &str,
38         range: Range<usize>,
39         f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
40     ) {
41         // For now, we only check "full" URLs (meaning, starting with "http://" or "https://").
42         for match_ in self.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_non_autolinks(krate: Crate, cx: &DocContext<'_>) -> Crate {
56     if !cx.tcx.sess.is_nightly_build() {
57         krate
58     } else {
59         let mut coll = NonAutolinksLinter::new(cx);
60
61         coll.fold_crate(krate)
62     }
63 }
64
65 impl<'a, 'tcx> DocFolder for NonAutolinksLinter<'a, 'tcx> {
66     fn fold_item(&mut self, item: Item) -> Option<Item> {
67         let hir_id = match self.cx.as_local_hir_id(item.def_id) {
68             Some(hir_id) => hir_id,
69             None => {
70                 // If non-local, no need to check anything.
71                 return Some(self.fold_item_recur(item));
72             }
73         };
74         let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
75         if !dox.is_empty() {
76             let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range<usize>| {
77                 let sp = super::source_span_for_markdown_range(cx, &dox, &range, &item.attrs)
78                     .or_else(|| span_of_attrs(&item.attrs))
79                     .unwrap_or(item.source.span());
80                 cx.tcx.struct_span_lint_hir(lint::builtin::NON_AUTOLINKS, hir_id, sp, |lint| {
81                     lint.build(msg)
82                         .span_suggestion(
83                             sp,
84                             "use an automatic link instead",
85                             format!("<{}>", url),
86                             Applicability::MachineApplicable,
87                         )
88                         .emit()
89                 });
90             };
91
92             let mut p = Parser::new_ext(&dox, opts()).into_offset_iter();
93
94             while let Some((event, range)) = p.next() {
95                 match event {
96                     Event::Start(Tag::Link(kind, _, _)) => {
97                         let ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
98                         let mut title = String::new();
99
100                         while let Some((event, range)) = p.next() {
101                             match event {
102                                 Event::End(Tag::Link(_, url, _)) => {
103                                     // NOTE: links cannot be nested, so we don't need to
104                                     // check `kind`
105                                     if url.as_ref() == title && !ignore && self.regex.is_match(&url)
106                                     {
107                                         report_diag(
108                                             self.cx,
109                                             "unneeded long form for URL",
110                                             &url,
111                                             range,
112                                         );
113                                     }
114                                     break;
115                                 }
116                                 Event::Text(s) if !ignore => title.push_str(&s),
117                                 _ => {}
118                             }
119                         }
120                     }
121                     Event::Text(s) => self.find_raw_urls(&s, range, &report_diag),
122                     Event::Start(Tag::CodeBlock(_)) => {
123                         // We don't want to check the text inside the code blocks.
124                         while let Some((event, _)) = p.next() {
125                             match event {
126                                 Event::End(Tag::CodeBlock(_)) => break,
127                                 _ => {}
128                             }
129                         }
130                     }
131                     _ => {}
132                 }
133             }
134         }
135
136         Some(self.fold_item_recur(item))
137     }
138 }