Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(cli/fmt_errors): Format stack frames in prepareStackTrace() #4706

Merged
merged 3 commits into from
Apr 11, 2020
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
115 changes: 6 additions & 109 deletions cli/fmt_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::colors;
use crate::source_maps::apply_source_map;
use crate::source_maps::SourceMapGetter;
use deno_core::ErrBox;
use deno_core::JSStackFrame;
use std::error::Error;
use std::fmt;
use std::ops::Deref;
Expand All @@ -25,18 +24,13 @@ fn format_source_name(
script_name: String,
line_number: i64,
column: i64,
is_internal: bool,
) -> String {
let line_number = line_number + 1;
let column = column + 1;
if is_internal {
format!("{}:{}:{}", script_name, line_number, column)
} else {
let script_name_c = colors::cyan(script_name);
let line_c = colors::yellow(line_number.to_string());
let column_c = colors::yellow(column.to_string());
format!("{}:{}:{}", script_name_c, line_c, column_c)
}
let script_name_c = colors::cyan(script_name);
let line_c = colors::yellow(line_number.to_string());
let column_c = colors::yellow(column.to_string());
format!("{}:{}:{}", script_name_c, line_c, column_c)
}

/// Formats optional source, line number and column into a single string.
Expand All @@ -55,7 +49,6 @@ pub fn format_maybe_source_name(
script_name.unwrap(),
line_number.unwrap(),
column.unwrap(),
false,
)
}

Expand Down Expand Up @@ -127,54 +120,6 @@ pub fn format_error_message(msg: String) -> String {
format!("{} {}", preamble, msg)
}

fn format_stack_frame(frame: &JSStackFrame, is_internal_frame: bool) -> String {
// Note when we print to string, we change from 0-indexed to 1-indexed.
let function_name = if is_internal_frame {
colors::italic_bold_gray(frame.function_name.clone()).to_string()
} else {
colors::italic_bold(frame.function_name.clone()).to_string()
};
let mut source_loc = format_source_name(
frame.script_name.clone(),
frame.line_number,
frame.column,
is_internal_frame,
);

// Each chunk of styled text is auto-resetted on end,
// which make nesting not working.
// Explicitly specify color for each section.
let mut at_prefix = " at".to_owned();
if is_internal_frame {
at_prefix = colors::gray(at_prefix).to_string();
}
if !frame.function_name.is_empty() || frame.is_eval {
source_loc = format!("({})", source_loc); // wrap then style
}
if is_internal_frame {
source_loc = colors::gray(source_loc).to_string();
}
if !frame.function_name.is_empty() {
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)
}
}

/// Wrapper around deno_core::JSError which provides color to_string.
#[derive(Debug)]
pub struct JSError(deno_core::JSError);
Expand Down Expand Up @@ -251,10 +196,8 @@ impl fmt::Display for JSError {
self.format_source_name(),
self.format_source_line(0),
)?;

