rustdoc/passes/lint/
unportable_markdown.rs1use 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 let mut spaceless_block_quotes = BTreeSet::new();
35
36 let mut missing_footnote_references = BTreeMap::new();
41 let mut found_footnote_references = BTreeSet::new();
42
43 {
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 {
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 && !dox[span.clone()].starts_with("> ")
78 {
79 spaceless_block_quotes.remove(&span.start);
80 }
81 if let cmarko::Event::FootnoteReference(_) = event
82 && !found_footnote_references.contains(&(span.start + 1))
83 {
84 missing_footnote_references.insert(span.start + 1, span);
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}