Handle compiler diagnostics in Rust (#2445)

This commit is contained in:
Kitson Kelly 2019-06-04 23:03:56 +10:00 committed by Ryan Dahl
parent 60d4522641
commit a71305b4fe
16 changed files with 1044 additions and 87 deletions

View File

@ -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",

View File

@ -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() {

View File

@ -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,

668
cli/diagnostics.rs Normal file
View File

@ -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()));
}
}

View File

@ -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;

View File

@ -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");

View File

@ -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);
// 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();
};
};

218
js/diagnostics.ts Normal file
View File

@ -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 };
}

View File

@ -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",

View File

@ -239,6 +239,7 @@ export default function makeConfig(commandOptions) {
"parseConfigFileTextToJson",
"version",
"CompilerHost",
"DiagnosticCategory",
"Extension",
"ModuleKind",
"ScriptKind",

View File

@ -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();

View File

@ -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

View File

@ -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"
}
]
});

View File

@ -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.

View File

@ -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

@ -1 +1 @@
Subproject commit 0761d3cee6dd43c38f676268b496a37527fc9bae
Subproject commit 72a4202a0341516115a92aa18951eb3010fb75fa