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