rustdoc/passes/lint/
footnotes.rs1use std::ops::Range;
2
3use rustc_data_structures::fx::{FxHashMap, FxHashSet};
4use rustc_errors::DiagDecorator;
5use rustc_hir::HirId;
6use rustc_lint_defs::Applicability;
7use rustc_resolve::rustdoc::pulldown_cmark::{Event, Options, Parser, Tag};
8use rustc_resolve::rustdoc::source_span_for_markdown_range;
9
10use crate::clean::Item;
11use crate::core::DocContext;
12
13pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
14 let tcx = cx.tcx;
15
16 let mut missing_footnote_references = FxHashSet::default();
17 let mut footnote_references = FxHashSet::default();
18 let mut footnote_definitions = FxHashMap::default();
19
20 let options = Options::ENABLE_FOOTNOTES;
21 let mut parser = Parser::new_ext(dox, options).into_offset_iter().peekable();
22 while let Some((event, span)) = parser.next() {
23 match event {
24 Event::Text(text)
25 if &*text == "["
26 && (span.start == 0 || dox.as_bytes().get(span.start - 1) != Some(&b'\\'))
27 && let Some(len) = scan_footnote_ref(&dox[span.start..]) =>
28 {
29 missing_footnote_references
30 .insert(Range { start: span.start, end: span.start + len });
31 }
32 Event::FootnoteReference(label) => {
33 footnote_references.insert(label);
34 }
35 Event::Start(Tag::FootnoteDefinition(label)) => {
36 footnote_definitions.insert(label, span.start + 1);
37 }
38 _ => {}
39 }
40 }
41
42 #[allow(rustc::potential_query_instability)]
43 for (footnote, span) in footnote_definitions {
44 if !footnote_references.contains(&footnote) {
45 let (span, _) = source_span_for_markdown_range(
46 tcx,
47 dox,
48 &(span..span + 1),
49 &item.attrs.doc_strings,
50 )
51 .unwrap_or_else(|| (item.attr_span(tcx), false));
52
53 tcx.emit_node_span_lint(
54 crate::lint::UNUSED_FOOTNOTE_DEFINITION,
55 hir_id,
56 span,
57 DiagDecorator(|lint| {
58 lint.primary_message("unused footnote definition");
59 }),
60 );
61 }
62 }
63
64 #[allow(rustc::potential_query_instability)]
65 for span in missing_footnote_references {
66 let ref_span = source_span_for_markdown_range(tcx, dox, &span, &item.attrs.doc_strings)
67 .map(|(span, _)| span)
68 .unwrap_or_else(|| item.attr_span(tcx));
69
70 tcx.emit_node_span_lint(
71 crate::lint::BROKEN_FOOTNOTE,
72 hir_id,
73 ref_span,
74 DiagDecorator(|lint| {
75 lint.primary_message("no footnote definition matching this footnote");
76 lint.span_suggestion(
77 ref_span.shrink_to_lo(),
78 "if it should not be a footnote, escape it",
79 "\\",
80 Applicability::MaybeIncorrect,
81 );
82 }),
83 );
84 }
85}
86
87fn scan_footnote_ref(dox: &str) -> Option<usize> {
88 let dox = dox.as_bytes();
89 let mut i = 0;
90 if dox.get(i) != Some(&b'[') {
91 return None;
92 }
93 i += 1;
94 if dox.get(i) != Some(&b'^') {
95 return None;
96 }
97 i += 1;
98 while let Some(&c) = dox.get(i) {
99 if c == b']' {
100 i += 1;
101 return Some(i);
102 }
103 if c == b'\r' || c == b'\n' || c == b'[' {
104 break;
106 }
107 if c == b'\\' {
108 i += 1;
109 }
110 if dox.get(i) == Some(&b'\r') || dox.get(i) == Some(&b'\n') {
111 break;
113 }
114 i += 1;
115 }
116 None
117}