diff --git a/BUILD.gn b/BUILD.gn index 07375aa4f0..239a0f995d 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -31,6 +31,7 @@ main_extern = [ "$rust_build:libc", "$rust_build:log", "$rust_build:rand", + "$rust_build:regex", "$rust_build:remove_dir_all", "$rust_build:ring", "$rust_build:rustyline", diff --git a/src/ansi.rs b/src/ansi.rs new file mode 100644 index 0000000000..47bf6c9419 --- /dev/null +++ b/src/ansi.rs @@ -0,0 +1,69 @@ +use ansi_term::Color::Cyan; +use ansi_term::Color::Red; +use ansi_term::Color::Yellow; +use ansi_term::Style; +use regex::Regex; +use std::borrow::Cow; +use std::env; +use std::fmt; + +lazy_static! { + // STRIP_ANSI_RE and strip_ansi_codes are lifted from the "console" crate. + // Copyright 2017 Armin Ronacher . MIT License. + static ref STRIP_ANSI_RE: Regex = Regex::new( + r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]" + ).unwrap(); + static ref NO_COLOR: bool = { + env::var_os("NO_COLOR").is_some() + }; +} + +/// Helper function to strip ansi codes. +#[allow(dead_code)] +pub fn strip_ansi_codes(s: &str) -> Cow { + STRIP_ANSI_RE.replace_all(s, "") +} + +pub fn use_color() -> bool { + *NO_COLOR == false +} + +pub fn red_bold(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.bold().fg(Red); + } + style.paint(s) +} + +pub fn italic_bold(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.italic().bold(); + } + style.paint(s) +} + +pub fn yellow(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.fg(Yellow); + } + style.paint(s) +} + +pub fn cyan(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.fg(Cyan); + } + style.paint(s) +} + +pub fn bold(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.bold(); + } + style.paint(s) +} diff --git a/src/js_errors.rs b/src/js_errors.rs index 851d64be46..bc7f376830 100644 --- a/src/js_errors.rs +++ b/src/js_errors.rs @@ -9,6 +9,7 @@ // console.log(err.stack); // It would require calling into Rust from Error.prototype.prepareStackTrace. +use crate::ansi; use serde_json; use source_map_mappings::parse_mappings; use source_map_mappings::Bias; @@ -32,8 +33,8 @@ type CachedMaps = HashMap>; #[derive(Debug, PartialEq)] pub struct StackFrame { - pub line: u32, // zero indexed - pub column: u32, // zero indexed + pub line: i64, // zero indexed + pub column: i64, // zero indexed pub script_name: String, pub function_name: String, pub is_eval: bool, @@ -57,46 +58,69 @@ pub struct JSError { pub frames: Vec, } -impl ToString for StackFrame { - fn to_string(&self) -> String { +impl fmt::Display for StackFrame { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Note when we print to string, we change from 0-indexed to 1-indexed. - let (line, column) = (self.line + 1, self.column + 1); + let function_name = ansi::italic_bold(self.function_name.clone()); + let script_line_column = + format_script_line_column(&self.script_name, self.line, self.column); + if !self.function_name.is_empty() { - format!( - " at {} ({}:{}:{})", - self.function_name, self.script_name, line, column - ) + write!(f, " at {} ({})", function_name, script_line_column) } else if self.is_eval { - format!(" at eval ({}:{}:{})", self.script_name, line, column) + write!(f, " at eval ({})", script_line_column) } else { - format!(" at {}:{}:{}", self.script_name, line, column) + write!(f, " at {}", script_line_column) } } } +fn format_script_line_column( + script_name: &str, + line: i64, + column: i64, +) -> String { + // TODO match this style with how typescript displays errors. + let line = ansi::yellow((1 + line).to_string()); + let column = ansi::yellow((1 + column).to_string()); + let script_name = ansi::cyan(script_name.to_string()); + format!("{}:{}:{}", script_name, line, column) +} + impl fmt::Display for JSError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.script_resource_name.is_some() { let script_resource_name = self.script_resource_name.as_ref().unwrap(); // Avoid showing internal code from gen/bundle/main.js - if script_resource_name != "gen/bundle/main.js" { - write!(f, "{}", script_resource_name)?; - if self.line_number.is_some() { - write!( - f, - ":{}:{}", - self.line_number.unwrap(), - self.start_column.unwrap() - )?; + if script_resource_name != "gen/bundle/main.js" + && script_resource_name != "gen/bundle/compiler.js" + { + if self.line_number.is_some() && self.start_column.is_some() { + assert!(self.line_number.is_some()); assert!(self.start_column.is_some()); + let script_line_column = format_script_line_column( + script_resource_name, + self.line_number.unwrap() - 1, + self.start_column.unwrap() - 1, + ); + write!(f, "{}", script_line_column)?; } if self.source_line.is_some() { - write!(f, "\n{}\n\n", self.source_line.as_ref().unwrap())?; + write!(f, "\n{}\n", self.source_line.as_ref().unwrap())?; + let mut s = String::new(); + for i in 0..self.end_column.unwrap() { + if i >= self.start_column.unwrap() { + s.push('^'); + } else { + s.push(' '); + } + } + write!(f, "{}\n", ansi::red_bold(s))?; } } } - write!(f, "{}", &self.message)?; + write!(f, "{}", ansi::bold(self.message.clone()))?; for frame in &self.frames { write!(f, "\n{}", &frame.to_string())?; @@ -117,13 +141,13 @@ impl StackFrame { if !line_v.is_u64() { return None; } - let line = line_v.as_u64().unwrap() as u32; + let line = line_v.as_u64().unwrap() as i64; let column_v = &obj["column"]; if !column_v.is_u64() { return None; } - let column = column_v.as_u64().unwrap() as u32; + let column = column_v.as_u64().unwrap() as i64; let script_name_v = &obj["scriptName"]; if !script_name_v.is_string() { @@ -184,12 +208,16 @@ impl StackFrame { ) -> StackFrame { let maybe_sm = get_mappings(self.script_name.as_ref(), mappings_map, getter); - let frame_pos = (self.script_name.to_owned(), self.line, self.column); + let frame_pos = ( + self.script_name.to_owned(), + self.line as i64, + self.column as i64, + ); let (script_name, line, column) = match maybe_sm { None => frame_pos, Some(sm) => match sm.mappings.original_location_for( - self.line, - self.column, + self.line as u32, + self.column as u32, Bias::default(), ) { None => frame_pos, @@ -199,8 +227,8 @@ impl StackFrame { let orig_source = sm.sources[original.source as usize].clone(); ( orig_source, - original.original_line, - original.original_column, + original.original_line as i64, + original.original_column as i64, ) } }, @@ -379,6 +407,7 @@ fn get_mappings<'a>( #[cfg(test)] mod tests { use super::*; + use crate::ansi::strip_ansi_codes; fn error1() -> JSError { JSError { @@ -546,14 +575,20 @@ mod tests { #[test] fn stack_frame_to_string() { let e = error1(); - assert_eq!(" at foo (foo_bar.ts:5:17)", e.frames[0].to_string()); - assert_eq!(" at qat (bar_baz.ts:6:21)", e.frames[1].to_string()); + assert_eq!( + " at foo (foo_bar.ts:5:17)", + strip_ansi_codes(&e.frames[0].to_string()) + ); + assert_eq!( + " at qat (bar_baz.ts:6:21)", + strip_ansi_codes(&e.frames[1].to_string()) + ); } #[test] fn js_error_to_string() { let e = error1(); - assert_eq!("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", e.to_string()); + assert_eq!("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", strip_ansi_codes(&e.to_string())); } #[test] diff --git a/src/main.rs b/src/main.rs index 175464e31c..3daea0d31f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ extern crate futures; #[macro_use] extern crate serde_json; +mod ansi; pub mod compiler; pub mod deno_dir; pub mod errors; diff --git a/tests/async_error.ts.out b/tests/async_error.ts.out index 4a2b78f6ce..2bd958f9a4 100644 --- a/tests/async_error.ts.out +++ b/tests/async_error.ts.out @@ -3,7 +3,7 @@ before error world [WILDCARD]tests/async_error.ts:4:10 throw Error("error"); - + ^ Uncaught Error: error at foo ([WILDCARD]tests/async_error.ts:4:9) at [WILDCARD]tests/async_error.ts:7:1 diff --git a/tests/error_003_typescript.ts.out b/tests/error_003_typescript.ts.out index 20f5c95d0d..65bc33591c 100644 --- a/tests/error_003_typescript.ts.out +++ b/tests/error_003_typescript.ts.out @@ -1,10 +1,10 @@ -[WILDCARD]tests/error_003_typescript.tsILDCARD] - error TS2552: Cannot find name 'consol'. Did you mean 'console'? +[WILDCARD]tests/error_003_typescript.ts[WILDCARD] - error TS2552: Cannot find name 'consol'. Did you mean 'console'? [WILDCARD] consol.log("hello world!"); -[WILDCARD]~~~~~~ +[WILDCARD]~~~~~~ - $asset$/lib.deno_runtime.d.tsILDCARD] + $asset$/lib.deno_runtime.d.ts[WILDCARD] [WILDCARD]declare const console: consoleTypes.Console; -[WILDCARD]~~~~~~~ +[WILDCARD]~~~~~~~ [WILDCARD]'console' is declared here. diff --git a/tests/error_004_missing_module.ts.out b/tests/error_004_missing_module.ts.out index 049817ef6b..21cef70a89 100644 --- a/tests/error_004_missing_module.ts.out +++ b/tests/error_004_missing_module.ts.out @@ -1,5 +1,4 @@ Compiling [WILDCARD]tests/error_004_missing_module.ts -[WILDCARD] Uncaught NotFound: Cannot resolve module "bad-module.ts" from "[WILDCARD]/tests/error_004_missing_module.ts" at DenoError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeError ([WILDCARD]/js/errors.ts:[WILDCARD]) diff --git a/tests/error_006_import_ext_failure.ts.out b/tests/error_006_import_ext_failure.ts.out index d4c56ab5c3..7ba9ac6b43 100644 --- a/tests/error_006_import_ext_failure.ts.out +++ b/tests/error_006_import_ext_failure.ts.out @@ -1,5 +1,4 @@ Compiling [WILDCARD]tests/error_006_import_ext_failure.ts -[WILDCARD] Uncaught NotFound: Cannot resolve module "./non-existent" from "[WILDCARD]/tests/error_006_import_ext_failure.ts" at DenoError ([WILDCARD]/js/errors.ts:[WILDCARD]) at maybeError ([WILDCARD]/js/errors.ts:[WILDCARD]) diff --git a/tests/error_008_checkjs.js.out b/tests/error_008_checkjs.js.out index 6d899f9d6b..2341af23fb 100644 --- a/tests/error_008_checkjs.js.out +++ b/tests/error_008_checkjs.js.out @@ -1,5 +1,5 @@ [WILDCARD]tests/error_008_checkjs.js:2:0 consol.log("hello world!"); - +^ Uncaught ReferenceError: consol is not defined at [WILDCARD]tests/error_008_checkjs.js:2:1 diff --git a/tests/error_syntax.js.out b/tests/error_syntax.js.out index a106b95d8d..ac2f99f1fc 100644 --- a/tests/error_syntax.js.out +++ b/tests/error_syntax.js.out @@ -1,4 +1,4 @@ [WILDCARD]tests/error_syntax.js:3:5 (the following is a syntax error ^^ ! ) - + ^^^^^^^^^ Uncaught SyntaxError: Unexpected identifier diff --git a/tools/integration_tests.py b/tools/integration_tests.py index 66f59f0a03..dfb83e19ac 100755 --- a/tools/integration_tests.py +++ b/tools/integration_tests.py @@ -16,6 +16,12 @@ import argparse from util import root_path, tests_path, pattern_match, \ green_ok, red_failed, rmtree, executable_suffix + +def strip_ansi_codes(s): + ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + return ansi_escape.sub('', s) + + def read_test(file_name): with open(file_name, "r") as f: test_file = f.read() @@ -51,20 +57,20 @@ def integration_tests(deno_exe, test_filter = None): continue test_abs = os.path.join(tests_path, test_filename) - print "read_test", test_abs test = read_test(test_abs) exit_code = int(test.get("exit_code", 0)) args = test.get("args", "").split(" ") check_stderr = str2bool(test.get("check_stderr", "false")) - stderr = subprocess.STDOUT if check_stderr else None + + stderr = subprocess.STDOUT if check_stderr else open(os.devnull, 'w') output_abs = os.path.join(root_path, test.get("output", "")) with open(output_abs, 'r') as f: expected_out = f.read() cmd = [deno_exe] + args - print "test %s" % (test_filename) - print " ".join(cmd) + sys.stdout.write("tests/%s ... " % (test_filename)) + sys.stdout.flush() actual_code = 0 try: actual_out = subprocess.check_output( @@ -80,14 +86,16 @@ def integration_tests(deno_exe, test_filter = None): print actual_out sys.exit(1) + actual_out = strip_ansi_codes(actual_out) + if pattern_match(expected_out, actual_out) != True: - print "... " + red_failed() + print red_failed() print "Expected output does not match actual." print "Expected output: \n" + expected_out print "Actual output: \n" + actual_out sys.exit(1) - print "... " + green_ok() + print green_ok() def main(): parser = argparse.ArgumentParser()