miette/miette_diagnostic.rs
1use std::{
2 borrow::Cow,
3 error::Error,
4 fmt::{self, Debug, Display},
5 mem,
6 ops::{Deref, DerefMut},
7 slice::{Iter, IterMut},
8};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13use crate::{Diagnostic, LabeledSpan, Severity};
14
15/// Diagnostic that can be created at runtime.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct MietteDiagnostic {
19 /// Displayed diagnostic message
20 pub message: String,
21 /// Unique diagnostic code to look up more information
22 /// about this Diagnostic. Ideally also globally unique, and documented
23 /// in the toplevel crate's documentation for easy searching.
24 /// Rust path format (`foo::bar::baz`) is recommended, but more classic
25 /// codes like `E0123` will work just fine
26 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
27 pub code: Option<String>,
28 /// [`Diagnostic`] severity. Intended to be used by
29 /// [`ReportHandler`](crate::ReportHandler)s to change the way different
30 /// [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`]
31 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
32 pub severity: Option<Severity>,
33 /// Additional help text related to this Diagnostic
34 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
35 pub help: Option<String>,
36 /// Additional note text related to this Diagnostic
37 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
38 pub note: Option<String>,
39 /// URL to visit for a more detailed explanation/help about this
40 /// [`Diagnostic`].
41 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
42 pub url: Option<String>,
43 /// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
44 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Labels::is_empty"))]
45 pub labels: Labels,
46}
47
48/// Container for a [`MietteDiagnostic`]'s labels.
49///
50/// Most diagnostics carry only one or two labels, so those cases are stored
51/// inline without a heap allocation. Diagnostics with three or more labels spill
52/// to a [`Vec`].
53#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub enum Labels {
55 /// No labels.
56 #[default]
57 None,
58 /// A single label, stored inline.
59 One([LabeledSpan; 1]),
60 /// Two labels, stored inline.
61 Two([LabeledSpan; 2]),
62 /// Three or more labels, stored on the heap.
63 Many(Vec<LabeledSpan>),
64}
65
66impl Labels {
67 /// Returns the labels as a contiguous slice.
68 #[must_use]
69 pub fn as_slice(&self) -> &[LabeledSpan] {
70 match self {
71 Labels::None => &[],
72 Labels::One(labels) => labels,
73 Labels::Two(labels) => labels,
74 Labels::Many(labels) => labels,
75 }
76 }
77
78 /// Returns the labels as a mutable contiguous slice.
79 #[must_use]
80 pub fn as_mut_slice(&mut self) -> &mut [LabeledSpan] {
81 match self {
82 Labels::None => &mut [],
83 Labels::One(labels) => labels,
84 Labels::Two(labels) => labels,
85 Labels::Many(labels) => labels,
86 }
87 }
88
89 /// Returns `true` if there are no labels.
90 #[must_use]
91 pub fn is_empty(&self) -> bool {
92 matches!(self, Labels::None)
93 }
94
95 /// Returns the number of labels.
96 #[must_use]
97 pub fn len(&self) -> usize {
98 self.as_slice().len()
99 }
100
101 /// Appends a label, keeping the storage inline while possible.
102 pub fn push(&mut self, label: LabeledSpan) {
103 // Fast path: already on the heap, push in place without moving the `Vec`.
104 if let Labels::Many(labels) = self {
105 labels.push(label);
106 return;
107 }
108 *self = match mem::take(self) {
109 Labels::None => Labels::One([label]),
110 Labels::One([a]) => Labels::Two([a, label]),
111 Labels::Two([a, b]) => Labels::Many(vec![a, b, label]),
112 Labels::Many(_) => unreachable!("handled by the fast path above"),
113 };
114 }
115}
116
117impl Deref for Labels {
118 type Target = [LabeledSpan];
119
120 fn deref(&self) -> &Self::Target {
121 self.as_slice()
122 }
123}
124
125impl DerefMut for Labels {
126 fn deref_mut(&mut self) -> &mut Self::Target {
127 self.as_mut_slice()
128 }
129}
130
131impl<'a> IntoIterator for &'a Labels {
132 type Item = &'a LabeledSpan;
133 type IntoIter = Iter<'a, LabeledSpan>;
134
135 fn into_iter(self) -> Self::IntoIter {
136 self.as_slice().iter()
137 }
138}
139
140impl<'a> IntoIterator for &'a mut Labels {
141 type Item = &'a mut LabeledSpan;
142 type IntoIter = IterMut<'a, LabeledSpan>;
143
144 fn into_iter(self) -> Self::IntoIter {
145 self.as_mut_slice().iter_mut()
146 }
147}
148
149impl Extend<LabeledSpan> for Labels {
150 fn extend<I: IntoIterator<Item = LabeledSpan>>(&mut self, iter: I) {
151 let mut iter = iter.into_iter();
152 // Fill the inline tiers first — allocation-free while staying at 1-2.
153 while !matches!(self, Labels::Many(_)) {
154 match iter.next() {
155 Some(label) => self.push(label),
156 None => return,
157 }
158 }
159 // Once on the heap, reserve once and bulk-extend instead of re-growing
160 // the `Vec` on every element.
161 if let Labels::Many(labels) = self {
162 labels.reserve(iter.size_hint().0);
163 labels.extend(iter);
164 }
165 }
166}
167
168impl FromIterator<LabeledSpan> for Labels {
169 fn from_iter<I: IntoIterator<Item = LabeledSpan>>(iter: I) -> Self {
170 let mut iter = iter.into_iter();
171 // If the iterator already reports more than two elements, it will spill
172 // to the heap regardless, so collect straight into a `Vec`. For a
173 // `vec::IntoIter` source `collect` reuses the original allocation, so
174 // `with_labels(vec)` does not allocate at all.
175 if iter.size_hint().0 > 2 {
176 return Labels::Many(iter.collect());
177 }
178 // Otherwise pull up to three elements to pick the smallest variant
179 // that fits without allocating for the common one/two-label cases.
180 let Some(a) = iter.next() else { return Labels::None };
181 let Some(b) = iter.next() else { return Labels::One([a]) };
182 let Some(c) = iter.next() else { return Labels::Two([a, b]) };
183 let mut labels = Vec::with_capacity(3 + iter.size_hint().0);
184 labels.extend([a, b, c]);
185 labels.extend(iter);
186 Labels::Many(labels)
187 }
188}
189
190impl From<Vec<LabeledSpan>> for Labels {
191 fn from(labels: Vec<LabeledSpan>) -> Self {
192 if labels.len() <= 2 { labels.into_iter().collect() } else { Labels::Many(labels) }
193 }
194}
195
196impl From<LabeledSpan> for Labels {
197 fn from(label: LabeledSpan) -> Self {
198 Labels::One([label])
199 }
200}
201
202impl From<[LabeledSpan; 1]> for Labels {
203 fn from(labels: [LabeledSpan; 1]) -> Self {
204 Labels::One(labels)
205 }
206}
207
208impl From<[LabeledSpan; 2]> for Labels {
209 fn from(labels: [LabeledSpan; 2]) -> Self {
210 Labels::Two(labels)
211 }
212}
213
214#[cfg(feature = "serde")]
215impl Serialize for Labels {
216 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
217 self.as_slice().serialize(serializer)
218 }
219}
220
221#[cfg(feature = "serde")]
222impl<'de> Deserialize<'de> for Labels {
223 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
224 // Accept both a sequence and `null` (the latter mirrors the previous
225 // `Option<Vec<LabeledSpan>>` representation).
226 let labels = Option::<Vec<LabeledSpan>>::deserialize(deserializer)?;
227 Ok(labels.map_or(Labels::None, Labels::from))
228 }
229}
230
231impl Display for MietteDiagnostic {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 write!(f, "{}", self.message)
234 }
235}
236
237impl Error for MietteDiagnostic {}
238
239impl Diagnostic for MietteDiagnostic {
240 fn code(&self) -> Option<Cow<'_, str>> {
241 self.code.as_deref().map(Cow::Borrowed)
242 }
243
244 fn severity(&self) -> Option<Severity> {
245 self.severity
246 }
247
248 fn help(&self) -> Option<Cow<'_, str>> {
249 self.help.as_deref().map(Cow::Borrowed)
250 }
251
252 fn note(&self) -> Option<Cow<'_, str>> {
253 self.note.as_deref().map(Cow::Borrowed)
254 }
255
256 fn url(&self) -> Option<Cow<'_, str>> {
257 self.url.as_deref().map(Cow::Borrowed)
258 }
259
260 fn labels(&self) -> Labels {
261 self.labels.clone()
262 }
263}
264
265impl MietteDiagnostic {
266 /// Create a new dynamic diagnostic with the given message.
267 ///
268 /// # Examples
269 /// ```
270 /// use miette::{Diagnostic, MietteDiagnostic, Severity};
271 ///
272 /// let diag = MietteDiagnostic::new("Oops, something went wrong!");
273 /// assert_eq!(diag.to_string(), "Oops, something went wrong!");
274 /// assert_eq!(diag.message, "Oops, something went wrong!");
275 /// ```
276 #[must_use]
277 pub fn new(message: impl Into<String>) -> Self {
278 Self {
279 message: message.into(),
280 labels: Labels::None,
281 severity: None,
282 code: None,
283 help: None,
284 note: None,
285 url: None,
286 }
287 }
288
289 /// Return new diagnostic with the given code.
290 ///
291 /// # Examples
292 /// ```
293 /// use miette::{Diagnostic, MietteDiagnostic};
294 ///
295 /// let diag = MietteDiagnostic::new("Oops, something went wrong!").with_code("foo::bar::baz");
296 /// assert_eq!(diag.message, "Oops, something went wrong!");
297 /// assert_eq!(diag.code, Some("foo::bar::baz".to_string()));
298 /// ```
299 #[must_use]
300 pub fn with_code(mut self, code: impl Into<String>) -> Self {
301 self.code = Some(code.into());
302 self
303 }
304
305 /// Return new diagnostic with the given severity.
306 ///
307 /// # Examples
308 /// ```
309 /// use miette::{Diagnostic, MietteDiagnostic, Severity};
310 ///
311 /// let diag = MietteDiagnostic::new("I warn you to stop!").with_severity(Severity::Warning);
312 /// assert_eq!(diag.message, "I warn you to stop!");
313 /// assert_eq!(diag.severity, Some(Severity::Warning));
314 /// ```
315 #[must_use]
316 pub fn with_severity(mut self, severity: Severity) -> Self {
317 self.severity = Some(severity);
318 self
319 }
320
321 /// Return new diagnostic with the given help message.
322 ///
323 /// # Examples
324 /// ```
325 /// use miette::{Diagnostic, MietteDiagnostic};
326 ///
327 /// let diag = MietteDiagnostic::new("PC is not working").with_help("Try to reboot it again");
328 /// assert_eq!(diag.message, "PC is not working");
329 /// assert_eq!(diag.help, Some("Try to reboot it again".to_string()));
330 /// ```
331 #[must_use]
332 pub fn with_help(mut self, help: impl Into<String>) -> Self {
333 self.help = Some(help.into());
334 self
335 }
336
337 /// Return new diagnostic with the given note.
338 ///
339 /// # Examples
340 /// ```
341 /// use miette::{Diagnostic, MietteDiagnostic};
342 ///
343 /// let diag = MietteDiagnostic::new("Something went wrong")
344 /// .with_note("This is additional context");
345 /// assert_eq!(diag.note, Some("This is additional context".to_string()));
346 /// assert_eq!(diag.message, "Something went wrong");
347 /// ```
348 #[must_use]
349 pub fn with_note(mut self, note: impl Into<String>) -> Self {
350 self.note = Some(note.into());
351 self
352 }
353
354 /// Return new diagnostic with the given URL.
355 ///
356 /// # Examples
357 /// ```
358 /// use miette::{Diagnostic, MietteDiagnostic};
359 ///
360 /// let diag = MietteDiagnostic::new("PC is not working")
361 /// .with_url("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work");
362 /// assert_eq!(diag.message, "PC is not working");
363 /// assert_eq!(
364 /// diag.url,
365 /// Some("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work".to_string())
366 /// );
367 /// ```
368 #[must_use]
369 pub fn with_url(mut self, url: impl Into<String>) -> Self {
370 self.url = Some(url.into());
371 self
372 }
373
374 /// Return new diagnostic with the given label.
375 ///
376 /// Discards previous labels
377 ///
378 /// # Examples
379 /// ```
380 /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
381 ///
382 /// let source = "cpp is the best language";
383 ///
384 /// let label = LabeledSpan::at(0..3, "This should be Rust");
385 /// let diag = MietteDiagnostic::new("Wrong best language").with_label(label.clone());
386 /// assert_eq!(diag.message, "Wrong best language");
387 /// assert_eq!(diag.labels.as_slice(), &[label]);
388 /// ```
389 #[must_use]
390 pub fn with_label(mut self, label: impl Into<LabeledSpan>) -> Self {
391 self.labels = Labels::One([label.into()]);
392 self
393 }
394
395 /// Return new diagnostic with the given labels.
396 ///
397 /// Discards previous labels
398 ///
399 /// # Examples
400 /// ```
401 /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
402 ///
403 /// let source = "hello wrld";
404 ///
405 /// let labels = vec![
406 /// LabeledSpan::at_offset(3, "add 'l'"),
407 /// LabeledSpan::at_offset(6, "add 'r'"),
408 /// ];
409 /// let diag = MietteDiagnostic::new("Typos in 'hello world'").with_labels(labels.clone());
410 /// assert_eq!(diag.message, "Typos in 'hello world'");
411 /// assert_eq!(diag.labels.as_slice(), labels.as_slice());
412 /// ```
413 #[must_use]
414 pub fn with_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
415 self.labels = labels.into_iter().collect();
416 self
417 }
418
419 /// Return new diagnostic with new label added to the existing ones.
420 ///
421 /// # Examples
422 /// ```
423 /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
424 ///
425 /// let source = "hello wrld";
426 ///
427 /// let label1 = LabeledSpan::at_offset(3, "add 'l'");
428 /// let label2 = LabeledSpan::at_offset(6, "add 'r'");
429 /// let diag = MietteDiagnostic::new("Typos in 'hello world'")
430 /// .and_label(label1.clone())
431 /// .and_label(label2.clone());
432 /// assert_eq!(diag.message, "Typos in 'hello world'");
433 /// assert_eq!(diag.labels.as_slice(), &[label1, label2]);
434 /// ```
435 #[must_use]
436 pub fn and_label(mut self, label: impl Into<LabeledSpan>) -> Self {
437 self.labels.push(label.into());
438 self
439 }
440
441 /// Return new diagnostic with new labels added to the existing ones.
442 ///
443 /// # Examples
444 /// ```
445 /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
446 ///
447 /// let source = "hello wrld";
448 ///
449 /// let label1 = LabeledSpan::at_offset(3, "add 'l'");
450 /// let label2 = LabeledSpan::at_offset(6, "add 'r'");
451 /// let label3 = LabeledSpan::at_offset(9, "add '!'");
452 /// let diag = MietteDiagnostic::new("Typos in 'hello world!'")
453 /// .and_label(label1.clone())
454 /// .and_labels([label2.clone(), label3.clone()]);
455 /// assert_eq!(diag.message, "Typos in 'hello world!'");
456 /// assert_eq!(diag.labels.as_slice(), &[label1, label2, label3]);
457 /// ```
458 #[must_use]
459 pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
460 self.labels.extend(labels);
461 self
462 }
463}
464
465#[cfg(feature = "serde")]
466#[test]
467fn test_serialize_miette_diagnostic() {
468 use serde_json::json;
469
470 use crate::diagnostic;
471
472 let diag = diagnostic!("message");
473 let json = json!({ "message": "message" });
474 assert_eq!(json!(diag), json);
475
476 let diag = diagnostic!(
477 code = "code",
478 help = "help",
479 url = "url",
480 labels = [LabeledSpan::at_offset(0, "label1"), LabeledSpan::at(1..3, "label2")],
481 severity = Severity::Warning,
482 "message"
483 );
484 let json = json!({
485 "message": "message",
486 "code": "code",
487 "help": "help",
488 "url": "url",
489 "severity": "Warning",
490 "labels": [
491 {
492 "span": {
493 "offset": 0,
494 "length": 0
495 },
496 "label": "label1",
497 "primary": false
498 },
499 {
500 "span": {
501 "offset": 1,
502 "length": 2
503 },
504 "label": "label2",
505 "primary": false
506 }
507 ]
508 });
509 assert_eq!(json!(diag), json);
510}
511
512#[cfg(feature = "serde")]
513#[test]
514fn test_deserialize_miette_diagnostic() {
515 use serde_json::json;
516
517 use crate::diagnostic;
518
519 let json = json!({ "message": "message" });
520 let diag = diagnostic!("message");
521 assert_eq!(diag, serde_json::from_value(json).unwrap());
522
523 let json = json!({
524 "message": "message",
525 "help": null,
526 "code": null,
527 "severity": null,
528 "url": null,
529 "labels": null
530 });
531 assert_eq!(diag, serde_json::from_value(json).unwrap());
532
533 let diag = diagnostic!(
534 code = "code",
535 help = "help",
536 url = "url",
537 labels = [LabeledSpan::at_offset(0, "label1"), LabeledSpan::at(1..3, "label2")],
538 severity = Severity::Warning,
539 "message"
540 );
541 let json = json!({
542 "message": "message",
543 "code": "code",
544 "help": "help",
545 "url": "url",
546 "severity": "Warning",
547 "labels": [
548 {
549 "span": {
550 "offset": 0,
551 "length": 0
552 },
553 "label": "label1",
554 "primary": false
555 },
556 {
557 "span": {
558 "offset": 1,
559 "length": 2
560 },
561 "label": "label2",
562 "primary": false
563 }
564 ]
565 });
566 assert_eq!(diag, serde_json::from_value(json).unwrap());
567}