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 level(mut self, level: DiagnosticLevel) -> Self {
215        self.level = level;
216        self
217    }
218    pub fn message(mut self, message: impl Into<Message>) -> Self {
219        self.labels.message = message.into();
220        self
221    }
222
223    /// Attach a message to the primary label of this diagnostic.
224    pub fn primary_label(mut self, primary_label: impl Into<Message>) -> Self {
225        self.labels.primary_label = Some(primary_label.into());
226        self
227    }
228
229    /// Attach a secondary label to this diagnostic.
230    pub fn secondary_label(
231        mut self,
232        span: impl Into<FullSpan>,
233        message: impl Into<Message>,
234    ) -> Self {
235        self.labels
236            .secondary_labels
237            .push((span.into(), message.into()));
238        self
239    }
240
241    /// Attach a simple (one-line) note to this diagnostic.
242    pub fn note(mut self, message: impl Into<Message>) -> Self {
243        self.add_note(message);
244        self
245    }
246
247    /// Attach a simple (one-line) note to this diagnostic.
248    ///
249    /// Modifying version of [Self::note].
250    pub fn add_note(&mut self, message: impl Into<Message>) -> &mut Self {
251        self.subdiagnostics.push(Subdiagnostic::Note {
252            level: SubdiagnosticLevel::Note,
253            message: message.into(),
254        });
255        self
256    }
257
258    /// Attach a simple (one-line) help to this diagnostic.
259    ///
260    /// Builder version of [Self::add_help].
261    pub fn help(mut self, message: impl Into<Message>) -> Self {
262        self.add_help(message);
263        self
264    }
265
266    /// Attach a simple (one-line) help to this diagnostic.
267    ///
268    /// Modifying version of [Self::help].
269    pub fn add_help(&mut self, message: impl Into<Message>) -> &mut Self {
270        self.subdiagnostics.push(Subdiagnostic::Note {
271            level: SubdiagnosticLevel::Help,
272            message: message.into(),
273        });
274        self
275    }
276
277    /// Attach a general subdiagnostic to this diagnostic.
278    ///
279    /// Prefer a more specific convenicence method (see the [crate documentation])
280    /// if you can. This is intended for [spanned notes] since they need a builder
281    /// in order to be constructed.
282    ///
283    /// [crate documentation]: crate
284    /// [spanned notes]: Subdiagnostic::SpannedNote
285    pub fn subdiagnostic(mut self, subdiagnostic: Subdiagnostic) -> Self {
286        self.subdiagnostics.push(subdiagnostic);
287        self
288    }
289
290    /// See [Self::subdiagnostic].
291    pub fn push_subdiagnostic(&mut self, subdiagnostic: Subdiagnostic) -> &mut Self {
292        self.subdiagnostics.push(subdiagnostic);
293        self
294    }
295
296    pub fn span_suggest(
297        self,
298        message: impl Into<Message>,
299        span: impl Into<FullSpan>,
300        code: impl Into<String>,
301    ) -> Self {
302        self.subdiagnostic(Subdiagnostic::Suggestion {
303            parts: vec![(span.into(), code.into())],
304            message: message.into(),
305        })
306    }
307
308    /// Convenience method to suggest some code that can be inserted directly before some span.
309    ///
310    /// Note that this will be _after_ any preceding whitespace. Use
311    /// [`Diagnostic::span_suggest_insert_after`] if you want the suggestion to insert before
312    /// preceding whitespace.
313    pub fn span_suggest_insert_before(
314        self,
315        message: impl Into<Message>,
316        span: impl Into<FullSpan>,
317        code: impl Into<String>,
318    ) -> Self {
319        let (span, file) = span.into();
320        let code = code.into();
321
322        assert!(!code.is_empty());
323
324        self.span_suggest(message, (Span::new(span.start(), span.start()), file), code)
325    }
326
327    /// Convenience method to suggest some code that can be inserted directly after some span.
328    ///
329    /// Note that this will be _before_ any preceding whitespace. Use
330    /// [`Diagnostic::span_suggest_insert_before`] if you want the suggestion to insert after
331    /// preceding whitespace.
332    pub fn span_suggest_insert_after(
333        self,
334        message: impl Into<Message>,
335        span: impl Into<FullSpan>,
336        code: impl Into<String>,
337    ) -> Self {
338        let (span, file) = span.into();
339        let code = code.into();
340
341        assert!(!code.is_empty());
342
343        self.span_suggest(message, (Span::new(span.end(), span.end()), file), code)
344    }
345
346    /// Convenience method to suggest some code that can be replaced.
347    pub fn span_suggest_replace(
348        self,
349        message: impl Into<Message>,
350        span: impl Into<FullSpan>,
351        code: impl Into<String>,
352    ) -> Self {
353        let (span, file) = span.into();
354        let code = code.into();
355
356        assert!(span.start() != span.end());
357        assert!(!code.is_empty());
358
359        self.span_suggest(message, (span, file), code)
360    }
361
362    /// Convenience method to suggest some code that can be removed.
363    pub fn span_suggest_remove(
364        self,
365        message: impl Into<Message>,
366        span: impl Into<FullSpan>,
367    ) -> Self {
368        let (span, file) = span.into();
369
370        assert!(span.start() != span.end());
371
372        self.span_suggest(message, (span, file), "")
373    }
374
375    /// Suggest a change that consists of multiple parts.
376    pub fn span_suggest_multipart(
377        mut self,
378        message: impl Into<Message>,
379        parts: SuggestionParts,
380    ) -> Self {
381        self.push_span_suggest_multipart(message, parts);
382        self
383    }
384
385    /// Suggest a change that consists of multiple parts, but usable outside of builders.
386    pub fn push_span_suggest_multipart(
387        &mut self,
388        message: impl Into<Message>,
389        SuggestionParts(parts): SuggestionParts,
390    ) -> &mut Self {
391        self.subdiagnostics.push(Subdiagnostic::Suggestion {
392            parts,
393            message: message.into(),
394        });
395        self
396    }
397
398    pub fn type_error(
399        mut self,
400        expected: String,
401        expected_outer: Option<String>,
402        got: String,
403        got_outer: Option<String>,
404    ) -> Self {
405        self.push_subdiagnostic(Subdiagnostic::TypeMismatch {
406            got,
407            got_outer,
408            expected,
409            expected_outer,
410        });
411        self
412    }
413}
414
415// Assert that something holds, if it does not, return a [`Diagnostic::bug`] with the specified
416// span
417#[macro_export]
418macro_rules! diag_assert {
419    ($span:expr, $condition:expr) => {
420        diag_assert!($span, $condition, "Assertion {} failed", stringify!($condition))
421    };
422    ($span:expr, $condition:expr, $($rest:tt)*) => {
423        if !$condition {
424            return Err(Diagnostic::bug(
425                $span,
426                format!($($rest)*),
427            )
428            .into());
429        }
430    };
431}
432
433/// Like `anyhow!` but for diagnostics. Attaches the message to the specified expression
434#[macro_export]
435macro_rules! diag_anyhow {
436    ($span:expr, $($arg:tt)*) => {
437        spade_diagnostics::Diagnostic::bug($span, format!($($arg)*))
438            .note(format!("Triggered at {}:{}", file!(), line!()))
439    }
440}
441
442/// Like `bail!` but for diagnostics. Attaches the message to the specified expression
443#[macro_export]
444macro_rules! diag_bail {
445    ($span:expr, $($arg:tt)*) => {
446        return Err(spade_diagnostics::diag_anyhow!($span, $($arg)*).into())
447    }
448}