1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use super::{span_of_attrs, Pass};
use crate::clean::*;
use crate::core::DocContext;
use crate::fold::DocFolder;
use crate::html::markdown::opts;
use core::ops::Range;
use pulldown_cmark::{Event, LinkType, Parser, Tag};
use regex::Regex;
use rustc_errors::Applicability;

crate const CHECK_NON_AUTOLINKS: Pass = Pass {
    name: "check-non-autolinks",
    run: check_non_autolinks,
    description: "detects URLs that could be linkified",
};

const URL_REGEX: &str = concat!(
    r"https?://",                          // url scheme
    r"([-a-zA-Z0-9@:%._\+~#=]{2,256}\.)+", // one or more subdomains
    r"[a-zA-Z]{2,63}",                     // root domain
    r"\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)"      // optional query or url fragments
);

struct NonAutolinksLinter<'a, 'tcx> {
    cx: &'a mut DocContext<'tcx>,
    regex: Regex,
}

impl<'a, 'tcx> NonAutolinksLinter<'a, 'tcx> {
    fn find_raw_urls(
        &self,
        text: &str,
        range: Range<usize>,
        f: &impl Fn(&DocContext<'_>, &str, &str, Range<usize>),
    ) {
        // For now, we only check "full" URLs (meaning, starting with "http://" or "https://").
        for match_ in self.regex.find_iter(&text) {
            let url = match_.as_str();
            let url_range = match_.range();
            f(
                self.cx,
                "this URL is not a hyperlink",
                url,
                Range { start: range.start + url_range.start, end: range.start + url_range.end },
            );
        }
    }
}

crate fn check_non_autolinks(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
    if !cx.tcx.sess.is_nightly_build() {
        krate
    } else {
        let mut coll =
            NonAutolinksLinter { cx, regex: Regex::new(URL_REGEX).expect("failed to build regex") };

        coll.fold_crate(krate)
    }
}

impl<'a, 'tcx> DocFolder for NonAutolinksLinter<'a, 'tcx> {
    fn fold_item(&mut self, item: Item) -> Option<Item> {
        let hir_id = match DocContext::as_local_hir_id(self.cx.tcx, item.def_id) {
            Some(hir_id) => hir_id,
            None => {
                // If non-local, no need to check anything.
                return Some(self.fold_item_recur(item));
            }
        };
        let dox = item.attrs.collapsed_doc_value().unwrap_or_default();
        if !dox.is_empty() {
            let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range<usize>| {
                let sp = super::source_span_for_markdown_range(cx.tcx, &dox, &range, &item.attrs)
                    .or_else(|| span_of_attrs(&item.attrs))
                    .unwrap_or(item.source.span());
                cx.tcx.struct_span_lint_hir(crate::lint::NON_AUTOLINKS, hir_id, sp, |lint| {
                    lint.build(msg)
                        .span_suggestion(
                            sp,
                            "use an automatic link instead",
                            format!("<{}>", url),
                            Applicability::MachineApplicable,
                        )
                        .emit()
                });
            };

            let mut p = Parser::new_ext(&dox, opts()).into_offset_iter();

            while let Some((event, range)) = p.next() {
                match event {
                    Event::Start(Tag::Link(kind, _, _)) => {
                        let ignore = matches!(kind, LinkType::Autolink | LinkType::Email);
                        let mut title = String::new();

                        while let Some((event, range)) = p.next() {
                            match event {
                                Event::End(Tag::Link(_, url, _)) => {
                                    // NOTE: links cannot be nested, so we don't need to
                                    // check `kind`
                                    if url.as_ref() == title && !ignore && self.regex.is_match(&url)
                                    {
                                        report_diag(
                                            self.cx,
                                            "unneeded long form for URL",
                                            &url,
                                            range,
                                        );
                                    }
                                    break;
                                }
                                Event::Text(s) if !ignore => title.push_str(&s),
                                _ => {}
                            }
                        }
                    }
                    Event::Text(s) => self.find_raw_urls(&s, range, &report_diag),
                    Event::Start(Tag::CodeBlock(_)) => {
                        // We don't want to check the text inside the code blocks.
                        while let Some((event, _)) = p.next() {
                            match event {
                                Event::End(Tag::CodeBlock(_)) => break,
                                _ => {}
                            }
                        }
                    }
                    _ => {}
                }
            }
        }

        Some(self.fold_item_recur(item))
    }
}