1use crate::{Code, Path, Span};
4
5#[non_exhaustive]
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub enum Severity {
9 Error,
11 Warning,
13}
14
15impl core::fmt::Display for Severity {
16 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
17 f.write_str(match self {
18 Self::Error => "error",
19 Self::Warning => "warning",
20 })
21 }
22}
23
24#[non_exhaustive]
26#[derive(Clone, Debug, PartialEq, Eq, Hash)]
27pub struct Label {
28 pub span: Span,
30 pub message: String,
32}
33
34impl Label {
35 #[must_use]
37 pub fn new(span: Span, message: impl Into<String>) -> Self {
38 Self {
39 span,
40 message: message.into(),
41 }
42 }
43}
44
45#[non_exhaustive]
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
48pub enum Applicability {
49 MachineApplicable,
52 MaybeIncorrect,
54}
55
56#[non_exhaustive]
59#[derive(Clone, Debug, PartialEq, Eq, Hash)]
60pub struct Replacement {
61 pub span: Span,
63 pub text: String,
65}
66
67impl Replacement {
68 #[must_use]
70 pub fn new(span: Span, text: impl Into<String>) -> Self {
71 Self {
72 span,
73 text: text.into(),
74 }
75 }
76}
77
78#[non_exhaustive]
82#[derive(Clone, Debug, PartialEq, Eq, Hash)]
83pub struct Suggestion {
84 pub message: String,
86 pub edits: Vec<Replacement>,
88 pub applicability: Applicability,
90}
91
92impl Suggestion {
93 #[must_use]
95 pub fn new(
96 message: impl Into<String>,
97 edits: Vec<Replacement>,
98 applicability: Applicability,
99 ) -> Self {
100 Self {
101 message: message.into(),
102 edits,
103 applicability,
104 }
105 }
106
107 #[must_use]
111 pub fn apply(&self, source: &str) -> Option<String> {
112 let mut out = String::with_capacity(source.len());
113 let mut at = 0usize;
114 for edit in &self.edits {
115 if edit.span.start < at || edit.span.end < edit.span.start {
116 return None;
117 }
118 out.push_str(source.get(at..edit.span.start)?);
119 source.get(edit.span.start..edit.span.end)?;
120 out.push_str(&edit.text);
121 at = edit.span.end;
122 }
123 out.push_str(source.get(at..)?);
124 Some(out)
125 }
126}
127
128#[non_exhaustive]
159#[derive(Clone, Debug, PartialEq, Eq, Hash)]
160pub struct Diagnostic {
161 pub code: Code,
163 pub severity: Severity,
165 pub message: String,
167 pub primary: Label,
169 pub secondary: Vec<Label>,
171 pub help: Option<String>,
173 pub path: Option<Path>,
175 pub suggestions: Vec<Suggestion>,
177}
178
179impl Diagnostic {
180 #[must_use]
182 pub fn new(code: Code, severity: Severity, message: impl Into<String>, primary: Label) -> Self {
183 Self {
184 code,
185 severity,
186 message: message.into(),
187 primary,
188 secondary: Vec::new(),
189 help: None,
190 path: None,
191 suggestions: Vec::new(),
192 }
193 }
194
195 #[must_use]
197 pub fn error(code: Code, message: impl Into<String>, primary: Label) -> Self {
198 Self::new(code, Severity::Error, message, primary)
199 }
200
201 #[must_use]
203 pub fn warning(code: Code, message: impl Into<String>, primary: Label) -> Self {
204 Self::new(code, Severity::Warning, message, primary)
205 }
206
207 #[must_use]
209 pub fn with_secondary(mut self, label: Label) -> Self {
210 self.secondary.push(label);
211 self
212 }
213
214 #[must_use]
216 pub fn with_help(mut self, help: impl Into<String>) -> Self {
217 self.help = Some(help.into());
218 self
219 }
220
221 #[must_use]
223 pub fn with_path(mut self, path: Path) -> Self {
224 self.path = Some(path);
225 self
226 }
227
228 #[must_use]
230 pub fn with_suggestion(mut self, suggestion: Suggestion) -> Self {
231 self.suggestions.push(suggestion);
232 self
233 }
234}
235
236impl core::fmt::Display for Diagnostic {
237 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
240 write!(f, "{}[{}]: {}", self.severity, self.code, self.message)?;
241 if let Some(path) = &self.path {
242 write!(f, " at {path}")?;
243 }
244 Ok(())
245 }
246}
247
248impl core::error::Error for Diagnostic {}
249
250#[cfg(test)]
251mod tests {
252 #![allow(clippy::unwrap_used)]
253
254 use super::*;
255
256 #[test]
257 fn builders_and_display() {
258 let code = Code::new(1003).unwrap();
259 let d = Diagnostic::warning(code, "ignored", Label::new(Span::new(0, 4), "here"))
260 .with_secondary(Label::new(Span::new(5, 6), "also"))
261 .with_help("remove it")
262 .with_path(Path::new().with_key("a").with_index(2));
263 assert_eq!(d.severity, Severity::Warning);
264 assert_eq!(d.secondary.len(), 1);
265 assert_eq!(d.help.as_deref(), Some("remove it"));
266 assert_eq!(d.to_string(), "warning[Y1003]: ignored at a[2]");
267
268 let e = Diagnostic::error(code, "bad", Label::new(Span::empty(0), ""));
269 assert_eq!(e.severity, Severity::Error);
270 assert_eq!(e.path, None);
271 assert_eq!(e.to_string(), "error[Y1003]: bad");
272 assert_eq!(
273 e,
274 Diagnostic::new(code, Severity::Error, "bad", Label::new(Span::empty(0), ""))
275 );
276 assert!(e.suggestions.is_empty());
277 }
278
279 #[test]
280 fn suggestions_apply() {
281 let src = "a: b: c\nd: é\n";
282 let quote = Suggestion::new(
283 "quote the value",
284 vec![Replacement::new(Span::new(3, 7), "'b: c'")],
285 Applicability::MachineApplicable,
286 );
287 assert_eq!(quote.apply(src).unwrap(), "a: 'b: c'\nd: é\n");
288 let multi = Suggestion::new(
289 "insert and delete",
290 vec![
291 Replacement::new(Span::empty(0), "# x\n"),
292 Replacement::new(Span::new(8, 13), ""),
293 ],
294 Applicability::MaybeIncorrect,
295 );
296 assert_eq!(multi.apply(src).unwrap(), "# x\na: b: c\n\n");
297 for bad in [
300 vec![
301 Replacement::new(Span::new(0, 4), ""),
302 Replacement::new(Span::new(3, 5), ""),
303 ],
304 vec![Replacement::new(Span::new(5, 4), "")],
305 vec![Replacement::new(Span::new(0, 99), "")],
306 vec![Replacement::new(Span::new(0, 12), "")],
307 ] {
308 let s = Suggestion::new("bad", bad, Applicability::MaybeIncorrect);
309 assert_eq!(s.apply(src), None);
310 }
311 let d = Diagnostic::error(Code::new(6).unwrap(), "x", Label::new(Span::empty(0), ""))
312 .with_suggestion(quote.clone());
313 assert_eq!(d.suggestions, vec![quote]);
314 }
315}