mirror of
https://github.com/denoland/deno.git
synced 2024-11-22 15:06:54 -05:00
ab0c33ebf8
This commit stabilizes "Deno.consoleSize()" API. There is one change compared to previous unstable API, in that the API doesn't accept any arguments. Console size is established by querying syscalls for stdio streams at fd 0, 1 and 2.
608 lines
16 KiB
Rust
608 lines
16 KiB
Rust
// 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] = &[
|
|
"BenchDefinition",
|
|
"CreateHttpClientOptions",
|
|
"DatagramConn",
|
|
"Diagnostic",
|
|
"DiagnosticCategory",
|
|
"DiagnosticItem",
|
|
"DiagnosticMessageChain",
|
|
"EmitOptions",
|
|
"EmitResult",
|
|
"HttpClient",
|
|
"Location",
|
|
"Metrics",
|
|
"OpMetrics",
|
|
"SignalStream",
|
|
"StartTlsOptions",
|
|
"SystemMemoryInfo",
|
|
"UnixConnectOptions",
|
|
"UnixListenOptions",
|
|
"addSignalListener",
|
|
"bench",
|
|
"connect",
|
|
"createHttpClient",
|
|
"kill",
|
|
"listen",
|
|
"listenDatagram",
|
|
"dlopen",
|
|
"osRelease",
|
|
"ppid",
|
|
"removeSignalListener",
|
|
"shutdown",
|
|
"Signal",
|
|
"startTls",
|
|
"systemMemoryInfo",
|
|
"umask",
|
|
"spawnChild",
|
|
"Child",
|
|
"spawn",
|
|
"spawnSync",
|
|
"ChildStatus",
|
|
"SpawnOutput",
|
|
"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");
|
|
}
|
|
}
|