1use thiserror::Error;
2
3pub enum JSResult {
4 String(String),
5}
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum JSErrorKind {
9 Execution,
10 Compile,
11 Runtime,
12 Internal,
13}
14
15#[derive(Error, Debug)]
16pub struct JSError {
17 kind: JSErrorKind,
18 msg: String,
19 filename: Option<String>,
20 snippet: Option<String>,
21}
22
23impl JSError {
24 pub fn new(msg: impl Into<String>) -> Self {
25 Self {
26 kind: JSErrorKind::Execution,
27 msg: msg.into(),
28 filename: None,
29 snippet: None,
30 }
31 }
32
33 pub fn compile(msg: impl Into<String>) -> Self {
34 Self::new(msg).with_kind(JSErrorKind::Compile)
35 }
36
37 pub fn runtime(msg: impl Into<String>) -> Self {
38 Self::new(msg).with_kind(JSErrorKind::Runtime)
39 }
40
41 pub fn internal(msg: impl Into<String>) -> Self {
42 Self::new(msg).with_kind(JSErrorKind::Internal)
43 }
44
45 pub fn with_kind(mut self, kind: JSErrorKind) -> Self {
46 self.kind = kind;
47 self
48 }
49
50 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
51 self.filename = Some(filename.into());
52 self
53 }
54
55 pub fn with_source(mut self, source: impl Into<String>) -> Self {
56 self.snippet = Some(source.into());
57 self
58 }
59
60 pub fn kind(&self) -> JSErrorKind {
61 self.kind
62 }
63
64 pub fn message(&self) -> &str {
65 &self.msg
66 }
67
68 pub fn filename(&self) -> Option<&str> {
69 self.filename.as_deref()
70 }
71
72 pub fn source_snippet(&self) -> Option<&str> {
73 self.snippet.as_deref()
74 }
75}
76
77impl std::fmt::Display for JSError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 let color = use_color();
80 let red = ansi(color, "1;31");
81 let yellow = ansi(color, "1;33");
82 let cyan = ansi(color, "1;36");
83 let dim = ansi(color, "2");
84 let reset = ansi(color, "0");
85
86 let kind = match self.kind {
87 JSErrorKind::Execution => "Execution Error",
88 JSErrorKind::Compile => "Compile Error",
89 JSErrorKind::Runtime => "Runtime Error",
90 JSErrorKind::Internal => "Internal Error",
91 };
92
93 write!(f, "{red}{kind}{reset}: {}", self.msg)?;
94
95 if let Some(filename) = &self.filename {
96 write!(f, "\n{cyan}--> {reset}{filename}")?;
97 }
98
99 if let Some(source) = self
100 .snippet
101 .as_deref()
102 .map(str::trim)
103 .filter(|s| !s.is_empty())
104 {
105 let snippet = shorten(source, 140);
106 write!(f, "\n{dim} |{reset} {yellow}{snippet}{reset}")?;
107 }
108
109 Ok(())
110 }
111}
112
113fn use_color() -> bool {
114 if std::env::var_os("NO_COLOR").is_some() {
115 return false;
116 }
117 !matches!(std::env::var("TERM").ok().as_deref(), Some("dumb"))
118}
119
120fn ansi(enabled: bool, code: &str) -> &'static str {
121 if !enabled {
122 return "";
123 }
124 match code {
125 "0" => "\x1b[0m",
126 "1;31" => "\x1b[1;31m",
127 "1;33" => "\x1b[1;33m",
128 "1;36" => "\x1b[1;36m",
129 "2" => "\x1b[2m",
130 _ => "",
131 }
132}
133
134fn shorten(input: &str, max_chars: usize) -> String {
135 let mut out = String::new();
136 for ch in input.chars().take(max_chars) {
137 if ch == '\n' || ch == '\r' {
138 out.push(' ');
139 } else {
140 out.push(ch);
141 }
142 }
143 if input.chars().count() > max_chars {
144 out.push_str("...");
145 }
146 out
147}