diff --git a/Cargo.lock b/Cargo.lock index 3d40ded19dd..3a1dae971cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4297,6 +4297,7 @@ dependencies = [ "itertools 0.9.0", "minifier", "pulldown-cmark 0.8.0", + "regex", "rustc-rayon", "serde", "serde_json", diff --git a/src/doc/rustdoc/src/lints.md b/src/doc/rustdoc/src/lints.md index 2c10f6c06a9..a85aa882af8 100644 --- a/src/doc/rustdoc/src/lints.md +++ b/src/doc/rustdoc/src/lints.md @@ -294,6 +294,7 @@ which could use the "automatic" link syntax. For example: ```rust #![warn(automatic_links)] +/// http://hello.rs /// [http://a.com](http://a.com) /// [http://b.com] /// @@ -304,24 +305,27 @@ pub fn foo() {} Which will give: ```text -error: Unneeded long form for URL +warning: won't be a link as is --> foo.rs:3:5 | -3 | /// [http://a.com](http://a.com) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +3 | /// http://hello.rs + | ^^^^^^^^^^^^^^^ help: use an automatic link instead: `` | note: the lint level is defined here --> foo.rs:1:9 | -1 | #![deny(automatic_links)] +1 | #![warn(automatic_links)] | ^^^^^^^^^^^^^^^ - = help: Try with `` instead -error: Unneeded long form for URL +warning: unneeded long form for URL + --> foo.rs:4:5 + | +4 | /// [http://a.com](http://a.com) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +warning: unneeded long form for URL --> foo.rs:5:5 | 5 | /// [http://b.com] - | ^^^^^^^^^^^^^^ - | - = help: Try with `` instead + | ^^^^^^^^^^^^^^ help: use an automatic link instead: `` ``` diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml index a40a44fe27d..b0f5bac6abd 100644 --- a/src/librustdoc/Cargo.toml +++ b/src/librustdoc/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0" smallvec = "1.0" tempfile = "3" itertools = "0.9" +regex = "1" [dev-dependencies] expect-test = "1.0" diff --git a/src/librustdoc/passes/automatic_links.rs b/src/librustdoc/passes/automatic_links.rs index 79542241326..11c1a4d0bfb 100644 --- a/src/librustdoc/passes/automatic_links.rs +++ b/src/librustdoc/passes/automatic_links.rs @@ -3,23 +3,55 @@ use crate::clean::*; use crate::core::DocContext; use crate::fold::DocFolder; use crate::html::markdown::opts; -use pulldown_cmark::{Event, Parser, Tag}; +use core::ops::Range; +use pulldown_cmark::{Event, LinkType, Parser, Tag}; +use regex::Regex; +use rustc_errors::Applicability; use rustc_feature::UnstableFeatures; use rustc_session::lint; pub const CHECK_AUTOMATIC_LINKS: Pass = Pass { name: "check-automatic-links", run: check_automatic_links, - description: "detects URLS/email addresses that could be written using brackets", + description: "detects URLS/email addresses that could be written using angle brackets", }; +const URL_REGEX: &str = + r"https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)"; + struct AutomaticLinksLinter<'a, 'tcx> { cx: &'a DocContext<'tcx>, + regex: Regex, } impl<'a, 'tcx> AutomaticLinksLinter<'a, 'tcx> { fn new(cx: &'a DocContext<'tcx>) -> Self { - AutomaticLinksLinter { cx } + AutomaticLinksLinter { cx, regex: Regex::new(URL_REGEX).expect("failed to build regex") } + } + + fn find_raw_urls( + &self, + text: &str, + range: Range, + f: &impl Fn(&DocContext<'_>, &str, &str, Range), + ) { + for (pos, c) in text.char_indices() { + // For now, we only check "full" URLs. + if c == 'h' { + let text = &text[pos..]; + if text.starts_with("http://") || text.starts_with("https://") { + if let Some(m) = self.regex.find(text) { + let url = &text[..m.end()]; + f( + self.cx, + "won't be a link as is", + url, + Range { start: range.start + pos, end: range.start + pos + m.end() }, + ) + } + } + } + } } } @@ -44,45 +76,48 @@ impl<'a, 'tcx> DocFolder for AutomaticLinksLinter<'a, 'tcx> { }; let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); if !dox.is_empty() { - let cx = &self.cx; + let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range| { + let sp = super::source_span_for_markdown_range(cx, &dox, &range, &item.attrs) + .or_else(|| span_of_attrs(&item.attrs)) + .unwrap_or(item.source.span()); + cx.tcx.struct_span_lint_hir(lint::builtin::AUTOMATIC_LINKS, hir_id, sp, |lint| { + lint.build(msg) + .span_suggestion( + sp, + "use an automatic link instead", + format!("<{}>", url), + Applicability::MachineApplicable, + ) + .emit() + }); + }; let p = Parser::new_ext(&dox, opts()).into_offset_iter(); let mut title = String::new(); let mut in_link = false; + let mut ignore = false; for (event, range) in p { match event { - Event::Start(Tag::Link(..)) => in_link = true, + Event::Start(Tag::Link(kind, _, _)) => { + in_link = true; + ignore = matches!(kind, LinkType::Autolink | LinkType::Email); + } Event::End(Tag::Link(_, url, _)) => { in_link = false; - if url.as_ref() != title { - continue; + if url.as_ref() == title && !ignore { + report_diag(self.cx, "unneeded long form for URL", &url, range); } - let sp = match super::source_span_for_markdown_range( - cx, - &dox, - &range, - &item.attrs, - ) { - Some(sp) => sp, - None => span_of_attrs(&item.attrs).unwrap_or(item.source.span()), - }; - cx.tcx.struct_span_lint_hir( - lint::builtin::AUTOMATIC_LINKS, - hir_id, - sp, - |lint| { - lint.build("Unneeded long form for URL") - .help(&format!("Try with `<{}>` instead", url)) - .emit() - }, - ); title.clear(); + ignore = false; } Event::Text(s) if in_link => { - title.push_str(&s); + if !ignore { + title.push_str(&s); + } } + Event::Text(s) => self.find_raw_urls(&s, range, &report_diag), _ => {} } } diff --git a/src/test/rustdoc-ui/automatic-links.rs b/src/test/rustdoc-ui/automatic-links.rs index 9273b854aee..f9dbe67e5b1 100644 --- a/src/test/rustdoc-ui/automatic-links.rs +++ b/src/test/rustdoc-ui/automatic-links.rs @@ -1,15 +1,20 @@ #![deny(automatic_links)] /// [http://a.com](http://a.com) -//~^ ERROR Unneeded long form for URL +//~^ ERROR unneeded long form for URL /// [http://b.com] -//~^ ERROR Unneeded long form for URL +//~^ ERROR unneeded long form for URL /// /// [http://b.com]: http://b.com /// /// [http://c.com][http://c.com] pub fn a() {} +/// https://somewhere.com?hello=12 +//~^ ERROR won't be a link as is +pub fn c() {} + +/// /// [a](http://a.com) /// [b] /// diff --git a/src/test/rustdoc-ui/automatic-links.stderr b/src/test/rustdoc-ui/automatic-links.stderr index 2922fedb238..d2c0c51d7a4 100644 --- a/src/test/rustdoc-ui/automatic-links.stderr +++ b/src/test/rustdoc-ui/automatic-links.stderr @@ -1,23 +1,26 @@ -error: Unneeded long form for URL +error: unneeded long form for URL --> $DIR/automatic-links.rs:3:5 | LL | /// [http://a.com](http://a.com) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` | note: the lint level is defined here --> $DIR/automatic-links.rs:1:9 | LL | #![deny(automatic_links)] | ^^^^^^^^^^^^^^^ - = help: Try with `` instead -error: Unneeded long form for URL +error: unneeded long form for URL --> $DIR/automatic-links.rs:5:5 | LL | /// [http://b.com] - | ^^^^^^^^^^^^^^ + | ^^^^^^^^^^^^^^ help: use an automatic link instead: `` + +error: won't be a link as is + --> $DIR/automatic-links.rs:13:5 | - = help: Try with `` instead +LL | /// https://somewhere.com?hello=12 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `` -error: aborting due to 2 previous errors +error: aborting due to 3 previous errors