for frame in &self.0.frames {
let is_internal_frame = frame.script_name.starts_with("$deno$");
write!(f, "\n{}", format_stack_frame(&frame, is_internal_frame))?;
for formatted_frame in &self.0.formatted_frames {
write!(f, "\n at {}", formatted_frame)?;
}
Ok(())
}
Expand All @@ -267,52 +210,6 @@ mod tests {
use super::*;
use crate::colors::strip_ansi_codes;

#[test]
fn js_error_to_string() {
let core_js_error = deno_core::JSError {
message: "Error: foo bar".to_string(),
source_line: None,
script_resource_name: None,
line_number: None,
start_column: None,
end_column: None,
frames: vec![
JSStackFrame {
line_number: 4,
column: 16,
script_name: "foo_bar.ts".to_string(),
function_name: "foo".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 5,
column: 20,
script_name: "bar_baz.ts".to_string(),
function_name: "qat".to_string(),
is_eval: false,
is_constructor: false,
is_async: false,
},
JSStackFrame {
line_number: 1,
column: 1,
script_name: "deno_main.js".to_string(),
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);
let expected = "error: 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";
assert_eq!(actual, expected);
}

#[test]
fn test_format_none_source_name() {
let actual = format_maybe_source_name(None, None, None);
Expand Down
13 changes: 13 additions & 0 deletions cli/js/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,16 @@ export function white(str: string): string {
export function gray(str: string): string {
return run(str, code(90, 39));
}

// https:/chalk/ansi-regex/blob/2b56fb0c7a07108e5b54241e8faec160d393aedb/index.js
const ANSI_PATTERN = new RegExp(
[
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
].join("|"),
"g"
);

export function stripColor(string: string): string {
return string.replace(ANSI_PATTERN, "");
}
63 changes: 42 additions & 21 deletions cli/js/error_stack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
// Some of the code here is adapted directly from V8 and licensed under a BSD
// style license available here: https:/v8/v8/blob/24886f2d1c565287d33d71e4109a53bf0b54b75c/LICENSE.v8
import * as colors from "./colors.ts";
import { applySourceMap, Location } from "./ops/errors.ts";
import { assert } from "./util.ts";
import { exposeForTest } from "./internals.ts";
Expand Down Expand Up @@ -93,9 +94,12 @@ function getMethodCall(callSite: CallSite): string {
return result;
}

function getFileLocation(callSite: CallSite): string {
function getFileLocation(callSite: CallSite, isInternal = false): string {
const cyan = isInternal ? colors.gray : colors.cyan;
const yellow = isInternal ? colors.gray : colors.yellow;
const black = isInternal ? colors.gray : (s: string): string => s;
if (callSite.isNative()) {
return "native";
return cyan("native");
}

let result = "";
Expand All @@ -104,29 +108,32 @@ function getFileLocation(callSite: CallSite): string {
if (!fileName && callSite.isEval()) {
const evalOrigin = callSite.getEvalOrigin();
assert(evalOrigin != null);
result += `${evalOrigin}, `;
result += cyan(`${evalOrigin}, `);
}

if (fileName) {
result += fileName;
result += cyan(fileName);
} else {
result += "<anonymous>";
result += cyan("<anonymous>");
}

const lineNumber = callSite.getLineNumber();
if (lineNumber != null) {
result += `:${lineNumber}`;
result += `${black(":")}${yellow(lineNumber.toString())}`;

const columnNumber = callSite.getColumnNumber();
if (columnNumber != null) {
result += `:${columnNumber}`;
result += `${black(":")}${yellow(columnNumber.toString())}`;
}
}

return result;
}

function callSiteToString(callSite: CallSite): string {
function callSiteToString(callSite: CallSite, isInternal = false): string {
const cyan = isInternal ? colors.gray : colors.cyan;
const black = isInternal ? colors.gray : (s: string): string => s;

let result = "";
const functionName = callSite.getFunctionName();

Expand All @@ -137,29 +144,33 @@ function callSiteToString(callSite: CallSite): string {
const isMethodCall = !(isTopLevel || isConstructor);

if (isAsync) {
result += "async ";
result += colors.gray("async ");
}
if (isPromiseAll) {
result += `Promise.all (index ${callSite.getPromiseIndex()})`;
result += colors.bold(
colors.italic(black(`Promise.all (index ${callSite.getPromiseIndex()})`))
);
return result;
}
if (isMethodCall) {
result += getMethodCall(callSite);
result += colors.bold(colors.italic(black(getMethodCall(callSite))));
} else if (isConstructor) {
result += "new ";
result += colors.gray("new ");
if (functionName) {
result += functionName;
result += colors.bold(colors.italic(black(functionName)));
} else {
result += "<anonymous>";
result += cyan("<anonymous>");
}
} else if (functionName) {
result += functionName;
result += colors.bold(colors.italic(black(functionName)));
} else {
result += getFileLocation(callSite);
result += getFileLocation(callSite, isInternal);
return result;
}

result += ` (${getFileLocation(callSite)})`;
result += ` ${black("(")}${getFileLocation(callSite, isInternal)}${black(
")"
)}`;
return result;
}

Expand Down Expand Up @@ -207,7 +218,10 @@ function prepareStackTrace(
error: Error,
structuredStackTrace: CallSite[]
): string {
Object.defineProperty(error, "__callSiteEvals", { value: [] });
Object.defineProperties(error, {
__callSiteEvals: { value: [] },
__formattedFrames: { value: [] },
});
const errorString =
`${error.name}: ${error.message}\n` +
structuredStackTrace
Expand All @@ -230,16 +244,23 @@ function prepareStackTrace(
}
)
.map((callSite): string => {
const isInternal =
callSite.getFileName()?.startsWith("$deno$") ?? false;
const string = callSiteToString(callSite, isInternal);
const callSiteEv = Object.freeze(evaluateCallSite(callSite));
if (callSiteEv.lineNumber != null && callSiteEv.columnNumber != null) {
// @ts-ignore
error["__callSiteEvals"].push(callSiteEv);
error.__callSiteEvals.push(callSiteEv);
// @ts-ignore
error.__formattedFrames.push(string);
}
return ` at ${callSiteToString(callSite)}`;
return ` at ${colors.stripColor(string)}`;
})
.join("\n");
// @ts-ignore
Object.freeze(error["__callSiteEvals"]);
Object.freeze(error.__callSiteEvals);
// @ts-ignore
Object.freeze(error.__formattedFrames);
return errorString;
}

Expand Down
Loading