Skip to main content

deno_ast/
diagnostics.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt;
6use std::fmt::Display;
7use std::fmt::Write as _;
8use std::path::PathBuf;
9
10use deno_error::JsError;
11use deno_terminal::colors;
12use unicode_width::UnicodeWidthStr;
13
14use crate::ModuleSpecifier;
15use crate::SourcePos;
16use crate::SourceRange;
17use crate::SourceRanged;
18use crate::SourceTextInfo;
19
20use crate::swc::common::errors::Diagnostic as SwcDiagnostic;
21
22pub enum DiagnosticLevel {
23  Error,
24  Warning,
25}
26
27#[derive(Clone, Copy, Debug)]
28pub struct DiagnosticSourceRange {
29  pub start: DiagnosticSourcePos,
30  pub end: DiagnosticSourcePos,
31}
32
33#[derive(Clone, Copy, Debug)]
34pub enum DiagnosticSourcePos {
35  SourcePos(SourcePos),
36  ByteIndex(usize),
37  LineAndCol {
38    // 0-indexed line number in bytes
39    line: usize,
40    // 0-indexed column number in bytes
41    column: usize,
42  },
43}
44
45impl DiagnosticSourcePos {
46  fn pos(&self, source: &SourceTextInfo) -> SourcePos {
47    match self {
48      DiagnosticSourcePos::SourcePos(pos) => *pos,
49      DiagnosticSourcePos::ByteIndex(index) => source.range().start() + *index,
50      DiagnosticSourcePos::LineAndCol { line, column } => {
51        source.line_start(*line) + *column
52      }
53    }
54  }
55}
56
57#[derive(Clone, Debug)]
58pub enum DiagnosticLocation<'a> {
59  /// The diagnostic is relevant to a specific path.
60  Path { path: PathBuf },
61  /// The diagnostic is relevant to an entire module.
62  Module {
63    /// The specifier of the module that contains the diagnostic.
64    specifier: Cow<'a, ModuleSpecifier>,
65  },
66  /// The diagnostic is relevant to a specific position in a module.
67  ///
68  /// This variant will get the relevant `SouceTextInfo` from the cache using
69  /// the given specifier, and will then calculate the line and column numbers
70  /// from the given `SourcePos`.
71  ModulePosition {
72    /// The specifier of the module that contains the diagnostic.
73    specifier: Cow<'a, ModuleSpecifier>,
74    /// The source position of the diagnostic.
75    source_pos: DiagnosticSourcePos,
76    text_info: Cow<'a, SourceTextInfo>,
77  },
78}
79
80impl DiagnosticLocation<'_> {
81  /// Return the line and column number of the diagnostic.
82  ///
83  /// The line number is 1-indexed.
84  ///
85  /// The column number is 1-indexed. This is the number of UTF-16 code units
86  /// from the start of the line to the diagnostic.
87  /// Why UTF-16 code units? Because that's what VS Code understands, and
88  /// everyone uses VS Code. :)
89  fn position(&self) -> Option<(usize, usize)> {
90    match self {
91      DiagnosticLocation::Path { .. } => None,
92      DiagnosticLocation::Module { .. } => None,
93      DiagnosticLocation::ModulePosition {
94        specifier: _specifier,
95        source_pos,
96        text_info,
97      } => {
98        let pos = source_pos.pos(text_info);
99        let line_index = text_info.line_index(pos);
100        let line_start_pos = text_info.line_start(line_index);
101        // todo(dsherret): fix in text_lines
102        let content =
103          text_info.range_text(&SourceRange::new(line_start_pos, pos));
104        let line = line_index + 1;
105        let column = content.encode_utf16().count() + 1;
106        Some((line, column))
107      }
108    }
109  }
110}
111
112pub struct DiagnosticSnippet<'a> {
113  /// The source text for this snippet. The
114  pub source: Cow<'a, crate::SourceTextInfo>,
115  /// The piece of the snippet that should be highlighted. For best results, the
116  /// highlights should not overlap and be ordered by their start position.
117  pub highlights: Vec<DiagnosticSnippetHighlight<'a>>,
118}
119
120#[derive(Clone)]
121pub struct DiagnosticSnippetHighlight<'a> {
122  /// The range of the snippet that should be highlighted.
123  pub range: DiagnosticSourceRange,
124  /// The style of the highlight.
125  pub style: DiagnosticSnippetHighlightStyle,
126  /// An optional inline description of the highlight.
127  pub description: Option<Cow<'a, str>>,
128}
129
130#[derive(Clone, Copy)]
131pub enum DiagnosticSnippetHighlightStyle {
132  /// The highlight is an error. This will place red carets under the highlight.
133  Error,
134  #[allow(dead_code)]
135  /// The highlight is a warning. This will place yellow carets under the
136  /// highlight.
137  Warning,
138  #[allow(dead_code)]
139  /// The highlight shows a hint. This will place blue dashes under the
140  /// highlight.
141  Hint,
142}
143
144impl DiagnosticSnippetHighlightStyle {
145  fn style_underline(
146    &self,
147    s: impl std::fmt::Display,
148  ) -> impl std::fmt::Display {
149    match self {
150      DiagnosticSnippetHighlightStyle::Error => colors::red_bold(s),
151      DiagnosticSnippetHighlightStyle::Warning => colors::yellow_bold(s),
152      DiagnosticSnippetHighlightStyle::Hint => colors::intense_blue(s),
153    }
154  }
155
156  fn underline_char(&self) -> char {
157    match self {
158      DiagnosticSnippetHighlightStyle::Error => '^',
159      DiagnosticSnippetHighlightStyle::Warning => '^',
160      DiagnosticSnippetHighlightStyle::Hint => '-',
161    }
162  }
163}
164
165/// Returns the text of the line with the given number.
166fn line_text(source: &SourceTextInfo, line_number: usize) -> &str {
167  source.line_text(line_number - 1)
168}
169
170/// Returns the line number (1 indexed) of the line that contains the given
171/// position.
172fn line_number(source: &SourceTextInfo, pos: DiagnosticSourcePos) -> usize {
173  source.line_index(pos.pos(source)) + 1
174}
175
176pub trait Diagnostic {
177  /// The level of the diagnostic.
178  fn level(&self) -> DiagnosticLevel;
179
180  /// The diagnostic code, like `no-explicit-any` or `ban-untagged-ignore`.
181  fn code(&self) -> Cow<'_, str>;
182
183  /// The human-readable diagnostic message.
184  fn message(&self) -> Cow<'_, str>;
185
186  /// The location this diagnostic is associated with.
187  fn location(&self) -> DiagnosticLocation<'_>;
188
189  /// A snippet showing the source code associated with the diagnostic.
190  fn snippet(&self) -> Option<DiagnosticSnippet<'_>>;
191
192  /// A hint for fixing the diagnostic.
193  fn hint(&self) -> Option<Cow<'_, str>>;
194
195  /// A snippet showing how the diagnostic can be fixed.
196  fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>>;
197
198  fn info(&self) -> Cow<'_, [Cow<'_, str>]>;
199
200  /// An optional URL to the documentation for the diagnostic.
201  fn docs_url(&self) -> Option<Cow<'_, str>>;
202
203  fn display(&self) -> DiagnosticDisplay<'_, Self> {
204    DiagnosticDisplay { diagnostic: self }
205  }
206}
207
208struct RepeatingCharFmt(char, usize);
209impl fmt::Display for RepeatingCharFmt {
210  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211    for _ in 0..self.1 {
212      f.write_char(self.0)?;
213    }
214    Ok(())
215  }
216}
217
218/// How many spaces a tab should be displayed as. 2 is the default used for
219/// `deno fmt`, so we'll use that here.
220const TAB_WIDTH: usize = 2;
221
222struct ReplaceTab<'a>(&'a str);
223impl fmt::Display for ReplaceTab<'_> {
224  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225    let mut written = 0;
226    for (i, c) in self.0.char_indices() {
227      if c == '\t' {
228        self.0[written..i].fmt(f)?;
229        RepeatingCharFmt(' ', TAB_WIDTH).fmt(f)?;
230        written = i + 1;
231      }
232    }
233    self.0[written..].fmt(f)?;
234    Ok(())
235  }
236}
237
238/// The width of the string as displayed, assuming tabs are 2 spaces wide.
239///
240/// This display width assumes that zero-width-joined characters are the width
241/// of their consituent characters. This means that "Person: Red Hair" (which is
242/// represented as "Person" + "ZWJ" + "Red Hair") will have a width of 4.
243///
244/// Whether this is correct is unfortunately dependent on the font / terminal
245/// being used. Here is a list of what terminals consider the length of
246/// "Person: Red Hair" to be:
247///
248/// | Terminal         | Rendered Width |
249/// | ---------------- | -------------- |
250/// | Windows Terminal | 5 chars        |
251/// | iTerm (macOS)    | 2 chars        |
252/// | Terminal (macOS) | 2 chars        |
253/// | VS Code terminal | 4 chars        |
254/// | GNOME Terminal   | 4 chars        |
255///
256/// If we really wanted to, we could try and detect the terminal being used and
257/// adjust the width accordingly. However, this is probably not worth the
258/// effort.
259fn display_width(str: &str) -> usize {
260  let num_tabs = str.chars().filter(|c| *c == '\t').count();
261  str.width_cjk() + num_tabs * TAB_WIDTH - num_tabs
262}
263
264pub struct DiagnosticDisplay<'a, T: Diagnostic + ?Sized> {
265  diagnostic: &'a T,
266}
267
268impl<T: Diagnostic + ?Sized> Display for DiagnosticDisplay<'_, T> {
269  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270    print_diagnostic(f, self.diagnostic)
271  }
272}
273
274// error[missing-return-type]: missing explicit return type on public function
275//   at /mnt/artemis/Projects/github.com/denoland/deno/test.ts:1:16
276//    |
277//  1 | export function test() {
278//    |                 ^^^^
279//    = hint: add an explicit return type to the function
280//    |
281//  1 | export function test(): string {
282//    |                       ^^^^^^^^
283//
284//   info: all functions that are exported from a module must have an explicit return type to support fast check and documentation generation.
285//   docs: https://jsr.io/d/missing-return-type
286fn print_diagnostic(
287  io: &mut dyn std::fmt::Write,
288  diagnostic: &(impl Diagnostic + ?Sized),
289) -> Result<(), std::fmt::Error> {
290  match diagnostic.level() {
291    DiagnosticLevel::Error => {
292      write!(
293        io,
294        "{}",
295        colors::red_bold(format_args!("error[{}]", diagnostic.code()))
296      )?;
297    }
298    DiagnosticLevel::Warning => {
299      write!(
300        io,
301        "{}",
302        colors::yellow_bold(format_args!("warning[{}]", diagnostic.code()))
303      )?;
304    }
305  }
306
307  writeln!(io, ": {}", colors::bold(diagnostic.message()))?;
308
309  let mut max_line_number_digits = 1;
310  if let Some(snippet) = diagnostic.snippet() {
311    for highlight in snippet.highlights.iter() {
312      let last_line = line_number(&snippet.source, highlight.range.end);
313      max_line_number_digits =
314        max_line_number_digits.max(last_line.ilog10() + 1);
315    }
316  }
317
318  if let Some(snippet) = diagnostic.snippet_fixed() {
319    for highlight in snippet.highlights.iter() {
320      let last_line = line_number(&snippet.source, highlight.range.end);
321      max_line_number_digits =
322        max_line_number_digits.max(last_line.ilog10() + 1);
323    }
324  }
325
326  let location = diagnostic.location();
327  write!(
328    io,
329    "{}{}",
330    RepeatingCharFmt(' ', max_line_number_digits as usize),
331    colors::intense_blue("-->"),
332  )?;
333  match &location {
334    DiagnosticLocation::Path { path } => {
335      write!(io, " {}", colors::cyan(path.display()))?;
336    }
337    DiagnosticLocation::Module { specifier }
338    | DiagnosticLocation::ModulePosition { specifier, .. } => {
339      if let Some(path) = specifier_to_file_path(specifier) {
340        write!(io, " {}", colors::cyan(path.display()))?;
341      } else {
342        write!(io, " {}", colors::cyan(specifier.as_str()))?;
343      }
344    }
345  }
346  if let Some((line, column)) = location.position() {
347    write!(
348      io,
349      "{}",
350      colors::yellow(format_args!(":{}:{}", line, column))
351    )?;
352  }
353
354  if diagnostic.snippet().is_some()
355    || diagnostic.hint().is_some()
356    || diagnostic.snippet_fixed().is_some()
357    || !diagnostic.info().is_empty()
358    || diagnostic.docs_url().is_some()
359  {
360    writeln!(io)?;
361  }
362
363  if let Some(snippet) = diagnostic.snippet() {
364    print_snippet(io, &snippet, max_line_number_digits)?;
365  };
366
367  if let Some(hint) = diagnostic.hint() {
368    write!(
369      io,
370      "{} {} ",
371      RepeatingCharFmt(' ', max_line_number_digits as usize),
372      colors::intense_blue("=")
373    )?;
374    writeln!(io, "{}: {}", colors::bold("hint"), hint)?;
375  }
376
377  if let Some(snippet) = diagnostic.snippet_fixed() {
378    print_snippet(io, &snippet, max_line_number_digits)?;
379  }
380
381  if !diagnostic.info().is_empty() || diagnostic.docs_url().is_some() {
382    writeln!(io)?;
383  }
384
385  for info in diagnostic.info().iter() {
386    writeln!(io, "  {}: {}", colors::intense_blue("info"), info)?;
387  }
388  if let Some(docs_url) = diagnostic.docs_url() {
389    writeln!(io, "  {}: {}", colors::intense_blue("docs"), docs_url)?;
390  }
391
392  Ok(())
393}
394
395/// Prints a snippet to the given writer and returns the line number indent.
396fn print_snippet(
397  io: &mut dyn std::fmt::Write,
398  snippet: &DiagnosticSnippet<'_>,
399  max_line_number_digits: u32,
400) -> Result<(), std::fmt::Error> {
401  let DiagnosticSnippet { source, highlights } = snippet;
402
403  fn print_padded(
404    io: &mut dyn std::fmt::Write,
405    text: impl std::fmt::Display,
406    padding: u32,
407  ) -> Result<(), std::fmt::Error> {
408    for _ in 0..padding {
409      write!(io, " ")?;
410    }
411    write!(io, "{}", text)?;
412    Ok(())
413  }
414
415  let mut lines_to_show = HashMap::<usize, Vec<usize>>::new();
416  let mut highlights_info = Vec::new();
417  for (i, highlight) in highlights.iter().enumerate() {
418    let start_line_number = line_number(source, highlight.range.start);
419    let end_line_number = line_number(source, highlight.range.end);
420    highlights_info.push((start_line_number, end_line_number));
421    for line_number in start_line_number..=end_line_number {
422      lines_to_show.entry(line_number).or_default().push(i);
423    }
424  }
425
426  let mut lines_to_show = lines_to_show.into_iter().collect::<Vec<_>>();
427  lines_to_show.sort();
428
429  print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
430  writeln!(io)?;
431  let mut previous_line_number = None;
432  let mut previous_line_empty = false;
433  for (line_number, highlight_indexes) in lines_to_show {
434    if previous_line_number.is_some()
435      && previous_line_number == Some(line_number - 1)
436      && !previous_line_empty
437    {
438      print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
439      writeln!(io)?;
440    }
441
442    print_padded(
443      io,
444      colors::intense_blue(format_args!("{} | ", line_number)),
445      max_line_number_digits - line_number.ilog10() - 1,
446    )?;
447
448    let line_start_pos = source.line_start(line_number - 1);
449    let line_end_pos = source.line_end(line_number - 1);
450    let line_text = line_text(source, line_number);
451    writeln!(io, "{}", ReplaceTab(line_text))?;
452    previous_line_empty = false;
453
454    let mut wrote_description = false;
455    for highlight_index in highlight_indexes {
456      let highlight = &highlights[highlight_index];
457      let (start_line_number, end_line_number) =
458        highlights_info[highlight_index];
459
460      let padding_width;
461      let highlight_width;
462      if start_line_number == end_line_number {
463        padding_width = display_width(source.range_text(&SourceRange::new(
464          line_start_pos,
465          highlight.range.start.pos(source),
466        )));
467        highlight_width = display_width(source.range_text(&SourceRange::new(
468          highlight.range.start.pos(source),
469          highlight.range.end.pos(source),
470        )));
471      } else if start_line_number == line_number {
472        padding_width = display_width(source.range_text(&SourceRange::new(
473          line_start_pos,
474          highlight.range.start.pos(source),
475        )));
476        highlight_width = display_width(source.range_text(&SourceRange::new(
477          highlight.range.start.pos(source),
478          line_end_pos,
479        )));
480      } else if end_line_number == line_number {
481        padding_width = 0;
482        highlight_width = display_width(source.range_text(&SourceRange::new(
483          line_start_pos,
484          highlight.range.end.pos(source),
485        )));
486      } else {
487        padding_width = 0;
488        highlight_width = display_width(line_text);
489      }
490
491      let underline =
492        RepeatingCharFmt(highlight.style.underline_char(), highlight_width);
493      print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
494      write!(io, "{}", RepeatingCharFmt(' ', padding_width))?;
495      write!(io, "{}", highlight.style.style_underline(underline))?;
496
497      if line_number == end_line_number
498        && let Some(description) = &highlight.description
499      {
500        write!(io, " {}", highlight.style.style_underline(description))?;
501        wrote_description = true;
502      }
503
504      writeln!(io)?;
505    }
506
507    if wrote_description {
508      print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
509      writeln!(io)?;
510      previous_line_empty = true;
511    }
512
513    previous_line_number = Some(line_number);
514  }
515
516  Ok(())
517}
518
519/// Attempts to convert a specifier to a file path. By default, uses the Url
520/// crate's `to_file_path()` method, but falls back to try and resolve unix-style
521/// paths on Windows.
522fn specifier_to_file_path(specifier: &ModuleSpecifier) -> Option<PathBuf> {
523  fn to_file_path_if_not_wasm(_specifier: &ModuleSpecifier) -> Option<PathBuf> {
524    #[cfg(target_arch = "wasm32")]
525    {
526      None
527    }
528    #[cfg(not(target_arch = "wasm32"))]
529    {
530      // not available in Wasm
531      _specifier.to_file_path().ok()
532    }
533  }
534
535  if specifier.scheme() != "file" {
536    None
537  } else if cfg!(windows) {
538    match to_file_path_if_not_wasm(specifier) {
539      Some(path) => Some(path),
540      None => {
541        // This might be a unix-style path which is used in the tests even on Windows.
542        // Attempt to see if we can convert it to a `PathBuf`. This code should be removed
543        // once/if https://github.com/servo/rust-url/issues/730 is implemented.
544        if specifier.scheme() == "file"
545          && specifier.host().is_none()
546          && specifier.port().is_none()
547          && specifier.path_segments().is_some()
548        {
549          let path_str = specifier.path();
550          match String::from_utf8(
551            percent_encoding::percent_decode(path_str.as_bytes()).collect(),
552          ) {
553            Ok(path_str) => Some(PathBuf::from(path_str)),
554            Err(_) => None,
555          }
556        } else {
557          None
558        }
559      }
560    }
561  } else {
562    to_file_path_if_not_wasm(specifier)
563  }
564}
565
566#[cfg(any(feature = "transpiling", feature = "type_strip"))]
567pub(crate) type DiagnosticsCell = crate::swc::common::sync::Lrc<
568  crate::swc::common::sync::Lock<Vec<SwcDiagnostic>>,
569>;
570
571#[cfg(any(feature = "transpiling", feature = "type_strip"))]
572#[derive(Default, Clone)]
573pub(crate) struct DiagnosticCollector {
574  diagnostics: DiagnosticsCell,
575}
576
577#[cfg(any(feature = "transpiling", feature = "type_strip"))]
578impl DiagnosticCollector {
579  pub fn into_handler_and_cell(
580    self,
581  ) -> (crate::swc::common::errors::Handler, DiagnosticsCell) {
582    let cell = self.diagnostics.clone();
583    (
584      crate::swc::common::errors::Handler::with_emitter(
585        true,
586        false,
587        Box::new(self),
588      ),
589      cell,
590    )
591  }
592}
593
594#[cfg(any(feature = "transpiling", feature = "type_strip"))]
595impl crate::swc::common::errors::Emitter for DiagnosticCollector {
596  fn emit(
597    &mut self,
598    db: &mut crate::swc::common::errors::DiagnosticBuilder<'_>,
599  ) {
600    let mut diagnostics = self.diagnostics.lock();
601    diagnostics.push(db.take());
602  }
603}
604
605#[derive(Debug, JsError)]
606#[class(syntax)]
607pub struct SwcFoldDiagnosticsError(Vec<String>);
608
609impl std::error::Error for SwcFoldDiagnosticsError {}
610
611impl std::fmt::Display for SwcFoldDiagnosticsError {
612  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613    for (i, diagnostic) in self.0.iter().enumerate() {
614      if i > 0 {
615        write!(f, "\n\n")?;
616      }
617
618      write!(f, "{}", diagnostic)?
619    }
620
621    Ok(())
622  }
623}
624
625pub fn ensure_no_fatal_swc_diagnostics(
626  source_map: &swc_common::SourceMap,
627  diagnostics: impl Iterator<Item = SwcDiagnostic>,
628) -> Result<(), SwcFoldDiagnosticsError> {
629  let fatal_diagnostics = diagnostics
630    .filter(is_fatal_swc_diagnostic)
631    .collect::<Vec<_>>();
632  if !fatal_diagnostics.is_empty() {
633    Err(SwcFoldDiagnosticsError(
634      fatal_diagnostics
635        .iter()
636        .map(|d| format_swc_diagnostic(source_map, d))
637        .collect::<Vec<_>>(),
638    ))
639  } else {
640    Ok(())
641  }
642}
643
644fn is_fatal_swc_diagnostic(diagnostic: &SwcDiagnostic) -> bool {
645  use crate::swc::common::errors::Level;
646  match diagnostic.level {
647    Level::Bug
648    | Level::Cancelled
649    | Level::FailureNote
650    | Level::Fatal
651    | Level::PhaseFatal
652    | Level::Error => true,
653    Level::Help | Level::Note | Level::Warning => false,
654  }
655}
656
657fn format_swc_diagnostic(
658  source_map: &swc_common::SourceMap,
659  diagnostic: &SwcDiagnostic,
660) -> String {
661  if let Some(span) = &diagnostic.span.primary_span() {
662    let file_name = source_map.span_to_filename(*span);
663    let loc = source_map.lookup_char_pos(span.lo);
664    format!(
665      "{} at {}:{}:{}",
666      diagnostic.message(),
667      file_name,
668      loc.line,
669      loc.col_display + 1,
670    )
671  } else {
672    diagnostic.message()
673  }
674}
675
676#[cfg(test)]
677mod tests {
678  use std::borrow::Cow;
679
680  use super::*;
681  use crate::ModuleSpecifier;
682  use crate::SourceTextInfo;
683
684  #[test]
685  fn test_display_width() {
686    assert_eq!(display_width("abc"), 3);
687    assert_eq!(display_width("\t"), 2);
688    assert_eq!(display_width("\t\t123"), 7);
689    assert_eq!(display_width("πŸŽ„"), 2);
690    assert_eq!(display_width("πŸŽ„πŸŽ„"), 4);
691    assert_eq!(display_width("πŸ§‘β€πŸ¦°"), 2);
692  }
693
694  #[test]
695  fn test_position_in_file_from_text_info_simple() {
696    let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap();
697    let text_info = SourceTextInfo::new("foo\nbar\nbaz".into());
698    let pos = text_info.line_start(1);
699    let location = DiagnosticLocation::ModulePosition {
700      specifier: Cow::Borrowed(&specifier),
701      source_pos: DiagnosticSourcePos::SourcePos(pos),
702      text_info: Cow::Owned(text_info),
703    };
704    let position = location.position().unwrap();
705    assert_eq!(position, (2, 1))
706  }
707
708  #[test]
709  fn test_position_in_file_from_text_info_emoji() {
710    let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap();
711    let text_info = SourceTextInfo::new("πŸ§‘β€πŸ¦°text".into());
712    let pos = text_info.line_start(0) + 11; // the end of the emoji
713    let location = DiagnosticLocation::ModulePosition {
714      specifier: Cow::Borrowed(&specifier),
715      source_pos: DiagnosticSourcePos::SourcePos(pos),
716      text_info: Cow::Owned(text_info),
717    };
718    let position = location.position().unwrap();
719    assert_eq!(position, (1, 6))
720  }
721
722  #[test]
723  fn test_specifier_to_file_path() {
724    run_success_test("file:///", "/");
725    run_success_test("file:///test", "/test");
726    run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt");
727    run_success_test(
728      "file:///dir/test%20test/test.txt",
729      "/dir/test test/test.txt",
730    );
731
732    fn run_success_test(specifier: &str, expected_path: &str) {
733      let result =
734        specifier_to_file_path(&ModuleSpecifier::parse(specifier).unwrap())
735          .unwrap();
736      assert_eq!(result, PathBuf::from(expected_path));
737    }
738  }
739}