2024-01-23 10:37:43 -05:00
|
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
|
|
|
|
|
|
use std::borrow::Cow;
|
|
|
|
|
use std::fmt;
|
|
|
|
|
use std::fmt::Display;
|
|
|
|
|
use std::fmt::Write as _;
|
2024-01-24 16:24:52 -05:00
|
|
|
|
use std::path::PathBuf;
|
2024-01-23 10:37:43 -05:00
|
|
|
|
|
|
|
|
|
use deno_ast::ModuleSpecifier;
|
|
|
|
|
use deno_ast::SourcePos;
|
|
|
|
|
use deno_ast::SourceRange;
|
|
|
|
|
use deno_ast::SourceRanged;
|
|
|
|
|
use deno_ast::SourceTextInfo;
|
|
|
|
|
use deno_graph::ParsedSourceStore;
|
|
|
|
|
use deno_runtime::colors;
|
|
|
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
|
|
|
|
|
|
pub trait SourceTextStore {
|
|
|
|
|
fn get_source_text<'a>(
|
|
|
|
|
&'a self,
|
|
|
|
|
specifier: &ModuleSpecifier,
|
|
|
|
|
) -> Option<Cow<'a, SourceTextInfo>>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct SourceTextParsedSourceStore<'a>(pub &'a dyn ParsedSourceStore);
|
|
|
|
|
|
|
|
|
|
impl SourceTextStore for SourceTextParsedSourceStore<'_> {
|
|
|
|
|
fn get_source_text<'a>(
|
|
|
|
|
&'a self,
|
|
|
|
|
specifier: &ModuleSpecifier,
|
|
|
|
|
) -> Option<Cow<'a, SourceTextInfo>> {
|
|
|
|
|
let parsed_source = self.0.get_parsed_source(specifier)?;
|
|
|
|
|
Some(Cow::Owned(parsed_source.text_info().clone()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub enum DiagnosticLevel {
|
|
|
|
|
Error,
|
|
|
|
|
Warning,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
pub struct DiagnosticSourceRange {
|
|
|
|
|
pub start: DiagnosticSourcePos,
|
|
|
|
|
pub end: DiagnosticSourcePos,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
pub enum DiagnosticSourcePos {
|
|
|
|
|
SourcePos(SourcePos),
|
|
|
|
|
ByteIndex(usize),
|
2024-01-24 16:59:18 -05:00
|
|
|
|
LineAndCol {
|
|
|
|
|
// 0-indexed line number
|
|
|
|
|
line: usize,
|
|
|
|
|
// 0-indexed column number
|
|
|
|
|
column: usize,
|
|
|
|
|
},
|
2024-01-23 10:37:43 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DiagnosticSourcePos {
|
|
|
|
|
fn pos(&self, source: &SourceTextInfo) -> SourcePos {
|
|
|
|
|
match self {
|
|
|
|
|
DiagnosticSourcePos::SourcePos(pos) => *pos,
|
|
|
|
|
DiagnosticSourcePos::ByteIndex(index) => source.range().start() + *index,
|
2024-01-24 16:59:18 -05:00
|
|
|
|
DiagnosticSourcePos::LineAndCol { line, column } => {
|
|
|
|
|
source.line_start(*line) + *column
|
|
|
|
|
}
|
2024-01-23 10:37:43 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum DiagnosticLocation<'a> {
|
2024-01-24 16:24:52 -05:00
|
|
|
|
/// The diagnostic is relevant to a specific path.
|
|
|
|
|
Path { path: PathBuf },
|
|
|
|
|
/// The diagnostic is relevant to an entire module.
|
|
|
|
|
Module {
|
2024-01-23 10:37:43 -05:00
|
|
|
|
/// The specifier of the module that contains the diagnostic.
|
|
|
|
|
specifier: Cow<'a, ModuleSpecifier>,
|
|
|
|
|
},
|
2024-01-24 16:24:52 -05:00
|
|
|
|
/// The diagnostic is relevant to a specific position in a module.
|
2024-01-23 10:37:43 -05:00
|
|
|
|
///
|
|
|
|
|
/// This variant will get the relevant `SouceTextInfo` from the cache using
|
|
|
|
|
/// the given specifier, and will then calculate the line and column numbers
|
|
|
|
|
/// from the given `SourcePos`.
|
2024-01-24 16:24:52 -05:00
|
|
|
|
ModulePosition {
|
2024-01-23 10:37:43 -05:00
|
|
|
|
/// The specifier of the module that contains the diagnostic.
|
|
|
|
|
specifier: Cow<'a, ModuleSpecifier>,
|
|
|
|
|
/// The source position of the diagnostic.
|
|
|
|
|
source_pos: DiagnosticSourcePos,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> DiagnosticLocation<'a> {
|
|
|
|
|
/// Return the line and column number of the diagnostic.
|
|
|
|
|
///
|
|
|
|
|
/// The line number is 1-indexed.
|
|
|
|
|
///
|
|
|
|
|
/// The column number is 1-indexed. This is the number of UTF-16 code units
|
|
|
|
|
/// from the start of the line to the diagnostic.
|
|
|
|
|
/// Why UTF-16 code units? Because that's what VS Code understands, and
|
|
|
|
|
/// everyone uses VS Code. :)
|
|
|
|
|
fn position(&self, sources: &dyn SourceTextStore) -> Option<(usize, usize)> {
|
|
|
|
|
match self {
|
2024-01-24 16:24:52 -05:00
|
|
|
|
DiagnosticLocation::Path { .. } => None,
|
|
|
|
|
DiagnosticLocation::Module { .. } => None,
|
|
|
|
|
DiagnosticLocation::ModulePosition {
|
2024-01-23 10:37:43 -05:00
|
|
|
|
specifier,
|
|
|
|
|
source_pos,
|
|
|
|
|
} => {
|
|
|
|
|
let source = sources.get_source_text(specifier).expect(
|
|
|
|
|
"source text should be in the cache if the location is in a file",
|
|
|
|
|
);
|
|
|
|
|
let pos = source_pos.pos(&source);
|
|
|
|
|
let line_index = source.line_index(pos);
|
|
|
|
|
let line_start_pos = source.line_start(line_index);
|
|
|
|
|
let content = source.range_text(&SourceRange::new(line_start_pos, pos));
|
|
|
|
|
let line = line_index + 1;
|
|
|
|
|
let column = content.encode_utf16().count() + 1;
|
|
|
|
|
Some((line, column))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct DiagnosticSnippet<'a> {
|
|
|
|
|
/// The source text for this snippet. The
|
|
|
|
|
pub source: DiagnosticSnippetSource<'a>,
|
|
|
|
|
/// The piece of the snippet that should be highlighted.
|
|
|
|
|
pub highlight: DiagnosticSnippetHighlight<'a>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct DiagnosticSnippetHighlight<'a> {
|
|
|
|
|
/// The range of the snippet that should be highlighted.
|
|
|
|
|
pub range: DiagnosticSourceRange,
|
|
|
|
|
/// The style of the highlight.
|
|
|
|
|
pub style: DiagnosticSnippetHighlightStyle,
|
|
|
|
|
/// An optional inline description of the highlight.
|
|
|
|
|
pub description: Option<Cow<'a, str>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub enum DiagnosticSnippetHighlightStyle {
|
|
|
|
|
/// The highlight is an error. This will place red carets under the highlight.
|
|
|
|
|
Error,
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
/// The highlight is a warning. This will place yellow carets under the
|
|
|
|
|
/// highlight.
|
|
|
|
|
Warning,
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
/// The highlight shows code additions. This will place green + signs under
|
|
|
|
|
/// the highlight and will highlight the code in green.
|
|
|
|
|
Addition,
|
|
|
|
|
/// The highlight shows a hint. This will place blue dashes under the
|
|
|
|
|
/// highlight.
|
|
|
|
|
Hint,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DiagnosticSnippetHighlightStyle {
|
|
|
|
|
fn style_underline(
|
|
|
|
|
&self,
|
|
|
|
|
s: impl std::fmt::Display,
|
|
|
|
|
) -> impl std::fmt::Display {
|
|
|
|
|
match self {
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Error => colors::red_bold(s),
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Warning => colors::yellow_bold(s),
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Addition => colors::green_bold(s),
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Hint => colors::intense_blue(s),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn underline_char(&self) -> char {
|
|
|
|
|
match self {
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Error => '^',
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Warning => '^',
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Addition => '+',
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Hint => '-',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub enum DiagnosticSnippetSource<'a> {
|
|
|
|
|
/// The specifier of the module that should be displayed in this snippet. The
|
|
|
|
|
/// contents of the file will be retrieved from the `SourceTextStore`.
|
|
|
|
|
Specifier(Cow<'a, ModuleSpecifier>),
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
/// The source text that should be displayed in this snippet.
|
|
|
|
|
///
|
|
|
|
|
/// This should be used if the text of the snippet is not available in the
|
|
|
|
|
/// `SourceTextStore`.
|
|
|
|
|
SourceTextInfo(Cow<'a, deno_ast::SourceTextInfo>),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> DiagnosticSnippetSource<'a> {
|
|
|
|
|
fn to_source_text_info(
|
|
|
|
|
&self,
|
|
|
|
|
sources: &'a dyn SourceTextStore,
|
|
|
|
|
) -> Cow<'a, SourceTextInfo> {
|
|
|
|
|
match self {
|
|
|
|
|
DiagnosticSnippetSource::Specifier(specifier) => {
|
|
|
|
|
sources.get_source_text(specifier).expect(
|
|
|
|
|
"source text should be in the cache if snippet source is a specifier",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
DiagnosticSnippetSource::SourceTextInfo(info) => info.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the text of the line with the given number.
|
|
|
|
|
fn line_text(source: &SourceTextInfo, line_number: usize) -> &str {
|
|
|
|
|
source.line_text(line_number - 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the text of the line that contains the given position, split at the
|
|
|
|
|
/// given position.
|
|
|
|
|
fn line_text_split(
|
|
|
|
|
source: &SourceTextInfo,
|
|
|
|
|
pos: DiagnosticSourcePos,
|
|
|
|
|
) -> (&str, &str) {
|
|
|
|
|
let pos = pos.pos(source);
|
|
|
|
|
let line_index = source.line_index(pos);
|
|
|
|
|
let line_start_pos = source.line_start(line_index);
|
|
|
|
|
let line_end_pos = source.line_end(line_index);
|
|
|
|
|
let before = source.range_text(&SourceRange::new(line_start_pos, pos));
|
|
|
|
|
let after = source.range_text(&SourceRange::new(pos, line_end_pos));
|
|
|
|
|
(before, after)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the text of the line that contains the given positions, split at the
|
|
|
|
|
/// given positions.
|
|
|
|
|
///
|
|
|
|
|
/// If the positions are on different lines, this will panic.
|
|
|
|
|
fn line_text_split3(
|
|
|
|
|
source: &SourceTextInfo,
|
|
|
|
|
start_pos: DiagnosticSourcePos,
|
|
|
|
|
end_pos: DiagnosticSourcePos,
|
|
|
|
|
) -> (&str, &str, &str) {
|
|
|
|
|
let start_pos = start_pos.pos(source);
|
|
|
|
|
let end_pos = end_pos.pos(source);
|
|
|
|
|
let line_index = source.line_index(start_pos);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
line_index,
|
|
|
|
|
source.line_index(end_pos),
|
|
|
|
|
"start and end must be on the same line"
|
|
|
|
|
);
|
|
|
|
|
let line_start_pos = source.line_start(line_index);
|
|
|
|
|
let line_end_pos = source.line_end(line_index);
|
|
|
|
|
let before = source.range_text(&SourceRange::new(line_start_pos, start_pos));
|
|
|
|
|
let between = source.range_text(&SourceRange::new(start_pos, end_pos));
|
|
|
|
|
let after = source.range_text(&SourceRange::new(end_pos, line_end_pos));
|
|
|
|
|
(before, between, after)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the line number (1 indexed) of the line that contains the given
|
|
|
|
|
/// position.
|
|
|
|
|
fn line_number(source: &SourceTextInfo, pos: DiagnosticSourcePos) -> usize {
|
|
|
|
|
source.line_index(pos.pos(source)) + 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait Diagnostic {
|
|
|
|
|
/// The level of the diagnostic.
|
|
|
|
|
fn level(&self) -> DiagnosticLevel;
|
|
|
|
|
|
|
|
|
|
/// The diagnostic code, like `no-explicit-any` or `ban-untagged-ignore`.
|
|
|
|
|
fn code(&self) -> impl fmt::Display + '_;
|
|
|
|
|
|
|
|
|
|
/// The human-readable diagnostic message.
|
|
|
|
|
fn message(&self) -> impl fmt::Display + '_;
|
|
|
|
|
|
|
|
|
|
/// The location this diagnostic is associated with.
|
|
|
|
|
fn location(&self) -> DiagnosticLocation;
|
|
|
|
|
|
|
|
|
|
/// A snippet showing the source code associated with the diagnostic.
|
|
|
|
|
fn snippet(&self) -> Option<DiagnosticSnippet<'_>>;
|
|
|
|
|
|
|
|
|
|
/// A hint for fixing the diagnostic.
|
|
|
|
|
fn hint(&self) -> Option<impl fmt::Display + '_>;
|
|
|
|
|
|
|
|
|
|
/// A snippet showing how the diagnostic can be fixed.
|
|
|
|
|
fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>>;
|
|
|
|
|
|
|
|
|
|
fn info(&self) -> Cow<'_, [Cow<'_, str>]>;
|
|
|
|
|
|
|
|
|
|
/// An optional URL to the documentation for the diagnostic.
|
|
|
|
|
fn docs_url(&self) -> Option<impl fmt::Display + '_>;
|
|
|
|
|
|
|
|
|
|
fn display<'a>(
|
|
|
|
|
&'a self,
|
|
|
|
|
sources: &'a dyn SourceTextStore,
|
|
|
|
|
) -> DiagnosticDisplay<'a, Self> {
|
|
|
|
|
DiagnosticDisplay {
|
|
|
|
|
diagnostic: self,
|
|
|
|
|
sources,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct RepeatingCharFmt(char, usize);
|
|
|
|
|
impl fmt::Display for RepeatingCharFmt {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
for _ in 0..self.1 {
|
|
|
|
|
f.write_char(self.0)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// How many spaces a tab should be displayed as. 2 is the default used for
|
|
|
|
|
/// `deno fmt`, so we'll use that here.
|
|
|
|
|
const TAB_WIDTH: usize = 2;
|
|
|
|
|
|
|
|
|
|
struct ReplaceTab<'a>(&'a str);
|
|
|
|
|
impl fmt::Display for ReplaceTab<'_> {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
let mut written = 0;
|
|
|
|
|
for (i, c) in self.0.char_indices() {
|
|
|
|
|
if c == '\t' {
|
|
|
|
|
self.0[written..i].fmt(f)?;
|
|
|
|
|
RepeatingCharFmt(' ', TAB_WIDTH).fmt(f)?;
|
|
|
|
|
written = i + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.0[written..].fmt(f)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The width of the string as displayed, assuming tabs are 2 spaces wide.
|
|
|
|
|
///
|
|
|
|
|
/// This display width assumes that zero-width-joined characters are the width
|
|
|
|
|
/// of their consituent characters. This means that "Person: Red Hair" (which is
|
|
|
|
|
/// represented as "Person" + "ZWJ" + "Red Hair") will have a width of 4.
|
|
|
|
|
///
|
|
|
|
|
/// Whether this is correct is unfortunately dependent on the font / terminal
|
|
|
|
|
/// being used. Here is a list of what terminals consider the length of
|
|
|
|
|
/// "Person: Red Hair" to be:
|
|
|
|
|
///
|
|
|
|
|
/// | Terminal | Rendered Width |
|
|
|
|
|
/// | ---------------- | -------------- |
|
|
|
|
|
/// | Windows Terminal | 5 chars |
|
|
|
|
|
/// | iTerm (macOS) | 2 chars |
|
|
|
|
|
/// | Terminal (macOS) | 2 chars |
|
|
|
|
|
/// | VS Code terminal | 4 chars |
|
|
|
|
|
/// | GNOME Terminal | 4 chars |
|
|
|
|
|
///
|
|
|
|
|
/// If we really wanted to, we could try and detect the terminal being used and
|
|
|
|
|
/// adjust the width accordingly. However, this is probably not worth the
|
|
|
|
|
/// effort.
|
|
|
|
|
fn display_width(str: &str) -> usize {
|
|
|
|
|
str.width_cjk() + (str.chars().filter(|c| *c == '\t').count() * TAB_WIDTH)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct DiagnosticDisplay<'a, T: Diagnostic + ?Sized> {
|
|
|
|
|
diagnostic: &'a T,
|
|
|
|
|
sources: &'a dyn SourceTextStore,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: Diagnostic + ?Sized> Display for DiagnosticDisplay<'_, T> {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
print_diagnostic(f, self.sources, self.diagnostic)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// error[missing-return-type]: missing explicit return type on public function
|
|
|
|
|
// at /mnt/artemis/Projects/github.com/denoland/deno/test.ts:1:16
|
|
|
|
|
// |
|
|
|
|
|
// 1 | export function test() {
|
|
|
|
|
// | ^^^^
|
|
|
|
|
// = hint: add an explicit return type to the function
|
|
|
|
|
// |
|
|
|
|
|
// 1 | export function test(): string {
|
|
|
|
|
// | ^^^^^^^^
|
|
|
|
|
//
|
|
|
|
|
// info: all functions that are exported from a module must have an explicit return type to support fast check and documentation generation.
|
|
|
|
|
// docs: https://jsr.io/d/missing-return-type
|
|
|
|
|
fn print_diagnostic(
|
|
|
|
|
io: &mut dyn std::fmt::Write,
|
|
|
|
|
sources: &dyn SourceTextStore,
|
|
|
|
|
diagnostic: &(impl Diagnostic + ?Sized),
|
|
|
|
|
) -> Result<(), std::fmt::Error> {
|
|
|
|
|
match diagnostic.level() {
|
|
|
|
|
DiagnosticLevel::Error => {
|
|
|
|
|
write!(
|
|
|
|
|
io,
|
|
|
|
|
"{}",
|
|
|
|
|
colors::red_bold(format_args!("error[{}]", diagnostic.code()))
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
DiagnosticLevel::Warning => {
|
|
|
|
|
write!(
|
|
|
|
|
io,
|
|
|
|
|
"{}",
|
2024-01-24 16:24:52 -05:00
|
|
|
|
colors::yellow_bold(format_args!("warning[{}]", diagnostic.code()))
|
2024-01-23 10:37:43 -05:00
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeln!(io, ": {}", colors::bold(diagnostic.message()))?;
|
|
|
|
|
|
|
|
|
|
let mut max_line_number_digits = 1;
|
|
|
|
|
if let Some(snippet) = diagnostic.snippet() {
|
|
|
|
|
let source = snippet.source.to_source_text_info(sources);
|
|
|
|
|
let last_line = line_number(&source, snippet.highlight.range.end);
|
|
|
|
|
max_line_number_digits = max_line_number_digits.max(last_line.ilog10() + 1);
|
|
|
|
|
}
|
|
|
|
|
if let Some(snippet) = diagnostic.snippet_fixed() {
|
|
|
|
|
let source = snippet.source.to_source_text_info(sources);
|
|
|
|
|
let last_line = line_number(&source, snippet.highlight.range.end);
|
|
|
|
|
max_line_number_digits = max_line_number_digits.max(last_line.ilog10() + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let location = diagnostic.location();
|
|
|
|
|
write!(
|
|
|
|
|
io,
|
|
|
|
|
"{}{}",
|
|
|
|
|
RepeatingCharFmt(' ', max_line_number_digits as usize),
|
|
|
|
|
colors::intense_blue("-->"),
|
|
|
|
|
)?;
|
2024-01-24 16:24:52 -05:00
|
|
|
|
match &location {
|
|
|
|
|
DiagnosticLocation::Path { path } => {
|
|
|
|
|
write!(io, " {}", colors::cyan(path.display()))?;
|
|
|
|
|
}
|
|
|
|
|
DiagnosticLocation::Module { specifier }
|
|
|
|
|
| DiagnosticLocation::ModulePosition { specifier, .. } => {
|
|
|
|
|
if let Ok(path) = specifier.to_file_path() {
|
|
|
|
|
write!(io, " {}", colors::cyan(path.display()))?;
|
|
|
|
|
} else {
|
|
|
|
|
write!(io, " {}", colors::cyan(specifier.as_str()))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-23 10:37:43 -05:00
|
|
|
|
}
|
|
|
|
|
if let Some((line, column)) = location.position(sources) {
|
|
|
|
|
write!(
|
|
|
|
|
io,
|
|
|
|
|
"{}",
|
|
|
|
|
colors::yellow(format_args!(":{}:{}", line, column))
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
writeln!(io)?;
|
|
|
|
|
|
|
|
|
|
if let Some(snippet) = diagnostic.snippet() {
|
|
|
|
|
print_snippet(io, sources, &snippet, max_line_number_digits)?;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(hint) = diagnostic.hint() {
|
|
|
|
|
write!(
|
|
|
|
|
io,
|
|
|
|
|
"{} {} ",
|
|
|
|
|
RepeatingCharFmt(' ', max_line_number_digits as usize),
|
|
|
|
|
colors::intense_blue("=")
|
|
|
|
|
)?;
|
|
|
|
|
writeln!(io, "{}: {}", colors::bold("hint"), hint)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(snippet) = diagnostic.snippet_fixed() {
|
|
|
|
|
print_snippet(io, sources, &snippet, max_line_number_digits)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeln!(io)?;
|
|
|
|
|
|
|
|
|
|
let mut needs_final_newline = false;
|
|
|
|
|
for info in diagnostic.info().iter() {
|
|
|
|
|
needs_final_newline = true;
|
|
|
|
|
writeln!(io, " {}: {}", colors::intense_blue("info"), info)?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(docs_url) = diagnostic.docs_url() {
|
|
|
|
|
needs_final_newline = true;
|
|
|
|
|
writeln!(io, " {}: {}", colors::intense_blue("docs"), docs_url)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if needs_final_newline {
|
|
|
|
|
writeln!(io)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Prints a snippet to the given writer and returns the line number indent.
|
|
|
|
|
fn print_snippet(
|
|
|
|
|
io: &mut dyn std::fmt::Write,
|
|
|
|
|
sources: &dyn SourceTextStore,
|
|
|
|
|
snippet: &DiagnosticSnippet<'_>,
|
|
|
|
|
max_line_number_digits: u32,
|
|
|
|
|
) -> Result<(), std::fmt::Error> {
|
|
|
|
|
let DiagnosticSnippet { source, highlight } = snippet;
|
|
|
|
|
|
|
|
|
|
fn print_padded(
|
|
|
|
|
io: &mut dyn std::fmt::Write,
|
|
|
|
|
text: impl std::fmt::Display,
|
|
|
|
|
padding: u32,
|
|
|
|
|
) -> Result<(), std::fmt::Error> {
|
|
|
|
|
for _ in 0..padding {
|
|
|
|
|
write!(io, " ")?;
|
|
|
|
|
}
|
|
|
|
|
write!(io, "{}", text)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let source = source.to_source_text_info(sources);
|
|
|
|
|
|
|
|
|
|
let start_line_number = line_number(&source, highlight.range.start);
|
|
|
|
|
let end_line_number = line_number(&source, highlight.range.end);
|
|
|
|
|
|
|
|
|
|
print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
|
|
|
|
|
writeln!(io)?;
|
|
|
|
|
for line_number in start_line_number..=end_line_number {
|
|
|
|
|
print_padded(
|
|
|
|
|
io,
|
|
|
|
|
colors::intense_blue(format_args!("{} | ", line_number)),
|
|
|
|
|
max_line_number_digits - line_number.ilog10() - 1,
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
let padding_width;
|
|
|
|
|
let highlight_width;
|
|
|
|
|
if line_number == start_line_number && start_line_number == end_line_number
|
|
|
|
|
{
|
|
|
|
|
let (before, between, after) =
|
|
|
|
|
line_text_split3(&source, highlight.range.start, highlight.range.end);
|
|
|
|
|
write!(io, "{}", ReplaceTab(before))?;
|
|
|
|
|
match highlight.style {
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Addition => {
|
|
|
|
|
write!(io, "{}", colors::green(ReplaceTab(between)))?;
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
write!(io, "{}", ReplaceTab(between))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
writeln!(io, "{}", ReplaceTab(after))?;
|
|
|
|
|
padding_width = display_width(before);
|
|
|
|
|
highlight_width = display_width(between);
|
|
|
|
|
} else if line_number == start_line_number {
|
|
|
|
|
let (before, after) = line_text_split(&source, highlight.range.start);
|
|
|
|
|
write!(io, "{}", ReplaceTab(before))?;
|
|
|
|
|
match highlight.style {
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Addition => {
|
|
|
|
|
write!(io, "{}", colors::green(ReplaceTab(after)))?;
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
write!(io, "{}", ReplaceTab(after))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
writeln!(io)?;
|
|
|
|
|
padding_width = display_width(before);
|
|
|
|
|
highlight_width = display_width(after);
|
|
|
|
|
} else if line_number == end_line_number {
|
|
|
|
|
let (before, after) = line_text_split(&source, highlight.range.end);
|
|
|
|
|
match highlight.style {
|
|
|
|
|
DiagnosticSnippetHighlightStyle::Addition => {
|
|
|
|
|
write!(io, "{}", colors::green(ReplaceTab(before)))?;
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
write!(io, "{}", ReplaceTab(before))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
write!(io, "{}", ReplaceTab(after))?;
|
|
|
|
|
writeln!(io)?;
|
|
|
|
|
padding_width = 0;
|
|
|
|
|
highlight_width = display_width(before);
|
|
|
|
|
} else {
|
|
|
|
|
let line = line_text(&source, line_number);
|
|
|
|
|
writeln!(io, "{}", ReplaceTab(line))?;
|
|
|
|
|
padding_width = 0;
|
|
|
|
|
highlight_width = display_width(line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
|
|
|
|
|
write!(io, "{}", RepeatingCharFmt(' ', padding_width))?;
|
|
|
|
|
let underline =
|
|
|
|
|
RepeatingCharFmt(highlight.style.underline_char(), highlight_width);
|
|
|
|
|
write!(io, "{}", highlight.style.style_underline(underline))?;
|
|
|
|
|
|
|
|
|
|
if line_number == end_line_number {
|
|
|
|
|
if let Some(description) = &highlight.description {
|
|
|
|
|
write!(io, " {}", highlight.style.style_underline(description))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeln!(io)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::borrow::Cow;
|
|
|
|
|
|
|
|
|
|
use deno_ast::ModuleSpecifier;
|
|
|
|
|
use deno_ast::SourceTextInfo;
|
|
|
|
|
|
|
|
|
|
use super::SourceTextStore;
|
|
|
|
|
|
|
|
|
|
struct TestSource {
|
|
|
|
|
specifier: ModuleSpecifier,
|
|
|
|
|
text_info: SourceTextInfo,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SourceTextStore for TestSource {
|
|
|
|
|
fn get_source_text<'a>(
|
|
|
|
|
&'a self,
|
|
|
|
|
specifier: &ModuleSpecifier,
|
|
|
|
|
) -> Option<Cow<'a, SourceTextInfo>> {
|
|
|
|
|
if specifier == &self.specifier {
|
|
|
|
|
Some(Cow::Borrowed(&self.text_info))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_display_width() {
|
|
|
|
|
assert_eq!(super::display_width("abc"), 3);
|
|
|
|
|
assert_eq!(super::display_width("\t"), 2);
|
|
|
|
|
assert_eq!(super::display_width("\t\t123"), 7);
|
|
|
|
|
assert_eq!(super::display_width("🎄"), 2);
|
|
|
|
|
assert_eq!(super::display_width("🎄🎄"), 4);
|
|
|
|
|
assert_eq!(super::display_width("🧑🦰"), 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_position_in_file_from_text_info_simple() {
|
|
|
|
|
let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap();
|
|
|
|
|
let text_info = SourceTextInfo::new("foo\nbar\nbaz".into());
|
|
|
|
|
let pos = text_info.line_start(1);
|
|
|
|
|
let sources = TestSource {
|
|
|
|
|
specifier: specifier.clone(),
|
|
|
|
|
text_info,
|
|
|
|
|
};
|
2024-01-24 16:24:52 -05:00
|
|
|
|
let location = super::DiagnosticLocation::ModulePosition {
|
2024-01-23 10:37:43 -05:00
|
|
|
|
specifier: Cow::Borrowed(&specifier),
|
|
|
|
|
source_pos: super::DiagnosticSourcePos::SourcePos(pos),
|
|
|
|
|
};
|
|
|
|
|
let position = location.position(&sources).unwrap();
|
|
|
|
|
assert_eq!(position, (2, 1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_position_in_file_from_text_info_emoji() {
|
|
|
|
|
let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap();
|
|
|
|
|
let text_info = SourceTextInfo::new("🧑🦰text".into());
|
|
|
|
|
let pos = text_info.line_start(0) + 11; // the end of the emoji
|
|
|
|
|
let sources = TestSource {
|
|
|
|
|
specifier: specifier.clone(),
|
|
|
|
|
text_info,
|
|
|
|
|
};
|
2024-01-24 16:24:52 -05:00
|
|
|
|
let location = super::DiagnosticLocation::ModulePosition {
|
2024-01-23 10:37:43 -05:00
|
|
|
|
specifier: Cow::Borrowed(&specifier),
|
|
|
|
|
source_pos: super::DiagnosticSourcePos::SourcePos(pos),
|
|
|
|
|
};
|
|
|
|
|
let position = location.position(&sources).unwrap();
|
|
|
|
|
assert_eq!(position, (1, 6))
|
|
|
|
|
}
|
|
|
|
|
}
|