Skip to main content
Module

x/deno/cli/tsc/diagnostics.rs

A modern runtime for JavaScript and TypeScript.
Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use deno_ast::ModuleSpecifier;use deno_graph::ModuleGraph;use deno_terminal::colors;
use deno_core::serde::Deserialize;use deno_core::serde::Deserializer;use deno_core::serde::Serialize;use deno_core::serde::Serializer;use deno_core::sourcemap::SourceMap;use std::error::Error;use std::fmt;
const MAX_SOURCE_LINE_LENGTH: usize = 150;
#[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 { /// 0-indexed line number pub line: u64, /// 0-indexed character number 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>, /// Position of this diagnostic in the original non-mapped source. /// /// This will exist and be different from the `start` for fast /// checked modules where the TypeScript source will differ /// from the original source. #[serde(skip_serializing)] pub original_source_start: Option<Position>, pub message_text: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub message_chain: Option<DiagnosticMessageChain>, #[serde(skip_serializing_if = "Option::is_none")] pub source: Option<String>, pub source_line: Option<String>, pub file_name: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] 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.original_source_start.as_ref().or(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$}{}", "", self.message_text.as_deref().unwrap_or_default(), 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() { if !related_information.is_empty() { 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) -> Option<Diagnostic>, { let diagnostics = self.0.iter().filter_map(predicate).collect(); Self(diagnostics) }
pub fn is_empty(&self) -> bool { self.0.is_empty() }
/// Modifies all the diagnostics to have their display positions /// modified to point at the original source. pub fn apply_fast_check_source_maps(&mut self, graph: &ModuleGraph) { fn visit_diagnostic(d: &mut Diagnostic, graph: &ModuleGraph) { if let Some(specifier) = d .file_name .as_ref() .and_then(|n| ModuleSpecifier::parse(n).ok()) { if let Ok(Some(module)) = graph.try_get_prefer_types(&specifier) { if let Some(fast_check_module) = module.js().and_then(|m| m.fast_check_module()) { // todo(dsherret): use a short lived cache to prevent parsing // source maps so often if let Ok(source_map) = SourceMap::from_slice(&fast_check_module.source_map) { if let Some(start) = d.start.as_mut() { let maybe_token = source_map .lookup_token(start.line as u32, start.character as u32); if let Some(token) = maybe_token { d.original_source_start = Some(Position { line: token.get_src_line() as u64, character: token.get_src_col() as u64, }); } } } } } }
if let Some(related) = &mut d.related_information { for d in related.iter_mut() { visit_diagnostic(d, graph); } } }
for d in &mut self.0 { visit_diagnostic(d, graph); } }}
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 {i} errors.")?; }
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"); }}