2020-01-02 15:13:47 -05:00
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
2019-06-04 09:03:56 -04:00
//! This module encodes TypeScript errors (diagnostics) into Rust structs and
//! contains code for printing them to the console.
2020-02-24 14:48:14 -05:00
// TODO(ry) This module does a lot of JSON parsing manually. It should use
// serde_json.
2019-09-15 14:48:25 -04:00
use crate ::colors ;
2020-04-20 15:39:02 -04:00
use crate ::fmt_errors ::format_stack ;
2020-05-05 12:23:15 -04:00
use serde ::Deserialize ;
use serde ::Deserializer ;
2019-07-10 18:53:48 -04:00
use std ::error ::Error ;
2019-06-04 09:03:56 -04:00
use std ::fmt ;
2020-05-05 12:23:15 -04:00
#[ derive(Clone, Debug, Deserialize, PartialEq) ]
#[ serde(rename_all = " camelCase " ) ]
2019-06-04 09:03:56 -04:00
pub struct Diagnostic {
pub items : Vec < DiagnosticItem > ,
}
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 {
2020-04-20 15:39:02 -04:00
write! ( f , " \n \n " ) ? ;
2019-06-04 09:03:56 -04:00
}
write! ( f , " {} " , item . to_string ( ) ) ? ;
i + = 1 ;
}
if i > 1 {
2020-04-20 15:39:02 -04:00
write! ( f , " \n \n Found {} errors. " , i ) ? ;
2019-06-04 09:03:56 -04:00
}
Ok ( ( ) )
}
}
2019-07-10 18:53:48 -04:00
impl Error for Diagnostic {
fn description ( & self ) -> & str {
& self . items [ 0 ] . message
}
}
2020-05-05 12:23:15 -04:00
#[ derive(Clone, Debug, Deserialize, PartialEq) ]
#[ serde(rename_all = " camelCase " ) ]
2019-06-04 09:03:56 -04:00
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.
2019-09-17 12:24:44 -04:00
pub message_chain : Option < DiagnosticMessageChain > ,
2019-06-04 09:03:56 -04:00
/// Other diagnostic items that are related to the diagnostic, usually these
/// are suggestions of why an error occurred.
pub related_information : Option < Vec < DiagnosticItem > > ,
/// The source line the diagnostic is in reference to.
pub source_line : Option < String > ,
/// Zero-based index to the line number of the error.
pub line_number : Option < i64 > ,
/// The resource name provided to the TypeScript compiler.
pub script_resource_name : Option < String > ,
/// Zero-based index to the start position in the entire script resource.
pub start_position : Option < i64 > ,
/// Zero-based index to the end position in the entire script resource.
pub end_position : Option < i64 > ,
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 < i64 > ,
/// Zero-based index to the end column on `line_number`.
pub end_column : Option < i64 > ,
}
2020-04-20 15:39:02 -04:00
fn format_category_and_code (
category : & DiagnosticCategory ,
code : i64 ,
) -> String {
let category = match category {
2020-05-09 15:09:46 -04:00
DiagnosticCategory ::Error = > " ERROR " . to_string ( ) ,
DiagnosticCategory ::Warning = > " WARN " . to_string ( ) ,
DiagnosticCategory ::Debug = > " DEBUG " . to_string ( ) ,
DiagnosticCategory ::Info = > " INFO " . to_string ( ) ,
2020-04-20 15:39:02 -04:00
_ = > " " . to_string ( ) ,
} ;
2019-06-04 09:03:56 -04:00
2020-04-20 15:39:02 -04:00
let code = colors ::bold ( format! ( " TS {} " , code . to_string ( ) ) ) . to_string ( ) ;
2019-06-04 09:03:56 -04:00
2020-05-09 15:09:46 -04:00
format! ( " {} [ {} ] " , code , category )
2020-04-20 15:39:02 -04:00
}
fn format_message (
message_chain : & Option < DiagnosticMessageChain > ,
message : & str ,
level : usize ,
) -> String {
debug! ( " format_message " ) ;
if message_chain . is_none ( ) {
return format! ( " {:indent$} {} " , " " , message , indent = level ) ;
2019-06-04 09:03:56 -04:00
}
2020-04-20 15:39:02 -04:00
let mut s = message_chain . clone ( ) . unwrap ( ) . format_message ( level ) ;
s . pop ( ) ;
2019-06-04 09:03:56 -04:00
2020-04-20 15:39:02 -04:00
s
}
2019-06-04 09:03:56 -04:00
2020-04-20 15:39:02 -04:00
/// Formats optional source, line and column numbers into a single string.
fn format_maybe_frame (
file_name : Option < String > ,
line_number : Option < i64 > ,
column_number : Option < i64 > ,
) -> String {
if file_name . is_none ( ) {
return " " . to_string ( ) ;
2019-06-04 09:03:56 -04:00
}
2020-04-20 15:39:02 -04:00
assert! ( line_number . is_some ( ) ) ;
assert! ( column_number . is_some ( ) ) ;
2019-06-04 09:03:56 -04:00
2020-04-20 15:39:02 -04:00
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 )
}
2019-06-04 09:03:56 -04:00
2020-04-20 15:39:02 -04:00
fn format_maybe_related_information (
related_information : & Option < Vec < DiagnosticItem > > ,
) -> String {
if related_information . is_none ( ) {
return " " . to_string ( ) ;
2019-06-04 09:03:56 -04:00
}
2020-04-20 15:39:02 -04:00
let mut s = String ::new ( ) ;
let related_information = related_information . clone ( ) . unwrap ( ) ;
for rd in related_information {
s . push_str ( " \n \n " ) ;
s . push_str ( & format_stack (
match rd . category {
2019-06-19 22:07:01 -04:00
DiagnosticCategory ::Error = > true ,
_ = > false ,
} ,
2020-04-20 15:39:02 -04:00
format_message ( & rd . message_chain , & rd . message , 0 ) ,
rd . source_line . clone ( ) ,
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 . clone ( ) ,
rd . line_number . map ( | n | n + 1 ) ,
rd . start_column . map ( | n | n + 1 ) ,
) ] ,
4 ,
) ) ;
2019-06-04 09:03:56 -04:00
}
2020-04-20 15:39:02 -04:00
s
2019-06-04 09:03:56 -04:00
}
impl fmt ::Display for DiagnosticItem {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
write! (
f ,
2020-04-20 15:39:02 -04:00
" {} " ,
format_stack (
match self . category {
DiagnosticCategory ::Error = > true ,
_ = > false ,
} ,
format! (
" {}: {} " ,
format_category_and_code ( & self . category , self . code ) ,
format_message ( & self . message_chain , & self . message , 0 )
) ,
self . source_line . clone ( ) ,
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 . clone ( ) ,
self . line_number . map ( | n | n + 1 ) ,
self . start_column . map ( | n | n + 1 )
) ] ,
0
)
) ? ;
write! (
f ,
" {} " ,
format_maybe_related_information ( & self . related_information ) ,
2019-06-19 22:07:01 -04:00
)
2019-06-04 09:03:56 -04:00
}
}
2020-05-05 12:23:15 -04:00
#[ derive(Clone, Debug, Deserialize, PartialEq) ]
#[ serde(rename_all = " camelCase " ) ]
2019-06-04 09:03:56 -04:00
pub struct DiagnosticMessageChain {
pub message : String ,
pub code : i64 ,
pub category : DiagnosticCategory ,
2019-09-17 12:24:44 -04:00
pub next : Option < Vec < DiagnosticMessageChain > > ,
2019-06-04 09:03:56 -04:00
}
impl DiagnosticMessageChain {
2019-09-17 12:24:44 -04:00
pub fn format_message ( & self , level : usize ) -> String {
let mut s = String ::new ( ) ;
s . push_str ( & std ::iter ::repeat ( " " ) . take ( level * 2 ) . collect ::< String > ( ) ) ;
s . push_str ( & self . message ) ;
s . push ( '\n' ) ;
if self . next . is_some ( ) {
let arr = self . next . clone ( ) . unwrap ( ) ;
for dm in arr {
s . push_str ( & dm . format_message ( level + 1 ) ) ;
}
}
s
2019-06-04 09:03:56 -04:00
}
}
2020-05-05 12:23:15 -04:00
#[ derive(Clone, Debug, PartialEq) ]
2019-06-04 09:03:56 -04:00
pub enum DiagnosticCategory {
Log , // 0
Debug , // 1
Info , // 2
Error , // 3
Warning , // 4
Suggestion , // 5
}
2020-05-05 12:23:15 -04:00
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 ) )
}
}
2019-06-04 09:03:56 -04:00
impl From < i64 > 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 ::* ;
2019-09-15 14:48:25 -04:00
use crate ::colors ::strip_ansi_codes ;
2019-06-04 09:03:56 -04:00
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<B>[]'. " . to_string ( ) ,
2019-09-17 12:24:44 -04:00
message_chain : Some ( DiagnosticMessageChain {
2019-06-04 09:03:56 -04:00
message : " Type '(o: T) => { v: any; f: (x: B) => string; }[]' is not assignable to type '(r: B) => Value<B>[]'. " . to_string ( ) ,
code : 2322 ,
category : DiagnosticCategory ::Error ,
2019-09-17 12:24:44 -04:00
next : Some ( vec! [ DiagnosticMessageChain {
2019-06-04 09:03:56 -04:00
message : " Types of parameters 'o' and 'r' are incompatible. " . to_string ( ) ,
code : 2328 ,
category : DiagnosticCategory ::Error ,
2019-09-17 12:24:44 -04:00
next : Some ( vec! [ DiagnosticMessageChain {
2019-06-04 09:03:56 -04:00
message : " Type 'B' is not assignable to type 'T'. " . to_string ( ) ,
code : 2322 ,
category : DiagnosticCategory ::Error ,
next : None ,
2019-09-17 12:24:44 -04:00
} ] ) ,
} ] ) ,
} ) ,
2019-06-04 09:03:56 -04:00
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<B>' " . to_string ( ) ,
message_chain : None ,
related_information : None ,
code : 6500 ,
source_line : Some ( " values?: (r: T) => Array<Value<T>>; " . 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 ( ) {
2020-05-05 12:23:15 -04:00
let r = serde_json ::from_str ::< Diagnostic > (
2019-06-04 09:03:56 -04:00
& r #" {
" items " : [
{
2019-09-17 12:24:44 -04:00
" message " : " Type '{ a(): { b: number; }; }' is not assignable to type '{ a(): { b: string; }; }'. " ,
2019-06-04 09:03:56 -04:00
" messageChain " : {
2019-09-17 12:24:44 -04:00
" message " : " Type '{ a(): { b: number; }; }' is not assignable to type '{ a(): { b: string; }; }'. " ,
2019-06-04 09:03:56 -04:00
" code " : 2322 ,
" category " : 3 ,
2019-09-17 12:24:44 -04:00
" next " : [
{
" message " : " Types of property 'a' are incompatible. " ,
" code " : 2326 ,
2019-06-04 09:03:56 -04:00
" category " : 3
}
2019-09-17 12:24:44 -04:00
]
2019-06-04 09:03:56 -04:00
} ,
" code " : 2322 ,
" category " : 3 ,
2019-09-17 12:24:44 -04:00
" startPosition " : 352 ,
" endPosition " : 353 ,
" sourceLine " : " x = y; " ,
" lineNumber " : 29 ,
" scriptResourceName " : " /deno/tests/error_003_typescript.ts " ,
" startColumn " : 0 ,
" endColumn " : 1
2019-06-04 09:03:56 -04:00
}
]
} " #,
) . unwrap ( ) ;
2020-05-05 12:23:15 -04:00
let expected =
2019-09-17 12:24:44 -04:00
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 ( ) ,
2019-06-04 09:03:56 -04:00
code : 2322 ,
category : DiagnosticCategory ::Error ,
2019-09-17 12:24:44 -04:00
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 )
}
]
2020-05-05 12:23:15 -04:00
} ;
2019-06-04 09:03:56 -04:00
assert_eq! ( expected , r ) ;
}
#[ test ]
fn diagnostic_to_string1 ( ) {
let d = diagnostic1 ( ) ;
2020-05-09 15:09:46 -04:00
let expected = " TS2322 [ERROR]: Type \' (o: T) => { v: any; f: (x: B) => string; }[] \' is not assignable to type \' (r: B) => Value<B>[] \' . \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<B> \' \n values?: (r: T) => Array<Value<T>>; \n ~~~~~~ \n at deno/tests/complex_diagnostics.ts:7:3 " ;
2019-06-04 09:03:56 -04:00
assert_eq! ( expected , strip_ansi_codes ( & d . to_string ( ) ) ) ;
}
#[ test ]
fn diagnostic_to_string2 ( ) {
let d = diagnostic2 ( ) ;
2020-05-09 15:09:46 -04:00
let expected = " TS2322 [ERROR]: Example 1 \n values: o => [ \n ~~~~~~ \n at deno/tests/complex_diagnostics.ts:19:3 \n \n TS2000 [ERROR]: Example 2 \n values: undefined, \n ~~~~~~ \n at /foo/bar.ts:129:3 \n \n Found 2 errors. " ;
2019-06-04 09:03:56 -04:00
assert_eq! ( expected , strip_ansi_codes ( & d . to_string ( ) ) ) ;
}
2020-04-20 15:39:02 -04:00
#[ 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 " . to_string ( ) ) ,
Some ( 1 ) ,
Some ( 2 ) ,
) ;
assert_eq! ( strip_ansi_codes ( & actual ) , " file://foo/bar.ts:1:2 " ) ;
}
2019-06-04 09:03:56 -04:00
}