diff --git a/cli/ansi.rs b/cli/ansi.rs index b9e9fe12351781..32936ace047065 100644 --- a/cli/ansi.rs +++ b/cli/ansi.rs @@ -79,14 +79,6 @@ pub fn red(s: String) -> impl fmt::Display { style.paint(s) } -pub fn grey(s: String) -> impl fmt::Display { - let mut style = Style::new(); - if use_color() { - style = style.fg(Fixed(8)); - } - style.paint(s) -} - pub fn bold(s: String) -> impl fmt::Display { let mut style = Style::new(); if use_color() { diff --git a/cli/compiler.rs b/cli/compiler.rs index de5b5c609fb49a..b6fb0375cb1657 100644 --- a/cli/compiler.rs +++ b/cli/compiler.rs @@ -1,4 +1,6 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::deno_error::err_check; +use crate::deno_error::DenoError; use crate::diagnostics::Diagnostic; use crate::msg; use crate::resources; @@ -6,7 +8,6 @@ use crate::startup_data; use crate::state::*; use crate::tokio_util; use crate::worker::Worker; -use deno::js_check; use deno::Buf; use futures::Future; use futures::Stream; @@ -92,7 +93,7 @@ pub fn bundle_async( state: ThreadSafeState, module_name: String, out_file: String, -) -> impl Future { +) -> impl Future { debug!( "Invoking the compiler to bundle. module_name: {}", module_name @@ -112,9 +113,9 @@ pub fn bundle_async( // as was done previously. state.clone(), ); - js_check(worker.execute("denoMain()")); - js_check(worker.execute("workerMain()")); - js_check(worker.execute("compilerMain()")); + err_check(worker.execute("denoMain()")); + err_check(worker.execute("workerMain()")); + err_check(worker.execute("compilerMain()")); let resource = worker.state.resource.clone(); let compiler_rid = resource.rid; @@ -140,7 +141,7 @@ pub fn bundle_async( let json_str = std::str::from_utf8(&msg).unwrap(); debug!("Message: {}", json_str); if let Some(diagnostics) = Diagnostic::from_emit_result(json_str) { - return Err(diagnostics); + return Err(DenoError::from(diagnostics)); } } @@ -152,7 +153,7 @@ pub fn bundle_async( pub fn compile_async( state: ThreadSafeState, module_meta_data: &ModuleMetaData, -) -> impl Future { +) -> impl Future { let module_name = module_meta_data.module_name.clone(); debug!( @@ -174,9 +175,9 @@ pub fn compile_async( // as was done previously. state.clone(), ); - js_check(worker.execute("denoMain()")); - js_check(worker.execute("workerMain()")); - js_check(worker.execute("compilerMain()")); + err_check(worker.execute("denoMain()")); + err_check(worker.execute("workerMain()")); + err_check(worker.execute("compilerMain()")); let compiling_job = state.progress.add(format!("Compiling {}", module_name)); @@ -205,7 +206,7 @@ pub fn compile_async( let json_str = std::str::from_utf8(&msg).unwrap(); debug!("Message: {}", json_str); if let Some(diagnostics) = Diagnostic::from_emit_result(json_str) { - return Err(diagnostics); + return Err(DenoError::from(diagnostics)); } } @@ -235,7 +236,7 @@ pub fn compile_async( pub fn compile_sync( state: ThreadSafeState, module_meta_data: &ModuleMetaData, -) -> Result { +) -> Result { tokio_util::block_on(compile_async(state, module_meta_data)) } @@ -306,6 +307,6 @@ mod tests { ]); let out = bundle_async(state, module_name, String::from("$deno$/bundle.js")); - assert_eq!(tokio_util::block_on(out), Ok(())); + assert!(tokio_util::block_on(out).is_ok()); } } diff --git a/cli/deno_dir.rs b/cli/deno_dir.rs index c0ee051b838d74..ba0217bbe52eb9 100644 --- a/cli/deno_dir.rs +++ b/cli/deno_dir.rs @@ -1,14 +1,14 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. use crate::compiler::ModuleMetaData; -use crate::errors; -use crate::errors::DenoError; -use crate::errors::DenoResult; -use crate::errors::ErrorKind; +use crate::deno_error; +use crate::deno_error::DenoError; +use crate::deno_error::DenoResult; +use crate::deno_error::ErrorKind; use crate::fs as deno_fs; use crate::http_util; -use crate::js_errors::SourceMapGetter; use crate::msg; use crate::progress::Progress; +use crate::source_maps::SourceMapGetter; use crate::tokio_util; use crate::version; use dirs; @@ -152,7 +152,7 @@ impl DenoDir { referrer: &str, use_cache: bool, no_fetch: bool, - ) -> impl Future { + ) -> impl Future { debug!( "fetch_module_meta_data. specifier {} referrer {}", specifier, referrer @@ -187,7 +187,7 @@ impl DenoDir { Err(err) => { if err.kind() == ErrorKind::NotFound { // For NotFound, change the message to something better. - return Err(errors::new( + return Err(deno_error::new( ErrorKind::NotFound, format!( "Cannot resolve module \"{}\" from \"{}\"", @@ -255,7 +255,7 @@ impl DenoDir { referrer: &str, use_cache: bool, no_fetch: bool, - ) -> Result { + ) -> Result { tokio_util::block_on( self .fetch_module_meta_data_async(specifier, referrer, use_cache, no_fetch), @@ -349,6 +349,20 @@ impl SourceMapGetter for DenoDir { }, } } + + fn get_source_line(&self, script_name: &str, line: usize) -> Option { + match self.fetch_module_meta_data(script_name, ".", true, true) { + Ok(out) => match str::from_utf8(&out.source_code) { + Ok(v) => { + let lines: Vec<&str> = v.lines().collect(); + assert!(lines.len() > line); + Some(lines[line].to_string()) + } + _ => None, + }, + _ => None, + } + } } /// This fetches source code, locally or remotely. diff --git a/cli/deno_error.rs b/cli/deno_error.rs new file mode 100644 index 00000000000000..6e14f39025c2c0 --- /dev/null +++ b/cli/deno_error.rs @@ -0,0 +1,538 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::diagnostics; +use crate::fmt_errors::JSErrorColor; +use crate::import_map; +pub use crate::msg::ErrorKind; +use crate::resolve_addr::ResolveAddrError; +use crate::source_maps::apply_source_map; +use crate::source_maps::SourceMapGetter; +use deno::JSError; +use hyper; +#[cfg(unix)] +use nix::{errno::Errno, Error as UnixError}; +use std; +use std::fmt; +use std::io; +use std::str; +use url; + +pub type DenoResult = std::result::Result; + +#[derive(Debug)] +pub struct DenoError { + repr: Repr, +} + +#[derive(Debug)] +enum Repr { + Simple(ErrorKind, String), + IoErr(io::Error), + UrlErr(url::ParseError), + HyperErr(hyper::Error), + ImportMapErr(import_map::ImportMapError), + Diagnostic(diagnostics::Diagnostic), + JSError(JSError), +} + +/// Create a new simple DenoError. +pub fn new(kind: ErrorKind, msg: String) -> DenoError { + DenoError { + repr: Repr::Simple(kind, msg), + } +} + +impl DenoError { + pub fn kind(&self) -> ErrorKind { + match self.repr { + Repr::Simple(kind, ref _msg) => kind, + // Repr::Simple(kind) => kind, + Repr::IoErr(ref err) => { + use std::io::ErrorKind::*; + match err.kind() { + NotFound => ErrorKind::NotFound, + PermissionDenied => ErrorKind::PermissionDenied, + ConnectionRefused => ErrorKind::ConnectionRefused, + ConnectionReset => ErrorKind::ConnectionReset, + ConnectionAborted => ErrorKind::ConnectionAborted, + NotConnected => ErrorKind::NotConnected, + AddrInUse => ErrorKind::AddrInUse, + AddrNotAvailable => ErrorKind::AddrNotAvailable, + BrokenPipe => ErrorKind::BrokenPipe, + AlreadyExists => ErrorKind::AlreadyExists, + WouldBlock => ErrorKind::WouldBlock, + InvalidInput => ErrorKind::InvalidInput, + InvalidData => ErrorKind::InvalidData, + TimedOut => ErrorKind::TimedOut, + Interrupted => ErrorKind::Interrupted, + WriteZero => ErrorKind::WriteZero, + Other => ErrorKind::Other, + UnexpectedEof => ErrorKind::UnexpectedEof, + _ => unreachable!(), + } + } + Repr::UrlErr(ref err) => { + use url::ParseError::*; + match err { + EmptyHost => ErrorKind::EmptyHost, + IdnaError => ErrorKind::IdnaError, + InvalidPort => ErrorKind::InvalidPort, + InvalidIpv4Address => ErrorKind::InvalidIpv4Address, + InvalidIpv6Address => ErrorKind::InvalidIpv6Address, + InvalidDomainCharacter => ErrorKind::InvalidDomainCharacter, + RelativeUrlWithoutBase => ErrorKind::RelativeUrlWithoutBase, + RelativeUrlWithCannotBeABaseBase => { + ErrorKind::RelativeUrlWithCannotBeABaseBase + } + SetHostOnCannotBeABaseUrl => ErrorKind::SetHostOnCannotBeABaseUrl, + Overflow => ErrorKind::Overflow, + } + } + Repr::HyperErr(ref err) => { + // For some reason hyper::errors::Kind is private. + if err.is_parse() { + ErrorKind::HttpParse + } else if err.is_user() { + ErrorKind::HttpUser + } else if err.is_canceled() { + ErrorKind::HttpCanceled + } else if err.is_closed() { + ErrorKind::HttpClosed + } else { + ErrorKind::HttpOther + } + } + Repr::ImportMapErr(ref _err) => ErrorKind::ImportMapError, + Repr::Diagnostic(ref _err) => ErrorKind::Diagnostic, + Repr::JSError(ref _err) => ErrorKind::JSError, + } + } + + pub fn apply_source_map(self, getter: &G) -> Self { + if let Repr::JSError(js_error) = self.repr { + return DenoError { + repr: Repr::JSError(apply_source_map(&js_error, getter)), + }; + } else { + panic!("attempt to apply source map an unremappable error") + } + } +} + +impl fmt::Display for DenoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.repr { + Repr::Simple(_kind, ref err_str) => f.pad(err_str), + Repr::IoErr(ref err) => err.fmt(f), + Repr::UrlErr(ref err) => err.fmt(f), + Repr::HyperErr(ref err) => err.fmt(f), + Repr::ImportMapErr(ref err) => f.pad(&err.msg), + Repr::Diagnostic(ref err) => err.fmt(f), + Repr::JSError(ref err) => JSErrorColor(err).fmt(f), + } + } +} + +impl std::error::Error for DenoError { + fn description(&self) -> &str { + match self.repr { + Repr::Simple(_kind, ref msg) => msg.as_str(), + Repr::IoErr(ref err) => err.description(), + Repr::UrlErr(ref err) => err.description(), + Repr::HyperErr(ref err) => err.description(), + Repr::ImportMapErr(ref err) => &err.msg, + Repr::Diagnostic(ref err) => &err.items[0].message, + Repr::JSError(ref err) => &err.description(), + } + } + + fn cause(&self) -> Option<&dyn std::error::Error> { + match self.repr { + Repr::Simple(_kind, ref _msg) => None, + Repr::IoErr(ref err) => Some(err), + Repr::UrlErr(ref err) => Some(err), + Repr::HyperErr(ref err) => Some(err), + Repr::ImportMapErr(ref _err) => None, + Repr::Diagnostic(ref _err) => None, + Repr::JSError(ref err) => Some(err), + } + } +} + +impl From for DenoError { + #[inline] + fn from(err: io::Error) -> Self { + Self { + repr: Repr::IoErr(err), + } + } +} + +impl From for DenoError { + #[inline] + fn from(err: url::ParseError) -> Self { + Self { + repr: Repr::UrlErr(err), + } + } +} + +impl From for DenoError { + #[inline] + fn from(err: hyper::Error) -> Self { + Self { + repr: Repr::HyperErr(err), + } + } +} + +impl From for DenoError { + fn from(e: ResolveAddrError) -> Self { + match e { + ResolveAddrError::Syntax => Self { + repr: Repr::Simple( + ErrorKind::InvalidInput, + "invalid address syntax".to_string(), + ), + }, + ResolveAddrError::Resolution(io_err) => Self { + repr: Repr::IoErr(io_err), + }, + } + } +} + +#[cfg(unix)] +impl From for DenoError { + fn from(e: UnixError) -> Self { + match e { + UnixError::Sys(Errno::EPERM) => Self { + repr: Repr::Simple( + ErrorKind::PermissionDenied, + Errno::EPERM.desc().to_owned(), + ), + }, + UnixError::Sys(Errno::EINVAL) => Self { + repr: Repr::Simple( + ErrorKind::InvalidInput, + Errno::EINVAL.desc().to_owned(), + ), + }, + UnixError::Sys(Errno::ENOENT) => Self { + repr: Repr::Simple( + ErrorKind::NotFound, + Errno::ENOENT.desc().to_owned(), + ), + }, + UnixError::Sys(err) => Self { + repr: Repr::Simple(ErrorKind::UnixError, err.desc().to_owned()), + }, + _ => Self { + repr: Repr::Simple(ErrorKind::Other, format!("{}", e)), + }, + } + } +} + +impl From for DenoError { + fn from(err: import_map::ImportMapError) -> Self { + Self { + repr: Repr::ImportMapErr(err), + } + } +} + +impl From for DenoError { + fn from(diagnostic: diagnostics::Diagnostic) -> Self { + Self { + repr: Repr::Diagnostic(diagnostic), + } + } +} + +impl From for DenoError { + fn from(err: JSError) -> Self { + Self { + repr: Repr::JSError(err), + } + } +} + +pub fn bad_resource() -> DenoError { + new(ErrorKind::BadResource, String::from("bad resource id")) +} + +pub fn permission_denied() -> DenoError { + new( + ErrorKind::PermissionDenied, + String::from("permission denied"), + ) +} + +pub fn op_not_implemented() -> DenoError { + new( + ErrorKind::OpNotAvailable, + String::from("op not implemented"), + ) +} + +pub fn worker_init_failed() -> DenoError { + // TODO(afinch7) pass worker error data through here + new( + ErrorKind::WorkerInitFailed, + String::from("worker init failed"), + ) +} + +pub fn no_buffer_specified() -> DenoError { + new(ErrorKind::InvalidInput, String::from("no buffer specified")) +} + +pub fn no_async_support() -> DenoError { + new( + ErrorKind::NoAsyncSupport, + String::from("op doesn't support async calls"), + ) +} + +pub fn no_sync_support() -> DenoError { + new( + ErrorKind::NoSyncSupport, + String::from("op doesn't support sync calls"), + ) +} + +pub fn err_check(r: Result) { + if let Err(e) = r { + panic!(e.to_string()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ansi::strip_ansi_codes; + use crate::diagnostics::Diagnostic; + use crate::diagnostics::DiagnosticCategory; + use crate::diagnostics::DiagnosticItem; + use crate::import_map::ImportMapError; + use deno::StackFrame; + + fn js_error() -> JSError { + JSError { + message: "Error: foo bar".to_string(), + source_line: None, + script_resource_name: None, + line_number: None, + start_position: None, + end_position: None, + error_level: None, + start_column: None, + end_column: None, + frames: vec![ + StackFrame { + line: 4, + column: 16, + script_name: "foo_bar.ts".to_string(), + function_name: "foo".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 5, + column: 20, + script_name: "bar_baz.ts".to_string(), + function_name: "qat".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 1, + column: 1, + script_name: "deno_main.js".to_string(), + function_name: "".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + ], + } + } + + fn diagnostic() -> Diagnostic { + Diagnostic { + items: vec![ + DiagnosticItem { + message: "Example 1".to_string(), + message_chain: None, + code: 2322, + category: DiagnosticCategory::Error, + start_position: Some(267), + end_position: Some(273), + source_line: Some(" values: o => [".to_string()), + line_number: Some(18), + script_resource_name: Some( + "deno/tests/complex_diagnostics.ts".to_string(), + ), + start_column: Some(2), + end_column: Some(8), + related_information: None, + }, + DiagnosticItem { + message: "Example 2".to_string(), + message_chain: None, + code: 2000, + category: DiagnosticCategory::Error, + start_position: Some(2), + end_position: Some(2), + source_line: Some(" values: undefined,".to_string()), + line_number: Some(128), + script_resource_name: Some("/foo/bar.ts".to_string()), + start_column: Some(2), + end_column: Some(8), + related_information: None, + }, + ], + } + } + + struct MockSourceMapGetter {} + + impl SourceMapGetter for MockSourceMapGetter { + fn get_source_map(&self, _script_name: &str) -> Option> { + Some(vec![]) + } + + fn get_source_line( + &self, + _script_name: &str, + _line: usize, + ) -> Option { + None + } + } + + fn io_error() -> io::Error { + io::Error::from(io::ErrorKind::NotFound) + } + + fn url_error() -> url::ParseError { + url::ParseError::EmptyHost + } + + fn import_map_error() -> ImportMapError { + ImportMapError { + msg: "an import map error".to_string(), + } + } + + #[test] + fn test_simple_error() { + let err = new(ErrorKind::NoError, "foo".to_string()); + assert_eq!(err.kind(), ErrorKind::NoError); + assert_eq!(err.to_string(), "foo"); + } + + #[test] + fn test_io_error() { + let err = DenoError::from(io_error()); + assert_eq!(err.kind(), ErrorKind::NotFound); + assert_eq!(err.to_string(), "entity not found"); + } + + #[test] + fn test_url_error() { + let err = DenoError::from(url_error()); + assert_eq!(err.kind(), ErrorKind::EmptyHost); + assert_eq!(err.to_string(), "empty host"); + } + + // TODO find a way to easily test tokio errors and unix errors + + #[test] + fn test_diagnostic() { + let err = DenoError::from(diagnostic()); + assert_eq!(err.kind(), ErrorKind::Diagnostic); + assert_eq!(strip_ansi_codes(&err.to_string()), "error TS2322: Example 1\n\n► deno/tests/complex_diagnostics.ts:19:3\n\n19 values: o => [\n ~~~~~~\n\nerror TS2000: Example 2\n\n► /foo/bar.ts:129:3\n\n129 values: undefined,\n ~~~~~~\n\n\nFound 2 errors.\n"); + } + + #[test] + fn test_js_error() { + let err = DenoError::from(js_error()); + assert_eq!(err.kind(), ErrorKind::JSError); + assert_eq!(strip_ansi_codes(&err.to_string()), "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"); + } + + #[test] + fn test_import_map_error() { + let err = DenoError::from(import_map_error()); + assert_eq!(err.kind(), ErrorKind::ImportMapError); + assert_eq!(err.to_string(), "an import map error"); + } + + #[test] + fn test_bad_resource() { + let err = bad_resource(); + assert_eq!(err.kind(), ErrorKind::BadResource); + assert_eq!(err.to_string(), "bad resource id"); + } + + #[test] + fn test_permission_denied() { + let err = permission_denied(); + assert_eq!(err.kind(), ErrorKind::PermissionDenied); + assert_eq!(err.to_string(), "permission denied"); + } + + #[test] + fn test_op_not_implemented() { + let err = op_not_implemented(); + assert_eq!(err.kind(), ErrorKind::OpNotAvailable); + assert_eq!(err.to_string(), "op not implemented"); + } + + #[test] + fn test_worker_init_failed() { + let err = worker_init_failed(); + assert_eq!(err.kind(), ErrorKind::WorkerInitFailed); + assert_eq!(err.to_string(), "worker init failed"); + } + + #[test] + fn test_no_buffer_specified() { + let err = no_buffer_specified(); + assert_eq!(err.kind(), ErrorKind::InvalidInput); + assert_eq!(err.to_string(), "no buffer specified"); + } + + #[test] + fn test_no_async_support() { + let err = no_async_support(); + assert_eq!(err.kind(), ErrorKind::NoAsyncSupport); + assert_eq!(err.to_string(), "op doesn't support async calls"); + } + + #[test] + fn test_no_sync_support() { + let err = no_sync_support(); + assert_eq!(err.kind(), ErrorKind::NoSyncSupport); + assert_eq!(err.to_string(), "op doesn't support sync calls"); + } + + #[test] + #[should_panic] + fn test_apply_source_map_invalid() { + let getter = MockSourceMapGetter {}; + let err = new(ErrorKind::NotFound, "not found".to_string()); + err.apply_source_map(&getter); + } + + #[test] + #[should_panic] + fn test_err_check() { + err_check( + Err(new(ErrorKind::NotFound, "foo".to_string())) as Result<(), DenoError> + ); + } +} diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs index af384f277b386a..69b58459e330cd 100644 --- a/cli/diagnostics.rs +++ b/cli/diagnostics.rs @@ -2,20 +2,13 @@ //! This module encodes TypeScript errors (diagnostics) into Rust structs and //! contains code for printing them to the console. use crate::ansi; +use crate::fmt_errors::format_maybe_source_line; +use crate::fmt_errors::format_maybe_source_name; +use crate::fmt_errors::DisplayFormatter; use serde_json; use serde_json::value::Value; use std::fmt; -// A trait which specifies parts of a diagnostic like item needs to be able to -// generate to conform its display to other diagnostic like items -pub trait DisplayFormatter { - fn format_category_and_code(&self) -> String; - fn format_message(&self, level: usize) -> String; - fn format_related_info(&self) -> String; - fn format_source_line(&self, level: usize) -> String; - fn format_source_name(&self, level: usize) -> String; -} - #[derive(Debug, PartialEq, Clone)] pub struct Diagnostic { pub items: Vec, @@ -179,23 +172,21 @@ impl DiagnosticItem { } } -// TODO should chare logic with cli/js_errors, possibly with JSError -// implementing the `DisplayFormatter` trait. impl DisplayFormatter for DiagnosticItem { fn format_category_and_code(&self) -> String { let category = match self.category { DiagnosticCategory::Error => { - format!("- {}", ansi::red("error".to_string())) + format!("{}", ansi::red_bold("error".to_string())) } - DiagnosticCategory::Warning => "- warn".to_string(), - DiagnosticCategory::Debug => "- debug".to_string(), - DiagnosticCategory::Info => "- info".to_string(), + DiagnosticCategory::Warning => "warn".to_string(), + DiagnosticCategory::Debug => "debug".to_string(), + DiagnosticCategory::Info => "info".to_string(), _ => "".to_string(), }; - let code = ansi::grey(format!(" TS{}:", self.code.to_string())).to_string(); + let code = ansi::bold(format!(" TS{}", self.code.to_string())).to_string(); - format!("{}{} ", category, code) + format!("{}{}: ", category, code) } fn format_message(&self, level: usize) -> String { @@ -229,10 +220,10 @@ impl DisplayFormatter for DiagnosticItem { for related_diagnostic in related_information { let rd = &related_diagnostic; s.push_str(&format!( - "\n{}{}{}\n", - rd.format_source_name(2), + "\n{}\n\n ► {}{}\n", + rd.format_message(2), + rd.format_source_name(), rd.format_source_line(4), - rd.format_message(4), )); } @@ -240,74 +231,24 @@ impl DisplayFormatter for DiagnosticItem { } fn format_source_line(&self, level: usize) -> String { - if self.source_line.is_none() { - return "".to_string(); - } - - let source_line = self.source_line.as_ref().unwrap(); - // sometimes source_line gets set with an empty string, which then outputs - // an empty source line when displayed, so need just short circuit here - if source_line.is_empty() { - return "".to_string(); - } - - assert!(self.line_number.is_some()); - assert!(self.start_column.is_some()); - assert!(self.end_column.is_some()); - let line = (1 + self.line_number.unwrap()).to_string(); - let line_color = ansi::black_on_white(line.to_string()); - let line_len = line.clone().len(); - let line_padding = - ansi::black_on_white(format!("{:indent$}", "", indent = line_len)) - .to_string(); - let mut s = String::new(); - let start_column = self.start_column.unwrap(); - let end_column = self.end_column.unwrap(); - // TypeScript uses `~` always, but V8 would utilise `^` always, even when - // doing ranges, so here, if we only have one marker (very common with V8 - // errors) we will use `^` instead. - let underline_char = if (end_column - start_column) <= 1 { - '^' - } else { - '~' - }; - for i in 0..end_column { - if i >= start_column { - s.push(underline_char); - } else { - s.push(' '); - } - } - let color_underline = match self.category { - DiagnosticCategory::Error => ansi::red(s).to_string(), - _ => ansi::cyan(s).to_string(), - }; - - let indent = format!("{:indent$}", "", indent = level); - - format!( - "\n\n{}{} {}\n{}{} {}\n", - indent, line_color, source_line, indent, line_padding, color_underline + format_maybe_source_line( + self.source_line.clone(), + self.line_number, + self.start_column, + self.end_column, + match self.category { + DiagnosticCategory::Error => true, + _ => false, + }, + level, ) } - fn format_source_name(&self, level: usize) -> String { - if self.script_resource_name.is_none() { - return "".to_string(); - } - - let script_name = ansi::cyan(self.script_resource_name.clone().unwrap()); - assert!(self.line_number.is_some()); - assert!(self.start_column.is_some()); - let line = ansi::yellow((1 + self.line_number.unwrap()).to_string()); - let column = ansi::yellow((1 + self.start_column.unwrap()).to_string()); - format!( - "{:indent$}{}:{}:{} ", - "", - script_name, - line, - column, - indent = level + fn format_source_name(&self) -> String { + format_maybe_source_name( + self.script_resource_name.clone(), + self.line_number, + self.start_column, ) } } @@ -316,15 +257,13 @@ impl fmt::Display for DiagnosticItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "{}{}{}{}{}", - self.format_source_name(0), + "{}{}\n\n► {}{}{}", self.format_category_and_code(), self.format_message(0), + self.format_source_name(), self.format_source_line(0), self.format_related_info(), - )?; - - Ok(()) + ) } } @@ -655,14 +594,14 @@ mod tests { #[test] fn diagnostic_to_string1() { let d = diagnostic1(); - let expected = "deno/tests/complex_diagnostics.ts:19:3 - error TS2322: Type \'(o: T) => { v: any; f: (x: B) => string; }[]\' is not assignable to type \'(r: B) => Value[]\'.\n Types of parameters \'o\' and \'r\' are incompatible.\n Type \'B\' is not assignable to type \'T\'.\n\n19 values: o => [\n ~~~~~~\n\n deno/tests/complex_diagnostics.ts:7:3 \n\n 7 values?: (r: T) => Array>;\n ~~~~~~\n The expected type comes from property \'values\' which is declared here on type \'SettingsInterface\'\n"; + let expected = "error TS2322: Type \'(o: T) => { v: any; f: (x: B) => string; }[]\' is not assignable to type \'(r: B) => Value[]\'.\n Types of parameters \'o\' and \'r\' are incompatible.\n Type \'B\' is not assignable to type \'T\'.\n\n► deno/tests/complex_diagnostics.ts:19:3\n\n19 values: o => [\n ~~~~~~\n\n The expected type comes from property \'values\' which is declared here on type \'SettingsInterface\'\n\n ► deno/tests/complex_diagnostics.ts:7:3\n\n 7 values?: (r: T) => Array>;\n ~~~~~~\n\n"; assert_eq!(expected, strip_ansi_codes(&d.to_string())); } #[test] fn diagnostic_to_string2() { let d = diagnostic2(); - let expected = "deno/tests/complex_diagnostics.ts:19:3 - error TS2322: Example 1\n\n19 values: o => [\n ~~~~~~\n\n/foo/bar.ts:129:3 - error TS2000: Example 2\n\n129 values: undefined,\n ~~~~~~\n\n\nFound 2 errors.\n"; + let expected = "error TS2322: Example 1\n\n► deno/tests/complex_diagnostics.ts:19:3\n\n19 values: o => [\n ~~~~~~\n\nerror TS2000: Example 2\n\n► /foo/bar.ts:129:3\n\n129 values: undefined,\n ~~~~~~\n\n\nFound 2 errors.\n"; assert_eq!(expected, strip_ansi_codes(&d.to_string())); } } diff --git a/cli/dispatch_minimal.rs b/cli/dispatch_minimal.rs index e00203576e6702..bda9ea535eda65 100644 --- a/cli/dispatch_minimal.rs +++ b/cli/dispatch_minimal.rs @@ -124,27 +124,27 @@ pub fn dispatch_minimal( } mod ops { - use crate::errors; + use crate::deno_error; use crate::resources; use crate::tokio_write; use deno::PinnedBuf; use futures::Future; - type MinimalOp = dyn Future + Send; + type MinimalOp = dyn Future + Send; pub fn read(rid: i32, zero_copy: Option) -> Box { debug!("read rid={}", rid); let zero_copy = match zero_copy { None => { - return Box::new(futures::future::err(errors::no_buffer_specified())) + return Box::new(futures::future::err(deno_error::no_buffer_specified())) } Some(buf) => buf, }; match resources::lookup(rid as u32) { - None => Box::new(futures::future::err(errors::bad_resource())), + None => Box::new(futures::future::err(deno_error::bad_resource())), Some(resource) => Box::new( tokio::io::read(resource, zero_copy) - .map_err(errors::DenoError::from) + .map_err(deno_error::DenoError::from) .and_then(move |(_resource, _buf, nread)| Ok(nread as i32)), ), } @@ -154,15 +154,15 @@ mod ops { debug!("write rid={}", rid); let zero_copy = match zero_copy { None => { - return Box::new(futures::future::err(errors::no_buffer_specified())) + return Box::new(futures::future::err(deno_error::no_buffer_specified())) } Some(buf) => buf, }; match resources::lookup(rid as u32) { - None => Box::new(futures::future::err(errors::bad_resource())), + None => Box::new(futures::future::err(deno_error::bad_resource())), Some(resource) => Box::new( tokio_write::write(resource, zero_copy) - .map_err(errors::DenoError::from) + .map_err(deno_error::DenoError::from) .and_then(move |(_resource, _buf, nwritten)| Ok(nwritten as i32)), ), } diff --git a/cli/errors.rs b/cli/errors.rs deleted file mode 100644 index 67eb54ea79df5d..00000000000000 --- a/cli/errors.rs +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -use crate::import_map::ImportMapError; -use crate::js_errors::JSErrorColor; -pub use crate::msg::ErrorKind; -use crate::resolve_addr::ResolveAddrError; -use deno::JSError; -use hyper; -#[cfg(unix)] -use nix::{errno::Errno, Error as UnixError}; -use std; -use std::fmt; -use std::io; -use url; - -pub type DenoResult = std::result::Result; - -#[derive(Debug)] -pub struct DenoError { - repr: Repr, -} - -#[derive(Debug)] -enum Repr { - Simple(ErrorKind, String), - IoErr(io::Error), - UrlErr(url::ParseError), - HyperErr(hyper::Error), - ImportMapErr(ImportMapError), -} - -pub fn new(kind: ErrorKind, msg: String) -> DenoError { - DenoError { - repr: Repr::Simple(kind, msg), - } -} - -impl DenoError { - pub fn kind(&self) -> ErrorKind { - match self.repr { - Repr::Simple(kind, ref _msg) => kind, - // Repr::Simple(kind) => kind, - Repr::IoErr(ref err) => { - use std::io::ErrorKind::*; - match err.kind() { - NotFound => ErrorKind::NotFound, - PermissionDenied => ErrorKind::PermissionDenied, - ConnectionRefused => ErrorKind::ConnectionRefused, - ConnectionReset => ErrorKind::ConnectionReset, - ConnectionAborted => ErrorKind::ConnectionAborted, - NotConnected => ErrorKind::NotConnected, - AddrInUse => ErrorKind::AddrInUse, - AddrNotAvailable => ErrorKind::AddrNotAvailable, - BrokenPipe => ErrorKind::BrokenPipe, - AlreadyExists => ErrorKind::AlreadyExists, - WouldBlock => ErrorKind::WouldBlock, - InvalidInput => ErrorKind::InvalidInput, - InvalidData => ErrorKind::InvalidData, - TimedOut => ErrorKind::TimedOut, - Interrupted => ErrorKind::Interrupted, - WriteZero => ErrorKind::WriteZero, - Other => ErrorKind::Other, - UnexpectedEof => ErrorKind::UnexpectedEof, - _ => unreachable!(), - } - } - Repr::UrlErr(ref err) => { - use url::ParseError::*; - match err { - EmptyHost => ErrorKind::EmptyHost, - IdnaError => ErrorKind::IdnaError, - InvalidPort => ErrorKind::InvalidPort, - InvalidIpv4Address => ErrorKind::InvalidIpv4Address, - InvalidIpv6Address => ErrorKind::InvalidIpv6Address, - InvalidDomainCharacter => ErrorKind::InvalidDomainCharacter, - RelativeUrlWithoutBase => ErrorKind::RelativeUrlWithoutBase, - RelativeUrlWithCannotBeABaseBase => { - ErrorKind::RelativeUrlWithCannotBeABaseBase - } - SetHostOnCannotBeABaseUrl => ErrorKind::SetHostOnCannotBeABaseUrl, - Overflow => ErrorKind::Overflow, - } - } - Repr::HyperErr(ref err) => { - // For some reason hyper::errors::Kind is private. - if err.is_parse() { - ErrorKind::HttpParse - } else if err.is_user() { - ErrorKind::HttpUser - } else if err.is_canceled() { - ErrorKind::HttpCanceled - } else if err.is_closed() { - ErrorKind::HttpClosed - } else { - ErrorKind::HttpOther - } - } - Repr::ImportMapErr(ref _err) => ErrorKind::ImportMapError, - } - } -} - -impl fmt::Display for DenoError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.repr { - Repr::Simple(_kind, ref err_str) => f.pad(err_str), - Repr::IoErr(ref err) => err.fmt(f), - Repr::UrlErr(ref err) => err.fmt(f), - Repr::HyperErr(ref err) => err.fmt(f), - Repr::ImportMapErr(ref err) => f.pad(&err.msg), - } - } -} - -impl std::error::Error for DenoError { - fn description(&self) -> &str { - match self.repr { - Repr::Simple(_kind, ref msg) => msg.as_str(), - Repr::IoErr(ref err) => err.description(), - Repr::UrlErr(ref err) => err.description(), - Repr::HyperErr(ref err) => err.description(), - Repr::ImportMapErr(ref err) => &err.msg, - } - } - - fn cause(&self) -> Option<&dyn std::error::Error> { - match self.repr { - Repr::Simple(_kind, ref _msg) => None, - Repr::IoErr(ref err) => Some(err), - Repr::UrlErr(ref err) => Some(err), - Repr::HyperErr(ref err) => Some(err), - Repr::ImportMapErr(ref _err) => None, - } - } -} - -impl From for DenoError { - #[inline] - fn from(err: io::Error) -> Self { - Self { - repr: Repr::IoErr(err), - } - } -} - -impl From for DenoError { - #[inline] - fn from(err: url::ParseError) -> Self { - Self { - repr: Repr::UrlErr(err), - } - } -} - -impl From for DenoError { - #[inline] - fn from(err: hyper::Error) -> Self { - Self { - repr: Repr::HyperErr(err), - } - } -} - -impl From for DenoError { - fn from(e: ResolveAddrError) -> Self { - match e { - ResolveAddrError::Syntax => Self { - repr: Repr::Simple( - ErrorKind::InvalidInput, - "invalid address syntax".to_string(), - ), - }, - ResolveAddrError::Resolution(io_err) => Self { - repr: Repr::IoErr(io_err), - }, - } - } -} - -#[cfg(unix)] -impl From for DenoError { - fn from(e: UnixError) -> Self { - match e { - UnixError::Sys(Errno::EPERM) => Self { - repr: Repr::Simple( - ErrorKind::PermissionDenied, - Errno::EPERM.desc().to_owned(), - ), - }, - UnixError::Sys(Errno::EINVAL) => Self { - repr: Repr::Simple( - ErrorKind::InvalidInput, - Errno::EINVAL.desc().to_owned(), - ), - }, - UnixError::Sys(Errno::ENOENT) => Self { - repr: Repr::Simple( - ErrorKind::NotFound, - Errno::ENOENT.desc().to_owned(), - ), - }, - UnixError::Sys(err) => Self { - repr: Repr::Simple(ErrorKind::UnixError, err.desc().to_owned()), - }, - _ => Self { - repr: Repr::Simple(ErrorKind::Other, format!("{}", e)), - }, - } - } -} - -impl From for DenoError { - fn from(err: ImportMapError) -> Self { - Self { - repr: Repr::ImportMapErr(err), - } - } -} - -pub fn bad_resource() -> DenoError { - new(ErrorKind::BadResource, String::from("bad resource id")) -} - -pub fn permission_denied() -> DenoError { - new( - ErrorKind::PermissionDenied, - String::from("permission denied"), - ) -} - -pub fn op_not_implemented() -> DenoError { - new(ErrorKind::OpNotAvaiable, String::from("op not implemented")) -} - -pub fn worker_init_failed() -> DenoError { - // TODO(afinch7) pass worker error data through here - new( - ErrorKind::WorkerInitFailed, - String::from("worker init failed"), - ) -} - -pub fn no_buffer_specified() -> DenoError { - new(ErrorKind::InvalidInput, String::from("no buffer specified")) -} - -pub fn no_async_support() -> DenoError { - new( - ErrorKind::NoAsyncSupport, - String::from("op doesn't support async calls"), - ) -} - -pub fn no_sync_support() -> DenoError { - new( - ErrorKind::NoSyncSupport, - String::from("op doesn't support sync calls"), - ) -} - -#[derive(Debug)] -pub enum RustOrJsError { - Rust(DenoError), - Js(JSError), -} - -impl From for RustOrJsError { - fn from(e: DenoError) -> Self { - RustOrJsError::Rust(e) - } -} - -impl From for RustOrJsError { - fn from(e: JSError) -> Self { - RustOrJsError::Js(e) - } -} - -impl fmt::Display for RustOrJsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RustOrJsError::Rust(e) => e.fmt(f), - RustOrJsError::Js(e) => JSErrorColor(e).fmt(f), - } - } -} - -// TODO(ry) This is ugly. They are essentially the same type. -impl From> for RustOrJsError { - fn from(e: deno::JSErrorOr) -> Self { - match e { - deno::JSErrorOr::JSError(err) => RustOrJsError::Js(err), - deno::JSErrorOr::Other(err) => RustOrJsError::Rust(err), - } - } -} diff --git a/cli/fmt_errors.rs b/cli/fmt_errors.rs new file mode 100644 index 00000000000000..957e799ca4dc2f --- /dev/null +++ b/cli/fmt_errors.rs @@ -0,0 +1,289 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +//! This mod provides DenoError to unify errors across Deno. +use crate::ansi; +use deno::JSError; +use deno::StackFrame; +use std::fmt; + +/// A trait which specifies parts of a diagnostic like item needs to be able to +/// generate to conform its display to other diagnostic like items +pub trait DisplayFormatter { + fn format_category_and_code(&self) -> String; + fn format_message(&self, level: usize) -> String; + fn format_related_info(&self) -> String; + fn format_source_line(&self, level: usize) -> String; + fn format_source_name(&self) -> String; +} + +fn format_source_name(script_name: String, line: i64, column: i64) -> String { + let script_name_c = ansi::cyan(script_name); + let line_c = ansi::yellow((1 + line).to_string()); + let column_c = ansi::yellow((1 + column).to_string()); + format!("{}:{}:{}", script_name_c, line_c, column_c,) +} + +/// Formats optional source, line and column into a single string. +pub fn format_maybe_source_name( + script_name: Option, + line: Option, + column: Option, +) -> String { + if script_name.is_none() { + return "".to_string(); + } + + assert!(line.is_some()); + assert!(column.is_some()); + format_source_name(script_name.unwrap(), line.unwrap(), column.unwrap()) +} + +/// Take an optional source line and associated information to format it into +/// a pretty printed version of that line. +pub fn format_maybe_source_line( + source_line: Option, + line_number: Option, + start_column: Option, + end_column: Option, + is_error: bool, + level: usize, +) -> String { + if source_line.is_none() || line_number.is_none() { + return "".to_string(); + } + + let source_line = source_line.as_ref().unwrap(); + // sometimes source_line gets set with an empty string, which then outputs + // an empty source line when displayed, so need just short circuit here + if source_line.is_empty() { + return "".to_string(); + } + + assert!(start_column.is_some()); + assert!(end_column.is_some()); + let line = (1 + line_number.unwrap()).to_string(); + let line_color = ansi::black_on_white(line.to_string()); + let line_len = line.clone().len(); + let line_padding = + ansi::black_on_white(format!("{:indent$}", "", indent = line_len)) + .to_string(); + let mut s = String::new(); + let start_column = start_column.unwrap(); + let end_column = end_column.unwrap(); + // TypeScript uses `~` always, but V8 would utilise `^` always, even when + // doing ranges, so here, if we only have one marker (very common with V8 + // errors) we will use `^` instead. + let underline_char = if (end_column - start_column) <= 1 { + '^' + } else { + '~' + }; + for i in 0..end_column { + if i >= start_column { + s.push(underline_char); + } else { + s.push(' '); + } + } + let color_underline = if is_error { + ansi::red(s).to_string() + } else { + ansi::cyan(s).to_string() + }; + + let indent = format!("{:indent$}", "", indent = level); + + format!( + "\n\n{}{} {}\n{}{} {}\n", + indent, line_color, source_line, indent, line_padding, color_underline + ) +} + +/// Format a message to preface with `error: ` with ansi codes for red. +pub fn format_error_message(msg: String) -> String { + let preamble = ansi::red("error:".to_string()); + format!("{} {}", preamble, msg) +} + +/// Wrapper around JSError which provides color to_string. +pub struct JSErrorColor<'a>(pub &'a JSError); + +impl<'a> DisplayFormatter for JSErrorColor<'a> { + fn format_category_and_code(&self) -> String { + "".to_string() + } + + fn format_message(&self, _level: usize) -> String { + format!( + "{}{}", + ansi::red_bold("error: ".to_string()), + self.0.message.clone() + ) + } + + fn format_related_info(&self) -> String { + "".to_string() + } + + fn format_source_line(&self, level: usize) -> String { + format_maybe_source_line( + self.0.source_line.clone(), + self.0.line_number, + self.0.start_column, + self.0.end_column, + true, + level, + ) + } + + fn format_source_name(&self) -> String { + let e = self.0; + if e.script_resource_name.is_none() { + return "".to_string(); + } + + format!( + "\n► {}", + format_maybe_source_name( + e.script_resource_name.clone(), + e.line_number, + e.start_column, + ) + ) + } +} + +impl<'a> fmt::Display for JSErrorColor<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}{}{}", + self.format_message(0), + self.format_source_name(), + self.format_source_line(0), + )?; + + for frame in &self.0.frames { + write!(f, "\n{}", StackFrameColor(&frame).to_string())?; + } + Ok(()) + } +} + +struct StackFrameColor<'a>(&'a StackFrame); + +impl<'a> fmt::Display for StackFrameColor<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let frame = self.0; + // Note when we print to string, we change from 0-indexed to 1-indexed. + let function_name = ansi::italic_bold(frame.function_name.clone()); + let script_line_column = + format_source_name(frame.script_name.clone(), frame.line, frame.column); + + if !frame.function_name.is_empty() { + write!(f, " at {} ({})", function_name, script_line_column) + } else if frame.is_eval { + write!(f, " at eval ({})", script_line_column) + } else { + write!(f, " at {}", script_line_column) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ansi::strip_ansi_codes; + + fn error1() -> JSError { + JSError { + message: "Error: foo bar".to_string(), + source_line: None, + script_resource_name: None, + line_number: None, + start_position: None, + end_position: None, + error_level: None, + start_column: None, + end_column: None, + frames: vec![ + StackFrame { + line: 4, + column: 16, + script_name: "foo_bar.ts".to_string(), + function_name: "foo".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 5, + column: 20, + script_name: "bar_baz.ts".to_string(), + function_name: "qat".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 1, + column: 1, + script_name: "deno_main.js".to_string(), + function_name: "".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + ], + } + } + + #[test] + fn js_error_to_string() { + let e = error1(); + assert_eq!("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", strip_ansi_codes(&JSErrorColor(&e).to_string())); + } + + #[test] + fn test_format_none_source_name() { + let actual = format_maybe_source_name(None, None, None); + assert_eq!(actual, ""); + } + + #[test] + fn test_format_some_source_name() { + let actual = format_maybe_source_name( + Some("file://foo/bar.ts".to_string()), + Some(1), + Some(2), + ); + assert_eq!(strip_ansi_codes(&actual), "file://foo/bar.ts:2:3"); + } + + #[test] + fn test_format_none_source_line() { + let actual = format_maybe_source_line(None, None, None, None, false, 0); + assert_eq!(actual, ""); + } + + #[test] + fn test_format_some_source_line() { + let actual = format_maybe_source_line( + Some("console.log('foo');".to_string()), + Some(8), + Some(8), + Some(11), + true, + 0, + ); + assert_eq!( + strip_ansi_codes(&actual), + "\n\n9 console.log(\'foo\');\n ~~~\n" + ); + } + + #[test] + fn test_format_error_message() { + let actual = format_error_message("foo".to_string()); + assert_eq!(strip_ansi_codes(&actual), "error: foo"); + } +} diff --git a/cli/fs.rs b/cli/fs.rs index 73d7701e697dbe..3b5709804d2cf2 100644 --- a/cli/fs.rs +++ b/cli/fs.rs @@ -15,7 +15,7 @@ use std::os::unix::fs::DirBuilderExt; #[cfg(any(unix))] use std::os::unix::fs::PermissionsExt; -use crate::errors::DenoResult; +use crate::deno_error::DenoResult; pub fn write_file>( filename: &Path, @@ -115,7 +115,7 @@ pub fn normalize_path(path: &Path) -> String { #[cfg(unix)] pub fn chown(path: &str, uid: u32, gid: u32) -> DenoResult<()> { - use crate::errors::DenoError; + use crate::deno_error::DenoError; let nix_uid = Uid::from_raw(uid); let nix_gid = Gid::from_raw(gid); unix_chown(path, Option::Some(nix_uid), Option::Some(nix_gid)) @@ -126,6 +126,6 @@ pub fn chown(path: &str, uid: u32, gid: u32) -> DenoResult<()> { pub fn chown(_path: &str, _uid: u32, _gid: u32) -> DenoResult<()> { // Noop // TODO: implement chown for Windows - use crate::errors; - Err(errors::op_not_implemented()) + use crate::deno_error; + Err(deno_error::op_not_implemented()) } diff --git a/cli/http_util.rs b/cli/http_util.rs index 13d1ce45be33d2..a3310b1bf7a594 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -1,6 +1,6 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -use crate::errors; -use crate::errors::DenoError; +use crate::deno_error; +use crate::deno_error::DenoError; #[cfg(test)] use futures::future::{loop_fn, Loop}; use futures::{future, Future, Stream}; @@ -58,7 +58,7 @@ fn resolve_uri_from_location(base_uri: &Uri, location: &str) -> Uri { } #[cfg(test)] -use crate::errors::DenoResult; +use crate::deno_error::DenoResult; #[cfg(test)] use crate::tokio_util; #[cfg(test)] @@ -108,8 +108,8 @@ pub fn fetch_string_once( } else if response.status().is_client_error() || response.status().is_server_error() { - return Box::new(future::err(errors::new( - errors::ErrorKind::Other, + return Box::new(future::err(deno_error::new( + deno_error::ErrorKind::Other, format!("Import '{}' failed: {}", &url, response.status()), ))); } @@ -165,8 +165,8 @@ pub fn fetch_string( return Ok(Loop::Continue((client, new_url))); } if !response.status().is_success() { - return Err(errors::new( - errors::ErrorKind::NotFound, + return Err(deno_error::new( + deno_error::ErrorKind::NotFound, "module not found".to_string(), )); } diff --git a/cli/main.rs b/cli/main.rs index ef132927e41679..d56b074797bd95 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -17,16 +17,16 @@ extern crate rand; mod ansi; pub mod compiler; pub mod deno_dir; +pub mod deno_error; pub mod diagnostics; mod dispatch_minimal; -pub mod errors; pub mod flags; +pub mod fmt_errors; mod fs; mod global_timer; mod http_body; mod http_util; mod import_map; -pub mod js_errors; pub mod msg; pub mod msg_util; pub mod ops; @@ -36,6 +36,7 @@ mod repl; pub mod resolve_addr; pub mod resources; mod signal; +pub mod source_maps; mod startup_data; pub mod state; mod tokio_util; @@ -44,7 +45,7 @@ pub mod version; pub mod worker; use crate::compiler::bundle_async; -use crate::errors::RustOrJsError; +use crate::deno_error::DenoError; use crate::progress::Progress; use crate::state::ThreadSafeState; use crate::worker::Worker; @@ -82,14 +83,14 @@ impl log::Log for Logger { fn flush(&self) {} } -fn print_err_and_exit(err: RustOrJsError) { +fn print_err_and_exit(err: DenoError) { eprintln!("{}", err.to_string()); std::process::exit(1); } fn js_check(r: Result<(), E>) where - E: Into, + E: Into, { if let Err(err) = r { print_err_and_exit(err.into()); @@ -258,9 +259,7 @@ fn xeval_command(flags: DenoFlags, argv: Vec) { .then(|result| { js_check(result); Ok(()) - }).map_err(|(err, _worker): (RustOrJsError, Worker)| { - print_err_and_exit(err) - }) + }).map_err(|(err, _worker): (DenoError, Worker)| print_err_and_exit(err)) }); tokio_util::run(main_future); } @@ -295,9 +294,7 @@ fn run_repl(flags: DenoFlags, argv: Vec) { .then(|result| { js_check(result); Ok(()) - }).map_err(|(err, _worker): (RustOrJsError, Worker)| { - print_err_and_exit(err) - }) + }).map_err(|(err, _worker): (DenoError, Worker)| print_err_and_exit(err)) }); tokio_util::run(main_future); } diff --git a/cli/msg.fbs b/cli/msg.fbs index 7e8292740ce297..d76e70d8525b86 100644 --- a/cli/msg.fbs +++ b/cli/msg.fbs @@ -133,12 +133,16 @@ enum ErrorKind: byte { // custom errors InvalidUri, InvalidSeekMode, - OpNotAvaiable, + OpNotAvailable, WorkerInitFailed, UnixError, NoAsyncSupport, NoSyncSupport, ImportMapError, + + // other kinds + Diagnostic, + JSError, } table Cwd {} diff --git a/cli/msg_util.rs b/cli/msg_util.rs index 71bcc19d9a53dd..0ecb7f0d634cc6 100644 --- a/cli/msg_util.rs +++ b/cli/msg_util.rs @@ -1,7 +1,7 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. // Helpers for serialization. -use crate::errors; -use crate::errors::DenoResult; +use crate::deno_error; +use crate::deno_error::DenoResult; use crate::msg; use flatbuffers; @@ -104,7 +104,7 @@ pub fn deserialize_request( let u = header_msg.url().unwrap(); let u = Uri::from_str(u) - .map_err(|e| errors::new(msg::ErrorKind::InvalidUri, e.to_string()))?; + .map_err(|e| deno_error::new(msg::ErrorKind::InvalidUri, e.to_string()))?; *r.uri_mut() = u; if let Some(method) = header_msg.method() { diff --git a/cli/ops.rs b/cli/ops.rs index a9279f070b506e..d4add61e08af31 100644 --- a/cli/ops.rs +++ b/cli/ops.rs @@ -2,14 +2,15 @@ use atty; use crate::ansi; use crate::deno_dir::resolve_path; +use crate::deno_error; +use crate::deno_error::err_check; +use crate::deno_error::DenoError; +use crate::deno_error::DenoResult; +use crate::deno_error::ErrorKind; use crate::dispatch_minimal::dispatch_minimal; use crate::dispatch_minimal::parse_min_record; -use crate::errors; -use crate::errors::{DenoError, DenoResult, ErrorKind}; use crate::fs as deno_fs; use crate::http_util; -use crate::js_errors::apply_source_map; -use crate::js_errors::JSErrorColor; use crate::msg; use crate::msg_util; use crate::rand; @@ -25,7 +26,6 @@ use crate::tokio_util; use crate::tokio_write; use crate::version; use crate::worker::Worker; -use deno::js_check; use deno::Buf; use deno::CoreOp; use deno::JSError; @@ -401,11 +401,11 @@ fn op_format_error( let orig_error = String::from(inner.error().unwrap()); let js_error = JSError::from_v8_exception(&orig_error).unwrap(); - let js_error_mapped = apply_source_map(&js_error, &state.dir); - let js_error_string = JSErrorColor(&js_error_mapped).to_string(); + let error_mapped = DenoError::from(js_error).apply_source_map(&state.dir); + let error_string = error_mapped.to_string(); let mut builder = FlatBufferBuilder::new(); - let new_error = builder.create_string(&js_error_string); + let new_error = builder.create_string(&error_string); let inner = msg::FormatErrorRes::create( &mut builder, @@ -496,7 +496,7 @@ fn op_fetch_module_meta_data( data: Option, ) -> CliOpResult { if !base.sync() { - return Err(errors::no_async_support()); + return Err(deno_error::no_async_support()); } assert!(data.is_none()); let inner = base.inner_as_fetch_module_meta_data().unwrap(); @@ -563,7 +563,7 @@ fn op_global_timer_stop( data: Option, ) -> CliOpResult { if !base.sync() { - return Err(errors::no_async_support()); + return Err(deno_error::no_async_support()); } assert!(data.is_none()); let state = state; @@ -578,7 +578,7 @@ fn op_global_timer( data: Option, ) -> CliOpResult { if base.sync() { - return Err(errors::no_sync_support()); + return Err(deno_error::no_sync_support()); } assert!(data.is_none()); let cmd_id = base.cmd_id(); @@ -1002,7 +1002,7 @@ fn op_close( let inner = base.inner_as_close().unwrap(); let rid = inner.rid(); match resources::lookup(rid) { - None => Err(errors::bad_resource()), + None => Err(deno_error::bad_resource()), Some(resource) => { resource.close(); ok_buf(empty_buf()) @@ -1033,7 +1033,7 @@ fn op_shutdown( let rid = inner.rid(); let how = inner.how(); match resources::lookup(rid) { - None => Err(errors::bad_resource()), + None => Err(deno_error::bad_resource()), Some(mut resource) => { let shutdown_mode = match how { 0 => Shutdown::Read, @@ -1059,7 +1059,7 @@ fn op_read( let rid = inner.rid(); match resources::lookup(rid) { - None => Err(errors::bad_resource()), + None => Err(deno_error::bad_resource()), Some(resource) => { let op = tokio::io::read(resource, data.unwrap()) .map_err(DenoError::from) @@ -1102,7 +1102,7 @@ fn op_write( let rid = inner.rid(); match resources::lookup(rid) { - None => Err(errors::bad_resource()), + None => Err(deno_error::bad_resource()), Some(resource) => { let op = tokio_write::write(resource, data.unwrap()) .map_err(DenoError::from) @@ -1146,7 +1146,7 @@ fn op_seek( let whence = inner.whence(); match resources::lookup(rid) { - None => Err(errors::bad_resource()), + None => Err(deno_error::bad_resource()), Some(resource) => { let op = resources::seek(resource, offset, whence) .and_then(move |_| Ok(empty_buf())); @@ -1205,7 +1205,7 @@ fn op_copy_file( // See https://github.com/rust-lang/rust/issues/54800 // Once the issue is reolved, we should remove this workaround. if cfg!(unix) && !from.is_file() { - return Err(errors::new( + return Err(deno_error::new( ErrorKind::NotFound, "File not found".to_string(), )); @@ -1417,7 +1417,10 @@ fn op_symlink( state.check_write(&newname_)?; // TODO Use type for Windows. if cfg!(windows) { - return Err(errors::new(ErrorKind::Other, "Not implemented".to_string())); + return Err(deno_error::new( + ErrorKind::Other, + "Not implemented".to_string(), + )); } blocking(base.sync(), move || { debug!("op_symlink {} {}", oldname.display(), newname.display()); @@ -1638,7 +1641,7 @@ fn op_accept( let server_rid = inner.rid(); match resources::lookup(server_rid) { - None => Err(errors::bad_resource()), + None => Err(deno_error::bad_resource()), Some(server_resource) => { let op = tokio_util::accept(server_resource) .map_err(DenoError::from) @@ -1767,7 +1770,7 @@ fn op_run( data: Option, ) -> CliOpResult { if !base.sync() { - return Err(errors::no_async_support()); + return Err(deno_error::no_async_support()); } let cmd_id = base.cmd_id(); @@ -1906,7 +1909,7 @@ fn op_worker_get_message( data: Option, ) -> CliOpResult { if base.sync() { - return Err(errors::no_sync_support()); + return Err(deno_error::no_sync_support()); } assert!(data.is_none()); let cmd_id = base.cmd_id(); @@ -1952,7 +1955,7 @@ fn op_worker_post_message( }; tx.send(d) .wait() - .map_err(|e| errors::new(ErrorKind::Other, e.to_string()))?; + .map_err(|e| deno_error::new(ErrorKind::Other, e.to_string()))?; let builder = &mut FlatBufferBuilder::new(); ok_buf(serialize_response( @@ -1988,34 +1991,32 @@ fn op_create_worker( let mut worker = Worker::new(name, startup_data::deno_isolate_init(), child_state); - js_check(worker.execute("denoMain()")); - js_check(worker.execute("workerMain()")); + err_check(worker.execute("denoMain()")); + err_check(worker.execute("workerMain()")); let module_specifier = ModuleSpecifier::resolve_root(specifier)?; - let op = worker - .execute_mod_async(&module_specifier, false) - .and_then(move |()| { - let mut workers_tl = parent_state.workers.lock().unwrap(); - workers_tl.insert(rid, worker.shared()); - let builder = &mut FlatBufferBuilder::new(); - let msg_inner = msg::CreateWorkerRes::create( - builder, - &msg::CreateWorkerResArgs { rid }, - ); - Ok(serialize_response( - cmd_id, - builder, - msg::BaseArgs { - inner: Some(msg_inner.as_union_value()), - inner_type: msg::Any::CreateWorkerRes, - ..Default::default() - }, - )) - }).map_err(|err| match err { - errors::RustOrJsError::Js(_) => errors::worker_init_failed(), - errors::RustOrJsError::Rust(err) => err, - }); + let op = + worker + .execute_mod_async(&module_specifier, false) + .and_then(move |()| { + let mut workers_tl = parent_state.workers.lock().unwrap(); + workers_tl.insert(rid, worker.shared()); + let builder = &mut FlatBufferBuilder::new(); + let msg_inner = msg::CreateWorkerRes::create( + builder, + &msg::CreateWorkerResArgs { rid }, + ); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(msg_inner.as_union_value()), + inner_type: msg::Any::CreateWorkerRes, + ..Default::default() + }, + )) + }); let result = op.wait()?; Ok(Op::Sync(result)) @@ -2028,7 +2029,7 @@ fn op_host_get_worker_closed( data: Option, ) -> CliOpResult { if base.sync() { - return Err(errors::no_sync_support()); + return Err(deno_error::no_sync_support()); } assert!(data.is_none()); let cmd_id = base.cmd_id(); @@ -2063,7 +2064,7 @@ fn op_host_get_message( data: Option, ) -> CliOpResult { if base.sync() { - return Err(errors::no_sync_support()); + return Err(deno_error::no_sync_support()); } assert!(data.is_none()); let cmd_id = base.cmd_id(); @@ -2107,7 +2108,7 @@ fn op_host_post_message( resources::post_message_to_worker(rid, d) .wait() - .map_err(|e| errors::new(ErrorKind::Other, e.to_string()))?; + .map_err(|e| deno_error::new(ErrorKind::Other, e.to_string()))?; let builder = &mut FlatBufferBuilder::new(); ok_buf(serialize_response( diff --git a/cli/permissions.rs b/cli/permissions.rs index e2e3e22a4d0b16..2220165321b05b 100644 --- a/cli/permissions.rs +++ b/cli/permissions.rs @@ -4,8 +4,8 @@ use atty; use crate::flags::DenoFlags; use ansi_term::Style; -use crate::errors::permission_denied; -use crate::errors::DenoResult; +use crate::deno_error::permission_denied; +use crate::deno_error::DenoResult; use std::collections::HashSet; use std::fmt; use std::io; diff --git a/cli/repl.rs b/cli/repl.rs index 86fa92e0811091..e0d029439bd2e9 100644 --- a/cli/repl.rs +++ b/cli/repl.rs @@ -5,8 +5,8 @@ use crate::msg::ErrorKind; use std::error::Error; use crate::deno_dir::DenoDir; -use crate::errors::new as deno_error; -use crate::errors::DenoResult; +use crate::deno_error::new as deno_error; +use crate::deno_error::DenoResult; use std::path::PathBuf; #[cfg(not(windows))] diff --git a/cli/resources.rs b/cli/resources.rs index a6cee811f0e0fb..e98327b63e1206 100644 --- a/cli/resources.rs +++ b/cli/resources.rs @@ -8,10 +8,10 @@ // descriptors". This module implements a global resource table. Ops (AKA // handlers) look up resources by their integer id here. -use crate::errors; -use crate::errors::bad_resource; -use crate::errors::DenoError; -use crate::errors::DenoResult; +use crate::deno_error; +use crate::deno_error::bad_resource; +use crate::deno_error::DenoError; +use crate::deno_error::DenoResult; use crate::http_body::HttpBody; use crate::repl::Repl; use crate::state::WorkerChannels; @@ -372,10 +372,9 @@ impl Future for WorkerReceiver { let mut table = RESOURCE_TABLE.lock().unwrap(); let maybe_repr = table.get_mut(&self.rid); match maybe_repr { - Some(Repr::Worker(ref mut wc)) => wc - .1 - .poll() - .map_err(|err| errors::new(errors::ErrorKind::Other, err.to_string())), + Some(Repr::Worker(ref mut wc)) => wc.1.poll().map_err(|err| { + deno_error::new(deno_error::ErrorKind::Other, err.to_string()) + }), _ => Err(bad_resource()), } } @@ -398,10 +397,9 @@ impl Stream for WorkerReceiverStream { let mut table = RESOURCE_TABLE.lock().unwrap(); let maybe_repr = table.get_mut(&self.rid); match maybe_repr { - Some(Repr::Worker(ref mut wc)) => wc - .1 - .poll() - .map_err(|err| errors::new(errors::ErrorKind::Other, err.to_string())), + Some(Repr::Worker(ref mut wc)) => wc.1.poll().map_err(|err| { + deno_error::new(deno_error::ErrorKind::Other, err.to_string()) + }), _ => Err(bad_resource()), } } @@ -535,8 +533,8 @@ pub fn seek( 1 => SeekFrom::Current(i64::from(offset)), 2 => SeekFrom::End(i64::from(offset)), _ => { - return Box::new(futures::future::err(errors::new( - errors::ErrorKind::InvalidSeekMode, + return Box::new(futures::future::err(deno_error::new( + deno_error::ErrorKind::InvalidSeekMode, format!("Invalid seek mode: {}", whence), ))); } diff --git a/cli/signal.rs b/cli/signal.rs index 7d67ba7433209f..bedb43ad25005a 100644 --- a/cli/signal.rs +++ b/cli/signal.rs @@ -3,11 +3,11 @@ use nix::sys::signal::{kill as unix_kill, Signal}; #[cfg(unix)] use nix::unistd::Pid; -use crate::errors::DenoResult; +use crate::deno_error::DenoResult; #[cfg(unix)] pub fn kill(pid: i32, signo: i32) -> DenoResult<()> { - use crate::errors::DenoError; + use crate::deno_error::DenoError; let sig = Signal::from_c_int(signo)?; unix_kill(Pid::from_raw(pid), Option::Some(sig)).map_err(DenoError::from) } diff --git a/cli/js_errors.rs b/cli/source_maps.rs similarity index 68% rename from cli/js_errors.rs rename to cli/source_maps.rs index ae49ed63636f3f..11c3a35325967b 100644 --- a/cli/js_errors.rs +++ b/cli/source_maps.rs @@ -1,23 +1,18 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -//! This mod adds source maps and ANSI color display to deno::JSError. -use crate::ansi; +//! This mod provides functions to remap a deno::JSError based on a source map use deno::JSError; use deno::StackFrame; +use serde_json; use source_map_mappings::parse_mappings; use source_map_mappings::Bias; use source_map_mappings::Mappings; use std::collections::HashMap; -use std::fmt; use std::str; -/// Wrapper around JSError which provides color to_string. -pub struct JSErrorColor<'a>(pub &'a JSError); - -struct StackFrameColor<'a>(&'a StackFrame); - pub trait SourceMapGetter { /// Returns the raw source map file. fn get_source_map(&self, script_name: &str) -> Option>; + fn get_source_line(&self, script_name: &str, line: usize) -> Option; } /// Cached filename lookups. The key can be None if a previous lookup failed to @@ -29,80 +24,9 @@ struct SourceMap { sources: Vec, } -impl<'a> fmt::Display for StackFrameColor<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let frame = self.0; - // Note when we print to string, we change from 0-indexed to 1-indexed. - let function_name = ansi::italic_bold(frame.function_name.clone()); - let script_line_column = - format_script_line_column(&frame.script_name, frame.line, frame.column); - - if !frame.function_name.is_empty() { - write!(f, " at {} ({})", function_name, script_line_column) - } else if frame.is_eval { - write!(f, " at eval ({})", script_line_column) - } else { - 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<'a> fmt::Display for JSErrorColor<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let e = self.0; - if e.script_resource_name.is_some() { - let script_resource_name = e.script_resource_name.as_ref().unwrap(); - // Avoid showing internal code from gen/cli/bundle/main.js - if script_resource_name != "gen/cli/bundle/main.js" - && script_resource_name != "gen/cli/bundle/compiler.js" - { - if e.line_number.is_some() && e.start_column.is_some() { - assert!(e.line_number.is_some()); - assert!(e.start_column.is_some()); - let script_line_column = format_script_line_column( - script_resource_name, - e.line_number.unwrap() - 1, - e.start_column.unwrap() - 1, - ); - write!(f, "{}", script_line_column)?; - } - if e.source_line.is_some() { - write!(f, "\n{}\n", e.source_line.as_ref().unwrap())?; - let mut s = String::new(); - for i in 0..e.end_column.unwrap() { - if i >= e.start_column.unwrap() { - s.push('^'); - } else { - s.push(' '); - } - } - writeln!(f, "{}", ansi::red_bold(s))?; - } - } - } - - write!(f, "{}", ansi::bold(e.message.clone()))?; - - for frame in &e.frames { - write!(f, "\n{}", StackFrameColor(&frame).to_string())?; - } - Ok(()) - } -} - impl SourceMap { + /// Take a JSON string and attempt to decode it, returning an optional + /// instance of `SourceMap`. fn from_json(json_str: &str) -> Option { // Ugly. Maybe use serde_derive. match serde_json::from_str::(json_str) { @@ -137,110 +61,178 @@ impl SourceMap { } } -fn frame_apply_source_map( - frame: &StackFrame, - mappings_map: &mut CachedMaps, - getter: &G, -) -> StackFrame { - let maybe_sm = get_mappings(frame.script_name.as_ref(), mappings_map, getter); - let frame_pos = ( - frame.script_name.to_owned(), - frame.line as i64, - frame.column as i64, - ); - let (script_name, line, column) = match maybe_sm { - None => frame_pos, - Some(sm) => match sm.mappings.original_location_for( - frame.line as u32, - frame.column as u32, - Bias::default(), - ) { - None => frame_pos, - Some(mapping) => match &mapping.original { - None => frame_pos, - Some(original) => { - let orig_source = sm.sources[original.source as usize].clone(); - ( - orig_source, - i64::from(original.original_line), - i64::from(original.original_column), - ) - } - }, - }, - }; +// The bundle does not get built for 'cargo check', so we don't embed the +// bundle source map. The built in source map is the source map for the main +// JavaScript bundle which is then used to create the snapshot. Runtime stack +// traces can contain positions within the bundle which we will map to the +// original Deno TypeScript code. +#[cfg(feature = "check-only")] +fn builtin_source_map(_: &str) -> Option> { + None +} - StackFrame { - script_name, - function_name: frame.function_name.clone(), - line, - column, - is_eval: frame.is_eval, - is_constructor: frame.is_constructor, - is_wasm: frame.is_wasm, +#[cfg(not(feature = "check-only"))] +fn builtin_source_map(script_name: &str) -> Option> { + match script_name { + "gen/cli/bundle/main.js" => Some( + include_bytes!(concat!( + env!("GN_OUT_DIR"), + "/gen/cli/bundle/main.js.map" + )).to_vec(), + ), + "gen/cli/bundle/compiler.js" => Some( + include_bytes!(concat!( + env!("GN_OUT_DIR"), + "/gen/cli/bundle/compiler.js.map" + )).to_vec(), + ), + _ => None, } } +/// Apply a source map to a JSError, returning a JSError where the filenames, +/// the lines and the columns point to their original source location, not their +/// transpiled location if applicable. pub fn apply_source_map( js_error: &JSError, getter: &G, ) -> JSError { let mut mappings_map: CachedMaps = HashMap::new(); + let mut frames = Vec::::new(); for frame in &js_error.frames { let f = frame_apply_source_map(&frame, &mut mappings_map, getter); frames.push(f); } + + let (script_resource_name, line_number, start_column) = + get_maybe_orig_position( + js_error.script_resource_name.clone(), + js_error.line_number, + js_error.start_column, + &mut mappings_map, + getter, + ); + // It is better to just move end_column to be the same distance away from + // start column because sometimes the code point is not available in the + // source file map. + let end_column = match js_error.end_column { + Some(ec) => { + if start_column.is_some() { + Some(ec - (js_error.start_column.unwrap() - start_column.unwrap())) + } else { + None + } + } + _ => None, + }; + // if there is a source line that we might be different in the source file, we + // will go fetch it from the getter + let source_line = if js_error.source_line.is_some() + && script_resource_name.is_some() + && line_number.is_some() + { + getter.get_source_line( + &js_error.script_resource_name.clone().unwrap(), + line_number.unwrap() as usize, + ) + } else { + js_error.source_line.clone() + }; + JSError { message: js_error.message.clone(), frames, error_level: js_error.error_level, - source_line: js_error.source_line.clone(), - // TODO the following need to be source mapped: - script_resource_name: js_error.script_resource_name.clone(), - line_number: js_error.line_number, + source_line, + script_resource_name, + line_number, + start_column, + end_column, + // These are difficult to map to their original position and they are not + // currently used in any output, so we don't remap them. start_position: js_error.start_position, end_position: js_error.end_position, - start_column: js_error.start_column, - end_column: js_error.end_column, } } -// The bundle does not get built for 'cargo check', so we don't embed the -// bundle source map. -#[cfg(feature = "check-only")] -fn builtin_source_map(_: &str) -> Option> { - None +fn frame_apply_source_map( + frame: &StackFrame, + mappings_map: &mut CachedMaps, + getter: &G, +) -> StackFrame { + let (script_name, line, column) = get_orig_position( + frame.script_name.to_string(), + frame.line, + frame.column, + mappings_map, + getter, + ); + + StackFrame { + script_name, + function_name: frame.function_name.clone(), + line, + column, + is_eval: frame.is_eval, + is_constructor: frame.is_constructor, + is_wasm: frame.is_wasm, + } } -#[cfg(not(feature = "check-only"))] -fn builtin_source_map(script_name: &str) -> Option> { - match script_name { - "gen/cli/bundle/main.js" => Some( - include_bytes!(concat!( - env!("GN_OUT_DIR"), - "/gen/cli/bundle/main.js.map" - )).to_vec(), - ), - "gen/cli/bundle/compiler.js" => Some( - include_bytes!(concat!( - env!("GN_OUT_DIR"), - "/gen/cli/bundle/compiler.js.map" - )).to_vec(), - ), - _ => None, +fn get_maybe_orig_position( + script_name: Option, + line: Option, + column: Option, + mappings_map: &mut CachedMaps, + getter: &G, +) -> (Option, Option, Option) { + match (script_name, line, column) { + (Some(script_name_v), Some(line_v), Some(column_v)) => { + let (script_name, line, column) = get_orig_position( + script_name_v, + line_v - 1, + column_v, + mappings_map, + getter, + ); + (Some(script_name), Some(line), Some(column)) + } + _ => (None, None, None), } } -fn parse_map_string( - script_name: &str, +fn get_orig_position( + script_name: String, + line: i64, + column: i64, + mappings_map: &mut CachedMaps, getter: &G, -) -> Option { - builtin_source_map(script_name) - .or_else(|| getter.get_source_map(script_name)) - .and_then(|raw_source_map| { - SourceMap::from_json(str::from_utf8(&raw_source_map).unwrap()) - }) +) -> (String, i64, i64) { + let maybe_sm = get_mappings(&script_name, mappings_map, getter); + let default_pos = (script_name, line, column); + + match maybe_sm { + None => default_pos, + Some(sm) => match sm.mappings.original_location_for( + line as u32, + column as u32, + Bias::default(), + ) { + None => default_pos, + Some(mapping) => match &mapping.original { + None => default_pos, + Some(original) => { + let orig_source = sm.sources[original.source as usize].clone(); + ( + orig_source, + i64::from(original.original_line), + i64::from(original.original_column), + ) + } + }, + }, + } } fn get_mappings<'a, G: SourceMapGetter>( @@ -253,10 +245,57 @@ fn get_mappings<'a, G: SourceMapGetter>( .or_insert_with(|| parse_map_string(script_name, getter)) } +// TODO(kitsonk) parsed source maps should probably be cached in state in +// the module meta data. +fn parse_map_string( + script_name: &str, + getter: &G, +) -> Option { + builtin_source_map(script_name) + .or_else(|| getter.get_source_map(script_name)) + .and_then(|raw_source_map| { + SourceMap::from_json(str::from_utf8(&raw_source_map).unwrap()) + }) +} + #[cfg(test)] mod tests { use super::*; - use crate::ansi::strip_ansi_codes; + + struct MockSourceMapGetter {} + + impl SourceMapGetter for MockSourceMapGetter { + fn get_source_map(&self, script_name: &str) -> Option> { + let s = match script_name { + "foo_bar.ts" => r#"{"sources": ["foo_bar.ts"], "mappings":";;;IAIA,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IAC/C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAE3C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC"}"#, + "bar_baz.ts" => r#"{"sources": ["bar_baz.ts"], "mappings":";;;IAEA,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,GAAG,GAAG,sDAAa,OAAO,2BAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC,EAAE,CAAC;IAEQ,QAAA,GAAG,GAAG,KAAK,CAAC;IAEzB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC"}"#, + _ => return None, + }; + Some(s.as_bytes().to_owned()) + } + + fn get_source_line( + &self, + script_name: &str, + line: usize, + ) -> Option { + let s = match script_name { + "foo_bar.ts" => vec![ + "console.log('foo');", + "console.log('foo');", + "console.log('foo');", + "console.log('foo');", + "console.log('foo');", + ], + _ => return None, + }; + if s.len() > line { + Some(s[line].to_string()) + } else { + None + } + } + } fn error1() -> JSError { JSError { @@ -301,25 +340,6 @@ mod tests { } } - struct MockSourceMapGetter {} - - impl SourceMapGetter for MockSourceMapGetter { - fn get_source_map(&self, script_name: &str) -> Option> { - let s = match script_name { - "foo_bar.ts" => r#"{"sources": ["foo_bar.ts"], "mappings":";;;IAIA,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IAC/C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAE3C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC"}"#, - "bar_baz.ts" => r#"{"sources": ["bar_baz.ts"], "mappings":";;;IAEA,CAAC,KAAK,IAAI,EAAE;QACV,MAAM,GAAG,GAAG,sDAAa,OAAO,2BAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC,CAAC,EAAE,CAAC;IAEQ,QAAA,GAAG,GAAG,KAAK,CAAC;IAEzB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC"}"#, - _ => return None, - }; - Some(s.as_bytes().to_owned()) - } - } - - #[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", strip_ansi_codes(&e.to_string())); - } - #[test] fn js_error_apply_source_map_1() { let e = error1(); @@ -398,6 +418,25 @@ mod tests { assert!(actual.frames[0].script_name.ends_with("js/util.ts")); } + #[test] + fn js_error_apply_source_map_line() { + let e = JSError { + message: "TypeError: baz".to_string(), + source_line: Some("foo".to_string()), + script_resource_name: Some("foo_bar.ts".to_string()), + line_number: Some(4), + start_position: None, + end_position: None, + error_level: None, + start_column: Some(16), + end_column: None, + frames: vec![], + }; + let getter = MockSourceMapGetter {}; + let actual = apply_source_map(&e, &getter); + assert_eq!(actual.source_line, Some("console.log('foo');".to_string())); + } + #[test] fn source_map_from_json() { let json = r#"{"version":3,"file":"error_001.js","sourceRoot":"","sources":["file:///Users/rld/src/deno/tests/error_001.ts"],"names":[],"mappings":"AAAA,SAAS,GAAG;IACV,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,GAAG;IACV,GAAG,EAAE,CAAC;AACR,CAAC;AAED,GAAG,EAAE,CAAC"}"#; diff --git a/cli/state.rs b/cli/state.rs index aa4690d4451fd9..69a2bf74d4b5bc 100644 --- a/cli/state.rs +++ b/cli/state.rs @@ -2,8 +2,8 @@ use crate::compiler::compile_async; use crate::compiler::ModuleMetaData; use crate::deno_dir; -use crate::errors::DenoError; -use crate::errors::DenoResult; +use crate::deno_error::DenoError; +use crate::deno_error::DenoResult; use crate::flags; use crate::global_timer::GlobalTimer; use crate::import_map::ImportMap; diff --git a/cli/worker.rs b/cli/worker.rs index 9170a293faf84c..65da666e87dbc7 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -1,11 +1,8 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -use crate::errors::DenoError; -use crate::errors::RustOrJsError; -use crate::js_errors; +use crate::deno_error::DenoError; use crate::state::ThreadSafeState; use crate::tokio_util; use deno; -use deno::JSError; use deno::ModuleSpecifier; use deno::StartupData; use futures::Async; @@ -39,7 +36,7 @@ impl Worker { } /// Same as execute2() but the filename defaults to "". - pub fn execute(&mut self, js_source: &str) -> Result<(), JSError> { + pub fn execute(&mut self, js_source: &str) -> Result<(), DenoError> { self.execute2("", js_source) } @@ -49,9 +46,12 @@ impl Worker { &mut self, js_filename: &str, js_source: &str, - ) -> Result<(), JSError> { + ) -> Result<(), DenoError> { let mut isolate = self.isolate.lock().unwrap(); - isolate.execute(js_filename, js_source) + match isolate.execute(js_filename, js_source) { + Ok(_) => Ok(()), + Err(err) => Err(DenoError::from(err)), + } } /// Executes the provided JavaScript module. @@ -59,7 +59,7 @@ impl Worker { &mut self, module_specifier: &ModuleSpecifier, is_prefetch: bool, - ) -> impl Future { + ) -> impl Future { let worker = self.clone(); let worker_ = worker.clone(); let loader = self.state.clone(); @@ -87,12 +87,12 @@ impl Worker { } }).map_err(move |err| { worker_.state.progress.done(); - // Convert to RustOrJsError AND apply_source_map. + // Convert to DenoError AND apply_source_map. match err { deno::JSErrorOr::JSError(err) => { - RustOrJsError::Js(worker_.apply_source_map(err)) + worker_.apply_source_map(DenoError::from(err)) } - deno::JSErrorOr::Other(err) => RustOrJsError::Rust(err), + deno::JSErrorOr::Other(err) => err, } }) } @@ -102,29 +102,32 @@ impl Worker { &mut self, module_specifier: &ModuleSpecifier, is_prefetch: bool, - ) -> Result<(), RustOrJsError> { + ) -> Result<(), DenoError> { tokio_util::block_on(self.execute_mod_async(module_specifier, is_prefetch)) } /// Applies source map to the error. - fn apply_source_map(&self, err: JSError) -> JSError { - js_errors::apply_source_map(&err, &self.state.dir) + fn apply_source_map(&self, err: DenoError) -> DenoError { + err.apply_source_map(&self.state.dir) } } impl Future for Worker { type Item = (); - type Error = JSError; + type Error = DenoError; fn poll(&mut self) -> Result, Self::Error> { let mut isolate = self.isolate.lock().unwrap(); - isolate.poll().map_err(|err| self.apply_source_map(err)) + isolate + .poll() + .map_err(|err| self.apply_source_map(DenoError::from(err))) } } #[cfg(test)] mod tests { use super::*; + use crate::deno_error::err_check; use crate::flags; use crate::ops::op_selector_std; use crate::progress::Progress; @@ -132,7 +135,6 @@ mod tests { use crate::startup_data; use crate::state::ThreadSafeState; use crate::tokio_util; - use deno::js_check; use futures::future::lazy; use std::sync::atomic::Ordering; @@ -208,7 +210,7 @@ mod tests { startup_data::deno_isolate_init(), state, ); - js_check(worker.execute("denoMain()")); + err_check(worker.execute("denoMain()")); let result = worker.execute_mod(&module_specifier, false); if let Err(err) = result { eprintln!("execute_mod err {:?}", err); @@ -229,8 +231,8 @@ mod tests { ]); let mut worker = Worker::new("TEST".to_string(), startup_data::deno_isolate_init(), state); - js_check(worker.execute("denoMain()")); - js_check(worker.execute("workerMain()")); + err_check(worker.execute("denoMain()")); + err_check(worker.execute("workerMain()")); worker } @@ -251,7 +253,7 @@ mod tests { console.log("after postMessage"); } "#; - js_check(worker.execute(source)); + err_check(worker.execute(source)); let resource = worker.state.resource.clone(); let resource_ = resource.clone(); @@ -259,7 +261,7 @@ mod tests { tokio::spawn(lazy(move || { worker.then(move |r| -> Result<(), ()> { resource_.close(); - js_check(r); + err_check(r); Ok(()) }) })); @@ -289,7 +291,7 @@ mod tests { fn removed_from_resource_table_on_close() { tokio_util::init(|| { let mut worker = create_test_worker(); - js_check( + err_check( worker.execute("onmessage = () => { delete window.onmessage; }"), ); @@ -300,7 +302,7 @@ mod tests { .then(move |r| -> Result<(), ()> { resource.close(); println!("workers.rs after resource close"); - js_check(r); + err_check(r); Ok(()) }).shared(); @@ -322,7 +324,7 @@ mod tests { #[test] fn execute_mod_resolve_error() { tokio_util::init(|| { - // "foo" is not a vailid module specifier so this should return an error. + // "foo" is not a valid module specifier so this should return an error. let mut worker = create_test_worker(); let module_specifier = ModuleSpecifier::resolve_root("does-not-exist").unwrap(); diff --git a/core/modules.rs b/core/modules.rs index a8252bea5f25b3..8a229e7706a90e 100644 --- a/core/modules.rs +++ b/core/modules.rs @@ -142,8 +142,6 @@ impl RecursiveLoad { } } -// TODO(ry) This is basically the same thing as RustOrJsError. They should be -// combined into one type. #[derive(Debug, PartialEq)] pub enum JSErrorOr { JSError(JSError), diff --git a/tests/async_error.ts.out b/tests/async_error.ts.out index 2bd958f9a440ec..d07ba8cfe0135a 100644 --- a/tests/async_error.ts.out +++ b/tests/async_error.ts.out @@ -1,9 +1,11 @@ [WILDCARD]hello before error world -[WILDCARD]tests/async_error.ts:4:10 - throw Error("error"); +error: Uncaught Error: error +[WILDCARD]tests/async_error.ts:4:9 + +4 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/config.ts.out b/tests/config.ts.out index a57d3056b6940d..db5a8340e1d685 100644 --- a/tests/config.ts.out +++ b/tests/config.ts.out @@ -1,7 +1,9 @@ [WILDCARD]Unsupported compiler options in "[WILDCARD]config.tsconfig.json" The following options were ignored: module, target -[WILDCARD]tests/config.ts:3:5 - error TS2532: Object is possibly 'undefined'. +[WILDCARD]error TS2532: Object is possibly 'undefined'. + +[WILDCARD]tests/config.ts:3:5 3 if (map.get("bar").foo) { ~~~~~~~~~~~~~~ diff --git a/tests/error_001.ts.out b/tests/error_001.ts.out index 7a84912255ee7d..3c7e2828ea7de3 100644 --- a/tests/error_001.ts.out +++ b/tests/error_001.ts.out @@ -1,4 +1,9 @@ -[WILDCARD]Error: bad - at foo (file://[WILDCARD]tests/error_001.ts:2:9) - at bar (file://[WILDCARD]tests/error_001.ts:6:3) - at file://[WILDCARD]tests/error_001.ts:9:1 +[WILDCARD]error: Uncaught Error: bad +[WILDCARD]tests/error_001.ts:2:9 + +2 throw Error("bad"); + ^ + + at foo ([WILDCARD]tests/error_001.ts:2:9) + at bar ([WILDCARD]tests/error_001.ts:6:3) + at [WILDCARD]tests/error_001.ts:9:1 diff --git a/tests/error_002.ts.out b/tests/error_002.ts.out index 0f3b0830335f7a..292544a3371c22 100644 --- a/tests/error_002.ts.out +++ b/tests/error_002.ts.out @@ -1,4 +1,9 @@ -[WILDCARD]Error: exception from mod1 - at throwsError (file://[WILDCARD]/tests/subdir/mod1.ts:16:9) - at foo (file://[WILDCARD]/tests/error_002.ts:4:3) - at file://[WILDCARD]/tests/error_002.ts:7:1 +[WILDCARD]error: Uncaught Error: exception from mod1 +[WILDCARD]tests/subdir/mod1.ts:16:9 + +16 throw Error("exception from mod1"); + ^ + + at throwsError ([WILDCARD]tests/subdir/mod1.ts:16:9) + at foo ([WILDCARD]tests/error_002.ts:4:3) + at [WILDCARD]tests/error_002.ts:7:1 diff --git a/tests/error_003_typescript.ts.out b/tests/error_003_typescript.ts.out index e2efd563996dad..f00a935e452d8d 100644 --- a/tests/error_003_typescript.ts.out +++ b/tests/error_003_typescript.ts.out @@ -1,18 +1,24 @@ -[WILDCARD]/tests/error_003_typescript.ts:20:3 - error TS2322: Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'. +[WILDCARD]error TS2322: Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'. Types of parameters 'o' and 'r' are incompatible. Type 'B' is not assignable to type 'T'. 'B' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'. +[WILDCARD]tests/error_003_typescript.ts:20:3 + 20 values: o => [ ~~~~~~ - [WILDCARD]/tests/error_003_typescript.ts:8:3 + The expected type comes from property 'values' which is declared here on type 'C' + + [WILDCARD]tests/error_003_typescript.ts:8:3 8 values?: (r: T) => Array>; ~~~~~~ - The expected type comes from property 'values' which is declared here on type 'C' -[WILDCARD]/tests/error_003_typescript.ts:22:12 - error TS2339: Property 't' does not exist on type 'T'. + +error TS2339: Property 't' does not exist on type 'T'. + +[WILDCARD]tests/error_003_typescript.ts:22:12 22 v: o.t, ^ diff --git a/tests/error_004_missing_module.ts.out b/tests/error_004_missing_module.ts.out index ca43bd5ff04cbc..6add996f2afc31 100644 --- a/tests/error_004_missing_module.ts.out +++ b/tests/error_004_missing_module.ts.out @@ -1,4 +1,5 @@ -[WILDCARD]Uncaught NotFound: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]/tests/error_004_missing_module.ts" +[WILDCARD]error: Uncaught NotFound: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]tests/error_004_missing_module.ts" +[WILDCARD] js/errors.ts:[WILDCARD] at DenoError (js/errors.ts:[WILDCARD]) at maybeError (js/errors.ts:[WILDCARD]) at maybeThrowError (js/errors.ts:[WILDCARD]) diff --git a/tests/error_005_missing_dynamic_import.ts.out b/tests/error_005_missing_dynamic_import.ts.out index ef4972940489d1..7c161e7546dba2 100644 --- a/tests/error_005_missing_dynamic_import.ts.out +++ b/tests/error_005_missing_dynamic_import.ts.out @@ -1,4 +1,5 @@ -[WILDCARD]NotFound: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]/tests/error_005_missing_dynamic_import.ts" +[WILDCARD]error: Uncaught NotFound: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]tests/error_005_missing_dynamic_import.ts" +[WILDCARD] js/errors.ts:[WILDCARD] at DenoError (js/errors.ts:[WILDCARD]) at maybeError (js/errors.ts:[WILDCARD]) at maybeThrowError (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 335a7f4a8b1759..797b785020cc7c 100644 --- a/tests/error_006_import_ext_failure.ts.out +++ b/tests/error_006_import_ext_failure.ts.out @@ -1,4 +1,5 @@ -[WILDCARD]Uncaught NotFound: Cannot resolve module "[WILDCARD]/non-existent" from "[WILDCARD]/tests/error_006_import_ext_failure.ts" +[WILDCARD]error: Uncaught NotFound: Cannot resolve module "[WILDCARD]/non-existent" from "[WILDCARD]tests/error_006_import_ext_failure.ts" +[WILDCARD] js/errors.ts:[WILDCARD] at DenoError (js/errors.ts:[WILDCARD]) at maybeError (js/errors.ts:[WILDCARD]) at maybeThrowError (js/errors.ts:[WILDCARD]) diff --git a/tests/error_007_any.ts.out b/tests/error_007_any.ts.out index f577d6070782cb..45dbffd04a9e35 100644 --- a/tests/error_007_any.ts.out +++ b/tests/error_007_any.ts.out @@ -1 +1 @@ -[WILDCARD]Uncaught # +[WILDCARD]error: Uncaught # diff --git a/tests/error_008_checkjs.js.out b/tests/error_008_checkjs.js.out index 2341af23fb73cd..5c50e85131ab65 100644 --- a/tests/error_008_checkjs.js.out +++ b/tests/error_008_checkjs.js.out @@ -1,5 +1,7 @@ -[WILDCARD]tests/error_008_checkjs.js:2:0 -consol.log("hello world!"); -^ -Uncaught ReferenceError: consol is not defined +[WILDCARD]error: Uncaught ReferenceError: consol is not defined +[WILDCARD]tests/error_008_checkjs.js:2:1 + +2 consol.log("hello world!"); + ^ + at [WILDCARD]tests/error_008_checkjs.js:2:1 diff --git a/tests/error_011_bad_module_specifier.ts.out b/tests/error_011_bad_module_specifier.ts.out index ed005206255d54..4225327653655e 100644 --- a/tests/error_011_bad_module_specifier.ts.out +++ b/tests/error_011_bad_module_specifier.ts.out @@ -1,4 +1,5 @@ -[WILDCARD]Uncaught RelativeUrlWithCannotBeABaseBase: relative URL with a cannot-be-a-base base +[WILDCARD]error: Uncaught RelativeUrlWithCannotBeABaseBase: relative URL with a cannot-be-a-base base +[WILDCARD] js/errors.ts:[WILDCARD] at DenoError (js/errors.ts:[WILDCARD]) at maybeError (js/errors.ts:[WILDCARD]) at maybeThrowError (js/errors.ts:[WILDCARD]) diff --git a/tests/error_012_bad_dynamic_import_specifier.ts.out b/tests/error_012_bad_dynamic_import_specifier.ts.out index cdfc547f926931..1337028b2141b8 100644 --- a/tests/error_012_bad_dynamic_import_specifier.ts.out +++ b/tests/error_012_bad_dynamic_import_specifier.ts.out @@ -1,4 +1,5 @@ -[WILDCARD]Uncaught RelativeUrlWithCannotBeABaseBase: relative URL with a cannot-be-a-base base +[WILDCARD]error: Uncaught RelativeUrlWithCannotBeABaseBase: relative URL with a cannot-be-a-base base +[WILDCARD] js/errors.ts:[WILDCARD] at DenoError (js/errors.ts:[WILDCARD]) at maybeError (js/errors.ts:[WILDCARD]) at maybeThrowError (js/errors.ts:[WILDCARD]) diff --git a/tests/error_syntax.js.out b/tests/error_syntax.js.out index ac2f99f1fc1f0e..6253f3dd5bcbfd 100644 --- a/tests/error_syntax.js.out +++ b/tests/error_syntax.js.out @@ -1,4 +1,6 @@ -[WILDCARD]tests/error_syntax.js:3:5 -(the following is a syntax error ^^ ! ) - ^^^^^^^^^ -Uncaught SyntaxError: Unexpected identifier +error: Uncaught SyntaxError: Unexpected identifier +[WILDCARD]tests/error_syntax.js:3:6 + +3 (the following is a syntax error ^^ ! ) + ~~~~~~~~~ +