Skip to main content

trident/diagnostic/
mod.rs

1use crate::span::Span;
2use std::cell::Cell;
3
4thread_local! {
5    static SUPPRESS_WARNINGS: Cell<bool> = const { Cell::new(false) };
6}
7
8/// Suppress warning diagnostics on the current thread.
9/// Returns a guard that restores the previous state on drop.
10pub fn suppress_warnings() -> SuppressGuard {
11    let prev = SUPPRESS_WARNINGS.with(|s| s.replace(true));
12    SuppressGuard(prev)
13}
14
15pub struct SuppressGuard(bool);
16
17impl Drop for SuppressGuard {
18    fn drop(&mut self) {
19        SUPPRESS_WARNINGS.with(|s| s.set(self.0));
20    }
21}
22
23fn warnings_suppressed() -> bool {
24    SUPPRESS_WARNINGS.with(|s| s.get())
25}
26
27/// A compiler diagnostic (error, warning, or hint).
28#[derive(Clone, Debug)]
29pub struct Diagnostic {
30    pub severity: Severity,
31    pub message: String,
32    pub span: Span,
33    pub notes: Vec<String>,
34    pub help: Option<String>,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Severity {
39    Error,
40    Warning,
41}
42
43impl Diagnostic {
44    pub fn error(message: String, span: Span) -> Self {
45        Self {
46            severity: Severity::Error,
47            message,
48            span,
49            notes: Vec::new(),
50            help: None,
51        }
52    }
53
54    pub fn warning(message: String, span: Span) -> Self {
55        Self {
56            severity: Severity::Warning,
57            message,
58            span,
59            notes: Vec::new(),
60            help: None,
61        }
62    }
63
64    pub fn with_note(mut self, note: String) -> Self {
65        self.notes.push(note);
66        self
67    }
68
69    pub fn with_help(mut self, help: String) -> Self {
70        self.help = Some(help);
71        self
72    }
73
74    /// Render the diagnostic to stderr using ariadne.
75    pub fn render(&self, filename: &str, source: &str) {
76        if self.severity == Severity::Warning && warnings_suppressed() {
77            return;
78        }
79        use ariadne::{Color, Label, Report, ReportKind, Source};
80
81        let kind = match self.severity {
82            Severity::Error => ReportKind::Error,
83            Severity::Warning => ReportKind::Warning,
84        };
85
86        let color = match self.severity {
87            Severity::Error => Color::Red,
88            Severity::Warning => Color::Yellow,
89        };
90
91        let mut report = Report::build(kind, filename, self.span.start as usize)
92            .with_message(&self.message)
93            .with_label(
94                Label::new((filename, self.span.start as usize..self.span.end as usize))
95                    .with_message(&self.message)
96                    .with_color(color),
97            );
98
99        for note in &self.notes {
100            report = report.with_note(note);
101        }
102
103        if let Some(help) = &self.help {
104            report = report.with_help(help);
105        }
106
107        let _ = report.finish().eprint((filename, Source::from(source)));
108    }
109}
110
111/// Render a list of diagnostics.
112pub fn render_diagnostics(diagnostics: &[Diagnostic], filename: &str, source: &str) {
113    for diag in diagnostics {
114        diag.render(filename, source);
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_error_construction() {
124        let span = Span::new(0, 10, 15);
125        let d = Diagnostic::error("type mismatch".to_string(), span);
126        assert_eq!(d.severity, Severity::Error);
127        assert_eq!(d.message, "type mismatch");
128        assert_eq!(d.span.start, 10);
129        assert_eq!(d.span.end, 15);
130        assert!(d.notes.is_empty());
131        assert!(d.help.is_none());
132    }
133
134    #[test]
135    fn test_warning_construction() {
136        let span = Span::dummy();
137        let d = Diagnostic::warning("unused variable".to_string(), span);
138        assert_eq!(d.severity, Severity::Warning);
139        assert_eq!(d.message, "unused variable");
140    }
141
142    #[test]
143    fn test_with_note() {
144        let d = Diagnostic::error("error".to_string(), Span::dummy())
145            .with_note("expected Field".to_string())
146            .with_note("found U32".to_string());
147        assert_eq!(d.notes.len(), 2);
148        assert_eq!(d.notes[0], "expected Field");
149        assert_eq!(d.notes[1], "found U32");
150    }
151
152    #[test]
153    fn test_with_help() {
154        let d = Diagnostic::error("error".to_string(), Span::dummy())
155            .with_help("try as_field()".to_string());
156        assert_eq!(d.help.as_deref(), Some("try as_field()"));
157    }
158
159    #[test]
160    fn test_chained_builders() {
161        let d = Diagnostic::warning("hint".to_string(), Span::new(0, 0, 5))
162            .with_note("note 1".to_string())
163            .with_help("help text".to_string())
164            .with_note("note 2".to_string());
165        assert_eq!(d.severity, Severity::Warning);
166        assert_eq!(d.notes.len(), 2);
167        assert!(d.help.is_some());
168    }
169
170    #[test]
171    fn test_render_does_not_panic() {
172        let source = "let x: Field = 1\nlet y: U32 = x\n";
173        let d = Diagnostic::error("type mismatch".to_string(), Span::new(0, 18, 32))
174            .with_note("expected U32, found Field".to_string());
175        // Render to stderr — just verify it doesn't panic
176        d.render("test.tri", source);
177    }
178
179    #[test]
180    fn test_render_diagnostics_multiple() {
181        let source = "let x = 1\nlet y = 2\n";
182        let diagnostics = vec![
183            Diagnostic::warning("unused x".to_string(), Span::new(0, 4, 5)),
184            Diagnostic::warning("unused y".to_string(), Span::new(0, 14, 15)),
185        ];
186        // Just verify it doesn't panic
187        render_diagnostics(&diagnostics, "test.tri", source);
188    }
189
190    #[test]
191    fn test_render_warning_does_not_panic() {
192        let source = "fn main() {\n    as_u32(x)\n}\n";
193        let d = Diagnostic::warning("redundant range check".to_string(), Span::new(0, 16, 25))
194            .with_help("x is already proven U32".to_string());
195        d.render("test.tri", source);
196    }
197}