Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 139 additions & 1 deletion core/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Consistent error handling utilities for builtins.
//!
//! This module provides helper functions to create V8 exceptions with consistent
//! error messages across all builtin implementations.
//! error messages across all builtin implementations, as well as formatting
//! exceptions with source information and stack traces.

/// Throws a TypeError with the given message.
///
Expand Down Expand Up @@ -175,3 +176,140 @@ pub(crate) fn try_get_array_result<'s>(
) -> Result<v8::Local<'s, v8::Array>, &'static str> {
v8::Local::<v8::Array>::try_from(value).map_err(|_| "Value must be an array")
}

/// Format an exception with file name, line number, source code, and stack trace.
/// This provides detailed error information similar to Node.js.
///
/// Returns a formatted error string that includes:
/// - File path and line number
/// - Source code line with error
/// - Caret (^^^) pointing to error location
/// - Error message
/// - Stack trace (when available)
pub(crate) fn format_exception(
tc: &mut v8::PinnedRef<'_, v8::TryCatch<v8::HandleScope>>,
) -> String {
let isolate: &v8::Isolate = tc;

// Get the exception value
let exception = match tc.exception() {
Some(e) => e,
None => return "Unknown error".to_string(),
};

// Get the error message from the exception
let exception_string = exception
.to_string(tc)
.map(|s| s.to_rust_string_lossy(isolate))
.unwrap_or_else(|| "Unknown error".to_string());

// Try to get the Message object for detailed error information
if let Some(message) = tc.message() {
let mut output = String::new();

// Get file name and line number
let resource_name = message
.get_script_resource_name(tc)
.and_then(|v| v.to_string(tc))
.map(|s| s.to_rust_string_lossy(isolate));

let line_number = message.get_line_number(tc);

// Get source line if available
let source_line = message
.get_source_line(tc)
.map(|s| s.to_string(tc).unwrap().to_rust_string_lossy(isolate));

// Get column information
let start_column = message.get_start_column();
let end_column = message.get_end_column();

// Format the output similar to Node.js
if let (Some(file), Some(line)) = (resource_name, line_number) {
output.push_str(&format!("{}:{}\n", file, line));

// Add source line if available
if let Some(source) = source_line {
output.push_str(&source);
output.push('\n');

// Add caret indicator
// Add spaces for indentation
for _ in 0..start_column {
output.push(' ');
}

// Add carets
let caret_count = (end_column - start_column).max(1);

for _ in 0..caret_count {
output.push('^');
}
output.push('\n');
}
}

// Add the error message
output.push('\n');
output.push_str(&exception_string);

// Try to get stack trace - check if the exception has a stack property
if let Ok(exception_obj) = v8::Local::<v8::Object>::try_from(exception) {
let stack_key = v8::String::new(tc, "stack").unwrap();
if let Some(stack_val) = exception_obj.get(tc, stack_key.into())
&& let Some(stack_str) = stack_val.to_string(tc)
{
let stack = stack_str.to_rust_string_lossy(isolate);
// Only add stack if it's different from the exception string
// and contains actual stack information
if !stack.is_empty() && stack != exception_string && stack.contains('\n') {
// The stack already includes the error message in most cases,
// so we'll use it as-is if it contains the error message,
// otherwise append it
if stack.starts_with(&exception_string) || stack.contains(&exception_string) {
output = String::new();
if let (Some(file), Some(line)) = (
message
.get_script_resource_name(tc)
.and_then(|v| v.to_string(tc))
.map(|s| s.to_rust_string_lossy(isolate)),
message.get_line_number(tc),
) {
output.push_str(&format!("{}:{}\n", file, line));

if let Some(source) = message
.get_source_line(tc)
.map(|s| s.to_string(tc).unwrap().to_rust_string_lossy(isolate))
{
output.push_str(&source);
output.push('\n');

// Add spaces for indentation
for _ in 0..start_column {
output.push(' ');
}

let caret_count = (end_column - start_column).max(1);

for _ in 0..caret_count {
output.push('^');
}
output.push('\n');
}
output.push('\n');
}
output.push_str(&stack);
} else {
output.push('\n');
output.push_str(&stack);
}
}
}
}

output
} else {
// If no message object, fall back to just the exception string
exception_string
}
}
36 changes: 30 additions & 6 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,36 @@ impl JSTime {
let cwd = cwd.into_os_string().into_string().unwrap();
match loader.import(&mut scope, &cwd, filename) {
Ok(_) => Ok(()),
Err(e) => {
Err(exception) => {
// Format the exception value directly
let isolate: &v8::Isolate = &scope;
Err(e.to_string(&scope).unwrap().to_rust_string_lossy(isolate))
let exception_str = exception
.to_string(&scope)
.map(|s| s.to_rust_string_lossy(isolate))
.unwrap_or_else(|| "Unknown error".to_string());

// Try to get stack property for more details
if let Ok(exception_obj) = v8::Local::<v8::Object>::try_from(exception) {
let stack_key = v8::String::new(&scope, "stack").unwrap();
if let Some(stack_val) = exception_obj.get(&scope, stack_key.into())
&& let Some(stack_str) = stack_val.to_string(&scope)
{
let stack = stack_str.to_rust_string_lossy(isolate);
if !stack.is_empty() && stack != exception_str {
return Err(stack);
}
}
}

// Remove "Error: " prefix if present (V8 adds this when creating Error objects)
let exception_str =
if let Some(stripped) = exception_str.strip_prefix("Error: ") {
stripped.to_string()
} else {
exception_str
};

Err(exception_str)
}
}
};
Expand Down Expand Up @@ -202,10 +229,7 @@ impl JSTime {
let isolate: &v8::Isolate = &scope;
Ok(v.to_string(&scope).unwrap().to_rust_string_lossy(isolate))
}
Err(e) => {
let isolate: &v8::Isolate = &scope;
Err(e.to_string(&scope).unwrap().to_rust_string_lossy(isolate))
}
Err(e) => Err(e),
}
}

