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