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_URL_IMPROVEMENTS: Pass = Pass {
14 name: "check-url-improvements",
15 run: check_url_improvements,
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,63}", // root domain
23 r"\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)" // optional query or url fragments
26 struct UrlImprovementsLinter<'a, 'tcx> {
27 cx: &'a DocContext<'tcx>,
31 impl<'a, 'tcx> UrlImprovementsLinter<'a, 'tcx> {
32 fn new(cx: &'a DocContext<'tcx>) -> Self {
33 UrlImprovementsLinter { 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_url_improvements(krate: Crate, cx: &DocContext<'_>) -> Crate {
57 if !UnstableFeatures::from_environment().is_nightly_build() {
60 let mut coll = UrlImprovementsLinter::new(cx);
62 coll.fold_crate(krate)
66 impl<'a, 'tcx> DocFolder for UrlImprovementsLinter<'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::URL_IMPROVEMENTS, hir_id, sp, |lint| {
85 "use an automatic link instead",
87 Applicability::MachineApplicable,
93 let mut p = Parser::new_ext(&dox, opts()).into_offset_iter();
95 while let Some((event, range)) = p.next() {
97 Event::Start(Tag::Link(kind, _, _)) => {
98 let ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
99 let mut title = String::new();
101 while let Some((event, range)) = p.next() {
103 Event::End(Tag::Link(_, url, _)) => {
104 // NOTE: links cannot be nested, so we don't need to
106 if url.as_ref() == title && !ignore && self.regex.matches(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 self.fold_item_recur(item)