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