From da95d5cd88d675423870218c77adb2fdf0bcc207 Mon Sep 17 00:00:00 2001 From: Qai Juang <237468078+qaijuang@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:33:54 -0400 Subject: [PATCH 01/31] rustdoc: check redundant explicit links against generated URLs --- .../passes/lint/redundant_explicit_links.rs | 132 ++++++++++++------ tests/rustdoc-ui/lints/no-redundancy.rs | 6 + .../lints/redundant_explicit_links.fixed | 7 + .../lints/redundant_explicit_links.rs | 7 + .../lints/redundant_explicit_links.stderr | 18 ++- 5 files changed, 128 insertions(+), 42 deletions(-) diff --git a/src/librustdoc/passes/lint/redundant_explicit_links.rs b/src/librustdoc/passes/lint/redundant_explicit_links.rs index cd7b7caac69ad..de2414ccb262d 100644 --- a/src/librustdoc/passes/lint/redundant_explicit_links.rs +++ b/src/librustdoc/passes/lint/redundant_explicit_links.rs @@ -12,9 +12,11 @@ use rustc_resolve::rustdoc::{prepare_to_doc_link_resolution, source_span_for_mar use rustc_span::def_id::DefId; use rustc_span::{Span, Symbol}; -use crate::clean::Item; use crate::clean::utils::{find_nearest_parent_module, inherits_doc_hidden}; +use crate::clean::{Item, inline}; use crate::core::DocContext; +use crate::formats::item_type::ItemType; +use crate::html::format::href_relative_parts; use crate::html::markdown::main_body_opts; #[derive(Debug)] @@ -71,12 +73,13 @@ fn check_redundant_explicit_link_for_did( return; }; - check_redundant_explicit_link(cx, item, hir_id, doc, resolutions); + check_redundant_explicit_link(cx, item, module_id, hir_id, doc, resolutions); } fn check_redundant_explicit_link<'md>( cx: &DocContext<'_>, item: &Item, + module_id: DefId, hir_id: HirId, doc: &'md str, resolutions: &DocLinkResMap, @@ -114,41 +117,41 @@ fn check_redundant_explicit_link<'md>( continue; } - if dest_url.ends_with(resolvable_link) || resolvable_link.ends_with(&*dest_url) { - let check_result = match link_type { - LinkType::Inline | LinkType::ReferenceUnknown => { - check_inline_or_reference_unknown_redundancy( - cx, - item, - hir_id, - doc, - resolutions, - link_range, - dest_url.to_string(), - link_data, - if link_type == LinkType::Inline { (b'(', b')') } else { (b'[', b']') }, - ) - } - LinkType::Reference => check_reference_redundancy( + let check_result = match link_type { + LinkType::Inline | LinkType::ReferenceUnknown => { + check_inline_or_reference_unknown_redundancy( cx, item, + module_id, hir_id, doc, resolutions, link_range, - &dest_url, + dest_url.to_string(), 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, - ); + if link_type == LinkType::Inline { (b'(', b')') } else { (b'[', b']') }, + ) } + LinkType::Reference => check_reference_redundancy( + cx, + item, + module_id, + 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, + ); } } } @@ -179,6 +182,7 @@ impl<'a> Diagnostic<'a, ()> for RedundantExplicitLinksWithoutSuggestion { fn check_inline_or_reference_unknown_redundancy( cx: &DocContext<'_>, item: &Item, + module_id: DefId, hir_id: HirId, doc: &str, resolutions: &DocLinkResMap, @@ -226,13 +230,8 @@ fn check_inline_or_reference_unknown_redundancy( 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 { + if explicit_link_is_redundant(cx, module_id, resolutions, &dest, resolvable_link) { 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) @@ -302,6 +301,7 @@ fn check_inline_or_reference_unknown_redundancy( fn check_reference_redundancy( cx: &DocContext<'_>, item: &Item, + module_id: DefId, hir_id: HirId, doc: &str, resolutions: &DocLinkResMap, @@ -347,13 +347,8 @@ fn check_reference_redundancy( 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 { + if explicit_link_is_redundant(cx, module_id, resolutions, dest, resolvable_link) { 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) @@ -437,6 +432,61 @@ fn check_reference_redundancy( Ok(()) } +fn explicit_link_is_redundant( + cx: &DocContext<'_>, + module_id: DefId, + resolutions: &DocLinkResMap, + dest: &str, + resolvable_link: &str, +) -> bool { + let Some(display_res) = find_resolution(resolutions, resolvable_link) else { + return false; + }; + + if (dest.ends_with(resolvable_link) || resolvable_link.ends_with(dest)) + && find_resolution(resolutions, dest).is_some_and(|dest_res| dest_res == display_res) + { + return true; + } + + if dest.contains('#') || !dest.ends_with(".html") { + return false; + } + + local_href_for_res(cx, module_id, display_res).is_some_and(|href| href == dest) +} + +fn local_href_for_res(cx: &DocContext<'_>, module_id: DefId, res: Res) -> Option { + let mut did = res.opt_def_id()?; + if matches!(cx.tcx.def_kind(did), DefKind::Ctor(..)) { + did = cx.tcx.parent(did); + } + + if matches!( + cx.tcx.def_kind(did), + DefKind::AssocTy | DefKind::AssocFn | DefKind::AssocConst { .. } | DefKind::Variant + ) || !did.is_local() + { + return None; + } + + let item_type = ItemType::from_def_id(did, cx.tcx); + let fqp = inline::get_item_path(cx.tcx, did, item_type); + let module_fqp = if item_type == ItemType::Module { &fqp[..] } else { &fqp[..fqp.len() - 1] }; + let current_fqp = inline::get_item_path(cx.tcx, module_id, ItemType::Module); + + let mut url_parts = href_relative_parts(module_fqp, ¤t_fqp); + match item_type { + ItemType::Module => url_parts.push("index.html"), + _ => url_parts.push_fmt(format_args!( + "{}.{last}.html", + item_type.as_str(), + last = fqp.last()? + )), + } + Some(url_parts.finish()) +} + fn find_resolution(resolutions: &DocLinkResMap, path: &str) -> Option> { [Namespace::TypeNS, Namespace::ValueNS, Namespace::MacroNS] .into_iter() diff --git a/tests/rustdoc-ui/lints/no-redundancy.rs b/tests/rustdoc-ui/lints/no-redundancy.rs index 6609ce6a4f8d4..d51ada4d13527 100644 --- a/tests/rustdoc-ui/lints/no-redundancy.rs +++ b/tests/rustdoc-ui/lints/no-redundancy.rs @@ -5,3 +5,9 @@ /// [Vec][std::vec::Vec#examples] should not warn, because it's not actually redundant! /// [This is just an `Option`][std::option::Option] has different display content to actual link! pub fn func() {} + +// Regression guard for https://github.com/rust-lang/rust/issues/155458. +/// [NoRedundancyTarget](struct.NoRedundancyTarget.html#fragment) should not warn. +pub struct NoRedundancySource; + +pub struct NoRedundancyTarget; diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links.fixed b/tests/rustdoc-ui/lints/redundant_explicit_links.fixed index c40c5691e6082..ad0c6d217ffef 100644 --- a/tests/rustdoc-ui/lints/redundant_explicit_links.fixed +++ b/tests/rustdoc-ui/lints/redundant_explicit_links.fixed @@ -156,3 +156,10 @@ pub fn should_warn_reference() {} /// [`Vec`]: Vec /// [`Vec`]: std::vec::Vec pub fn should_not_warn_reference() {} + +// Regression test for https://github.com/rust-lang/rust/issues/155458. +/// [Issue155458B] +//~^ ERROR redundant explicit link target +pub struct Issue155458A; + +pub struct Issue155458B; diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links.rs b/tests/rustdoc-ui/lints/redundant_explicit_links.rs index dc64a5613fb2b..207eeb7ece2db 100644 --- a/tests/rustdoc-ui/lints/redundant_explicit_links.rs +++ b/tests/rustdoc-ui/lints/redundant_explicit_links.rs @@ -156,3 +156,10 @@ pub fn should_warn_reference() {} /// [`Vec`]: Vec /// [`Vec`]: std::vec::Vec pub fn should_not_warn_reference() {} + +// Regression test for https://github.com/rust-lang/rust/issues/155458. +/// [Issue155458B](struct.Issue155458B.html) +//~^ ERROR redundant explicit link target +pub struct Issue155458A; + +pub struct Issue155458B; diff --git a/tests/rustdoc-ui/lints/redundant_explicit_links.stderr b/tests/rustdoc-ui/lints/redundant_explicit_links.stderr index f90c41af9f1ac..32c7ffe1cabec 100644 --- a/tests/rustdoc-ui/lints/redundant_explicit_links.stderr +++ b/tests/rustdoc-ui/lints/redundant_explicit_links.stderr @@ -1063,5 +1063,21 @@ LL - /// [`dummy_target`][dummy_target] TEXT LL + /// [`dummy_target`] TEXT | -error: aborting due to 60 previous errors +error: redundant explicit link target + --> $DIR/redundant_explicit_links.rs:161:20 + | +LL | /// [Issue155458B](struct.Issue155458B.html) + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^ 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 - /// [Issue155458B](struct.Issue155458B.html) +LL + /// [Issue155458B] + | + +error: aborting due to 61 previous errors From fd82481ad9f2d00b61cfe7ef8b9a1d133ee86b8f Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:44:03 +0200 Subject: [PATCH 02/31] Only use dlltool.exe on MinGW if -Cdlltool is passed The linker used by MinGW now works with ar_archive_writer generated short import libraries too. --- compiler/rustc_codegen_ssa/src/back/archive.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_codegen_ssa/src/back/archive.rs b/compiler/rustc_codegen_ssa/src/back/archive.rs index c4107b4a60f27..d5a67f21f9e2c 100644 --- a/compiler/rustc_codegen_ssa/src/back/archive.rs +++ b/compiler/rustc_codegen_ssa/src/back/archive.rs @@ -87,12 +87,12 @@ pub trait ArchiveBuilderBuilder { items: Vec, output_path: &Path, ) { - if common::is_mingw_gnu_toolchain(&sess.target) { - // The binutils linker used on -windows-gnu targets cannot read the import - // libraries generated by LLVM: in our attempts, the linker produced an .EXE - // that loaded but crashed with an AV upon calling one of the imported - // functions. Therefore, use binutils to create the import library instead, - // by writing a .DEF file to the temp dir and calling binutils's dlltool. + if common::is_mingw_gnu_toolchain(&sess.target) && sess.opts.cg.dlltool.is_some() { + // Previously we always used dlltool on -windows-gnu targets due to the binutils + // linker not entirely correctly handling import libraries generated by + // LLVM/ar_archive_writer. This has since been fixed. To ease the transition, will + // temporarily still use dlltool if explicitly specified, but use ar_archive_writer + // like on MSVC if not. create_mingw_dll_import_lib(sess, lib_name, items, output_path); } else { trace!("creating import library"); From 299213ffae4d994943719d3ced29421b247dd66b Mon Sep 17 00:00:00 2001 From: Augie Fackler Date: Fri, 14 Aug 2026 10:43:40 -0400 Subject: [PATCH 03/31] PassWrapper: handle LLVM 24 change in function types LLVM 24 stopped using llvm::Any here to avoid heap allocations in PassInstrumentation. --- .../rustc_llvm/llvm-wrapper/PassWrapper.cpp | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp index 3ad59c53a5bf3..50cdcbc29874f 100644 --- a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp +++ b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp @@ -523,6 +523,17 @@ extern "C" typedef void (*LLVMRustSelfProfileBeforePassCallback)( extern "C" typedef void (*LLVMRustSelfProfileAfterPassCallback)( void *); // LlvmSelfProfiler +#if LLVM_VERSION_GE(24, 0) +std::string LLVMRustwrappedIrGetName(const llvm::IRUnitRef &WrappedIr) { + if (const auto *Cast = dyn_cast(WrappedIr)) + return Cast->getName().str(); + if (const auto *Cast = dyn_cast(WrappedIr)) + return Cast->getName().str(); + if (const auto *Cast = dyn_cast(WrappedIr)) + return Cast->getName().str(); + if (const auto *Cast = dyn_cast(WrappedIr)) + return Cast->getName(); +#else std::string LLVMRustwrappedIrGetName(const llvm::Any &WrappedIr) { if (const auto *Cast = any_cast(&WrappedIr)) return (*Cast)->getName().str(); @@ -532,6 +543,7 @@ std::string LLVMRustwrappedIrGetName(const llvm::Any &WrappedIr) { return (*Cast)->getName().str(); if (const auto *Cast = any_cast(&WrappedIr)) return (*Cast)->getName(); +#endif return ""; } @@ -540,15 +552,24 @@ void LLVMSelfProfileInitializeCallbacks( LLVMRustSelfProfileBeforePassCallback BeforePassCallback, LLVMRustSelfProfileAfterPassCallback AfterPassCallback) { PIC.registerBeforeNonSkippedPassCallback( +#if LLVM_VERSION_GE(24, 0) + [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::IRUnitRef Ir) { +#else [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::Any Ir) { +#endif std::string PassName = Pass.str(); std::string IrName = LLVMRustwrappedIrGetName(Ir); BeforePassCallback(LlvmSelfProfiler, PassName.c_str(), IrName.c_str()); }); PIC.registerAfterPassCallback( +#if LLVM_VERSION_GE(24, 0) + [LlvmSelfProfiler, AfterPassCallback]( + StringRef Pass, llvm::IRUnitRef IR, const PreservedAnalyses &Preserved) { +#else [LlvmSelfProfiler, AfterPassCallback]( StringRef Pass, llvm::Any IR, const PreservedAnalyses &Preserved) { +#endif AfterPassCallback(LlvmSelfProfiler); }); @@ -557,16 +578,25 @@ void LLVMSelfProfileInitializeCallbacks( AfterPassCallback](StringRef Pass, const PreservedAnalyses &Preserved) { AfterPassCallback(LlvmSelfProfiler); }); - +#if LLVM_VERSION_GE(24, 0) + PIC.registerBeforeAnalysisCallback( + [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::IRUnitRef Ir) { +#else PIC.registerBeforeAnalysisCallback( [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::Any Ir) { +#endif std::string PassName = Pass.str(); std::string IrName = LLVMRustwrappedIrGetName(Ir); BeforePassCallback(LlvmSelfProfiler, PassName.c_str(), IrName.c_str()); }); +#if LLVM_VERSION_GE(24, 0) + PIC.registerAfterAnalysisCallback( + [LlvmSelfProfiler, AfterPassCallback](StringRef Pass, llvm::IRUnitRef Ir) { +#else PIC.registerAfterAnalysisCallback( [LlvmSelfProfiler, AfterPassCallback](StringRef Pass, llvm::Any Ir) { +#endif AfterPassCallback(LlvmSelfProfiler); }); } From 50b867f932c2dd7239571fbcc540d8f8f9ecd057 Mon Sep 17 00:00:00 2001 From: Augie Fackler Date: Fri, 14 Aug 2026 11:23:43 -0400 Subject: [PATCH 04/31] PassWrapper: clang-format --- .../rustc_llvm/llvm-wrapper/PassWrapper.cpp | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp index 50cdcbc29874f..06526e718b791 100644 --- a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp +++ b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp @@ -553,7 +553,8 @@ void LLVMSelfProfileInitializeCallbacks( LLVMRustSelfProfileAfterPassCallback AfterPassCallback) { PIC.registerBeforeNonSkippedPassCallback( #if LLVM_VERSION_GE(24, 0) - [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::IRUnitRef Ir) { + [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, + llvm::IRUnitRef Ir) { #else [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::Any Ir) { #endif @@ -564,8 +565,9 @@ void LLVMSelfProfileInitializeCallbacks( PIC.registerAfterPassCallback( #if LLVM_VERSION_GE(24, 0) - [LlvmSelfProfiler, AfterPassCallback]( - StringRef Pass, llvm::IRUnitRef IR, const PreservedAnalyses &Preserved) { + [LlvmSelfProfiler, + AfterPassCallback](StringRef Pass, llvm::IRUnitRef IR, + const PreservedAnalyses &Preserved) { #else [LlvmSelfProfiler, AfterPassCallback]( StringRef Pass, llvm::Any IR, const PreservedAnalyses &Preserved) { @@ -579,26 +581,26 @@ void LLVMSelfProfileInitializeCallbacks( AfterPassCallback(LlvmSelfProfiler); }); #if LLVM_VERSION_GE(24, 0) - PIC.registerBeforeAnalysisCallback( - [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::IRUnitRef Ir) { + PIC.registerBeforeAnalysisCallback([LlvmSelfProfiler, BeforePassCallback]( + StringRef Pass, llvm::IRUnitRef Ir) { #else PIC.registerBeforeAnalysisCallback( [LlvmSelfProfiler, BeforePassCallback](StringRef Pass, llvm::Any Ir) { #endif - std::string PassName = Pass.str(); - std::string IrName = LLVMRustwrappedIrGetName(Ir); - BeforePassCallback(LlvmSelfProfiler, PassName.c_str(), IrName.c_str()); - }); + std::string PassName = Pass.str(); + std::string IrName = LLVMRustwrappedIrGetName(Ir); + BeforePassCallback(LlvmSelfProfiler, PassName.c_str(), IrName.c_str()); + }); #if LLVM_VERSION_GE(24, 0) - PIC.registerAfterAnalysisCallback( - [LlvmSelfProfiler, AfterPassCallback](StringRef Pass, llvm::IRUnitRef Ir) { + PIC.registerAfterAnalysisCallback([LlvmSelfProfiler, AfterPassCallback]( + StringRef Pass, llvm::IRUnitRef Ir) { #else PIC.registerAfterAnalysisCallback( [LlvmSelfProfiler, AfterPassCallback](StringRef Pass, llvm::Any Ir) { #endif - AfterPassCallback(LlvmSelfProfiler); - }); + AfterPassCallback(LlvmSelfProfiler); + }); } enum class LLVMRustOptStage { From af7623a1804bf9045ca3dacb34ad0fb30caa27b4 Mon Sep 17 00:00:00 2001 From: cyrgani Date: Tue, 18 Aug 2026 12:26:29 +0000 Subject: [PATCH 05/31] add crashtests --- tests/crashes/{108428.rs => 108248.rs} | 2 +- tests/crashes/138262.rs | 12 ++++++++++++ tests/crashes/142155.rs | 12 ++++++++++++ tests/crashes/144241.rs | 4 ++++ tests/crashes/149562.rs | 10 ++++++++++ tests/crashes/152414.rs | 6 ++++++ tests/crashes/152416.rs | 17 +++++++++++++++++ tests/crashes/152626.rs | 7 +++++++ tests/crashes/154903.rs | 7 +++++++ tests/crashes/154963.rs | 10 ++++++++++ tests/crashes/155053.rs | 11 +++++++++++ tests/crashes/156101.rs | 4 ++++ tests/crashes/156288.rs | 3 +++ 13 files changed, 104 insertions(+), 1 deletion(-) rename tests/crashes/{108428.rs => 108248.rs} (84%) create mode 100644 tests/crashes/138262.rs create mode 100644 tests/crashes/142155.rs create mode 100644 tests/crashes/144241.rs create mode 100644 tests/crashes/149562.rs create mode 100644 tests/crashes/152414.rs create mode 100644 tests/crashes/152416.rs create mode 100644 tests/crashes/152626.rs create mode 100644 tests/crashes/154903.rs create mode 100644 tests/crashes/154963.rs create mode 100644 tests/crashes/155053.rs create mode 100644 tests/crashes/156101.rs create mode 100644 tests/crashes/156288.rs diff --git a/tests/crashes/108428.rs b/tests/crashes/108248.rs similarity index 84% rename from tests/crashes/108428.rs rename to tests/crashes/108248.rs index b18123b6a7c40..36252e29d33f0 100644 --- a/tests/crashes/108428.rs +++ b/tests/crashes/108248.rs @@ -1,4 +1,4 @@ -//@ known-bug: #108428 +//@ known-bug: #108248 //@ needs-rustc-debug-assertions //@ compile-flags: -Wunused-lifetimes fn main() { diff --git a/tests/crashes/138262.rs b/tests/crashes/138262.rs new file mode 100644 index 0000000000000..ce5b3bb257e5d --- /dev/null +++ b/tests/crashes/138262.rs @@ -0,0 +1,12 @@ +//@ known-bug: #138262 +//@ compile-flags: -Zsanitizer=cfi -Ccodegen-units=1 -Clto -Clink-dead-code=true -Cunsafe-allow-abi-mismatch=sanitizer -Ctarget-feature=-crt-static +//@ ignore-backends: gcc +//@ needs-sanitizer-cfi +fn foo() {} + +core::arch::global_asm!("/* {} */", sym foo::<{ + || {}; + 0 +}>); + +fn main() {} diff --git a/tests/crashes/142155.rs b/tests/crashes/142155.rs new file mode 100644 index 0000000000000..8c0769bf2b586 --- /dev/null +++ b/tests/crashes/142155.rs @@ -0,0 +1,12 @@ +//@ known-bug: #142155 +//@ needs-rustc-debug-assertions +//@ edition: 2021 + +#![warn(tail_expr_drop_order)] +use core::future::Future; + +fn f() -> impl Future> { + async { Some("nope".into()) } +} + +fn main() {} diff --git a/tests/crashes/144241.rs b/tests/crashes/144241.rs new file mode 100644 index 0000000000000..3f91fcc7c6275 --- /dev/null +++ b/tests/crashes/144241.rs @@ -0,0 +1,4 @@ +//@ known-bug: #144241 +fn main() { + |_: dyn ?Sized + !Send| {} +} diff --git a/tests/crashes/149562.rs b/tests/crashes/149562.rs new file mode 100644 index 0000000000000..4d032a0af5c3e --- /dev/null +++ b/tests/crashes/149562.rs @@ -0,0 +1,10 @@ +//@ known-bug: #149562 +//@ needs-rustc-debug-assertions +fn a() -> T +where + T: ?Sized, + T: ?Sized, +{ +} + +fn main() {} diff --git a/tests/crashes/152414.rs b/tests/crashes/152414.rs new file mode 100644 index 0000000000000..226f9e29faad6 --- /dev/null +++ b/tests/crashes/152414.rs @@ -0,0 +1,6 @@ +//@ known-bug: #152414 +//@ needs-rustc-debug-assertions +#![feature(generic_assert)] +fn main() { + assert!(size_of(val, 1) >= 1); +} diff --git a/tests/crashes/152416.rs b/tests/crashes/152416.rs new file mode 100644 index 0000000000000..9ca418cce3628 --- /dev/null +++ b/tests/crashes/152416.rs @@ -0,0 +1,17 @@ +//@ known-bug: #152416 +//@ needs-rustc-debug-assertions +//@ compile-flags: -Zunstable-options + +trait AssetID {} +trait Archive { + fn name(&self); +} +struct NorthlightAssetID; +impl AssetID for NorthlightAssetID {} +fn get() -> Box> { + let x: Box> = todo!(); + x +} +fn main() { + get().name(); +} diff --git a/tests/crashes/152626.rs b/tests/crashes/152626.rs new file mode 100644 index 0000000000000..eafb714c2f5c2 --- /dev/null +++ b/tests/crashes/152626.rs @@ -0,0 +1,7 @@ +//@ known-bug: #152626 +//@ needs-rustc-debug-assertions +struct A>(T); +fn f() -> A<&'static ()> { + todo!() +} +fn main() {} diff --git a/tests/crashes/154903.rs b/tests/crashes/154903.rs new file mode 100644 index 0000000000000..63e80d8f9e251 --- /dev/null +++ b/tests/crashes/154903.rs @@ -0,0 +1,7 @@ +//@ known-bug: #154903 +//@ compile-flags: -Zlint-mir +#![feature(guard_patterns)] + +fn a(((x if true, _) | (_, x)): (i32, i32)) {} + +fn main() {} diff --git a/tests/crashes/154963.rs b/tests/crashes/154963.rs new file mode 100644 index 0000000000000..8fafc29c48342 --- /dev/null +++ b/tests/crashes/154963.rs @@ -0,0 +1,10 @@ +//@ known-bug: #154963 +#![feature(extern_types, negative_impls)] + +unsafe extern "C" { + type ExternType; +} + +impl !Unpin for ExternType {} + +fn main() {} diff --git a/tests/crashes/155053.rs b/tests/crashes/155053.rs new file mode 100644 index 0000000000000..31b9ccaf20540 --- /dev/null +++ b/tests/crashes/155053.rs @@ -0,0 +1,11 @@ +//@ known-bug: #155053 +#![feature(pin_ergonomics)] +#![feature(extern_types)] + +unsafe extern "C" { + type ExternType; +} + +impl Unpin for ExternType {} + +fn main() {} diff --git a/tests/crashes/156101.rs b/tests/crashes/156101.rs new file mode 100644 index 0000000000000..c95361fab2ecc --- /dev/null +++ b/tests/crashes/156101.rs @@ -0,0 +1,4 @@ +//@ known-bug: #156101 +fn main() { + format_args!(concat!("šæ", "{f:?#}")); +} diff --git a/tests/crashes/156288.rs b/tests/crashes/156288.rs new file mode 100644 index 0000000000000..b745cfe063dda --- /dev/null +++ b/tests/crashes/156288.rs @@ -0,0 +1,3 @@ +//@ known-bug: #156288 +#[warn(rust_2021_incompatible_closure_captures)] +const _: () = |b| move || b; From afcb367812e059f45c77196f039c96f03f684727 Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Mon, 24 Aug 2026 12:21:01 +0200 Subject: [PATCH 06/31] Don't treat slashes as path seps after drive letters in verbatim paths --- library/std/src/sys/path/windows/tests.rs | 23 +++++++++++++++++++--- library/std/src/sys/path/windows_prefix.rs | 2 +- library/std/tests/path.rs | 10 +++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/library/std/src/sys/path/windows/tests.rs b/library/std/src/sys/path/windows/tests.rs index 830f48d7bfc94..4ca47fc10c2e1 100644 --- a/library/std/src/sys/path/windows/tests.rs +++ b/library/std/src/sys/path/windows/tests.rs @@ -83,6 +83,9 @@ fn verbatim() { // Make sure opening a drive will work. check("Z:", "Z:"); + // Verbatim drive paths begin with `LETTER:\`. `/` is just a regular character here + check(r"\\?\C:/path\somewhere", r"\\?\C:/path\somewhere"); + // A path that contains null is not a valid path. assert!(maybe_verbatim(Path::new("\0")).is_err()); } @@ -93,9 +96,23 @@ fn parse_prefix(path: &str) -> Option> { #[test] fn test_parse_prefix_verbatim() { - let prefix = Some(Prefix::VerbatimDisk(b'C')); - assert_eq!(prefix, parse_prefix(r"\\?\C:/windows/system32/notepad.exe")); - assert_eq!(prefix, parse_prefix(r"\\?\C:\windows\system32\notepad.exe")); + assert_eq!( + parse_prefix(r"\\?\C:\windows\system32\notepad.exe"), + Some(Prefix::VerbatimDisk(b'C')), + ); +} + +#[test] +fn test_verbatim_disk_issue_161651() { + use crate::path::Path; + + // This is not a `VerbatimDisk` path, because `/` is not a separator in verbatim paths! + assert_eq!( + parse_prefix(r"\\?\C:/windows\system32"), + Some(Prefix::Verbatim(OsStr::new("C:/windows"))), + ); + + assert_ne!(Path::new(r"\\?\C:/foo"), Path::new(r"\\?\C:\foo")); } #[test] diff --git a/library/std/src/sys/path/windows_prefix.rs b/library/std/src/sys/path/windows_prefix.rs index b9dfe754485ab..5413269e9edee 100644 --- a/library/std/src/sys/path/windows_prefix.rs +++ b/library/std/src/sys/path/windows_prefix.rs @@ -142,7 +142,7 @@ fn parse_drive(path: &OsStr) -> Option { // Parses a drive prefix exactly, e.g. "C:" fn parse_drive_exact(path: &OsStr) -> Option { // only parse two bytes: the drive letter and the drive separator - if path.as_encoded_bytes().get(2).map(|&x| is_sep_byte(x)).unwrap_or(true) { + if path.as_encoded_bytes().get(2).map(|&x| is_verbatim_sep(x)).unwrap_or(true) { parse_drive(path) } else { None diff --git a/library/std/tests/path.rs b/library/std/tests/path.rs index 8997b8ad192dc..4d42437fbd871 100644 --- a/library/std/tests/path.rs +++ b/library/std/tests/path.rs @@ -989,14 +989,14 @@ pub fn test_decompositions_windows() { ); t!("\\\\?\\C:/foo/bar", - iter: ["\\\\?\\C:", "\\", "foo/bar"], + iter: ["\\\\?\\C:/foo/bar"], has_root: true, is_absolute: true, - parent: Some("\\\\?\\C:/"), - file_name: Some("foo/bar"), - file_stem: Some("foo/bar"), + parent: None, + file_name: None, + file_stem: None, extension: None, - file_prefix: Some("foo/bar") + file_prefix: None ); t!("\\\\.\\foo\\bar", From 97967dee57e5f5d09263bdfb816dca03dae0b10c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 20 Jul 2026 00:34:00 +0200 Subject: [PATCH 07/31] Add new `unescaped_pipe_in_table_cell` rustdoc lint --- src/librustdoc/lint.rs | 12 +++ src/librustdoc/passes/lint.rs | 5 + .../passes/lint/table_pipe_escape.rs | 99 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/librustdoc/passes/lint/table_pipe_escape.rs diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs index 1c3d1c421b545..b0dd2108c5660 100644 --- a/src/librustdoc/lint.rs +++ b/src/librustdoc/lint.rs @@ -209,6 +209,17 @@ declare_rustdoc_lint! { "detects unused footnote definitions" } +declare_rustdoc_lint! { + /// This lint is **warn-by-default**. It detects unescaped pipes in table rows which + /// lead to some row cells being ignored. This is a `rustdoc` only lint, see the + /// documentation in the [rustdoc book]. + /// + /// [rustdoc book]: ../../../rustdoc/lints.html#unescaped_pipe_in_table_cell + UNESCAPED_PIPE_IN_TABLE_CELL, + Warn, + "detects unescaped pipe in table rows in doc comments" +} + pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { vec![ BROKEN_INTRA_DOC_LINKS, @@ -224,6 +235,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { REDUNDANT_EXPLICIT_LINKS, BROKEN_FOOTNOTE, UNUSED_FOOTNOTE_DEFINITION, + UNESCAPED_PIPE_IN_TABLE_CELL, ] }); diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs index bb952b32393cf..2093ff328413d 100644 --- a/src/librustdoc/passes/lint.rs +++ b/src/librustdoc/passes/lint.rs @@ -6,6 +6,7 @@ mod check_code_block_syntax; mod footnotes; mod html_tags; mod redundant_explicit_links; +mod table_pipe_escape; mod unescaped_backticks; use super::Pass; @@ -35,6 +36,7 @@ impl DocVisitor<'_> for Linter<'_, '_> { if !dox.is_empty() { let may_have_link = dox.contains(&[':', '['][..]); let may_have_block_comment_or_html = dox.contains(['<', '>']); + let may_have_table = dox.contains(&['|'][..]); // ~~~rust // // This is a real, supported commonmark syntax for block code // ~~~ @@ -51,6 +53,9 @@ impl DocVisitor<'_> for Linter<'_, '_> { if may_have_block_comment_or_html { html_tags::visit_item(self.cx, item, hir_id, &dox); } + if may_have_table { + table_pipe_escape::visit_item(self.cx, item, hir_id, &dox); + } } self.visit_item_recur(item) diff --git a/src/librustdoc/passes/lint/table_pipe_escape.rs b/src/librustdoc/passes/lint/table_pipe_escape.rs new file mode 100644 index 0000000000000..1ba19eabab1ac --- /dev/null +++ b/src/librustdoc/passes/lint/table_pipe_escape.rs @@ -0,0 +1,99 @@ +//! Detects table rows where some content seems to have been discarded because there are too many +//! pipe characters. + +use std::ops::Range; + +use rustc_hir::HirId; +use rustc_macros::Diagnostic; +use rustc_resolve::rustdoc::pulldown_cmark::{Event, Parser, Tag, TagEnd}; +use rustc_resolve::rustdoc::source_span_for_markdown_range; + +use crate::clean::*; +use crate::core::DocContext; +use crate::html::markdown::main_body_opts; + +#[derive(Diagnostic)] +#[diag("table row has too many columns")] +#[help("to escape `|` characters in tables, add a `\\` before them like `\\|`")] +struct UnescapedPipeInTableCell { + #[primary_span] + #[label("any content after this column divider is discarded")] + span: rustc_span::Span, +} + +pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) { + let mut p = Parser::new_ext(dox, main_body_opts()).into_offset_iter(); + + while let Some((event, _range)) = p.next() { + if let Event::Start(Tag::Table(_)) = event + && let Some((Event::Start(Tag::TableHead), _)) = p.next() + { + let mut expected_cells = 0; + while let Some((event, _)) = p.next() { + match event { + Event::End(TagEnd::TableCell) => expected_cells += 1, + Event::End(TagEnd::TableHead) => break, + _ => {} + } + } + let mut prev_range = None; + while let Some((event, range)) = p.next() { + match event { + Event::End(TagEnd::TableCell) => { + prev_range = Some(range); + } + Event::End(TagEnd::TableRow) => { + if let Some(prev_range) = &prev_range + // So here what is happening: when `pulldown-cmark` is parsing a table + // and a table row has too many cells, it doesn't emit events for the + // extra cells. So the only way for us to know these extra cells exist + // is to compare the row's span with the last emitted cell event's span. + // If the span ends don't match, then there are extra cells. + && prev_range.end + 1 != range.end + { + // Something seems wrong, the range diff doesn't match, some content + // was left out. We now check the number of unescaped `|`. + let row = &dox[range.clone()]; + let mut iter = row.chars(); + let mut divider_count = 0; + while let Some(c) = iter.next() { + if c == '\\' { + iter.next(); + } else if c == '|' { + divider_count += 1; + } + } + // + 1 is to handle the `|` at the end of the table row. + if divider_count <= expected_cells + 1 + || dox[Range { start: prev_range.end + 1, end: range.end }] + .trim() + .is_empty() + { + // Seems all good so let's ignore it and continue;. + continue; + } + let last_cell_separator = + Range { start: prev_range.end, end: prev_range.end + 1 }; + + if let Some((span, _)) = source_span_for_markdown_range( + cx.tcx, + dox, + &last_cell_separator, + &item.attrs.doc_strings, + ) { + cx.tcx.emit_node_span_lint( + crate::lint::UNESCAPED_PIPE_IN_TABLE_CELL, + hir_id, + span, + UnescapedPipeInTableCell { span }, + ); + } + } + } + Event::End(TagEnd::Table) => break, + _ => {} + } + } + } + } +} From dbe2c8ec57a3114974729ca4f98799c1aa85067b Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 20 Jul 2026 00:34:41 +0200 Subject: [PATCH 08/31] Add ui regression test for new rustdoc `unescaped_pipe_in_table_cell` lint --- .../lints/unescaped_pipe_in_table_cell.rs | 38 +++++++++++++++++++ .../lints/unescaped_pipe_in_table_cell.stderr | 31 +++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs create mode 100644 tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr diff --git a/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs b/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs new file mode 100644 index 0000000000000..1a0918340c646 --- /dev/null +++ b/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs @@ -0,0 +1,38 @@ +#![deny(rustdoc::unescaped_pipe_in_table_cell)] + +//! | col1 | +//! | ---- | +//! | `code_with(|arg| arg)` | +//~^ ERROR unescaped_pipe_in_table_cell +//! | one `|` b | +//~^ ERROR unescaped_pipe_in_table_cell +//! +// Testing another lint emission on the same doc comment. +//! +//! | col1 | +//! | ---- | +//! | `code_with(|arg| arg)` | +//~^ ERROR unescaped_pipe_in_table_cell + +// We check that the extra whitespace characters at the end of the line won't trigger +// the lint. +mod b { + //! | col | + //! | ---- | + #![doc = "| code_with | "] +} + +// We check that the `\|` is correctly handled as well (ie not emitting the lint). +mod c { + //! | col | + //! | ---- | + //! | a \| still same cell | +} + +// We check that content after a table row is ignored. +mod d { + //! | col | + //! | ---- | + //! | code_with | aaaaa + //^ the "aaaaa" part will be ignored. +} diff --git a/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr b/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr new file mode 100644 index 0000000000000..03a10c078cded --- /dev/null +++ b/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr @@ -0,0 +1,31 @@ +error: table row has too many columns + --> $DIR/unescaped_pipe_in_table_cell.rs:5:18 + | +LL | //! | `code_with(|arg| arg)` | + | ^ any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` +note: the lint level is defined here + --> $DIR/unescaped_pipe_in_table_cell.rs:1:9 + | +LL | #![deny(rustdoc::unescaped_pipe_in_table_cell)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: table row has too many columns + --> $DIR/unescaped_pipe_in_table_cell.rs:7:12 + | +LL | //! | one `|` b | + | ^ any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` + +error: table row has too many columns + --> $DIR/unescaped_pipe_in_table_cell.rs:14:18 + | +LL | //! | `code_with(|arg| arg)` | + | ^ any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` + +error: aborting due to 3 previous errors + From f74d66c50cb0aa55cf353c50071f83914038a599 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 20 Jul 2026 00:38:02 +0200 Subject: [PATCH 09/31] Add documentation for rustdoc `unescaped_pipe_in_table_cell` lint --- src/doc/rustdoc/src/lints.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/doc/rustdoc/src/lints.md b/src/doc/rustdoc/src/lints.md index 9dee33ef6eb85..8c284a339d210 100644 --- a/src/doc/rustdoc/src/lints.md +++ b/src/doc/rustdoc/src/lints.md @@ -456,3 +456,31 @@ note: the lint level is defined here | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: Remove explicit link instead ``` + +## `unescaped_pipe_in_table_cell` + +This lint is **warn-by-default**. It detects unescaped pipes (`|`) in table rows which +lead to some row cells being ignored. For example: + +```rust +//! | col1 | +//! | ---- | +//! | `code_with(|arg| arg)` | +``` + +Which will give: + +```text +error: table row has too many columns + --> $DIR/unescaped_pipe_in_table_cell.rs:5:18 + | +5 | //! | `code_with(|arg| arg)` | + | ^ help: any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` +note: the lint level is defined here + --> $DIR/unescaped_pipe_in_table_cell.rs:1:9 + | +1 | #![deny(rustdoc::unescaped_pipe_in_table_cell)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` From fe90bb22b85c7aadf332edf5cb46090750c855ea Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 24 Jul 2026 16:26:28 +0200 Subject: [PATCH 10/31] Rename lint `unescaped_pipe_in_table_cell` into `invalid_markdown_table` --- src/doc/rustdoc/src/lints.md | 8 ++++---- src/librustdoc/lint.rs | 6 +++--- src/librustdoc/passes/lint.rs | 4 ++-- ...able_pipe_escape.rs => invalid_markdown_table.rs} | 2 +- ...pe_in_table_cell.rs => invalid_markdown_table.rs} | 8 ++++---- ...ble_cell.stderr => invalid_markdown_table.stderr} | 12 ++++++------ 6 files changed, 20 insertions(+), 20 deletions(-) rename src/librustdoc/passes/lint/{table_pipe_escape.rs => invalid_markdown_table.rs} (98%) rename tests/rustdoc-ui/lints/{unescaped_pipe_in_table_cell.rs => invalid_markdown_table.rs} (80%) rename tests/rustdoc-ui/lints/{unescaped_pipe_in_table_cell.stderr => invalid_markdown_table.stderr} (72%) diff --git a/src/doc/rustdoc/src/lints.md b/src/doc/rustdoc/src/lints.md index 8c284a339d210..abd436bb5561c 100644 --- a/src/doc/rustdoc/src/lints.md +++ b/src/doc/rustdoc/src/lints.md @@ -457,7 +457,7 @@ note: the lint level is defined here = help: Remove explicit link instead ``` -## `unescaped_pipe_in_table_cell` +## `invalid_markdown_table` This lint is **warn-by-default**. It detects unescaped pipes (`|`) in table rows which lead to some row cells being ignored. For example: @@ -472,15 +472,15 @@ Which will give: ```text error: table row has too many columns - --> $DIR/unescaped_pipe_in_table_cell.rs:5:18 + --> $DIR/foo.rs:5:18 | 5 | //! | `code_with(|arg| arg)` | | ^ help: any content after this column divider is discarded | = help: to escape `|` characters in tables, add a `\` before them like `\|` note: the lint level is defined here - --> $DIR/unescaped_pipe_in_table_cell.rs:1:9 + --> $DIR/foo.rs:1:9 | -1 | #![deny(rustdoc::unescaped_pipe_in_table_cell)] +1 | #![deny(rustdoc::invalid_markdown_table)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs index b0dd2108c5660..5d8675aecb86a 100644 --- a/src/librustdoc/lint.rs +++ b/src/librustdoc/lint.rs @@ -214,8 +214,8 @@ declare_rustdoc_lint! { /// lead to some row cells being ignored. This is a `rustdoc` only lint, see the /// documentation in the [rustdoc book]. /// - /// [rustdoc book]: ../../../rustdoc/lints.html#unescaped_pipe_in_table_cell - UNESCAPED_PIPE_IN_TABLE_CELL, + /// [rustdoc book]: ../../../rustdoc/lints.html#invalid_markdown_table + INVALID_MARKDOWN_TABLE, Warn, "detects unescaped pipe in table rows in doc comments" } @@ -235,7 +235,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { REDUNDANT_EXPLICIT_LINKS, BROKEN_FOOTNOTE, UNUSED_FOOTNOTE_DEFINITION, - UNESCAPED_PIPE_IN_TABLE_CELL, + INVALID_MARKDOWN_TABLE, ] }); diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs index 2093ff328413d..a417bbaab4ed5 100644 --- a/src/librustdoc/passes/lint.rs +++ b/src/librustdoc/passes/lint.rs @@ -5,8 +5,8 @@ mod bare_urls; mod check_code_block_syntax; mod footnotes; mod html_tags; +mod invalid_markdown_table; mod redundant_explicit_links; -mod table_pipe_escape; mod unescaped_backticks; use super::Pass; @@ -54,7 +54,7 @@ impl DocVisitor<'_> for Linter<'_, '_> { html_tags::visit_item(self.cx, item, hir_id, &dox); } if may_have_table { - table_pipe_escape::visit_item(self.cx, item, hir_id, &dox); + invalid_markdown_table::visit_item(self.cx, item, hir_id, &dox); } } diff --git a/src/librustdoc/passes/lint/table_pipe_escape.rs b/src/librustdoc/passes/lint/invalid_markdown_table.rs similarity index 98% rename from src/librustdoc/passes/lint/table_pipe_escape.rs rename to src/librustdoc/passes/lint/invalid_markdown_table.rs index 1ba19eabab1ac..929ff9b5f4ff2 100644 --- a/src/librustdoc/passes/lint/table_pipe_escape.rs +++ b/src/librustdoc/passes/lint/invalid_markdown_table.rs @@ -82,7 +82,7 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & &item.attrs.doc_strings, ) { cx.tcx.emit_node_span_lint( - crate::lint::UNESCAPED_PIPE_IN_TABLE_CELL, + crate::lint::INVALID_MARKDOWN_TABLE, hir_id, span, UnescapedPipeInTableCell { span }, diff --git a/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs b/tests/rustdoc-ui/lints/invalid_markdown_table.rs similarity index 80% rename from tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs rename to tests/rustdoc-ui/lints/invalid_markdown_table.rs index 1a0918340c646..fe47213da3f31 100644 --- a/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs +++ b/tests/rustdoc-ui/lints/invalid_markdown_table.rs @@ -1,18 +1,18 @@ -#![deny(rustdoc::unescaped_pipe_in_table_cell)] +#![deny(rustdoc::invalid_markdown_table)] //! | col1 | //! | ---- | //! | `code_with(|arg| arg)` | -//~^ ERROR unescaped_pipe_in_table_cell +//~^ ERROR invalid_markdown_table //! | one `|` b | -//~^ ERROR unescaped_pipe_in_table_cell +//~^ ERROR invalid_markdown_table //! // Testing another lint emission on the same doc comment. //! //! | col1 | //! | ---- | //! | `code_with(|arg| arg)` | -//~^ ERROR unescaped_pipe_in_table_cell +//~^ ERROR invalid_markdown_table // We check that the extra whitespace characters at the end of the line won't trigger // the lint. diff --git a/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr b/tests/rustdoc-ui/lints/invalid_markdown_table.stderr similarity index 72% rename from tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr rename to tests/rustdoc-ui/lints/invalid_markdown_table.stderr index 03a10c078cded..b0974dfa51b5a 100644 --- a/tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr +++ b/tests/rustdoc-ui/lints/invalid_markdown_table.stderr @@ -1,18 +1,18 @@ error: table row has too many columns - --> $DIR/unescaped_pipe_in_table_cell.rs:5:18 + --> $DIR/invalid_markdown_table.rs:5:18 | LL | //! | `code_with(|arg| arg)` | | ^ any content after this column divider is discarded | = help: to escape `|` characters in tables, add a `\` before them like `\|` note: the lint level is defined here - --> $DIR/unescaped_pipe_in_table_cell.rs:1:9 + --> $DIR/invalid_markdown_table.rs:1:9 | -LL | #![deny(rustdoc::unescaped_pipe_in_table_cell)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | #![deny(rustdoc::invalid_markdown_table)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: table row has too many columns - --> $DIR/unescaped_pipe_in_table_cell.rs:7:12 + --> $DIR/invalid_markdown_table.rs:7:12 | LL | //! | one `|` b | | ^ any content after this column divider is discarded @@ -20,7 +20,7 @@ LL | //! | one `|` b | = help: to escape `|` characters in tables, add a `\` before them like `\|` error: table row has too many columns - --> $DIR/unescaped_pipe_in_table_cell.rs:14:18 + --> $DIR/invalid_markdown_table.rs:14:18 | LL | //! | `code_with(|arg| arg)` | | ^ any content after this column divider is discarded From 67bfe3ad49f017d6cdc9cc5522eefafe550a87de Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 24 Jul 2026 17:17:27 +0200 Subject: [PATCH 11/31] Also warn in case there is content after the last table cell --- .../passes/lint/invalid_markdown_table.rs | 82 +++++++++++++------ .../lints/invalid_markdown_table.rs | 19 ++++- .../lints/invalid_markdown_table.stderr | 14 +++- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/src/librustdoc/passes/lint/invalid_markdown_table.rs b/src/librustdoc/passes/lint/invalid_markdown_table.rs index 929ff9b5f4ff2..cf04fbf82d723 100644 --- a/src/librustdoc/passes/lint/invalid_markdown_table.rs +++ b/src/librustdoc/passes/lint/invalid_markdown_table.rs @@ -14,13 +14,21 @@ use crate::html::markdown::main_body_opts; #[derive(Diagnostic)] #[diag("table row has too many columns")] -#[help("to escape `|` characters in tables, add a `\\` before them like `\\|`")] +#[help(r"to escape `|` characters in tables, add a `\` before them like `\|`")] struct UnescapedPipeInTableCell { #[primary_span] #[label("any content after this column divider is discarded")] span: rustc_span::Span, } +#[derive(Diagnostic)] +#[diag("unused content after last table cell")] +struct ContentAfterLastPipe { + #[primary_span] + #[label("this content is discarded")] + span: rustc_span::Span, +} + pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) { let mut p = Parser::new_ext(dox, main_body_opts()).into_offset_iter(); @@ -49,10 +57,17 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & // extra cells. So the only way for us to know these extra cells exist // is to compare the row's span with the last emitted cell event's span. // If the span ends don't match, then there are extra cells. - && prev_range.end + 1 != range.end + && prev_range.end + 1 < range.end { // Something seems wrong, the range diff doesn't match, some content - // was left out. We now check the number of unescaped `|`. + // was left out. + let mut after_last_cell_range = + Range { start: prev_range.end + 1, end: range.end }; + if dox[after_last_cell_range.clone()].trim().is_empty() { + // Seems all good so let's ignore it and continue;. + continue; + } + // We now check the number of unescaped `|`. let row = &dox[range.clone()]; let mut iter = row.chars(); let mut divider_count = 0; @@ -64,29 +79,46 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & } } // + 1 is to handle the `|` at the end of the table row. - if divider_count <= expected_cells + 1 - || dox[Range { start: prev_range.end + 1, end: range.end }] - .trim() - .is_empty() - { - // Seems all good so let's ignore it and continue;. - continue; - } - let last_cell_separator = - Range { start: prev_range.end, end: prev_range.end + 1 }; + let too_many_pipes = divider_count > expected_cells + 1; + + if too_many_pipes { + // Seems like a pipe was not escaped as it should have been. + let last_cell_separator = + Range { start: prev_range.end, end: prev_range.end + 1 }; - if let Some((span, _)) = source_span_for_markdown_range( - cx.tcx, - dox, - &last_cell_separator, - &item.attrs.doc_strings, - ) { - cx.tcx.emit_node_span_lint( - crate::lint::INVALID_MARKDOWN_TABLE, - hir_id, - span, - UnescapedPipeInTableCell { span }, - ); + if let Some((span, _)) = source_span_for_markdown_range( + cx.tcx, + dox, + &last_cell_separator, + &item.attrs.doc_strings, + ) { + cx.tcx.emit_node_span_lint( + crate::lint::INVALID_MARKDOWN_TABLE, + hir_id, + span, + UnescapedPipeInTableCell { span }, + ); + } + } else { + // An unclosed cell maybe? There is content after the last cell so + // let's lint about it. + let content = &dox[after_last_cell_range.clone()]; + after_last_cell_range.end -= + content.len() - content.trim_end().len(); + + if let Some((span, _)) = source_span_for_markdown_range( + cx.tcx, + dox, + &after_last_cell_range, + &item.attrs.doc_strings, + ) { + cx.tcx.emit_node_span_lint( + crate::lint::INVALID_MARKDOWN_TABLE, + hir_id, + span, + ContentAfterLastPipe { span }, + ); + } } } } diff --git a/tests/rustdoc-ui/lints/invalid_markdown_table.rs b/tests/rustdoc-ui/lints/invalid_markdown_table.rs index fe47213da3f31..8ba4da3b970a6 100644 --- a/tests/rustdoc-ui/lints/invalid_markdown_table.rs +++ b/tests/rustdoc-ui/lints/invalid_markdown_table.rs @@ -29,10 +29,25 @@ mod c { //! | a \| still same cell | } -// We check that content after a table row is ignored. +// We check that content after a table row also emits. mod d { //! | col | //! | ---- | //! | code_with | aaaaa - //^ the "aaaaa" part will be ignored. + //~^ ERROR invalid_markdown_table + //! blob + //! + //! one | two + //! -|- + //! a | b | c + //~^ ERROR invalid_markdown_table + //! a | +} + +// More cases that are ignored. +mod e { + //! | one | two | + //! |-|-| + //! | a | + //! | b } diff --git a/tests/rustdoc-ui/lints/invalid_markdown_table.stderr b/tests/rustdoc-ui/lints/invalid_markdown_table.stderr index b0974dfa51b5a..5f0b4e2416521 100644 --- a/tests/rustdoc-ui/lints/invalid_markdown_table.stderr +++ b/tests/rustdoc-ui/lints/invalid_markdown_table.stderr @@ -27,5 +27,17 @@ LL | //! | `code_with(|arg| arg)` | | = help: to escape `|` characters in tables, add a `\` before them like `\|` -error: aborting due to 3 previous errors +error: unused content after last table cell + --> $DIR/invalid_markdown_table.rs:36:22 + | +LL | //! | code_with | aaaaa + | ^^^^^^ this content is discarded + +error: unused content after last table cell + --> $DIR/invalid_markdown_table.rs:42:16 + | +LL | //! a | b | c + | ^^ this content is discarded + +error: aborting due to 5 previous errors From df734af6ae8a7fdc287c5da23fe610573bf2a761 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Fri, 24 Jul 2026 14:06:31 -0700 Subject: [PATCH 12/31] rustdoc: clean up some unneeded table lint code The essential problem is that, with this table: ```text one | ----| a | b | c a | b | a | b a | ``` And this logic: ```rust let too_many_pipes = divider_count > expected_cells + 1; ``` `expected_cells + 1` winds up as 2, so you get this warning: ```text error: unused content after last table cell --> $DIR/invalid_markdown_table.rs:81:14 | LL | //! a | b | c | ^^^^^^ this content is discarded error: unused content after last table cell --> $DIR/invalid_markdown_table.rs:83:14 | LL | //! a | b | | ^^^^ this content is discarded error: unused content after last table cell --> $DIR/invalid_markdown_table.rs:85:14 | LL | //! a | b | ^^ this content is discarded ``` We really want our warning to give the suggest-escaping flow, like this: ```text error: table row has too many columns --> $DIR/invalid_markdown_table.rs:81:13 | LL | //! a | b | c | ^ any content after this column divider is discarded | = help: to escape `|` characters in tables, add a `\` before them like `\|` error: table row has too many columns --> $DIR/invalid_markdown_table.rs:83:13 | LL | //! a | b | | ^ any content after this column divider is discarded | = help: to escape `|` characters in tables, add a `\` before them like `\|` error: unused content after last table cell --> $DIR/invalid_markdown_table.rs:85:14 | LL | //! a | b | ^^ this content is discarded ``` By only scanning the text between the end of the last cell and the row, instead of doing the entire row, we don't have to re-implement as much of pulldown-cmark's logic. --- .../passes/lint/invalid_markdown_table.rs | 33 ++++------ .../lints/invalid_markdown_table.rs | 50 +++++++++++++++ .../lints/invalid_markdown_table.stderr | 62 ++++++++++++++++++- 3 files changed, 120 insertions(+), 25 deletions(-) diff --git a/src/librustdoc/passes/lint/invalid_markdown_table.rs b/src/librustdoc/passes/lint/invalid_markdown_table.rs index cf04fbf82d723..dd44f2ec92445 100644 --- a/src/librustdoc/passes/lint/invalid_markdown_table.rs +++ b/src/librustdoc/passes/lint/invalid_markdown_table.rs @@ -33,17 +33,7 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & let mut p = Parser::new_ext(dox, main_body_opts()).into_offset_iter(); while let Some((event, _range)) = p.next() { - if let Event::Start(Tag::Table(_)) = event - && let Some((Event::Start(Tag::TableHead), _)) = p.next() - { - let mut expected_cells = 0; - while let Some((event, _)) = p.next() { - match event { - Event::End(TagEnd::TableCell) => expected_cells += 1, - Event::End(TagEnd::TableHead) => break, - _ => {} - } - } + if Event::Start(Tag::TableRow) == event { let mut prev_range = None; while let Some((event, range)) = p.next() { match event { @@ -67,21 +57,20 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & // Seems all good so let's ignore it and continue;. continue; } - // We now check the number of unescaped `|`. - let row = &dox[range.clone()]; - let mut iter = row.chars(); - let mut divider_count = 0; + // Check if any pipes appear after the end of the row. + let mut iter = dox[after_last_cell_range.clone()].bytes().peekable(); + let mut found_divider = false; while let Some(c) = iter.next() { - if c == '\\' { + // the sequence `\\|` still escapes the pipe because GFM + // processes block structures like tables in its own pass + if c == b'\\' && iter.peek() == Some(&b'|') { iter.next(); - } else if c == '|' { - divider_count += 1; + } else if c == b'|' { + found_divider = true; + break; } } - // + 1 is to handle the `|` at the end of the table row. - let too_many_pipes = divider_count > expected_cells + 1; - - if too_many_pipes { + if found_divider { // Seems like a pipe was not escaped as it should have been. let last_cell_separator = Range { start: prev_range.end, end: prev_range.end + 1 }; diff --git a/tests/rustdoc-ui/lints/invalid_markdown_table.rs b/tests/rustdoc-ui/lints/invalid_markdown_table.rs index 8ba4da3b970a6..d910af1266c19 100644 --- a/tests/rustdoc-ui/lints/invalid_markdown_table.rs +++ b/tests/rustdoc-ui/lints/invalid_markdown_table.rs @@ -27,6 +27,15 @@ mod c { //! | col | //! | ---- | //! | a \| still same cell | + //! + //! now with double-backslashes; + //! yes, it really does work this way, + //! but it's *weird* compared to what + //! you would naively expect + //! + //! | col | + //! | ---- | + //! | a \\| still same cell | } // We check that content after a table row also emits. @@ -37,6 +46,18 @@ mod d { //~^ ERROR invalid_markdown_table //! blob //! + //! | col | + //! | ---- | + //! | code_with | \| + //~^ ERROR invalid_markdown_table + //! blob + //! + //! | col | + //! | ---- | + //! | code_with | \\| + //~^ ERROR invalid_markdown_table + //! blob + //! //! one | two //! -|- //! a | b | c @@ -51,3 +72,32 @@ mod e { //! | a | //! | b } + +// Weird corner case where the table ends with a pipe, +// but doesn't start with it +mod f { + //! one | + //! ----| + //! a | b | c + //~^ ERROR invalid_markdown_table + //! a | b | + //~^ ERROR invalid_markdown_table + //! a | b + //~^ ERROR invalid_markdown_table + //! a | +} + +// Weird corner case where the table ends with a pipe, +// but doesn't start with it +mod g { + //! | one + //! |---- + //! | a | b | c + //~^ ERROR invalid_markdown_table + //! | a | b | + //~^ ERROR invalid_markdown_table + //! | a | b + //~^ ERROR invalid_markdown_table + //! | a | + //! | a +} diff --git a/tests/rustdoc-ui/lints/invalid_markdown_table.stderr b/tests/rustdoc-ui/lints/invalid_markdown_table.stderr index 5f0b4e2416521..87321c77dfdfc 100644 --- a/tests/rustdoc-ui/lints/invalid_markdown_table.stderr +++ b/tests/rustdoc-ui/lints/invalid_markdown_table.stderr @@ -28,16 +28,72 @@ LL | //! | `code_with(|arg| arg)` | = help: to escape `|` characters in tables, add a `\` before them like `\|` error: unused content after last table cell - --> $DIR/invalid_markdown_table.rs:36:22 + --> $DIR/invalid_markdown_table.rs:45:22 | LL | //! | code_with | aaaaa | ^^^^^^ this content is discarded error: unused content after last table cell - --> $DIR/invalid_markdown_table.rs:42:16 + --> $DIR/invalid_markdown_table.rs:51:22 + | +LL | //! | code_with | \| + | ^^^ this content is discarded + +error: unused content after last table cell + --> $DIR/invalid_markdown_table.rs:57:22 + | +LL | //! | code_with | \| + | ^^^^ this content is discarded + +error: unused content after last table cell + --> $DIR/invalid_markdown_table.rs:63:16 | LL | //! a | b | c | ^^ this content is discarded -error: aborting due to 5 previous errors +error: table row has too many columns + --> $DIR/invalid_markdown_table.rs:81:13 + | +LL | //! a | b | c + | ^ any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` + +error: table row has too many columns + --> $DIR/invalid_markdown_table.rs:83:13 + | +LL | //! a | b | + | ^ any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` + +error: unused content after last table cell + --> $DIR/invalid_markdown_table.rs:85:14 + | +LL | //! a | b + | ^^ this content is discarded + +error: table row has too many columns + --> $DIR/invalid_markdown_table.rs:95:14 + | +LL | //! | a | b | c + | ^ any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` + +error: table row has too many columns + --> $DIR/invalid_markdown_table.rs:97:14 + | +LL | //! | a | b | + | ^ any content after this column divider is discarded + | + = help: to escape `|` characters in tables, add a `\` before them like `\|` + +error: unused content after last table cell + --> $DIR/invalid_markdown_table.rs:99:15 + | +LL | //! | a | b + | ^^ this content is discarded + +error: aborting due to 13 previous errors From aeb0d4a35aba47b1645a89265a08871e50aeb0cd Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 24 Aug 2026 16:19:21 +0200 Subject: [PATCH 13/31] Update lint to new rustdoc table lint --- tests/rustdoc-ui/lints/invalid-html-tags.rs | 1 + .../rustdoc-ui/lints/invalid-html-tags.stderr | 62 +++++++++---------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/tests/rustdoc-ui/lints/invalid-html-tags.rs b/tests/rustdoc-ui/lints/invalid-html-tags.rs index d0aa97c9e4074..7a244e6cc58f5 100644 --- a/tests/rustdoc-ui/lints/invalid-html-tags.rs +++ b/tests/rustdoc-ui/lints/invalid-html-tags.rs @@ -1,5 +1,6 @@ #![deny(rustdoc::invalid_html_tags)] //~^ NOTE the lint level is defined here +#![allow(rustdoc::invalid_markdown_table)] //!

šŸ’©

//~^ ERROR unclosed HTML tag `p` diff --git a/tests/rustdoc-ui/lints/invalid-html-tags.stderr b/tests/rustdoc-ui/lints/invalid-html-tags.stderr index 15b88496b7557..d0830321536dd 100644 --- a/tests/rustdoc-ui/lints/invalid-html-tags.stderr +++ b/tests/rustdoc-ui/lints/invalid-html-tags.stderr @@ -1,5 +1,5 @@ error: unclosed HTML tag `p` - --> $DIR/invalid-html-tags.rs:4:5 + --> $DIR/invalid-html-tags.rs:5:5 | LL | //!

šŸ’©

| ^^^ @@ -11,115 +11,115 @@ LL | #![deny(rustdoc::invalid_html_tags)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unclosed HTML tag `p` - --> $DIR/invalid-html-tags.rs:4:9 + --> $DIR/invalid-html-tags.rs:5:9 | LL | //!

šŸ’©

| ^^^ error: unclosed HTML tag `unknown` - --> $DIR/invalid-html-tags.rs:12:5 + --> $DIR/invalid-html-tags.rs:13:5 | LL | /// | ^^^^^^^^^ error: unclosed HTML tag `script` - --> $DIR/invalid-html-tags.rs:15:5 + --> $DIR/invalid-html-tags.rs:16:5 | LL | ///