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}