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;
6use core::fmt;
7
8use crate::{Code, Label, Severity, Suggestion};
9
10/// One thing a compiler stage has to say about the source: a [`Severity`], a
11/// headline message, a primary [`Label`] pointing at the span it concerns, and
12/// optionally secondary labels for related locations and trailing note/help lines.
13/// Since 1.1.0 it can also carry a [`Code`] naming its kind and fix-it
14/// [`Suggestion`]s.
15///
16/// A `Diagnostic` is the unit a lexer, parser, or type checker emits and a
17/// [`Renderer`](crate::Renderer) turns into caret-annotated output. Constructing
18/// one is cheap — it allocates only what the message, labels, and notes require —
19/// because it is on the front-end's hot path. The primary label is supplied at
20/// construction, so a diagnostic always knows where it points; secondary labels
21/// and notes are added with the chainable `with_*` builders.
22///
23/// # Examples
24///
25/// ```
26/// use diag_lang::{Diagnostic, Label, Severity};
27/// use span_lang::Span;
28///
29/// let diag = Diagnostic::new(
30///     Severity::Error,
31///     "mismatched types",
32///     Label::new(Span::new(20, 27), "expected `i32`, found `&str`"),
33/// )
34/// .with_secondary(Label::new(Span::new(8, 11), "expected due to this"))
35/// .with_note("string literals have type `&str`")
36/// .with_help("convert with `.parse()`");
37///
38/// assert_eq!(diag.secondary().len(), 1);
39/// assert_eq!(diag.notes().count(), 1);
40/// assert_eq!(diag.help().count(), 1);
41/// ```
42#[derive(Clone)]
43pub struct Diagnostic {
44    severity: Severity,
45    code: Option<Code>,
46    message: Box<str>,
47    primary: Label,
48    secondary: Vec<Label>,
49    notes: Vec<Box<str>>,
50    help: Vec<Box<str>>,
51    /// Allocated on the first suggestion. Most diagnostics have none, and one
52    /// pointer instead of an inline `Vec` keeps the type within 16 bytes of its
53    /// 1.0 size, so building and moving a diagnostic on the front end's hot
54    /// path costs what it did. `None` whenever there are no suggestions.
55    // The box is the point: an 8-byte thin pointer instead of a 24-byte inline
56    // `Vec`. A boxed slice would reallocate on every push.
57    #[allow(clippy::box_collection)]
58    suggestions: Option<Box<Vec<Suggestion>>>,
59}
60
61impl Diagnostic {
62    /// Builds a diagnostic at `severity`, headed by `message`, pointing at
63    /// `primary`.
64    ///
65    /// The message is taken by value (a `&str` or an owned `String`), so the
66    /// diagnostic owns its text. The primary label is required: a diagnostic
67    /// always has a span to render a caret under, which keeps the type total and
68    /// the renderer free of a "no location" branch. Secondary labels and notes
69    /// start empty; add them with [`with_secondary`](Diagnostic::with_secondary),
70    /// [`with_note`](Diagnostic::with_note), and [`with_help`](Diagnostic::with_help).
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use diag_lang::{Diagnostic, Label, Severity};
76    /// use span_lang::Span;
77    ///
78    /// let diag = Diagnostic::new(
79    ///     Severity::Warning,
80    ///     "unused variable `x`",
81    ///     Label::new(Span::new(4, 5), "first defined here"),
82    /// );
83    /// assert_eq!(diag.severity(), Severity::Warning);
84    /// assert!(diag.secondary().is_empty());
85    /// ```
86    #[inline]
87    #[must_use]
88    pub fn new(severity: Severity, message: impl Into<Box<str>>, primary: Label) -> Self {
89        Self {
90            severity,
91            code: None,
92            message: message.into(),
93            primary,
94            secondary: Vec::new(),
95            notes: Vec::new(),
96            help: Vec::new(),
97            suggestions: None,
98        }
99    }
100
101    /// Adds a secondary label, returning the diagnostic for chaining.
102    ///
103    /// Secondary labels mark locations related to the primary one — "expected
104    /// because of this", "first defined here". A renderer draws them with a `-`
105    /// underline to distinguish them from the primary `^`. Any number may be
106    /// added; they render in a stable order driven by where they point, not by the
107    /// order they were added.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use diag_lang::{Diagnostic, Label, Severity};
113    /// use span_lang::Span;
114    ///
115    /// let diag = Diagnostic::new(
116    ///     Severity::Error,
117    ///     "duplicate definition",
118    ///     Label::new(Span::new(30, 33), "redefined here"),
119    /// )
120    /// .with_secondary(Label::new(Span::new(4, 7), "first defined here"));
121    /// assert_eq!(diag.secondary().len(), 1);
122    /// ```
123    #[must_use]
124    pub fn with_secondary(mut self, label: Label) -> Self {
125        self.secondary.push(label);
126        self
127    }
128
129    /// Adds a trailing note, returning the diagnostic for chaining.
130    ///
131    /// A note is neutral context with no span of its own; a renderer prints it on
132    /// its own line below the frame, marked `note:`. Notes render in the order they
133    /// are added, after any help lines' siblings — see
134    /// [`with_help`](Diagnostic::with_help).
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use diag_lang::{Diagnostic, Label, Severity};
140    /// use span_lang::Span;
141    ///
142    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
143    ///     .with_note("the value was moved on the previous line");
144    /// assert_eq!(diag.notes().next(), Some("the value was moved on the previous line"));
145    /// ```
146    #[must_use]
147    pub fn with_note(mut self, note: impl Into<Box<str>>) -> Self {
148        self.notes.push(note.into());
149        self
150    }
151
152    /// Adds a trailing help line, returning the diagnostic for chaining.
153    ///
154    /// A help line is a suggestion toward a fix; a renderer prints it below the
155    /// notes, marked `help:`. Help lines render in the order they are added.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use diag_lang::{Diagnostic, Label, Severity};
161    /// use span_lang::Span;
162    ///
163    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
164    ///     .with_help("add a semicolon");
165    /// assert_eq!(diag.help().next(), Some("add a semicolon"));
166    /// ```
167    #[must_use]
168    pub fn with_help(mut self, help: impl Into<Box<str>>) -> Self {
169        self.help.push(help.into());
170        self
171    }
172
173    /// Sets the diagnostic's [`Code`], returning the diagnostic for chaining.
174    ///
175    /// The code is the stable, machine-readable name of this kind of diagnostic
176    /// (see [`Code`] for the format and the recommended ranges). The renderer
177    /// shows it in the header, `error[E0001]: …`, and the JSON output carries
178    /// it. Setting a code again replaces the previous one.
179    ///
180    /// *Since 1.1.0.*
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use diag_lang::{Code, Diagnostic, Label, Severity, Span};
186    ///
187    /// const UNKNOWN_NAME: Code = Code::new("E", 2001).unwrap();
188    ///
189    /// let diag = Diagnostic::new(Severity::Error, "unknown name", Label::unlabelled(Span::empty(0)))
190    ///     .with_code(UNKNOWN_NAME);
191    /// assert_eq!(diag.code(), Some(UNKNOWN_NAME));
192    /// ```
193    #[inline]
194    #[must_use]
195    pub fn with_code(mut self, code: Code) -> Self {
196        self.code = Some(code);
197        self
198    }
199
200    /// Adds a fix-it [`Suggestion`], returning the diagnostic for chaining.
201    ///
202    /// Suggestions render after the notes and help lines, each as a `help:`
203    /// block showing the source as it would read with the fix applied; they
204    /// appear in the JSON output with every edit; and
205    /// [`apply_fixes`](crate::apply_fixes) applies the machine-applicable ones.
206    /// Any number may be added; they keep the order they were added in.
207    ///
208    /// *Since 1.1.0.*
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use diag_lang::{Applicability, Diagnostic, Edit, Label, Severity, Span, Suggestion};
214    ///
215    /// let diag = Diagnostic::new(Severity::Error, "expected `;`", Label::unlabelled(Span::empty(9)))
216    ///     .with_suggestion(
217    ///         Suggestion::new("add `;`", Applicability::MachineApplicable)
218    ///             .with_edit(Edit::insert(9, ";")),
219    ///     );
220    /// assert_eq!(diag.suggestions().len(), 1);
221    /// ```
222    #[must_use]
223    pub fn with_suggestion(mut self, suggestion: Suggestion) -> Self {
224        self.suggestions
225            .get_or_insert_with(Box::default)
226            .push(suggestion);
227        self
228    }
229
230    /// Returns the diagnostic's code, if one was set with
231    /// [`with_code`](Diagnostic::with_code).
232    ///
233    /// *Since 1.1.0.*
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use diag_lang::{Code, Diagnostic, Label, Severity, Span};
239    ///
240    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)));
241    /// assert_eq!(diag.code(), None);
242    /// let diag = diag.with_code(Code::new("E", 1).unwrap());
243    /// assert_eq!(diag.code().map(|c| c.number()), Some(1));
244    /// ```
245    #[inline]
246    #[must_use]
247    pub const fn code(&self) -> Option<Code> {
248        self.code
249    }
250
251    /// Returns the fix-it suggestions, in the order they were added.
252    ///
253    /// *Since 1.1.0.*
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use diag_lang::{Applicability, Diagnostic, Label, Severity, Span, Suggestion};
259    ///
260    /// let diag = Diagnostic::new(Severity::Warning, "w", Label::unlabelled(Span::empty(0)))
261    ///     .with_suggestion(Suggestion::new("try this", Applicability::MaybeIncorrect));
262    /// assert_eq!(diag.suggestions()[0].message(), "try this");
263    /// ```
264    #[inline]
265    #[must_use]
266    pub fn suggestions(&self) -> &[Suggestion] {
267        self.suggestions.as_deref().map_or(&[], Vec::as_slice)
268    }
269
270    /// Returns the level this diagnostic reports at.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use diag_lang::{Diagnostic, Label, Severity};
276    /// use span_lang::Span;
277    ///
278    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)));
279    /// assert_eq!(diag.severity(), Severity::Error);
280    /// ```
281    #[inline]
282    #[must_use]
283    pub const fn severity(&self) -> Severity {
284        self.severity
285    }
286
287    /// Returns the headline message.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use diag_lang::{Diagnostic, Label, Severity};
293    /// use span_lang::Span;
294    ///
295    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)));
296    /// assert_eq!(diag.message(), "boom");
297    /// ```
298    #[inline]
299    #[must_use]
300    pub fn message(&self) -> &str {
301        &self.message
302    }
303
304    /// Returns the primary label — the span the caret underlines.
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use diag_lang::{Diagnostic, Label, Severity};
310    /// use span_lang::Span;
311    ///
312    /// let diag = Diagnostic::new(
313    ///     Severity::Error,
314    ///     "boom",
315    ///     Label::new(Span::new(2, 5), "right here"),
316    /// );
317    /// assert_eq!(diag.primary().message(), "right here");
318    /// ```
319    #[inline]
320    #[must_use]
321    pub const fn primary(&self) -> &Label {
322        &self.primary
323    }
324
325    /// Returns the secondary labels, in the order they were added.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use diag_lang::{Diagnostic, Label, Severity};
331    /// use span_lang::Span;
332    ///
333    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
334    ///     .with_secondary(Label::new(Span::new(1, 2), "related"));
335    /// assert_eq!(diag.secondary()[0].message(), "related");
336    /// ```
337    #[inline]
338    #[must_use]
339    pub fn secondary(&self) -> &[Label] {
340        &self.secondary
341    }
342
343    /// Iterates over the note lines, in the order they were added.
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// use diag_lang::{Diagnostic, Label, Severity};
349    /// use span_lang::Span;
350    ///
351    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
352    ///     .with_note("one")
353    ///     .with_note("two");
354    /// assert_eq!(diag.notes().collect::<Vec<_>>(), ["one", "two"]);
355    /// ```
356    pub fn notes(&self) -> impl ExactSizeIterator<Item = &str> {
357        self.notes.iter().map(AsRef::as_ref)
358    }
359
360    /// Iterates over the help lines, in the order they were added.
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// use diag_lang::{Diagnostic, Label, Severity};
366    /// use span_lang::Span;
367    ///
368    /// let diag = Diagnostic::new(Severity::Error, "boom", Label::unlabelled(Span::empty(0)))
369    ///     .with_help("try this");
370    /// assert_eq!(diag.help().collect::<Vec<_>>(), ["try this"]);
371    /// ```
372    pub fn help(&self) -> impl ExactSizeIterator<Item = &str> {
373        self.help.iter().map(AsRef::as_ref)
374    }
375}
376
377impl PartialEq for Diagnostic {
378    /// Two diagnostics are equal when every part is: severity, code, message,
379    /// primary label, and the secondary labels, notes, help lines, and
380    /// suggestions in order. Compared through the accessors, so an empty
381    /// suggestion list equals an absent one however it is stored.
382    fn eq(&self, other: &Self) -> bool {
383        self.severity == other.severity
384            && self.code == other.code
385            && self.message == other.message
386            && self.primary == other.primary
387            && self.secondary() == other.secondary()
388            && self.notes().eq(other.notes())
389            && self.help().eq(other.help())
390            && self.suggestions() == other.suggestions()
391    }
392}
393
394impl Eq for Diagnostic {}
395
396impl fmt::Debug for Diagnostic {
397    /// Lists every part by name, in the 1.0 field order with the 1.1 parts
398    /// (`code`, `suggestions`) after them.
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        f.debug_struct("Diagnostic")
401            .field("severity", &self.severity)
402            .field("message", &self.message)
403            .field("primary", &self.primary)
404            .field("secondary", &self.secondary())
405            .field("notes", &self.notes)
406            .field("help", &self.help)
407            .field("code", &self.code)
408            .field("suggestions", &self.suggestions())
409            .finish()
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use span_lang::Span;
416
417    use super::*;
418
419    #[test]
420    fn test_new_starts_with_empty_extras() {
421        let diag = Diagnostic::new(
422            Severity::Error,
423            "type mismatch",
424            Label::new(Span::new(8, 11), "here"),
425        );
426        assert_eq!(diag.severity(), Severity::Error);
427        assert_eq!(diag.message(), "type mismatch");
428        assert_eq!(diag.primary().span(), Span::new(8, 11));
429        assert!(diag.secondary().is_empty());
430        assert_eq!(diag.notes().count(), 0);
431        assert_eq!(diag.help().count(), 0);
432        assert_eq!(diag.code(), None);
433        assert!(diag.suggestions().is_empty());
434    }
435
436    #[test]
437    fn test_plain_diagnostic_stays_small() {
438        // The 1.0 layout plus an inline code and one pointer for suggestions:
439        // at most 16 bytes larger than 1.0's 120 on 64-bit targets.
440        assert!(core::mem::size_of::<Diagnostic>() <= 136);
441    }
442
443    #[test]
444    fn test_equality_and_debug_ignore_storage() {
445        let plain = Diagnostic::new(Severity::Error, "m", Label::unlabelled(Span::empty(0)));
446        let rich = plain.clone().with_note("n");
447        assert_eq!(plain, plain.clone());
448        assert_ne!(plain, rich);
449        assert_eq!(rich, rich.clone());
450        let debug = alloc::format!("{plain:?}");
451        assert!(debug.starts_with("Diagnostic { severity: Error, message: \"m\""));
452        assert!(debug.contains("secondary: [], notes: [], help: [], code: None, suggestions: []"));
453    }
454
455    #[test]
456    fn test_code_is_replaced_and_suggestions_accumulate() {
457        use crate::Applicability;
458
459        let e1 = Code::new("E", 1).unwrap();
460        let e2 = Code::new("E", 2).unwrap();
461        let diag = Diagnostic::new(Severity::Error, "m", Label::unlabelled(Span::empty(0)))
462            .with_code(e1)
463            .with_code(e2)
464            .with_suggestion(Suggestion::new("a", Applicability::MachineApplicable))
465            .with_suggestion(Suggestion::new("b", Applicability::Unspecified));
466        assert_eq!(diag.code(), Some(e2));
467        let order: Vec<_> = diag.suggestions().iter().map(Suggestion::message).collect();
468        assert_eq!(order, ["a", "b"]);
469    }
470
471    #[test]
472    fn test_builders_accumulate_in_order() {
473        let diag = Diagnostic::new(Severity::Error, "m", Label::unlabelled(Span::empty(0)))
474            .with_secondary(Label::new(Span::new(1, 2), "a"))
475            .with_secondary(Label::new(Span::new(3, 4), "b"))
476            .with_note("n1")
477            .with_note("n2")
478            .with_help("h1");
479
480        let secondary: Vec<_> = diag.secondary().iter().map(Label::message).collect();
481        assert_eq!(secondary, ["a", "b"]);
482        assert_eq!(diag.notes().collect::<Vec<_>>(), ["n1", "n2"]);
483        assert_eq!(diag.help().collect::<Vec<_>>(), ["h1"]);
484    }
485}