Skip to main content

oxilean_parse/diagnostic/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::tokens::Span;
6
7/// Synchronization token for error recovery.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SyncToken {
10    /// Semicolon `;`
11    Semicolon,
12    /// End keyword
13    End,
14    /// Declaration keyword (def, theorem, etc.)
15    Declaration,
16    /// Right brace `}`
17    RightBrace,
18    /// Right parenthesis `)`
19    RightParen,
20    /// End of file
21    Eof,
22}
23/// Renders diagnostics to human-readable strings in various formats.
24#[allow(dead_code)]
25pub struct DiagnosticRenderer {
26    /// Source text for context lines.
27    pub source: String,
28    /// Whether to include ANSI color codes.
29    pub use_color: bool,
30    /// Whether to include fix suggestions in the output.
31    pub show_fixes: bool,
32    /// Maximum number of context lines to show around each diagnostic.
33    pub context_lines: usize,
34}
35#[allow(dead_code)]
36impl DiagnosticRenderer {
37    /// Create a renderer for the given source.
38    pub fn new(source: impl Into<String>) -> Self {
39        Self {
40            source: source.into(),
41            use_color: false,
42            show_fixes: true,
43            context_lines: 1,
44        }
45    }
46    /// Enable or disable ANSI color output.
47    pub fn with_color(mut self, v: bool) -> Self {
48        self.use_color = v;
49        self
50    }
51    /// Enable or disable fix suggestion output.
52    pub fn with_show_fixes(mut self, v: bool) -> Self {
53        self.show_fixes = v;
54        self
55    }
56    /// Set the number of context lines to display.
57    pub fn with_context_lines(mut self, n: usize) -> Self {
58        self.context_lines = n;
59        self
60    }
61    /// Render a single `Diagnostic` to a string.
62    pub fn render(&self, diag: &Diagnostic) -> String {
63        let mut out = String::new();
64        let severity_tag = match diag.severity {
65            Severity::Error => "error",
66            Severity::Warning => "warning",
67            Severity::Info => "info",
68            Severity::Hint => "hint",
69        };
70        if let Some(code) = &diag.code {
71            out.push_str(&format!("{}[{}]: {}\n", severity_tag, code, diag.message));
72        } else {
73            out.push_str(&format!("{}: {}\n", severity_tag, diag.message));
74        }
75        out.push_str(&format!(" --> {}:{}\n", diag.span.line, diag.span.column));
76        let ctx = self.extract_context(diag.span.line);
77        out.push_str(&ctx);
78        let col = if diag.span.column > 0 {
79            diag.span.column - 1
80        } else {
81            0
82        };
83        let len = (diag.span.end.saturating_sub(diag.span.start)).max(1);
84        out.push_str(&format!("{}^\n", " ".repeat(col)));
85        let _ = len;
86        for label in &diag.labels {
87            out.push_str(&format!("  note: {}\n", label.text));
88        }
89        if let Some(h) = &diag.help {
90            out.push_str(&format!("  help: {}\n", h));
91        }
92        if self.show_fixes {
93            for fix in &diag.fixes {
94                out.push_str(&format!(
95                    "  suggestion: {} → `{}`\n",
96                    fix.message, fix.replacement
97                ));
98            }
99        }
100        out
101    }
102    /// Render a slice of diagnostics in order.
103    pub fn render_all(&self, diags: &[Diagnostic]) -> String {
104        diags
105            .iter()
106            .map(|d| self.render(d))
107            .collect::<Vec<_>>()
108            .join("\n")
109    }
110    /// Render only errors from a collector.
111    pub fn render_errors(&self, collector: &DiagnosticCollector) -> String {
112        let errors: Vec<&Diagnostic> = collector
113            .diagnostics()
114            .iter()
115            .filter(|d| d.is_error())
116            .collect();
117        errors
118            .iter()
119            .map(|d| self.render(d))
120            .collect::<Vec<_>>()
121            .join("\n")
122    }
123    fn extract_context(&self, line: usize) -> String {
124        if line == 0 {
125            return String::new();
126        }
127        let start_line = line.saturating_sub(self.context_lines);
128        let end_line = line + self.context_lines;
129        let lines: Vec<&str> = self.source.lines().collect();
130        let mut out = String::new();
131        for (idx, l) in lines.iter().enumerate() {
132            let lnum = idx + 1;
133            if lnum >= start_line && lnum <= end_line {
134                out.push_str(&format!("{:4} | {}\n", lnum, l));
135            }
136        }
137        out
138    }
139}
140/// Exports diagnostics to various text formats.
141#[allow(dead_code)]
142pub struct DiagnosticExporter;
143#[allow(dead_code)]
144impl DiagnosticExporter {
145    /// Export a single `Diagnostic` as a JSON-like string.
146    pub fn to_json(d: &Diagnostic) -> String {
147        let severity = match d.severity {
148            Severity::Error => "error",
149            Severity::Warning => "warning",
150            Severity::Info => "info",
151            Severity::Hint => "hint",
152        };
153        let code = d
154            .code
155            .map(|c| format!("\"{}\"", c))
156            .unwrap_or_else(|| "null".to_string());
157        format!(
158            r#"{{"severity":"{}","code":{},"message":"{}","line":{},"col":{}}}"#,
159            severity,
160            code,
161            d.message.replace('"', "\\\""),
162            d.span.line,
163            d.span.column
164        )
165    }
166    /// Export a `DiagnosticCollector` as a JSON array.
167    pub fn collector_to_json(c: &DiagnosticCollector) -> String {
168        let items: Vec<String> = c.diagnostics().iter().map(Self::to_json).collect();
169        format!("[{}]", items.join(","))
170    }
171    /// Export a `Diagnostic` as a compact one-liner.
172    pub fn to_oneliner(d: &Diagnostic) -> String {
173        format!(
174            "{}:{}: {}: {}",
175            d.span.line,
176            d.span.column,
177            match d.severity {
178                Severity::Error => "error",
179                Severity::Warning => "warning",
180                Severity::Info => "info",
181                Severity::Hint => "hint",
182            },
183            d.message
184        )
185    }
186    /// Export all diagnostics from a collector as one-liners, one per line.
187    pub fn collector_to_oneliners(c: &DiagnosticCollector) -> String {
188        c.diagnostics()
189            .iter()
190            .map(Self::to_oneliner)
191            .collect::<Vec<_>>()
192            .join("\n")
193    }
194    /// Export as a CSV line: `line,col,severity,message`.
195    pub fn to_csv(d: &Diagnostic) -> String {
196        let severity = match d.severity {
197            Severity::Error => "error",
198            Severity::Warning => "warning",
199            Severity::Info => "info",
200            Severity::Hint => "hint",
201        };
202        format!(
203            "{},{},{},\"{}\"",
204            d.span.line,
205            d.span.column,
206            severity,
207            d.message.replace('"', "\"\"")
208        )
209    }
210    /// Export all diagnostics from a collector as CSV rows (with header).
211    pub fn collector_to_csv(c: &DiagnosticCollector) -> String {
212        let mut out = "line,col,severity,message\n".to_string();
213        for d in c.diagnostics() {
214            out.push_str(&Self::to_csv(d));
215            out.push('\n');
216        }
217        out
218    }
219}
220/// Aggregated statistics over a `DiagnosticCollector`.
221#[allow(dead_code)]
222#[derive(Debug, Clone, Default)]
223pub struct DiagnosticStats {
224    /// Total errors.
225    pub errors: usize,
226    /// Total warnings.
227    pub warnings: usize,
228    /// Total infos.
229    pub infos: usize,
230    /// Total hints.
231    pub hints: usize,
232    /// Diagnostics with code.
233    pub with_code: usize,
234    /// Diagnostics with fix.
235    pub with_fix: usize,
236    /// Diagnostics with help.
237    pub with_help: usize,
238}
239#[allow(dead_code)]
240impl DiagnosticStats {
241    /// Compute stats from a `DiagnosticCollector`.
242    pub fn from_collector(c: &DiagnosticCollector) -> Self {
243        let mut s = Self::default();
244        for d in c.diagnostics() {
245            match d.severity {
246                Severity::Error => s.errors += 1,
247                Severity::Warning => s.warnings += 1,
248                Severity::Info => s.infos += 1,
249                Severity::Hint => s.hints += 1,
250            }
251            if d.code.is_some() {
252                s.with_code += 1;
253            }
254            if !d.fixes.is_empty() {
255                s.with_fix += 1;
256            }
257            if d.help.is_some() {
258                s.with_help += 1;
259            }
260        }
261        s
262    }
263    /// Total diagnostics.
264    pub fn total(&self) -> usize {
265        self.errors + self.warnings + self.infos + self.hints
266    }
267    /// True if there are any errors.
268    pub fn has_errors(&self) -> bool {
269        self.errors > 0
270    }
271    /// Format a compact summary.
272    pub fn summary(&self) -> String {
273        format!(
274            "{} errors, {} warnings, {} infos, {} hints",
275            self.errors, self.warnings, self.infos, self.hints
276        )
277    }
278}
279/// Policy that controls how errors affect compilation flow.
280#[allow(dead_code)]
281#[derive(Clone, Copy, Debug, PartialEq, Eq)]
282pub enum DiagnosticPolicy {
283    /// Abort on first error.
284    FailFast,
285    /// Collect all errors before failing.
286    CollectAll,
287    /// Treat warnings as errors.
288    WarningsAsErrors,
289    /// Never fail (permissive mode, useful for IDEs).
290    Permissive,
291}
292#[allow(dead_code)]
293impl DiagnosticPolicy {
294    /// Return `true` if the collector indicates a compilation failure under this policy.
295    pub fn should_fail(&self, c: &DiagnosticCollector) -> bool {
296        match self {
297            DiagnosticPolicy::FailFast => c.has_errors(),
298            DiagnosticPolicy::CollectAll => c.has_errors(),
299            DiagnosticPolicy::WarningsAsErrors => c.has_errors() || c.warning_count() > 0,
300            DiagnosticPolicy::Permissive => false,
301        }
302    }
303    /// Human-readable name.
304    pub fn name(&self) -> &'static str {
305        match self {
306            DiagnosticPolicy::FailFast => "fail-fast",
307            DiagnosticPolicy::CollectAll => "collect-all",
308            DiagnosticPolicy::WarningsAsErrors => "warnings-as-errors",
309            DiagnosticPolicy::Permissive => "permissive",
310        }
311    }
312}
313/// A diagnostic event for logging.
314#[allow(dead_code)]
315#[allow(missing_docs)]
316#[derive(Debug, Clone)]
317pub struct DiagnosticEvent {
318    /// The message
319    pub message: String,
320    /// A unique event ID
321    pub id: u64,
322}
323impl DiagnosticEvent {
324    /// Create a new event.
325    #[allow(dead_code)]
326    pub fn new(id: u64, message: &str) -> Self {
327        DiagnosticEvent {
328            id,
329            message: message.to_string(),
330        }
331    }
332}
333/// Diagnostic label for additional context.
334#[derive(Debug, Clone)]
335pub struct DiagnosticLabel {
336    /// Label text
337    pub text: String,
338    /// Label span
339    pub span: Span,
340}
341/// Metadata associated with a `SyncToken`.
342#[allow(dead_code)]
343pub struct SyncTokenInfo {
344    /// The sync token kind.
345    pub kind: SyncToken,
346    /// Human-readable name.
347    pub name: &'static str,
348    /// Whether this sync token terminates a statement.
349    pub is_statement_end: bool,
350}
351#[allow(dead_code)]
352impl SyncTokenInfo {
353    /// Return info for all sync token kinds.
354    pub fn all() -> &'static [SyncTokenInfo] {
355        &[
356            SyncTokenInfo {
357                kind: SyncToken::Semicolon,
358                name: "semicolon",
359                is_statement_end: true,
360            },
361            SyncTokenInfo {
362                kind: SyncToken::End,
363                name: "end",
364                is_statement_end: true,
365            },
366            SyncTokenInfo {
367                kind: SyncToken::Declaration,
368                name: "declaration keyword",
369                is_statement_end: true,
370            },
371            SyncTokenInfo {
372                kind: SyncToken::RightBrace,
373                name: "right brace",
374                is_statement_end: false,
375            },
376            SyncTokenInfo {
377                kind: SyncToken::RightParen,
378                name: "right paren",
379                is_statement_end: false,
380            },
381            SyncTokenInfo {
382                kind: SyncToken::Eof,
383                name: "end of file",
384                is_statement_end: true,
385            },
386        ]
387    }
388}
389/// Utilities for working with `Span` values in diagnostic contexts.
390#[allow(dead_code)]
391pub struct SpanUtils;
392#[allow(dead_code)]
393impl SpanUtils {
394    /// Return `true` if `inner` is contained within `outer`.
395    pub fn contains(outer: &Span, inner: &Span) -> bool {
396        outer.start <= inner.start && inner.end <= outer.end
397    }
398    /// Return `true` if two spans overlap.
399    pub fn overlaps(a: &Span, b: &Span) -> bool {
400        a.start < b.end && b.start < a.end
401    }
402    /// Merge two spans into the smallest span that covers both.
403    pub fn merge(a: &Span, b: &Span) -> Span {
404        let start = a.start.min(b.start);
405        let end = a.end.max(b.end);
406        let line = a.line.min(b.line);
407        let column = if a.line < b.line {
408            a.column
409        } else if b.line < a.line {
410            b.column
411        } else {
412            a.column.min(b.column)
413        };
414        Span::new(start, end, line, column)
415    }
416    /// Length of a span in bytes.
417    pub fn byte_len(span: &Span) -> usize {
418        span.end.saturating_sub(span.start)
419    }
420    /// True if the span is empty (zero length).
421    pub fn is_empty(span: &Span) -> bool {
422        Self::byte_len(span) == 0
423    }
424    /// Expand a span by `n` bytes in both directions, clamped to `[0, max_end]`.
425    pub fn expand(span: &Span, n: usize, max_end: usize) -> Span {
426        let start = span.start.saturating_sub(n);
427        let end = (span.end + n).min(max_end);
428        Span::new(start, end, span.line, span.column)
429    }
430    /// Extract source text covered by the span.
431    pub fn extract<'a>(span: &Span, source: &'a str) -> &'a str {
432        source.get(span.start..span.end).unwrap_or("")
433    }
434    /// Build a span from a byte range and a source string (computing line/col).
435    pub fn from_byte_range(start: usize, end: usize, source: &str) -> Span {
436        let before = &source[..start.min(source.len())];
437        let line = before.chars().filter(|&c| c == '\n').count() + 1;
438        let col = before.rfind('\n').map(|p| start - p).unwrap_or(start + 1);
439        Span::new(start, end, line, col)
440    }
441}
442/// A diagnostic severity filter.
443#[allow(dead_code)]
444#[allow(missing_docs)]
445pub struct SeverityFilter {
446    /// Minimum severity to include
447    pub min_severity: u8,
448}
449impl SeverityFilter {
450    /// Create a new filter that shows all.
451    #[allow(dead_code)]
452    pub fn all() -> Self {
453        SeverityFilter { min_severity: 0 }
454    }
455    /// Create a filter that only shows errors.
456    #[allow(dead_code)]
457    pub fn errors_only() -> Self {
458        SeverityFilter { min_severity: 2 }
459    }
460}
461/// Simple plaintext diagnostic printer (no colour, no source context).
462#[allow(dead_code)]
463pub struct DiagnosticPrinter {
464    policy: DiagnosticPolicy,
465}
466#[allow(dead_code)]
467impl DiagnosticPrinter {
468    /// Create a printer with the given policy.
469    pub fn new(policy: DiagnosticPolicy) -> Self {
470        Self { policy }
471    }
472    /// Print all diagnostics from a collector to a string.
473    pub fn print(&self, c: &DiagnosticCollector) -> String {
474        let mut out = String::new();
475        for d in c.diagnostics() {
476            out.push_str(&format!("{}\n", d));
477        }
478        out
479    }
480    /// Return `true` if compilation should be considered failed.
481    pub fn should_fail(&self, c: &DiagnosticCollector) -> bool {
482        self.policy.should_fail(c)
483    }
484}
485/// Diagnostic severity level.
486#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
487pub enum Severity {
488    /// Error - compilation cannot proceed
489    Error,
490    /// Warning - potential issue
491    Warning,
492    /// Info - informational message
493    Info,
494    /// Hint - suggestion for improvement
495    Hint,
496}
497/// Diagnostic code for structured error categories.
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum DiagnosticCode {
500    /// Unexpected token
501    E0001,
502    /// Unterminated string
503    E0002,
504    /// Unmatched bracket
505    E0003,
506    /// Missing semicolon
507    E0004,
508    /// Invalid number literal
509    E0005,
510    /// Type mismatch
511    E0100,
512    /// Undeclared variable
513    E0101,
514    /// Cannot infer type
515    E0102,
516    /// Too many arguments
517    E0103,
518    /// Too few arguments
519    E0104,
520    /// No goals to solve
521    E0200,
522    /// Tactic failed
523    E0201,
524    /// Unsolved goals
525    E0202,
526    /// Internal error
527    E0900,
528    /// Not implemented
529    E0901,
530}
531/// Fluent builder for constructing `Diagnostic` values.
532#[allow(dead_code)]
533pub struct DiagnosticBuilder {
534    severity: Severity,
535    message: String,
536    span: Span,
537    labels: Vec<DiagnosticLabel>,
538    help: Option<String>,
539    code: Option<DiagnosticCode>,
540    fixes: Vec<CodeFix>,
541}
542#[allow(dead_code)]
543impl DiagnosticBuilder {
544    /// Start building an error diagnostic.
545    pub fn error(message: impl Into<String>, span: Span) -> Self {
546        Self {
547            severity: Severity::Error,
548            message: message.into(),
549            span,
550            labels: Vec::new(),
551            help: None,
552            code: None,
553            fixes: Vec::new(),
554        }
555    }
556    /// Start building a warning diagnostic.
557    pub fn warning(message: impl Into<String>, span: Span) -> Self {
558        Self {
559            severity: Severity::Warning,
560            message: message.into(),
561            span,
562            labels: Vec::new(),
563            help: None,
564            code: None,
565            fixes: Vec::new(),
566        }
567    }
568    /// Start building an info diagnostic.
569    pub fn info(message: impl Into<String>, span: Span) -> Self {
570        Self {
571            severity: Severity::Info,
572            message: message.into(),
573            span,
574            labels: Vec::new(),
575            help: None,
576            code: None,
577            fixes: Vec::new(),
578        }
579    }
580    /// Start building a hint diagnostic.
581    pub fn hint(message: impl Into<String>, span: Span) -> Self {
582        Self {
583            severity: Severity::Hint,
584            message: message.into(),
585            span,
586            labels: Vec::new(),
587            help: None,
588            code: None,
589            fixes: Vec::new(),
590        }
591    }
592    /// Add a label.
593    pub fn label(mut self, text: impl Into<String>, span: Span) -> Self {
594        self.labels.push(DiagnosticLabel {
595            text: text.into(),
596            span,
597        });
598        self
599    }
600    /// Set help text.
601    pub fn help(mut self, h: impl Into<String>) -> Self {
602        self.help = Some(h.into());
603        self
604    }
605    /// Set the diagnostic code.
606    pub fn code(mut self, c: DiagnosticCode) -> Self {
607        self.code = Some(c);
608        self
609    }
610    /// Add a fix suggestion.
611    pub fn fix(
612        mut self,
613        message: impl Into<String>,
614        span: Span,
615        replacement: impl Into<String>,
616    ) -> Self {
617        self.fixes.push(CodeFix {
618            message: message.into(),
619            span,
620            replacement: replacement.into(),
621        });
622        self
623    }
624    /// Finalise and produce a `Diagnostic`.
625    pub fn build(self) -> Diagnostic {
626        Diagnostic {
627            severity: self.severity,
628            message: self.message,
629            span: self.span,
630            labels: self.labels,
631            help: self.help,
632            code: self.code,
633            fixes: self.fixes,
634        }
635    }
636}
637/// Diagnostic message.
638#[derive(Debug, Clone)]
639pub struct Diagnostic {
640    /// Severity level
641    pub severity: Severity,
642    /// Primary message
643    pub message: String,
644    /// Source location
645    pub span: Span,
646    /// Additional labels
647    pub labels: Vec<DiagnosticLabel>,
648    /// Help text
649    pub help: Option<String>,
650    /// Structured diagnostic code
651    pub code: Option<DiagnosticCode>,
652    /// Suggested code fixes
653    pub fixes: Vec<CodeFix>,
654}
655impl Diagnostic {
656    /// Create a new error diagnostic.
657    pub fn error(message: String, span: Span) -> Self {
658        Self {
659            severity: Severity::Error,
660            message,
661            span,
662            labels: Vec::new(),
663            help: None,
664            code: None,
665            fixes: Vec::new(),
666        }
667    }
668    /// Create a new warning diagnostic.
669    pub fn warning(message: String, span: Span) -> Self {
670        Self {
671            severity: Severity::Warning,
672            message,
673            span,
674            labels: Vec::new(),
675            help: None,
676            code: None,
677            fixes: Vec::new(),
678        }
679    }
680    /// Create a new info diagnostic.
681    pub fn info(message: String, span: Span) -> Self {
682        Self {
683            severity: Severity::Info,
684            message,
685            span,
686            labels: Vec::new(),
687            help: None,
688            code: None,
689            fixes: Vec::new(),
690        }
691    }
692    /// Create a note diagnostic (alias for info with specific semantics).
693    #[allow(dead_code)]
694    pub fn note(message: String, span: Span) -> Self {
695        Self {
696            severity: Severity::Info,
697            message,
698            span,
699            labels: Vec::new(),
700            help: None,
701            code: None,
702            fixes: Vec::new(),
703        }
704    }
705    /// Add a label to this diagnostic.
706    pub fn with_label(mut self, text: String, span: Span) -> Self {
707        self.labels.push(DiagnosticLabel { text, span });
708        self
709    }
710    /// Add help text to this diagnostic.
711    pub fn with_help(mut self, help: String) -> Self {
712        self.help = Some(help);
713        self
714    }
715    /// Set the diagnostic code.
716    #[allow(dead_code)]
717    pub fn with_code(mut self, code: DiagnosticCode) -> Self {
718        self.code = Some(code);
719        self
720    }
721    /// Add a code fix suggestion.
722    #[allow(dead_code)]
723    pub fn with_fix(mut self, fix: CodeFix) -> Self {
724        self.fixes.push(fix);
725        self
726    }
727    /// Check if this is an error.
728    pub fn is_error(&self) -> bool {
729        self.severity == Severity::Error
730    }
731    /// Check if this is a warning.
732    pub fn is_warning(&self) -> bool {
733        self.severity == Severity::Warning
734    }
735    /// Format this diagnostic with rich source context.
736    ///
737    /// Produces output similar to rustc error messages, including
738    /// the source line and an underline marker pointing to the error.
739    #[allow(dead_code)]
740    pub fn format_rich(&self, source: &str) -> String {
741        let severity_str = match self.severity {
742            Severity::Error => "error",
743            Severity::Warning => "warning",
744            Severity::Info => "info",
745            Severity::Hint => "hint",
746        };
747        let mut output = String::new();
748        if let Some(code) = &self.code {
749            output.push_str(&format!("{}[{}]: {}\n", severity_str, code, self.message));
750        } else {
751            output.push_str(&format!("{}: {}\n", severity_str, self.message));
752        }
753        output.push_str(&format!(" --> {}:{}\n", self.span.line, self.span.column));
754        let highlight = Self::format_line_highlight(source, &self.span);
755        if !highlight.is_empty() {
756            output.push_str(&highlight);
757        }
758        for label in &self.labels {
759            output.push_str(&format!("  = {}\n", label.text));
760        }
761        if let Some(help) = &self.help {
762            output.push_str(&format!("  = help: {}\n", help));
763        }
764        for fix in &self.fixes {
765            output.push_str(&format!(
766                "  = fix: {} -> `{}`\n",
767                fix.message, fix.replacement
768            ));
769        }
770        output
771    }
772    /// Format a source line with an underline highlighting the span.
773    ///
774    /// Returns a string with the source line and a caret line pointing
775    /// to the error location.
776    #[allow(dead_code)]
777    pub fn format_line_highlight(source: &str, span: &Span) -> String {
778        let lines: Vec<&str> = source.lines().collect();
779        if span.line == 0 || span.line > lines.len() {
780            return String::new();
781        }
782        let line_content = lines[span.line - 1];
783        let line_num = span.line;
784        let line_num_width = format!("{}", line_num).len();
785        let mut output = String::new();
786        output.push_str(&format!("{} |\n", " ".repeat(line_num_width)));
787        output.push_str(&format!("{} | {}\n", line_num, line_content));
788        let col = if span.column > 0 { span.column - 1 } else { 0 };
789        let underline_len = if span.end > span.start {
790            span.end - span.start
791        } else {
792            1
793        };
794        let underline_len = underline_len.min(line_content.len().saturating_sub(col));
795        let underline_len = if underline_len == 0 { 1 } else { underline_len };
796        output.push_str(&format!(
797            "{} | {}{}",
798            " ".repeat(line_num_width),
799            " ".repeat(col),
800            "^".repeat(underline_len)
801        ));
802        output.push('\n');
803        output
804    }
805}
806/// Aggregates multiple `DiagnosticCollector`s into a unified view.
807#[allow(dead_code)]
808pub struct DiagnosticAggregator {
809    collectors: Vec<DiagnosticCollector>,
810    label: String,
811}
812#[allow(dead_code)]
813impl DiagnosticAggregator {
814    /// Create a new aggregator.
815    pub fn new(label: impl Into<String>) -> Self {
816        Self {
817            collectors: Vec::new(),
818            label: label.into(),
819        }
820    }
821    /// Add a collector to the aggregator.
822    pub fn add_collector(&mut self, c: DiagnosticCollector) {
823        self.collectors.push(c);
824    }
825    /// Total error count across all collectors.
826    pub fn total_errors(&self) -> usize {
827        self.collectors.iter().map(|c| c.error_count()).sum()
828    }
829    /// Total warning count across all collectors.
830    pub fn total_warnings(&self) -> usize {
831        self.collectors.iter().map(|c| c.warning_count()).sum()
832    }
833    /// Total diagnostic count across all collectors.
834    pub fn total_count(&self) -> usize {
835        self.collectors.iter().map(|c| c.diagnostics().len()).sum()
836    }
837    /// Flatten all diagnostics into a single sorted (by position) list.
838    pub fn flat_sorted(&self) -> Vec<Diagnostic> {
839        let mut all: Vec<Diagnostic> = self
840            .collectors
841            .iter()
842            .flat_map(|c| c.diagnostics().iter().cloned())
843            .collect();
844        all.sort_by(|a, b| {
845            a.span
846                .line
847                .cmp(&b.span.line)
848                .then(a.span.column.cmp(&b.span.column))
849        });
850        all
851    }
852    /// True if any collector has errors.
853    pub fn has_errors(&self) -> bool {
854        self.collectors.iter().any(|c| c.has_errors())
855    }
856    /// Summary line.
857    pub fn summary(&self) -> String {
858        format!(
859            "DiagnosticAggregator [{}]: {} errors, {} warnings across {} collectors",
860            self.label,
861            self.total_errors(),
862            self.total_warnings(),
863            self.collectors.len()
864        )
865    }
866    /// Number of collectors.
867    pub fn collector_count(&self) -> usize {
868        self.collectors.len()
869    }
870}
871/// Suppresses diagnostics matching specific criteria before they are added.
872#[allow(dead_code)]
873pub struct DiagnosticSuppressor {
874    suppressed_codes: Vec<DiagnosticCode>,
875    suppress_warnings: bool,
876    suppress_hints: bool,
877}
878#[allow(dead_code)]
879impl DiagnosticSuppressor {
880    /// Create a new suppressor with no rules.
881    pub fn new() -> Self {
882        Self {
883            suppressed_codes: Vec::new(),
884            suppress_warnings: false,
885            suppress_hints: false,
886        }
887    }
888    /// Suppress a specific diagnostic code.
889    pub fn suppress_code(mut self, code: DiagnosticCode) -> Self {
890        self.suppressed_codes.push(code);
891        self
892    }
893    /// Suppress all warnings.
894    pub fn suppress_all_warnings(mut self) -> Self {
895        self.suppress_warnings = true;
896        self
897    }
898    /// Suppress all hints.
899    pub fn suppress_all_hints(mut self) -> Self {
900        self.suppress_hints = true;
901        self
902    }
903    /// Return `true` if the given diagnostic should be suppressed.
904    pub fn should_suppress(&self, d: &Diagnostic) -> bool {
905        if self.suppress_warnings && d.severity == Severity::Warning {
906            return true;
907        }
908        if self.suppress_hints && d.severity == Severity::Hint {
909            return true;
910        }
911        if let Some(code) = d.code {
912            return self.suppressed_codes.contains(&code);
913        }
914        false
915    }
916    /// Filter a list of diagnostics, removing suppressed ones.
917    pub fn filter(&self, diags: Vec<Diagnostic>) -> Vec<Diagnostic> {
918        diags
919            .into_iter()
920            .filter(|d| !self.should_suppress(d))
921            .collect()
922    }
923    /// Filter a collector, returning a new collector with unsuppressed diagnostics.
924    pub fn filter_collector(&self, c: &DiagnosticCollector) -> DiagnosticCollector {
925        let mut new_c = DiagnosticCollector::new();
926        for d in c.diagnostics() {
927            if !self.should_suppress(d) {
928                new_c.add(d.clone());
929            }
930        }
931        new_c
932    }
933}
934/// Filters a `DiagnosticCollector` based on various criteria.
935#[allow(dead_code)]
936pub struct DiagnosticFilter<'a> {
937    collector: &'a DiagnosticCollector,
938}
939#[allow(dead_code)]
940impl<'a> DiagnosticFilter<'a> {
941    /// Create a new filter wrapping the given collector.
942    pub fn new(collector: &'a DiagnosticCollector) -> Self {
943        Self { collector }
944    }
945    /// Diagnostics with the given code.
946    pub fn with_code(&self, code: DiagnosticCode) -> Vec<&Diagnostic> {
947        self.collector
948            .diagnostics()
949            .iter()
950            .filter(|d| d.code == Some(code))
951            .collect()
952    }
953    /// Diagnostics whose message contains the given substring.
954    pub fn message_contains(&self, needle: &str) -> Vec<&Diagnostic> {
955        self.collector
956            .diagnostics()
957            .iter()
958            .filter(|d| d.message.contains(needle))
959            .collect()
960    }
961    /// Diagnostics in a line range `[from_line, to_line]` (inclusive, 1-indexed).
962    pub fn in_line_range(&self, from_line: usize, to_line: usize) -> Vec<&Diagnostic> {
963        self.collector
964            .diagnostics()
965            .iter()
966            .filter(|d| d.span.line >= from_line && d.span.line <= to_line)
967            .collect()
968    }
969    /// Diagnostics that have at least one fix suggestion.
970    pub fn with_fixes(&self) -> Vec<&Diagnostic> {
971        self.collector
972            .diagnostics()
973            .iter()
974            .filter(|d| !d.fixes.is_empty())
975            .collect()
976    }
977    /// Diagnostics that have help text.
978    pub fn with_help(&self) -> Vec<&Diagnostic> {
979        self.collector
980            .diagnostics()
981            .iter()
982            .filter(|d| d.help.is_some())
983            .collect()
984    }
985    /// Diagnostics of severity Error.
986    pub fn errors(&self) -> Vec<&Diagnostic> {
987        self.collector.filter_severity(Severity::Error)
988    }
989    /// Diagnostics of severity Warning.
990    pub fn warnings(&self) -> Vec<&Diagnostic> {
991        self.collector.filter_severity(Severity::Warning)
992    }
993    /// Diagnostics of severity Info.
994    pub fn infos(&self) -> Vec<&Diagnostic> {
995        self.collector.filter_severity(Severity::Info)
996    }
997    /// Diagnostics of severity Hint.
998    pub fn hints(&self) -> Vec<&Diagnostic> {
999        self.collector.filter_severity(Severity::Hint)
1000    }
1001}
1002/// Diagnostic collector for gathering multiple diagnostics.
1003pub struct DiagnosticCollector {
1004    /// Collected diagnostics
1005    diagnostics: Vec<Diagnostic>,
1006    /// Error count
1007    error_count: usize,
1008    /// Warning count
1009    warning_count: usize,
1010}
1011impl DiagnosticCollector {
1012    /// Create a new diagnostic collector.
1013    pub fn new() -> Self {
1014        Self {
1015            diagnostics: Vec::new(),
1016            error_count: 0,
1017            warning_count: 0,
1018        }
1019    }
1020    /// Add a diagnostic.
1021    pub fn add(&mut self, diagnostic: Diagnostic) {
1022        if diagnostic.is_error() {
1023            self.error_count += 1;
1024        } else if diagnostic.is_warning() {
1025            self.warning_count += 1;
1026        }
1027        self.diagnostics.push(diagnostic);
1028    }
1029    /// Get all diagnostics.
1030    pub fn diagnostics(&self) -> &[Diagnostic] {
1031        &self.diagnostics
1032    }
1033    /// Get error count.
1034    pub fn error_count(&self) -> usize {
1035        self.error_count
1036    }
1037    /// Get warning count.
1038    pub fn warning_count(&self) -> usize {
1039        self.warning_count
1040    }
1041    /// Check if there are any errors.
1042    pub fn has_errors(&self) -> bool {
1043        self.error_count > 0
1044    }
1045    /// Clear all diagnostics.
1046    pub fn clear(&mut self) {
1047        self.diagnostics.clear();
1048        self.error_count = 0;
1049        self.warning_count = 0;
1050    }
1051    /// Get diagnostics at a specific line number.
1052    #[allow(dead_code)]
1053    pub fn diagnostics_at(&self, line: usize) -> Vec<&Diagnostic> {
1054        self.diagnostics
1055            .iter()
1056            .filter(|d| d.span.line == line)
1057            .collect()
1058    }
1059    /// Count info-level diagnostics.
1060    #[allow(dead_code)]
1061    pub fn info_count(&self) -> usize {
1062        self.diagnostics
1063            .iter()
1064            .filter(|d| d.severity == Severity::Info)
1065            .count()
1066    }
1067    /// Sort diagnostics by severity (errors first, then warnings, then info, then hints).
1068    #[allow(dead_code)]
1069    pub fn sort_by_severity(&mut self) {
1070        self.diagnostics.sort_by_key(|d| d.severity);
1071    }
1072    /// Sort diagnostics by source position (line, then column).
1073    #[allow(dead_code)]
1074    pub fn sort_by_position(&mut self) {
1075        self.diagnostics.sort_by(|a, b| {
1076            a.span
1077                .line
1078                .cmp(&b.span.line)
1079                .then(a.span.column.cmp(&b.span.column))
1080        });
1081    }
1082    /// Filter diagnostics by severity.
1083    #[allow(dead_code)]
1084    pub fn filter_severity(&self, severity: Severity) -> Vec<&Diagnostic> {
1085        self.diagnostics
1086            .iter()
1087            .filter(|d| d.severity == severity)
1088            .collect()
1089    }
1090    /// Merge another collector's diagnostics into this one.
1091    #[allow(dead_code)]
1092    pub fn merge(&mut self, other: &DiagnosticCollector) {
1093        for diag in &other.diagnostics {
1094            self.add(diag.clone());
1095        }
1096    }
1097    /// Generate a human-readable summary string.
1098    ///
1099    /// Example: "3 errors, 2 warnings"
1100    #[allow(dead_code)]
1101    pub fn summary(&self) -> String {
1102        let info = self.info_count();
1103        let mut parts = Vec::new();
1104        if self.error_count > 0 {
1105            parts.push(format!(
1106                "{} error{}",
1107                self.error_count,
1108                if self.error_count == 1 { "" } else { "s" }
1109            ));
1110        }
1111        if self.warning_count > 0 {
1112            parts.push(format!(
1113                "{} warning{}",
1114                self.warning_count,
1115                if self.warning_count == 1 { "" } else { "s" }
1116            ));
1117        }
1118        if info > 0 {
1119            parts.push(format!("{} info{}", info, if info == 1 { "" } else { "s" }));
1120        }
1121        if parts.is_empty() {
1122            "no diagnostics".to_string()
1123        } else {
1124            parts.join(", ")
1125        }
1126    }
1127}
1128/// A code fix suggestion.
1129#[derive(Debug, Clone)]
1130pub struct CodeFix {
1131    /// Human-readable description of the fix
1132    pub message: String,
1133    /// Span of code to replace
1134    pub span: Span,
1135    /// Replacement text
1136    pub replacement: String,
1137}
1138/// A named group of related diagnostics (e.g. from one file or one pass).
1139#[allow(dead_code)]
1140#[derive(Debug, Default, Clone)]
1141pub struct DiagnosticGroup {
1142    /// Group name or file path.
1143    pub name: String,
1144    /// Diagnostics in this group.
1145    pub diagnostics: Vec<Diagnostic>,
1146}
1147#[allow(dead_code)]
1148impl DiagnosticGroup {
1149    /// Create an empty group.
1150    pub fn new(name: impl Into<String>) -> Self {
1151        Self {
1152            name: name.into(),
1153            diagnostics: Vec::new(),
1154        }
1155    }
1156    /// Add a diagnostic to the group.
1157    pub fn add(&mut self, d: Diagnostic) {
1158        self.diagnostics.push(d);
1159    }
1160    /// Error count.
1161    pub fn error_count(&self) -> usize {
1162        self.diagnostics.iter().filter(|d| d.is_error()).count()
1163    }
1164    /// Warning count.
1165    pub fn warning_count(&self) -> usize {
1166        self.diagnostics.iter().filter(|d| d.is_warning()).count()
1167    }
1168    /// True if there are any errors.
1169    pub fn has_errors(&self) -> bool {
1170        self.diagnostics.iter().any(|d| d.is_error())
1171    }
1172    /// Total number of diagnostics.
1173    pub fn len(&self) -> usize {
1174        self.diagnostics.len()
1175    }
1176    /// True if there are no diagnostics.
1177    pub fn is_empty(&self) -> bool {
1178        self.diagnostics.is_empty()
1179    }
1180    /// Produce a summary string for this group.
1181    pub fn summary(&self) -> String {
1182        format!(
1183            "[{}]: {} errors, {} warnings",
1184            self.name,
1185            self.error_count(),
1186            self.warning_count()
1187        )
1188    }
1189    /// Sort the group's diagnostics by position.
1190    pub fn sort_by_position(&mut self) {
1191        self.diagnostics.sort_by(|a, b| {
1192            a.span
1193                .line
1194                .cmp(&b.span.line)
1195                .then(a.span.column.cmp(&b.span.column))
1196        });
1197    }
1198}