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_feature::UnstableFeatures;
11 use rustc_session::lint;
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",
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
26 struct AutomaticLinksLinter<'a, 'tcx> {
27 cx: &'a DocContext<'tcx>,
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") }
40 f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
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();
48 "this URL is not a hyperlink",
50 Range { start: range.start + url_range.start, end: range.start + url_range.end },
56 pub fn check_automatic_links(krate: Crate, cx: &DocContext<'_>) -> Crate {
57 if !UnstableFeatures::from_environment().is_nightly_build() {
60 let mut coll = AutomaticLinksLinter::new(cx);
62 coll.fold_crate(krate)
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,
71 // If non-local, no need to check anything.
72 return self.fold_item_recur(item);
75 let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
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| {
85 "use an automatic link instead",
87 Applicability::MachineApplicable,
93 let p = Parser::new_ext(&dox, opts()).into_offset_iter();
95 let mut title = String::new();
96 let mut in_link = false;
97 let mut ignore = false;
99 for (event, range) in p {
101 Event::Start(Tag::Link(kind, _, _)) => {
103 ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
105 Event::End(Tag::Link(_, url, _)) => {
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);
114 Event::Text(s) if in_link => {
119 Event::Text(s) => self.find_raw_urls(&s, range, &report_diag),
125 self.fold_item_recur(item)