From a71305b4febc3d8db95d3d144ae3a64c023718f0 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Tue, 4 Jun 2019 23:03:56 +1000 Subject: [PATCH] Handle compiler diagnostics in Rust (#2445) --- cli/BUILD.gn | 1 + cli/ansi.rs | 26 ++ cli/compiler.rs | 19 +- cli/diagnostics.rs | 668 ++++++++++++++++++++++++++++++ cli/main.rs | 1 + cli/worker.rs | 3 +- js/compiler.ts | 126 +++--- js/diagnostics.ts | 218 ++++++++++ package.json | 2 +- rollup.config.js | 1 + tests/013_dynamic_import.ts | 8 +- tests/error_003_typescript.test | 1 + tests/error_003_typescript.ts | 28 +- tests/error_003_typescript.ts.out | 26 +- tests/error_003_typescript2.test | 1 + third_party | 2 +- 16 files changed, 1044 insertions(+), 87 deletions(-) create mode 100644 cli/diagnostics.rs create mode 100644 js/diagnostics.ts diff --git a/cli/BUILD.gn b/cli/BUILD.gn index 7887624e21..45386f3208 100644 --- a/cli/BUILD.gn +++ b/cli/BUILD.gn @@ -74,6 +74,7 @@ ts_sources = [ "../js/core.ts", "../js/custom_event.ts", "../js/deno.ts", + "../js/diagnostics.ts", "../js/dir.ts", "../js/dispatch.ts", "../js/dispatch_minimal.ts", diff --git a/cli/ansi.rs b/cli/ansi.rs index 95b5e06947..b9e9fe1235 100644 --- a/cli/ansi.rs +++ b/cli/ansi.rs @@ -1,6 +1,8 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use ansi_term::Color::Black; use ansi_term::Color::Fixed; use ansi_term::Color::Red; +use ansi_term::Color::White; use ansi_term::Style; use regex::Regex; use std::env; @@ -43,6 +45,14 @@ pub fn italic_bold(s: String) -> impl fmt::Display { style.paint(s) } +pub fn black_on_white(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.on(White).fg(Black); + } + style.paint(s) +} + pub fn yellow(s: String) -> impl fmt::Display { let mut style = Style::new(); if use_color() { @@ -61,6 +71,22 @@ pub fn cyan(s: String) -> impl fmt::Display { style.paint(s) } +pub fn red(s: String) -> impl fmt::Display { + let mut style = Style::new(); + if use_color() { + style = style.fg(Red); + } + 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 e1bb561307..0b6c278f9f 100644 --- a/cli/compiler.rs +++ b/cli/compiler.rs @@ -1,4 +1,5 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::diagnostics::Diagnostic; use crate::msg; use crate::resources; use crate::startup_data; @@ -7,7 +8,6 @@ use crate::tokio_util; use crate::worker::Worker; use deno::js_check; use deno::Buf; -use deno::JSError; use futures::Future; use futures::Stream; use std::str; @@ -87,7 +87,7 @@ pub fn compile_async( specifier: &str, referrer: &str, module_meta_data: &ModuleMetaData, -) -> impl Future { +) -> impl Future { debug!( "Running rust part of compile_sync. specifier: {}, referrer: {}", &specifier, &referrer @@ -136,14 +136,15 @@ pub fn compile_async( first_msg_fut .map_err(|_| panic!("not handled")) .and_then(move |maybe_msg: Option| { - let _res_msg = maybe_msg.unwrap(); - debug!("Received message from worker"); - // TODO res is EmitResult, use serde_derive to parse it. Errors from the - // worker or Diagnostics should be somehow forwarded to the caller! - // Currently they are handled inside compiler.ts with os.exit(1) and above - // with std::process::exit(1). This bad. + if let Some(msg) = maybe_msg { + 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); + } + } let r = state.dir.fetch_module_meta_data( &module_meta_data_.module_name, @@ -169,7 +170,7 @@ pub fn compile_sync( specifier: &str, referrer: &str, module_meta_data: &ModuleMetaData, -) -> Result { +) -> Result { tokio_util::block_on(compile_async( state, specifier, diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs new file mode 100644 index 0000000000..af384f277b --- /dev/null +++ b/cli/diagnostics.rs @@ -0,0 +1,668 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +//! This module encodes TypeScript errors (diagnostics) into Rust structs and +//! contains code for printing them to the console. +use crate::ansi; +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, +} + +impl Diagnostic { + /// Take a JSON value and attempt to map it to a + pub fn from_json_value(v: &serde_json::Value) -> Option { + if !v.is_object() { + return None; + } + let obj = v.as_object().unwrap(); + + let mut items = Vec::::new(); + let items_v = &obj["items"]; + if items_v.is_array() { + let items_values = items_v.as_array().unwrap(); + + for item_v in items_values { + items.push(DiagnosticItem::from_json_value(item_v)); + } + } + + Some(Self { items }) + } + + pub fn from_emit_result(json_str: &str) -> Option { + let v = serde_json::from_str::(json_str) + .expect("Error decoding JSON string."); + let diagnostics_o = v.get("diagnostics"); + if let Some(diagnostics_v) = diagnostics_o { + return Self::from_json_value(diagnostics_v); + } + + None + } +} + +impl fmt::Display for Diagnostic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut i = 0; + for item in &self.items { + if i > 0 { + writeln!(f)?; + } + write!(f, "{}", item.to_string())?; + i += 1; + } + + if i > 1 { + write!(f, "\n\nFound {} errors.\n", i)?; + } + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct DiagnosticItem { + /// The top level message relating to the diagnostic item. + pub message: String, + + /// A chain of messages, code, and categories of messages which indicate the + /// full diagnostic information. + pub message_chain: Option>, + + /// Other diagnostic items that are related to the diagnostic, usually these + /// are suggestions of why an error occurred. + pub related_information: Option>, + + /// The source line the diagnostic is in reference to. + pub source_line: Option, + + /// Zero-based index to the line number of the error. + pub line_number: Option, + + /// The resource name provided to the TypeScript compiler. + pub script_resource_name: Option, + + /// Zero-based index to the start position in the entire script resource. + pub start_position: Option, + + /// Zero-based index to the end position in the entire script resource. + pub end_position: Option, + pub category: DiagnosticCategory, + + /// This is defined in TypeScript and can be referenced via + /// [diagnosticMessages.json](https://github.com/microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json). + pub code: i64, + + /// Zero-based index to the start column on `line_number`. + pub start_column: Option, + + /// Zero-based index to the end column on `line_number`. + pub end_column: Option, +} + +impl DiagnosticItem { + pub fn from_json_value(v: &serde_json::Value) -> Self { + let obj = v.as_object().unwrap(); + + // required attributes + let message = obj + .get("message") + .and_then(|v| v.as_str().map(String::from)) + .unwrap(); + let category = DiagnosticCategory::from( + obj.get("category").and_then(Value::as_i64).unwrap(), + ); + let code = obj.get("code").and_then(Value::as_i64).unwrap(); + + // optional attributes + let source_line = obj + .get("sourceLine") + .and_then(|v| v.as_str().map(String::from)); + let script_resource_name = obj + .get("scriptResourceName") + .and_then(|v| v.as_str().map(String::from)); + let line_number = obj.get("lineNumber").and_then(Value::as_i64); + let start_position = obj.get("startPosition").and_then(Value::as_i64); + let end_position = obj.get("endPosition").and_then(Value::as_i64); + let start_column = obj.get("startColumn").and_then(Value::as_i64); + let end_column = obj.get("endColumn").and_then(Value::as_i64); + + let message_chain_v = obj.get("messageChain"); + let message_chain = match message_chain_v { + Some(v) => DiagnosticMessageChain::from_json_value(v), + _ => None, + }; + + let related_information_v = obj.get("relatedInformation"); + let related_information = match related_information_v { + Some(r) => { + let mut related_information = Vec::::new(); + let related_info_values = r.as_array().unwrap(); + + for related_info_v in related_info_values { + related_information + .push(DiagnosticItem::from_json_value(related_info_v)); + } + + Some(related_information) + } + _ => None, + }; + + Self { + message, + message_chain, + related_information, + code, + source_line, + script_resource_name, + line_number, + start_position, + end_position, + category, + start_column, + end_column, + } + } +} + +// 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())) + } + 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(); + + format!("{}{} ", category, code) + } + + fn format_message(&self, level: usize) -> String { + if self.message_chain.is_none() { + return format!("{:indent$}{}", "", self.message, indent = level); + } + + let mut s = String::new(); + let mut i = level / 2; + let mut item_o = self.message_chain.clone(); + while item_o.is_some() { + let item = item_o.unwrap(); + s.push_str(&std::iter::repeat(" ").take(i * 2).collect::()); + s.push_str(&item.message); + s.push('\n'); + item_o = item.next.clone(); + i += 1; + } + s.pop(); + + s + } + + fn format_related_info(&self) -> String { + if self.related_information.is_none() { + return "".to_string(); + } + + let mut s = String::new(); + let related_information = self.related_information.clone().unwrap(); + for related_diagnostic in related_information { + let rd = &related_diagnostic; + s.push_str(&format!( + "\n{}{}{}\n", + rd.format_source_name(2), + rd.format_source_line(4), + rd.format_message(4), + )); + } + + s + } + + 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 + ) + } + + 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 + ) + } +} + +impl fmt::Display for DiagnosticItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}{}{}{}{}", + self.format_source_name(0), + self.format_category_and_code(), + self.format_message(0), + self.format_source_line(0), + self.format_related_info(), + )?; + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct DiagnosticMessageChain { + pub message: String, + pub code: i64, + pub category: DiagnosticCategory, + pub next: Option>, +} + +impl DiagnosticMessageChain { + pub fn from_json_value(v: &serde_json::Value) -> Option> { + if !v.is_object() { + return None; + } + + let obj = v.as_object().unwrap(); + let message = obj + .get("message") + .and_then(|v| v.as_str().map(String::from)) + .unwrap(); + let code = obj.get("code").and_then(Value::as_i64).unwrap(); + let category = DiagnosticCategory::from( + obj.get("category").and_then(Value::as_i64).unwrap(), + ); + + let next_v = obj.get("next"); + let next = match next_v { + Some(n) => DiagnosticMessageChain::from_json_value(n), + _ => None, + }; + + Some(Box::new(Self { + message, + code, + category, + next, + })) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum DiagnosticCategory { + Log, // 0 + Debug, // 1 + Info, // 2 + Error, // 3 + Warning, // 4 + Suggestion, // 5 +} + +impl From for DiagnosticCategory { + fn from(value: i64) -> Self { + match value { + 0 => DiagnosticCategory::Log, + 1 => DiagnosticCategory::Debug, + 2 => DiagnosticCategory::Info, + 3 => DiagnosticCategory::Error, + 4 => DiagnosticCategory::Warning, + 5 => DiagnosticCategory::Suggestion, + _ => panic!("Unknown value: {}", value), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ansi::strip_ansi_codes; + + fn diagnostic1() -> Diagnostic { + Diagnostic { + items: vec![ + DiagnosticItem { + message: "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.".to_string(), + message_chain: Some(Box::new(DiagnosticMessageChain { + message: "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.".to_string(), + code: 2322, + category: DiagnosticCategory::Error, + next: Some(Box::new(DiagnosticMessageChain { + message: "Types of parameters 'o' and 'r' are incompatible.".to_string(), + code: 2328, + category: DiagnosticCategory::Error, + next: Some(Box::new(DiagnosticMessageChain { + message: "Type 'B' is not assignable to type 'T'.".to_string(), + code: 2322, + category: DiagnosticCategory::Error, + next: 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: Some(vec![ + DiagnosticItem { + message: "The expected type comes from property 'values' which is declared here on type 'SettingsInterface'".to_string(), + message_chain: None, + related_information: None, + code: 6500, + source_line: Some(" values?: (r: T) => Array>;".to_string()), + script_resource_name: Some("deno/tests/complex_diagnostics.ts".to_string()), + line_number: Some(6), + start_position: Some(94), + end_position: Some(100), + category: DiagnosticCategory::Info, + start_column: Some(2), + end_column: Some(8), + } + ]) + } + ] + } + } + + fn diagnostic2() -> 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, + }, + ], + } + } + + #[test] + fn from_json() { + let v = serde_json::from_str::( + &r#"{ + "items": [ + { + "message": "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.", + "messageChain": { + "message": "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.", + "code": 2322, + "category": 3, + "next": { + "message": "Types of parameters 'o' and 'r' are incompatible.", + "code": 2328, + "category": 3, + "next": { + "message": "Type 'B' is not assignable to type 'T'.", + "code": 2322, + "category": 3 + } + } + }, + "code": 2322, + "category": 3, + "startPosition": 235, + "endPosition": 241, + "sourceLine": " values: o => [", + "lineNumber": 18, + "scriptResourceName": "/deno/tests/complex_diagnostics.ts", + "startColumn": 2, + "endColumn": 8, + "relatedInformation": [ + { + "message": "The expected type comes from property 'values' which is declared here on type 'C'", + "code": 6500, + "category": 2, + "startPosition": 78, + "endPosition": 84, + "sourceLine": " values?: (r: T) => Array>;", + "lineNumber": 6, + "scriptResourceName": "/deno/tests/complex_diagnostics.ts", + "startColumn": 2, + "endColumn": 8 + } + ] + }, + { + "message": "Property 't' does not exist on type 'T'.", + "code": 2339, + "category": 3, + "startPosition": 267, + "endPosition": 268, + "sourceLine": " v: o.t,", + "lineNumber": 20, + "scriptResourceName": "/deno/tests/complex_diagnostics.ts", + "startColumn": 11, + "endColumn": 12 + } + ] + }"#, + ).unwrap(); + let r = Diagnostic::from_json_value(&v); + let expected = Some(Diagnostic { + items: vec![ + DiagnosticItem { + message: "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.".to_string(), + message_chain: Some(Box::new(DiagnosticMessageChain { + message: "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.".to_string(), + code: 2322, + category: DiagnosticCategory::Error, + next: Some(Box::new(DiagnosticMessageChain { + message: "Types of parameters 'o' and 'r' are incompatible.".to_string(), + code: 2328, + category: DiagnosticCategory::Error, + next: Some(Box::new(DiagnosticMessageChain { + message: "Type 'B' is not assignable to type 'T'.".to_string(), + code: 2322, + category: DiagnosticCategory::Error, + next: None, + })), + })), + })), + related_information: Some(vec![ + DiagnosticItem { + message: "The expected type comes from property 'values' which is declared here on type 'C'".to_string(), + message_chain: None, + related_information: None, + source_line: Some(" values?: (r: T) => Array>;".to_string()), + line_number: Some(6), + script_resource_name: Some("/deno/tests/complex_diagnostics.ts".to_string()), + start_position: Some(78), + end_position: Some(84), + category: DiagnosticCategory::Info, + code: 6500, + start_column: Some(2), + end_column: Some(8), + } + ]), + source_line: Some(" values: o => [".to_string()), + line_number: Some(18), + script_resource_name: Some("/deno/tests/complex_diagnostics.ts".to_string()), + start_position: Some(235), + end_position: Some(241), + category: DiagnosticCategory::Error, + code: 2322, + start_column: Some(2), + end_column: Some(8), + }, + DiagnosticItem { + message: "Property 't' does not exist on type 'T'.".to_string(), + message_chain: None, + related_information: None, + source_line: Some(" v: o.t,".to_string()), + line_number: Some(20), + script_resource_name: Some("/deno/tests/complex_diagnostics.ts".to_string()), + start_position: Some(267), + end_position: Some(268), + category: DiagnosticCategory::Error, + code: 2339, + start_column: Some(11), + end_column: Some(12), + }, + ], + }); + assert_eq!(expected, r); + } + + #[test] + fn from_emit_result() { + let r = Diagnostic::from_emit_result( + &r#"{ + "emitSkipped": false, + "diagnostics": { + "items": [ + { + "message": "foo bar", + "code": 9999, + "category": 3 + } + ] + } + }"#, + ); + let expected = Some(Diagnostic { + items: vec![DiagnosticItem { + message: "foo bar".to_string(), + message_chain: None, + related_information: None, + source_line: None, + line_number: None, + script_resource_name: None, + start_position: None, + end_position: None, + category: DiagnosticCategory::Error, + code: 9999, + start_column: None, + end_column: None, + }], + }); + assert_eq!(expected, r); + } + + #[test] + fn from_emit_result_none() { + let r = &r#"{"emitSkipped":false}"#; + assert!(Diagnostic::from_emit_result(r).is_none()); + } + + #[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"; + 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"; + assert_eq!(expected, strip_ansi_codes(&d.to_string())); + } +} diff --git a/cli/main.rs b/cli/main.rs index 5d70805ae2..953e019430 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -16,6 +16,7 @@ extern crate rand; mod ansi; pub mod compiler; pub mod deno_dir; +pub mod diagnostics; mod dispatch_minimal; pub mod errors; pub mod flags; diff --git a/cli/worker.rs b/cli/worker.rs index 59eecda6fc..c08a433857 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -4,7 +4,6 @@ use crate::compiler::ModuleMetaData; use crate::errors::DenoError; use crate::errors::RustOrJsError; use crate::js_errors; -use crate::js_errors::JSErrorColor; use crate::msg; use crate::state::ThreadSafeState; use crate::tokio_util; @@ -233,7 +232,7 @@ fn fetch_module_meta_data_and_maybe_compile_async( compile_async(state_.clone(), &specifier, &referrer, &out) .map_err(|e| { debug!("compiler error exiting!"); - eprintln!("{}", JSErrorColor(&e).to_string()); + eprintln!("\n{}", e.to_string()); std::process::exit(1); }).and_then(move |out| { debug!(">>>>> compile_sync END"); diff --git a/js/compiler.ts b/js/compiler.ts index 15adba7461..6b0881700d 100644 --- a/js/compiler.ts +++ b/js/compiler.ts @@ -1,6 +1,7 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import * as msg from "gen/cli/msg_generated"; import { core } from "./core"; +import { Diagnostic, fromTypeScriptDiagnostic } from "./diagnostics"; import * as flatbuffers from "./flatbuffers"; import { sendSync } from "./dispatch"; import { TextDecoder } from "./text_encoding"; @@ -37,6 +38,11 @@ interface CompilerReq { config?: string; } +interface ConfigureResponse { + ignoredOptions?: string[]; + diagnostics?: ts.Diagnostic[]; +} + /** Options that either do nothing in Deno, or would cause undesired behavior * if modified. */ const ignoredCompilerOptions: ReadonlyArray = [ @@ -105,6 +111,11 @@ interface ModuleMetaData { sourceCode: string | undefined; } +interface EmitResult { + emitSkipped: boolean; + diagnostics?: Diagnostic; +} + function fetchModuleMetaData( specifier: string, referrer: string @@ -193,22 +204,19 @@ class Host implements ts.CompilerHost { * compiler's configuration options. The method returns an array of compiler * options which were ignored, or `undefined`. */ - configure(path: string, configurationText: string): string[] | undefined { + configure(path: string, configurationText: string): ConfigureResponse { util.log("compile.configure", path); const { config, error } = ts.parseConfigFileTextToJson( path, configurationText ); if (error) { - this._logDiagnostics([error]); + return { diagnostics: [error] }; } const { options, errors } = ts.convertCompilerOptionsFromJson( config.compilerOptions, cwd() ); - if (errors.length) { - this._logDiagnostics(errors); - } const ignoredOptions: string[] = []; for (const key of Object.keys(options)) { if ( @@ -220,7 +228,10 @@ class Host implements ts.CompilerHost { } } Object.assign(this._options, options); - return ignoredOptions.length ? ignoredOptions : undefined; + return { + ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined, + diagnostics: errors.length ? errors : undefined + }; } getCompilationSettings(): ts.CompilerOptions { @@ -228,19 +239,6 @@ class Host implements ts.CompilerHost { return this._options; } - /** Log TypeScript diagnostics to the console and exit */ - _logDiagnostics(diagnostics: ReadonlyArray): never { - const errMsg = os.noColor - ? ts.formatDiagnostics(diagnostics, this) - : ts.formatDiagnosticsWithColorAndContext(diagnostics, this); - - console.log(errMsg); - // TODO The compiler isolate shouldn't call os.exit(). (In fact, it - // shouldn't even have access to call that op.) Errors should be forwarded - // to to the caller and the caller exit. - return os.exit(1); - } - fileExists(_fileName: string): boolean { return notImplemented(); } @@ -362,10 +360,17 @@ class Host implements ts.CompilerHost { window.compilerMain = function compilerMain(): void { // workerMain should have already been called since a compiler is a worker. window.onmessage = ({ data }: { data: CompilerReq }): void => { + let emitSkipped = true; + let diagnostics: ts.Diagnostic[] | undefined; + const { rootNames, configPath, config } = data; const host = new Host(); - if (config && config.length) { - const ignoredOptions = host.configure(configPath!, config); + + // if there is a configuration supplied, we need to parse that + if (config && config.length && configPath) { + const configResult = host.configure(configPath, config); + const ignoredOptions = configResult.ignoredOptions; + diagnostics = configResult.diagnostics; if (ignoredOptions) { console.warn( yellow(`Unsupported compiler options in "${configPath}"\n`) + @@ -377,51 +382,52 @@ window.compilerMain = function compilerMain(): void { } } - const options = host.getCompilationSettings(); - const program = ts.createProgram(rootNames, options, host); + // if there was a configuration and no diagnostics with it, we will continue + // to generate the program and possibly emit it. + if (!diagnostics || (diagnostics && diagnostics.length === 0)) { + const options = host.getCompilationSettings(); + const program = ts.createProgram(rootNames, options, host); - const preEmitDiagnostics = ts.getPreEmitDiagnostics(program).filter( - ({ code }): boolean => { - // TS2691: An import path cannot end with a '.ts' extension. Consider - // importing 'bad-module' instead. - if (code === 2691) return false; - // TS5009: Cannot find the common subdirectory path for the input files. - if (code === 5009) return false; - // TS5055: Cannot write file - // 'http://localhost:4545/tests/subdir/mt_application_x_javascript.j4.js' - // because it would overwrite input file. - if (code === 5055) return false; - // TypeScript is overly opinionated that only CommonJS modules kinds can - // support JSON imports. Allegedly this was fixed in - // Microsoft/TypeScript#26825 but that doesn't seem to be working here, - // so we will ignore complaints about this compiler setting. - if (code === 5070) return false; - return true; + diagnostics = ts.getPreEmitDiagnostics(program).filter( + ({ code }): boolean => { + // TS2691: An import path cannot end with a '.ts' extension. Consider + // importing 'bad-module' instead. + if (code === 2691) return false; + // TS5009: Cannot find the common subdirectory path for the input files. + if (code === 5009) return false; + // TS5055: Cannot write file + // 'http://localhost:4545/tests/subdir/mt_application_x_javascript.j4.js' + // because it would overwrite input file. + if (code === 5055) return false; + // TypeScript is overly opinionated that only CommonJS modules kinds can + // support JSON imports. Allegedly this was fixed in + // Microsoft/TypeScript#26825 but that doesn't seem to be working here, + // so we will ignore complaints about this compiler setting. + if (code === 5070) return false; + return true; + } + ); + + // We will only proceed with the emit if there are no diagnostics. + if (diagnostics && diagnostics.length === 0) { + const emitResult = program.emit(); + emitSkipped = emitResult.emitSkipped; + // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned + // without casting. + diagnostics = emitResult.diagnostics as ts.Diagnostic[]; } - ); - if (preEmitDiagnostics.length > 0) { - host._logDiagnostics(preEmitDiagnostics); - // The above _logDiagnostics calls os.exit(). The return is here just for - // clarity. - return; } - const emitResult = program!.emit(); + const result: EmitResult = { + emitSkipped, + diagnostics: diagnostics.length + ? fromTypeScriptDiagnostic(diagnostics) + : undefined + }; - // TODO(ry) Print diagnostics in Rust. - // https://github.com/denoland/deno/pull/2310 + postMessage(result); - const { diagnostics } = emitResult; - if (diagnostics.length > 0) { - host._logDiagnostics(diagnostics); - // The above _logDiagnostics calls os.exit(). The return is here just for - // clarity. - return; - } - - postMessage(emitResult); - - // The compiler isolate exits after a single messsage. + // The compiler isolate exits after a single message. workerClose(); }; }; diff --git a/js/diagnostics.ts b/js/diagnostics.ts new file mode 100644 index 0000000000..1207eca4fd --- /dev/null +++ b/js/diagnostics.ts @@ -0,0 +1,218 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +// Diagnostic provides an abstraction for advice/errors received from a +// compiler, which is strongly influenced by the format of TypeScript +// diagnostics. + +import * as ts from "typescript"; + +/** The log category for a diagnostic message */ +export enum DiagnosticCategory { + Log = 0, + Debug = 1, + Info = 2, + Error = 3, + Warning = 4, + Suggestion = 5 +} + +export interface DiagnosticMessageChain { + message: string; + category: DiagnosticCategory; + code: number; + next?: DiagnosticMessageChain; +} + +export interface DiagnosticItem { + /** A string message summarizing the diagnostic. */ + message: string; + + /** An ordered array of further diagnostics. */ + messageChain?: DiagnosticMessageChain; + + /** Information related to the diagnostic. This is present when there is a + * suggestion or other additional diagnostic information */ + relatedInformation?: DiagnosticItem[]; + + /** The text of the source line related to the diagnostic */ + sourceLine?: string; + + /** The line number that is related to the diagnostic */ + lineNumber?: number; + + /** The name of the script resource related to the diagnostic */ + scriptResourceName?: string; + + /** The start position related to the diagnostic */ + startPosition?: number; + + /** The end position related to the diagnostic */ + endPosition?: number; + + /** The category of the diagnostic */ + category: DiagnosticCategory; + + /** A number identifier */ + code: number; + + /** The the start column of the sourceLine related to the diagnostic */ + startColumn?: number; + + /** The end column of the sourceLine related to the diagnostic */ + endColumn?: number; +} + +export interface Diagnostic { + /** An array of diagnostic items. */ + items: DiagnosticItem[]; +} + +interface SourceInformation { + sourceLine: string; + lineNumber: number; + scriptResourceName: string; + startColumn: number; + endColumn: number; +} + +function fromDiagnosticCategory( + category: ts.DiagnosticCategory +): DiagnosticCategory { + switch (category) { + case ts.DiagnosticCategory.Error: + return DiagnosticCategory.Error; + case ts.DiagnosticCategory.Message: + return DiagnosticCategory.Info; + case ts.DiagnosticCategory.Suggestion: + return DiagnosticCategory.Suggestion; + case ts.DiagnosticCategory.Warning: + return DiagnosticCategory.Warning; + default: + throw new Error( + `Unexpected DiagnosticCategory: "${category}"/"${ + ts.DiagnosticCategory[category] + }"` + ); + } +} + +function getSourceInformation( + sourceFile: ts.SourceFile, + start: number, + length: number +): SourceInformation { + const scriptResourceName = sourceFile.fileName; + const { + line: lineNumber, + character: startColumn + } = sourceFile.getLineAndCharacterOfPosition(start); + const endPosition = sourceFile.getLineAndCharacterOfPosition(start + length); + const endColumn = + lineNumber === endPosition.line ? endPosition.character : startColumn; + const lastLineInFile = sourceFile.getLineAndCharacterOfPosition( + sourceFile.text.length + ).line; + const lineStart = sourceFile.getPositionOfLineAndCharacter(lineNumber, 0); + const lineEnd = + lineNumber < lastLineInFile + ? sourceFile.getPositionOfLineAndCharacter(lineNumber + 1, 0) + : sourceFile.text.length; + const sourceLine = sourceFile.text + .slice(lineStart, lineEnd) + .replace(/\s+$/g, "") + .replace("\t", " "); + return { + sourceLine, + lineNumber, + scriptResourceName, + startColumn, + endColumn + }; +} + +/** Converts a TypeScript diagnostic message chain to a Deno one. */ +function fromDiagnosticMessageChain( + messageChain: ts.DiagnosticMessageChain | undefined +): DiagnosticMessageChain | undefined { + if (!messageChain) { + return undefined; + } + + const { messageText: message, code, category, next } = messageChain; + return { + message, + code, + category: fromDiagnosticCategory(category), + next: fromDiagnosticMessageChain(next) + }; +} + +/** Parse out information from a TypeScript diagnostic structure. */ +function parseDiagnostic( + item: ts.Diagnostic | ts.DiagnosticRelatedInformation +): DiagnosticItem { + const { + messageText, + category: sourceCategory, + code, + file, + start: startPosition, + length + } = item; + const sourceInfo = + file && startPosition && length + ? getSourceInformation(file, startPosition, length) + : undefined; + const endPosition = + startPosition && length ? startPosition + length : undefined; + const category = fromDiagnosticCategory(sourceCategory); + + let message: string; + let messageChain: DiagnosticMessageChain | undefined; + if (typeof messageText === "string") { + message = messageText; + } else { + message = messageText.messageText; + messageChain = fromDiagnosticMessageChain(messageText); + } + + const base = { + message, + messageChain, + code, + category, + startPosition, + endPosition + }; + + return sourceInfo ? { ...base, ...sourceInfo } : base; +} + +/** Convert a diagnostic related information array into a Deno diagnostic + * array. */ +function parseRelatedInformation( + relatedInformation: readonly ts.DiagnosticRelatedInformation[] +): DiagnosticItem[] { + const result: DiagnosticItem[] = []; + for (const item of relatedInformation) { + result.push(parseDiagnostic(item)); + } + return result; +} + +/** Convert TypeScript diagnostics to Deno diagnostics. */ +export function fromTypeScriptDiagnostic( + diagnostics: readonly ts.Diagnostic[] +): Diagnostic { + let items: DiagnosticItem[] = []; + for (const sourceDiagnostic of diagnostics) { + const item: DiagnosticItem = parseDiagnostic(sourceDiagnostic); + if (sourceDiagnostic.relatedInformation) { + item.relatedInformation = parseRelatedInformation( + sourceDiagnostic.relatedInformation + ); + } + items.push(item); + } + return { items }; +} diff --git a/package.json b/package.json index 7ac0274477..8f87d42290 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "eslint-config-prettier": "4.1.0", "flatbuffers": "1.9.0", "magic-string": "0.25.2", - "prettier": "1.16.4", + "prettier": "1.17.1", "rollup": "1.4.1", "rollup-plugin-alias": "1.5.1", "rollup-plugin-analyzer": "3.0.0", diff --git a/rollup.config.js b/rollup.config.js index 635aace0da..41f738baee 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -239,6 +239,7 @@ export default function makeConfig(commandOptions) { "parseConfigFileTextToJson", "version", "CompilerHost", + "DiagnosticCategory", "Extension", "ModuleKind", "ScriptKind", diff --git a/tests/013_dynamic_import.ts b/tests/013_dynamic_import.ts index 20dc508dbd..6bbce31328 100644 --- a/tests/013_dynamic_import.ts +++ b/tests/013_dynamic_import.ts @@ -1,9 +1,7 @@ (async (): Promise => { - const { - returnsHi, - returnsFoo2, - printHello3 - } = await import("./subdir/mod1.ts"); + const { returnsHi, returnsFoo2, printHello3 } = await import( + "./subdir/mod1.ts" + ); printHello3(); diff --git a/tests/error_003_typescript.test b/tests/error_003_typescript.test index f721829a24..a7a68627ac 100644 --- a/tests/error_003_typescript.test +++ b/tests/error_003_typescript.test @@ -1,3 +1,4 @@ args: run --reload tests/error_003_typescript.ts +check_stderr: true exit_code: 1 output: tests/error_003_typescript.ts.out diff --git a/tests/error_003_typescript.ts b/tests/error_003_typescript.ts index ebd9fcbe64..6fb077ea08 100644 --- a/tests/error_003_typescript.ts +++ b/tests/error_003_typescript.ts @@ -1,2 +1,26 @@ -// console.log intentionally misspelled to trigger TypeScript error -consol.log("hello world!"); +/* eslint-disable */ +interface Value { + f?: (r: T) => any; + v?: string; +} + +interface C { + values?: (r: T) => Array>; +} + +class A { + constructor(private e?: T, public s?: C) {} +} + +class B { + t = "foo"; +} + +var a = new A(new B(), { + values: o => [ + { + v: o.t, + f: x => "bar" + } + ] +}); diff --git a/tests/error_003_typescript.ts.out b/tests/error_003_typescript.ts.out index 65bc33591c..e2efd56399 100644 --- a/tests/error_003_typescript.ts.out +++ b/tests/error_003_typescript.ts.out @@ -1,10 +1,22 @@ -[WILDCARD]tests/error_003_typescript.ts[WILDCARD] - error TS2552: Cannot find name 'consol'. Did you mean 'console'? +[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[]'. + 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] consol.log("hello world!"); -[WILDCARD]~~~~~~ +20 values: o => [ + ~~~~~~ - $asset$/lib.deno_runtime.d.ts[WILDCARD] -[WILDCARD]declare const console: consoleTypes.Console; -[WILDCARD]~~~~~~~ -[WILDCARD]'console' is declared here. + [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'. + +22 v: o.t, + ^ + + +Found 2 errors. diff --git a/tests/error_003_typescript2.test b/tests/error_003_typescript2.test index 62e66d7e7f..c4c724259c 100644 --- a/tests/error_003_typescript2.test +++ b/tests/error_003_typescript2.test @@ -3,5 +3,6 @@ # should result in the same output. # https://github.com/denoland/deno/issues/2436 args: run tests/error_003_typescript.ts +check_stderr: true exit_code: 1 output: tests/error_003_typescript.ts.out diff --git a/third_party b/third_party index 0761d3cee6..72a4202a03 160000 --- a/third_party +++ b/third_party @@ -1 +1 @@ -Subproject commit 0761d3cee6dd43c38f676268b496a37527fc9bae +Subproject commit 72a4202a0341516115a92aa18951eb3010fb75fa