Expand Down
33 changes: 30 additions & 3 deletions core/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ impl Loader {
}
}
None => {
// Check if we have a caught exception
if tc.has_caught() {
Err(tc.exception().unwrap())
} else {
panic!("Module import failed without exception")
// Error was caught during resolve, create a generic error
let msg = v8::String::new(tc, "Module import failed").unwrap();
let exception = v8::Exception::error(tc, msg);
Err(exception)
}
}
}
Expand Down Expand Up @@ -102,16 +106,39 @@ fn resolve<'a>(
let code = v8::String::new(scope, &js_src).unwrap();
let mut source = v8::script_compiler::Source::new(code, Some(&origin));

let module = v8::script_compiler::compile_module(scope, &mut source);
// Compile the module - errors will be thrown as exceptions
let (module, error_msg) = {
v8::tc_scope!(let tc, scope);
let module = v8::script_compiler::compile_module(tc, &mut source);

if let Some(module) = module {
(Some(module), None)
} else if tc.has_caught() {
// Format the compilation error as a string
let error_message = crate::error::format_exception(tc);
(None, Some(error_message))
} else {
(None, None)
}
};

if let Some(module) = module {
let isolate: &mut v8::Isolate = scope;
let state = IsolateState::get(isolate);
state
.borrow_mut()
.module_map
.insert(isolate, &requested_abs_path, module);
Some(module)
} else if let Some(error_msg) = error_msg {
// Now create a new error with our formatted message
let msg = v8::String::new(scope, &error_msg).unwrap();
let exception = v8::Exception::error(scope, msg);
scope.throw_exception(exception);
None
} else {
None
}
module
}

fn resolve_builtin_module<'a>(
Expand Down
10 changes: 3 additions & 7 deletions core/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub(crate) fn run<'s>(
scope: &mut v8::PinScope<'s, '_>,
js: &str,
filepath: &str,
) -> Result<v8::Local<'s, v8::Value>, v8::Local<'s, v8::Value>> {
) -> Result<v8::Local<'s, v8::Value>, String> {
v8::tc_scope!(let tc, scope);

let filepath = v8::String::new(tc, filepath).unwrap();
Expand All @@ -19,12 +19,8 @@ pub(crate) fn run<'s>(
Some(value) => Ok(value),
None => {
if tc.has_caught() {
// Try to get the stack trace first, fall back to exception
if let Some(stack_trace) = tc.stack_trace() {
Err(stack_trace)
} else {
Err(tc.exception().unwrap())
}
// Format the error with detailed information
Err(crate::error::format_exception(tc))
} else {
panic!("Script execution failed without exception")
}
Expand Down
9 changes: 7 additions & 2 deletions core/tests/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ mod api {
Ok(_result) => panic!(),
Err(e) => e,
};
assert_eq!(err, "ReferenceError: a is not defined\n at jstime:1:1");
// New format includes file:line, source code, caret indicator, and error message with stack
assert_eq!(
err,
"jstime:1\na\n^\n\nReferenceError: a is not defined\n at jstime:1:1"
);
let err = match jstime.run_script("}", "jstime") {
Ok(_result) => panic!(),
Err(e) => e,
};
assert_eq!(err, "SyntaxError: Unexpected token \'}\'");
// Syntax errors now include file:line, source code, caret indicator, and error message
assert_eq!(err, "jstime:1\n}\n^\n\nSyntaxError: Unexpected token \'}\'");
}
#[test]
fn import() {
Expand Down
Loading