1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-22 15:06:54 -05:00

fix(core/js_error): Get frame data from prepareStackTrace() (#4690)

Fixes: #2703
Fixes: #2710
Closes: #4153
Closes: #4232

Co-authored-by: Kevin (Kun) Kassimo Qian <kevinkassimo@gmail.com>
This commit is contained in:
Nayeem Rahman 2020-04-10 17:26:52 +01:00 committed by GitHub
parent 195ad4c626
commit 8b4508338b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 257 additions and 48 deletions

View file

@ -155,9 +155,21 @@ fn format_stack_frame(frame: &JSStackFrame, is_internal_frame: bool) -> String {
source_loc = colors::gray(source_loc).to_string();
}
if !frame.function_name.is_empty() {
format!("{} {} {}", at_prefix, function_name, source_loc)
if frame.is_async {
format!(
"{} {} {} {}",
at_prefix,
colors::gray("async".to_owned()).to_string(),
function_name,
source_loc
)
} else {
format!("{} {} {}", at_prefix, function_name, source_loc)
}
} else if frame.is_eval {
format!("{} eval {}", at_prefix, source_loc)
} else if frame.is_async {
format!("{} async {}", at_prefix, source_loc)
} else {
format!("{} {}", at_prefix, source_loc)
}
@ -272,6 +284,7 @@ mod tests {
function_name: "foo".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 5,
@ -280,6 +293,7 @@ mod tests {
function_name: "qat".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 1,
@ -288,8 +302,10 @@ mod tests {
function_name: "".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
],
already_source_mapped: true,
};
let formatted_error = JSError(core_js_error).to_string();
let actual = strip_ansi_codes(&formatted_error);

View file

@ -140,7 +140,7 @@ function callSiteToString(callSite: CallSite): string {
result += "async ";
}
if (isPromiseAll) {
result += `Promise.all (index ${callSite.getPromiseIndex})`;
result += `Promise.all (index ${callSite.getPromiseIndex()})`;
return result;
}
if (isMethodCall) {
@ -163,11 +163,52 @@ function callSiteToString(callSite: CallSite): string {
return result;
}
interface CallSiteEval {
this: unknown;
typeName: string;
function: Function;
functionName: string;
methodName: string;
fileName: string;
lineNumber: number | null;
columnNumber: number | null;
evalOrigin: string | null;
isToplevel: boolean;
isEval: boolean;
isNative: boolean;
isConstructor: boolean;
isAsync: boolean;
isPromiseAll: boolean;
promiseIndex: number | null;
}
function evaluateCallSite(callSite: CallSite): CallSiteEval {
return {
this: callSite.getThis(),
typeName: callSite.getTypeName(),
function: callSite.getFunction(),
functionName: callSite.getFunctionName(),
methodName: callSite.getMethodName(),
fileName: callSite.getFileName(),
lineNumber: callSite.getLineNumber(),
columnNumber: callSite.getColumnNumber(),
evalOrigin: callSite.getEvalOrigin(),
isToplevel: callSite.isToplevel(),
isEval: callSite.isEval(),
isNative: callSite.isNative(),
isConstructor: callSite.isConstructor(),
isAsync: callSite.isAsync(),
isPromiseAll: callSite.isPromiseAll(),
promiseIndex: callSite.getPromiseIndex(),
};
}
function prepareStackTrace(
error: Error,
structuredStackTrace: CallSite[]
): string {
return (
Object.defineProperty(error, "__callSiteEvals", { value: [] });
const errorString =
`${error.name}: ${error.message}\n` +
structuredStackTrace
.map(
@ -188,9 +229,18 @@ function prepareStackTrace(
return callSite;
}
)
.map((callSite): string => ` at ${callSiteToString(callSite)}`)
.join("\n")
);
.map((callSite): string => {
const callSiteEv = Object.freeze(evaluateCallSite(callSite));
if (callSiteEv.lineNumber != null && callSiteEv.columnNumber != null) {
// @ts-ignore
error["__callSiteEvals"].push(callSiteEv);
}
return ` at ${callSiteToString(callSite)}`;
})
.join("\n");
// @ts-ignore
Object.freeze(error["__callSiteEvals"]);
return errorString;
}
// @internal

View file

@ -49,11 +49,16 @@ pub fn apply_source_map<G: SourceMapGetter>(
) -> deno_core::JSError {
let mut mappings_map: CachedMaps = HashMap::new();
let mut frames = Vec::<JSStackFrame>::new();
for frame in &js_error.frames {
let f = frame_apply_source_map(&frame, &mut mappings_map, getter);
frames.push(f);
}
let frames = if !js_error.already_source_mapped {
let mut frames = Vec::<JSStackFrame>::new();
for frame in &js_error.frames {
let f = frame_apply_source_map(&frame, &mut mappings_map, getter);
frames.push(f);
}
frames
} else {
js_error.frames.clone()
};
let (script_resource_name, line_number, start_column) =
get_maybe_orig_position(
@ -98,6 +103,7 @@ pub fn apply_source_map<G: SourceMapGetter>(
start_column,
end_column,
frames,
already_source_mapped: js_error.already_source_mapped,
}
}
@ -121,6 +127,7 @@ fn frame_apply_source_map<G: SourceMapGetter>(
column,
is_eval: frame.is_eval,
is_constructor: frame.is_constructor,
is_async: frame.is_async,
}
}
@ -255,6 +262,7 @@ mod tests {
function_name: "foo".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 5,
@ -263,6 +271,7 @@ mod tests {
function_name: "qat".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 1,
@ -271,8 +280,10 @@ mod tests {
function_name: "".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
],
already_source_mapped: false,
};
let getter = MockSourceMapGetter {};
let actual = apply_source_map(&core_js_error, &getter);
@ -291,6 +302,7 @@ mod tests {
function_name: "foo".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 4,
@ -299,6 +311,7 @@ mod tests {
function_name: "qat".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 1,
@ -307,8 +320,10 @@ mod tests {
function_name: "".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
],
already_source_mapped: false,
};
assert_eq!(actual, expected);
}
@ -329,7 +344,9 @@ mod tests {
function_name: "setLogDebug".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
}],
already_source_mapped: false,
};
let getter = MockSourceMapGetter {};
let actual = apply_source_map(&e, &getter);
@ -348,6 +365,7 @@ mod tests {
start_column: Some(16),
end_column: None,
frames: vec![],
already_source_mapped: false,
};
let getter = MockSourceMapGetter {};
let actual = apply_source_map(&e, &getter);

View file

@ -1,6 +1,6 @@
[WILDCARD]
error: Uncaught BadResource: Bad resource ID
[WILDCARD]dispatch_json.ts:[WILDCARD]
at BadResource ([WILDCARD]errors.ts:[WILDCARD])
at unwrapResponse ([WILDCARD]dispatch_json.ts:[WILDCARD])
at sendAsync ([WILDCARD]dispatch_json.ts:[WILDCARD])
at async main ([WILDCARD]tests/044_bad_resource.ts:[WILDCARD])

View file

@ -1,5 +1,9 @@
[WILDCARD]error: Uncaught NotFound: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]/error_004_missing_module.ts"
[WILDCARD]dispatch_json.ts:[WILDCARD]
at NotFound ([WILDCARD]errors.ts:[WILDCARD])
at unwrapResponse ([WILDCARD]dispatch_json.ts:[WILDCARD])
at sendAsync[WILDCARD] ([WILDCARD]dispatch_json.ts:[WILDCARD])
at sendAsync ([WILDCARD]dispatch_json.ts:[WILDCARD])
at async processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async compile ([WILDCARD]compiler.ts:[WILDCARD])
at async tsCompilerOnMessage ([WILDCARD]compiler.ts:[WILDCARD])
at async workerMessageRecvCallback ([WILDCARD]runtime_worker.ts:[WILDCARD])

View file

@ -1,5 +1,9 @@
[WILDCARD]error: Uncaught NotFound: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]/error_005_missing_dynamic_import.ts"
[WILDCARD]dispatch_json.ts:[WILDCARD]
at NotFound ([WILDCARD]errors.ts:[WILDCARD])
at unwrapResponse ([WILDCARD]dispatch_json.ts:[WILDCARD])
at sendAsync[WILDCARD] ([WILDCARD]dispatch_json.ts:[WILDCARD])
at sendAsync ([WILDCARD]dispatch_json.ts:[WILDCARD])
at async processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async compile ([WILDCARD]compiler.ts:[WILDCARD])
at async tsCompilerOnMessage ([WILDCARD]compiler.ts:[WILDCARD])
at async workerMessageRecvCallback ([WILDCARD]runtime_worker.ts:[WILDCARD])

