refactor: improve tsc diagnostics (#7420)

This commit is contained in:
Kitson Kelly 2020-09-12 19:53:57 +10:00 committed by GitHub
parent 5276cc8592
commit 10fbfcbc79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 684 additions and 715 deletions

View file

@ -1,240 +1,152 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 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::colors; use crate::colors;
use crate::fmt_errors::format_stack;
use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use serde::Deserializer; use serde::Deserializer;
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
#[derive(Clone, Debug, Deserialize, PartialEq)] const MAX_SOURCE_LINE_LENGTH: usize = 150;
#[serde(rename_all = "camelCase")]
pub struct Diagnostic { const UNSTABLE_DENO_PROPS: &[&str] = &[
pub items: Vec<DiagnosticItem>, "CompilerOptions",
"DatagramConn",
"Diagnostic",
"DiagnosticCategory",
"DiagnosticItem",
"DiagnosticMessageChain",
"EnvPermissionDescriptor",
"HrtimePermissionDescriptor",
"HttpClient",
"LinuxSignal",
"Location",
"MacOSSignal",
"NetPermissionDescriptor",
"PermissionDescriptor",
"PermissionName",
"PermissionState",
"PermissionStatus",
"Permissions",
"PluginPermissionDescriptor",
"ReadPermissionDescriptor",
"RunPermissionDescriptor",
"ShutdownMode",
"Signal",
"SignalStream",
"StartTlsOptions",
"SymlinkOptions",
"TranspileOnlyResult",
"UnixConnectOptions",
"UnixListenOptions",
"WritePermissionDescriptor",
"applySourceMap",
"bundle",
"compile",
"connect",
"consoleSize",
"createHttpClient",
"fdatasync",
"fdatasyncSync",
"formatDiagnostics",
"futime",
"futimeSync",
"fstat",
"fstatSync",
"fsync",
"fsyncSync",
"ftruncate",
"ftruncateSync",
"hostname",
"kill",
"link",
"linkSync",
"listen",
"listenDatagram",
"loadavg",
"mainModule",
"openPlugin",
"osRelease",
"permissions",
"ppid",
"setRaw",
"shutdown",
"signal",
"signals",
"startTls",
"symlink",
"symlinkSync",
"transpileOnly",
"umask",
"utime",
"utimeSync",
];
lazy_static! {
static ref MSG_MISSING_PROPERTY_DENO: Regex =
Regex::new(r#"Property '([^']+)' does not exist on type 'typeof Deno'"#)
.unwrap();
static ref MSG_SUGGESTION: Regex =
Regex::new(r#" Did you mean '([^']+)'\?"#).unwrap();
} }
impl fmt::Display for Diagnostic { /// Potentially convert a "raw" diagnostic message from TSC to something that
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { /// provides a more sensible error message given a Deno runtime context.
let mut i = 0; fn format_message(msg: &str, code: &u64) -> String {
for item in &self.items { match code {
if i > 0 { 2339 => {
write!(f, "\n\n")?; if let Some(captures) = MSG_MISSING_PROPERTY_DENO.captures(msg) {
if let Some(property) = captures.get(1) {
if UNSTABLE_DENO_PROPS.contains(&property.as_str()) {
return format!("{} 'Deno.{}' is an unstable API. Did you forget to run with the '--unstable' flag?", msg, property.as_str());
} }
write!(f, "{}", item.to_string())?;
i += 1;
}
if i > 1 {
write!(f, "\n\nFound {} errors.", i)?;
}
Ok(())
}
}
impl Error for Diagnostic {
fn description(&self) -> &str {
&self.items[0].message
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
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<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>,
}
fn format_category_and_code(
category: &DiagnosticCategory,
code: i64,
) -> String {
let category = match category {
DiagnosticCategory::Error => "ERROR".to_string(),
DiagnosticCategory::Warning => "WARN".to_string(),
DiagnosticCategory::Debug => "DEBUG".to_string(),
DiagnosticCategory::Info => "INFO".to_string(),
_ => "".to_string(),
};
let code = colors::bold(&format!("TS{}", code.to_string())).to_string();
format!("{} [{}]", code, category)
}
fn format_message(
message_chain: &Option<DiagnosticMessageChain>,
message: &str,
level: usize,
) -> String {
debug!("format_message");
if let Some(message_chain) = message_chain {
let mut s = message_chain.format_message(level);
s.pop();
s
} else {
format!("{:indent$}{}", "", message, indent = level)
}
}
/// Formats optional source, line and column numbers into a single string.
fn format_maybe_frame(
file_name: Option<&str>,
line_number: Option<i64>,
column_number: Option<i64>,
) -> String {
if file_name.is_none() {
return "".to_string();
}
assert!(line_number.is_some());
assert!(column_number.is_some());
let line_number = line_number.unwrap();
let column_number = column_number.unwrap();
let file_name_c = colors::cyan(file_name.unwrap());
let line_c = colors::yellow(&line_number.to_string());
let column_c = colors::yellow(&column_number.to_string());
format!("{}:{}:{}", file_name_c, line_c, column_c)
}
fn format_maybe_related_information(
related_information: &Option<Vec<DiagnosticItem>>,
) -> String {
if related_information.is_none() {
return "".to_string();
}
let mut s = String::new();
if let Some(related_information) = related_information {
for rd in related_information {
s.push_str("\n\n");
s.push_str(&format_stack(
matches!(rd.category, DiagnosticCategory::Error),
&format_message(&rd.message_chain, &rd.message, 0),
rd.source_line.as_deref(),
rd.start_column,
rd.end_column,
// Formatter expects 1-based line and column numbers, but ours are 0-based.
&[format_maybe_frame(
rd.script_resource_name.as_deref(),
rd.line_number.map(|n| n + 1),
rd.start_column.map(|n| n + 1),
)],
4,
));
} }
} }
s msg.to_string()
} }
2551 => {
impl fmt::Display for DiagnosticItem { if let (Some(caps_property), Some(caps_suggestion)) = (
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { MSG_MISSING_PROPERTY_DENO.captures(msg),
write!( MSG_SUGGESTION.captures(msg),
f, ) {
"{}", if let (Some(property), Some(suggestion)) =
format_stack( (caps_property.get(1), caps_suggestion.get(1))
matches!(self.category, DiagnosticCategory::Error), {
&format!( if UNSTABLE_DENO_PROPS.contains(&property.as_str()) {
"{}: {}", return format!("{} 'Deno.{}' is an unstable API. Did you forget to run with the '--unstable' flag, or did you mean '{}'?", MSG_SUGGESTION.replace(msg, ""), property.as_str(), suggestion.as_str());
format_category_and_code(&self.category, self.code),
format_message(&self.message_chain, &self.message, 0)
),
self.source_line.as_deref(),
self.start_column,
self.end_column,
// Formatter expects 1-based line and column numbers, but ours are 0-based.
&[format_maybe_frame(
self.script_resource_name.as_deref(),
self.line_number.map(|n| n + 1),
self.start_column.map(|n| n + 1)
)],
0
)
)?;
write!(
f,
"{}",
format_maybe_related_information(&self.related_information),
)
} }
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticMessageChain {
pub message: String,
pub code: i64,
pub category: DiagnosticCategory,
pub next: Option<Vec<DiagnosticMessageChain>>,
}
impl DiagnosticMessageChain {
pub fn format_message(&self, level: usize) -> String {
let mut s = String::new();
s.push_str(&std::iter::repeat(" ").take(level * 2).collect::<String>());
s.push_str(&self.message);
s.push('\n');
if let Some(next) = &self.next {
let arr = next.clone();
for dm in arr {
s.push_str(&dm.format_message(level + 1));
} }
} }
s msg.to_string()
}
_ => msg.to_string(),
} }
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum DiagnosticCategory { pub enum DiagnosticCategory {
Log, // 0 Warning,
Debug, // 1 Error,
Info, // 2 Suggestion,
Error, // 3 Message,
Warning, // 4 }
Suggestion, // 5
impl fmt::Display for DiagnosticCategory {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
DiagnosticCategory::Warning => "WARN ",
DiagnosticCategory::Error => "ERROR ",
DiagnosticCategory::Suggestion => "",
DiagnosticCategory::Message => "",
}
)
}
} }
impl<'de> Deserialize<'de> for DiagnosticCategory { impl<'de> Deserialize<'de> for DiagnosticCategory {
@ -250,202 +162,464 @@ impl<'de> Deserialize<'de> for DiagnosticCategory {
impl From<i64> for DiagnosticCategory { impl From<i64> for DiagnosticCategory {
fn from(value: i64) -> Self { fn from(value: i64) -> Self {
match value { match value {
0 => DiagnosticCategory::Log, 0 => DiagnosticCategory::Warning,
1 => DiagnosticCategory::Debug, 1 => DiagnosticCategory::Error,
2 => DiagnosticCategory::Info, 2 => DiagnosticCategory::Suggestion,
3 => DiagnosticCategory::Error, 3 => DiagnosticCategory::Message,
4 => DiagnosticCategory::Warning,
5 => DiagnosticCategory::Suggestion,
_ => panic!("Unknown value: {}", value), _ => panic!("Unknown value: {}", value),
} }
} }
} }
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticMessageChain {
message_text: String,
category: DiagnosticCategory,
code: i64,
next: Option<Vec<DiagnosticMessageChain>>,
}
impl DiagnosticMessageChain {
pub fn format_message(&self, level: usize) -> String {
let mut s = String::new();
s.push_str(&std::iter::repeat(" ").take(level * 2).collect::<String>());
s.push_str(&self.message_text);
if let Some(next) = &self.next {
s.push('\n');
let arr = next.clone();
for dm in arr {
s.push_str(&dm.format_message(level + 1));
}
}
s
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub line: u64,
pub character: u64,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Diagnostic {
category: DiagnosticCategory,
code: u64,
start: Option<Position>,
end: Option<Position>,
message_text: Option<String>,
message_chain: Option<DiagnosticMessageChain>,
source: Option<String>,
source_line: Option<String>,
file_name: Option<String>,
related_information: Option<Vec<Diagnostic>>,
}
impl Diagnostic {
fn fmt_category_and_code(&self, f: &mut fmt::Formatter) -> fmt::Result {
let category = match self.category {
DiagnosticCategory::Error => "ERROR",
DiagnosticCategory::Warning => "WARN",
_ => "",
};
if !category.is_empty() {
write!(
f,
"{} [{}]: ",
colors::bold(&format!("TS{}", self.code)),
category
)
} else {
Ok(())
}
}
fn fmt_frame(&self, f: &mut fmt::Formatter, level: usize) -> fmt::Result {
if let (Some(file_name), Some(start)) =
(self.file_name.as_ref(), self.start.as_ref())
{
write!(
f,
"\n{:indent$} at {}:{}:{}",
"",
colors::cyan(file_name),
colors::yellow(&(start.line + 1).to_string()),
colors::yellow(&(start.character + 1).to_string()),
indent = level
)
} else {
Ok(())
}
}
fn fmt_message(&self, f: &mut fmt::Formatter, level: usize) -> fmt::Result {
if let Some(message_chain) = &self.message_chain {
write!(f, "{}", message_chain.format_message(level))
} else {
write!(
f,
"{:indent$}{}",
"",
format_message(&self.message_text.clone().unwrap(), &self.code),
indent = level,
)
}
}
fn fmt_source_line(
&self,
f: &mut fmt::Formatter,
level: usize,
) -> fmt::Result {
if let (Some(source_line), Some(start), Some(end)) =
(&self.source_line, &self.start, &self.end)
{
if !source_line.is_empty() && source_line.len() <= MAX_SOURCE_LINE_LENGTH
{
write!(f, "\n{:indent$}{}", "", source_line, indent = level)?;
let length = if start.line == end.line {
end.character - start.character
} else {
1
};
let mut s = String::new();
for i in 0..start.character {
s.push(if source_line.chars().nth(i as usize).unwrap() == '\t' {
'\t'
} else {
' '
});
}
// TypeScript always uses `~` when underlining, but v8 always uses `^`.
// We will use `^` to indicate a single point, or `~` when spanning
// multiple characters.
let ch = if length > 1 { '~' } else { '^' };
for _i in 0..length {
s.push(ch)
}
let underline = if self.is_error() {
colors::red(&s).to_string()
} else {
colors::cyan(&s).to_string()
};
write!(f, "\n{:indent$}{}", "", underline, indent = level)?;
}
}
Ok(())
}
fn fmt_related_information(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(related_information) = self.related_information.as_ref() {
write!(f, "\n\n")?;
for info in related_information {
info.fmt_stack(f, 4)?;
}
}
Ok(())
}
fn fmt_stack(&self, f: &mut fmt::Formatter, level: usize) -> fmt::Result {
self.fmt_category_and_code(f)?;
self.fmt_message(f, level)?;
self.fmt_source_line(f, level)?;
self.fmt_frame(f, level)
}
fn is_error(&self) -> bool {
self.category == DiagnosticCategory::Error
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.fmt_stack(f, 0)?;
self.fmt_related_information(f)
}
}
#[derive(Clone, Debug)]
pub struct Diagnostics(pub Vec<Diagnostic>);
impl<'de> Deserialize<'de> for Diagnostics {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let items: Vec<Diagnostic> = Deserialize::deserialize(deserializer)?;
Ok(Diagnostics(items))
}
}
impl fmt::Display for Diagnostics {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut i = 0;
for item in &self.0 {
if i > 0 {
write!(f, "\n\n")?;
}
write!(f, "{}", item.to_string())?;
i += 1;
}
if i > 1 {
write!(f, "\n\nFound {} errors.", i)?;
}
Ok(())
}
}
impl Error for Diagnostics {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::colors::strip_ansi_codes; use colors::strip_ansi_codes;
use serde_json::json;
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(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(vec![DiagnosticMessageChain {
message: "Types of parameters 'o' and 'r' are incompatible.".to_string(),
code: 2328,
category: DiagnosticCategory::Error,
next: Some(vec![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] #[test]
fn from_json() { fn test_de_diagnostics() {
let r = serde_json::from_str::<Diagnostic>( let value = json!([
&r#"{
"items": [
{ {
"message": "Type '{ a(): { b: number; }; }' is not assignable to type '{ a(): { b: string; }; }'.", "messageText": "Unknown compiler option 'invalid'.",
"messageChain": { "category": 1,
"message": "Type '{ a(): { b: number; }; }' is not assignable to type '{ a(): { b: string; }; }'.", "code": 5023
"code": 2322, },
{
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 7
},
"fileName": "test.ts",
"messageText": "Cannot find name 'console'. Do you need to change your target library? Try changing the `lib` compiler option to include 'dom'.",
"sourceLine": "console.log(\"a\");",
"category": 1,
"code": 2584
},
{
"start": {
"line": 7,
"character": 0
},
"end": {
"line": 7,
"character": 7
},
"fileName": "test.ts",
"messageText": "Cannot find name 'foo_Bar'. Did you mean 'foo_bar'?",
"sourceLine": "foo_Bar();",
"relatedInformation": [
{
"start": {
"line": 3,
"character": 9
},
"end": {
"line": 3,
"character": 16
},
"fileName": "test.ts",
"messageText": "'foo_bar' is declared here.",
"sourceLine": "function foo_bar() {",
"category": 3, "category": 3,
"code": 2728
}
],
"category": 1,
"code": 2552
},
{
"start": {
"line": 18,
"character": 0
},
"end": {
"line": 18,
"character": 1
},
"fileName": "test.ts",
"messageChain": {
"messageText": "Type '{ a: { b: { c(): { d: number; }; }; }; }' is not assignable to type '{ a: { b: { c(): { d: string; }; }; }; }'.",
"category": 1,
"code": 2322,
"next": [ "next": [
{ {
"message": "Types of property 'a' are incompatible.", "messageText": "The types of 'a.b.c().d' are incompatible between these types.",
"code": 2326, "category": 1,
"category": 3 "code": 2200,
"next": [
{
"messageText": "Type 'number' is not assignable to type 'string'.",
"category": 1,
"code": 2322
}
]
} }
] ]
}, },
"code": 2322,
"category": 3,
"startPosition": 352,
"endPosition": 353,
"sourceLine": "x = y;", "sourceLine": "x = y;",
"lineNumber": 29, "code": 2322,
"scriptResourceName": "/deno/tests/error_003_typescript.ts", "category": 1
"startColumn": 0,
"endColumn": 1
} }
] ]);
}"#, let diagnostics: Diagnostics =
).unwrap(); serde_json::from_value(value).expect("cannot deserialize");
let expected = assert_eq!(diagnostics.0.len(), 4);
Diagnostic { assert!(diagnostics.0[0].source_line.is_none());
items: vec![ assert!(diagnostics.0[0].file_name.is_none());
DiagnosticItem { assert!(diagnostics.0[0].start.is_none());
message: "Type \'{ a(): { b: number; }; }\' is not assignable to type \'{ a(): { b: string; }; }\'.".to_string(), assert!(diagnostics.0[0].end.is_none());
message_chain: Some( assert!(diagnostics.0[0].message_text.is_some());
DiagnosticMessageChain { assert!(diagnostics.0[0].message_chain.is_none());
message: "Type \'{ a(): { b: number; }; }\' is not assignable to type \'{ a(): { b: string; }; }\'.".to_string(), assert!(diagnostics.0[0].related_information.is_none());
code: 2322, assert!(diagnostics.0[1].source_line.is_some());
category: DiagnosticCategory::Error, assert!(diagnostics.0[1].file_name.is_some());
next: Some(vec![ assert!(diagnostics.0[1].start.is_some());
DiagnosticMessageChain { assert!(diagnostics.0[1].end.is_some());
message: "Types of property \'a\' are incompatible.".to_string(), assert!(diagnostics.0[1].message_text.is_some());
code: 2326, assert!(diagnostics.0[1].message_chain.is_none());
category: DiagnosticCategory::Error, assert!(diagnostics.0[1].related_information.is_none());
next: None, assert!(diagnostics.0[2].source_line.is_some());
} assert!(diagnostics.0[2].file_name.is_some());
]) assert!(diagnostics.0[2].start.is_some());
} assert!(diagnostics.0[2].end.is_some());
), assert!(diagnostics.0[2].message_text.is_some());
related_information: None, assert!(diagnostics.0[2].message_chain.is_none());
source_line: Some("x = y;".to_string()), assert!(diagnostics.0[2].related_information.is_some());
line_number: Some(29),
script_resource_name: Some("/deno/tests/error_003_typescript.ts".to_string()),
start_position: Some(352),
end_position: Some(353),
category: DiagnosticCategory::Error,
code: 2322,
start_column: Some(0),
end_column: Some(1)
}
]
};
assert_eq!(expected, r);
} }
#[test] #[test]
fn diagnostic_to_string1() { fn test_diagnostics_no_source() {
let d = diagnostic1(); let value = json!([
let expected = "TS2322 [ERROR]: 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 values: o => [\n ~~~~~~\n at deno/tests/complex_diagnostics.ts:19:3\n\n The expected type comes from property \'values\' which is declared here on type \'SettingsInterface<B>\'\n values?: (r: T) => Array<Value<T>>;\n ~~~~~~\n at deno/tests/complex_diagnostics.ts:7:3"; {
assert_eq!(expected, strip_ansi_codes(&d.to_string())); "messageText": "Unknown compiler option 'invalid'.",
"category":1,
"code":5023
}
]);
let diagnostics: Diagnostics = serde_json::from_value(value).unwrap();
let actual = format!("{}", diagnostics);
assert_eq!(
strip_ansi_codes(&actual),
"TS5023 [ERROR]: Unknown compiler option \'invalid\'."
);
} }
#[test] #[test]
fn diagnostic_to_string2() { fn test_diagnostics_basic() {
let d = diagnostic2(); let value = json!([
let expected = "TS2322 [ERROR]: Example 1\n values: o => [\n ~~~~~~\n at deno/tests/complex_diagnostics.ts:19:3\n\nTS2000 [ERROR]: Example 2\n values: undefined,\n ~~~~~~\n at /foo/bar.ts:129:3\n\nFound 2 errors."; {
assert_eq!(expected, strip_ansi_codes(&d.to_string())); "start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 7
},
"fileName": "test.ts",
"messageText": "Cannot find name 'console'. Do you need to change your target library? Try changing the `lib` compiler option to include 'dom'.",
"sourceLine": "console.log(\"a\");",
"category": 1,
"code": 2584
}
]);
let diagnostics: Diagnostics = serde_json::from_value(value).unwrap();
let actual = format!("{}", diagnostics);
assert_eq!(strip_ansi_codes(&actual), "TS2584 [ERROR]: Cannot find name \'console\'. Do you need to change your target library? Try changing the `lib` compiler option to include \'dom\'.\nconsole.log(\"a\");\n~~~~~~~\n at test.ts:1:1");
} }
#[test] #[test]
fn test_format_none_frame() { fn test_diagnostics_related_info() {
let actual = format_maybe_frame(None, None, None); let value = json!([
assert_eq!(actual, ""); {
"start": {
"line": 7,
"character": 0
},
"end": {
"line": 7,
"character": 7
},
"fileName": "test.ts",
"messageText": "Cannot find name 'foo_Bar'. Did you mean 'foo_bar'?",
"sourceLine": "foo_Bar();",
"relatedInformation": [
{
"start": {
"line": 3,
"character": 9
},
"end": {
"line": 3,
"character": 16
},
"fileName": "test.ts",
"messageText": "'foo_bar' is declared here.",
"sourceLine": "function foo_bar() {",
"category": 3,
"code": 2728
}
],
"category": 1,
"code": 2552
}
]);
let diagnostics: Diagnostics = serde_json::from_value(value).unwrap();
let actual = format!("{}", diagnostics);
assert_eq!(strip_ansi_codes(&actual), "TS2552 [ERROR]: Cannot find name \'foo_Bar\'. Did you mean \'foo_bar\'?\nfoo_Bar();\n~~~~~~~\n at test.ts:8:1\n\n \'foo_bar\' is declared here.\n function foo_bar() {\n ~~~~~~~\n at test.ts:4:10");
} }
#[test] #[test]
fn test_format_some_frame() { fn test_unstable_suggestion() {
let actual = let value = json![
format_maybe_frame(Some("file://foo/bar.ts"), Some(1), Some(2)); {
assert_eq!(strip_ansi_codes(&actual), "file://foo/bar.ts:1:2"); "start": {
"line": 0,
"character": 17
},
"end": {
"line": 0,
"character": 21
},
"fileName": "file:///cli/tests/unstable_ts2551.ts",
"messageText": "Property 'ppid' does not exist on type 'typeof Deno'. Did you mean 'pid'?",
"sourceLine": "console.log(Deno.ppid);",
"relatedInformation": [
{
"start": {
"line": 89,
"character": 15
},
"end": {
"line": 89,
"character": 18
},
"fileName": "asset:///lib.deno.ns.d.ts",
"messageText": "'pid' is declared here.",
"sourceLine": " export const pid: number;",
"category": 3,
"code": 2728
}
],
"category": 1,
"code": 2551
}
];
let diagnostics: Diagnostic = serde_json::from_value(value).unwrap();
let actual = format!("{}", diagnostics);
assert_eq!(strip_ansi_codes(&actual), "TS2551 [ERROR]: Property \'ppid\' does not exist on type \'typeof Deno\'. \'Deno.ppid\' is an unstable API. Did you forget to run with the \'--unstable\' flag, or did you mean \'pid\'?\nconsole.log(Deno.ppid);\n ~~~~\n at file:///cli/tests/unstable_ts2551.ts:1:18\n\n \'pid\' is declared here.\n export const pid: number;\n ~~~\n at asset:///lib.deno.ns.d.ts:90:16");
} }
} }

View file

@ -188,12 +188,10 @@ declare namespace Deno {
/** The log category for a diagnostic message. */ /** The log category for a diagnostic message. */
export enum DiagnosticCategory { export enum DiagnosticCategory {
Log = 0, Warning = 0,
Debug = 1, Error = 1,
Info = 2, Suggestion = 2,
Error = 3, Message = 3,
Warning = 4,
Suggestion = 5,
} }
export interface DiagnosticMessageChain { export interface DiagnosticMessageChain {
@ -203,37 +201,33 @@ declare namespace Deno {
next?: DiagnosticMessageChain[]; next?: DiagnosticMessageChain[];
} }
export interface DiagnosticItem { export interface Diagnostic {
/** A string message summarizing the diagnostic. */ /** A string message summarizing the diagnostic. */
message: string; messageText?: string;
/** An ordered array of further diagnostics. */ /** An ordered array of further diagnostics. */
messageChain?: DiagnosticMessageChain; messageChain?: DiagnosticMessageChain;
/** Information related to the diagnostic. This is present when there is a /** Information related to the diagnostic. This is present when there is a
* suggestion or other additional diagnostic information */ * suggestion or other additional diagnostic information */
relatedInformation?: DiagnosticItem[]; relatedInformation?: Diagnostic[];
/** The text of the source line related to the diagnostic. */ /** The text of the source line related to the diagnostic. */
sourceLine?: string; sourceLine?: string;
/** The line number that is related to the diagnostic. */ source?: string;
lineNumber?: number; /** The start position of the error. Zero based index. */
/** The name of the script resource related to the diagnostic. */ start?: {
scriptResourceName?: string; line: number;
/** The start position related to the diagnostic. */ character: number;
startPosition?: number; };
/** The end position related to the diagnostic. */ /** The end position of the error. Zero based index. */
endPosition?: number; end?: {
line: number;
character: number;
};
/** The filename of the resource related to the diagnostic message. */
fileName?: string;
/** The category of the diagnostic. */ /** The category of the diagnostic. */
category: DiagnosticCategory; category: DiagnosticCategory;
/** A number identifier. */ /** A number identifier. */
code: number; 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[];
} }
/** **UNSTABLE**: new API, yet to be vetted. /** **UNSTABLE**: new API, yet to be vetted.
@ -247,9 +241,9 @@ declare namespace Deno {
* console.log(Deno.formatDiagnostics(diagnostics)); // User friendly output of diagnostics * console.log(Deno.formatDiagnostics(diagnostics)); // User friendly output of diagnostics
* ``` * ```
* *
* @param items An array of diagnostic items to format * @param diagnostics An array of diagnostic items to format
*/ */
export function formatDiagnostics(items: DiagnosticItem[]): string; export function formatDiagnostics(diagnostics: Diagnostic[]): string;
/** **UNSTABLE**: new API, yet to be vetted. /** **UNSTABLE**: new API, yet to be vetted.
* *
@ -530,7 +524,7 @@ declare namespace Deno {
rootName: string, rootName: string,
sources?: Record<string, string>, sources?: Record<string, string>,
options?: CompilerOptions, options?: CompilerOptions,
): Promise<[DiagnosticItem[] | undefined, Record<string, string>]>; ): Promise<[Diagnostic[] | undefined, Record<string, string>]>;
/** **UNSTABLE**: new API, yet to be vetted. /** **UNSTABLE**: new API, yet to be vetted.
* *
@ -573,7 +567,7 @@ declare namespace Deno {
rootName: string, rootName: string,
sources?: Record<string, string>, sources?: Record<string, string>,
options?: CompilerOptions, options?: CompilerOptions,
): Promise<[DiagnosticItem[] | undefined, string]>; ): Promise<[Diagnostic[] | undefined, string]>;
/** **UNSTABLE**: Should not have same name as `window.location` type. */ /** **UNSTABLE**: Should not have same name as `window.location` type. */
interface Location { interface Location {

View file

@ -1,6 +1,6 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::diagnostics::Diagnostic; use crate::diagnostics::Diagnostics;
use crate::source_maps::get_orig_position; use crate::source_maps::get_orig_position;
use crate::source_maps::CachedMaps; use crate::source_maps::CachedMaps;
use deno_core::ErrBox; use deno_core::ErrBox;
@ -52,6 +52,6 @@ fn op_format_diagnostic(
args: Value, args: Value,
_zero_copy: &mut [ZeroCopyBuf], _zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, ErrBox> { ) -> Result<Value, ErrBox> {
let diagnostic = serde_json::from_value::<Diagnostic>(args)?; let diagnostic: Diagnostics = serde_json::from_value(args)?;
Ok(json!(diagnostic.to_string())) Ok(json!(diagnostic.to_string()))
} }

View file

@ -6,19 +6,15 @@
((window) => { ((window) => {
const DiagnosticCategory = { const DiagnosticCategory = {
0: "Log", 0: "Warning",
1: "Debug", 1: "Error",
2: "Info", 2: "Suggestion",
3: "Error", 3: "Message",
4: "Warning",
5: "Suggestion",
Log: 0, Warning: 0,
Debug: 1, Error: 1,
Info: 2, Suggestion: 2,
Error: 3, Message: 3,
Warning: 4,
Suggestion: 5,
}; };
window.__bootstrap.diagnostics = { window.__bootstrap.diagnostics = {

View file

@ -8,8 +8,8 @@
const internals = window.__bootstrap.internals; const internals = window.__bootstrap.internals;
const dispatchJson = window.__bootstrap.dispatchJson; const dispatchJson = window.__bootstrap.dispatchJson;
function opFormatDiagnostics(items) { function opFormatDiagnostics(diagnostics) {
return dispatchJson.sendSync("op_format_diagnostic", { items }); return dispatchJson.sendSync("op_format_diagnostic", diagnostics);
} }
function opApplySourceMap(location) { function opApplySourceMap(location) {

View file

@ -2,27 +2,33 @@
import { assert, unitTest } from "./test_util.ts"; import { assert, unitTest } from "./test_util.ts";
unitTest(function formatDiagnosticBasic() { unitTest(function formatDiagnosticBasic() {
const fixture: Deno.DiagnosticItem[] = [ const fixture: Deno.Diagnostic[] = [
{ {
message: "Example error", start: {
category: Deno.DiagnosticCategory.Error, line: 0,
sourceLine: "abcdefghijklmnopqrstuv", character: 0,
lineNumber: 1000, },
scriptResourceName: "foo.ts", end: {
startColumn: 1, line: 0,
endColumn: 2, character: 7,
code: 4000, },
fileName: "test.ts",
messageText:
"Cannot find name 'console'. Do you need to change your target library? Try changing the `lib` compiler option to include 'dom'.",
sourceLine: `console.log("a");`,
category: 1,
code: 2584,
}, },
]; ];
const out = Deno.formatDiagnostics(fixture); const out = Deno.formatDiagnostics(fixture);
assert(out.includes("Example error")); assert(out.includes("Cannot find name"));
assert(out.includes("foo.ts")); assert(out.includes("test.ts"));
}); });
unitTest(function formatDiagnosticError() { unitTest(function formatDiagnosticError() {
let thrown = false; let thrown = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const bad = ([{ hello: 123 }] as any) as Deno.DiagnosticItem[]; const bad = ([{ hello: 123 }] as any) as Deno.Diagnostic[];
try { try {
Deno.formatDiagnostics(bad); Deno.formatDiagnostics(bad);
} catch (e) { } catch (e) {

View file

@ -1,8 +1,7 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::colors; use crate::colors;
use crate::diagnostics::Diagnostic; use crate::diagnostics::Diagnostics;
use crate::diagnostics::DiagnosticItem;
use crate::disk_cache::DiskCache; use crate::disk_cache::DiskCache;
use crate::file_fetcher::SourceFile; use crate::file_fetcher::SourceFile;
use crate::file_fetcher::SourceFileFetcher; use crate::file_fetcher::SourceFileFetcher;
@ -396,7 +395,7 @@ struct EmittedSource {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct BundleResponse { struct BundleResponse {
diagnostics: Diagnostic, diagnostics: Diagnostics,
bundle_output: Option<String>, bundle_output: Option<String>,
stats: Option<Vec<Stat>>, stats: Option<Vec<Stat>>,
} }
@ -404,7 +403,7 @@ struct BundleResponse {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct CompileResponse { struct CompileResponse {
diagnostics: Diagnostic, diagnostics: Diagnostics,
emit_map: HashMap<String, EmittedSource>, emit_map: HashMap<String, EmittedSource>,
build_info: Option<String>, build_info: Option<String>,
stats: Option<Vec<Stat>>, stats: Option<Vec<Stat>>,
@ -425,14 +424,14 @@ struct TranspileTsOptions {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[allow(unused)] #[allow(unused)]
struct RuntimeBundleResponse { struct RuntimeBundleResponse {
diagnostics: Vec<DiagnosticItem>, diagnostics: Diagnostics,
output: String, output: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct RuntimeCompileResponse { struct RuntimeCompileResponse {
diagnostics: Vec<DiagnosticItem>, diagnostics: Diagnostics,
emit_map: HashMap<String, EmittedSource>, emit_map: HashMap<String, EmittedSource>,
} }
@ -647,7 +646,7 @@ impl TsCompiler {
let compile_response: CompileResponse = serde_json::from_str(&json_str)?; let compile_response: CompileResponse = serde_json::from_str(&json_str)?;
if !compile_response.diagnostics.items.is_empty() { if !compile_response.diagnostics.0.is_empty() {
return Err(ErrBox::error(compile_response.diagnostics.to_string())); return Err(ErrBox::error(compile_response.diagnostics.to_string()));
} }
@ -769,7 +768,7 @@ impl TsCompiler {
maybe_log_stats(bundle_response.stats); maybe_log_stats(bundle_response.stats);
if !bundle_response.diagnostics.items.is_empty() { if !bundle_response.diagnostics.0.is_empty() {
return Err(ErrBox::error(bundle_response.diagnostics.to_string())); return Err(ErrBox::error(bundle_response.diagnostics.to_string()));
} }
@ -1287,7 +1286,7 @@ pub async fn runtime_compile(
let response: RuntimeCompileResponse = serde_json::from_str(&json_str)?; let response: RuntimeCompileResponse = serde_json::from_str(&json_str)?;
if response.diagnostics.is_empty() && sources.is_none() { if response.diagnostics.0.is_empty() && sources.is_none() {
compiler.cache_emitted_files(response.emit_map)?; compiler.cache_emitted_files(response.emit_map)?;
} }

View file

@ -24,262 +24,62 @@ delete Object.prototype.__proto__;
const errorStack = window.__bootstrap.errorStack; const errorStack = window.__bootstrap.errorStack;
const errors = window.__bootstrap.errors.errors; const errors = window.__bootstrap.errors.errors;
function opNow() { /**
const res = dispatchJson.sendSync("op_now"); * @param {import("../dts/typescript").DiagnosticRelatedInformation} diagnostic
return res.seconds * 1e3 + res.subsecNanos / 1e6; */
function fromRelatedInformation({
start,
length,
file,
messageText: msgText,
...ri
}) {
let messageText;
let messageChain;
if (typeof msgText === "object") {
messageChain = msgText;
} else {
messageText = msgText;
} }
if (start !== undefined && length !== undefined && file) {
const DiagnosticCategory = { const startPos = file.getLineAndCharacterOfPosition(start);
0: "Log", const sourceLine = file.getFullText().split("\n")[startPos.line];
1: "Debug", const fileName = file.fileName;
2: "Info",
3: "Error",
4: "Warning",
5: "Suggestion",
Log: 0,
Debug: 1,
Info: 2,
Error: 3,
Warning: 4,
Suggestion: 5,
};
const unstableDenoGlobalProperties = [
"CompilerOptions",
"DatagramConn",
"Diagnostic",
"DiagnosticCategory",
"DiagnosticItem",
"DiagnosticMessageChain",
"EnvPermissionDescriptor",
"HrtimePermissionDescriptor",
"HttpClient",
"LinuxSignal",
"Location",
"MacOSSignal",
"NetPermissionDescriptor",
"PermissionDescriptor",
"PermissionName",
"PermissionState",
"PermissionStatus",
"Permissions",
"PluginPermissionDescriptor",
"ReadPermissionDescriptor",
"RunPermissionDescriptor",
"ShutdownMode",
"Signal",
"SignalStream",
"StartTlsOptions",
"SymlinkOptions",
"TranspileOnlyResult",
"UnixConnectOptions",
"UnixListenOptions",
"WritePermissionDescriptor",
"applySourceMap",
"bundle",
"compile",
"connect",
"consoleSize",
"createHttpClient",
"fdatasync",
"fdatasyncSync",
"formatDiagnostics",
"futime",
"futimeSync",
"fstat",
"fstatSync",
"fsync",
"fsyncSync",
"ftruncate",
"ftruncateSync",
"hostname",
"kill",
"link",
"linkSync",
"listen",
"listenDatagram",
"loadavg",
"mainModule",
"openPlugin",
"osRelease",
"permissions",
"ppid",
"setRaw",
"shutdown",
"signal",
"signals",
"startTls",
"symlink",
"symlinkSync",
"transpileOnly",
"umask",
"utime",
"utimeSync",
];
function transformMessageText(messageText, code) {
switch (code) {
case 2339: {
const property = messageText
.replace(/^Property '/, "")
.replace(/' does not exist on type 'typeof Deno'\./, "");
if (
messageText.endsWith("on type 'typeof Deno'.") &&
unstableDenoGlobalProperties.includes(property)
) {
return `${messageText} 'Deno.${property}' is an unstable API. Did you forget to run with the '--unstable' flag?`;
}
break;
}
case 2551: {
const suggestionMessagePattern = / Did you mean '(.+)'\?$/;
const property = messageText
.replace(/^Property '/, "")
.replace(/' does not exist on type 'typeof Deno'\./, "")
.replace(suggestionMessagePattern, "");
const suggestion = messageText.match(suggestionMessagePattern);
const replacedMessageText = messageText.replace(
suggestionMessagePattern,
"",
);
if (suggestion && unstableDenoGlobalProperties.includes(property)) {
const suggestedProperty = suggestion[1];
return `${replacedMessageText} 'Deno.${property}' is an unstable API. Did you forget to run with the '--unstable' flag, or did you mean '${suggestedProperty}'?`;
}
break;
}
}
return messageText;
}
function fromDiagnosticCategory(category) {
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, start, length) {
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 { return {
start: startPos,
end: file.getLineAndCharacterOfPosition(start + length),
fileName,
messageChain,
messageText,
sourceLine, sourceLine,
lineNumber, ...ri,
scriptResourceName,
startColumn,
endColumn,
}; };
} } else {
function fromDiagnosticMessageChain(messageChain) {
if (!messageChain) {
return undefined;
}
return messageChain.map(({ messageText, code, category, next }) => {
const message = transformMessageText(messageText, code);
return { return {
message, messageChain,
code, messageText,
category: fromDiagnosticCategory(category), ...ri,
next: fromDiagnosticMessageChain(next),
}; };
}
}
/**
* @param {import("../dts/typescript").Diagnostic[]} diagnostics
*/
function fromTypeScriptDiagnostic(diagnostics) {
return diagnostics.map(({ relatedInformation: ri, source, ...diag }) => {
const value = fromRelatedInformation(diag);
value.relatedInformation = ri
? ri.map(fromRelatedInformation)
: undefined;
value.source = source;
return value;
}); });
} }
function parseDiagnostic(item) { function opNow() {
const { const res = dispatchJson.sendSync("op_now");
messageText, return res.seconds * 1e3 + res.subsecNanos / 1e6;
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;
let messageChain;
if (typeof messageText === "string") {
message = transformMessageText(messageText, code);
} else {
message = transformMessageText(messageText.messageText, messageText.code);
messageChain = fromDiagnosticMessageChain([messageText])[0];
}
const base = {
message,
messageChain,
code,
category,
startPosition,
endPosition,
};
return sourceInfo ? { ...base, ...sourceInfo } : base;
}
function parseRelatedInformation(relatedInformation) {
const result = [];
for (const item of relatedInformation) {
result.push(parseDiagnostic(item));
}
return result;
}
function fromTypeScriptDiagnostic(diagnostics) {
const items = [];
for (const sourceDiagnostic of diagnostics) {
const item = parseDiagnostic(sourceDiagnostic);
if (sourceDiagnostic.relatedInformation) {
item.relatedInformation = parseRelatedInformation(
sourceDiagnostic.relatedInformation,
);
}
items.push(item);
}
return { items };
} }
// We really don't want to depend on JSON dispatch during snapshotting, so // We really don't want to depend on JSON dispatch during snapshotting, so
@ -1353,7 +1153,7 @@ delete Object.prototype.__proto__;
}); });
const maybeDiagnostics = diagnostics.length const maybeDiagnostics = diagnostics.length
? fromTypeScriptDiagnostic(diagnostics).items ? fromTypeScriptDiagnostic(diagnostics)
: []; : [];
return { return {
@ -1413,7 +1213,7 @@ delete Object.prototype.__proto__;
}); });
const maybeDiagnostics = diagnostics.length const maybeDiagnostics = diagnostics.length
? fromTypeScriptDiagnostic(diagnostics).items ? fromTypeScriptDiagnostic(diagnostics)
: []; : [];
return { return {