1mod context;
6mod span;
7
8use alloc::{
9 string::{String, ToString},
10 vec,
11 vec::Vec,
12};
13use core::fmt::Write;
14
15use ts_ansi::{
16 format_error, format_warning,
17 style::{BOLD, CYAN, DEFAULT, RED, RESET, YELLOW},
18};
19
20pub use context::Context;
21pub use span::Span;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum Severity {
27 Error,
29 Warning,
31}
32impl Severity {
33 pub(crate) fn colour(self) -> &'static str {
34 match &self {
35 Self::Error => RED,
36 Self::Warning => YELLOW,
37 }
38 }
39
40 pub(crate) fn word(self) -> &'static str {
41 match &self {
42 Self::Error => "error",
43 Self::Warning => "warning",
44 }
45 }
46}
47
48#[derive(Debug)]
49pub struct Diagnostics {
51 pub problems: Vec<Diagnostic>,
53 pub context: String,
55}
56impl Diagnostics {
57 pub fn new<S: ToString>(context: S) -> Self {
59 Self {
60 problems: vec![],
61 context: context.to_string(),
62 }
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.problems.is_empty()
68 }
69
70 pub fn push(&mut self, diagnostic: Diagnostic) {
72 self.problems.push(diagnostic);
73 }
74
75 pub fn errors(&self) -> impl Iterator<Item = &Diagnostic> {
77 self.problems
78 .iter()
79 .filter(|problem| problem.severity == Severity::Error)
80 }
81
82 pub fn warnings(&self) -> impl Iterator<Item = &Diagnostic> {
84 self.problems
85 .iter()
86 .filter(|problem| problem.severity == Severity::Warning)
87 }
88}
89impl core::fmt::Display for Diagnostics {
90 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91 let warnings: Vec<_> = self.warnings().collect();
92 let errors: Vec<_> = self.errors().collect();
93
94 for error in &errors {
95 writeln!(f, "{error}")?;
96 }
97 for warning in &warnings {
98 writeln!(f, "{warning}")?;
99 }
100
101 if !errors.is_empty() {
102 writeln!(
103 f,
104 "{}",
105 format_error!("{} generated {} errors", self.context, errors.len())
106 )?;
107 }
108 if !warnings.is_empty() {
109 writeln!(
110 f,
111 "{}",
112 format_warning!("{} generated {} warnings", self.context, warnings.len())
113 )?;
114 }
115
116 Ok(())
117 }
118}
119impl core::error::Error for Diagnostics {}
120
121#[derive(Debug)]
122pub struct Diagnostic {
124 pub severity: Severity,
126 pub headline: String,
128 pub file_path: Option<String>,
130 pub context: Option<Context>,
132 pub notes: Vec<String>,
134}
135
136impl Diagnostic {
137 pub fn new<S: ToString>(severity: Severity, headling: S) -> Self {
139 Self {
140 severity,
141 headline: headling.to_string(),
142 file_path: None,
143 context: None,
144 notes: Vec::new(),
145 }
146 }
147
148 pub fn error<S: ToString>(headling: S) -> Self {
150 Self {
151 severity: Severity::Error,
152 headline: headling.to_string(),
153 file_path: None,
154 context: None,
155 notes: Vec::new(),
156 }
157 }
158
159 pub fn warning<S: ToString>(headling: S) -> Self {
161 Self {
162 severity: Severity::Warning,
163 headline: headling.to_string(),
164 file_path: None,
165 context: None,
166 notes: Vec::new(),
167 }
168 }
169
170 pub fn file_path<S: ToString>(mut self, path: S) -> Self {
172 self.file_path = Some(path.to_string());
173 self
174 }
175
176 pub fn add_note<S: ToString>(mut self, note: S) -> Self {
178 self.notes.push(note.to_string());
179 self
180 }
181
182 pub fn context(mut self, context: Context) -> Self {
184 self.context = Some(context);
185 self
186 }
187}
188
189impl core::fmt::Display for Diagnostic {
190 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
191 let colour = self.severity.colour();
192 let severity = self.severity.word();
193
194 writeln!(
197 f,
198 "{BOLD}{colour}{severity}{DEFAULT}: {}{RESET}",
199 self.headline
200 )?;
201
202 let line_number_size = self
203 .context
204 .as_ref()
205 .map_or(1, |context| context.span.line.to_string().len());
206 let indent = " ".repeat(line_number_size);
207
208 if let Some(file_path) = &self.file_path {
211 write!(f, "{indent}{CYAN}{BOLD}-->{RESET} {file_path}",)?;
212
213 if let Some(context) = &self.context {
215 write!(f, ":{}:{}", context.span.line, context.span.column)?;
216 }
217 f.write_char('\n')?;
218 }
219 else if let Some(context) = &self.context {
222 writeln!(
223 f,
224 "{indent}{CYAN}{BOLD}-->{RESET} line {}, column {}",
225 context.span.line, context.span.column
226 )?;
227 }
228 writeln!(f, "{indent}{CYAN}{BOLD} | {RESET}")?;
230
231 if let Some(context) = &self.context {
233 for (index, line) in context.context.iter().enumerate() {
238 let line_number = (context.span.line.saturating_sub(
239 context
240 .context
241 .len()
242 .saturating_sub(index)
243 .saturating_sub(1),
244 ))
245 .to_string();
246 let padding = " ".repeat(line_number_size - line_number.len());
247 writeln!(f, "{CYAN}{BOLD}{line_number}{padding} | {RESET}{line}",)?;
248 }
249
250 write!(
253 f,
254 "{indent}{CYAN}{BOLD} | {RESET}{}{colour}{BOLD}{}",
255 " ".repeat(context.span_indent),
256 "^".repeat(context.span.length)
257 )?;
258 if let Some(label) = &context.label {
260 f.write_char(' ')?;
261 f.write_str(label)?;
262 }
263 writeln!(f, "{RESET}")?;
264 }
265
266 if !self.notes.is_empty() {
268 writeln!(f, "{indent}{CYAN}{BOLD} | {RESET}")?;
269 for note in &self.notes {
270 writeln!(f, "{indent}{CYAN}{BOLD} = {DEFAULT}note{RESET}: {note}")?;
271 }
272 }
273
274 Ok(())
275 }
276}
277
278impl core::error::Error for Diagnostic {}
279
280#[cfg(test)]
281mod test {
282 extern crate std;
283
284 use std::io::{Write, stderr, stdout};
285
286 use alloc::string::ToString;
287
288 use crate::diagnostic::{Context, Diagnostic, Diagnostics, Span};
289
290 const SOURCE: &str = r#"use alloc::boxed::Box;
291use core::{error::Error, fmt};
292
293use ts_ansi::style::{BOLD, DEFAULT, RED, RESET};
294
295/// An error report, displays the error stack of some error.
296pub struct Report<'e> {
297 /// The error for this report.
298 pub source: Box<dyn Error + 'e>,
299}
300impl<'e> Report<'e> {
301 /// Create a new error report.
302 pub fn new<E: Error + 'e>(source: E) -> Self {
303 Self {
304 source: Box::new(source),
305 }
306 }
307}
308impl Error for Report<'static> {
309 fn source(&self) -> Option<&(dyn Error + 'static)> {
310 Some(self.source.as_ref())
311 }
312}
313impl fmt::Debug for Report<'_> {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 write!(f, "{self}")
316 }
317}
318impl fmt::Display for Report<'_> {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 let mut current_error = Some(self.source.as_ref());
321 let mut count = 1;
322
323 while let Some(error) = current_error {
324 writeln!(f, " {BOLD}{RED}{count}{DEFAULT}.{RESET} {error}")?;
325
326 count += 1;
327 current_error = error.source();
328 }
329
330 Ok(())
331 }
332}"#;
333
334 const MINIFIED_SOURCE: &str = r#"async function Ui(n){return location.href=n,await mu()}function mu(){let n=t=>{setTimeout(()=>n(t),400)};return new Promise(n)}var br=class{element;contents;action;constructor(t,e){this.element=ht(`${t}/error`,HTMLElement),this.contents=ht(`${t}/error/content`,HTMLElement),this.action=e}clearError(){this.element.classList.add("collapse"),this.element.ariaHidden="true",this.contents.textContent=""}addError(t){if(this.contents.textContent===""){this.element.classList.remove("collapse"),this.element.ariaHidden="false",this.contents.textContent=`Could not ${this.action}: ${t}`;return}this.contents.textContent+=`, ${t}`}setSomethingWentWrong(){this.element.classList.remove("collapse"),this.element.ariaHidden="false",this.contents.textContent=`Something went wrong while trying to ${this.action}. Try again later.`}},Nr=class{input;error;constructor(t,e){this.input=ht(`${t}${e}/input`,HTMLInputElement),this.error=ht(`${t}${e}/error`,HTMLElement),this.input.addEventListener("input",()=>{this.input.setCustomValidity("")})}getValue(){return this.input.type==="checkbox"?this.input.checked?"checked":"unchecked":this.input.value}setLock(t){this.input.disabled=t}clearError(){this.input.setCustomValidity(""),this.error.classList.add("hidden"),this.error.ariaHidden="true",this.error.textContent="!"}addError(t){if(this.error.textContent==="!"){this.input.setCustomValidity(t),this.error.classList.remove("hidden"),this.error.ariaHidden="false",this.error.textContent=`Invalid value: ${t}`;return}this.error.textContent+=`, ${t}`,this.input.setCustomValidity(this.error.textContent??"Invalid value")}},ge=class{form;formError;submitButton;inputs;constructor(t,e,r){this.form=ht(t,HTMLFormElement),this.formError=new br(t,r),this.submitButton=ht(`${t}/submit`,HTMLButtonElement);let o=new Map;for(let i of e)o.set(i,new Nr(t,i));this.inputs=o}clearErrors(){this.formError.clearError();for(let t of this.inputs.values())t.clearError()}setLock(t){this.submitButton.disabled=t;for(let e of this.inputs.values())e.setLock(t)}setInputErrors(t){if(!t||t.length===0){this.formError.addError("an unknown field is invalid");return}for(let e of t){let r=this.inputs.get(e.pointer)??null;r?r.addError(e.detail):this.formError.addError(`field ${e.pointer} ${e.detail}`)}}getValues(){let t=new Map;for(let[e,r]of this.inputs)t.set(e,r.getValue());return t}};"#;
335
336 #[test]
337 fn show_output() {
338 let _stdout = stdout().lock();
339 let mut stderr = stderr().lock();
340
341 let warning = Diagnostic::warning("struct `Report` is never used")
342 .file_path("crates/ts-error/src/report.rs")
343 .context(Context::new(
344 SOURCE,
345 Span::default().line(7).column(12).length(6),
346 ))
347 .add_note("`#[warn(dead_code)]` on by default");
348
349 let error = Diagnostic::error("struct `Report` is never used")
350 .context(
351 Context::new(SOURCE, Span::default().line(7).column(12).length(6))
352 .label("this is unused"),
353 )
354 .add_note("`#[warn(dead_code)]` on by default");
355
356 let minified_error = Diagnostic::error("some headline here")
357 .context(
358 Context::new(
359 MINIFIED_SOURCE,
360 Span::default().line(1).column(200).length(50),
361 )
362 .label("some label here"),
363 )
364 .add_note("some note here")
365 .add_note("this code is trimmed");
366
367 stderr
368 .write_all(error.to_string().as_bytes())
369 .expect("writing to stderr should not fail");
370 stderr
371 .write_all(b"\n")
372 .expect("writing to stderr should not fail");
373 stderr
374 .write_all(minified_error.to_string().as_bytes())
375 .expect("writing to stderr should not fail");
376 stderr
377 .write_all(b"\n")
378 .expect("writing to stderr should not fail");
379 stderr
380 .write_all(warning.to_string().as_bytes())
381 .expect("writing to stderr should not fail");
382
383 stderr
384 .write_all(b"\n-----\n")
385 .expect("writing to stderr should not fail");
386
387 let mut diagnostics = Diagnostics::new("test");
388 diagnostics.push(warning);
389 diagnostics.push(minified_error);
390 diagnostics.push(error);
391 stderr
392 .write_all(diagnostics.to_string().as_bytes())
393 .expect("writing to stderr should not fail");
394
395 stderr.flush().expect("flusing stderr should not fail");
396 }
397}