mirror of
https://github.com/denoland/deno.git
synced 2024-11-01 09:24:20 -04:00
629 lines
17 KiB
Rust
629 lines
17 KiB
Rust
// Copyright 2018-2021 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 deno_graph::ModuleGraphError;
|
|
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] = &[
|
|
"CompilerOptions",
|
|
"CreateHttpClientOptions",
|
|
"DatagramConn",
|
|
"Diagnostic",
|
|
"DiagnosticCategory",
|
|
"DiagnosticItem",
|
|
"DiagnosticMessageChain",
|
|
"EmitOptions",
|
|
"EmitResult",
|
|
"HttpClient",
|
|
"Location",
|
|
"MXRecord",
|
|
"Metrics",
|
|
"OpMetrics",
|
|
"RecordType",
|
|
"SRVRecord",
|
|
"SetRawOptions",
|
|
"SignalStream",
|
|
"StartTlsOptions",
|
|
"SystemMemoryInfo",
|
|
"UnixConnectOptions",
|
|
"UnixListenOptions",
|
|
"addSignalListener",
|
|
"applySourceMap",
|
|
"connect",
|
|
"consoleSize",
|
|
"createHttpClient",
|
|
"emit",
|
|
"formatDiagnostics",
|
|
"futime",
|
|
"futimeSync",
|
|
"hostname",
|
|
"kill",
|
|
"listen",
|
|
"listenDatagram",
|
|
"loadavg",
|
|
"dlopen",
|
|
"osRelease",
|
|
"ppid",
|
|
"removeSignalListener",
|
|
"setRaw",
|
|
"shutdown",
|
|
"Signal",
|
|
"sleepSync",
|
|
"startTls",
|
|
"systemMemoryInfo",
|
|
"umask",
|
|
"utime",
|
|
"utimeSync",
|
|
];
|
|
|
|
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)
|
|
}
|
|
|
|
pub fn extend_graph_errors(&mut self, errors: Vec<ModuleGraphError>) {
|
|
self.0.extend(errors.into_iter().map(|err| Diagnostic {
|
|
category: DiagnosticCategory::Error,
|
|
code: 900001,
|
|
start: None,
|
|
end: None,
|
|
message_text: Some(err.to_string()),
|
|
message_chain: None,
|
|
source: None,
|
|
source_line: None,
|
|
file_name: Some(err.specifier().to_string()),
|
|
related_information: None,
|
|
}));
|
|
}
|
|
|
|
/// 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.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 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");
|
|
}
|
|
}
|