rustdoc/passes/lint/invalid_markdown_table.rs
1//! Detects table rows where some content seems to have been discarded because there are too many
2//! pipe characters.
3
4use std::ops::Range;
5
6use rustc_hir::HirId;
7use rustc_macros::Diagnostic;
8use rustc_resolve::rustdoc::pulldown_cmark::{Event, Parser, Tag, TagEnd};
9use rustc_resolve::rustdoc::source_span_for_markdown_range;
10
11use crate::clean::*;
12use crate::core::DocContext;
13use 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 `\|`")]
18struct 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")]
26struct ContentAfterLastPipe {
27 #[primary_span]
28 #[label("this content is discarded")]
29 span: rustc_span::Span,
30}
31
32pub(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}