Skip to main content

yog_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 Yog tool says and every Yog tool reads.
5
6use std::fmt;
7
8use crate::{Atom, LineCol, Path, Schema, SyntaxError};
9
10/// How bad it is.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Schema)]
12pub enum Severity {
13    #[default]
14    Error,
15    Warning,
16    Note,
17}
18
19/// One thing found.
20#[derive(Debug, Clone, PartialEq, Eq, Default, Schema)]
21#[data(rename_all = "camelCase")]
22pub struct Diagnostic {
23    pub severity: Severity,
24    /// What it is: one of `yog_ids`' problem codes (`YD0001`, `YB0002`), or
25    /// the tool's own (`E0308`, a lint's name); none until it's known.
26    pub code: Option<Atom>,
27    pub message: String,
28    /// The file it's in.
29    pub file: Option<Atom>,
30    /// The value it's about, in its document (`entries[2].icon`).
31    #[data(default)]
32    pub path: Path,
33    /// About the key at `path` rather than its value (an unknown field).
34    #[data(default)]
35    pub on_key: bool,
36    /// 1-based.
37    pub line: Option<u32>,
38    pub col: Option<u32>,
39    pub end_line: Option<u32>,
40    pub end_col: Option<u32>,
41    /// Who found it, when not Yog itself (`rustc`, `cargo`).
42    pub tool: Option<Atom>,
43}
44
45impl Diagnostic {
46    /// An error of kind `code`.
47    pub fn error(code: &str, message: impl fmt::Display) -> Self {
48        Self::of(Severity::Error, code, message)
49    }
50
51    /// A warning of kind `code`.
52    pub fn warning(code: &str, message: impl fmt::Display) -> Self {
53        Self::of(Severity::Warning, code, message)
54    }
55
56    pub fn of(severity: Severity, code: &str, message: impl fmt::Display) -> Self {
57        Diagnostic {
58            severity,
59            code: Some(Atom::new(code)),
60            message: message.to_string(),
61            ..Self::default()
62        }
63    }
64
65    /// Text that doesn't parse, where it stops.
66    pub fn syntax(e: SyntaxError) -> Self {
67        Self::error(yog_ids::data_problem::SYNTAX, e.message).at(e.at)
68    }
69
70    /// In `file`.
71    #[must_use]
72    pub fn in_file(mut self, file: impl fmt::Display) -> Self {
73        self.file = Some(Atom::new(&file.to_string()));
74        self
75    }
76
77    /// At `line` of its file.
78    #[must_use]
79    pub fn at_line(mut self, line: usize) -> Self {
80        self.line = u32::try_from(line).ok();
81        self
82    }
83
84    /// At `at` (a line and column) of its file.
85    #[must_use]
86    pub fn at(mut self, at: LineCol) -> Self {
87        self.line = Some(at.line);
88        self.col = Some(at.col);
89        self
90    }
91
92    /// About the value at `path`.
93    #[must_use]
94    pub fn at_path(mut self, path: Path) -> Self {
95        self.path = path;
96        self
97    }
98
99    /// Found by `tool`.
100    #[must_use]
101    pub fn by(mut self, tool: &str) -> Self {
102        self.tool = Some(Atom::new(tool));
103        self
104    }
105
106    /// Of kind `code` when its kind isn't known yet (it was met as plain text).
107    #[must_use]
108    pub fn or_code(mut self, code: &str) -> Self {
109        self.code.get_or_insert_with(|| Atom::new(code));
110        self
111    }
112
113    /// Its message `wrap`ped (`<Slot> {e}`), the rest kept.
114    #[must_use]
115    pub fn map(mut self, wrap: impl FnOnce(String) -> String) -> Self {
116        self.message = wrap(self.message);
117        self
118    }
119
120    pub fn is_error(&self) -> bool {
121        self.severity == Severity::Error
122    }
123}
124
125/// As text: `file:line:col: path: message`, each part when it's known.
126impl fmt::Display for Diagnostic {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match (&self.file, self.line, self.col) {
129            (Some(file), Some(line), Some(col)) => write!(f, "{file}:{line}:{col}: ")?,
130            (Some(file), Some(line), None) => write!(f, "{file}:{line}: ")?,
131            (Some(file), None, _) => write!(f, "{file}: ")?,
132            (None, Some(line), Some(col)) => write!(f, "{line}:{col}: ")?,
133            (None, Some(line), None) => write!(f, "line {line}: ")?,
134            (None, None, _) => {}
135        }
136        if !self.path.is_root() {
137            write!(f, "{}: ", self.path)?;
138        }
139        f.write_str(&self.message)
140    }
141}
142
143impl std::error::Error for Diagnostic {}
144
145/// An error met as plain text: its kind not known yet ([`Diagnostic::or_code`]).
146impl From<String> for Diagnostic {
147    fn from(message: String) -> Self {
148        Diagnostic {
149            message,
150            ..Self::default()
151        }
152    }
153}
154
155impl From<&str> for Diagnostic {
156    fn from(message: &str) -> Self {
157        message.to_owned().into()
158    }
159}
160
161/// As text, for what says errors as text.
162impl From<Diagnostic> for String {
163    fn from(d: Diagnostic) -> Self {
164        d.to_string()
165    }
166}
167
168/// Every problem found reading a document, in its order.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct Errors(pub Vec<Diagnostic>);
171
172impl fmt::Display for Errors {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        for (i, e) in self.0.iter().enumerate() {
175            if i > 0 {
176                f.write_str("\n")?;
177            }
178            write!(f, "{e}")?;
179        }
180        Ok(())
181    }
182}
183
184impl std::error::Error for Errors {}
185
186impl From<SyntaxError> for Errors {
187    fn from(e: SyntaxError) -> Self {
188        Errors(vec![Diagnostic::syntax(e)])
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn said_with_what_is_known_and_a_frame_reads_back() {
198        let d = Diagnostic::error("YB0002", "no model")
199            .in_file("content/ruby.yblock")
200            .at_line(3)
201            .at_path(Path::root().key("model"));
202        assert_eq!(d.to_string(), "content/ruby.yblock:3: model: no model");
203        let mut bytes = Vec::new();
204        crate::io::write_frame(&mut bytes, &d).unwrap();
205        let back: Diagnostic = crate::io::read_frame(&mut bytes.as_slice())
206            .unwrap()
207            .unwrap();
208        assert_eq!(back, d);
209        assert_eq!(
210            Diagnostic::from("bad").or_code("YB0004").code.as_deref(),
211            Some("YB0004")
212        );
213    }
214}