View file

@ -1,5 +1,9 @@
[WILDCARD]error: Uncaught NotFound: Cannot resolve module "[WILDCARD]/non-existent" from "[WILDCARD]/error_006_import_ext_failure.ts"
[WILDCARD]dispatch_json.ts:[WILDCARD]
at NotFound ([WILDCARD]errors.ts:[WILDCARD])
at unwrapResponse ([WILDCARD]dispatch_json.ts:[WILDCARD])
at sendAsync[WILDCARD] ([WILDCARD]dispatch_json.ts:[WILDCARD])
at async processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async compile ([WILDCARD]compiler.ts:[WILDCARD])
at async tsCompilerOnMessage ([WILDCARD]compiler.ts:[WILDCARD])
at async workerMessageRecvCallback ([WILDCARD]runtime_worker.ts:[WILDCARD])

View file

@ -1,7 +1,10 @@
[WILDCARD]error: Uncaught URIError: relative import path "bad-module.ts" not prefixed with / or ./ or ../ Imported from "[WILDCARD]/error_011_bad_module_specifier.ts"
[WILDCARD]dispatch_json.ts:[WILDCARD]
at unwrapResponse ($deno$/ops/dispatch_json.ts:[WILDCARD])
at sendSync ($deno$/ops/dispatch_json.ts:[WILDCARD])
at resolveModules ($deno$/compiler/imports.ts:[WILDCARD])
at processImports ($deno$/compiler/imports.ts:[WILDCARD])
at processImports ($deno$/compiler/imports.ts:[WILDCARD])
at unwrapResponse ([WILDCARD]ops/dispatch_json.ts:[WILDCARD])
at sendSync ([WILDCARD]ops/dispatch_json.ts:[WILDCARD])
at resolveModules ([WILDCARD]compiler/imports.ts:[WILDCARD])
at processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async compile ([WILDCARD]compiler.ts:[WILDCARD])
at async tsCompilerOnMessage ([WILDCARD]compiler.ts:[WILDCARD])
at async workerMessageRecvCallback ([WILDCARD]runtime_worker.ts:[WILDCARD])

