Thanks to visit codestin.com
Credit goes to docs.rs

rustyscript/
error.rs

1//! Contains the error type for the runtime
2//! And some associated utilities
3use std::path::PathBuf;
4
5use deno_core::error::CoreErrorKind;
6use thiserror::Error;
7
8use crate::Module;
9
10/// Options for [`Error::as_highlighted`]
11#[allow(clippy::struct_excessive_bools)]
12#[derive(Debug, Clone)]
13pub struct ErrorFormattingOptions {
14    /// Include the filename in the output
15    /// Appears on the first line
16    pub include_filename: bool,
17
18    /// Include the line number in the output
19    /// Appears on the first line
20    pub include_line_number: bool,
21
22    /// Include the column number in the output
23    /// Appears on the first line
24    pub include_column_number: bool,
25
26    /// Hide the current directory in the output
27    pub hide_current_directory: bool,
28
29    /// Used to set the directory to remove, in cases where the runtime and system CWD differ
30    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/// Represents the errors that can occur during execution of a module
46#[derive(Error, Debug, Clone, serde::Serialize, serde::Deserialize, deno_error::JsError)]
47pub enum Error {
48    /// Triggers when a module has no stated entrypoint (default or registered at runtime)
49    #[class(generic)]
50    #[error("{0} has no entrypoint. Register one, or add a default to the runtime")]
51    MissingEntrypoint(Module),
52
53    /// Triggers when an attempt to find a value by name fails
54    #[class(generic)]
55    #[error("{0} could not be found in global, or module exports")]
56    ValueNotFound(String),
57
58    /// Triggers when attempting to call a value as a function
59    #[class(generic)]
60    #[error("{0} is not a function")]
61    ValueNotCallable(String),
62
63    /// Triggers when a string could not be encoded for v8
64    #[class(generic)]
65    #[error("{0} could not be encoded as a v8 value")]
66    V8Encoding(String),
67
68    /// Triggers when a result could not be deserialize to the requested type
69    #[class(generic)]
70    #[error("value could not be deserialized: {0}")]
71    JsonDecode(String),
72
73    /// Triggers when a module could not be loaded from the filesystem
74    #[class(generic)]
75    #[error("{0}")]
76    ModuleNotFound(String),
77
78    /// Triggers when attempting to use a worker that has already been shutdown
79    #[class(generic)]
80    #[error("This worker has been destroyed")]
81    WorkerHasStopped,
82
83    /// Triggers on runtime issues during execution of a module
84    #[class(generic)]
85    #[error("{0}")]
86    Runtime(String),
87
88    /// Runtime error we successfully downcast
89    #[class(generic)]
90    #[error("{0}")]
91    JsError(Box<deno_core::error::JsError>),
92
93    /// Triggers when a module times out before finishing
94    #[class(generic)]
95    #[error("Module timed out: {0}")]
96    Timeout(String),
97
98    /// Triggers when the heap (via `max_heap_size`) is exhausted during execution
99    #[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    /// Formats an error for display in a terminal
112    /// If the error is a `JsError`, it will attempt to highlight the source line
113    /// in this format:
114    /// ```text
115    /// | let x = 1 + 2
116    /// |       ^
117    /// = Unexpected token '='
118    /// ```
119    ///
120    /// Otherwise, it will just display the error message normally
121    #[must_use]
122    pub fn as_highlighted(&self, options: ErrorFormattingOptions) -> String {
123        let mut e = if let Error::JsError(e) = self {
124            // Extract basic information about position
125            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            // Get at most 50 characters, centered on column_number
142            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            //
164            // Format all the parts using the options
165            //
166
167            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            // Combine all the parts
198            format!("{position_part}{source_line_part}{msg_part}",)
199        } else {
200            self.to_string()
201        };
202
203        //
204        // Hide directory
205        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    /// Maps one error type to another
221    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}