use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ScriptErrorKind {
Syntax,
Runtime,
Timeout,
MemoryLimit,
Internal,
}
impl fmt::Display for ScriptErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Syntax => write!(f, "syntax_error"),
Self::Runtime => write!(f, "runtime_error"),
Self::Timeout => write!(f, "timeout"),
Self::MemoryLimit => write!(f, "memory_limit"),
Self::Internal => write!(f, "internal_error"),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ScriptError {
pub kind: ScriptErrorKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stack: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_snippet: Option<String>,
}
impl ScriptError {
pub fn redact(&mut self, redactor: &dyn crate::redact::Redactor) {
if redactor.is_empty() {
return;
}
crate::redact::redact_in_place(redactor, &mut self.message);
for field in [&mut self.name, &mut self.stack, &mut self.source_snippet] {
if let Some(text) = field.as_mut() {
crate::redact::redact_in_place(redactor, text);
}
}
}
#[must_use]
pub fn internal(message: impl Into<String>) -> Self {
Self {
kind: ScriptErrorKind::Internal,
name: None,
message: message.into(),
stack: None,
line: None,
column: None,
source_snippet: None,
}
}
#[must_use]
pub fn named(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..Self::internal(message)
}
}
#[must_use]
pub fn timeout(elapsed_ms: u64, limit_ms: u64) -> Self {
Self {
kind: ScriptErrorKind::Timeout,
name: None,
message: format!("script exceeded timeout: {elapsed_ms}ms > {limit_ms}ms"),
stack: None,
line: None,
column: None,
source_snippet: None,
}
}
#[must_use]
pub fn memory_limit(limit_bytes: usize) -> Self {
Self {
kind: ScriptErrorKind::MemoryLimit,
name: None,
message: format!("script exceeded memory limit of {limit_bytes} bytes"),
stack: None,
line: None,
column: None,
source_snippet: None,
}
}
}
impl ScriptError {
#[must_use]
pub fn from_caught(ctx: &rquickjs::Ctx<'_>, caught: rquickjs::CaughtError<'_>, source: &str) -> Self {
Self::from_caught_offset(ctx, caught, source, 0)
}
#[must_use]
pub fn from_caught_offset(
ctx: &rquickjs::Ctx<'_>,
caught: rquickjs::CaughtError<'_>,
source: &str,
line_offset: u32,
) -> Self {
let mut err = Self::from_caught_unmapped(caught, source, line_offset);
if let Some(stack) = err.stack.take() {
err.stack = Some(crate::source_map::remap_stack(ctx, &stack));
}
err
}
#[must_use]
pub fn from_caught_unmapped(caught: rquickjs::CaughtError<'_>, source: &str, line_offset: u32) -> Self {
let (name, message, stack, line, column) = match caught {
rquickjs::CaughtError::Exception(ex) => {
let message = ex.message().unwrap_or_else(|| "exception".to_string());
let stack = ex.stack();
let obj = ex.as_object();
let name = obj.get::<_, String>("name").ok().filter(|n| !n.is_empty());
let mut line = obj.get::<_, u32>("lineNumber").ok();
let mut column = obj.get::<_, u32>("columnNumber").ok();
if line.is_none()
&& let Some((_, l, c)) = stack.as_deref().and_then(crate::source_map::innermost_frame)
{
line = Some(l);
column = Some(c);
}
(name, message, stack, line, column)
},
rquickjs::CaughtError::Value(v) if v.is_null() => {
return Self {
kind: ScriptErrorKind::MemoryLimit,
name: None,
message: "out of memory: the engine could not allocate an error object".to_string(),
stack: None,
line: None,
column: None,
source_snippet: None,
};
},
rquickjs::CaughtError::Value(v) => (None, format!("{v:?}"), None, None, None),
rquickjs::CaughtError::Error(e) => (None, format!("{e}"), None, None, None),
};
let kind = if name.as_deref() == Some("SyntaxError") {
ScriptErrorKind::Syntax
} else if name.as_deref() == Some("InternalError") && message.contains("out of memory") {
ScriptErrorKind::MemoryLimit
} else {
ScriptErrorKind::Runtime
};
let line = line.and_then(|l| l.checked_sub(line_offset).filter(|l| *l >= 1));
Self {
kind,
name,
message,
stack,
line,
column,
source_snippet: line.and_then(|l| snippet_around_line(source, l, 2)),
}
}
}
fn snippet_around_line(source: &str, line_1based: u32, context_lines: u32) -> Option<String> {
use std::fmt::Write as _;
let lines: Vec<&str> = source.lines().collect();
if lines.is_empty() {
return None;
}
let target = line_1based.saturating_sub(1) as usize;
if target >= lines.len() {
return None;
}
let start = target.saturating_sub(context_lines as usize);
let end = (target + context_lines as usize + 1).min(lines.len());
let mut out = String::new();
for (i, text) in lines[start..end].iter().enumerate() {
let ln = start + i + 1;
let marker = if ln == line_1based as usize { ">>>" } else { " " };
let _ = writeln!(out, "{marker} {ln:>4}: {text}");
}
Some(out)
}
impl fmt::Display for ScriptError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.kind, self.message)?;
if let (Some(l), Some(c)) = (self.line, self.column) {
write!(f, " (at {l}:{c})")?;
}
Ok(())
}
}
impl std::error::Error for ScriptError {}
impl From<rquickjs::Error> for ScriptError {
fn from(e: rquickjs::Error) -> Self {
Self::internal(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::snippet_around_line;
#[test]
fn a_line_past_the_end_has_no_snippet() {
assert_eq!(snippet_around_line("one\ntwo\nthree", 40, 2), None);
}
#[test]
fn the_target_line_is_marked() {
let out = snippet_around_line("one\ntwo\nthree", 2, 1).expect("snippet");
assert_eq!(out, " 1: one\n>>> 2: two\n 3: three\n");
}
}