diff --git a/src/librustdoc/passes/lint/bare_urls.rs b/src/librustdoc/passes/lint/bare_urls.rs index 0928980e390a8..287a1f50b5aa1 100644 --- a/src/librustdoc/passes/lint/bare_urls.rs +++ b/src/librustdoc/passes/lint/bare_urls.rs @@ -2,13 +2,14 @@ //! Suggests wrapping the link with angle brackets: `Go to .` to linkify it. use core::ops::Range; -use std::mem; use std::sync::LazyLock; use regex::Regex; use rustc_errors::{Applicability, DiagDecorator}; use rustc_hir::HirId; -use rustc_resolve::rustdoc::pulldown_cmark::{Event, Parser, Tag}; +use rustc_resolve::rustdoc::pulldown_cmark::{ + DefaultBrokenLinkCallback, Event, Tag, TextMergeWithOffset, +}; use rustc_resolve::rustdoc::source_span_for_markdown_range; use tracing::trace; @@ -55,21 +56,20 @@ pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & ); }; - let mut p = Parser::new_ext(dox, main_body_opts()).into_offset_iter(); + // pulldown-cmark can split a URL into multiple `Text` events while processing + // characters such as `_` according to CommonMark's emphasis rules. + // `TextMergeWithOffset` merges these events so we can check the complete URL. + let mut p = TextMergeWithOffset::::new_ext(dox, main_body_opts()); while let Some((event, range)) = p.next() { match event { Event::Text(s) => find_raw_urls(cx, dox, &s, range, &report_diag), // We don't want to check the text inside code blocks or links. Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link { .. })) => { + let end = tag.to_end(); for (event, _) in p.by_ref() { - match event { - Event::End(end) - if mem::discriminant(&end) == mem::discriminant(&tag.to_end()) => - { - break; - } - _ => {} + if matches!(event, Event::End(tag) if tag == end) { + break; } } } @@ -83,7 +83,12 @@ static URL_REGEX: LazyLock = LazyLock::new(|| { 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 + // Match URL characters and balanced parenthesized segments, without + // consuming a trailing `)` that belongs to the surrounding prose. + r"\b(?:", + r"[-a-zA-Z0-9@:%_\+.~#?&/=]", + r"|\([-a-zA-Z0-9@:%_\+.~#?&/=]*\)", + r")*", )) .expect("failed to build regex") }); diff --git a/tests/rustdoc-ui/lints/bare-urls.fixed b/tests/rustdoc-ui/lints/bare-urls.fixed index 996214b5ff14f..b18aae11c77cf 100644 --- a/tests/rustdoc-ui/lints/bare-urls.fixed +++ b/tests/rustdoc-ui/lints/bare-urls.fixed @@ -92,3 +92,7 @@ pub fn trailing_period() {} /// ] //~^ ERROR this URL is not a hyperlink pub fn lint_with_brackets() {} + +/// See +//~^ ERROR this URL is not a hyperlink +pub fn hippo() {} diff --git a/tests/rustdoc-ui/lints/bare-urls.rs b/tests/rustdoc-ui/lints/bare-urls.rs index 9b4fe68e00322..fb39ec6b6ccbd 100644 --- a/tests/rustdoc-ui/lints/bare-urls.rs +++ b/tests/rustdoc-ui/lints/bare-urls.rs @@ -92,3 +92,7 @@ pub fn trailing_period() {} /// https://bloob.blob] //~^ ERROR this URL is not a hyperlink pub fn lint_with_brackets() {} + +/// See https://en.wikipedia.org/wiki/Rust_(programming_language) +//~^ ERROR this URL is not a hyperlink +pub fn hippo() {} diff --git a/tests/rustdoc-ui/lints/bare-urls.stderr b/tests/rustdoc-ui/lints/bare-urls.stderr index 05ddd2ed42ab1..a3a291e8e4bca 100644 --- a/tests/rustdoc-ui/lints/bare-urls.stderr +++ b/tests/rustdoc-ui/lints/bare-urls.stderr @@ -364,5 +364,17 @@ help: use an automatic link instead LL | /// ] | + + -error: aborting due to 30 previous errors +error: this URL is not a hyperlink + --> $DIR/bare-urls.rs:96:9 + | +LL | /// See https://en.wikipedia.org/wiki/Rust_(programming_language) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: bare URLs are not automatically turned into clickable links +help: use an automatic link instead + | +LL | /// See + | + + + +error: aborting due to 31 previous errors