summaryrefslogtreecommitdiff
path: root/cli/tsc/diagnostics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tsc/diagnostics.rs')
-rw-r--r--cli/tsc/diagnostics.rs595
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");
+ }
+}