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}