Skip to main content

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}