1 use super::{span_of_attrs, Pass};
3 use crate::core::DocContext;
4 use crate::fold::DocFolder;
5 use crate::html::markdown::opts;
7 use pulldown_cmark::{Event, LinkType, Parser, Tag};
9 use rustc_errors::Applicability;
10 use rustc_session::lint;
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",
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
25 struct NonAutolinksLinter<'a, 'tcx> {
26 cx: &'a DocContext<'tcx>,
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") }
39 f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
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();
47 "this URL is not a hyperlink",
49 Range { start: range.start + url_range.start, end: range.start + url_range.end },
55 crate fn check_non_autolinks(krate: Crate, cx: &DocContext<'_>) -> Crate {
56 if !cx.tcx.sess.is_nightly_build() {
59 let mut coll = NonAutolinksLinter::new(cx);
61 coll.fold_crate(krate)
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,
70 // If non-local, no need to check anything.
71 return Some(self.fold_item_recur(item));
74 let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
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| {
84 "use an automatic link instead",
86 Applicability::MachineApplicable,
92 let mut p = Parser::new_ext(&dox, opts()).into_offset_iter();
94 while let Some((event, range)) = p.next() {
96 Event::Start(Tag::Link(kind, _, _)) => {
97 let ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
98 let mut title = String::new();
100 while let Some((event, range)) = p.next() {
102 Event::End(Tag::Link(_, url, _)) => {
103 // NOTE: links cannot be nested, so we don't need to
105 if url.as_ref() == title && !ignore && self.regex.is_match(&url)
109 "unneeded long form for URL",
116 Event::Text(s) if !ignore => title.push_str(&s),
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() {
126 Event::End(Tag::CodeBlock(_)) => break,
136 Some(self.fold_item_recur(item))