Skip to main content

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}