// Copyright 2018-2019 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::ansi; use serde_json; use serde_json::value::Value; use std::fmt; // A trait which specifies parts of a diagnostic like item needs to be able to // generate to conform its display to other diagnostic like items pub trait DisplayFormatter { fn format_category_and_code(&self) -> String; fn format_message(&self, level: usize) -> String; fn format_related_info(&self) -> String; fn format_source_line(&self, level: usize) -> String; fn format_source_name(&self, level: usize) -> String; } #[derive(Debug, PartialEq, Clone)] pub struct Diagnostic { pub items: Vec, } impl Diagnostic { /// Take a JSON value and attempt to map it to a pub fn from_json_value(v: &serde_json::Value) -> Option { if !v.is_object() { return None; } let obj = v.as_object().unwrap(); let mut items = Vec::::new(); let items_v = &obj["items"]; if items_v.is_array() { let items_values = items_v.as_array().unwrap(); for item_v in items_values { items.push(DiagnosticItem::from_json_value(item_v)); } } Some(Self { items }) } pub fn from_emit_result(json_str: &str) -> Option { let v = serde_json::from_str::(json_str) .expect("Error decoding JSON string."); let diagnostics_o = v.get("diagnostics"); if let Some(diagnostics_v) = diagnostics_o { return Self::from_json_value(diagnostics_v); } None } } 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 { writeln!(f)?; } write!(f, "{}", item.to_string())?; i += 1; } if i > 1 { write!(f, "\n\nFound {} errors.\n", i)?; } Ok(()) } } #[derive(Debug, PartialEq, Clone)] 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, } impl DiagnosticItem { pub fn from_json_value(v: &serde_json::Value) -> Self { let obj = v.as_object().unwrap(); // required attributes let message = obj .get("message") .and_then(|v| v.as_str().map(String::from)) .unwrap(); let category = DiagnosticCategory::from( obj.get("category").and_then(Value::as_i64).unwrap(), ); let code = obj.get("code").and_then(Value::as_i64).unwrap(); // optional attributes let source_line = obj .get("sourceLine") .and_then(|v| v.as_str().map(String::from)); let script_resource_name = obj .get("scriptResourceName") .and_then(|v| v.as_str().map(String::from)); let line_number = obj.get("lineNumber").and_then(Value::as_i64); let start_position = obj.get("startPosition").and_then(Value::as_i64); let end_position = obj.get("endPosition").and_then(Value::as_i64); let start_column = obj.get("startColumn").and_then(Value::as_i64); let end_column = obj.get("endColumn").and_then(Value::as_i64); let message_chain_v = obj.get("messageChain"); let message_chain = match message_chain_v { Some(v) => DiagnosticMessageChain::from_json_value(v), _ => None, }; let related_information_v = obj.get("relatedInformation"); let related_information = match related_information_v { Some(r) => { let mut related_information = Vec::::new(); let related_info_values = r.as_array().unwrap(); for related_info_v in related_info_values { related_information .push(DiagnosticItem::from_json_value(related_info_v)); } Some(related_information) } _ => None, }; Self { message, message_chain, related_information, code, source_line, script_resource_name, line_number, start_position, end_position, category, start_column, end_column, } } } // TODO should chare logic with cli/js_errors, possibly with JSError // implementing the `DisplayFormatter` trait. impl DisplayFormatter for DiagnosticItem { fn format_category_and_code(&self) -> String { let category = match self.category { DiagnosticCategory::Error => { format!("- {}", ansi::red("error".to_string())) } DiagnosticCategory::Warning => "- warn".to_string(), DiagnosticCategory::Debug => "- debug".to_string(), DiagnosticCategory::Info => "- info".to_string(), _ => "".to_string(), }; let code = ansi::grey(format!(" TS{}:", self.code.to_string())).to_string(); format!("{}{} ", category, code) } fn format_message(&self, level: usize) -> String { if self.message_chain.is_none() { return format!("{:indent$}{}", "", self.message, indent = level); } let mut s = String::new(); let mut i = level / 2; let mut item_o = self.message_chain.clone(); while item_o.is_some() { let item = item_o.unwrap(); s.push_str(&std::iter::repeat(" ").take(i * 2).collect::()); s.push_str(&item.message); s.push('\n'); item_o = item.next.clone(); i += 1; } s.pop(); s } fn format_related_info(&self) -> String { if self.related_information.is_none() { return "".to_string(); } let mut s = String::new(); let related_information = self.related_information.clone().unwrap(); for related_diagnostic in related_information { let rd = &related_diagnostic; s.push_str(&format!( "\n{}{}{}\n", rd.format_source_name(2), rd.format_source_line(4), rd.format_message(4), )); } s } fn format_source_line(&self, level: usize) -> String { if self.source_line.is_none() { return "".to_string(); } let source_line = self.source_line.as_ref().unwrap(); // sometimes source_line gets set with an empty string, which then outputs // an empty source line when displayed, so need just short circuit here if source_line.is_empty() { return "".to_string(); } assert!(self.line_number.is_some()); assert!(self.start_column.is_some()); assert!(self.end_column.is_some()); let line = (1 + self.line_number.unwrap()).to_string(); let line_color = ansi::black_on_white(line.to_string()); let line_len = line.clone().len(); let line_padding = ansi::black_on_white(format!("{:indent$}", "", indent = line_len)) .to_string(); let mut s = String::new(); let start_column = self.start_column.unwrap(); let end_column = self.end_column.unwrap(); // TypeScript uses `~` always, but V8 would utilise `^` always, even when // doing ranges, so here, if we only have one marker (very common with V8 // errors) we will use `^` instead. let underline_char = if (end_column - start_column) <= 1 { '^' } else { '~' }; for i in 0..end_column { if i >= start_column { s.push(underline_char); } else { s.push(' '); } } let color_underline = match self.category { DiagnosticCategory::Error => ansi::red(s).to_string(), _ => ansi::cyan(s).to_string(), }; let indent = format!("{:indent$}", "", indent = level); format!( "\n\n{}{} {}\n{}{} {}\n", indent, line_color, source_line, indent, line_padding, color_underline ) } fn format_source_name(&self, level: usize) -> String { if self.script_resource_name.is_none() { return "".to_string(); } let script_name = ansi::cyan(self.script_resource_name.clone().unwrap()); assert!(self.line_number.is_some()); assert!(self.start_column.is_some()); let line = ansi::yellow((1 + self.line_number.unwrap()).to_string()); let column = ansi::yellow((1 + self.start_column.unwrap()).to_string()); format!( "{:indent$}{}:{}:{} ", "", script_name, line, column, indent = level ) } } impl fmt::Display for DiagnosticItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}{}{}{}{}", self.format_source_name(0), self.format_category_and_code(), self.format_message(0), self.format_source_line(0), self.format_related_info(), )?; Ok(()) } } #[derive(Debug, PartialEq, Clone)] pub struct DiagnosticMessageChain { pub message: String, pub code: i64, pub category: DiagnosticCategory, pub next: Option>, } impl DiagnosticMessageChain { pub fn from_json_value(v: &serde_json::Value) -> Option> { if !v.is_object() { return None; } let obj = v.as_object().unwrap(); let message = obj .get("message") .and_then(|v| v.as_str().map(String::from)) .unwrap(); let code = obj.get("code").and_then(Value::as_i64).unwrap(); let category = DiagnosticCategory::from( obj.get("category").and_then(Value::as_i64).unwrap(), ); let next_v = obj.get("next"); let next = match next_v { Some(n) => DiagnosticMessageChain::from_json_value(n), _ => None, }; Some(Box::new(Self { message, code, category, next, })) } } #[derive(Debug, PartialEq, Clone)] pub enum DiagnosticCategory { Log, // 0 Debug, // 1 Info, // 2 Error, // 3 Warning, // 4 Suggestion, // 5 } 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, _ => panic!("Unknown value: {}", value), } } } #[cfg(test)] mod tests { use super::*; use crate::ansi::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(Box::new(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(Box::new(DiagnosticMessageChain { message: "Types of parameters 'o' and 'r' are incompatible.".to_string(), code: 2328, category: DiagnosticCategory::Error, next: Some(Box::new(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, }, ], } } #[test] fn from_json() { let v = serde_json::from_str::( &r#"{ "items": [ { "message": "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.", "messageChain": { "message": "Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value[]'.", "code": 2322, "category": 3, "next": { "message": "Types of parameters 'o' and 'r' are incompatible.", "code": 2328, "category": 3, "next": { "message": "Type 'B' is not assignable to type 'T'.", "code": 2322, "category": 3 } } }, "code": 2322, "category": 3, "startPosition": 235, "endPosition": 241, "sourceLine": " values: o => [", "lineNumber": 18, "scriptResourceName": "/deno/tests/complex_diagnostics.ts", "startColumn": 2, "endColumn": 8, "relatedInformation": [ { "message": "The expected type comes from property 'values' which is declared here on type 'C'", "code": 6500, "category": 2, "startPosition": 78, "endPosition": 84, "sourceLine": " values?: (r: T) => Array>;", "lineNumber": 6, "scriptResourceName": "/deno/tests/complex_diagnostics.ts", "startColumn": 2, "endColumn": 8 } ] }, { "message": "Property 't' does not exist on type 'T'.", "code": 2339, "category": 3, "startPosition": 267, "endPosition": 268, "sourceLine": " v: o.t,", "lineNumber": 20, "scriptResourceName": "/deno/tests/complex_diagnostics.ts", "startColumn": 11, "endColumn": 12 } ] }"#, ).unwrap(); let r = Diagnostic::from_json_value(&v); let expected = Some(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(Box::new(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(Box::new(DiagnosticMessageChain { message: "Types of parameters 'o' and 'r' are incompatible.".to_string(), code: 2328, category: DiagnosticCategory::Error, next: Some(Box::new(DiagnosticMessageChain { message: "Type 'B' is not assignable to type 'T'.".to_string(), code: 2322, category: DiagnosticCategory::Error, next: None, })), })), })), related_information: Some(vec![ DiagnosticItem { message: "The expected type comes from property 'values' which is declared here on type 'C'".to_string(), message_chain: None, related_information: None, source_line: Some(" values?: (r: T) => Array>;".to_string()), line_number: Some(6), script_resource_name: Some("/deno/tests/complex_diagnostics.ts".to_string()), start_position: Some(78), end_position: Some(84), category: DiagnosticCategory::Info, code: 6500, start_column: Some(2), end_column: Some(8), } ]), source_line: Some(" values: o => [".to_string()), line_number: Some(18), script_resource_name: Some("/deno/tests/complex_diagnostics.ts".to_string()), start_position: Some(235), end_position: Some(241), category: DiagnosticCategory::Error, code: 2322, start_column: Some(2), end_column: Some(8), }, DiagnosticItem { message: "Property 't' does not exist on type 'T'.".to_string(), message_chain: None, related_information: None, source_line: Some(" v: o.t,".to_string()), line_number: Some(20), script_resource_name: Some("/deno/tests/complex_diagnostics.ts".to_string()), start_position: Some(267), end_position: Some(268), category: DiagnosticCategory::Error, code: 2339, start_column: Some(11), end_column: Some(12), }, ], }); assert_eq!(expected, r); } #[test] fn from_emit_result() { let r = Diagnostic::from_emit_result( &r#"{ "emitSkipped": false, "diagnostics": { "items": [ { "message": "foo bar", "code": 9999, "category": 3 } ] } }"#, ); let expected = Some(Diagnostic { items: vec![DiagnosticItem { message: "foo bar".to_string(), message_chain: None, related_information: None, source_line: None, line_number: None, script_resource_name: None, start_position: None, end_position: None, category: DiagnosticCategory::Error, code: 9999, start_column: None, end_column: None, }], }); assert_eq!(expected, r); } #[test] fn from_emit_result_none() { let r = &r#"{"emitSkipped":false}"#; assert!(Diagnostic::from_emit_result(r).is_none()); } #[test] fn diagnostic_to_string1() { let d = diagnostic1(); let expected = "deno/tests/complex_diagnostics.ts:19:3 - error TS2322: 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\n19 values: o => [\n ~~~~~~\n\n deno/tests/complex_diagnostics.ts:7:3 \n\n 7 values?: (r: T) => Array>;\n ~~~~~~\n The expected type comes from property \'values\' which is declared here on type \'SettingsInterface\'\n"; assert_eq!(expected, strip_ansi_codes(&d.to_string())); } #[test] fn diagnostic_to_string2() { let d = diagnostic2(); let expected = "deno/tests/complex_diagnostics.ts:19:3 - error TS2322: Example 1\n\n19 values: o => [\n ~~~~~~\n\n/foo/bar.ts:129:3 - error TS2000: Example 2\n\n129 values: undefined,\n ~~~~~~\n\n\nFound 2 errors.\n"; assert_eq!(expected, strip_ansi_codes(&d.to_string())); } }