Skip to content

Commit ff4bc60

Browse files
committed
Unrolled build for #159583 in rollup 161706
Rollup merge of #159583 - GuillaumeGomez:unescaped_pipe_in_table_cell, r=Urgau,notriddle,camelid Add new `invalid_markdown_table` rustdoc lint Fixes #159186. r? @Urgau
2 parents e776960 + aeb0d4a commit ff4bc60

8 files changed

Lines changed: 399 additions & 31 deletions

File tree

‎src/doc/rustdoc/src/lints.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,31 @@ note: the lint level is defined here
456456
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
457457
= help: Remove explicit link instead
458458
```
459+
460+
## `invalid_markdown_table`
461+
462+
This lint is **warn-by-default**. It detects unescaped pipes (`|`) in table rows which
463+
lead to some row cells being ignored. For example:
464+
465+
```rust
466+
//! | col1 |
467+
//! | ---- |
468+
//! | `code_with(|arg| arg)` |
469+
```
470+
471+
Which will give:
472+
473+
```text
474+
error: table row has too many columns
475+
--> $DIR/foo.rs:5:18
476+
|
477+
5 | //! | `code_with(|arg| arg)` |
478+
| ^ help: any content after this column divider is discarded
479+
|
480+
= help: to escape `|` characters in tables, add a `\` before them like `\|`
481+
note: the lint level is defined here
482+
--> $DIR/foo.rs:1:9
483+
|
484+
1 | #![deny(rustdoc::invalid_markdown_table)]
485+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
486+
```

‎src/librustdoc/lint.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,17 @@ declare_rustdoc_lint! {
209209
"detects unused footnote definitions"
210210
}
211211

212+
declare_rustdoc_lint! {
213+
/// This lint is **warn-by-default**. It detects unescaped pipes in table rows which
214+
/// lead to some row cells being ignored. This is a `rustdoc` only lint, see the
215+
/// documentation in the [rustdoc book].
216+
///
217+
/// [rustdoc book]: ../../../rustdoc/lints.html#invalid_markdown_table
218+
INVALID_MARKDOWN_TABLE,
219+
Warn,
220+
"detects unescaped pipe in table rows in doc comments"
221+
}
222+
212223
pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
213224
vec![
214225
BROKEN_INTRA_DOC_LINKS,
@@ -224,6 +235,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
224235
REDUNDANT_EXPLICIT_LINKS,
225236
BROKEN_FOOTNOTE,
226237
UNUSED_FOOTNOTE_DEFINITION,
238+
INVALID_MARKDOWN_TABLE,
227239
]
228240
});
229241

‎src/librustdoc/passes/lint.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod bare_urls;
55
mod check_code_block_syntax;
66
mod footnotes;
77
mod html_tags;
8+
mod invalid_markdown_table;
89
mod redundant_explicit_links;
910
mod unescaped_backticks;
1011

@@ -35,6 +36,7 @@ impl DocVisitor<'_> for Linter<'_, '_> {
3536
if !dox.is_empty() {
3637
let may_have_link = dox.contains(&[':', '['][..]);
3738
let may_have_block_comment_or_html = dox.contains(['<', '>']);
39+
let may_have_table = dox.contains(&['|'][..]);
3840
// ~~~rust
3941
// // This is a real, supported commonmark syntax for block code
4042
// ~~~
@@ -51,6 +53,9 @@ impl DocVisitor<'_> for Linter<'_, '_> {
5153
if may_have_block_comment_or_html {
5254
html_tags::visit_item(self.cx, item, hir_id, &dox);
5355
}
56+
if may_have_table {
57+
invalid_markdown_table::visit_item(self.cx, item, hir_id, &dox);
58+
}
5459
}
5560

