diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs index 41ee5ec223..083c11bf5a 100644 --- a/cli/diagnostics.rs +++ b/cli/diagnostics.rs @@ -1,240 +1,152 @@ // 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::fmt_errors::format_stack; + +use regex::Regex; use serde::Deserialize; use serde::Deserializer; use std::error::Error; use std::fmt; -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct Diagnostic { - pub items: Vec, +const MAX_SOURCE_LINE_LENGTH: usize = 150; + +const UNSTABLE_DENO_PROPS: &[&str] = &[ + "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 { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut i = 0; - for item in &self.items { - if i > 0 { - write!(f, "\n\n")?; +/// Potentially convert a "raw" diagnostic message from TSC to something that +/// provides a more sensible error message given a Deno runtime context. +fn format_message(msg: &str, code: &u64) -> String { + match code { + 2339 => { + 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; + + msg.to_string() } - - 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, - - /// Other diagnostic items that are related to the diagnostic, usually these - /// are suggestions of why an error occurred. - pub related_information: Option>, - - /// The source line the diagnostic is in reference to. - pub source_line: Option, - - /// Zero-based index to the line number of the error. - pub line_number: Option, - - /// The resource name provided to the TypeScript compiler. - pub script_resource_name: Option, - - /// Zero-based index to the start position in the entire script resource. - pub start_position: Option, - - /// Zero-based index to the end position in the entire script resource. - pub end_position: Option, - pub category: DiagnosticCategory, - - /// This is defined in TypeScript and can be referenced via - /// [diagnosticMessages.json](https://github.com/microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json). - pub code: i64, - - /// Zero-based index to the start column on `line_number`. - pub start_column: Option, - - /// Zero-based index to the end column on `line_number`. - pub end_column: Option, -} - -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, - 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, - column_number: Option, -) -> 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>, -) -> 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 -} - -impl fmt::Display for DiagnosticItem { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - format_stack( - matches!(self.category, DiagnosticCategory::Error), - &format!( - "{}: {}", - 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>, -} - -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::()); - 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)); + 2551 => { + if let (Some(caps_property), Some(caps_suggestion)) = ( + MSG_MISSING_PROPERTY_DENO.captures(msg), + MSG_SUGGESTION.captures(msg), + ) { + if let (Some(property), Some(suggestion)) = + (caps_property.get(1), caps_suggestion.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, or did you mean '{}'?", MSG_SUGGESTION.replace(msg, ""), property.as_str(), suggestion.as_str()); + } + } } - } - s + msg.to_string() + } + _ => msg.to_string(), } } #[derive(Clone, Debug, PartialEq)] pub enum DiagnosticCategory { - Log, // 0 - Debug, // 1 - Info, // 2 - Error, // 3 - Warning, // 4 - Suggestion, // 5 + Warning, + Error, + Suggestion, + Message, +} + +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 { @@ -250,202 +162,464 @@ impl<'de> Deserialize<'de> for DiagnosticCategory { impl From for DiagnosticCategory { fn from(value: i64) -> Self { match value { - 0 => DiagnosticCategory::Log, - 1 => DiagnosticCategory::Debug, - 2 => DiagnosticCategory::Info, - 3 => DiagnosticCategory::Error, - 4 => DiagnosticCategory::Warning, - 5 => DiagnosticCategory::Suggestion, + 0 => DiagnosticCategory::Warning, + 1 => DiagnosticCategory::Error, + 2 => DiagnosticCategory::Suggestion, + 3 => DiagnosticCategory::Message, _ => panic!("Unknown value: {}", value), } } } +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DiagnosticMessageChain { + message_text: String, + category: DiagnosticCategory, + code: i64, + next: Option>, +} + +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::()); + 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, + end: Option, + message_text: Option, + message_chain: Option, + source: Option, + source_line: Option, + file_name: Option, + related_information: Option>, +} + +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); + +impl<'de> Deserialize<'de> for Diagnostics { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let items: Vec = 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)] mod tests { use super::*; - use crate::colors::strip_ansi_codes; - - fn diagnostic1() -> Diagnostic { - Diagnostic { - items: vec![ - DiagnosticItem { - message: "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.".to_string(), - message_chain: Some(DiagnosticMessageChain { - message: "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.".to_string(), - code: 2322, - category: DiagnosticCategory::Error, - next: Some(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'".to_string(), - message_chain: None, - related_information: None, - code: 6500, - source_line: Some(" values?: (r: T) => Array>;".to_string()), - script_resource_name: Some("deno/tests/complex_diagnostics.ts".to_string()), - line_number: Some(6), - start_position: Some(94), - end_position: Some(100), - category: DiagnosticCategory::Info, - start_column: Some(2), - end_column: Some(8), - } - ]) - } - ] - } - } - - fn diagnostic2() -> Diagnostic { - Diagnostic { - items: vec![ - DiagnosticItem { - message: "Example 1".to_string(), - message_chain: None, - code: 2322, - category: DiagnosticCategory::Error, - start_position: Some(267), - end_position: Some(273), - source_line: Some(" values: o => [".to_string()), - line_number: Some(18), - script_resource_name: Some( - "deno/tests/complex_diagnostics.ts".to_string(), - ), - start_column: Some(2), - end_column: Some(8), - related_information: None, - }, - DiagnosticItem { - message: "Example 2".to_string(), - message_chain: None, - code: 2000, - category: DiagnosticCategory::Error, - start_position: Some(2), - end_position: Some(2), - source_line: Some(" values: undefined,".to_string()), - line_number: Some(128), - script_resource_name: Some("/foo/bar.ts".to_string()), - start_column: Some(2), - end_column: Some(8), - related_information: None, - }, - ], - } - } + use colors::strip_ansi_codes; + use serde_json::json; #[test] - fn from_json() { - let r = serde_json::from_str::( - &r#"{ - "items": [ + fn test_de_diagnostics() { + let value = json!([ + { + "messageText": "Unknown compiler option 'invalid'.", + "category": 1, + "code": 5023 + }, + { + "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": [ { - "message": "Type '{ a(): { b: number; }; }' is not assignable to type '{ a(): { b: string; }; }'.", - "messageChain": { - "message": "Type '{ a(): { b: number; }; }' is not assignable to type '{ a(): { b: string; }; }'.", - "code": 2322, - "category": 3, + "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 + }, + { + "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": [ + { + "messageText": "The types of 'a.b.c().d' are incompatible between these types.", + "category": 1, + "code": 2200, "next": [ { - "message": "Types of property 'a' are incompatible.", - "code": 2326, - "category": 3 + "messageText": "Type 'number' is not assignable to type 'string'.", + "category": 1, + "code": 2322 } ] + } + ] + }, + "sourceLine": "x = y;", + "code": 2322, + "category": 1 + } + ]); + let diagnostics: Diagnostics = + serde_json::from_value(value).expect("cannot deserialize"); + assert_eq!(diagnostics.0.len(), 4); + assert!(diagnostics.0[0].source_line.is_none()); + assert!(diagnostics.0[0].file_name.is_none()); + assert!(diagnostics.0[0].start.is_none()); + assert!(diagnostics.0[0].end.is_none()); + assert!(diagnostics.0[0].message_text.is_some()); + assert!(diagnostics.0[0].message_chain.is_none()); + assert!(diagnostics.0[0].related_information.is_none()); + assert!(diagnostics.0[1].source_line.is_some()); + assert!(diagnostics.0[1].file_name.is_some()); + assert!(diagnostics.0[1].start.is_some()); + assert!(diagnostics.0[1].end.is_some()); + assert!(diagnostics.0[1].message_text.is_some()); + assert!(diagnostics.0[1].message_chain.is_none()); + assert!(diagnostics.0[1].related_information.is_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()); + assert!(diagnostics.0[2].message_chain.is_none()); + assert!(diagnostics.0[2].related_information.is_some()); + } + + #[test] + fn test_diagnostics_no_source() { + let value = json!([ + { + "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] + fn test_diagnostics_basic() { + let value = json!([ + { + "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] + fn test_diagnostics_related_info() { + let value = json!([ + { + "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 }, - "code": 2322, + "end": { + "line": 3, + "character": 16 + }, + "fileName": "test.ts", + "messageText": "'foo_bar' is declared here.", + "sourceLine": "function foo_bar() {", "category": 3, - "startPosition": 352, - "endPosition": 353, - "sourceLine": "x = y;", - "lineNumber": 29, - "scriptResourceName": "/deno/tests/error_003_typescript.ts", - "startColumn": 0, - "endColumn": 1 + "code": 2728 } - ] - }"#, - ).unwrap(); - let expected = - Diagnostic { - items: vec![ - DiagnosticItem { - message: "Type \'{ a(): { b: number; }; }\' is not assignable to type \'{ a(): { b: string; }; }\'.".to_string(), - message_chain: Some( - DiagnosticMessageChain { - message: "Type \'{ a(): { b: number; }; }\' is not assignable to type \'{ a(): { b: string; }; }\'.".to_string(), - code: 2322, - category: DiagnosticCategory::Error, - next: Some(vec![ - DiagnosticMessageChain { - message: "Types of property \'a\' are incompatible.".to_string(), - code: 2326, - category: DiagnosticCategory::Error, - next: None, - } - ]) - } - ), - related_information: None, - source_line: Some("x = y;".to_string()), - 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) + ], + "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] + fn test_unstable_suggestion() { + let value = json![ + { + "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 } - ] - }; - assert_eq!(expected, r); - } - - #[test] - fn diagnostic_to_string1() { - let d = diagnostic1(); - let expected = "TS2322 [ERROR]: Type \'(o: T) => { v: any; f: (x: B) => string; }[]\' is not assignable to type \'(r: B) => Value[]\'.\n Types of parameters \'o\' and \'r\' are incompatible.\n Type \'B\' is not assignable to type \'T\'.\n 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\'\n values?: (r: T) => Array>;\n ~~~~~~\n at deno/tests/complex_diagnostics.ts:7:3"; - assert_eq!(expected, strip_ansi_codes(&d.to_string())); - } - - #[test] - fn diagnostic_to_string2() { - let d = diagnostic2(); - 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())); - } - - #[test] - fn test_format_none_frame() { - let actual = format_maybe_frame(None, None, None); - assert_eq!(actual, ""); - } - - #[test] - fn test_format_some_frame() { - let actual = - format_maybe_frame(Some("file://foo/bar.ts"), Some(1), Some(2)); - assert_eq!(strip_ansi_codes(&actual), "file://foo/bar.ts:1:2"); + ], + "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"); } } diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index fad31a3f2b..04c08b8938 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -188,12 +188,10 @@ declare namespace Deno { /** The log category for a diagnostic message. */ export enum DiagnosticCategory { - Log = 0, - Debug = 1, - Info = 2, - Error = 3, - Warning = 4, - Suggestion = 5, + Warning = 0, + Error = 1, + Suggestion = 2, + Message = 3, } export interface DiagnosticMessageChain { @@ -203,37 +201,33 @@ declare namespace Deno { next?: DiagnosticMessageChain[]; } - export interface DiagnosticItem { + export interface Diagnostic { /** A string message summarizing the diagnostic. */ - message: string; + messageText?: 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[]; + relatedInformation?: Diagnostic[]; /** 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; + source?: string; + /** The start position of the error. Zero based index. */ + start?: { + line: number; + character: number; + }; + /** The end position of the error. Zero based index. */ + end?: { + line: number; + character: number; + }; + /** The filename of the resource related to the diagnostic message. */ + fileName?: string; /** 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[]; } /** **UNSTABLE**: new API, yet to be vetted. @@ -247,9 +241,9 @@ declare namespace Deno { * 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. * @@ -530,7 +524,7 @@ declare namespace Deno { rootName: string, sources?: Record, options?: CompilerOptions, - ): Promise<[DiagnosticItem[] | undefined, Record]>; + ): Promise<[Diagnostic[] | undefined, Record]>; /** **UNSTABLE**: new API, yet to be vetted. * @@ -573,7 +567,7 @@ declare namespace Deno { rootName: string, sources?: Record, options?: CompilerOptions, - ): Promise<[DiagnosticItem[] | undefined, string]>; + ): Promise<[Diagnostic[] | undefined, string]>; /** **UNSTABLE**: Should not have same name as `window.location` type. */ interface Location { diff --git a/cli/ops/errors.rs b/cli/ops/errors.rs index 40ec1da30c..c1b07a9a20 100644 --- a/cli/ops/errors.rs +++ b/cli/ops/errors.rs @@ -1,6 +1,6 @@ // 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::CachedMaps; use deno_core::ErrBox; @@ -52,6 +52,6 @@ fn op_format_diagnostic( args: Value, _zero_copy: &mut [ZeroCopyBuf], ) -> Result { - let diagnostic = serde_json::from_value::(args)?; + let diagnostic: Diagnostics = serde_json::from_value(args)?; Ok(json!(diagnostic.to_string())) } diff --git a/cli/rt/40_diagnostics.js b/cli/rt/40_diagnostics.js index 110d3d7670..2b74578530 100644 --- a/cli/rt/40_diagnostics.js +++ b/cli/rt/40_diagnostics.js @@ -6,19 +6,15 @@ ((window) => { const DiagnosticCategory = { - 0: "Log", - 1: "Debug", - 2: "Info", - 3: "Error", - 4: "Warning", - 5: "Suggestion", + 0: "Warning", + 1: "Error", + 2: "Suggestion", + 3: "Message", - Log: 0, - Debug: 1, - Info: 2, - Error: 3, - Warning: 4, - Suggestion: 5, + Warning: 0, + Error: 1, + Suggestion: 2, + Message: 3, }; window.__bootstrap.diagnostics = { diff --git a/cli/rt/40_error_stack.js b/cli/rt/40_error_stack.js index 5d1a077ad0..2b7c424757 100644 --- a/cli/rt/40_error_stack.js +++ b/cli/rt/40_error_stack.js @@ -8,8 +8,8 @@ const internals = window.__bootstrap.internals; const dispatchJson = window.__bootstrap.dispatchJson; - function opFormatDiagnostics(items) { - return dispatchJson.sendSync("op_format_diagnostic", { items }); + function opFormatDiagnostics(diagnostics) { + return dispatchJson.sendSync("op_format_diagnostic", diagnostics); } function opApplySourceMap(location) { diff --git a/cli/tests/unit/format_error_test.ts b/cli/tests/unit/format_error_test.ts index ae7559c82e..f769cc18c5 100644 --- a/cli/tests/unit/format_error_test.ts +++ b/cli/tests/unit/format_error_test.ts @@ -2,27 +2,33 @@ import { assert, unitTest } from "./test_util.ts"; unitTest(function formatDiagnosticBasic() { - const fixture: Deno.DiagnosticItem[] = [ + const fixture: Deno.Diagnostic[] = [ { - message: "Example error", - category: Deno.DiagnosticCategory.Error, - sourceLine: "abcdefghijklmnopqrstuv", - lineNumber: 1000, - scriptResourceName: "foo.ts", - startColumn: 1, - endColumn: 2, - code: 4000, + 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, }, ]; const out = Deno.formatDiagnostics(fixture); - assert(out.includes("Example error")); - assert(out.includes("foo.ts")); + assert(out.includes("Cannot find name")); + assert(out.includes("test.ts")); }); unitTest(function formatDiagnosticError() { let thrown = false; // 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 { Deno.formatDiagnostics(bad); } catch (e) { diff --git a/cli/tsc.rs b/cli/tsc.rs index fa1c79589c..98e73ae21f 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -1,8 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::colors; -use crate::diagnostics::Diagnostic; -use crate::diagnostics::DiagnosticItem; +use crate::diagnostics::Diagnostics; use crate::disk_cache::DiskCache; use crate::file_fetcher::SourceFile; use crate::file_fetcher::SourceFileFetcher; @@ -396,7 +395,7 @@ struct EmittedSource { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct BundleResponse { - diagnostics: Diagnostic, + diagnostics: Diagnostics, bundle_output: Option, stats: Option>, } @@ -404,7 +403,7 @@ struct BundleResponse { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CompileResponse { - diagnostics: Diagnostic, + diagnostics: Diagnostics, emit_map: HashMap, build_info: Option, stats: Option>, @@ -425,14 +424,14 @@ struct TranspileTsOptions { #[serde(rename_all = "camelCase")] #[allow(unused)] struct RuntimeBundleResponse { - diagnostics: Vec, + diagnostics: Diagnostics, output: String, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct RuntimeCompileResponse { - diagnostics: Vec, + diagnostics: Diagnostics, emit_map: HashMap, } @@ -647,7 +646,7 @@ impl TsCompiler { 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())); } @@ -769,7 +768,7 @@ impl TsCompiler { 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())); } @@ -1287,7 +1286,7 @@ pub async fn runtime_compile( 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)?; } diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 895c6e8466..41b52d283c 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -24,262 +24,62 @@ delete Object.prototype.__proto__; const errorStack = window.__bootstrap.errorStack; const errors = window.__bootstrap.errors.errors; - function opNow() { - const res = dispatchJson.sendSync("op_now"); - return res.seconds * 1e3 + res.subsecNanos / 1e6; - } - - const DiagnosticCategory = { - 0: "Log", - 1: "Debug", - 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; - } + /** + * @param {import("../dts/typescript").DiagnosticRelatedInformation} diagnostic + */ + function fromRelatedInformation({ + start, + length, + file, + messageText: msgText, + ...ri + }) { + let messageText; + let messageChain; + if (typeof msgText === "object") { + messageChain = msgText; + } else { + messageText = msgText; } - - 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 { - sourceLine, - lineNumber, - scriptResourceName, - startColumn, - endColumn, - }; - } - - function fromDiagnosticMessageChain(messageChain) { - if (!messageChain) { - return undefined; - } - - return messageChain.map(({ messageText, code, category, next }) => { - const message = transformMessageText(messageText, code); + if (start !== undefined && length !== undefined && file) { + const startPos = file.getLineAndCharacterOfPosition(start); + const sourceLine = file.getFullText().split("\n")[startPos.line]; + const fileName = file.fileName; return { - message, - code, - category: fromDiagnosticCategory(category), - next: fromDiagnosticMessageChain(next), + start: startPos, + end: file.getLineAndCharacterOfPosition(start + length), + fileName, + messageChain, + messageText, + sourceLine, + ...ri, }; + } else { + return { + messageChain, + messageText, + ...ri, + }; + } + } + + /** + * @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) { - 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; - 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 }; + function opNow() { + const res = dispatchJson.sendSync("op_now"); + return res.seconds * 1e3 + res.subsecNanos / 1e6; } // 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 - ? fromTypeScriptDiagnostic(diagnostics).items + ? fromTypeScriptDiagnostic(diagnostics) : []; return { @@ -1413,7 +1213,7 @@ delete Object.prototype.__proto__; }); const maybeDiagnostics = diagnostics.length - ? fromTypeScriptDiagnostic(diagnostics).items + ? fromTypeScriptDiagnostic(diagnostics) : []; return {