diff options
Diffstat (limited to 'cli/tsc/diagnostics.rs')
-rw-r--r-- | cli/tsc/diagnostics.rs | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/cli/tsc/diagnostics.rs b/cli/tsc/diagnostics.rs new file mode 100644 index 000000000..05502dca4 --- /dev/null +++ b/cli/tsc/diagnostics.rs @@ -0,0 +1,595 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use deno_runtime::colors; + +use deno_core::serde::Deserialize; +use deno_core::serde::Deserializer; +use deno_core::serde::Serialize; +use deno_core::serde::Serializer; +use once_cell::sync::Lazy; +use regex::Regex; +use std::error::Error; +use std::fmt; + +const MAX_SOURCE_LINE_LENGTH: usize = 150; + +const UNSTABLE_DENO_PROPS: &[&str] = &[ + "CreateHttpClientOptions", + "DatagramConn", + "HttpClient", + "UnixConnectOptions", + "UnixListenOptions", + "connect", + "createHttpClient", + "kill", + "listen", + "listenDatagram", + "dlopen", + "ppid", + "removeSignalListener", + "shutdown", + "umask", + "spawnChild", + "Child", + "spawn", + "spawnSync", + "SpawnOptions", + "ChildStatus", + "SpawnOutput", + "command", + "Command", + "CommandOptions", + "CommandStatus", + "CommandOutput", + "serve", + "ServeInit", + "ServeTlsInit", + "Handler", +]; + +static MSG_MISSING_PROPERTY_DENO: Lazy<Regex> = Lazy::new(|| { + Regex::new(r#"Property '([^']+)' does not exist on type 'typeof Deno'"#) + .unwrap() +}); + +static MSG_SUGGESTION: Lazy<Regex> = + Lazy::new(|| Regex::new(r#" Did you mean '([^']+)'\?"#).unwrap()); + +/// 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()); + } + } + } + + msg.to_string() + } + 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()); + } + } + } + + msg.to_string() + } + _ => msg.to_string(), + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DiagnosticCategory { + 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 { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let s: i64 = Deserialize::deserialize(deserializer)?; + Ok(DiagnosticCategory::from(s)) + } +} + +impl Serialize for DiagnosticCategory { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let value = match self { + DiagnosticCategory::Warning => 0_i32, + DiagnosticCategory::Error => 1_i32, + DiagnosticCategory::Suggestion => 2_i32, + DiagnosticCategory::Message => 3_i32, + }; + Serialize::serialize(&value, serializer) + } +} + +impl From<i64> for DiagnosticCategory { + fn from(value: i64) -> Self { + match value { + 0 => DiagnosticCategory::Warning, + 1 => DiagnosticCategory::Error, + 2 => DiagnosticCategory::Suggestion, + 3 => DiagnosticCategory::Message, + _ => panic!("Unknown value: {}", value), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[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(&" ".repeat(level * 2)); + 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, Serialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Position { + pub line: u64, + pub character: u64, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Diagnostic { + pub category: DiagnosticCategory, + pub code: u64, + pub start: Option<Position>, + pub end: Option<Position>, + pub message_text: Option<String>, + pub message_chain: Option<DiagnosticMessageChain>, + pub source: Option<String>, + pub source_line: Option<String>, + pub file_name: Option<String>, + pub 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", + _ => "", + }; + + let code = if self.code >= 900001 { + "".to_string() + } else { + colors::bold(format!("TS{} ", self.code)).to_string() + }; + + if !category.is_empty() { + write!(f, "{}[{}]: ", 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, Default, Eq, PartialEq)] +pub struct Diagnostics(Vec<Diagnostic>); + +impl Diagnostics { + #[cfg(test)] + pub fn new(diagnostics: Vec<Diagnostic>) -> Self { + Diagnostics(diagnostics) + } + + /// Return a set of diagnostics where only the values where the predicate + /// returns `true` are included. + pub fn filter<P>(&self, predicate: P) -> Self + where + P: FnMut(&Diagnostic) -> bool, + { + let diagnostics = self.0.clone().into_iter().filter(predicate).collect(); + Self(diagnostics) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +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 Serialize for Diagnostics { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + Serialize::serialize(&self.0, serializer) + } +} + +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)?; + i += 1; + } + + if i > 1 { + write!(f, "\n\nFound {} errors.", i)?; + } + + Ok(()) + } +} + +impl Error for Diagnostics {} + +#[cfg(test)] +mod tests { + use super::*; + use deno_core::serde_json; + use deno_core::serde_json::json; + use test_util::strip_ansi_codes; + + #[test] + 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": [ + { + "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": [ + { + "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 = diagnostics.to_string(); + 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 = diagnostics.to_string(); + 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 + }, + "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 = diagnostics.to_string(); + 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"); + } +} |