Skip to main content

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}