View file

@ -1,7 +1,10 @@
[WILDCARD]error: Uncaught URIError: relative import path "bad-module.ts" not prefixed with / or ./ or ../ Imported from "[WILDCARD]/error_012_bad_dynamic_import_specifier.ts"
[WILDCARD]dispatch_json.ts:[WILDCARD]
at unwrapResponse ($deno$/ops/dispatch_json.ts:[WILDCARD])
at sendSync ($deno$/ops/dispatch_json.ts:[WILDCARD])
at resolveModules ($deno$/compiler/imports.ts:[WILDCARD])
at processImports ($deno$/compiler/imports.ts:[WILDCARD])
at processImports ($deno$/compiler/imports.ts:[WILDCARD])
at unwrapResponse ([WILDCARD]ops/dispatch_json.ts:[WILDCARD])
at sendSync ([WILDCARD]ops/dispatch_json.ts:[WILDCARD])
at resolveModules ([WILDCARD]compiler/imports.ts:[WILDCARD])
at processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async compile ([WILDCARD]compiler.ts:[WILDCARD])
at async tsCompilerOnMessage ([WILDCARD]compiler.ts:[WILDCARD])
at async workerMessageRecvCallback ([WILDCARD]runtime_worker.ts:[WILDCARD])

View file

@ -1,7 +1,11 @@
[WILDCARD]error: Uncaught URIError: relative import path "baz" not prefixed with / or ./ or ../ Imported from "[WILDCARD]/type_definitions/bar.d.ts"
[WILDCARD]dispatch_json.ts:[WILDCARD]
at unwrapResponse ($deno$/ops/dispatch_json.ts:[WILDCARD])
at sendSync ($deno$/ops/dispatch_json.ts:[WILDCARD])
at resolveModules ($deno$/compiler/imports.ts:[WILDCARD])
at processImports ($deno$/compiler/imports.ts:[WILDCARD])
at processImports ($deno$/compiler/imports.ts:[WILDCARD])
at unwrapResponse ([WILDCARD]ops/dispatch_json.ts:[WILDCARD])
at sendSync ([WILDCARD]ops/dispatch_json.ts:[WILDCARD])
at resolveModules ([WILDCARD]compiler/imports.ts:[WILDCARD])
at processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async processImports ([WILDCARD]compiler/imports.ts:[WILDCARD])
at async compile ([WILDCARD]compiler.ts:[WILDCARD])
at async tsCompilerOnMessage ([WILDCARD]compiler.ts:[WILDCARD])
at async workerMessageRecvCallback ([WILDCARD]runtime_worker.ts:[WILDCARD])

