lex_analysis/
diagnostics.rs1use crate::inline::{extract_inline_spans, InlineSpanKind};
2use crate::utils::for_each_text_content;
3use lex_core::lex::ast::{Document, Range};
4use lex_core::lex::inlines::ReferenceType;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum DiagnosticKind {
8 MissingFootnoteDefinition,
9 UnusedFootnoteDefinition,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct AnalysisDiagnostic {
14 pub range: Range,
15 pub kind: DiagnosticKind,
16 pub message: String,
17}
18
19pub fn analyze(document: &Document) -> Vec<AnalysisDiagnostic> {
20 let mut diagnostics = Vec::new();
21 check_footnotes(document, &mut diagnostics);
22 diagnostics
23}
24
25fn check_footnotes(document: &Document, diagnostics: &mut Vec<AnalysisDiagnostic>) {
26 let mut references = Vec::new();
28 for_each_text_content(document, &mut |text| {
29 for span in extract_inline_spans(text) {
30 if let InlineSpanKind::Reference(ReferenceType::FootnoteNumber { number }) = span.kind {
31 references.push((number, span.range));
32 } else if let InlineSpanKind::Reference(ReferenceType::FootnoteLabeled { label: _ }) =
33 span.kind
34 {
35 }
41 }
42 });
43
44 let definitions_list = crate::utils::collect_footnote_definitions(document);
46 let mut definitions = std::collections::HashSet::new();
47
48 for (label, _) in definitions_list {
49 if let Ok(number) = label.parse::<u32>() {
50 definitions.insert(number);
51 }
52 }
53
54 for (number, range) in &references {
56 if !definitions.contains(number) {
57 diagnostics.push(AnalysisDiagnostic {
58 range: range.clone(),
59 kind: DiagnosticKind::MissingFootnoteDefinition,
60 message: format!("Footnote [{number}] is referenced but not defined"),
61 });
62 }
63 }
64
65 }
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use lex_core::lex::parsing;
72
73 fn parse(source: &str) -> Document {
74 parsing::parse_document(source).expect("parse failed")
75 }
76
77 #[test]
78 fn detects_missing_footnote_definition() {
79 let doc = parse("Text with [1] reference.");
80 let diags = analyze(&doc);
81 assert_eq!(diags.len(), 1);
82 assert_eq!(diags[0].kind, DiagnosticKind::MissingFootnoteDefinition);
83 }
84
85 #[test]
86 fn ignores_valid_footnote() {
87 let doc = parse("Text [1].\n\n:: 1 ::\nNote.\n::\n");
88 let diags = analyze(&doc);
89 assert_eq!(diags.len(), 0);
90 }
91
92 #[test]
93 fn ignores_valid_list_footnote() {
94 let doc = parse("Text [1].\n\nNotes\n\n1. Note.\n");
96 let diags = analyze(&doc);
97 assert_eq!(diags.len(), 0);
98 }
99}