Skip to content

Commit

Permalink
refactor(cli/fmt_errors): Format stack frames in prepareStackTrace() (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nayeemrmn authored Apr 11, 2020
1 parent 2feb661 commit 2b362be
Show file tree
Hide file tree
Showing 25 changed files with 224 additions and 427 deletions.
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

0 comments on commit 2b362be

Please sign in to comment.