1use std::path::PathBuf;
4
5use deno_core::error::CoreErrorKind;
6use thiserror::Error;
7
8use crate::Module;
9
10#[allow(clippy::struct_excessive_bools)]
12#[derive(Debug, Clone)]
13pub struct ErrorFormattingOptions {
14 pub include_filename: bool,
17
18 pub include_line_number: bool,
21
22 pub include_column_number: bool,
25
26 pub hide_current_directory: bool,
28
29 pub current_directory: Option<PathBuf>,
31}
32impl Default for ErrorFormattingOptions {
33 fn default() -> Self {
34 Self {
35 include_filename: true,
36 include_line_number: true,
37 include_column_number: true,
38
39 hide_current_directory: false,
40 current_directory: None,
41 }
42 }
43}
44
45#[derive(Error, Debug, Clone, serde::Serialize, serde::Deserialize, deno_error::JsError)]
47pub enum Error {
48 #[class(generic)]
50 #[error("{0} has no entrypoint. Register one, or add a default to the runtime")]
51 MissingEntrypoint(Module),
52
53 #[class(generic)]
55 #[error("{0} could not be found in global, or module exports")]
56 ValueNotFound(String),
57
58 #[class(generic)]
60 #[error("{0} is not a function")]
61 ValueNotCallable(String),
62
63 #[class(generic)]
65 #[error("{0} could not be encoded as a v8 value")]
66 V8Encoding(String),
67
68 #[class(generic)]
70 #[error("value could not be deserialized: {0}")]
71 JsonDecode(String),
72
73 #[class(generic)]
75 #[error("{0}")]
76 ModuleNotFound(String),
77
78 #[class(generic)]
80 #[error("This worker has been destroyed")]
81 WorkerHasStopped,
82
83 #[class(generic)]
85 #[error("{0}")]
86 Runtime(String),
87
88 #[class(generic)]
90 #[error("{0}")]
91 JsError(Box<deno_core::error::JsError>),
92
93 #[class(generic)]
95 #[error("Module timed out: {0}")]
96 Timeout(String),
97
98 #[class(generic)]
100 #[error("Heap exhausted")]
101 HeapExhausted,
102}
103
104impl From<deno_core::error::JsError> for Error {
105 fn from(err: deno_core::error::JsError) -> Self {
106 Self::JsError(Box::new(err))
107 }
108}
109
110impl Error {
111 #[must_use]
122 pub fn as_highlighted(&self, options: ErrorFormattingOptions) -> String {
123 let mut e = if let Error::JsError(e) = self {
124 let (filename, row, col) = match e.frames.first() {
126 Some(f) => (
127 match &f.file_name {
128 Some(f) if f.is_empty() => None::<&str>,
129 Some(f) => Some(f.as_ref()),
130 None => None,
131 },
132 usize::try_from(f.line_number.unwrap_or(1)).unwrap_or_default(),
133 usize::try_from(f.column_number.unwrap_or(1)).unwrap_or_default(),
134 ),
135 None => (None, 1, 1),
136 };
137
138 let mut line = e.source_line.as_ref().map(|s| s.trim_end());
139 let col = col - 1;
140
141 let mut padding = String::new();
143 match line {
144 None => {}
145 Some(s) => {
146 let (start, end) = if s.len() < 50 {
147 (0, s.len())
148 } else if col < 25 {
149 (0, 50)
150 } else if col > s.len() - 25 {
151 (s.len() - 50, s.len())
152 } else {
153 (col - 25, col + 25)
154 };
155
156 line = Some(s.get(start..end).unwrap_or(s));
157 padding = " ".repeat(col - start);
158 }
159 }
160
161 let msg_lines = e.exception_message.split('\n').collect::<Vec<_>>();
162
163 let line_number_part = if options.include_line_number {
168 format!("{row}:")
169 } else {
170 String::new()
171 };
172
173 let col_number_part = if options.include_column_number {
174 format!("{col}:")
175 } else {
176 String::new()
177 };
178
179 let source_line_part = match line {
180 Some(s) => format!("| {s}\n| {padding}^\n"),
181 None => String::new(),
182 };
183
184 let msg_part = msg_lines
185 .into_iter()
186 .map(|l| format!("= {l}"))
187 .collect::<Vec<_>>()
188 .join("\n");
189
190 let position_part = format!("{line_number_part}{col_number_part}");
191 let position_part = match filename {
192 None if position_part.is_empty() => String::new(),
193 Some(f) if options.include_filename => format!("At {f}:{position_part}\n"),
194 _ => format!("At {position_part}\n"),
195 };
196
197 format!("{position_part}{source_line_part}{msg_part}",)
199 } else {
200 self.to_string()
201 };
202
203 if options.hide_current_directory {
206 let dir = options.current_directory.or(std::env::current_dir().ok());
207 if let Some(dir) = dir {
208 let dir = dir.to_string_lossy().replace('\\', "/");
209 e = e.replace(&dir, "");
210 e = e.replace("file:////", "file:///");
211 }
212 }
213
214 e
215 }
216}
217
218#[macro_use]
219mod error_macro {
220 macro_rules! map_error {
222 ($source_error:path, $impl:expr) => {
223 impl From<$source_error> for Error {
224 fn from(e: $source_error) -> Self {
225 let fmt: &dyn Fn($source_error) -> Self = &$impl;
226 fmt(e)
227 }
228 }
229 };
230 }
231}
232
233#[cfg(feature = "node_experimental")]
234map_error!(node_resolver::analyze::TranslateCjsToEsmError, |e| {
235 Error::Runtime(e.to_string())
236});
237
238map_error!(deno_ast::TranspileError, |e| Error::Runtime(e.to_string()));
239map_error!(deno_core::error::CoreError, |e| {
240 let e = e.into_kind();
241 match e {
242 CoreErrorKind::Js(js_error) => Error::JsError(Box::new(js_error)),
243 _ => Error::Runtime(e.to_string()),
244 }
245});
246map_error!(std::cell::BorrowMutError, |e| Error::Runtime(e.to_string()));
247map_error!(std::io::Error, |e| Error::ModuleNotFound(e.to_string()));
248map_error!(deno_core::v8::DataError, |e| Error::Runtime(e.to_string()));
249map_error!(deno_core::ModuleResolutionError, |e| Error::Runtime(
250 e.to_string()
251));
252map_error!(deno_core::url::ParseError, |e| Error::Runtime(
253 e.to_string()
254));
255map_error!(deno_core::serde_json::Error, |e| Error::JsonDecode(
256 e.to_string()
257));
258map_error!(deno_core::serde_v8::Error, |e| Error::JsonDecode(
259 e.to_string()
260));
261
262map_error!(deno_core::anyhow::Error, |e| {
263 Error::Runtime(e.to_string())
264});
265
266map_error!(tokio::time::error::Elapsed, |e| {
267 Error::Timeout(e.to_string())
268});
269map_error!(tokio::task::JoinError, |e| {
270 Error::Timeout(e.to_string())
271});
272map_error!(deno_core::futures::channel::oneshot::Canceled, |e| {
273 Error::Timeout(e.to_string())
274});
275
276#[cfg(feature = "broadcast_channel")]
277map_error!(deno_broadcast_channel::BroadcastChannelError, |e| {
278 Error::Runtime(e.to_string())
279});
280
281#[cfg(test)]
282mod test {
283 use crate::{error::ErrorFormattingOptions, Module, Runtime, RuntimeOptions, Undefined};
284
285 #[test]
286 #[rustfmt::skip]
287 fn test_highlights() {
288 let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
289
290 let e = runtime.eval::<Undefined>("1+1;\n1 + x").unwrap_err().as_highlighted(ErrorFormattingOptions::default());
291 assert_eq!(e, concat!(
292 "At 2:4:\n",
293 "= Uncaught ReferenceError: x is not defined"
294 ));
295
296 let module = Module::new("test.js", "1+1;\n1 + x");
297 let e = runtime.load_module(&module).unwrap_err().as_highlighted(ErrorFormattingOptions {
298 include_filename: false,
299 ..Default::default()
300 });
301 assert_eq!(e, concat!(
302 "At 2:4:\n",
303 "| 1 + x\n",
304 "| ^\n",
305 "= Uncaught (in promise) ReferenceError: x is not defined"
306 ));
307
308 let module = Module::new("test.js", "1+1;\n1 + x");
309 let e = runtime.load_module(&module).unwrap_err().as_highlighted(ErrorFormattingOptions {
310 hide_current_directory: true,
311 ..Default::default()
312 });
313 assert_eq!(e, concat!(
314 "At file:///test.js:2:4:\n",
315 "| 1 + x\n",
316 "| ^\n",
317 "= Uncaught (in promise) ReferenceError: x is not defined"
318 ));
319 }
320}