spade_diagnostics/
diagnostic.rs

1use spade_codespan::Span;
2use spade_codespan_reporting::diagnostic::Severity;
3
4use spade_common::location_info::FullSpan;
5
6const INTERNAL_BUG_NOTE: &str = r#"This is an internal bug in the compiler.
7We would appreciate if you opened an issue in the repository:
8https://gitlab.com/spade-lang/spade/-/issues/new?issuable_template=Internal%20bug"#;
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum Message {
12    Str(String),
13    // FluentIdentifier(String) for translated messages.
14}
15
16impl Message {
17    pub fn as_str(&self) -> &str {
18        match self {
19            Message::Str(s) => s,
20        }
21    }
22}
23
24impl From<String> for Message {
25    fn from(other: String) -> Message {
26        Message::Str(other)
27    }
28}
29
30impl From<&str> for Message {
31    fn from(other: &str) -> Message {
32        Message::from(other.to_string())
33    }
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub enum DiagnosticLevel {
38    /// An internal error in the compiler that shouldn't happen.
39    Bug,
40    Error,
41    Warning,
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub enum SubdiagnosticLevel {
46    Help,
47    Note,
48}
49
50impl DiagnosticLevel {
51    pub fn as_str(&self) -> &'static str {
52        match self {
53            DiagnosticLevel::Bug => "internal bug",
54            DiagnosticLevel::Error => "error",
55            DiagnosticLevel::Warning => "warning",
56        }
57    }
58
59    pub fn severity(&self) -> Severity {
60        match self {
61            DiagnosticLevel::Bug => Severity::Bug,
62            DiagnosticLevel::Error => Severity::Error,
63            DiagnosticLevel::Warning => Severity::Warning,
64        }
65    }
66}
67
68impl SubdiagnosticLevel {
69    pub fn as_str(&self) -> &'static str {
70        match self {
71            SubdiagnosticLevel::Help => "help",
72            SubdiagnosticLevel::Note => "note",
73        }
74    }
75
76    pub fn severity(&self) -> Severity {
77        match self {
78            SubdiagnosticLevel::Help => Severity::Help,
79            SubdiagnosticLevel::Note => Severity::Note,
80        }
81    }
82}
83
84#[derive(Debug, Clone, PartialEq)]
85pub struct Labels {
86    pub message: Message,
87    /// The "primary location" of this diagnostic.
88    pub span: FullSpan,
89    /// Optionally, the primary location can be labeled. If None, it is only underlined.
90    pub primary_label: Option<Message>,
91    /// Secondary locations that further explain the reasoning behind the diagnostic.
92    pub secondary_labels: Vec<(FullSpan, Message)>,
93}
94
95/// Something that is wrong in the code.
96#[must_use]
97#[derive(Debug, Clone, PartialEq)]
98pub struct Diagnostic {
99    pub level: DiagnosticLevel,
100    pub labels: Labels,
101    /// Extra diagnostics that are shown after the main diagnostic.
102    pub subdiagnostics: Vec<Subdiagnostic>,
103}
104
105/// An extra diagnostic that can further the main diagnostic in some way.
106#[derive(Debug, Clone, PartialEq)]
107pub enum Subdiagnostic {
108    /// A simple note without a span.
109    Note {
110        level: SubdiagnosticLevel,
111        message: Message,
112    },
113    TypeMismatch {
114        got: String,
115        got_outer: Option<String>,
116        expected: String,
117        expected_outer: Option<String>,
118    },
119    /// A longer note with additional spans and labels.
120    SpannedNote {
121        level: SubdiagnosticLevel,
122        labels: Labels,
123    },
124    TemplateTraceback {
125        span: FullSpan,
126        message: Message,
127    },
128    /// A change suggestion, made up of one or more suggestion parts.
129    Suggestion {
130        /// The individual replacements that make up this suggestion.
131        ///
132        /// Additions, removals and replacements are encoded using the span and the suggested
133        /// replacement according to the following table:
134        ///
135        ///```text
136        /// +-----------+-------------+----------------+
137        /// | Span      | Replacement | Interpretation |
138        /// +-----------+-------------+----------------+
139        /// | Non-empty | Non-empty   | Replacement    |
140        /// | Non-empty | Empty       | Removal        |
141        /// | Empty     | Non-empty   | Addition       |
142        /// | Empty     | Empty       | Invalid        |
143        /// +-----------+-------------+----------------+
144        ///```
145        parts: Vec<(FullSpan, String)>,
146        message: Message,
147    },
148}
149
150impl Subdiagnostic {
151    pub fn span_note(span: impl Into<FullSpan>, message: impl Into<Message>) -> Self {
152        Subdiagnostic::SpannedNote {
153            level: SubdiagnosticLevel::Note,
154            labels: Labels {
155                message: message.into(),
156                span: span.into(),
157                primary_label: None,
158                secondary_labels: Vec::new(),
159            },
160        }
161    }
162}
163
164/// Builder for use with [Diagnostic::span_suggest_multipart].
165pub struct SuggestionParts(Vec<(FullSpan, String)>);
166
167impl Default for SuggestionParts {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl SuggestionParts {
174    pub fn new() -> Self {
175        Self(Vec::new())
176    }
177
178    pub fn part(mut self, span: impl Into<FullSpan>, code: impl Into<String>) -> Self {
179        self.0.push((span.into(), code.into()));
180        self
181    }
182
183    pub fn push_part(&mut self, span: impl Into<FullSpan>, code: impl Into<String>) {
184        self.0.push((span.into(), code.into()));
185    }
186}
187
188impl Diagnostic {
189    fn new(level: DiagnosticLevel, span: impl Into<FullSpan>, message: impl Into<Message>) -> Self {
190        Self {
191            level,
192            labels: Labels {
193                message: message.into(),
194                span: span.into(),
195                primary_label: None,
196                secondary_labels: Vec::new(),
197            },
198            subdiagnostics: Vec::new(),
199        }
200    }
201
202    /// Report that something happened in the compiler that shouldn't be possible. This signifies
203    /// that something is wrong with the compiler. It will include a large footer instructing the
204    /// user to create an issue or otherwise get in touch.
205    pub fn bug(span: impl Into<FullSpan>, message: impl Into<Message>) -> Self {
206        Self::new(DiagnosticLevel::Bug, span, message).note(INTERNAL_BUG_NOTE)
207    }
208
209    /// Report that something is wrong with the supplied code.
210    pub fn error(span: impl Into<FullSpan>, message: impl Into<Message>) -> Self {
211        Self::new(DiagnosticLevel::Error, span, message)
212    }
213
214    pub fn warning(span: impl Into<FullSpan>, message: impl Into<Message>) -> Self {
215        Self::new(DiagnosticLevel::Warning, span, message)
216    }
217
218    pub fn level(mut self, level: DiagnosticLevel) -> Self {
219        self.level = level;
220        self
221    }
222    pub fn message(mut self, message: impl Into<Message>) -> Self {
223        self.labels.message = message.into();
224        self
225    }
226
227    /// Attach a message to the primary label of this diagnostic.
228    pub fn primary_label(mut self, primary_label: impl Into<Message>) -> Self {
229        self.labels.primary_label = Some(primary_label.into());
230        self
231    }
232
233    /// Attach a secondary label to this diagnostic.
234    pub fn secondary_label(
235        mut self,
236        span: impl Into<FullSpan>,
237        message: impl Into<Message>,
238    ) -> Self {
239        self.labels
240            .secondary_labels
241            .push((span.into(), message.into()));
242        self
243    }
244
245    /// Attach a simple (one-line) note to this diagnostic.
246    pub fn note(mut self, message: impl Into<Message>) -> Self {
247        self.add_note(message);
248        self
249    }
250
251    /// Attach a simple (one-line) note to this diagnostic.
252    ///
253    /// Modifying version of [Self::note].
254    pub fn add_note(&mut self, message: impl Into<Message>) -> &mut Self {
255        self.subdiagnostics.push(Subdiagnostic::Note {
256            level: SubdiagnosticLevel::Note,
257            message: message.into(),
258        });
259        self
260    }
261
262    /// Attach a simple (one-line) help to this diagnostic.
263    ///
264    /// Builder version of [Self::add_help].
265    pub fn help(mut self, message: impl Into<Message>) -> Self {
266        self.add_help(message);
267        self
268    }
269
270    /// Attach a simple (one-line) help to this diagnostic.
271    ///
272    /// Modifying version of [Self::help].
273    pub fn add_help(&mut self, message: impl Into<Message>) -> &mut Self {
274        self.subdiagnostics.push(Subdiagnostic::Note {
275            level: SubdiagnosticLevel::Help,
276            message: message.into(),
277        });
278        self
279    }
280
281    /// Attach a general subdiagnostic to this diagnostic.
282    ///
283    /// Prefer a more specific convenicence method (see the [crate documentation])
284    /// if you can. This is intended for [spanned notes] since they need a builder
285    /// in order to be constructed.
286    ///
287    /// [crate documentation]: crate
288    /// [spanned notes]: Subdiagnostic::SpannedNote
289    pub fn subdiagnostic(mut self, subdiagnostic: Subdiagnostic) -> Self {
290        self.subdiagnostics.push(subdiagnostic);
291        self
292    }
293
294    /// See [Self::subdiagnostic].
295    pub fn push_subdiagnostic(&mut self, subdiagnostic: Subdiagnostic) -> &mut Self {
296        self.subdiagnostics.push(subdiagnostic);
297        self
298    }
299
300    pub fn span_suggest(
301        self,
302        message: impl Into<Message>,
303        span: impl Into<FullSpan>,
304        code: impl Into<String>,
305    ) -> Self {
306        self.subdiagnostic(Subdiagnostic::Suggestion {
307            parts: vec![(span.into(), code.into())],
308            message: message.into(),
309        })
310    }
311
312    /// Convenience method to suggest some code that can be inserted directly before some span.
313    ///
314    /// Note that this will be _after_ any preceding whitespace. Use
315    /// [`Diagnostic::span_suggest_insert_after`] if you want the suggestion to insert before
316    /// preceding whitespace.
317    pub fn span_suggest_insert_before(
318        self,
319        message: impl Into<Message>,
320        span: impl Into<FullSpan>,
321        code: impl Into<String>,
322    ) -> Self {
323        let (span, file) = span.into();
324        let code = code.into();
325
326        assert!(!code.is_empty());
327
328        self.span_suggest(message, (Span::new(span.start(), span.start()), file), code)
329    }
330
331    /// Convenience method to suggest some code that can be inserted directly after some span.
332    ///
333    /// Note that this will be _before_ any preceding whitespace. Use
334    /// [`Diagnostic::span_suggest_insert_before`] if you want the suggestion to insert after
335    /// preceding whitespace.
336    pub fn span_suggest_insert_after(
337        self,
338        message: impl Into<Message>,
339        span: impl Into<FullSpan>,
340        code: impl Into<String>,
341    ) -> Self {
342        let (span, file) = span.into();
343        let code = code.into();
344
345        assert!(!code.is_empty());
346
347        self.span_suggest(message, (Span::new(span.end(), span.end()), file), code)
348    }
349
350    /// Convenience method to suggest some code that can be replaced.
351    pub fn span_suggest_replace(
352        self,
353        message: impl Into<Message>,
354        span: impl Into<FullSpan>,
355        code: impl Into<String>,
356    ) -> Self {
357        let (span, file) = span.into();
358        let code = code.into();
359
360        assert!(span.start() != span.end());
361        assert!(!code.is_empty());
362
363        self.span_suggest(message, (span, file), code)
364    }
365
366    /// Convenience method to suggest some code that can be removed.
367    pub fn span_suggest_remove(
368        self,
369        message: impl Into<Message>,
370        span: impl Into<FullSpan>,
371    ) -> Self {
372        let (span, file) = span.into();
373
374        assert!(span.start() != span.end());
375
376        self.span_suggest(message, (span, file), "")
377    }
378
379    /// Suggest a change that consists of multiple parts.
380    pub fn span_suggest_multipart(
381        mut self,
382        message: impl Into<Message>,
383        parts: SuggestionParts,
384    ) -> Self {
385        self.push_span_suggest_multipart(message, parts);
386        self
387    }
388
389    /// Suggest a change that consists of multiple parts, but usable outside of builders.
390    pub fn push_span_suggest_multipart(
391        &mut self,
392        message: impl Into<Message>,
393        SuggestionParts(parts): SuggestionParts,
394    ) -> &mut Self {
395        self.subdiagnostics.push(Subdiagnostic::Suggestion {
396            parts,
397            message: message.into(),
398        });
399        self
400    }
401
402    pub fn type_error(
403        mut self,
404        expected: String,
405        expected_outer: Option<String>,
406        got: String,
407        got_outer: Option<String>,
408    ) -> Self {
409        self.push_subdiagnostic(Subdiagnostic::TypeMismatch {
410            got,
411            got_outer,
412            expected,
413            expected_outer,
414        });
415        self
416    }
417}
418
419// Assert that something holds, if it does not, return a [`Diagnostic::bug`] with the specified
420// span
421#[macro_export]
422macro_rules! diag_assert {
423    ($span:expr, $condition:expr) => {
424        diag_assert!($span, $condition, "Assertion {} failed", stringify!($condition))
425    };
426    ($span:expr, $condition:expr, $($rest:tt)*) => {
427        if !$condition {
428            return Err(Diagnostic::bug(
429                $span,
430                format!($($rest)*),
431            )
432            .into());
433        }
434    };
435}
436
437/// Like `anyhow!` but for diagnostics. Attaches the message to the specified expression
438#[macro_export]
439macro_rules! diag_anyhow {
440    ($span:expr, $($arg:tt)*) => {
441        {
442            let bt = std::backtrace::Backtrace::capture();
443            let bt = if bt.status() == std::backtrace::BacktraceStatus::Captured {
444                format!("{bt}")
445            } else {
446                format!("Rerun with RUST_BACKTRACE=1 to capture a backtrace")
447            };
448            spade_diagnostics::Diagnostic::bug($span, format!($($arg)*))
449                .note(format!("Triggered at {}:{}\n{bt}", file!(), line!()))
450        }
451    }
452}
453
454/// Like `bail!` but for diagnostics. Attaches the message to the specified expression
455#[macro_export]
456macro_rules! diag_bail {
457    ($span:expr, $($arg:tt)*) => {
458        return Err(spade_diagnostics::diag_anyhow!($span, $($arg)*).into())
459    }
460}