Skip to main content

substrate_data/
diagnostic.rs

1//! What a tool finds wrong — reading a document, building a mod, serving a
2//! request — each as one record: what it is (a code), how bad, what's
3//! wrong and where (a file, its line and column, a value's path in it).
4//! The one kind of problem every tool says and every tool reads.
5
6use std::fmt;
7
8use crate::{Atom, LineCol, Path, Schema, SyntaxError};
9
10/// What's wrong with a value, as this crate finds it: each a stable code
11/// (`SD…`) tools can match on.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Problem {
14    /// Any other problem with a value.
15    Invalid,
16    /// Text that doesn't parse.
17    Syntax,
18    /// A value of another type than its field's.
19    Expected,
20    /// A bare id where a reference goes (`@file:…`).
21    NotAReference,
22    /// A reference to another kind than its field takes.
23    WrongKind,
24    /// A key the type doesn't have.
25    UnknownField,
26    /// A required field that isn't there.
27    MissingField,
28    /// A number outside its field's range.
29    OutOfRange,
30    /// A string written as a typed value that isn't one (`@File:x`).
31    BadTyped,
32    /// Binary data that won't decode.
33    BadBinary,
34    /// A reference whose id isn't of its kind's id type (`@user:abc` where
35    /// a user's id is a number).
36    BadId,
37    /// A reference to nothing there is (a [`crate::Resolve`] says so).
38    Unresolved,
39}
40
41impl Problem {
42    /// Every problem, in code order.
43    pub const ALL: [Problem; 12] = [
44        Problem::Invalid,
45        Problem::Syntax,
46        Problem::Expected,
47        Problem::NotAReference,
48        Problem::WrongKind,
49        Problem::UnknownField,
50        Problem::MissingField,
51        Problem::OutOfRange,
52        Problem::BadTyped,
53        Problem::BadBinary,
54        Problem::BadId,
55        Problem::Unresolved,
56    ];
57
58    /// Its stable code: `SD0000` …
59    #[must_use]
60    pub const fn code(self) -> &'static str {
61        match self {
62            Problem::Invalid => "SD0000",
63            Problem::Syntax => "SD0001",
64            Problem::Expected => "SD0002",
65            Problem::NotAReference => "SD0003",
66            Problem::WrongKind => "SD0004",
67            Problem::UnknownField => "SD0005",
68            Problem::MissingField => "SD0006",
69            Problem::OutOfRange => "SD0007",
70            Problem::BadTyped => "SD0008",
71            Problem::BadBinary => "SD0009",
72            Problem::BadId => "SD0010",
73            Problem::Unresolved => "SD0011",
74        }
75    }
76
77    /// The problem a code names, if it's one of this crate's.
78    #[must_use]
79    pub fn of_code(code: &str) -> Option<Problem> {
80        Problem::ALL.into_iter().find(|p| p.code() == code)
81    }
82}
83
84impl fmt::Display for Problem {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(self.code())
87    }
88}
89
90/// A diagnostic's kind: one of this crate's [`Problem`]s, or a tool's own
91/// code (`E0308`, a lint's name), kept as an [`Atom`]. Written as its code
92/// in every form.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum Code {
95    /// One of this crate's problems (`SD0001`).
96    Data(Problem),
97    /// A tool's own code (`E0308`).
98    Tool(Atom),
99}
100
101impl Code {
102    /// A tool's own code; one of this crate's codes is that [`Problem`].
103    pub fn tool(code: Atom) -> Code {
104        Problem::of_code(code.as_str()).map_or(Code::Tool(code), Code::Data)
105    }
106
107    /// As written: `SD0001`, `E0308`.
108    #[must_use]
109    pub fn as_str(self) -> &'static str {
110        match self {
111            Code::Data(p) => p.code(),
112            Code::Tool(a) => a.as_str(),
113        }
114    }
115
116    /// As an atom: what the code is written as.
117    fn atom(self) -> Atom {
118        match self {
119            Code::Data(p) => Atom::new(p.code()),
120            Code::Tool(a) => a,
121        }
122    }
123}
124
125impl From<Problem> for Code {
126    fn from(p: Problem) -> Code {
127        Code::Data(p)
128    }
129}
130
131impl From<Atom> for Code {
132    fn from(a: Atom) -> Code {
133        Code::tool(a)
134    }
135}
136
137impl fmt::Display for Code {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.write_str(self.as_str())
140    }
141}
142
143impl PartialEq<Problem> for Code {
144    fn eq(&self, p: &Problem) -> bool {
145        *self == Code::Data(*p)
146    }
147}
148
149/// Written as its code; packed in a frame as an [`Atom`] (its number).
150impl Schema for Code {
151    fn shape() -> crate::Shape {
152        Atom::shape()
153    }
154    fn read(v: &crate::ValueRef<'_>, cx: &mut crate::Reader<'_>) -> Option<Self> {
155        Atom::read(v, cx).map(Code::tool)
156    }
157    fn write(&self) -> crate::Value {
158        self.atom().write()
159    }
160    fn encode(&self, w: &mut crate::binary::Writer) {
161        self.atom().encode(w);
162    }
163    fn decode(d: &mut crate::binary::Decoder<'_>) -> Option<Self> {
164        Atom::decode(d).map(Code::tool)
165    }
166    fn pack(&self, p: &mut crate::frame::Packer) {
167        self.atom().pack(p);
168    }
169    fn unpack(u: &mut crate::frame::Unpacker<'_>) -> Option<Self> {
170        Atom::unpack(u).map(Code::tool)
171    }
172}
173
174/// How bad it is.
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Schema)]
176pub enum Severity {
177    /// It's wrong: the document isn't read.
178    #[default]
179    Error,
180    /// It may be wrong.
181    Warning,
182    /// Something worth knowing.
183    Note,
184}
185
186/// One thing found.
187#[derive(Debug, Clone, PartialEq, Eq, Default, Schema)]
188#[data(rename_all = "camelCase")]
189pub struct Diagnostic {
190    /// How bad it is.
191    pub severity: Severity,
192    /// What it is: one of this crate's [`Problem`]s (`SD0001`) or a
193    /// tool's own code (`E0308`, a lint's name); none until it's known.
194    pub code: Option<Code>,
195    /// What's wrong, for a person.
196    pub message: String,
197    /// The file it's in.
198    pub file: Option<Atom>,
199    /// The value it's about, in its document (`entries[2].icon`).
200    #[data(default)]
201    pub path: Path,
202    /// About the key at `path` rather than its value (an unknown field).
203    #[data(default)]
204    pub on_key: bool,
205    /// 1-based.
206    pub line: Option<u32>,
207    /// Its 1-based column, when known.
208    pub col: Option<u32>,
209    /// The line it ends on, when known.
210    pub end_line: Option<u32>,
211    /// The column it ends at, when known.
212    pub end_col: Option<u32>,
213    /// Who found it, when not the tool itself (`rustc`, `cargo`).
214    pub tool: Option<Atom>,
215}
216
217impl Diagnostic {
218    /// An error of kind `code`.
219    pub fn error(code: impl Into<Code>, message: impl fmt::Display) -> Self {
220        Self::of(Severity::Error, code, message)
221    }
222
223    /// A warning of kind `code`.
224    pub fn warning(code: impl Into<Code>, message: impl fmt::Display) -> Self {
225        Self::of(Severity::Warning, code, message)
226    }
227
228    /// A diagnostic of `severity`, `code` and `message`, not yet placed.
229    pub fn of(severity: Severity, code: impl Into<Code>, message: impl fmt::Display) -> Self {
230        Diagnostic {
231            severity,
232            code: Some(code.into()),
233            message: message.to_string(),
234            ..Self::default()
235        }
236    }
237
238    /// Text that doesn't parse, where it stops.
239    #[must_use]
240    pub fn syntax(e: SyntaxError) -> Self {
241        Self::error(Problem::Syntax, e.message).at(e.at)
242    }
243
244    /// In `file`.
245    #[must_use]
246    pub fn in_file(mut self, file: impl fmt::Display) -> Self {
247        self.file = Some(Atom::new(&file.to_string()));
248        self
249    }
250
251    /// At `line` of its file.
252    #[must_use]
253    pub fn at_line(mut self, line: usize) -> Self {
254        self.line = u32::try_from(line).ok();
255        self
256    }
257
258    /// At `at` (a line and column) of its file.
259    #[must_use]
260    pub fn at(mut self, at: LineCol) -> Self {
261        self.line = Some(at.line);
262        self.col = Some(at.col);
263        self
264    }
265
266    /// About the value at `path`.
267    #[must_use]
268    pub fn at_path(mut self, path: Path) -> Self {
269        self.path = path;
270        self
271    }
272
273    /// Found by `tool`.
274    #[must_use]
275    pub fn by(mut self, tool: Atom) -> Self {
276        self.tool = Some(tool);
277        self
278    }
279
280    /// Of kind `code` when its kind isn't known yet (it was met as plain text).
281    #[must_use]
282    pub fn or_code(mut self, code: impl Into<Code>) -> Self {
283        self.code.get_or_insert(code.into());
284        self
285    }
286
287    /// Its message `wrap`ped (`<Slot> {e}`), the rest kept.
288    #[must_use]
289    pub fn map(mut self, wrap: impl FnOnce(String) -> String) -> Self {
290        self.message = wrap(self.message);
291        self
292    }
293
294    /// Whether it's an error.
295    #[must_use]
296    pub fn is_error(&self) -> bool {
297        self.severity == Severity::Error
298    }
299}
300
301/// As text: `file:line:col: path: message`, each part when it's known.
302impl fmt::Display for Diagnostic {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        match (&self.file, self.line, self.col) {
305            (Some(file), Some(line), Some(col)) => write!(f, "{file}:{line}:{col}: ")?,
306            (Some(file), Some(line), None) => write!(f, "{file}:{line}: ")?,
307            (Some(file), None, _) => write!(f, "{file}: ")?,
308            (None, Some(line), Some(col)) => write!(f, "{line}:{col}: ")?,
309            (None, Some(line), None) => write!(f, "line {line}: ")?,
310            (None, None, _) => {}
311        }
312        if !self.path.is_root() {
313            write!(f, "{}: ", self.path)?;
314        }
315        f.write_str(&self.message)
316    }
317}
318
319impl std::error::Error for Diagnostic {}
320
321/// An error met as plain text: its kind not known yet ([`Diagnostic::or_code`]).
322impl From<String> for Diagnostic {
323    fn from(message: String) -> Self {
324        Diagnostic {
325            message,
326            ..Self::default()
327        }
328    }
329}
330
331impl From<&str> for Diagnostic {
332    fn from(message: &str) -> Self {
333        message.to_owned().into()
334    }
335}
336
337/// As text, for what says errors as text.
338impl From<Diagnostic> for String {
339    fn from(d: Diagnostic) -> Self {
340        d.to_string()
341    }
342}
343
344/// Every problem found reading a document, in its order.
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct Errors(pub Vec<Diagnostic>);
347
348impl fmt::Display for Errors {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        for (i, e) in self.0.iter().enumerate() {
351            if i > 0 {
352                f.write_str("\n")?;
353            }
354            write!(f, "{e}")?;
355        }
356        Ok(())
357    }
358}
359
360impl std::error::Error for Errors {}
361
362impl From<SyntaxError> for Errors {
363    fn from(e: SyntaxError) -> Self {
364        Errors(vec![Diagnostic::syntax(e)])
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn said_with_what_is_known_and_a_frame_reads_back() {
374        let d = Diagnostic::error(Atom::new("XY0002"), "no owner")
375            .in_file("docs/intro.doc")
376            .at_line(3)
377            .at_path(Path::root().key("owner"));
378        assert_eq!(d.to_string(), "docs/intro.doc:3: owner: no owner");
379        let mut bytes = Vec::new();
380        crate::io::write_frame(&mut bytes, &d).unwrap();
381        let back: Diagnostic = crate::io::read_frame(&mut bytes.as_slice())
382            .unwrap()
383            .unwrap();
384        assert_eq!(back, d);
385        assert_eq!(
386            Diagnostic::from("bad")
387                .or_code(Atom::new("XY0004"))
388                .code
389                .map(Code::as_str),
390            Some("XY0004")
391        );
392        // A crate code read back is its problem, typed.
393        let missing = Diagnostic::error(Problem::MissingField, "missing `id`");
394        let mut framed = Vec::new();
395        crate::io::write_frame(&mut framed, &missing).unwrap();
396        let read_back: Diagnostic = crate::io::read_frame(&mut framed.as_slice())
397            .unwrap()
398            .unwrap();
399        assert_eq!(read_back.code, Some(Code::Data(Problem::MissingField)));
400        assert_eq!(Code::tool(Atom::new("SD0006")), Problem::MissingField);
401        assert!(
402            Problem::ALL
403                .iter()
404                .all(|p| Problem::of_code(p.code()) == Some(*p))
405        );
406    }
407}