use std::cell::{Cell, RefCell};
use crate::error::{StatorError, StatorResult};
use crate::objects::property_map::PropertyMap;
use crate::objects::value::JsValue;
static STACK_TRACE_LIMIT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(10);
pub fn get_stack_trace_limit() -> usize {
STACK_TRACE_LIMIT.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_stack_trace_limit(limit: usize) {
STACK_TRACE_LIMIT.store(limit, std::sync::atomic::Ordering::Relaxed);
}
thread_local! {
static CALL_STACK: RefCell<Vec<(usize, &'static str)>> = const { RefCell::new(Vec::new()) };
static CALL_DEPTH: Cell<usize> = const { Cell::new(0) };
}
pub const MAX_CALL_STACK_DEPTH: usize = 128;
pub fn push_call_frame(name: &'static str) -> StatorResult<()> {
CALL_DEPTH.with(|d| {
let depth = d.get();
if depth >= MAX_CALL_STACK_DEPTH {
return Err(StatorError::RangeError(
"Maximum call stack size exceeded".to_string(),
));
}
d.set(depth + 1);
if name != "<anonymous>" {
CALL_STACK.with(|cs| cs.borrow_mut().push((depth + 1, name)));
}
Ok(())
})
}
pub fn call_stack_depth() -> usize {
CALL_DEPTH.with(Cell::get)
}
pub fn pop_call_frame_ex(named: bool) {
CALL_DEPTH.with(|d| {
let cur = d.get();
d.set(cur.saturating_sub(1));
if named {
CALL_STACK.with(|cs| {
let mut stack = cs.borrow_mut();
if stack.last().is_some_and(|&(depth, _)| depth == cur) {
stack.pop();
}
});
}
});
}
#[inline(always)]
pub fn with_anon_call_frame<R, F>(f: F) -> StatorResult<R>
where
F: FnOnce(usize) -> StatorResult<R>,
{
CALL_DEPTH.with(|d| {
let depth = d.get();
if depth >= MAX_CALL_STACK_DEPTH {
return Err(StatorError::RangeError(
"Maximum call stack size exceeded".to_string(),
));
}
d.set(depth + 1);
let result = f(depth + 1);
d.set(depth);
result
})
}
pub fn pop_call_frame() {
pop_call_frame_ex(true);
}
pub fn capture_call_stack() -> Vec<&'static str> {
let depth = CALL_DEPTH.with(Cell::get);
CALL_STACK.with(|cs| {
let named = cs.borrow();
let mut result = Vec::with_capacity(depth);
let mut named_idx = 0;
for d in 1..=depth {
if named_idx < named.len() && named[named_idx].0 == d {
result.push(named[named_idx].1);
named_idx += 1;
} else {
result.push("<anonymous>");
}
}
result
})
}
pub fn clear_call_stack() {
CALL_DEPTH.with(|d| d.set(0));
CALL_STACK.with(|cs| cs.borrow_mut().clear());
}
pub fn capture_stack_trace(error_name: &str, message: &str) -> String {
let limit = get_stack_trace_limit();
let mut result = format!("{error_name}: {message}");
let stack = capture_call_stack();
for frame in stack.iter().rev().take(limit) {
result.push_str("\n at ");
result.push_str(frame);
}
result
}
pub fn error_capture_stack_trace(target: &mut JsError, _constructor_name: Option<&str>) {
let new_stack = capture_stack_trace(target.name(), &target.message);
target.stack = new_stack;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
Error,
TypeError,
RangeError,
ReferenceError,
SyntaxError,
URIError,
EvalError,
AggregateError,
}
impl ErrorKind {
pub fn as_name(self) -> &'static str {
match self {
Self::Error => "Error",
Self::TypeError => "TypeError",
Self::RangeError => "RangeError",
Self::ReferenceError => "ReferenceError",
Self::SyntaxError => "SyntaxError",
Self::URIError => "URIError",
Self::EvalError => "EvalError",
Self::AggregateError => "AggregateError",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct JsError {
pub kind: ErrorKind,
pub message: String,
pub stack: String,
pub errors: Vec<JsValue>,
pub cause: Option<JsValue>,
pub props: RefCell<PropertyMap>,
}
impl JsError {
pub fn new(kind: ErrorKind, message: String) -> Self {
let stack = capture_stack_trace(kind.as_name(), &message);
Self {
kind,
message,
stack,
errors: Vec::new(),
cause: None,
props: RefCell::new(PropertyMap::new()),
}
}
pub fn new_aggregate(errors: Vec<JsValue>, message: String) -> Self {
let stack = capture_stack_trace("AggregateError", &message);
Self {
kind: ErrorKind::AggregateError,
message,
stack,
errors,
cause: None,
props: RefCell::new(PropertyMap::new()),
}
}
pub fn name(&self) -> &str {
self.kind.as_name()
}
pub fn message(&self) -> &str {
&self.message
}
pub fn stack(&self) -> &str {
&self.stack
}
pub fn cause(&self) -> Option<&JsValue> {
self.cause.as_ref()
}
pub fn with_cause(mut self, cause: JsValue) -> Self {
self.cause = Some(cause);
self
}
pub fn to_error_string(&self) -> String {
let props = self.props.borrow();
let name = match props.get("name") {
Some(JsValue::String(s)) => s.to_string(),
Some(JsValue::Undefined) => "Error".to_string(),
Some(v) => v.to_js_string().unwrap_or_else(|_| "Error".to_string()),
_ => self.kind.as_name().to_string(),
};
let msg = match props.get("message") {
Some(JsValue::String(s)) => s.to_string(),
Some(JsValue::Undefined) => String::new(),
Some(v) => v.to_js_string().unwrap_or_default(),
_ => self.message.clone(),
};
drop(props);
if name.is_empty() {
return msg;
}
if msg.is_empty() {
return name;
}
format!("{name}: {msg}")
}
}
pub fn error_new(message: String) -> JsError {
JsError::new(ErrorKind::Error, message)
}
pub fn type_error_new(message: String) -> JsError {
JsError::new(ErrorKind::TypeError, message)
}
pub fn range_error_new(message: String) -> JsError {
JsError::new(ErrorKind::RangeError, message)
}
pub fn reference_error_new(message: String) -> JsError {
JsError::new(ErrorKind::ReferenceError, message)
}
pub fn syntax_error_new(message: String) -> JsError {
JsError::new(ErrorKind::SyntaxError, message)
}
pub fn uri_error_new(message: String) -> JsError {
JsError::new(ErrorKind::URIError, message)
}
pub fn eval_error_new(message: String) -> JsError {
JsError::new(ErrorKind::EvalError, message)
}
pub fn aggregate_error_new(errors: Vec<JsValue>, message: String) -> JsError {
JsError::new_aggregate(errors, message)
}
#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
#[test]
fn test_error_kind_names() {
assert_eq!(ErrorKind::Error.as_name(), "Error");
assert_eq!(ErrorKind::TypeError.as_name(), "TypeError");
assert_eq!(ErrorKind::RangeError.as_name(), "RangeError");
assert_eq!(ErrorKind::ReferenceError.as_name(), "ReferenceError");
assert_eq!(ErrorKind::SyntaxError.as_name(), "SyntaxError");
assert_eq!(ErrorKind::URIError.as_name(), "URIError");
assert_eq!(ErrorKind::EvalError.as_name(), "EvalError");
assert_eq!(ErrorKind::AggregateError.as_name(), "AggregateError");
}
#[test]
fn test_error_new_type_error() {
let e = JsError::new(ErrorKind::TypeError, "not a function".to_string());
assert_eq!(e.kind, ErrorKind::TypeError);
assert_eq!(e.message(), "not a function");
assert_eq!(e.name(), "TypeError");
assert!(e.stack().starts_with("TypeError: not a function"));
assert!(e.errors.is_empty());
}
#[test]
fn test_error_new_range_error() {
let e = range_error_new("index out of range".to_string());
assert_eq!(e.kind, ErrorKind::RangeError);
assert_eq!(e.to_error_string(), "RangeError: index out of range");
}
#[test]
fn test_error_new_reference_error() {
let e = reference_error_new("x is not defined".to_string());
assert_eq!(e.name(), "ReferenceError");
assert_eq!(e.message(), "x is not defined");
}
#[test]
fn test_error_new_syntax_error() {
let e = syntax_error_new("unexpected token".to_string());
assert_eq!(e.name(), "SyntaxError");
assert_eq!(e.to_error_string(), "SyntaxError: unexpected token");
}
#[test]
fn test_error_new_uri_error() {
let e = uri_error_new("malformed URI".to_string());
assert_eq!(e.name(), "URIError");
}
#[test]
fn test_error_new_eval_error() {
let e = eval_error_new("eval is not supported".to_string());
assert_eq!(e.name(), "EvalError");
}
#[test]
fn test_error_empty_message() {
let e = error_new(String::new());
assert_eq!(e.to_error_string(), "Error");
assert!(e.stack().starts_with("Error"));
}
#[test]
fn test_aggregate_error_new() {
let e1 = JsValue::Error(Rc::new(type_error_new("bad type".to_string())));
let e2 = JsValue::Error(Rc::new(range_error_new("out of range".to_string())));
let agg = aggregate_error_new(
vec![e1.clone(), e2.clone()],
"multiple failures".to_string(),
);
assert_eq!(agg.kind, ErrorKind::AggregateError);
assert_eq!(agg.message(), "multiple failures");
assert_eq!(agg.name(), "AggregateError");
assert_eq!(agg.errors.len(), 2);
assert!(matches!(&agg.errors[0], JsValue::Error(e) if e.kind == ErrorKind::TypeError));
assert!(matches!(&agg.errors[1], JsValue::Error(e) if e.kind == ErrorKind::RangeError));
}
#[test]
fn test_to_error_string_with_message() {
let e = JsError::new(ErrorKind::Error, "something failed".to_string());
assert_eq!(e.to_error_string(), "Error: something failed");
}
#[test]
fn test_to_error_string_empty_message() {
let e = JsError::new(ErrorKind::TypeError, String::new());
assert_eq!(e.to_error_string(), "TypeError");
}
#[test]
fn test_to_error_string_overridden_message() {
let e = JsError::new(ErrorKind::Error, "original".to_string());
e.props
.borrow_mut()
.insert("message".to_string(), JsValue::String("overridden".into()));
assert_eq!(e.to_error_string(), "Error: overridden");
}
#[test]
fn test_to_error_string_overridden_name() {
let e = JsError::new(ErrorKind::Error, "msg".to_string());
e.props
.borrow_mut()
.insert("name".to_string(), JsValue::String("CustomError".into()));
assert_eq!(e.to_error_string(), "CustomError: msg");
}
#[test]
fn test_to_error_string_overridden_name_empty() {
let e = JsError::new(ErrorKind::Error, "msg".to_string());
e.props
.borrow_mut()
.insert("name".to_string(), JsValue::String(String::new().into()));
assert_eq!(e.to_error_string(), "msg");
}
#[test]
fn test_props_overlay_custom_property() {
let e = JsError::new(ErrorKind::Error, "test".to_string());
e.props
.borrow_mut()
.insert("code".to_string(), JsValue::Smi(42));
assert_eq!(e.props.borrow().get("code"), Some(&JsValue::Smi(42)));
}
#[test]
fn test_stack_trace_no_frames() {
let e = JsError::new(ErrorKind::Error, "test".to_string());
assert_eq!(e.stack(), "Error: test");
}
#[test]
fn test_stack_trace_with_frames() {
push_call_frame("outer");
push_call_frame("inner");
let e = JsError::new(ErrorKind::TypeError, "oops".to_string());
pop_call_frame();
pop_call_frame();
assert!(e.stack().starts_with("TypeError: oops"));
assert!(e.stack().contains("inner"));
assert!(e.stack().contains("outer"));
}
#[test]
fn test_stack_trace_limit() {
clear_call_stack();
let old_limit = get_stack_trace_limit();
set_stack_trace_limit(2);
push_call_frame("frameA");
push_call_frame("frameB");
push_call_frame("frameC");
push_call_frame("frameD");
let e = JsError::new(ErrorKind::Error, "limited".to_string());
pop_call_frame();
pop_call_frame();
pop_call_frame();
pop_call_frame();
let stack = e.stack();
assert!(
stack.contains("frameD"),
"most recent frame 'frameD' missing: {stack}"
);
assert!(
stack.contains("frameC"),
"second frame 'frameC' missing: {stack}"
);
assert!(
!stack.contains("frameB"),
"frame 'frameB' should be truncated: {stack}"
);
assert!(
!stack.contains("frameA"),
"frame 'frameA' should be truncated: {stack}"
);
set_stack_trace_limit(old_limit);
}
#[test]
fn test_capture_stack_trace() {
push_call_frame("myFunction");
let mut e = JsError::new(ErrorKind::Error, "captured".to_string());
error_capture_stack_trace(&mut e, None);
pop_call_frame();
assert!(e.stack().starts_with("Error: captured"));
assert!(e.stack().contains("myFunction"));
}
#[test]
fn test_stack_trace_limit_getter_setter() {
let original = get_stack_trace_limit();
set_stack_trace_limit(5);
assert_eq!(get_stack_trace_limit(), 5);
set_stack_trace_limit(original);
}
#[test]
fn test_push_call_frame_exceeds_limit_returns_range_error() {
for _ in 0..MAX_CALL_STACK_DEPTH {
push_call_frame("<test>").expect("should not fail below the limit");
}
let result = push_call_frame("<test>");
for _ in 0..MAX_CALL_STACK_DEPTH {
pop_call_frame();
}
assert!(
matches!(result, Err(crate::error::StatorError::RangeError(_))),
"expected RangeError when call stack is full, got {result:?}"
);
}
#[test]
fn test_error_cause_none_by_default() {
let e = JsError::new(ErrorKind::Error, "no cause".to_string());
assert!(e.cause().is_none());
}
#[test]
fn test_error_with_cause() {
let cause = JsValue::String("disk full".to_string().into());
let e =
JsError::new(ErrorKind::Error, "write failed".to_string()).with_cause(cause.clone());
assert_eq!(e.cause(), Some(&cause));
}
#[test]
fn test_error_cause_can_be_error_value() {
let inner = JsValue::Error(Rc::new(type_error_new("bad type".to_string())));
let outer = JsError::new(ErrorKind::Error, "wrapper".to_string()).with_cause(inner.clone());
assert_eq!(outer.cause(), Some(&inner));
}
#[test]
fn test_aggregate_error_cause_none_by_default() {
let agg = aggregate_error_new(vec![], "agg".to_string());
assert!(agg.cause().is_none());
}
#[test]
fn test_aggregate_error_with_cause() {
let cause = JsValue::Smi(42);
let mut agg = aggregate_error_new(vec![], "agg".to_string());
agg.cause = Some(cause.clone());
assert_eq!(agg.cause(), Some(&cause));
}
#[test]
fn test_all_error_kinds_have_correct_names() {
let kinds = [
(ErrorKind::Error, "Error"),
(ErrorKind::TypeError, "TypeError"),
(ErrorKind::RangeError, "RangeError"),
(ErrorKind::ReferenceError, "ReferenceError"),
(ErrorKind::SyntaxError, "SyntaxError"),
(ErrorKind::URIError, "URIError"),
(ErrorKind::EvalError, "EvalError"),
(ErrorKind::AggregateError, "AggregateError"),
];
for (kind, expected_name) in kinds {
let e = JsError::new(kind, "test".to_string());
assert_eq!(e.name(), expected_name, "wrong name for {kind:?}");
assert_eq!(e.message(), "test");
assert!(
e.stack().starts_with(&format!("{expected_name}: test")),
"stack should start with error string for {kind:?}"
);
}
}
}