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}