ts_error/diagnostic/
context.rs

1//! Context for a diagnostic.
2
3use crate::diagnostic::Span;
4
5use alloc::{
6    string::{String, ToString},
7    vec::Vec,
8};
9
10#[derive(Debug, Clone)]
11/// Context for a diagnostic.
12pub struct Context {
13    /// The context for the diagnostic, sequential lines of the source where the last string is the
14    /// relevant line for the diagnostic. Each line is at most 100 characters wide
15    pub context: Vec<String>,
16    /// The span of the context relevant to the diagnostic.
17    pub span: Span,
18    /// The label for the span.
19    pub label: Option<String>,
20    /// How indented into the context the span starts.
21    pub span_indent: usize,
22}
23impl Context {
24    /// Create the context for a diagnostic from a span and the source file.
25    pub fn new(source: &str, span: Span) -> Self {
26        const MAX_LENGTH: usize = 100;
27
28        let context_end = span.column.saturating_sub(1) + span.length.min(MAX_LENGTH);
29        let context_start = span.column.saturating_sub(1);
30
31        let span_start = context_start
32            .saturating_sub(MAX_LENGTH.saturating_sub(context_end.saturating_sub(context_start)));
33        let span_end = span_start + MAX_LENGTH;
34
35        let mut context = Vec::with_capacity(3);
36        let lines: Vec<&str> = source.lines().collect();
37        for i in (1..4).rev() {
38            if let Some(index) = span.line.checked_sub(i)
39                && let Some(line) = lines.get(index)
40            {
41                let line_context = line
42                    .get(span_start..span_end.min(line.len()))
43                    .unwrap_or_default();
44                context.push(line_context.to_string());
45            }
46        }
47
48        let span_indent = context_start.saturating_sub(span_start);
49
50        Self {
51            context,
52            span,
53            label: None,
54            span_indent,
55        }
56    }
57
58    /// Sets the label of the context.
59    pub fn label<S: ToString>(mut self, label: S) -> Self {
60        self.label = Some(label.to_string());
61        self
62    }
63}
64
65#[cfg(test)]
66mod test {
67    use alloc::{string::String, vec, vec::Vec};
68
69    use crate::diagnostic::{Context, Span};
70
71    const SOURCE: &str = r#"use alloc::boxed::Box;
72use core::{error::Error, fmt};
73
74use ts_ansi::style::{BOLD, DEFAULT, RED, RESET};
75
76/// An error report, displays the error stack of some error.
77pub struct Report<'e> {
78    /// The error for this report.
79    pub source: Box<dyn Error + 'e>,
80}
81impl<'e> Report<'e> {
82    /// Create a new error report.
83    pub fn new<E: Error + 'e>(source: E) -> Self {
84        Self {
85            source: Box::new(source),
86        }
87    }
88}
89impl Error for Report<'static> {
90    fn source(&self) -> Option<&(dyn Error + 'static)> {
91        Some(self.source.as_ref())
92    }
93}
94impl fmt::Debug for Report<'_> {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(f, "{self}")
97    }
98}
99impl fmt::Display for Report<'_> {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        let mut current_error = Some(self.source.as_ref());
102        let mut count = 1;
103
104        while let Some(error) = current_error {
105            writeln!(f, " {BOLD}{RED}{count}{DEFAULT}.{RESET} {error}")?;
106
107            count += 1;
108            current_error = error.source();
109        }
110
111        Ok(())
112    }
113}"#;
114
115    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}};"#;
116
117    #[test]
118    fn handles_context() {
119        let span = Span::default().line(7).column(12).length(6);
120        let context = Context::new(SOURCE, span);
121        assert_eq!(
122            vec![
123                r#""#,
124                r#"/// An error report, displays the error stack of some error."#,
125                r#"pub struct Report<'e> {"#
126            ],
127            context.context
128        );
129
130        let span = Span::default().line(36);
131        let context = Context::new(SOURCE, span);
132        assert_eq!(
133            vec![
134                r#"        while let Some(error) = current_error {"#,
135                r#"            writeln!(f, " {BOLD}{RED}{count}{DEFAULT}.{RESET} {error}")?;"#,
136                r#""#
137            ],
138            context.context
139        );
140
141        let span = Span::default().line(999);
142        let context = Context::new(SOURCE, span);
143        assert_eq!(Vec::<String>::new(), context.context);
144
145        let span = Span::default().line(35).column(999).length(999);
146        let context = Context::new(SOURCE, span);
147        assert_eq!(vec![r#""#, r#""#, r#""#], context.context);
148
149        let span = Span::default().line(1).column(200).length(50);
150        let context = Context::new(MINIFIED_SOURCE, span);
151        assert_eq!(
152            vec![
153                r#"ontents;action;constructor(t,e){this.element=ht(`${t}/error`,HTMLElement),this.contents=ht(`${t}/err"#
154            ],
155            context.context
156        );
157    }
158}