@@ -45,36 +45,70 @@ pub enum DDSAJsRuntimeError {
4545#[ derive( Debug , Clone ) ]
4646pub struct JsError {
4747 pub message : String ,
48- pub stack_trace : Vec < String > ,
48+ pub stack_trace_frames : Vec < JsCallSite > ,
49+ }
50+
51+ #[ derive( Debug , Clone , Eq , PartialEq ) ]
52+ pub struct JsCallSite {
53+ pub type_name : Option < String > ,
54+ pub function_name : Option < String > ,
55+ pub method_name : Option < String > ,
56+ pub file_name : Option < String > ,
57+ pub line_number : usize ,
58+ pub column_number : usize ,
59+ /// The string representation of this call site, as formatted by v8 (see [stack trace format] documentation).
60+ ///
61+ /// [stack trace format]: https://v8.dev/docs/stack-trace-api#appendix%3A-stack-trace-format
62+ v8_formatted_string : String ,
63+ }
64+
65+ impl Display for JsCallSite {
66+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
67+ write ! ( f, "{}" , self . v8_formatted_string)
68+ }
4969}
5070
5171impl From < deno_core:: error:: JsError > for JsError {
5272 fn from ( value : deno_core:: error:: JsError ) -> Self {
53- // `deno_core` formats the `deno_core::error::JsError::stack` such that the first line contains
54- // the error message, and all subsequent lines are a human-friendly stack trace. For example:
55- // ```
56- // TypeError: Cannot read properties of undefined (reading 'someProp')
57- // at SomeClass.someOtherFunction (ext:ddsa_lib/someModule:572:29)
58- // at SomeClass.someFunction (ext:ddsa_lib/someModule:157:22)
59- // at <anonymous>:1:13
60- // ```
61- let stack_trace = value. stack . map_or ( vec ! [ ] , |str| {
73+ // v8 formats an exception error message with a stable format described at
74+ // https://v8.dev/docs/stack-trace-api#appendix%3A-stack-trace-format. `deno_core` splits
75+ // this error message by line and assigns the resulting `Vec<String>` to the `stack` field.
76+ // At the same time, it constructs individual JsStackFrame structs with a typed breakdown
77+ // of v8's string format.
78+ //
79+ // The first line is the exception message (this is implicitly tested in the
80+ // `v8_exception_stack_trace_parsing` test, however it's listed here to communicate the expectation)
81+ // This uses a fuzzy check because there might be qualifiers like "Uncaught" before the message.
82+ debug_assert ! ( value
83+ . exception_message
84+ . contains( value. stack. as_ref( ) . unwrap( ) . lines( ) . next( ) . unwrap( ) ) , ) ;
85+ // ...so it is skipped to only collect the call sites.
86+ let stack_trace_frames = value. stack . map_or ( vec ! [ ] , |str| {
6287 str. lines ( )
6388 . skip ( 1 )
64- . map ( ToString :: to_string)
89+ . zip ( value. frames )
90+ . map ( |( display_str, frame) | JsCallSite {
91+ type_name : frame. type_name ,
92+ function_name : frame. function_name ,
93+ method_name : frame. method_name ,
94+ file_name : frame. file_name ,
95+ line_number : frame. line_number . unwrap_or_default ( ) as usize ,
96+ column_number : frame. column_number . unwrap_or_default ( ) as usize ,
97+ v8_formatted_string : display_str. to_string ( ) ,
98+ } )
6599 . collect :: < Vec < _ > > ( )
66100 } ) ;
67101 Self {
68102 message : value. exception_message ,
69- stack_trace ,
103+ stack_trace_frames ,
70104 }
71105 }
72106}
73107
74108impl Display for JsError {
75109 fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
76110 writeln ! ( f, "{}" , self . message) ?;
77- for line in & self . stack_trace {
111+ for line in & self . stack_trace_frames {
78112 writeln ! ( f, "{}" , line) ?;
79113 }
80114 Ok ( ( ) )
@@ -158,10 +192,8 @@ pub fn v8_interned<'s>(scope: &mut HandleScope<'s>, str: &str) -> v8::Local<'s,
158192}
159193
160194/// Creates a [`Normal`](v8::string::NewStringType::Normal) v8 string, which always allocates memory
161- /// to create the string, even if it has been seen by the runtime before.
162- ///
163- /// # Panics
164- /// Panics if `str` is longer than the v8 string length limit.
195+ /// to create the string, even if it has been seen by the runtime before. An empty string is
196+ /// returned if the string is larger than the v8 string length limit.
165197#[ inline( always) ]
166198pub fn v8_string < ' s > ( scope : & mut HandleScope < ' s > , str : & str ) -> v8:: Local < ' s , v8:: String > {
167199 v8:: String :: new_from_utf8 ( scope, str. as_bytes ( ) , v8:: NewStringType :: Normal )
@@ -298,14 +330,15 @@ pub fn set_key_value<G>(
298330 v8_object. set ( scope, v8_key. into ( ) , v8_value) ;
299331}
300332
301- /// Creates a `v8::Global` [`UnboundScript`](v8::UnboundScript) from the given code.
333+ /// Creates a `v8::Global` [`UnboundScript`](v8::UnboundScript) from the given code and origin .
302334pub fn compile_script (
303335 scope : & mut HandleScope ,
304336 code : & str ,
337+ origin : Option < & v8:: ScriptOrigin > ,
305338) -> Result < v8:: Global < v8:: UnboundScript > , DDSAJsRuntimeError > {
306339 let code_str = v8_string ( scope, code) ;
307340 let tc_scope = & mut v8:: TryCatch :: new ( scope) ;
308- let script_result = v8:: Script :: compile ( tc_scope, code_str, None ) ;
341+ let script_result = v8:: Script :: compile ( tc_scope, code_str, origin ) ;
309342
310343 let script = script_result. ok_or_else ( || {
311344 let exception = tc_scope
@@ -347,7 +380,7 @@ pub fn swallow_v8_error<F: FnOnce() -> T, T>(f: F) -> T {
347380#[ cfg( test) ]
348381mod tests {
349382 use crate :: analysis:: ddsa_lib:: common:: {
350- compile_script, v8_interned, v8_string, DDSAJsRuntimeError ,
383+ compile_script, v8_interned, v8_string, DDSAJsRuntimeError , JsCallSite ,
351384 } ;
352385 use crate :: analysis:: ddsa_lib:: runtime:: inner_make_deno_core_runtime;
353386 use crate :: analysis:: ddsa_lib:: test_utils:: { cfg_test_v8, try_execute} ;
@@ -358,7 +391,7 @@ mod tests {
358391 let invalid_js = r#"
359392const invalidSyntax = const;
360393"# ;
361- let err = compile_script ( & mut runtime. v8_handle_scope ( ) , invalid_js) . unwrap_err ( ) ;
394+ let err = compile_script ( & mut runtime. v8_handle_scope ( ) , invalid_js, None ) . unwrap_err ( ) ;
362395 let DDSAJsRuntimeError :: Interpreter { reason } = err else {
363396 panic ! ( "error variant should be `Interpreter`" ) ;
364397 } ;
@@ -371,7 +404,7 @@ const invalidSyntax = const;
371404 let invalid_js = r#"
372405const validSyntax = 123;
373406"# ;
374- assert ! ( compile_script( & mut runtime. v8_handle_scope( ) , invalid_js) . is_ok( ) ) ;
407+ assert ! ( compile_script( & mut runtime. v8_handle_scope( ) , invalid_js, None ) . is_ok( ) ) ;
375408 }
376409
377410 /// Tests that [`inner_make_deno_core_runtime`] can modify the default v8::Context for
@@ -395,7 +428,13 @@ const validSyntax = 123;
395428 None ,
396429 ) ;
397430 let value = try_execute ( & mut runtime. handle_scope ( ) , code) . unwrap_err ( ) ;
398- assert_eq ! ( value, "ReferenceError: Map is not defined" . to_string( ) ) ;
431+ let DDSAJsRuntimeError :: Execution { error : js_error } = value else {
432+ panic ! ( "should be this variant" )
433+ } ;
434+ assert_eq ! (
435+ js_error. message,
436+ "Uncaught ReferenceError: Map is not defined"
437+ ) ;
399438 }
400439
401440 // One byte characters
@@ -444,4 +483,47 @@ const validSyntax = 123;
444483 assert ! ( result. is_err( ) ) ;
445484 }
446485 }
486+
487+ /// The v8 exception stack trace string is properly parsed
488+ #[ test]
489+ fn v8_exception_stack_trace_parsing ( ) {
490+ let mut runtime = cfg_test_v8 ( ) . deno_core_rt ( ) ;
491+ // language=javascript
492+ let code = "\
493+ function someFunction() {
494+ someFunctionThatDoesntExist();
495+ }
496+ someFunction();
497+ " ;
498+ let DDSAJsRuntimeError :: Execution { error : js_error } =
499+ try_execute ( & mut runtime. handle_scope ( ) , code) . unwrap_err ( )
500+ else {
501+ panic ! ( "should be this variant" )
502+ } ;
503+ let expected = vec ! [
504+ JsCallSite {
505+ type_name: None ,
506+ function_name: Some ( "someFunction" . to_string( ) ) ,
507+ method_name: Some ( "someFunction" . to_string( ) ) ,
508+ file_name: None ,
509+ line_number: 2 ,
510+ column_number: 5 ,
511+ v8_formatted_string: " at someFunction (<anonymous>:2:5)" . to_string( ) ,
512+ } ,
513+ JsCallSite {
514+ type_name: None ,
515+ function_name: None ,
516+ method_name: None ,
517+ file_name: None ,
518+ line_number: 4 ,
519+ column_number: 1 ,
520+ v8_formatted_string: " at <anonymous>:4:1" . to_string( ) ,
521+ } ,
522+ ] ;
523+ assert_eq ! ( js_error. stack_trace_frames, expected) ;
524+ assert_eq ! (
525+ js_error. message,
526+ "Uncaught ReferenceError: someFunctionThatDoesntExist is not defined"
527+ ) ;
528+ }
447529}
0 commit comments