Skip to main content

deed_diagnostics/
diagnostic.rs

1//! Structured diagnostics.
2//!
3//! P7 in `design/01-principles.md` says compiler output is an API. That has one
4//! concrete consequence here: a diagnostic is data, and the human readable text
5//! is a rendering of that data rather than the thing itself. Nothing in the
6//! compiler is allowed to produce an error as a bare `String`.
7
8use crate::source::FileId;
9use crate::span::Span;
10
11#[derive(Clone, Copy, PartialEq, Eq, Debug)]
12pub enum Severity {
13    Error,
14    Warning,
15}
16
17impl Severity {
18    pub fn as_str(self) -> &'static str {
19        match self {
20            Severity::Error => "error",
21            Severity::Warning => "warning",
22        }
23    }
24}
25
26/// A span with something to say about it.
27///
28/// The file is `None` for the ordinary case, which is a label about the file
29/// the diagnostic was filed against, and that is most of them: 23 of the 31
30/// places this compiler builds one can only ever have a span from their own
31/// file in hand. `Some` is for the rest, where the thing worth pointing at is
32/// somewhere else, and until it existed those producers had to choose between
33/// drawing a caret over whatever happened to sit at those byte offsets in the
34/// wrong file and saying nothing. They all chose to say nothing, which is the
35/// right call and is still a label a reader does not get.
36#[derive(Clone, Debug)]
37pub struct Label {
38    pub span: Span,
39    /// Where `span` is an offset into. `None` means the diagnostic's own file.
40    pub file: Option<FileId>,
41    pub message: String,
42}
43
44impl Label {
45    pub fn new(span: Span, message: impl Into<String>) -> Self {
46        Self {
47            span,
48            file: None,
49            message: message.into(),
50        }
51    }
52
53    /// A label about a file other than the one the diagnostic is filed against.
54    pub fn in_file(file: FileId, span: Span, message: impl Into<String>) -> Self {
55        Self {
56            span,
57            file: Some(file),
58            message: message.into(),
59        }
60    }
61
62    /// The file this label is about, given the diagnostic it belongs to.
63    pub fn file_or(&self, diagnostic: FileId) -> FileId {
64        self.file.unwrap_or(diagnostic)
65    }
66}
67
68/// How much a tool should trust a fix.
69#[derive(Clone, Copy, PartialEq, Eq, Debug)]
70pub enum Applicability {
71    /// The fix is certainly what was meant and can be applied without asking.
72    MachineApplicable,
73    /// The fix is a guess. Offer it, do not apply it.
74    MaybeIncorrect,
75}
76
77impl Applicability {
78    pub fn as_str(self) -> &'static str {
79        match self {
80            Applicability::MachineApplicable => "machine-applicable",
81            Applicability::MaybeIncorrect => "maybe-incorrect",
82        }
83    }
84}
85
86/// A single text replacement.
87#[derive(Clone, Debug)]
88pub struct SuggestedEdit {
89    pub span: Span,
90    pub replacement: String,
91}
92
93/// A set of edits that together resolve a diagnostic.
94#[derive(Clone, Debug)]
95pub struct Fix {
96    pub message: String,
97    pub edits: Vec<SuggestedEdit>,
98    pub applicability: Applicability,
99}
100
101/// One problem, at one place, with one cause.
102///
103/// Cascades are deliberately not modelled. If a single root cause would produce
104/// several diagnostics, the producer is expected to emit one diagnostic with
105/// secondary labels instead, because a wall of derived errors buries the line
106/// that actually needs editing.
107#[derive(Clone, Debug)]
108pub struct Diagnostic {
109    /// Stable identifier, for example `DEED1002`. Codes are never reused.
110    pub code: &'static str,
111    pub severity: Severity,
112    pub message: String,
113    pub file: FileId,
114    pub primary: Label,
115    pub secondary: Vec<Label>,
116    pub notes: Vec<String>,
117    pub fix: Option<Fix>,
118}
119
120impl Diagnostic {
121    pub fn error(code: &'static str, file: FileId, span: Span, message: impl Into<String>) -> Self {
122        let message = message.into();
123        Self {
124            code,
125            severity: Severity::Error,
126            primary: Label::new(span, message.clone()),
127            message,
128            file,
129            secondary: Vec::new(),
130            notes: Vec::new(),
131            fix: None,
132        }
133    }
134
135    pub fn warning(
136        code: &'static str,
137        file: FileId,
138        span: Span,
139        message: impl Into<String>,
140    ) -> Self {
141        let mut diagnostic = Self::error(code, file, span, message);
142        diagnostic.severity = Severity::Warning;
143        diagnostic
144    }
145
146    /// Overrides the primary label, when the underline should say something
147    /// shorter or more specific than the headline message.
148    #[must_use]
149    pub fn with_primary_label(mut self, message: impl Into<String>) -> Self {
150        self.primary.message = message.into();
151        self
152    }
153
154    #[must_use]
155    pub fn with_secondary(mut self, span: Span, message: impl Into<String>) -> Self {
156        self.secondary.push(Label::new(span, message));
157        self
158    }
159
160    /// The same, about somewhere else.
161    ///
162    /// For the producer that has a span from another module in hand: a
163    /// precondition failure names a clause in the callee and is filed against
164    /// the caller, and a postcondition failure is the other way round. Passing
165    /// the diagnostic's own file here is harmless and says the same thing as
166    /// [`Self::with_secondary`].
167    #[must_use]
168    pub fn with_secondary_in(
169        mut self,
170        file: FileId,
171        span: Span,
172        message: impl Into<String>,
173    ) -> Self {
174        self.secondary.push(Label::in_file(file, span, message));
175        self
176    }
177
178    #[must_use]
179    pub fn with_note(mut self, note: impl Into<String>) -> Self {
180        self.notes.push(note.into());
181        self
182    }
183
184    #[must_use]
185    pub fn with_fix(
186        mut self,
187        message: impl Into<String>,
188        span: Span,
189        replacement: impl Into<String>,
190        applicability: Applicability,
191    ) -> Self {
192        self.fix = Some(Fix {
193            message: message.into(),
194            edits: vec![SuggestedEdit {
195                span,
196                replacement: replacement.into(),
197            }],
198            applicability,
199        });
200        self
201    }
202
203    /// A fix made of several edits that only mean anything together.
204    ///
205    /// One edit is the ordinary case, which is what [`Self::with_fix`] is for.
206    /// This is for a repair that has to wrap something: turning `n as String`
207    /// into `to_string(n)` is an insertion in front of the value and a
208    /// replacement behind it, and either half on its own leaves the line worse
209    /// than it was found. Whoever applies a fix has to take all of it or none.
210    ///
211    /// The edits are given in the order they appear in the file.
212    #[must_use]
213    pub fn with_edits(
214        mut self,
215        message: impl Into<String>,
216        edits: Vec<SuggestedEdit>,
217        applicability: Applicability,
218    ) -> Self {
219        self.fix = Some(Fix {
220            message: message.into(),
221            edits,
222            applicability,
223        });
224        self
225    }
226
227    pub fn is_error(&self) -> bool {
228        self.severity == Severity::Error
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::{Applicability, Diagnostic, Severity};
235    use crate::source::SourceMap;
236    use crate::span::Span;
237
238    #[test]
239    fn primary_label_defaults_to_the_message() {
240        let mut map = SourceMap::new();
241        let file = map.add("t.deed", "let x = 1");
242        let d = Diagnostic::error("DEED0001", file, Span::new(0, 3), "something went wrong");
243        assert_eq!(d.primary.message, "something went wrong");
244        assert!(d.is_error());
245    }
246
247    #[test]
248    fn builders_compose() {
249        let mut map = SourceMap::new();
250        let file = map.add("t.deed", "let x = 1");
251        let d = Diagnostic::warning("DEED0002", file, Span::new(0, 3), "headline")
252            .with_primary_label("here")
253            .with_secondary(Span::new(4, 5), "related")
254            .with_note("a note")
255            .with_fix(
256                "try this",
257                Span::new(0, 3),
258                "val",
259                Applicability::MachineApplicable,
260            );
261
262        assert_eq!(d.severity, Severity::Warning);
263        assert_eq!(d.primary.message, "here");
264        assert_eq!(d.secondary.len(), 1);
265        assert_eq!(d.notes, vec!["a note".to_string()]);
266        assert_eq!(d.fix.unwrap().edits[0].replacement, "val");
267    }
268}