1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Detects specific markdown syntax that's different between pulldown-cmark
//! 0.9 and 0.11.
//!
//! This is a mitigation for old parser bugs that affected some
//! real crates' docs. The old parser claimed to comply with CommonMark,
//! but it did not. These warnings will eventually be removed,
//! though some of them may become Clippy lints.
//!
//! <https://github.com/rust-lang/rust/pull/121659#issuecomment-1992752820>
//!
//! <https://rustc-dev-guide.rust-lang.org/bug-fix-procedure.html#add-the-lint-to-the-list-of-removed-lists>

use crate::clean::Item;
use crate::core::DocContext;
use pulldown_cmark as cmarkn;
use pulldown_cmark_old as cmarko;
use rustc_lint_defs::Applicability;
use rustc_resolve::rustdoc::source_span_for_markdown_range;
use std::collections::{BTreeMap, BTreeSet};

pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
    let tcx = cx.tcx;
    let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) else {
        // If non-local, no need to check anything.
        return;
    };

    let dox = item.doc_value();
    if dox.is_empty() {
        return;
    }

    // P1: unintended strikethrough was fixed by requiring single-tildes to flank
    // the same way underscores do, so nothing is done here

    // P2: block quotes without following space parsed wrong
    //
    // This is the set of starting points for block quotes with no space after
    // the `>`. It is populated by the new parser, and if the old parser fails to
    // clear it out, it'll produce a warning.
    let mut spaceless_block_quotes = BTreeSet::new();

    // P3: missing footnote references
    //
    // This is populated by listening for FootnoteReference from
    // the new parser and old parser.
    let mut missing_footnote_references = BTreeMap::new();
    let mut found_footnote_references = BTreeSet::new();

    // populate problem cases from new parser
    {
        pub fn main_body_opts_new() -> cmarkn::Options {
            cmarkn::Options::ENABLE_TABLES
                | cmarkn::Options::ENABLE_FOOTNOTES
                | cmarkn::Options::ENABLE_STRIKETHROUGH
                | cmarkn::Options::ENABLE_TASKLISTS
                | cmarkn::Options::ENABLE_SMART_PUNCTUATION
        }
        let mut parser_new = cmarkn::Parser::new_ext(&dox, main_body_opts_new()).into_offset_iter();
        while let Some((event, span)) = parser_new.next() {
            if let cmarkn::Event::Start(cmarkn::Tag::BlockQuote(_)) = event {
                if !dox[span.clone()].starts_with("> ") {
                    spaceless_block_quotes.insert(span.start);
                }
            }
            if let cmarkn::Event::FootnoteReference(_) = event {
                found_footnote_references.insert(span.start + 1);
            }
        }
    }

    // remove cases where they don't actually differ
    {
        pub fn main_body_opts_old() -> cmarko::Options {
            cmarko::Options::ENABLE_TABLES
                | cmarko::Options::ENABLE_FOOTNOTES
                | cmarko::Options::ENABLE_STRIKETHROUGH
                | cmarko::Options::ENABLE_TASKLISTS
                | cmarko::Options::ENABLE_SMART_PUNCTUATION
        }
        let mut parser_old = cmarko::Parser::new_ext(&dox, main_body_opts_old()).into_offset_iter();
        while let Some((event, span)) = parser_old.next() {
            if let cmarko::Event::Start(cmarko::Tag::BlockQuote) = event {
                if !dox[span.clone()].starts_with("> ") {
                    spaceless_block_quotes.remove(&span.start);
                }
            }
            if let cmarko::Event::FootnoteReference(_) = event {
                if !found_footnote_references.contains(&(span.start + 1)) {
                    missing_footnote_references.insert(span.start + 1, span);
                }
            }
        }
    }

    for start in spaceless_block_quotes {
        let (span, precise) =
            source_span_for_markdown_range(tcx, &dox, &(start..start + 1), &item.attrs.doc_strings)
                .map(|span| (span, true))
                .unwrap_or_else(|| (item.attr_span(tcx), false));

        tcx.node_span_lint(crate::lint::UNPORTABLE_MARKDOWN, hir_id, span, |lint| {
            lint.primary_message("unportable markdown");
            lint.help(format!("confusing block quote with no space after the `>` marker"));
            if precise {
                lint.span_suggestion(
                    span.shrink_to_hi(),
                    "if the quote is intended, add a space",
                    " ",
                    Applicability::MaybeIncorrect,
                );
                lint.span_suggestion(
                    span.shrink_to_lo(),
                    "if it should not be a quote, escape it",
                    "\\",
                    Applicability::MaybeIncorrect,
                );
            }
        });
    }
    for (_caret, span) in missing_footnote_references {
        let (ref_span, precise) =
            source_span_for_markdown_range(tcx, &dox, &span, &item.attrs.doc_strings)
                .map(|span| (span, true))
                .unwrap_or_else(|| (item.attr_span(tcx), false));

        tcx.node_span_lint(crate::lint::UNPORTABLE_MARKDOWN, hir_id, ref_span, |lint| {
            lint.primary_message("unportable markdown");
            if precise {
                lint.span_suggestion(
                    ref_span.shrink_to_lo(),
                    "if it should not be a footnote, escape it",
                    "\\",
                    Applicability::MaybeIncorrect,
                );
            }
            if dox.as_bytes().get(span.end) == Some(&b'[') {
                lint.help("confusing footnote reference and link");
                if precise {
                    lint.span_suggestion(
                        ref_span.shrink_to_hi(),
                        "if the footnote is intended, add a space",
                        " ",
                        Applicability::MaybeIncorrect,
                    );
                } else {
                    lint.help("there should be a space between the link and the footnote");
                }
            }
        });
    }
}