use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::fmt::Display;
use std::fmt::Write as _;
use std::path::PathBuf;
use deno_error::JsError;
use deno_terminal::colors;
use unicode_width::UnicodeWidthStr;
use crate::ModuleSpecifier;
use crate::SourcePos;
use crate::SourceRange;
use crate::SourceRanged;
use crate::SourceTextInfo;
use crate::swc::common::errors::Diagnostic as SwcDiagnostic;
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),
LineAndCol {
line: usize,
column: usize,
},
}
impl DiagnosticSourcePos {
fn pos(&self, source: &SourceTextInfo) -> SourcePos {
match self {
DiagnosticSourcePos::SourcePos(pos) => *pos,
DiagnosticSourcePos::ByteIndex(index) => source.range().start() + *index,
DiagnosticSourcePos::LineAndCol { line, column } => {
source.line_start(*line) + *column
}
}
}
}
#[derive(Clone, Debug)]
pub enum DiagnosticLocation<'a> {
Path { path: PathBuf },
Module {
specifier: Cow<'a, ModuleSpecifier>,
},
ModulePosition {
specifier: Cow<'a, ModuleSpecifier>,
source_pos: DiagnosticSourcePos,
text_info: Cow<'a, SourceTextInfo>,
},
}
impl DiagnosticLocation<'_> {
fn position(&self) -> Option<(usize, usize)> {
match self {
DiagnosticLocation::Path { .. } => None,
DiagnosticLocation::Module { .. } => None,
DiagnosticLocation::ModulePosition {
specifier: _specifier,
source_pos,
text_info,
} => {
let pos = source_pos.pos(text_info);
let line_index = text_info.line_index(pos);
let line_start_pos = text_info.line_start(line_index);
let content =
text_info.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> {
pub source: Cow<'a, crate::SourceTextInfo>,
pub highlights: Vec<DiagnosticSnippetHighlight<'a>>,
}
#[derive(Clone)]
pub struct DiagnosticSnippetHighlight<'a> {
pub range: DiagnosticSourceRange,
pub style: DiagnosticSnippetHighlightStyle,
pub description: Option<Cow<'a, str>>,
}
#[derive(Clone, Copy)]
pub enum DiagnosticSnippetHighlightStyle {
Error,
#[allow(dead_code)]
Warning,
#[allow(dead_code)]
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::Hint => colors::intense_blue(s),
}
}
fn underline_char(&self) -> char {
match self {
DiagnosticSnippetHighlightStyle::Error => '^',
DiagnosticSnippetHighlightStyle::Warning => '^',
DiagnosticSnippetHighlightStyle::Hint => '-',
}
}
}
fn line_text(source: &SourceTextInfo, line_number: usize) -> &str {
source.line_text(line_number - 1)
}
fn line_number(source: &SourceTextInfo, pos: DiagnosticSourcePos) -> usize {
source.line_index(pos.pos(source)) + 1
}
pub trait Diagnostic {
fn level(&self) -> DiagnosticLevel;
fn code(&self) -> Cow<'_, str>;
fn message(&self) -> Cow<'_, str>;
fn location(&self) -> DiagnosticLocation<'_>;
fn snippet(&self) -> Option<DiagnosticSnippet<'_>>;
fn hint(&self) -> Option<Cow<'_, str>>;
fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>>;
fn info(&self) -> Cow<'_, [Cow<'_, str>]>;
fn docs_url(&self) -> Option<Cow<'_, str>>;
fn display(&self) -> DiagnosticDisplay<'_, Self> {
DiagnosticDisplay { diagnostic: self }
}
}
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(())
}
}
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(())
}
}
fn display_width(str: &str) -> usize {
let num_tabs = str.chars().filter(|c| *c == '\t').count();
str.width_cjk() + num_tabs * TAB_WIDTH - num_tabs
}
pub struct DiagnosticDisplay<'a, T: Diagnostic + ?Sized> {
diagnostic: &'a T,
}
impl<T: Diagnostic + ?Sized> Display for DiagnosticDisplay<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
print_diagnostic(f, self.diagnostic)
}
}
fn print_diagnostic(
io: &mut dyn std::fmt::Write,
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,
"{}",
colors::yellow_bold(format_args!("warning[{}]", diagnostic.code()))
)?;
}
}
writeln!(io, ": {}", colors::bold(diagnostic.message()))?;
let mut max_line_number_digits = 1;
let mut has_multiline_highlight = false;
let mut note_highlight =
|source: &SourceTextInfo, highlight: &DiagnosticSnippetHighlight| {
let first_line = line_number(source, highlight.range.start);
let last_line = line_number(source, highlight.range.end);
max_line_number_digits =
max_line_number_digits.max(last_line.ilog10() + 1);
if first_line != last_line {
has_multiline_highlight = true;
}
};
if let Some(snippet) = diagnostic.snippet() {
for highlight in snippet.highlights.iter() {
note_highlight(&snippet.source, highlight);
}
}
if let Some(snippet) = diagnostic.snippet_fixed() {
for highlight in snippet.highlights.iter() {
note_highlight(&snippet.source, highlight);
}
}
let marker_width: usize = if has_multiline_highlight { 2 } else { 0 };
let location = diagnostic.location();
write!(
io,
"{}{}",
RepeatingCharFmt(' ', max_line_number_digits as usize + marker_width),
colors::intense_blue("-->"),
)?;
match &location {
DiagnosticLocation::Path { path } => {
write!(io, " {}", colors::cyan(path.display()))?;
}
DiagnosticLocation::Module { specifier }
| DiagnosticLocation::ModulePosition { specifier, .. } => {
if let Some(path) = specifier_to_file_path(specifier) {
write!(io, " {}", colors::cyan(path.display()))?;
} else {
write!(io, " {}", colors::cyan(specifier.as_str()))?;
}
}
}
if let Some((line, column)) = location.position() {
write!(
io,
"{}",
colors::yellow(format_args!(":{}:{}", line, column))
)?;
}
if diagnostic.snippet().is_some()
|| diagnostic.hint().is_some()
|| diagnostic.snippet_fixed().is_some()
|| !diagnostic.info().is_empty()
|| diagnostic.docs_url().is_some()
{
writeln!(io)?;
}
if let Some(snippet) = diagnostic.snippet() {
print_snippet(io, &snippet, max_line_number_digits, marker_width)?;
};
if let Some(hint) = diagnostic.hint() {
write!(
io,
"{} {} ",
RepeatingCharFmt(' ', max_line_number_digits as usize + marker_width),
colors::intense_blue("=")
)?;
writeln!(io, "{}: {}", colors::bold("hint"), hint)?;
}
if let Some(snippet) = diagnostic.snippet_fixed() {
print_snippet(io, &snippet, max_line_number_digits, marker_width)?;
}
if !diagnostic.info().is_empty() || diagnostic.docs_url().is_some() {
writeln!(io)?;
}
for info in diagnostic.info().iter() {
writeln!(io, " {}: {}", colors::intense_blue("info"), info)?;
}
if let Some(docs_url) = diagnostic.docs_url() {
writeln!(io, " {}: {}", colors::intense_blue("docs"), docs_url)?;
}
Ok(())
}
const HIGHLIGHT_HEAD_LINES: usize = 2;
const HIGHLIGHT_TAIL_LINES: usize = 2;
const MAX_HIGHLIGHT_LINES: usize =
HIGHLIGHT_HEAD_LINES + HIGHLIGHT_TAIL_LINES + 1;
fn print_snippet(
io: &mut dyn std::fmt::Write,
snippet: &DiagnosticSnippet<'_>,
max_line_number_digits: u32,
marker_width: usize,
) -> Result<(), std::fmt::Error> {
let DiagnosticSnippet { source, highlights } = snippet;
let bar_indent = max_line_number_digits + marker_width as u32;
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 mut lines_to_show = HashMap::<usize, Vec<usize>>::new();
let mut elided_after = HashSet::<usize>::new();
let mut highlights_info = Vec::new();
for (i, highlight) in highlights.iter().enumerate() {
let start_line_number = line_number(source, highlight.range.start);
let end_line_number = line_number(source, highlight.range.end);
highlights_info.push((start_line_number, end_line_number));
let line_count = end_line_number - start_line_number + 1;
if line_count <= MAX_HIGHLIGHT_LINES {
for line_number in start_line_number..=end_line_number {
lines_to_show.entry(line_number).or_default().push(i);
}
} else {
let head_end = start_line_number + HIGHLIGHT_HEAD_LINES - 1;
let tail_start = end_line_number - (HIGHLIGHT_TAIL_LINES - 1);
for line_number in start_line_number..=head_end {
lines_to_show.entry(line_number).or_default().push(i);
}
elided_after.insert(head_end);
for line_number in tail_start..=end_line_number {
lines_to_show.entry(line_number).or_default().push(i);
}
}
}
let mut lines_to_show = lines_to_show.into_iter().collect::<Vec<_>>();
lines_to_show.sort();
print_padded(io, colors::intense_blue(" | "), bar_indent)?;
writeln!(io)?;
let mut previous_line_number: Option<usize> = None;
for (line_number, highlight_indexes) in lines_to_show {
if previous_line_number
.is_some_and(|previous| elided_after.contains(&previous))
{
print_padded(io, colors::intense_blue("..."), bar_indent)?;
writeln!(io)?;
}
if marker_width > 0 {
let is_span_line = highlight_indexes.iter().any(|&i| {
let (start_line_number, end_line_number) = highlights_info[i];
start_line_number != end_line_number
});
if is_span_line {
write!(io, "{}", colors::intense_blue(">"))?;
print_padded(io, "", (marker_width - 1) as u32)?;
} else {
print_padded(io, "", marker_width as u32)?;
}
}
print_padded(
io,
colors::intense_blue(format_args!("{} | ", line_number)),
max_line_number_digits - line_number.ilog10() - 1,
)?;
let line_start_pos = source.line_start(line_number - 1);
let line_end_pos = source.line_end(line_number - 1);
let line_text = line_text(source, line_number);
writeln!(io, "{}", ReplaceTab(line_text))?;
let mut wrote_description = false;
for highlight_index in highlight_indexes {
let highlight = &highlights[highlight_index];
let (start_line_number, end_line_number) =
highlights_info[highlight_index];
let is_first_line = start_line_number == line_number;
let is_last_line = end_line_number == line_number;
if !is_first_line && !is_last_line {
continue;
}
let padding_width;
let highlight_width;
if start_line_number == end_line_number {
padding_width = display_width(source.range_text(&SourceRange::new(
line_start_pos,
highlight.range.start.pos(source),
)));
highlight_width = display_width(source.range_text(&SourceRange::new(
highlight.range.start.pos(source),
highlight.range.end.pos(source),
)));
} else if is_first_line {
padding_width = display_width(source.range_text(&SourceRange::new(
line_start_pos,
highlight.range.start.pos(source),
)));
highlight_width = display_width(source.range_text(&SourceRange::new(
highlight.range.start.pos(source),
line_end_pos,
)));
} else {
let line_prefix = source.range_text(&SourceRange::new(
line_start_pos,
highlight.range.end.pos(source),
));
let trimmed = line_prefix.trim_start();
let indent = &line_prefix[..line_prefix.len() - trimmed.len()];
padding_width = display_width(indent);
highlight_width = display_width(trimmed);
}
let underline =
RepeatingCharFmt(highlight.style.underline_char(), highlight_width);
print_padded(io, colors::intense_blue(" | "), bar_indent)?;
write!(io, "{}", RepeatingCharFmt(' ', padding_width))?;
write!(io, "{}", highlight.style.style_underline(underline))?;
if is_last_line && let Some(description) = &highlight.description {
write!(io, " {}", highlight.style.style_underline(description))?;
wrote_description = true;
}
writeln!(io)?;
}
if wrote_description {
print_padded(io, colors::intense_blue(" | "), bar_indent)?;
writeln!(io)?;
}
previous_line_number = Some(line_number);
}
Ok(())
}
fn specifier_to_file_path(specifier: &ModuleSpecifier) -> Option<PathBuf> {
fn to_file_path_if_not_wasm(_specifier: &ModuleSpecifier) -> Option<PathBuf> {
#[cfg(target_arch = "wasm32")]
{
None
}
#[cfg(not(target_arch = "wasm32"))]
{
_specifier.to_file_path().ok()
}
}
if specifier.scheme() != "file" {
None
} else if cfg!(windows) {
match to_file_path_if_not_wasm(specifier) {
Some(path) => Some(path),
None => {
if specifier.scheme() == "file"
&& specifier.host().is_none()
&& specifier.port().is_none()
&& specifier.path_segments().is_some()
{
let path_str = specifier.path();
match String::from_utf8(
percent_encoding::percent_decode(path_str.as_bytes()).collect(),
) {
Ok(path_str) => Some(PathBuf::from(path_str)),
Err(_) => None,
}
} else {
None
}
}
}
} else {
to_file_path_if_not_wasm(specifier)
}
}
#[cfg(any(feature = "transpiling", feature = "type_strip"))]
pub(crate) type DiagnosticsCell = crate::swc::common::sync::Lrc<
crate::swc::common::sync::Lock<Vec<SwcDiagnostic>>,
>;
#[cfg(any(feature = "transpiling", feature = "type_strip"))]
#[derive(Default, Clone)]
pub(crate) struct DiagnosticCollector {
diagnostics: DiagnosticsCell,
}
#[cfg(any(feature = "transpiling", feature = "type_strip"))]
impl DiagnosticCollector {
pub fn into_handler_and_cell(
self,
) -> (crate::swc::common::errors::Handler, DiagnosticsCell) {
let cell = self.diagnostics.clone();
(
crate::swc::common::errors::Handler::with_emitter(
true,
false,
Box::new(self),
),
cell,
)
}
}
#[cfg(any(feature = "transpiling", feature = "type_strip"))]
impl crate::swc::common::errors::Emitter for DiagnosticCollector {
fn emit(
&mut self,
db: &mut crate::swc::common::errors::DiagnosticBuilder<'_>,
) {
let mut diagnostics = self.diagnostics.lock();
diagnostics.push(db.take());
}
}
#[derive(Debug, JsError)]
#[class(syntax)]
pub struct SwcFoldDiagnosticsError(Vec<String>);
impl std::error::Error for SwcFoldDiagnosticsError {}
impl std::fmt::Display for SwcFoldDiagnosticsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, diagnostic) in self.0.iter().enumerate() {
if i > 0 {
write!(f, "\n\n")?;
}
write!(f, "{}", diagnostic)?
}
Ok(())
}
}
pub fn ensure_no_fatal_swc_diagnostics(
source_map: &swc_common::SourceMap,
diagnostics: impl Iterator<Item = SwcDiagnostic>,
) -> Result<(), SwcFoldDiagnosticsError> {
let fatal_diagnostics = diagnostics
.filter(is_fatal_swc_diagnostic)
.collect::<Vec<_>>();
if !fatal_diagnostics.is_empty() {
Err(SwcFoldDiagnosticsError(
fatal_diagnostics
.iter()
.map(|d| format_swc_diagnostic(source_map, d))
.collect::<Vec<_>>(),
))
} else {
Ok(())
}
}
fn is_fatal_swc_diagnostic(diagnostic: &SwcDiagnostic) -> bool {
use crate::swc::common::errors::Level;
match diagnostic.level {
Level::Bug
| Level::Cancelled
| Level::FailureNote
| Level::Fatal
| Level::PhaseFatal
| Level::Error => true,
Level::Help | Level::Note | Level::Warning => false,
}
}
fn format_swc_diagnostic(
source_map: &swc_common::SourceMap,
diagnostic: &SwcDiagnostic,
) -> String {
if let Some(span) = &diagnostic.span.primary_span() {
let file_name = source_map.span_to_filename(*span);
let loc = source_map.lookup_char_pos(span.lo);
format!(
"{} at {}:{}:{}",
diagnostic.message(),
file_name,
loc.line,
loc.col_display + 1,
)
} else {
diagnostic.message()
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::ModuleSpecifier;
use crate::SourceTextInfo;
struct TestDiagnostic {
text_info: SourceTextInfo,
range: DiagnosticSourceRange,
description: Option<&'static str>,
}
impl Diagnostic for TestDiagnostic {
fn level(&self) -> DiagnosticLevel {
DiagnosticLevel::Error
}
fn code(&self) -> Cow<'_, str> {
Cow::Borrowed("test-rule")
}
fn message(&self) -> Cow<'_, str> {
Cow::Borrowed("a test diagnostic")
}
fn location(&self) -> DiagnosticLocation<'_> {
DiagnosticLocation::ModulePosition {
specifier: Cow::Owned("file:///test.ts".parse().unwrap()),
source_pos: DiagnosticSourcePos::SourcePos(
self.range.start.pos(&self.text_info),
),
text_info: Cow::Borrowed(&self.text_info),
}
}
fn snippet(&self) -> Option<DiagnosticSnippet<'_>> {
Some(DiagnosticSnippet {
source: Cow::Borrowed(&self.text_info),
highlights: vec![DiagnosticSnippetHighlight {
range: self.range,
style: DiagnosticSnippetHighlightStyle::Error,
description: self.description.map(Cow::Borrowed),
}],
})
}
fn hint(&self) -> Option<Cow<'_, str>> {
None
}
fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>> {
None
}
fn info(&self) -> Cow<'_, [Cow<'_, str>]> {
Cow::Borrowed(&[])
}
fn docs_url(&self) -> Option<Cow<'_, str>> {
None
}
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
for c in chars.by_ref() {
if c == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
fn render(text: &str, range: DiagnosticSourceRange) -> String {
render_with_description(text, range, None)
}
fn render_with_description(
text: &str,
range: DiagnosticSourceRange,
description: Option<&'static str>,
) -> String {
let diagnostic = TestDiagnostic {
text_info: SourceTextInfo::new(text.into()),
range,
description,
};
strip_ansi(&diagnostic.display().to_string())
}
fn line_col(line: usize, column: usize) -> DiagnosticSourcePos {
DiagnosticSourcePos::LineAndCol { line, column }
}
#[test]
fn test_snippet_single_line() {
let output = render(
"const value = 1;\n",
DiagnosticSourceRange {
start: line_col(0, 6),
end: line_col(0, 11),
},
);
assert_eq!(
output,
"error[test-rule]: a test diagnostic\n \
--> /test.ts:1:7\n \
| \n\
1 | const value = 1;\n \
| ^^^^^\n"
);
}
#[test]
fn test_snippet_multi_line_elided() {
let text = "const p = new Promise(async (resolve) => {\n \
const a = 1;\n \
const b = 2;\n \
const c = 3;\n \
const d = 4;\n \
const e = 5;\n \
resolve(a);\n\
});\n";
let output = render(
text,
DiagnosticSourceRange {
start: line_col(0, 22),
end: line_col(7, 2),
},
);
assert_eq!(
output,
"error[test-rule]: a test diagnostic\n --> /test.ts:1:23\n | \n> 1 | const p = new Promise(async (resolve) => {\n | ^^^^^^^^^^^^^^^^^^^^\n> 2 | const a = 1;\n ...\n> 7 | resolve(a);\n> 8 | });\n | ^^\n"
);
}
#[test]
fn test_snippet_multi_line_short_not_elided() {
let text = "foo(\n 1,\n 2,\n)\n";
let output = render(
text,
DiagnosticSourceRange {
start: line_col(0, 0),
end: line_col(3, 1),
},
);
assert_eq!(
output,
"error[test-rule]: a test diagnostic\n --> /test.ts:1:1\n | \n> 1 | foo(\n | ^^^^\n> 2 | 1,\n> 3 | 2,\n> 4 | )\n | ^\n"
);
}
#[test]
fn test_snippet_multi_line_description_on_last_line() {
let text = "foo(\n 1,\n)\n";
let output = render_with_description(
text,
DiagnosticSourceRange {
start: line_col(0, 0),
end: line_col(2, 1),
},
Some("this call"),
);
assert_eq!(
output,
"error[test-rule]: a test diagnostic\n --> /test.ts:1:1\n | \n> 1 | foo(\n | ^^^^\n> 2 | 1,\n> 3 | )\n | ^ this call\n | \n"
);
}
#[test]
fn test_display_width() {
assert_eq!(display_width("abc"), 3);
assert_eq!(display_width("\t"), 2);
assert_eq!(display_width("\t\t123"), 7);
assert_eq!(display_width("π"), 2);
assert_eq!(display_width("ππ"), 4);
assert_eq!(display_width("π§βπ¦°"), 2);
}
#[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 location = DiagnosticLocation::ModulePosition {
specifier: Cow::Borrowed(&specifier),
source_pos: DiagnosticSourcePos::SourcePos(pos),
text_info: Cow::Owned(text_info),
};
let position = location.position().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; let location = DiagnosticLocation::ModulePosition {
specifier: Cow::Borrowed(&specifier),
source_pos: DiagnosticSourcePos::SourcePos(pos),
text_info: Cow::Owned(text_info),
};
let position = location.position().unwrap();
assert_eq!(position, (1, 6))
}
#[test]
fn test_specifier_to_file_path() {
run_success_test("file:///", "/");
run_success_test("file:///test", "/test");
run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt");
run_success_test(
"file:///dir/test%20test/test.txt",
"/dir/test test/test.txt",
);
fn run_success_test(specifier: &str, expected_path: &str) {
let result =
specifier_to_file_path(&ModuleSpecifier::parse(specifier).unwrap())
.unwrap();
assert_eq!(result, PathBuf::from(expected_path));
}
}
}