rustdoc/passes/lint/
bare_urls.rs1use core::ops::Range;
5use std::sync::LazyLock;
6
7use regex::Regex;
8use rustc_errors::{Applicability, DiagDecorator};
9use rustc_hir::HirId;
10use rustc_resolve::rustdoc::pulldown_cmark::{
11 DefaultBrokenLinkCallback, Event, Tag, TextMergeWithOffset,
12};
13use rustc_resolve::rustdoc::source_span_for_markdown_range;
14use tracing::trace;
15
16use crate::clean::*;
17use crate::core::DocContext;
18use crate::html::markdown::main_body_opts;
19
20pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
21 let report_diag = |cx: &DocContext<'_>,
22 msg: &'static str,
23 range: Range<usize>,
24 without_brackets: Option<&str>| {
25 let maybe_sp = source_span_for_markdown_range(cx.tcx, dox, &range, &item.attrs.doc_strings)
26 .map(|(sp, _)| sp);
27 let sp = maybe_sp.unwrap_or_else(|| item.attr_span(cx.tcx));
28 cx.tcx.emit_node_span_lint(
29 crate::lint::BARE_URLS,
30 hir_id,
31 sp,
32 DiagDecorator(|lint| {
33 lint.primary_message(msg)
34 .note("bare URLs are not automatically turned into clickable links");
35 if let Some(sp) = maybe_sp {
38 if let Some(without_brackets) = without_brackets {
39 lint.multipart_suggestion(
40 "use an automatic link instead",
41 vec![(sp, format!("<{without_brackets}>"))],
42 Applicability::MachineApplicable,
43 );
44 } else {
45 lint.multipart_suggestion(
46 "use an automatic link instead",
47 vec![
48 (sp.shrink_to_lo(), "<".to_string()),
49 (sp.shrink_to_hi(), ">".to_string()),
50 ],
51 Applicability::MachineApplicable,
52 );
53 }
54 }
55 }),
56 );
57 };
58
59 let mut p = TextMergeWithOffset::<DefaultBrokenLinkCallback>::new_ext(dox, main_body_opts());
63
64 while let Some((event, range)) = p.next() {
65 match event {
66 Event::Text(s) => find_raw_urls(cx, dox, &s, range, &report_diag),
67 Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link { .. })) => {
69 let end = tag.to_end();
70 for (event, _) in p.by_ref() {
71 if matches!(event, Event::End(tag) if tag == end) {
72 break;
73 }
74 }
75 }
76 _ => {}
77 }
78 }
79}
80
81static URL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
82 Regex::new(concat!(
83 r"https?://", r"([-a-zA-Z0-9@:%._\+~#=]{2,256}\.)+", r"[a-zA-Z]{2,63}", r"\b(?:",
89 r"[-a-zA-Z0-9@:%_\+.~#?&/=]",
90 r"|\([-a-zA-Z0-9@:%_\+.~#?&/=]*\)",
91 r")*",
92 ))
93 .expect("failed to build regex")
94});
95
96fn find_raw_urls(
97 cx: &DocContext<'_>,
98 dox: &str,
99 text: &str,
100 range: Range<usize>,
101 f: &impl Fn(&DocContext<'_>, &'static str, Range<usize>, Option<&str>),
102) {
103 trace!("looking for raw urls in {text}");
104 for match_ in URL_REGEX.find_iter(text) {
106 let mut url_range = match_.range();
107 url_range.start += range.start;
108 url_range.end += range.start;
109 let mut without_brackets = None;
110 if dox[..url_range.start].ends_with('[')
113 && url_range.end <= dox.len()
114 && dox[url_range.end..].starts_with(']')
115 {
116 url_range.start -= 1;
117 url_range.end += 1;
118 without_brackets = Some(match_.as_str());
119 } else {
120 let trailing_periods =
125 match_.as_str().len() - match_.as_str().trim_end_matches('.').len();
126 url_range.end -= trailing_periods;
127 }
128 f(cx, "this URL is not a hyperlink", url_range, without_brackets);
129 }
130}