Skip to main content

diag_lang/
diagnostic.rs

1//! The diagnostic: a severity, a message, the span it points at, and the related
2//! labels and notes that round it out.
3
4use alloc::boxed::Box;
5use alloc::vec::Vec;
6
7use crate::{Label, Severity};
8
9/// One thing a compiler stage has to say about the source: a [`Severity`], a
10/// headline message, a primary [`Label`] pointing at the span it concerns, and
11/// optionally secondary labels for related locations and trailing note/help lines.
12///
13/// A `Diagnostic` is the unit a lexer, parser, or type checker emits and a
14/// [`Renderer`](crate::Renderer) turns into caret-annotated output. Constructing
15/// one is cheap — it allocates only what the message, labels, and notes require —
16/// because it is on the front-end's hot path. The primary label is supplied at
17/// construction, so a diagnostic always knows where it points; secondary labels
18/// and notes are added with the chainable `with_*` builders.
19///
20/// # Examples
21///
22/// ```
23/// use diag_lang::{Diagnostic, Label, Severity};
24/// use span_lang::Span;
25///
26/// let diag = Diagnostic::new(
27///     Severity::Error,
28///     "mismatched types",
29///     Label::new(Span::new(20, 27), "expected `i32`, found `&str`"),
30/// )
31/// .with_secondary(Label::new(Span::new(8, 11), "expected due to this"))
32/// .with_note("string literals have type `&str`")
33/// .with_help("convert with `.parse()`");
34///
35/// assert_eq!(diag.secondary().len(), 1);
36/// assert_eq!(diag.notes().count(), 1);
37/// assert_eq!(diag.help().count(), 1);
38/// ```
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct Diagnostic {
41    severity: Severity,
42    message: Box<str>,
43    primary: Label,
44    secondary: Vec<Label>,
45    notes: Vec<Box<str>>,
46    help: Vec<Box<str>>,
47}
48
49impl Diagnostic {
50    /// Builds a diagnostic at `severity`, headed by `message`, pointing at
51    /// `primary`.
52    ///
53    /// The message is taken by value (a `&str` or an owned `String`), so the
54    /// diagnostic owns its text. The primary label is required: a diagnostic
55    /// always has a span to render a caret under, which keeps the type total and
56    /// the renderer free of a "no location" branch. Secondary labels and notes
57    /// start empty; add them with [`with_secondary`](Diagnostic::with_secondary),
58    /// [`with_note`](Diagnostic::with_note), and [`with_help`](Diagnostic::with_help).
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use diag_lang::{Diagnostic, Label, Severity};
64    /// use span_lang::Span;
65    ///
66    /// let diag = Diagnostic::new(
67    ///     Severity::Warning,
68    ///     "unused variable `x`",
69    ///     Label::new(Span::new(4, 5), "first defined here"),
70    /// );
71    /// assert_eq!(diag.severity(), Severity::Warning);
72    /// assert!(diag.secondary().is_empty());
73    /// ```
74    #[inline]
75    #[must_use]
76    pub fn new(severity: Severity, message: impl Into<Box<str>>, primary: Label) -> Self {
77        Self {
78            severity,
79            message: message.into(),
80            primary,
81            secondary: Vec::new(),
82            notes: Vec::new(),
83            help: Vec::new(),
84        }
85    }
86
87    /// Adds a secondary label, returning the diagnostic for chaining.
88    ///
89    /// Secondary labels mark locations related to the primary one — "expected
90    /// because of this", "first defined here". A renderer draws them with a `-`
91    /// underline to distinguish them from the primary `^`. Any number may be
92    /// added; they render in a stable order driven by where they point, not by the
93    /// order they were added.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use diag_lang::{Diagnostic, Label, Severity};
99    /// use span_lang::Span;
100    ///
101    /// let diag = Diagnostic::new(
102    ///     Severity::Error,
103    ///     "duplicate definition",
104    ///     Label::new(Span::new(30, 33), "redefined here"),
105    /// )
106    /// .with_secondary(Label::new(Span::new(4, 7), "first defined here"));
107    /// assert_eq!(diag.secondary().len(), 1);
108    /// ```
109    #[must_use]
110    pub fn with_secondary(mut self, label: Label) -> Self {
111        self.secondary.push(label);
112        self
113    }
114
115    /// Adds a trailing note, returning the diagnostic for chaining.
116    ///
117    /// A note is neutral context with no span of its own; a renderer prints it on
118    /// its own line below the frame, marked `note:`. Notes render in the order they
119    /// are added, after any help lines' siblings — see
120    /// [`with_help`](Diagnostic::with_help).
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use diag_lang::{Diagnostic, Label, Severity};
126    /// use span_lang::Span;
127    ///
128    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
129    ///     .with_note("the value was moved on the previous line");
130    /// assert_eq!(diag.notes().next(), Some("the value was moved on the previous line"));
131    /// ```
132    #[must_use]
133    pub fn with_note(mut self, note: impl Into<Box<str>>) -> Self {
134        self.notes.push(note.into());
135        self
136    }
137
138    /// Adds a trailing help line, returning the diagnostic for chaining.
139    ///
140    /// A help line is a suggestion toward a fix; a renderer prints it below the
141    /// notes, marked `help:`. Help lines render in the order they are added.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use diag_lang::{Diagnostic, Label, Severity};
147    /// use span_lang::Span;
148    ///
149    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
150    ///     .with_help("add a semicolon");
151    /// assert_eq!(diag.help().next(), Some("add a semicolon"));
152    /// ```
153    #[must_use]
154    pub fn with_help(mut self, help: impl Into<Box<str>>) -> Self {
155        self.help.push(help.into());
156        self
157    }
158
159    /// Returns the level this diagnostic reports at.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use diag_lang::{Diagnostic, Label, Severity};
165    /// use span_lang::Span;
166    ///
167    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)));
168    /// assert_eq!(diag.severity(), Severity::Error);
169    /// ```
170    #[inline]
171    #[must_use]
172    pub const fn severity(&self) -> Severity {
173        self.severity
174    }
175
176    /// Returns the headline message.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use diag_lang::{Diagnostic, Label, Severity};
182    /// use span_lang::Span;
183    ///
184    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)));
185    /// assert_eq!(diag.message(), "boom");
186    /// ```
187    #[inline]
188    #[must_use]
189    pub fn message(&self) -> &str {
190        &self.message
191    }
192
193    /// Returns the primary label — the span the caret underlines.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use diag_lang::{Diagnostic, Label, Severity};
199    /// use span_lang::Span;
200    ///
201    /// let diag = Diagnostic::new(
202    ///     Severity::Error,
203    ///     "boom",
204    ///     Label::new(Span::new(2, 5), "right here"),
205    /// );
206    /// assert_eq!(diag.primary().message(), "right here");
207    /// ```
208    #[inline]
209    #[must_use]
210    pub const fn primary(&self) -> &Label {
211        &self.primary
212    }
213
214    /// Returns the secondary labels, in the order they were added.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use diag_lang::{Diagnostic, Label, Severity};
220    /// use span_lang::Span;
221    ///
222    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
223    ///     .with_secondary(Label::new(Span::new(1, 2), "related"));
224    /// assert_eq!(diag.secondary()[0].message(), "related");
225    /// ```
226    #[inline]
227    #[must_use]
228    pub fn secondary(&self) -> &[Label] {
229        &self.secondary
230    }
231
232    /// Iterates over the note lines, in the order they were added.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use diag_lang::{Diagnostic, Label, Severity};
238    /// use span_lang::Span;
239    ///
240    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
241    ///     .with_note("one")
242    ///     .with_note("two");
243    /// assert_eq!(diag.notes().collect::<Vec<_>>(), ["one", "two"]);
244    /// ```
245    pub fn notes(&self) -> impl ExactSizeIterator<Item = &str> {
246        self.notes.iter().map(AsRef::as_ref)
247    }
248
249    /// Iterates over the help lines, in the order they were added.
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// use diag_lang::{Diagnostic, Label, Severity};
255    /// use span_lang::Span;
256    ///
257    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
258    ///     .with_help("try this");
259    /// assert_eq!(diag.help().collect::<Vec<_>>(), ["try this"]);
260    /// ```
261    pub fn help(&self) -> impl ExactSizeIterator<Item = &str> {
262        self.help.iter().map(AsRef::as_ref)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use span_lang::Span;
269
270    use super::*;
271
272    #[test]
273    fn test_new_starts_with_empty_extras() {
274        let diag = Diagnostic::new(
275            Severity::Error,
276            "type mismatch",
277            Label::new(Span::new(8, 11), "here"),
278        );
279        assert_eq!(diag.severity(), Severity::Error);
280        assert_eq!(diag.message(), "type mismatch");
281        assert_eq!(diag.primary().span(), Span::new(8, 11));
282        assert!(diag.secondary().is_empty());
283        assert_eq!(diag.notes().count(), 0);
284        assert_eq!(diag.help().count(), 0);
285    }
286
287    #[test]
288    fn test_builders_accumulate_in_order() {
289        let diag = Diagnostic::new(Severity::Error, "m", Label::unlabelled(Span::empty(0)))
290            .with_secondary(Label::new(Span::new(1, 2), "a"))
291            .with_secondary(Label::new(Span::new(3, 4), "b"))
292            .with_note("n1")
293            .with_note("n2")
294            .with_help("h1");
295
296        let secondary: Vec<_> = diag.secondary().iter().map(Label::message).collect();
297        assert_eq!(secondary, ["a", "b"]);
298        assert_eq!(diag.notes().collect::<Vec<_>>(), ["n1", "n2"]);
299        assert_eq!(diag.help().collect::<Vec<_>>(), ["h1"]);
300    }
301}