lex_analysis/
diagnostics.rs

1use crate::inline::{extract_inline_spans, InlineSpanKind};
2use crate::utils::{collect_all_annotations, 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    // 1. Collect all footnote references
27    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                // We handle numeric footnotes primarily as per request, but let's track labels too if needed.
36                // For now, the user specifically mentioned numeric reordering and validation.
37                // Let's stick to numeric for the specific "footnote" validation if the user context implies it.
38                // Actually, the user said "add diagnotics for mismatched footnotes".
39                // Let's handle both if possible, but the renumbering task implies numeric.
40            }
41        }
42    });
43
44    // 2. Collect all footnote definitions (annotations with labels that look like numbers?)
45    // Or does the user use `:: note ::`?
46    // In the known `lex` format, footnotes can be annotations with specific labels or maybe list items?
47    // The user example in `references.rs` tests used `:: note ::` and `[^note]`.
48    // But `[1]` usually corresponds to something.
49    // Let's assume for `[N]` references, we expect an annotation with label "N" or a list item starting with "N."?
50    // The user said: "add completion for footnotes refernce [<integer]... then create the respective note item at the end".
51    // And "re-order the number for the footnote itself".
52    // This implies the definition is likely an annotation with a numeric label, e.g. `:: 1 ::`.
53
54    let annotations = collect_all_annotations(document);
55    let mut definitions = std::collections::HashMap::new();
56
57    for annotation in &annotations {
58        let label = &annotation.data.label.value;
59        if let Ok(number) = label.parse::<u32>() {
60            definitions.insert(number, annotation);
61        }
62    }
63
64    // 3. Check for missing definitions
65    for (number, range) in &references {
66        if !definitions.contains_key(number) {
67            diagnostics.push(AnalysisDiagnostic {
68                range: range.clone(),
69                kind: DiagnosticKind::MissingFootnoteDefinition,
70                message: format!("Footnote [{}] is referenced but not defined", number),
71            });
72        }
73    }
74
75    // 4. Check for unused definitions
76    // The user said "footnotes without refs are ok", but let's see.
77    // "add diagnotics for mismatched footnotes (both a ref that has no footnote content , footnotes without refs are ok...)"
78    // Wait, "footnotes without refs are ok". So I should NOT warn on unused definitions.
79    // "both a ref that has no footnote content , footnotes without refs are ok"
80    // Rethink: "mismatched footnotes (both a ref that has no footnote content , footnotes without refs are ok)"
81    // This phrasing is slightly contradictory or I'm misparsing.
82    // "mismatched footnotes (both a ref that has no footnote content...)" -> Ref exists, Def missing.
83    // "...footnotes without refs are ok" -> Def exists, Ref missing is OK.
84    // So ONLY Ref -> Missing Def is an error.
85
86    // However, usually "mismatched" implies both directions.
87    // If the user explicitly said "footnotes without refs are ok", I will respect that.
88    // I will implementation missing definition check.
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use lex_core::lex::parsing;
95
96    fn parse(source: &str) -> Document {
97        parsing::parse_document(source).expect("parse failed")
98    }
99
100    #[test]
101    fn detects_missing_footnote_definition() {
102        let doc = parse("Text with [1] reference.");
103        let diags = analyze(&doc);
104        assert_eq!(diags.len(), 1);
105        assert_eq!(diags[0].kind, DiagnosticKind::MissingFootnoteDefinition);
106    }
107
108    #[test]
109    fn ignores_valid_footnote() {
110        let doc = parse("Text [1].\n\n:: 1 ::\nNote.\n::\n");
111        let diags = analyze(&doc);
112        assert_eq!(diags.len(), 0);
113    }
114}