From 84f289674c391d8a049373a3c1ea46759be2cdaf Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 6 Jun 2026 22:19:12 -0700 Subject: [PATCH 01/15] rustdoc: do not include extra stuff in span This change prevents our lints from returning a span with stuff in it that isn't actually part of the doc comment. When that happens, it returns `None` instead. --- compiler/rustc_resolve/src/rustdoc.rs | 67 ++++++++++++------- .../lints/invalid-html-tags-ice-146890.rs | 8 +-- .../lints/invalid-html-tags-ice-146890.stderr | 2 - tests/rustdoc-ui/lints/invalid-html-tags.rs | 2 +- .../rustdoc-ui/lints/invalid-html-tags.stderr | 1 - .../lints/redundant_explicit_links_split.rs | 43 ++++++++++++ .../redundant_explicit_links_split.stderr | 45 +++++++++++++ 7 files changed, 137 insertions(+), 31 deletions(-) create mode 100644 tests/rustdoc-ui/lints/redundant_explicit_links_split.rs create mode 100644 tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 92bc577f59201..fdfe5afb7d7ec 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::mem; use std::ops::Range; @@ -641,41 +642,61 @@ pub fn source_span_for_markdown_range_inner( let mut start_bytes = 0; let mut end_bytes = 0; + let span = span_of_fragments(fragments)?; + + let mut line_bytes = 0; + let mut sorted_fragments = fragments.to_vec(); + sorted_fragments.sort_by_key(|fragment| fragment.span.lo().0); 'outer: for (line_no, md_line) in md_lines.enumerate() { loop { let source_line = src_lines.next()?; - match source_line.find(md_line) { - Some(offset) => { - if line_no == starting_line { - start_bytes += offset; + // Since we're counting bytes, `source_line_len` includes the "\n". + let source_line_len = u32::try_from(source_line.len() + 1).unwrap(); + let has_fragment = sorted_fragments + .binary_search_by(|fragment| { + if fragment.span.hi().0 < span.lo().0 + line_bytes { + Ordering::Less + } else if fragment.span.lo().0 > span.lo().0 + line_bytes + source_line_len { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .is_ok(); + line_bytes += source_line_len; + if has_fragment && let Some(offset) = source_line.find(md_line) { + if line_no == starting_line { + start_bytes += offset; - if starting_line == ending_line { - break 'outer; - } - } else if line_no == ending_line { - end_bytes += offset; + if starting_line == ending_line { break 'outer; - } else if line_no < starting_line { - start_bytes += source_line.len() - md_line.len(); - } else { - end_bytes += source_line.len() - md_line.len(); } - break; + } else if line_no == ending_line { + end_bytes += offset; + break 'outer; + } else if line_no < starting_line { + start_bytes += source_line.len() - md_line.len(); + } else { + end_bytes += source_line.len() - md_line.len(); } - None => { - // Since this is a source line that doesn't include a markdown line, - // we have to count the newline that we split from earlier. - if line_no <= starting_line { - start_bytes += source_line.len() + 1; - } else { - end_bytes += source_line.len() + 1; - } + break; + } else { + // Since this is a source line that doesn't include a markdown line, + // we have to count the newline that we split from earlier. + if line_no <= starting_line { + start_bytes += source_line.len() + 1; + } else if source_line.chars().any(|c| !c.is_whitespace()) { + // If we're past the first line, but haven't found the last line, + // we can only return a contiguous span if every line is either + // part of the doc comment or blank. + return None; + } else { + end_bytes += source_line.len() + 1; } } } } - let span = span_of_fragments(fragments)?; let src_span = span.from_inner(InnerSpan::new( md_range.start + start_bytes, md_range.end + start_bytes + end_bytes, diff --git a/tests/rustdoc-ui/lints/invalid-html-tags-ice-146890.rs b/tests/rustdoc-ui/lints/invalid-html-tags-ice-146890.rs index d7efc201e7e39..53df3e9eb5a80 100644 --- a/tests/rustdoc-ui/lints/invalid-html-tags-ice-146890.rs +++ b/tests/rustdoc-ui/lints/invalid-html-tags-ice-146890.rs @@ -7,17 +7,17 @@ /// /// key +//~^^ ERROR: unclosed HTML tag `TH` /// +//~^^ ERROR: unopened HTML tag `TD` /// value +//~^^ ERROR: unclosed HTML tag `TH` /// +//~^^ ERROR: unopened HTML tag `TD` /// /// | |_____^ | @@ -18,7 +17,6 @@ error: unopened HTML tag `TD` | LL | /// | |_____^ diff --git a/tests/rustdoc-ui/lints/invalid-html-tags.rs b/tests/rustdoc-ui/lints/invalid-html-tags.rs index 8003e5efdd582..f5700e5846e51 100644 --- a/tests/rustdoc-ui/lints/invalid-html-tags.rs +++ b/tests/rustdoc-ui/lints/invalid-html-tags.rs @@ -164,8 +164,8 @@ pub fn r() {} /// >
/// > href="#broken" +//~^^ ERROR incomplete HTML tag `img` pub fn s() {} ///
diff --git a/tests/rustdoc-ui/lints/invalid-html-tags.stderr b/tests/rustdoc-ui/lints/invalid-html-tags.stderr index b6ec22c247901..3256004ddb6ab 100644 --- a/tests/rustdoc-ui/lints/invalid-html-tags.stderr +++ b/tests/rustdoc-ui/lints/invalid-html-tags.stderr @@ -123,7 +123,6 @@ error: incomplete HTML tag `img` | LL | /// > href="#broken" | |____________________^ diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs b/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs new file mode 100644 index 0000000000000..9de8bbe3b8c98 --- /dev/null +++ b/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs @@ -0,0 +1,43 @@ +#![deny(rustdoc::redundant_explicit_links)] + +// Right now, redundant_explicit_links won't produce a warning at all if +// rustdoc isn't able to calculate an accurate span for the link. +// +// If that changes and this test starts to fail, you should add the +// appropriate annotations, and, also, make sure that it doesn't +// suggest a correction that wipes out the `fn` formal signature +// or the `#[inline]` attribute. + +/// [std::clone::Clone]( +pub fn split_outer_inner() { + //! std::clone::Clone) +} + +/// [std::clone::Clone](std::clone::Clone +pub fn split_outer_inner_b() { + //! ) +} + +/// [std::clone::Clone]( +#[inline] +/// std::clone::Clone) +pub fn split_attr() { +} + +/// [std::clone::Clone](std::clone::Clone +#[inline] +/// ) +pub fn split_attr_b() { +} + +/// [std::clone::Clone]( +/// std::clone::Clone) +//~^^ ERROR redundant_explicit_links +pub fn not_split() { +} + +/// [std::clone::Clone](std::clone::Clone +/// ) +//~^^ ERROR redundant_explicit_links +pub fn not_split_b() { +} diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr b/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr new file mode 100644 index 0000000000000..3e2e99f10d9f7 --- /dev/null +++ b/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr @@ -0,0 +1,45 @@ +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:33:25 + | +LL | /// [std::clone::Clone]( + | ______-----------------__^ + | | | + | | because label contains path that resolves to same destination +LL | | /// std::clone::Clone) + | |_____________________^ explicit target is redundant + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links +note: the lint level is defined here + --> $DIR/redundant_explicit_links_split.rs:1:9 + | +LL | #![deny(rustdoc::redundant_explicit_links)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: remove explicit link target + | +LL - /// [std::clone::Clone]( +LL - /// std::clone::Clone) +LL + /// [std::clone::Clone] + | + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:39:25 + | +LL | /// [std::clone::Clone](std::clone::Clone + | ______-----------------__^ + | | | + | | because label contains path that resolves to same destination +LL | | /// ) + | |____^ explicit target is redundant + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links +help: remove explicit link target + | +LL - /// [std::clone::Clone](std::clone::Clone +LL - /// ) +LL + /// [std::clone::Clone] + | + +error: aborting due to 2 previous errors + From 2001b88a962332f3799d3719867812900561d5bb Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sun, 7 Jun 2026 17:41:40 -0700 Subject: [PATCH 02/15] Fix `unbalanced_ticks` when doc isn't contiguous --- src/tools/clippy/clippy_lints/src/doc/mod.rs | 13 +++---------- .../clippy/tests/ui/doc/unbalanced_ticks.stderr | 5 ++--- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/tools/clippy/clippy_lints/src/doc/mod.rs b/src/tools/clippy/clippy_lints/src/doc/mod.rs index c5816cb8b2d63..013fb32bdaf3c 100644 --- a/src/tools/clippy/clippy_lints/src/doc/mod.rs +++ b/src/tools/clippy/clippy_lints/src/doc/mod.rs @@ -1242,7 +1242,8 @@ fn check_doc<'a, Events: Iterator, Range, Range tests/ui/doc/unbalanced_ticks.rs:6:5 + --> tests/ui/doc/unbalanced_ticks.rs:6:1 | -LL | /// This is a doc comment with `unbalanced_tick marks and several words that - | _____^ +LL | / /// This is a doc comment with `unbalanced_tick marks and several words that LL | | LL | | /// should be `encompassed_by` tick marks because they `contain_underscores`. LL | | /// Because of the initial `unbalanced_tick` pair, the error message is From e66a50496cf5e996bb354f61508db540d62eb51a Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sun, 7 Jun 2026 17:46:08 -0700 Subject: [PATCH 03/15] Add test case for split missing punctuation --- ...doc_paragraph_missing_punctuation_split_16169.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/tools/clippy/tests/ui/doc/doc_paragraph_missing_punctuation_split_16169.rs diff --git a/src/tools/clippy/tests/ui/doc/doc_paragraph_missing_punctuation_split_16169.rs b/src/tools/clippy/tests/ui/doc/doc_paragraph_missing_punctuation_split_16169.rs new file mode 100644 index 0000000000000..c01b0c90b120e --- /dev/null +++ b/src/tools/clippy/tests/ui/doc/doc_paragraph_missing_punctuation_split_16169.rs @@ -0,0 +1,13 @@ +//@ check-pass +// https://github.com/rust-lang/rust-clippy/issues/16169 +#![allow(clippy::mixed_attributes_style)] + +/// +pub fn dont_warn_inner_outer() { + //!w +} +/// +#[inline] +///w +pub fn dont_warn_split_by_attr() { +} From f7302b9723082a9310e901b58eae530363571d96 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 13 Jun 2026 09:47:29 -0700 Subject: [PATCH 04/15] Skip the unhelpful binary search It requires an allocation, and doesn't seem to help in practice. Fixes a nit found during review. --- compiler/rustc_resolve/src/rustdoc.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index fdfe5afb7d7ec..8efad0a343ac5 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -1,4 +1,3 @@ -use std::cmp::Ordering; use std::mem; use std::ops::Range; @@ -645,24 +644,15 @@ pub fn source_span_for_markdown_range_inner( let span = span_of_fragments(fragments)?; let mut line_bytes = 0; - let mut sorted_fragments = fragments.to_vec(); - sorted_fragments.sort_by_key(|fragment| fragment.span.lo().0); 'outer: for (line_no, md_line) in md_lines.enumerate() { loop { let source_line = src_lines.next()?; // Since we're counting bytes, `source_line_len` includes the "\n". let source_line_len = u32::try_from(source_line.len() + 1).unwrap(); - let has_fragment = sorted_fragments - .binary_search_by(|fragment| { - if fragment.span.hi().0 < span.lo().0 + line_bytes { - Ordering::Less - } else if fragment.span.lo().0 > span.lo().0 + line_bytes + source_line_len { - Ordering::Greater - } else { - Ordering::Equal - } - }) - .is_ok(); + let has_fragment = fragments.iter().any(|fragment| { + fragment.span.hi().0 >= span.lo().0 + line_bytes + && fragment.span.lo().0 <= span.lo().0 + line_bytes + source_line_len + }); line_bytes += source_line_len; if has_fragment && let Some(offset) = source_line.find(md_line) { if line_no == starting_line { From b66a23fb4b2f9cff20ea6f8b34d223e4073a99d8 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 17 Jun 2026 17:58:10 -0700 Subject: [PATCH 05/15] Rename `span` and `line_bytes` to more specific Co-authored-by: binarycat --- compiler/rustc_resolve/src/rustdoc.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 8efad0a343ac5..9abb79d5ca02f 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -641,19 +641,20 @@ pub fn source_span_for_markdown_range_inner( let mut start_bytes = 0; let mut end_bytes = 0; - let span = span_of_fragments(fragments)?; + let span_of_all_fragments = span_of_fragments(fragments)?; - let mut line_bytes = 0; + let mut prev_lines_bytes = 0; 'outer: for (line_no, md_line) in md_lines.enumerate() { loop { let source_line = src_lines.next()?; // Since we're counting bytes, `source_line_len` includes the "\n". let source_line_len = u32::try_from(source_line.len() + 1).unwrap(); let has_fragment = fragments.iter().any(|fragment| { - fragment.span.hi().0 >= span.lo().0 + line_bytes - && fragment.span.lo().0 <= span.lo().0 + line_bytes + source_line_len + fragment.span.hi().0 >= span_of_all_fragments.lo().0 + prev_lines_bytes + && fragment.span.lo().0 + <= span_of_all_fragments.lo().0 + prev_lines_bytes + source_line_len }); - line_bytes += source_line_len; + prev_lines_bytes += source_line_len; if has_fragment && let Some(offset) = source_line.find(md_line) { if line_no == starting_line { start_bytes += offset; @@ -687,7 +688,7 @@ pub fn source_span_for_markdown_range_inner( } } - let src_span = span.from_inner(InnerSpan::new( + let src_span = span_of_all_fragments.from_inner(InnerSpan::new( md_range.start + start_bytes, md_range.end + start_bytes + end_bytes, )); From f93f2e446b457c871510340c22a4a68192ae48d1 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 17 Jun 2026 18:07:20 -0700 Subject: [PATCH 06/15] Use an intermediate variable for readability Co-authored-by: binarycat --- compiler/rustc_resolve/src/rustdoc.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 9abb79d5ca02f..4cae23ed7e12a 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -641,20 +641,20 @@ pub fn source_span_for_markdown_range_inner( let mut start_bytes = 0; let mut end_bytes = 0; - let span_of_all_fragments = span_of_fragments(fragments)?; + let span_of_all_fragments: Span = span_of_fragments(fragments)?; let mut prev_lines_bytes = 0; 'outer: for (line_no, md_line) in md_lines.enumerate() { loop { let source_line = src_lines.next()?; - // Since we're counting bytes, `source_line_len` includes the "\n". - let source_line_len = u32::try_from(source_line.len() + 1).unwrap(); - let has_fragment = fragments.iter().any(|fragment| { - fragment.span.hi().0 >= span_of_all_fragments.lo().0 + prev_lines_bytes - && fragment.span.lo().0 - <= span_of_all_fragments.lo().0 + prev_lines_bytes + source_line_len - }); - prev_lines_bytes += source_line_len; + let source_line_len = u32::try_from(source_line.len()).unwrap(); + let source_line_span = + span_of_all_fragments.split_at(prev_lines_bytes).1.split_at(source_line_len).0; + let has_fragment = fragments + .iter() + .any(|fragment| fragment.span.contains(source_line_span.shrink_to_hi())); + // Since we're counting bytes, `prev_line_bytes` includes the "\n". + prev_lines_bytes += source_line_len + 1; if has_fragment && let Some(offset) = source_line.find(md_line) { if line_no == starting_line { start_bytes += offset; From 7975dc779706cb384101867ffa9008816ed01d7e Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 17 Jun 2026 19:19:02 -0700 Subject: [PATCH 07/15] Add clarifying comment Co-authored-by: binarycat --- compiler/rustc_resolve/src/rustdoc.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 4cae23ed7e12a..42a5005552825 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -673,13 +673,14 @@ pub fn source_span_for_markdown_range_inner( break; } else { // Since this is a source line that doesn't include a markdown line, - // we have to count the newline that we split from earlier. + // we have to count it and its newline as non-markdown bytes. if line_no <= starting_line { start_bytes += source_line.len() + 1; } else if source_line.chars().any(|c| !c.is_whitespace()) { - // If we're past the first line, but haven't found the last line, - // we can only return a contiguous span if every line is either - // part of the doc comment or blank. + // We're past the first line, but haven't found the last line, + // but we found a non-empty non-markdown line. + // This could be an attribute, and we don't want a diagnostic + // suggesting to delete that attribute, so we return None to be safe. return None; } else { end_bytes += source_line.len() + 1; From 4d83004499d40452cef97b9cd103f53cb98fccd2 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 17 Jun 2026 19:34:27 -0700 Subject: [PATCH 08/15] Remove unused `Option` return --- src/librustdoc/passes/lint/redundant_explicit_links.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/librustdoc/passes/lint/redundant_explicit_links.rs b/src/librustdoc/passes/lint/redundant_explicit_links.rs index 8da21f100c6a3..8ccf819fff987 100644 --- a/src/librustdoc/passes/lint/redundant_explicit_links.rs +++ b/src/librustdoc/passes/lint/redundant_explicit_links.rs @@ -80,7 +80,7 @@ fn check_redundant_explicit_link<'md>( hir_id: HirId, doc: &'md str, resolutions: &DocLinkResMap, -) -> Option<()> { +) { let mut broken_line_callback = |link: BrokenLink<'md>| Some((link.reference, "".into())); let mut offset_iter = Parser::new_with_broken_link_callback( doc, @@ -146,8 +146,6 @@ fn check_redundant_explicit_link<'md>( } } } - - None } /// FIXME(ChAoSUnItY): Too many arguments. From 9438aac0c297bbc8804c0e6796c24637df1f6e99 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 17 Jun 2026 20:15:28 -0700 Subject: [PATCH 09/15] Handle span failure in redundant_explicit_links --- .../passes/lint/redundant_explicit_links.rs | 182 +++++++++++++----- .../lints/redundant_explicit_links_split.rs | 27 ++- .../redundant_explicit_links_split.stderr | 74 +++++-- 3 files changed, 205 insertions(+), 78 deletions(-) diff --git a/src/librustdoc/passes/lint/redundant_explicit_links.rs b/src/librustdoc/passes/lint/redundant_explicit_links.rs index 8ccf819fff987..ad45b6e1d1c5b 100644 --- a/src/librustdoc/passes/lint/redundant_explicit_links.rs +++ b/src/librustdoc/passes/lint/redundant_explicit_links.rs @@ -115,7 +115,7 @@ fn check_redundant_explicit_link<'md>( } if dest_url.ends_with(resolvable_link) || resolvable_link.ends_with(&*dest_url) { - match link_type { + let check_result = match link_type { LinkType::Inline | LinkType::ReferenceUnknown => { check_inline_or_reference_unknown_redundancy( cx, @@ -127,27 +127,54 @@ fn check_redundant_explicit_link<'md>( dest_url.to_string(), link_data, if link_type == LinkType::Inline { (b'(', b')') } else { (b'[', b']') }, - ); + ) } - LinkType::Reference => { - check_reference_redundancy( - cx, - item, - hir_id, - doc, - resolutions, - link_range, - &dest_url, - link_data, - ); - } - _ => {} + LinkType::Reference => check_reference_redundancy( + cx, + item, + hir_id, + doc, + resolutions, + link_range, + &dest_url, + link_data, + ), + _ => Ok(()), + }; + if let Err(lint) = check_result { + cx.tcx.emit_node_span_lint( + crate::lint::REDUNDANT_EXPLICIT_LINKS, + hir_id, + item.attr_span(cx.tcx), + lint, + ); } } } } } +struct RedundantExplicitLinksCannotSuggest { + attr_span: Span, + display_link: String, + dest_link: String, +} + +impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinksCannotSuggest { + fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> { + let Self { attr_span, display_link, dest_link } = self; + + Diag::new(dcx, level, "redundant explicit link target") + .with_span_label( + attr_span, + format!("explicit target `{dest_link}` is redundant because label `{display_link}` resolves to same destination") + ) + .with_note( + "when a link's destination is not specified,\nthe label is used to resolve intra-doc links" + ) + } +} + /// FIXME(ChAoSUnItY): Too many arguments. fn check_inline_or_reference_unknown_redundancy( cx: &DocContext<'_>, @@ -159,7 +186,7 @@ fn check_inline_or_reference_unknown_redundancy( dest: String, link_data: LinkData, (open, close): (u8, u8), -) -> Option<()> { +) -> Result<(), RedundantExplicitLinksCannotSuggest> { struct RedundantExplicitLinks { explicit_span: Span, display_span: Span, @@ -194,42 +221,65 @@ fn check_inline_or_reference_unknown_redundancy( } } - let (resolvable_link, resolvable_link_range) = - (&link_data.resolvable_link?, &link_data.resolvable_link_range?); - let (dest_res, display_res) = - (find_resolution(resolutions, &dest)?, find_resolution(resolutions, resolvable_link)?); + let (Some(resolvable_link), Some(resolvable_link_range)) = + (&link_data.resolvable_link, &link_data.resolvable_link_range) + else { + return Ok(()); + }; + let (Some(dest_res), Some(display_res)) = + (find_resolution(resolutions, &dest), find_resolution(resolutions, resolvable_link)) + else { + return Ok(()); + }; if dest_res == display_res { + let attr_span = item.attr_span(cx.tcx); let link_span = match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings) { Some((sp, from_expansion)) => { if from_expansion { - return None; + return Ok(()); } sp } - None => item.attr_span(cx.tcx), + None => attr_span, }; - let (explicit_span, false) = source_span_for_markdown_range( + let explicit_span = match source_span_for_markdown_range( cx.tcx, doc, &offset_explicit_range(doc, link_range, open, close), &item.attrs.doc_strings, - )? - else { + ) { + Some((explicit_span, false)) => explicit_span, // This `span` comes from macro expansion so skipping it. - return None; + Some((_, true)) => return Ok(()), + // Cannot give a contiguous span for this link. + None => { + return Err(RedundantExplicitLinksCannotSuggest { + display_link: resolvable_link.clone(), + dest_link: dest.to_string(), + attr_span, + }); + } }; - let (display_span, false) = source_span_for_markdown_range( + let display_span = match source_span_for_markdown_range( cx.tcx, doc, resolvable_link_range, &item.attrs.doc_strings, - )? - else { + ) { + Some((display_span, false)) => display_span, // This `span` comes from macro expansion so skipping it. - return None; + Some((_, true)) => return Ok(()), + // Cannot give a contiguous span for this link. + None => { + return Err(RedundantExplicitLinksCannotSuggest { + display_link: resolvable_link.clone(), + dest_link: dest.to_string(), + attr_span, + }); + } }; cx.tcx.emit_node_span_lint( @@ -245,7 +295,7 @@ fn check_inline_or_reference_unknown_redundancy( ); } - None + Ok(()) } /// FIXME(ChAoSUnItY): Too many arguments. @@ -258,7 +308,7 @@ fn check_reference_redundancy( link_range: Range, dest: &CowStr<'_>, link_data: LinkData, -) -> Option<()> { +) -> Result<(), RedundantExplicitLinksCannotSuggest> { struct RedundantExplicitLinkTarget { explicit_span: Span, display_span: Span, @@ -292,49 +342,83 @@ fn check_reference_redundancy( } } - let (resolvable_link, resolvable_link_range) = - (&link_data.resolvable_link?, &link_data.resolvable_link_range?); - let (dest_res, display_res) = - (find_resolution(resolutions, dest)?, find_resolution(resolutions, resolvable_link)?); + let (Some(resolvable_link), Some(resolvable_link_range)) = + (&link_data.resolvable_link, &link_data.resolvable_link_range) + else { + return Ok(()); + }; + let (Some(dest_res), Some(display_res)) = + (find_resolution(resolutions, dest), find_resolution(resolutions, resolvable_link)) + else { + return Ok(()); + }; if dest_res == display_res { + let attr_span = item.attr_span(cx.tcx); let link_span = match source_span_for_markdown_range(cx.tcx, doc, &link_range, &item.attrs.doc_strings) { Some((sp, from_expansion)) => { if from_expansion { - return None; + // This `span` comes from macro expansion so skipping it. + return Ok(()); } sp } - None => item.attr_span(cx.tcx), + None => attr_span, }; - let (explicit_span, false) = source_span_for_markdown_range( + let explicit_span = match source_span_for_markdown_range( cx.tcx, doc, &offset_explicit_range(doc, link_range.clone(), b'[', b']'), &item.attrs.doc_strings, - )? - else { + ) { + Some((explicit_span, false)) => explicit_span, // This `span` comes from macro expansion so skipping it. - return None; + Some((_, true)) => return Ok(()), + // Cannot give a contiguous span for this link. + None => { + return Err(RedundantExplicitLinksCannotSuggest { + display_link: resolvable_link.clone(), + dest_link: dest.to_string(), + attr_span, + }); + } }; - let (display_span, false) = source_span_for_markdown_range( + let display_span = match source_span_for_markdown_range( cx.tcx, doc, resolvable_link_range, &item.attrs.doc_strings, - )? - else { + ) { + Some((display_span, false)) => display_span, // This `span` comes from macro expansion so skipping it. - return None; + Some((_, true)) => return Ok(()), + // Cannot give a contiguous span for this link. + None => { + return Err(RedundantExplicitLinksCannotSuggest { + display_link: resolvable_link.clone(), + dest_link: dest.to_string(), + attr_span, + }); + } }; - let (def_span, _) = source_span_for_markdown_range( + let def_span = match source_span_for_markdown_range( cx.tcx, doc, &offset_reference_def_range(doc, dest, link_range), &item.attrs.doc_strings, - )?; + ) { + Some((def_span, _)) => def_span, + // Cannot give a contiguous span for this link. + None => { + return Err(RedundantExplicitLinksCannotSuggest { + display_link: resolvable_link.clone(), + dest_link: dest.to_string(), + attr_span, + }); + } + }; cx.tcx.emit_node_span_lint( crate::lint::REDUNDANT_EXPLICIT_LINKS, @@ -350,7 +434,7 @@ fn check_reference_redundancy( ); } - None + Ok(()) } fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option> { diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs b/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs index 9de8bbe3b8c98..2975ddfcc68bb 100644 --- a/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs +++ b/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs @@ -1,43 +1,42 @@ #![deny(rustdoc::redundant_explicit_links)] +use std::clone::Clone; -// Right now, redundant_explicit_links won't produce a warning at all if -// rustdoc isn't able to calculate an accurate span for the link. -// -// If that changes and this test starts to fail, you should add the -// appropriate annotations, and, also, make sure that it doesn't -// suggest a correction that wipes out the `fn` formal signature -// or the `#[inline]` attribute. - -/// [std::clone::Clone]( +/// [Clone]( pub fn split_outer_inner() { //! std::clone::Clone) +//~^^^ ERROR redundant_explicit_links } -/// [std::clone::Clone](std::clone::Clone +/// [Clone](std::clone::Clone pub fn split_outer_inner_b() { //! ) +//~^^^ ERROR redundant_explicit_links } -/// [std::clone::Clone]( +/// [Clone]( #[inline] /// std::clone::Clone) +//~^^^ ERROR redundant_explicit_links pub fn split_attr() { } -/// [std::clone::Clone](std::clone::Clone +/// [Clone](std::clone::Clone #[inline] /// ) +//~^^^ ERROR redundant_explicit_links pub fn split_attr_b() { } -/// [std::clone::Clone]( +/// [Clone]( /// std::clone::Clone) //~^^ ERROR redundant_explicit_links +//~| SUGGESTION [Clone] pub fn not_split() { } -/// [std::clone::Clone](std::clone::Clone +/// [Clone](std::clone::Clone /// ) //~^^ ERROR redundant_explicit_links +//~| SUGGESTION [Clone] pub fn not_split_b() { } diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr b/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr index 3e2e99f10d9f7..ff9bdfb48fc49 100644 --- a/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr +++ b/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr @@ -1,12 +1,10 @@ error: redundant explicit link target - --> $DIR/redundant_explicit_links_split.rs:33:25 + --> $DIR/redundant_explicit_links_split.rs:4:1 | -LL | /// [std::clone::Clone]( - | ______-----------------__^ - | | | - | | because label contains path that resolves to same destination -LL | | /// std::clone::Clone) - | |_____________________^ explicit target is redundant +LL | / /// [Clone]( +LL | | pub fn split_outer_inner() { +LL | | //! std::clone::Clone) + | |__________________________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination | = note: when a link's destination is not specified, the label is used to resolve intra-doc links @@ -15,18 +13,64 @@ note: the lint level is defined here | LL | #![deny(rustdoc::redundant_explicit_links)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:10:1 + | +LL | / /// [Clone](std::clone::Clone +LL | | pub fn split_outer_inner_b() { +LL | | //! ) + | |_________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:16:1 + | +LL | / /// [Clone]( +LL | | #[inline] +LL | | /// std::clone::Clone) + | |______________________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:23:1 + | +LL | / /// [Clone](std::clone::Clone +LL | | #[inline] +LL | | /// ) + | |_____^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:30:13 + | +LL | /// [Clone]( + | ______-----__^ + | | | + | | because label contains path that resolves to same destination +LL | | /// std::clone::Clone) + | |_____________________^ explicit target is redundant + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links help: remove explicit link target | -LL - /// [std::clone::Clone]( +LL - /// [Clone]( LL - /// std::clone::Clone) -LL + /// [std::clone::Clone] +LL + /// [Clone] | error: redundant explicit link target - --> $DIR/redundant_explicit_links_split.rs:39:25 + --> $DIR/redundant_explicit_links_split.rs:37:13 | -LL | /// [std::clone::Clone](std::clone::Clone - | ______-----------------__^ +LL | /// [Clone](std::clone::Clone + | ______-----__^ | | | | | because label contains path that resolves to same destination LL | | /// ) @@ -36,10 +80,10 @@ LL | | /// ) the label is used to resolve intra-doc links help: remove explicit link target | -LL - /// [std::clone::Clone](std::clone::Clone +LL - /// [Clone](std::clone::Clone LL - /// ) -LL + /// [std::clone::Clone] +LL + /// [Clone] | -error: aborting due to 2 previous errors +error: aborting due to 6 previous errors From 74c203dfbdccbc169eea104bf09778c2b8912bc7 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 17 Jun 2026 20:35:58 -0700 Subject: [PATCH 10/15] Add clarifying comment for shrink_to_hi --- compiler/rustc_resolve/src/rustdoc.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 42a5005552825..7ee5f04ef7243 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -652,6 +652,8 @@ pub fn source_span_for_markdown_range_inner( span_of_all_fragments.split_at(prev_lines_bytes).1.split_at(source_line_len).0; let has_fragment = fragments .iter() + // `source_line_span` might contain indentation that `fragment.span` doesn't contain, + // so `shrink_to_hi()` removes it .any(|fragment| fragment.span.contains(source_line_span.shrink_to_hi())); // Since we're counting bytes, `prev_line_bytes` includes the "\n". prev_lines_bytes += source_line_len + 1; From 162a90c2455223e6fab5d3b540c07851e8818a92 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Thu, 18 Jun 2026 12:12:03 -0700 Subject: [PATCH 11/15] Handle block doc comments better --- compiler/rustc_resolve/src/rustdoc.rs | 46 ++++++- .../lints/redundant_explicit_links_split.rs | 58 +++++++++ .../redundant_explicit_links_split.stderr | 118 +++++++++++++++++- 3 files changed, 214 insertions(+), 8 deletions(-) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 7ee5f04ef7243..f1e94b415787b 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -650,14 +650,50 @@ pub fn source_span_for_markdown_range_inner( let source_line_len = u32::try_from(source_line.len()).unwrap(); let source_line_span = span_of_all_fragments.split_at(prev_lines_bytes).1.split_at(source_line_len).0; - let has_fragment = fragments + let fragment = fragments .iter() - // `source_line_span` might contain indentation that `fragment.span` doesn't contain, - // so `shrink_to_hi()` removes it - .any(|fragment| fragment.span.contains(source_line_span.shrink_to_hi())); + // `source_line_span` might contain indentation that `fragment.span` doesn't contain + .find(|fragment| fragment.span.overlaps(source_line_span)); // Since we're counting bytes, `prev_line_bytes` includes the "\n". prev_lines_bytes += source_line_len + 1; - if has_fragment && let Some(offset) = source_line.find(md_line) { + if let Some(fragment) = fragment + && let Some(offset) = source_line.find(md_line) + { + if fragment.span.lo() > source_line_span.lo() + && source_line + [..usize::try_from(fragment.span.lo().0 - source_line_span.lo().0).unwrap()] + .chars() + .any(|c| !c.is_whitespace()) + { + // Make sure anything between the start of this line and the fragment itself is just indentation. + // Because source_line is built by splitting the span that covers all fragments, this only finds + // characters *between* doc comments, not characters before or after doc comments. + // + // 1| /** doc */ + // 2| #[inline] /** doc2 */ + // ^^^^^^^^^ + // | this + // + // 3| fn foo() {} + return None; + } + if fragment.span.hi() < source_line_span.hi() + && source_line + [usize::try_from(fragment.span.hi().0 - source_line_span.lo().0).unwrap()..] + .chars() + .any(|c| !c.is_whitespace()) + { + // Make sure anything between the start of this line and the fragment itself is just indentation. + // Because source_line is built by splitting the span that covers all fragments, this only finds + // characters *between* doc comments, not characters before or after doc comments. + // 1| /** doc */ #[inline] + // ^^^^^^^^^ + // | this + // + // 2| /** doc2 */ fn foo() {} + // 3| fn foo() {} + return None; + } if line_no == starting_line { start_bytes += offset; diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs b/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs index 2975ddfcc68bb..eebc6a0aa023c 100644 --- a/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs +++ b/tests/rustdoc-ui/lints/redundant_explicit_links_split.rs @@ -27,6 +27,64 @@ pub fn split_attr() { pub fn split_attr_b() { } +/** [Clone]( */ #[inline] /** std::clone::Clone) */ +//~^ ERROR redundant_explicit_links +pub fn split_blockcomment_attr() { +} + +/** [Clone](std::clone::Clone */ #[inline] /** ) */ +//~^ ERROR redundant_explicit_links +pub fn split_blockcomment_attr_b() { +} + +/** [Clone]( */ +#[inline] +/** std::clone::Clone) */ +//~^^^ ERROR redundant_explicit_links +pub fn split_blockcomment_multiline_attr() { +} + +/** [Clone](std::clone::Clone */ +#[inline] +/** ) */ +//~^^^ ERROR redundant_explicit_links +pub fn split_blockcomment_multiline_attr_b() { +} + +/** [Clone]( */ #[inline] +/** std::clone::Clone) */ +//~^^ ERROR redundant_explicit_links +pub fn split_blockcomment_twoline_attr() { +} + +/** [Clone](std::clone::Clone */ #[inline] +/** ) */ +//~^^ ERROR redundant_explicit_links +pub fn split_blockcomment_twoline_attr_b() { +} + +/** [Clone]( */ +#[inline] /** std::clone::Clone) */ +//~^^ ERROR redundant_explicit_links +pub fn split_blockcomment_secondline_attr() { +} + +/** [Clone](std::clone::Clone */ +#[inline] /** ) */ +//~^^ ERROR redundant_explicit_links +pub fn split_blockcomment_secondline_attr_b() { +} + +/** [Clone](std::clone::Clone) */ #[inline] +//~^ ERROR redundant_explicit_links +//~| SUGGESTION [Clone] +pub fn split_blockcomment_singleline_attr() { +} + +/** [Clone](std::clone::Clone) */ #[inline] pub fn split_blockcomment_singleline_attr_b() { } +//~^ ERROR redundant_explicit_links +//~| SUGGESTION [Clone] + /// [Clone]( /// std::clone::Clone) //~^^ ERROR redundant_explicit_links diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr b/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr index ff9bdfb48fc49..bfab5b751eedf 100644 --- a/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr +++ b/tests/rustdoc-ui/lints/redundant_explicit_links_split.stderr @@ -48,7 +48,119 @@ LL | | /// ) the label is used to resolve intra-doc links error: redundant explicit link target - --> $DIR/redundant_explicit_links_split.rs:30:13 + --> $DIR/redundant_explicit_links_split.rs:30:1 + | +LL | /** [Clone]( */ #[inline] /** std::clone::Clone) */ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:35:1 + | +LL | /** [Clone](std::clone::Clone */ #[inline] /** ) */ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:40:1 + | +LL | / /** [Clone]( */ +LL | | #[inline] +LL | | /** std::clone::Clone) */ + | |_________________________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:47:1 + | +LL | / /** [Clone](std::clone::Clone */ +LL | | #[inline] +LL | | /** ) */ + | |________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:54:1 + | +LL | / /** [Clone]( */ #[inline] +LL | | /** std::clone::Clone) */ + | |_________________________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:60:1 + | +LL | / /** [Clone](std::clone::Clone */ #[inline] +LL | | /** ) */ + | |________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:66:1 + | +LL | / /** [Clone]( */ +LL | | #[inline] /** std::clone::Clone) */ + | |___________________________________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:72:1 + | +LL | / /** [Clone](std::clone::Clone */ +LL | | #[inline] /** ) */ + | |__________________^ explicit target `std::clone::Clone` is redundant because label `Clone` resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:78:13 + | +LL | /** [Clone](std::clone::Clone) */ #[inline] + | ----- ^^^^^^^^^^^^^^^^^ explicit target is redundant + | | + | because label contains path that resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links +help: remove explicit link target + | +LL - /** [Clone](std::clone::Clone) */ #[inline] +LL + /** [Clone] */ #[inline] + | + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:84:13 + | +LL | /** [Clone](std::clone::Clone) */ #[inline] pub fn split_blockcomment_singleline_attr_b() { } + | ----- ^^^^^^^^^^^^^^^^^ explicit target is redundant + | | + | because label contains path that resolves to same destination + | + = note: when a link's destination is not specified, + the label is used to resolve intra-doc links +help: remove explicit link target + | +LL - /** [Clone](std::clone::Clone) */ #[inline] pub fn split_blockcomment_singleline_attr_b() { } +LL + /** [Clone] */ #[inline] pub fn split_blockcomment_singleline_attr_b() { } + | + +error: redundant explicit link target + --> $DIR/redundant_explicit_links_split.rs:88:13 | LL | /// [Clone]( | ______-----__^ @@ -67,7 +179,7 @@ LL + /// [Clone] | error: redundant explicit link target - --> $DIR/redundant_explicit_links_split.rs:37:13 + --> $DIR/redundant_explicit_links_split.rs:95:13 | LL | /// [Clone](std::clone::Clone | ______-----__^ @@ -85,5 +197,5 @@ LL - /// ) LL + /// [Clone] | -error: aborting due to 6 previous errors +error: aborting due to 16 previous errors From 042e779afae728bfd491bcfb690908dc762d82b5 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Tue, 7 Jul 2026 00:15:26 -0700 Subject: [PATCH 12/15] Add diagram to comment Co-authored-by: lolbinarycat --- compiler/rustc_resolve/src/rustdoc.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index f1e94b415787b..301ea7c71da06 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -719,6 +719,12 @@ pub fn source_span_for_markdown_range_inner( // but we found a non-empty non-markdown line. // This could be an attribute, and we don't want a diagnostic // suggesting to delete that attribute, so we return None to be safe. + // 1| /** doc */ + // 2 | #[inline] + // ^^^^^^^^^ + // | this + // 3| /** doc2 */ + // 4| fn foo() {} return None; } else { end_bytes += source_line.len() + 1; From 8f1fd2c6e4bf7e44dfca254e162120d260144983 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Tue, 7 Jul 2026 00:16:01 -0700 Subject: [PATCH 13/15] Fix wrong diagram in comment Co-authored-by: lolbinarycat --- compiler/rustc_resolve/src/rustdoc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 301ea7c71da06..ddf5600f76659 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -690,7 +690,7 @@ pub fn source_span_for_markdown_range_inner( // ^^^^^^^^^ // | this // - // 2| /** doc2 */ fn foo() {} + // 2| /** doc2 */ // 3| fn foo() {} return None; } From fd07857db3c77538b9214c4125aaa91ee48481d4 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Tue, 7 Jul 2026 00:17:23 -0700 Subject: [PATCH 14/15] Use better name for lint struct Co-authored-by: lolbinarycat --- .../passes/lint/redundant_explicit_links.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/librustdoc/passes/lint/redundant_explicit_links.rs b/src/librustdoc/passes/lint/redundant_explicit_links.rs index ad45b6e1d1c5b..cd7b7caac69ad 100644 --- a/src/librustdoc/passes/lint/redundant_explicit_links.rs +++ b/src/librustdoc/passes/lint/redundant_explicit_links.rs @@ -154,13 +154,13 @@ fn check_redundant_explicit_link<'md>( } } -struct RedundantExplicitLinksCannotSuggest { +struct RedundantExplicitLinksWithoutSuggestion { attr_span: Span, display_link: String, dest_link: String, } -impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinksCannotSuggest { +impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinksWithoutSuggestion { fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> { let Self { attr_span, display_link, dest_link } = self; @@ -186,7 +186,7 @@ fn check_inline_or_reference_unknown_redundancy( dest: String, link_data: LinkData, (open, close): (u8, u8), -) -> Result<(), RedundantExplicitLinksCannotSuggest> { +) -> Result<(), RedundantExplicitLinksWithoutSuggestion> { struct RedundantExplicitLinks { explicit_span: Span, display_span: Span, @@ -256,7 +256,7 @@ fn check_inline_or_reference_unknown_redundancy( Some((_, true)) => return Ok(()), // Cannot give a contiguous span for this link. None => { - return Err(RedundantExplicitLinksCannotSuggest { + return Err(RedundantExplicitLinksWithoutSuggestion { display_link: resolvable_link.clone(), dest_link: dest.to_string(), attr_span, @@ -274,7 +274,7 @@ fn check_inline_or_reference_unknown_redundancy( Some((_, true)) => return Ok(()), // Cannot give a contiguous span for this link. None => { - return Err(RedundantExplicitLinksCannotSuggest { + return Err(RedundantExplicitLinksWithoutSuggestion { display_link: resolvable_link.clone(), dest_link: dest.to_string(), attr_span, @@ -308,7 +308,7 @@ fn check_reference_redundancy( link_range: Range, dest: &CowStr<'_>, link_data: LinkData, -) -> Result<(), RedundantExplicitLinksCannotSuggest> { +) -> Result<(), RedundantExplicitLinksWithoutSuggestion> { struct RedundantExplicitLinkTarget { explicit_span: Span, display_span: Span, @@ -378,7 +378,7 @@ fn check_reference_redundancy( Some((_, true)) => return Ok(()), // Cannot give a contiguous span for this link. None => { - return Err(RedundantExplicitLinksCannotSuggest { + return Err(RedundantExplicitLinksWithoutSuggestion { display_link: resolvable_link.clone(), dest_link: dest.to_string(), attr_span, @@ -396,7 +396,7 @@ fn check_reference_redundancy( Some((_, true)) => return Ok(()), // Cannot give a contiguous span for this link. None => { - return Err(RedundantExplicitLinksCannotSuggest { + return Err(RedundantExplicitLinksWithoutSuggestion { display_link: resolvable_link.clone(), dest_link: dest.to_string(), attr_span, @@ -412,7 +412,7 @@ fn check_reference_redundancy( Some((def_span, _)) => def_span, // Cannot give a contiguous span for this link. None => { - return Err(RedundantExplicitLinksCannotSuggest { + return Err(RedundantExplicitLinksWithoutSuggestion { display_link: resolvable_link.clone(), dest_link: dest.to_string(), attr_span, From ebe343bb003c7418e4569569bd018aea1e5dd39f Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Thu, 9 Jul 2026 08:16:27 -0700 Subject: [PATCH 15/15] Clean up redundant explicit intra-doc link This is a weird, but unrelated, problem with doc comments and attribute macros. If I remove the `#[instrument]` attribute, the lint behaves the way it's supposed to, and points directly at the link instead of pointing at the whole line. warning: redundant explicit link target --> compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs:2373:5 | 2373 | /// Lower a [`hir::ConstArg`] to a (type-level) [`ty::Const`](Const). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ explicit target `Const` is redundant because label `ty::Const` resolves to same destination | = note: when a link's destination is not specified, the label is used to resolve intra-doc links = note: `#[warn(rustdoc::redundant_explicit_links)]` on by default --- compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs b/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs index baa9fddc2a651..5fd632f9b000f 100644 --- a/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs +++ b/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs @@ -2302,7 +2302,7 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ { self.check_param_uses_if_mcg(ct, tcx.hir_span(path_hir_id), false) } - /// Lower a [`hir::ConstArg`] to a (type-level) [`ty::Const`](Const). + /// Lower a [`hir::ConstArg`] to a (type-level) [`ty::Const`]. #[instrument(skip(self), level = "debug")] pub fn lower_const_arg(&self, const_arg: &hir::ConstArg<'tcx>, ty: Ty<'tcx>) -> Const<'tcx> { let tcx = self.tcx();