Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 39 additions & 18 deletions src/librustdoc/passes/lint/bare_urls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ use crate::core::DocContext;
use crate::html::markdown::main_body_opts;

pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
let report_diag = |cx: &DocContext<'_>, msg: &'static str, range: Range<usize>| {
let report_diag = |cx: &DocContext<'_>,
msg: &'static str,
range: Range<usize>,
without_brackets: Option<&str>| {
let maybe_sp = source_span_for_markdown_range(cx.tcx, dox, &range, &item.attrs.doc_strings)
.map(|(sp, _)| sp);
let sp = maybe_sp.unwrap_or_else(|| item.attr_span(cx.tcx));
Expand All @@ -27,14 +30,22 @@ pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &
// The fallback of using the attribute span is suitable for
// highlighting where the error is, but not for placing the < and >
if let Some(sp) = maybe_sp {
lint.multipart_suggestion(
"use an automatic link instead",
vec![
(sp.shrink_to_lo(), "<".to_string()),
(sp.shrink_to_hi(), ">".to_string()),
],
Applicability::MachineApplicable,
);
if let Some(without_brackets) = without_brackets {
lint.multipart_suggestion(
"use an automatic link instead",
vec![(sp, format!("<{without_brackets}>"))],
Applicability::MachineApplicable,
);
} else {
lint.multipart_suggestion(
"use an automatic link instead",
vec![
(sp.shrink_to_lo(), "<".to_string()),
(sp.shrink_to_hi(), ">".to_string()),
],
Applicability::MachineApplicable,
);
}
}
});
};
Expand All @@ -43,7 +54,7 @@ pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &

while let Some((event, range)) = p.next() {
match event {
Event::Text(s) => find_raw_urls(cx, &s, range, &report_diag),
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 { .. })) => {
for (event, _) in p.by_ref() {
Expand All @@ -67,25 +78,35 @@ static URL_REGEX: LazyLock<Regex> = 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
r"\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)", // optional query or url fragments
))
.expect("failed to build regex")
});

fn find_raw_urls(
cx: &DocContext<'_>,
dox: &str,
text: &str,
range: Range<usize>,
f: &impl Fn(&DocContext<'_>, &'static str, Range<usize>),
f: &impl Fn(&DocContext<'_>, &'static str, Range<usize>, Option<&str>),
) {
trace!("looking for raw urls in {text}");
// For now, we only check "full" URLs (meaning, starting with "http://" or "https://").
for match_ in URL_REGEX.find_iter(text) {
let url_range = match_.range();
f(
cx,
"this URL is not a hyperlink",
Range { start: range.start + url_range.start, end: range.start + url_range.end },
);
let mut url_range = match_.range();
url_range.start += range.start;
url_range.end += range.start;
let mut without_brackets = None;
// If the link is contained inside `[]`, then we need to replace the brackets and
// not just add `<>`.
if dox[..url_range.start].ends_with('[')
&& url_range.end <= dox.len()
&& dox[url_range.end..].starts_with(']')
{
url_range.start -= 1;
url_range.end += 1;
without_brackets = Some(match_.as_str());
}
f(cx, "this URL is not a hyperlink", url_range, without_brackets);
}
}
14 changes: 14 additions & 0 deletions tests/rustdoc-ui/lints/bare-urls.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,17 @@ pub mod foo {
/// https://somewhere.com/a?hello=12&bye=11#xyz
pub fn bar() {}
}

/// <https://bloob.blob>
//~^ ERROR this URL is not a hyperlink
/// [ <https://bloob.blob> ]
//~^ ERROR this URL is not a hyperlink
/// [ <https://bloob.blob>]
//~^ ERROR this URL is not a hyperlink
/// [<https://bloob.blob> ]
//~^ ERROR this URL is not a hyperlink
/// [<https://bloob.blob>
//~^ ERROR this URL is not a hyperlink
/// <https://bloob.blob>]
//~^ ERROR this URL is not a hyperlink
pub fn lint_with_brackets() {}
14 changes: 14 additions & 0 deletions tests/rustdoc-ui/lints/bare-urls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,17 @@ pub mod foo {
/// https://somewhere.com/a?hello=12&bye=11#xyz
pub fn bar() {}
}

/// [https://bloob.blob]
//~^ ERROR this URL is not a hyperlink
/// [ https://bloob.blob ]
//~^ ERROR this URL is not a hyperlink
/// [ https://bloob.blob]
//~^ ERROR this URL is not a hyperlink
/// [https://bloob.blob ]
//~^ ERROR this URL is not a hyperlink
/// [https://bloob.blob
//~^ ERROR this URL is not a hyperlink
/// https://bloob.blob]
//~^ ERROR this URL is not a hyperlink
pub fn lint_with_brackets() {}
70 changes: 69 additions & 1 deletion tests/rustdoc-ui/lints/bare-urls.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -243,5 +243,73 @@ help: use an automatic link instead
LL | #[doc = "<https://example.com/raw>"]
| + +

error: aborting due to 20 previous errors
error: this URL is not a hyperlink
--> $DIR/bare-urls.rs:72:5
|
LL | /// [https://bloob.blob]
| ^^^^^^^^^^^^^^^^^^^^ help: use an automatic link instead: `<https://bloob.blob>`
|
= note: bare URLs are not automatically turned into clickable links

error: this URL is not a hyperlink
--> $DIR/bare-urls.rs:74:7
|
LL | /// [ https://bloob.blob ]
| ^^^^^^^^^^^^^^^^^^
|
= note: bare URLs are not automatically turned into clickable links
help: use an automatic link instead
|
LL | /// [ <https://bloob.blob> ]
| + +

error: this URL is not a hyperlink
--> $DIR/bare-urls.rs:76:7
|
LL | /// [ https://bloob.blob]
| ^^^^^^^^^^^^^^^^^^
|
= note: bare URLs are not automatically turned into clickable links
help: use an automatic link instead
|
LL | /// [ <https://bloob.blob>]
| + +

error: this URL is not a hyperlink
--> $DIR/bare-urls.rs:78:6
|
LL | /// [https://bloob.blob ]
| ^^^^^^^^^^^^^^^^^^
|
= note: bare URLs are not automatically turned into clickable links
help: use an automatic link instead
|
LL | /// [<https://bloob.blob> ]
| + +

error: this URL is not a hyperlink
--> $DIR/bare-urls.rs:80:6
|
LL | /// [https://bloob.blob
| ^^^^^^^^^^^^^^^^^^
|
= note: bare URLs are not automatically turned into clickable links
help: use an automatic link instead
|
LL | /// [<https://bloob.blob>
| + +

error: this URL is not a hyperlink
--> $DIR/bare-urls.rs:82:5
|
LL | /// https://bloob.blob]
| ^^^^^^^^^^^^^^^^^^
|
= note: bare URLs are not automatically turned into clickable links
help: use an automatic link instead
|
LL | /// <https://bloob.blob>]
| + +

error: aborting due to 26 previous errors

Loading