5661
self.visit_item_recur(item)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//! Detects table rows where some content seems to have been discarded because there are too many
2+
//! pipe characters.
3+
4+
use std::ops::Range;
5+
6+
use rustc_hir::HirId;
7+
use rustc_macros::Diagnostic;
8+
use rustc_resolve::rustdoc::pulldown_cmark::{Event, Parser, Tag, TagEnd};
9+
use rustc_resolve::rustdoc::source_span_for_markdown_range;
10+
11+
use crate::clean::*;
12+
use crate::core::DocContext;
13+
use crate::html::markdown::main_body_opts;
14+
15+
#[derive(Diagnostic)]
16+
#[diag("table row has too many columns")]
17+
#[help(r"to escape `|` characters in tables, add a `\` before them like `\|`")]
18+
struct UnescapedPipeInTableCell {
19+
#[primary_span]
20+
#[label("any content after this column divider is discarded")]
21+
span: rustc_span::Span,
22+
}
23+
24+
#[derive(Diagnostic)]
25+
#[diag("unused content after last table cell")]
26+
struct ContentAfterLastPipe {
27+
#[primary_span]
28+
#[label("this content is discarded")]
29+
span: rustc_span::Span,
30+
}
31+
32+
pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
33+
let mut p = Parser::new_ext(dox, main_body_opts()).into_offset_iter();
34+
35+
while let Some((event, _range)) = p.next() {
36+
if Event::Start(Tag::TableRow) == event {
37+
let mut prev_range = None;
38+
while let Some((event, range)) = p.next() {
39+
match event {
40+
Event::End(TagEnd::TableCell) => {
41+
prev_range = Some(range);
42+
}
43+
Event::End(TagEnd::TableRow) => {
44+
if let Some(prev_range) = &prev_range
45+
// So here what is happening: when `pulldown-cmark` is parsing a table
46+
// and a table row has too many cells, it doesn't emit events for the
47+
// extra cells. So the only way for us to know these extra cells exist
48+
// is to compare the row's span with the last emitted cell event's span.
49+
// If the span ends don't match, then there are extra cells.
50+
&& prev_range.end + 1 < range.end
51+
{
52+
// Something seems wrong, the range diff doesn't match, some content
53+
// was left out.
54+
let mut after_last_cell_range =
55+
Range { start: prev_range.end + 1, end: range.end };
56+
if dox[after_last_cell_range.clone()].trim().is_empty() {
57+
// Seems all good so let's ignore it and continue;.
58+
continue;
59+
}
60+
// Check if any pipes appear after the end of the row.
61+
let mut iter = dox[after_last_cell_range.clone()].bytes().peekable();
62+
let mut found_divider = false;
63+
while let Some(c) = iter.next() {
64+
// the sequence `\\|` still escapes the pipe because GFM
65+
// processes block structures like tables in its own pass
66+
if c == b'\\' && iter.peek() == Some(&b'|') {
67+
iter.next();
68+
} else if c == b'|' {
69+
found_divider = true;
70+
break;
71+
}
72+
}
73+
if found_divider {
74+
// Seems like a pipe was not escaped as it should have been.
75+
let last_cell_separator =
76+
Range { start: prev_range.end, end: prev_range.end + 1 };
77+
78+
if let Some((span, _)) = source_span_for_markdown_range(
79+
cx.tcx,
80+
dox,
81+
&last_cell_separator,
82+
&item.attrs.doc_strings,
83+
) {
84+
cx.tcx.emit_node_span_lint(
85+
crate::lint::INVALID_MARKDOWN_TABLE,
86+
hir_id,
87+
span,
88+
UnescapedPipeInTableCell { span },
89+
);
90+
}
91+
} else {
92+
// An unclosed cell maybe? There is content after the last cell so
93+
// let's lint about it.
94+
let content = &dox[after_last_cell_range.clone()];
95+
after_last_cell_range.end -=
96+
content.len() - content.trim_end().len();
97+
98+
if let Some((span, _)) = source_span_for_markdown_range(
99+
cx.tcx,
100+
dox,
101+
&after_last_cell_range,
102+
&item.attrs.doc_strings,
103+
) {
104+
cx.tcx.emit_node_span_lint(
105+
crate::lint::INVALID_MARKDOWN_TABLE,
106+
hir_id,
107+
span,
108+
ContentAfterLastPipe { span },
109+
);
110+
}
111+
}
112+
}
113+
}
114+
Event::End(TagEnd::Table) => break,
115+
_ => {}
116+
}
117+
}
118+
}
119+
}
120+
}

‎tests/rustdoc-ui/lints/invalid-html-tags.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![deny(rustdoc::invalid_html_tags)]
22
//~^ NOTE the lint level is defined here
3+
#![allow(rustdoc::invalid_markdown_table)]
34

45
//! <p>💩<p>
56
//~^ ERROR unclosed HTML tag `p`

0 commit comments

Comments
 (0)