summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2019-06-04 23:03:56 +1000
committerRyan Dahl <ry@tinyclouds.org>2019-06-04 09:03:56 -0400
commita71305b4febc3d8db95d3d144ae3a64c023718f0 (patch)
treef0dcc6017f62380b02a08d800503fbf7242fbe72
parent60d452264198adb3da4820236cf8ea35d33486cd (diff)
Handle compiler diagnostics in Rust (#2445)
-rw-r--r--cli/BUILD.gn1
-rw-r--r--cli/ansi.rs26
-rw-r--r--cli/compiler.rs19
-rw-r--r--cli/diagnostics.rs668
-rw-r--r--cli/main.rs1
-rw-r--r--cli/worker.rs3
-rw-r--r--js/compiler.ts128
-rw-r--r--js/diagnostics.ts218
-rw-r--r--package.json2
-rw-r--r--rollup.config.js1
-rw-r--r--tests/013_dynamic_import.ts8
-rw-r--r--tests/error_003_typescript.test1
-rw-r--r--tests/error_003_typescript.ts28
-rw-r--r--tests/error_003_typescript.ts.out26
-rw-r--r--tests/error_003_typescript2.test1
m---------third_party0
16 files changed, 1044 insertions, 87 deletions
diff --git a/cli/BUILD.gn b/cli/BUILD.gn
index 7887624e2..45386f320 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 95b5e0694..b9e9fe123 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 e1bb56130..0b6c278f9 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<Item = ModuleMetaData, Error = JSError> {
+) -> impl Future<Item = ModuleMetaData, Error = Diagnostic> {
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<Buf>| {
- 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<ModuleMetaData, JSError> {
+) -> Result<ModuleMetaData, Diagnostic> {
tokio_util::block_on(compile_async(
state,
specifier,
diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs
new file mode 100644
index 000000000..af384f277
--- /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<DiagnosticItem>,
+}
+
+impl Diagnostic {
+ /// Take a JSON value and attempt to map it to a
+ pub fn from_json_value(v: &serde_json::Value) -> Option<Self> {
+ if !v.is_object() {
+ return None;
+ }
+ let obj = v.as_object().unwrap();
+
+ let mut items = Vec::<DiagnosticItem>::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<Self> {
+ let v = serde_json::from_str::<serde_json::Value>(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<Box<DiagnosticMessageChain>>,
+
+ /// Other diagnostic items that are related to the diagnostic, usually these
+ /// are suggestions of why an error occurred.
+ pub related_information: Option<Vec<DiagnosticItem>>,
+
+ /// The source line the diagnostic is in reference to.
+ pub source_line: Option<String>,
+
+ /// Zero-based index to the line number of the error.
+ pub line_number: Option<i64>,
+
+ /// The resource name provided to the TypeScript compiler.
+ pub script_resource_name: Option<String>,
+
+ /// Zero-based index to the start position in the entire script resource.
+ pub start_position: Option<i64>,
+
+ /// Zero-based index to the end position in the entire script resource.
+ pub end_position: Option<i64>,
+ 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<i64>,
+
+ /// Zero-based index to the end column on `line_number`.
+ pub end_column: Option<i64>,
+}
+
+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::<DiagnosticItem>::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::<String>());
+ 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<Box<DiagnosticMessageChain>>,
+}
+
+impl DiagnosticMessageChain {
+ pub fn from_json_value(v: &serde_json::Value) -> Option<Box<Self>> {
+ 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<i64> 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<B>[]'.".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<B>[]'.".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<B>'".to_string(),
+ message_chain: None,
+ related_information: None,
+ code: 6500,
+ source_line: Some(" values?: (r: T) => Array<Value<T>>;".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::<serde_json::Value>(
+ &r#"{
+ "items": [
+ {
+ "message": "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value<B>[]'.",
+ "messageChain": {
+ "message": "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value<B>[]'.",
+ "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<B>'",
+ "code": 6500,
+ "category": 2,
+ "startPosition": 78,
+ "endPosition": 84,
+ "sourceLine": " values?: (r: T) => Array<Value<T>>;",
+ "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<B>[]'.".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<B>[]'.".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<B>'".to_string(),
+ message_chain: None,
+ related_information: None,
+ source_line: Some(" values?: (r: T) => Array<Value<T>>;".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<B>[]\'.\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<Value<T>>;\n ~~~~~~\n The expected type comes from property \'values\' which is declared here on type \'SettingsInterface<B>\'\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 5d70805ae..953e01943 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 59eecda6f..c08a43385 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 15adba746..6b0881700 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<string> = [
@@ -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<ts.Diagnostic>): 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);
-
- 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;
+ // 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);
+
+ 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();
-
- // TODO(ry) Print diagnostics in Rust.
- // https://github.com/denoland/deno/pull/2310
-
- const { diagnostics } = emitResult;
- if (diagnostics.length > 0) {
- host._logDiagnostics(diagnostics);
- // The above _logDiagnostics calls os.exit(). The return is here just for
- // clarity.
- return;
- }
+ const result: EmitResult = {
+ emitSkipped,
+ diagnostics: diagnostics.length
+ ? fromTypeScriptDiagnostic(diagnostics)
+ : undefined
+ };
- postMessage(emitResult);
+ postMessage(result);
- // 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 000000000..1207eca4f
--- /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 7ac027447..8f87d4229 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 635aace0d..41f738bae 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 20dc508db..6bbce3132 100644
--- a/tests/013_dynamic_import.ts
+++ b/tests/013_dynamic_import.ts
@@ -1,9 +1,7 @@
(async (): Promise<void> => {
- 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 f721829a2..a7a68627a 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 ebd9fcbe6..6fb077ea0 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<T> {
+ f?: (r: T) => any;
+ v?: string;
+}
+
+interface C<T> {
+ values?: (r: T) => Array<Value<T>>;
+}
+
+class A<T> {
+ constructor(private e?: T, public s?: C<T>) {}
+}
+
+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 65bc33591..e2efd5639 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<B>[]'.
+ 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<Value<T>>;
+ ~~~~~~
+ The expected type comes from property 'values' which is declared here on type 'C<B>'
+
+[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 62e66d7e7..c4c724259 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
-Subproject 0761d3cee6dd43c38f676268b496a37527fc9ba
+Subproject 72a4202a0341516115a92aa18951eb3010fb75f