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 {
35 match &self {
36 Self::Error => RED,
37 Self::Warning => YELLOW,
38 }
39 }
40
41 pub(crate) fn word(self) -> &'static str {
43 match &self {
44 Self::Error => "error",
45 Self::Warning => "warning",
46 }
47 }
48}
49
50#[derive(Debug)]
51pub struct Diagnostics {
53 pub problems: Vec<Diagnostic>,
55 pub context: String,
57}
58impl Diagnostics {
59 pub fn new<S: ToString>(context: S) -> Self {
61 Self {
62 problems: vec![],
63 context: context.to_string(),
64 }
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.problems.is_empty()
70 }
71
72 pub fn push(&mut self, diagnostic: Diagnostic) {
74 self.problems.push(diagnostic);
75 }
76
77 pub fn errors(&self) -> impl Iterator<Item = &Diagnostic> {
79 self.problems
80 .iter()
81 .filter(|problem| problem.severity == Severity::Error)
82 }
83
84 pub fn warnings(&self) -> impl Iterator<Item = &Diagnostic> {
86 self.problems
87 .iter()
88 .filter(|problem| problem.severity == Severity::Warning)
89 }
90}
91impl core::fmt::Display for Diagnostics {
92 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93 let warnings: Vec<_> = self.warnings().collect();
94 let errors: Vec<_> = self.errors().collect();
95
96 for error in &errors {
97 writeln!(f, "{error}")?;
98 }
99 for warning in &warnings {
100 writeln!(f, "{warning}")?;
101 }
102
103 if !errors.is_empty() {
104 writeln!(
105 f,
106 "{}",
107 format_error!("{} generated {} errors", self.context, errors.len())
108 )?;
109 }
110 if !warnings.is_empty() {
111 writeln!(
112 f,
113 "{}",
114 format_warning!("{} generated {} warnings", self.context, warnings.len())
115 )?;
116 }
117
118 Ok(())
119 }
120}
121impl core::error::Error for Diagnostics {}
122
123#[derive(Debug)]
124pub struct Diagnostic {
126 pub severity: Severity,
128 pub headline: String,
130 pub file_path: Option<String>,
132 pub context: Option<Context>,
134 pub notes: Vec<String>,
136}
137
138impl Diagnostic {
139 pub fn new<S: ToString>(severity: Severity, headling: S) -> Self {
141 Self {
142 severity,
143 headline: headling.to_string(),
144 file_path: None,
145 context: None,
146 notes: Vec::new(),
147 }
148 }
149
150 pub fn error<S: ToString>(headling: S) -> Self {
152 Self {
153 severity: Severity::Error,
154 headline: headling.to_string(),
155 file_path: None,
156 context: None,
157 notes: Vec::new(),
158 }
159 }
160
161 pub fn warning<S: ToString>(headling: S) -> Self {
163 Self {
164 severity: Severity::Warning,
165 headline: headling.to_string(),
166 file_path: None,
167 context: None,
168 notes: Vec::new(),
169 }
170 }
171
172 pub fn file_path<S: ToString>(mut self, path: S) -> Self {
174 self.file_path = Some(path.to_string());
175 self
176 }
177
178 pub fn add_note<S: ToString>(mut self, note: S) -> Self {
180 self.notes.push(note.to_string());
181 self
182 }
183
184 pub fn context(mut self, context: Context) -> Self {
186 self.context = Some(context);
187 self
188 }
189}
190
191impl core::fmt::Display for Diagnostic {
192 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
193 let colour = self.severity.colour();
194 let severity = self.severity.word();
195
196 writeln!(
199 f,
200 "{BOLD}{colour}{severity}{DEFAULT}: {}{RESET}",
201 self.headline
202 )?;
203
204 let line_number_size = self
205 .context
206 .as_ref()
207 .map_or(1, |context| context.span.line.to_string().len());
208 let indent = " ".repeat(line_number_size);
209
210 if let Some(file_path) = &self.file_path {
213 write!(f, "{indent}{CYAN}{BOLD}-->{RESET} {file_path}",)?;
214
215 if let Some(context) = &self.context {
217 write!(f, ":{}:{}", context.span.line, context.span.column)?;
218 }
219 f.write_char('\n')?;
220 }
221 else if let Some(context) = &self.context {
224 writeln!(
225 f,
226 "{indent}{CYAN}{BOLD}-->{RESET} line {}, column {}",
227 context.span.line, context.span.column
228 )?;
229 }
230 writeln!(f, "{indent}{CYAN}{BOLD} | {RESET}")?;
232
233 if let Some(context) = &self.context {
235 for (index, line) in context.context.iter().enumerate() {
240 let line_number = (context.span.line.saturating_sub(
241 context
242 .context
243 .len()
244 .saturating_sub(index)
245 .saturating_sub(1),
246 ))
247 .to_string();
248 let padding = " ".repeat(line_number_size - line_number.len());
249 writeln!(f, "{CYAN}{BOLD}{line_number}{padding} | {RESET}{line}",)?;
250 }
251
252 write!(
255 f,
256 "{indent}{CYAN}{BOLD} | {RESET}{}{colour}{BOLD}{}",
257 " ".repeat(context.span_indent),
258 "^".repeat(context.span.length)
259 )?;
260 if let Some(label) = &context.label {
262 f.write_char(' ')?;
263 f.write_str(label)?;
264 }
265 writeln!(f, "{RESET}")?;
266 }
267
268 if !self.notes.is_empty() {
270 writeln!(f, "{indent}{CYAN}{BOLD} | {RESET}")?;
271 for note in &self.notes {
272 writeln!(f, "{indent}{CYAN}{BOLD} = {DEFAULT}note{RESET}: {note}")?;
273 }
274 }
275
276 Ok(())
277 }
278}
279
280impl core::error::Error for Diagnostic {}
281
282#[cfg(test)]
283mod test {
284 extern crate std;
285
286 use std::io::{Write, stderr, stdout};
287
288 use alloc::string::ToString;
289
290 use crate::diagnostic::{Context, Diagnostic, Diagnostics, Span};
291
292 const SOURCE: &str = r#"use alloc::boxed::Box;
293use core::{error::Error, fmt};
294
295use ts_ansi::style::{BOLD, DEFAULT, RED, RESET};
296
297/// An error report, displays the error stack of some error.
298pub struct Report<'e> {
299 /// The error for this report.
300 pub source: Box<dyn Error + 'e>,
301}
302impl<'e> Report<'e> {
303 /// Create a new error report.
304 pub fn new<E: Error + 'e>(source: E) -> Self {
305 Self {
306 source: Box::new(source),
307 }
308 }
309}
310impl Error for Report<'static> {
311 fn source(&self) -> Option<&(dyn Error + 'static)> {
312 Some(self.source.as_ref())
313 }
314}
315impl fmt::Debug for Report<'_> {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 write!(f, "{self}")
318 }
319}
320impl fmt::Display for Report<'_> {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 let mut current_error = Some(self.source.as_ref());
323 let mut count = 1;
324
325 while let Some(error) = current_error {
326 writeln!(f, " {BOLD}{RED}{count}{DEFAULT}.{RESET} {error}")?;
327
328 count += 1;
329 current_error = error.source();
330 }
331
332 Ok(())
333 }
334}"#;
335
336 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}};"#;
337
338 #[test]
339 fn show_output() {
340 let _stdout = stdout().lock();
341 let mut stderr = stderr().lock();
342
343 let warning = Diagnostic::warning("struct `Report` is never used")
344 .file_path("crates/ts-error/src/report.rs")
345 .context(Context::new(
346 SOURCE,
347 Span::default().line(7).column(12).length(6),
348 ))
349 .add_note("`#[warn(dead_code)]` on by default");
350
351 let error = Diagnostic::error("struct `Report` is never used")
352 .context(
353 Context::new(SOURCE, Span::default().line(7).column(12).length(6))
354 .label("this is unused"),
355 )
356 .add_note("`#[warn(dead_code)]` on by default");
357
358 let minified_error = Diagnostic::error("some headline here")
359 .context(
360 Context::new(
361 MINIFIED_SOURCE,
362 Span::default().line(1).column(200).length(50),
363 )
364 .label("some label here"),
365 )
366 .add_note("some note here")
367 .add_note("this code is trimmed");
368
369 stderr
370 .write_all(error.to_string().as_bytes())
371 .expect("writing to stderr should not fail");
372 stderr
373 .write_all(b"\n")
374 .expect("writing to stderr should not fail");
375 stderr
376 .write_all(minified_error.to_string().as_bytes())
377 .expect("writing to stderr should not fail");
378 stderr
379 .write_all(b"\n")
380 .expect("writing to stderr should not fail");
381 stderr
382 .write_all(warning.to_string().as_bytes())
383 .expect("writing to stderr should not fail");
384
385 stderr
386 .write_all(b"\n-----\n")
387 .expect("writing to stderr should not fail");
388
389 let mut diagnostics = Diagnostics::new("test");
390 diagnostics.push(warning);
391 diagnostics.push(minified_error);
392 diagnostics.push(error);
393 stderr
394 .write_all(diagnostics.to_string().as_bytes())
395 .expect("writing to stderr should not fail");
396
397 stderr.flush().expect("flusing stderr should not fail");
398 }
399}