From 299213ffae4d994943719d3ced29421b247dd66b Mon Sep 17 00:00:00 2001 From: Augie Fackler Date: Fri, 14 Aug 2026 10:43:40 -0400 Subject: [PATCH 01/19] 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 02/19] 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 226b17508f45632d1c962b228718e072d03a4037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 24 Aug 2026 08:04:10 +0200 Subject: [PATCH 03/19] Check for missing rustfmt in the stdarch intrinsic test step sooner --- src/bootstrap/src/core/build_steps/test.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs index e308801b2548f..31b31ecb89c46 100644 --- a/src/bootstrap/src/core/build_steps/test.rs +++ b/src/bootstrap/src/core/build_steps/test.rs @@ -1102,6 +1102,14 @@ impl CommandLineStep for IntrinsicTest { builder.info(&format!("Skipping intrinsic-test, as it is not available for {host}")); return; } + // intrinsic-test shells out to `cargo` and `rustfmt` make bootstrap's + // managed binaries findable by prepending their dirs to PATH. + let Some(rustfmt_path) = builder.ensure(InternalRustfmt) else { + eprintln!( + "WARNING: intrinsic-test skipped because rustfmt is required but not available on this channel" + ); + return; + }; let (input_file, skip_file, cflags, sde_runner) = if host.contains("x86_64-unknown-linux") { let Some(sde) = &builder.config.sde else { @@ -1172,14 +1180,6 @@ impl CommandLineStep for IntrinsicTest { cmd.arg("--cc-arg-style").arg("gcc"); cmd.env("CC", builder.cc(host)); cmd.env("CFLAGS", cflags); - // intrinsic-test shells out to `cargo` and `rustfmt` make bootstrap's - // managed binaries findable by prepending their dirs to PATH. - let Some(rustfmt_path) = builder.ensure(InternalRustfmt) else { - eprintln!( - "WARNING: intrinsic-test skipped because rustfmt is required but not available on this channel" - ); - return; - }; let mut path_dirs: Vec = Vec::new(); if let Some(cargo_dir) = builder.initial_cargo.parent() { From afcb367812e059f45c77196f039c96f03f684727 Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Mon, 24 Aug 2026 12:21:01 +0200 Subject: [PATCH 04/19] 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 7eaf48cc1fe89a38ab83ad2d427f25e9833dcd24 Mon Sep 17 00:00:00 2001 From: Jakob Koschel Date: Fri, 21 Aug 2026 14:29:22 +0000 Subject: [PATCH 05/19] Pass LargeDataThreshold to LLVM module IR Before, LargeDataThreshold was only set via `LLVMRustCreateTargetMachine`. When LTO is enabled, this information is required to be passed into the generated LLVM module IR files since the LTO linker spins up its own TargetMachine and otherwise has no knowledge of the LargeDataThreshold. Currently, `-Z large-data-threshold` has no affect at all when compiling with LTO. This commit makes sure that if `-Z large-data-threshold` is passed and not 0, it is preserved and stored into the generated LLVM IR. --- compiler/rustc_codegen_llvm/src/context.rs | 7 +++++++ compiler/rustc_codegen_llvm/src/llvm/ffi.rs | 1 + compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp | 5 +++++ tests/codegen-llvm/large_data_threshold.rs | 9 +++++++++ 4 files changed, 22 insertions(+) create mode 100644 tests/codegen-llvm/large_data_threshold.rs diff --git a/compiler/rustc_codegen_llvm/src/context.rs b/compiler/rustc_codegen_llvm/src/context.rs index 668072ff082ec..aabb4ce110d81 100644 --- a/compiler/rustc_codegen_llvm/src/context.rs +++ b/compiler/rustc_codegen_llvm/src/context.rs @@ -282,6 +282,13 @@ pub(crate) unsafe fn create_module<'ll>( llvm::LLVMRustSetModuleCodeModel(llmod, to_llvm_code_model(sess.code_model())); } + let large_data_threshold = sess.opts.unstable_opts.large_data_threshold.unwrap_or(0); + if large_data_threshold != 0 { + unsafe { + llvm::LLVMRustSetModuleLargeDataThreshold(llmod, large_data_threshold); + } + } + // If skipping the PLT is enabled, we need to add some module metadata // to ensure intrinsic calls don't use it. if !sess.needs_plt() { diff --git a/compiler/rustc_codegen_llvm/src/llvm/ffi.rs b/compiler/rustc_codegen_llvm/src/llvm/ffi.rs index 1a60b59a93525..8c9bf55b14e45 100644 --- a/compiler/rustc_codegen_llvm/src/llvm/ffi.rs +++ b/compiler/rustc_codegen_llvm/src/llvm/ffi.rs @@ -2508,6 +2508,7 @@ unsafe extern "C" { pub(crate) fn LLVMRustSetModulePICLevel(M: &Module); pub(crate) fn LLVMRustSetModulePIELevel(M: &Module); pub(crate) fn LLVMRustSetModuleCodeModel(M: &Module, Model: CodeModel); + pub(crate) fn LLVMRustSetModuleLargeDataThreshold(M: &Module, Threshold: u64); pub(crate) fn LLVMRustBufferPtr(p: &Buffer) -> *const u8; pub(crate) fn LLVMRustBufferLen(p: &Buffer) -> usize; pub(crate) fn LLVMRustBufferFree(p: &'static mut Buffer); diff --git a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp index 3ad59c53a5bf3..b2e02a0889f12 100644 --- a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp +++ b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp @@ -1179,6 +1179,11 @@ extern "C" void LLVMRustSetModuleCodeModel(LLVMModuleRef M, unwrap(M)->setCodeModel(*CM); } +extern "C" void LLVMRustSetModuleLargeDataThreshold(LLVMModuleRef M, + uint64_t Threshold) { + unwrap(M)->setLargeDataThreshold(Threshold); +} + // Here you'll find an implementation of ThinLTO as used by the Rust compiler // right now. This ThinLTO support is only enabled on "recent ish" versions of // LLVM, and otherwise it's just blanket rejected from other compilers. diff --git a/tests/codegen-llvm/large_data_threshold.rs b/tests/codegen-llvm/large_data_threshold.rs new file mode 100644 index 0000000000000..a63a333985cca --- /dev/null +++ b/tests/codegen-llvm/large_data_threshold.rs @@ -0,0 +1,9 @@ +//@ only-x86_64 +//@ revisions: DEFAULT EXPLICIT +//@[DEFAULT] compile-flags: -C code-model=medium +//@[EXPLICIT] compile-flags: -C code-model=medium -Z large-data-threshold=1024 + +#![crate_type = "lib"] + +// DEFAULT-NOT: !"Large Data Threshold" +// EXPLICIT: !{{[0-9]+}} = !{i32 1, !"Large Data Threshold", i64 1024} From 97967dee57e5f5d09263bdfb816dca03dae0b10c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 20 Jul 2026 00:34:00 +0200 Subject: [PATCH 06/19] 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 07/19] 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 08/19] 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 09/19] 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 10/19] 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 11/19] 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 12/19] 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 | ///