Skip to main content

rustdoc/passes/lint/
footnotes.rs

1use 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            // Can't nest things like this.
105            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            // Can't have line breaks in footnote refs
112            break;
113        }
114        i += 1;
115    }
116    None
117}