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