rustdoc/passes/lint/
unportable_markdown.rs

1//! Detects specific markdown syntax that's different between pulldown-cmark
2//! 0.9 and 0.11.
3//!
4//! This is a mitigation for old parser bugs that affected some
5//! real crates' docs. The old parser claimed to comply with CommonMark,
6//! but it did not. These warnings will eventually be removed,
7//! though some of them may become Clippy lints.
8//!
9//! <https://github.com/rust-lang/rust/pull/121659#issuecomment-1992752820>
10//!
11//! <https://rustc-dev-guide.rust-lang.org/bug-fix-procedure.html#add-the-lint-to-the-list-of-removed-lists>
12
13use std::collections::{BTreeMap, BTreeSet};
14
15use rustc_hir::HirId;
16use rustc_lint_defs::Applicability;
17use rustc_resolve::rustdoc::source_span_for_markdown_range;
18use {pulldown_cmark as cmarkn, pulldown_cmark_old as cmarko};
19
20use crate::clean::Item;
21use crate::core::DocContext;
22
23pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
24    let tcx = cx.tcx;
25
26    // P1: unintended strikethrough was fixed by requiring single-tildes to flank
27    // the same way underscores do, so nothing is done here
28
29    // P2: block quotes without following space parsed wrong
30    //
31    // This is the set of starting points for block quotes with no space after
32    // the `>`. It is populated by the new parser, and if the old parser fails to
33    // clear it out, it'll produce a warning.
34    let mut spaceless_block_quotes = BTreeSet::new();
35
36    // P3: missing footnote references
37    //
38    // This is populated by listening for FootnoteReference from
39    // the new parser and old parser.
40    let mut missing_footnote_references = BTreeMap::new();
41    let mut found_footnote_references = BTreeSet::new();
42
43    // populate problem cases from new parser
44    {
45        pub fn main_body_opts_new() -> cmarkn::Options {
46            cmarkn::Options::ENABLE_TABLES
47                | cmarkn::Options::ENABLE_FOOTNOTES
48                | cmarkn::Options::ENABLE_STRIKETHROUGH
49                | cmarkn::Options::ENABLE_TASKLISTS
50                | cmarkn::Options::ENABLE_SMART_PUNCTUATION
51        }
52        let parser_new = cmarkn::Parser::new_ext(dox, main_body_opts_new()).into_offset_iter();
53        for (event, span) in parser_new {
54            if let cmarkn::Event::Start(cmarkn::Tag::BlockQuote(_)) = event {
55                if !dox[span.clone()].starts_with("> ") {
56                    spaceless_block_quotes.insert(span.start);
57                }
58            }
59            if let cmarkn::Event::FootnoteReference(_) = event {
60                found_footnote_references.insert(span.start + 1);
61            }
62        }
63    }
64
65    // remove cases where they don't actually differ
66    {
67        pub fn main_body_opts_old() -> cmarko::Options {
68            cmarko::Options::ENABLE_TABLES
69                | cmarko::Options::ENABLE_FOOTNOTES
70                | cmarko::Options::ENABLE_STRIKETHROUGH
71                | cmarko::Options::ENABLE_TASKLISTS
72                | cmarko::Options::ENABLE_SMART_PUNCTUATION
73        }
74        let parser_old = cmarko::Parser::new_ext(dox, main_body_opts_old()).into_offset_iter();
75        for (event, span) in parser_old {
76            if let cmarko::Event::Start(cmarko::Tag::BlockQuote) = event {
77                if !dox[span.clone()].starts_with("> ") {
78                    spaceless_block_quotes.remove(&span.start);
79                }
80            }
81            if let cmarko::Event::FootnoteReference(_) = event {
82                if !found_footnote_references.contains(&(span.start + 1)) {
83                    missing_footnote_references.insert(span.start + 1, span);
84                }
85            }
86        }
87    }
88
89    for start in spaceless_block_quotes {
90        let (span, precise) =
91            source_span_for_markdown_range(tcx, dox, &(start..start + 1), &item.attrs.doc_strings)
92                .map(|span| (span, true))
93                .unwrap_or_else(|| (item.attr_span(tcx), false));
94
95        tcx.node_span_lint(crate::lint::UNPORTABLE_MARKDOWN, hir_id, span, |lint| {
96            lint.primary_message("unportable markdown");
97            lint.help("confusing block quote with no space after the `>` marker".to_string());
98            if precise {
99                lint.span_suggestion(
100                    span.shrink_to_hi(),
101                    "if the quote is intended, add a space",
102                    " ",
103                    Applicability::MaybeIncorrect,
104                );
105                lint.span_suggestion(
106                    span.shrink_to_lo(),
107                    "if it should not be a quote, escape it",
108                    "\\",
109                    Applicability::MaybeIncorrect,
110                );
111            }
112        });
113    }
114    for (_caret, span) in missing_footnote_references {
115        let (ref_span, precise) =
116            source_span_for_markdown_range(tcx, dox, &span, &item.attrs.doc_strings)
117                .map(|span| (span, true))
118                .unwrap_or_else(|| (item.attr_span(tcx), false));
119
120        tcx.node_span_lint(crate::lint::UNPORTABLE_MARKDOWN, hir_id, ref_span, |lint| {
121            lint.primary_message("unportable markdown");
122            if precise {
123                lint.span_suggestion(
124                    ref_span.shrink_to_lo(),
125                    "if it should not be a footnote, escape it",
126                    "\\",
127                    Applicability::MaybeIncorrect,
128                );
129            }
130            if dox.as_bytes().get(span.end) == Some(&b'[') {
131                lint.help("confusing footnote reference and link");
132                if precise {
133                    lint.span_suggestion(
134                        ref_span.shrink_to_hi(),
135                        "if the footnote is intended, add a space",
136                        " ",
137                        Applicability::MaybeIncorrect,
138                    );
139                } else {
140                    lint.help("there should be a space between the link and the footnote");
141                }
142            }
143        });
144    }
145}