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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/doc/rustdoc/src/lints.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)` |

@camelid camelid Jul 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow TIL that this is how GFM works. Very strange design...

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was super confused as well.

```

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)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```
12 changes: 12 additions & 0 deletions src/librustdoc/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ declare_rustdoc_lint! {
"detects redundant explicit links in doc comments"
}

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<Vec<&'static Lint>> = Lazy::new(|| {
vec![
BROKEN_INTRA_DOC_LINKS,
Expand All @@ -209,6 +220,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
MISSING_CRATE_LEVEL_DOCS,
UNESCAPED_BACKTICKS,
REDUNDANT_EXPLICIT_LINKS,
UNESCAPED_PIPE_IN_TABLE_CELL,
]
});

Expand Down
5 changes: 5 additions & 0 deletions src/librustdoc/passes/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod bare_urls;
mod check_code_block_syntax;
mod html_tags;
mod redundant_explicit_links;
mod table_pipe_escape;
mod unescaped_backticks;

use super::Pass;
Expand Down Expand Up @@ -34,6 +35,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
// ~~~
Expand All @@ -49,6 +51,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)
Expand Down
99 changes: 99 additions & 0 deletions src/librustdoc/passes/lint/table_pipe_escape.rs
Original file line number Diff line number Diff line change
@@ -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 `\\|`")]

@notriddle notriddle Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[help("to escape `|` characters in tables, add a `\\` before them like `\\|`")]
#[help(r"to escape `|` characters in tables, add a `\` before them like `\|`")]

View changes since the review

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()
Comment thread
Urgau marked this conversation as resolved.
{
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
Comment thread
Urgau marked this conversation as resolved.
{
// 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();
Comment thread
Urgau marked this conversation as resolved.
} 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,
_ => {}
}
}
}
}
}
38 changes: 38 additions & 0 deletions tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.rs

@notriddle notriddle Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Please add a test case for tables with fewer cells in a row than the head. I suppose it should be silent, but we want to make sure it doesn’t ICE or something.
| one | two |
|-|-|
| a |
| b
  • Please add test cases for tables that don’t have leading and trailing pipes. You’re parsing the table syntax, so you need to test these corner cases.
one | two
-|-
a | b | c
a |

*[View changes since the review](https://triagebot.infra.rust-lang.org/gh-changes-since/rust-lang/rust/159583/cde3f8aee5a30928872e2438e3f18238a3fa306c..6fd9b1bce2dd4cd99e32dc333104810bef776cc0)*

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#![deny(rustdoc::unescaped_pipe_in_table_cell)]

//! | col1 |
Comment thread
Urgau marked this conversation as resolved.
//! | ---- |
//! | `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.

@notriddle notriddle Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it ignored? It seems like an example of the thing you’re trying to warn about.

col
code_with

View changes since the review

}
31 changes: 31 additions & 0 deletions tests/rustdoc-ui/lints/unescaped_pipe_in_table_cell.stderr
Original file line number Diff line number Diff line change
@@ -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

Loading