Skip to main content

orrery_parser/error/
diagnostic.rs

1//! Core diagnostic type for the Orrery error system.
2//!
3//! A [`Diagnostic`] represents a single error or warning with optional
4//! error code, multiple labeled source spans, and help text.
5
6use std::fmt;
7
8use crate::{
9    error::{Severity, error_code::ErrorCode, label::Label},
10    span::Span,
11};
12
13/// A rich diagnostic message with source location information.
14///
15/// Diagnostics provide detailed information about errors and warnings,
16/// including:
17/// - A severity level
18/// - An optional error code for documentation and searchability
19/// - A primary message describing the issue
20/// - One or more labeled source spans
21/// - Optional help text with suggestions
22///
23/// # Example
24///
25/// ```text
26/// error[E301]: cannot override type `Rectangle`
27///   --> src/main.orr:10:1
28///    |
29/// 10 | type Rectangle = Oval[fill_color="red"];
30///    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type override not supported
31///    |
32///    = help: built-in types cannot be redefined
33/// ```
34#[derive(Debug, Clone)]
35pub struct Diagnostic {
36    severity: Severity,
37    code: Option<ErrorCode>,
38    message: String,
39    labels: Vec<Label>,
40    help: Option<String>,
41}
42
43impl Diagnostic {
44    /// Create an error diagnostic.
45    ///
46    /// # Example
47    ///
48    /// ```
49    /// # use orrery_parser::error::{Diagnostic, ErrorCode};
50    /// # use orrery_parser::Span;
51    ///
52    /// let span = Span::new(0..10);
53    /// let diag = Diagnostic::error("undefined type `Foo`")
54    ///     .with_code(ErrorCode::E300)
55    ///     .with_label(span, "not found")
56    ///     .with_help("did you mean `Bar`?");
57    /// ```
58    pub fn error(message: impl Into<String>) -> Self {
59        Self::new(Severity::Error, message)
60    }
61
62    /// Create a warning diagnostic.
63    ///
64    /// # Example
65    ///
66    /// ```
67    /// # use orrery_parser::error::Diagnostic;
68    /// # use orrery_parser::Span;
69    ///
70    /// let span = Span::new(0..10);
71    /// let diag = Diagnostic::warning("unused component")
72    ///     .with_label(span, "this component is never referenced")
73    ///     .with_help("consider removing it");
74    /// ```
75    pub fn warning(message: impl Into<String>) -> Self {
76        Self::new(Severity::Warning, message)
77    }
78
79    /// Get the severity of this diagnostic.
80    pub fn severity(&self) -> Severity {
81        self.severity
82    }
83
84    /// Get the error code, if any.
85    pub fn code(&self) -> Option<ErrorCode> {
86        self.code
87    }
88
89    /// Get the primary message.
90    pub fn message(&self) -> &str {
91        &self.message
92    }
93
94    /// Get all labels attached to this diagnostic.
95    pub fn labels(&self) -> &[Label] {
96        &self.labels
97    }
98
99    /// Get the help text, if any.
100    pub fn help(&self) -> Option<&str> {
101        self.help.as_deref()
102    }
103
104    /// Set the error code.
105    pub fn with_code(mut self, code: ErrorCode) -> Self {
106        self.code = Some(code);
107        self
108    }
109
110    /// Add a primary label to this diagnostic.
111    pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
112        self.labels.push(Label::primary(span, message));
113        self
114    }
115
116    /// Add a secondary label to this diagnostic.
117    pub fn with_secondary_label(mut self, span: Span, message: impl Into<String>) -> Self {
118        self.labels.push(Label::secondary(span, message));
119        self
120    }
121
122    /// Set the help text.
123    pub fn with_help(mut self, help: impl Into<String>) -> Self {
124        self.help = Some(help.into());
125        self
126    }
127
128    /// Create a new diagnostic with the given severity and message.
129    fn new(severity: Severity, message: impl Into<String>) -> Self {
130        Self {
131            severity,
132            code: None,
133            message: message.into(),
134            labels: Vec::new(),
135            help: None,
136        }
137    }
138}
139
140impl fmt::Display for Diagnostic {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        // Format: "error[E001]: message" or "error: message"
143        write!(f, "{}", self.severity)?;
144        if let Some(code) = self.code {
145            write!(f, "[{}]", code)?;
146        }
147        write!(f, ": {}", self.message)
148    }
149}
150
151impl std::error::Error for Diagnostic {}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_diagnostic_new() {
159        let diag = Diagnostic::new(Severity::Error, "test error");
160
161        assert!(diag.severity().is_error());
162        assert!(!diag.severity().is_warning());
163        assert_eq!(diag.message(), "test error");
164        assert!(diag.code().is_none());
165        assert!(diag.labels().is_empty());
166        assert!(diag.help().is_none());
167    }
168
169    #[test]
170    fn test_diagnostic_with_code() {
171        let diag = Diagnostic::new(Severity::Error, "undefined type").with_code(ErrorCode::E300);
172
173        assert_eq!(diag.code(), Some(ErrorCode::E300));
174    }
175
176    #[test]
177    fn test_diagnostic_with_label() {
178        let diag = Diagnostic::new(Severity::Error, "test error")
179            .with_label(Span::new(10..20), "error here");
180
181        assert_eq!(diag.labels().len(), 1);
182        assert!(diag.labels()[0].is_primary());
183        assert_eq!(diag.labels()[0].message(), "error here");
184    }
185
186    #[test]
187    fn test_diagnostic_with_secondary_label() {
188        let diag = Diagnostic::new(Severity::Error, "duplicate definition")
189            .with_label(Span::new(10..20), "duplicate here")
190            .with_secondary_label(Span::new(5..15), "first defined here");
191
192        assert_eq!(diag.labels().len(), 2);
193        assert!(diag.labels()[0].is_primary());
194        assert!(diag.labels()[1].is_secondary());
195    }
196
197    #[test]
198    fn test_diagnostic_with_help() {
199        let diag = Diagnostic::new(Severity::Warning, "unused variable")
200            .with_help("consider removing or prefixing with underscore");
201
202        assert_eq!(
203            diag.help(),
204            Some("consider removing or prefixing with underscore")
205        );
206    }
207
208    #[test]
209    fn test_diagnostic_display_with_code() {
210        let diag =
211            Diagnostic::new(Severity::Error, "undefined type `Foo`").with_code(ErrorCode::E300);
212
213        assert_eq!(diag.to_string(), "error[E300]: undefined type `Foo`");
214    }
215
216    #[test]
217    fn test_diagnostic_display_without_code() {
218        let diag = Diagnostic::new(Severity::Warning, "unused import");
219
220        assert_eq!(diag.to_string(), "warning: unused import");
221    }
222
223    #[test]
224    fn test_diagnostic_builder_chain() {
225        let diag = Diagnostic::new(Severity::Error, "cannot override built-in type `Component`")
226            .with_code(ErrorCode::E301)
227            .with_label(Span::new(100..120), "type override not supported")
228            .with_help("built-in types cannot be redefined");
229
230        assert!(diag.severity().is_error());
231        assert_eq!(diag.code(), Some(ErrorCode::E301));
232        assert_eq!(diag.message(), "cannot override built-in type `Component`");
233        assert_eq!(diag.labels().len(), 1);
234        assert_eq!(diag.help(), Some("built-in types cannot be redefined"));
235    }
236}