View file

@ -18,7 +18,7 @@ use std::fmt;
/// A `JSError` represents an exception coming from V8, with stack frames and
/// line numbers. The deno_cli crate defines another `JSError` type, which wraps
/// the one defined here, that adds source map support and colorful formatting.
/// the one defined here, that adds source map support and colorful formatting.
#[derive(Debug, PartialEq, Clone)]
pub struct JSError {
pub message: String,
@ -28,6 +28,12 @@ pub struct JSError {
pub start_column: Option<i64>,
pub end_column: Option<i64>,
pub frames: Vec<JSStackFrame>,
// TODO: Remove this field. It is required because JSError::from_v8_exception
// will generally (but not always) return stack frames passed from
// `prepareStackTrace()` which have already been source-mapped, and we need a
// flag saying not to do it again. Note: applies to `frames` but not
// `source_line`.
pub already_source_mapped: bool,
}
#[derive(Debug, PartialEq, Clone)]
@ -38,6 +44,18 @@ pub struct JSStackFrame {
pub function_name: String,
pub is_eval: bool,
pub is_constructor: bool,
pub is_async: bool,
// TODO(nayeemrmn): Support more CallSite fields.
}
fn get_property<'a>(
scope: &mut impl v8::ToLocal<'a>,
context: v8::Local<v8::Context>,
object: v8::Local<v8::Object>,
key: &str,
) -> Option<v8::Local<'a, v8::Value>> {
let key = v8::String::new(scope, key).unwrap();
object.get(scope, context, key.into())
}
impl JSError {
@ -53,23 +71,85 @@ impl JSError {
// handles below.
let mut hs = v8::HandleScope::new(scope);
let scope = hs.enter();
let context = scope.get_current_context().unwrap();
let context = { scope.get_current_context().unwrap() };
let msg = v8::Exception::create_message(scope, exception);
Self {
message: msg.get(scope).to_rust_string_lossy(scope),
script_resource_name: msg
.get_script_resource_name(scope)
.and_then(|v| v8::Local::<v8::String>::try_from(v).ok())
.map(|v| v.to_rust_string_lossy(scope)),
source_line: msg
.get_source_line(scope, context)
.map(|v| v.to_rust_string_lossy(scope)),
line_number: msg.get_line_number(context).and_then(|v| v.try_into().ok()),
start_column: msg.get_start_column().try_into().ok(),
end_column: msg.get_end_column().try_into().ok(),
frames: msg
let exception: Option<v8::Local<v8::Object>> =
exception.clone().try_into().ok();
let _ = exception.map(|e| get_property(scope, context, e, "stack"));
let maybe_call_sites = exception
.and_then(|e| get_property(scope, context, e, "__callSiteEvals"));
let maybe_call_sites: Option<v8::Local<v8::Array>> =
maybe_call_sites.and_then(|a| a.try_into().ok());
let already_source_mapped;
let frames = if let Some(call_sites) = maybe_call_sites {
already_source_mapped = true;
let mut output: Vec<JSStackFrame> = vec![];
for i in 0..call_sites.length() {
let call_site: v8::Local<v8::Object> = call_sites
.get_index(scope, context, i)
.unwrap()
.try_into()
.unwrap();
let line_number: v8::Local<v8::Integer> =
get_property(scope, context, call_site, "lineNumber")
.unwrap()
.try_into()
.unwrap();
let line_number = line_number.value() - 1;
let column_number: v8::Local<v8::Integer> =
get_property(scope, context, call_site, "columnNumber")
.unwrap()
.try_into()
.unwrap();
let column_number = column_number.value() - 1;
let file_name: Result<v8::Local<v8::String>, _> =
get_property(scope, context, call_site, "fileName")
.unwrap()
.try_into();
let file_name = file_name
.map_or_else(|_| String::new(), |s| s.to_rust_string_lossy(scope));
let function_name: Result<v8::Local<v8::String>, _> =
get_property(scope, context, call_site, "functionName")
.unwrap()
.try_into();
let function_name = function_name
.map_or_else(|_| String::new(), |s| s.to_rust_string_lossy(scope));
let is_constructor: v8::Local<v8::Boolean> =
get_property(scope, context, call_site, "isConstructor")
.unwrap()
.try_into()
.unwrap();
let is_constructor = is_constructor.is_true();
let is_eval: v8::Local<v8::Boolean> =
get_property(scope, context, call_site, "isEval")
.unwrap()
.try_into()
.unwrap();
let is_eval = is_eval.is_true();
let is_async: v8::Local<v8::Boolean> =
get_property(scope, context, call_site, "isAsync")
.unwrap()
.try_into()
.unwrap();
let is_async = is_async.is_true();
output.push(JSStackFrame {
line_number,
column: column_number,
script_name: file_name,
function_name,
is_constructor,
is_eval,
is_async,
});
}
output
} else {
already_source_mapped = false;
msg
.get_stack_trace(scope)
.map(|stack_trace| {
(0..stack_trace.get_frame_count())
@ -96,11 +176,28 @@ impl JSError {
.unwrap_or_else(|| "".to_owned()),
is_constructor: frame.is_constructor(),
is_eval: frame.is_eval(),
is_async: false,
}
})
.collect::<Vec<_>>()
})
.unwrap_or_else(Vec::<_>::new),
.unwrap_or_else(Vec::<_>::new)
};
Self {
message: msg.get(scope).to_rust_string_lossy(scope),
script_resource_name: msg
.get_script_resource_name(scope)
.and_then(|v| v8::Local::<v8::String>::try_from(v).ok())
.map(|v| v.to_rust_string_lossy(scope)),
source_line: msg
.get_source_line(scope, context)
.map(|v| v.to_rust_string_lossy(scope)),
line_number: msg.get_line_number(context).and_then(|v| v.try_into().ok()),
start_column: msg.get_start_column().try_into().ok(),
end_column: msg.get_end_column().try_into().ok(),
frames,
already_source_mapped,
}
}
}
@ -127,6 +224,8 @@ fn format_stack_frame(frame: &JSStackFrame) -> String {
format!(" at {} ({})", frame.function_name, source_loc)
} else if frame.is_eval {
format!(" at eval ({})", source_loc)
} else if frame.is_async {
format!(" at async ({})", source_loc)
} else {
format!(" at {}", source_loc)
}
@ -190,6 +289,7 @@ mod tests {
function_name: "foo".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 5,
@ -198,6 +298,7 @@ mod tests {
function_name: "qat".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 1,
@ -206,8 +307,10 @@ mod tests {
function_name: "".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
],
already_source_mapped: true,
};
let actual = js_error.to_string();
let expected = "Error: foo bar\n at foo (foo_bar.ts:5:17)\n at qat (bar_baz.ts:6:21)\n at deno_main.js:2:2";