// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::colors; use regex::Regex; use serde::Deserialize; use serde::Deserializer; use std::error::Error; use std::fmt; 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", "systemMemoryInfo", "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(); } /// 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, 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(deserializer: D) -> Result where D: Deserializer<'de>, { let s: i64 = Deserialize::deserialize(deserializer)?; Ok(DiagnosticCategory::from(s)) } } impl From 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, 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 colors::strip_ansi_codes; use serde_json::json; #[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 = 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 }, "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] 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 } ], "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"); } }