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>
1213use std::collections::{BTreeMap, BTreeSet};
1415use 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};
1920use crate::clean::Item;
21use crate::core::DocContext;
2223pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
24let tcx = cx.tcx;
2526// P1: unintended strikethrough was fixed by requiring single-tildes to flank
27 // the same way underscores do, so nothing is done here
2829 // 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.
34let mut spaceless_block_quotes = BTreeSet::new();
3536// P3: missing footnote references
37 //
38 // This is populated by listening for FootnoteReference from
39 // the new parser and old parser.
40let mut missing_footnote_references = BTreeMap::new();
41let mut found_footnote_references = BTreeSet::new();
4243// populate problem cases from new parser
44{
45pub 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 }
52let parser_new = cmarkn::Parser::new_ext(dox, main_body_opts_new()).into_offset_iter();
53for (event, span) in parser_new {
54if let cmarkn::Event::Start(cmarkn::Tag::BlockQuote(_)) = event {
55if !dox[span.clone()].starts_with("> ") {
56 spaceless_block_quotes.insert(span.start);
57 }
58 }
59if let cmarkn::Event::FootnoteReference(_) = event {
60 found_footnote_references.insert(span.start + 1);
61 }
62 }
63 }
6465// remove cases where they don't actually differ
66{
67pub 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 }
74let parser_old = cmarko::Parser::new_ext(dox, main_body_opts_old()).into_offset_iter();
75for (event, span) in parser_old {
76if let cmarko::Event::Start(cmarko::Tag::BlockQuote) = event {
77if !dox[span.clone()].starts_with("> ") {
78 spaceless_block_quotes.remove(&span.start);
79 }
80 }
81if let cmarko::Event::FootnoteReference(_) = event {
82if !found_footnote_references.contains(&(span.start + 1)) {
83 missing_footnote_references.insert(span.start + 1, span);
84 }
85 }
86 }
87 }
8889for start in spaceless_block_quotes {
90let (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));
9495 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());
98if 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 }
114for (_caret, span) in missing_footnote_references {
115let (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));
119120 tcx.node_span_lint(crate::lint::UNPORTABLE_MARKDOWN, hir_id, ref_span, |lint| {
121 lint.primary_message("unportable markdown");
122if 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 }
130if dox.as_bytes().get(span.end) == Some(&b'[') {
131 lint.help("confusing footnote reference and link");
132if 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}