1use 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 line: usize,
40 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 Path { path: PathBuf },
61 Module {
63 specifier: Cow<'a, ModuleSpecifier>,
65 },
66 ModulePosition {
72 specifier: Cow<'a, ModuleSpecifier>,
74 source_pos: DiagnosticSourcePos,
76 text_info: Cow<'a, SourceTextInfo>,
77 },
78}
79
80impl DiagnosticLocation<'_> {
81 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 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 pub source: Cow<'a, crate::SourceTextInfo>,
115 pub highlights: Vec<DiagnosticSnippetHighlight<'a>>,
118}
119
120#[derive(Clone)]
121pub struct DiagnosticSnippetHighlight<'a> {
122 pub range: DiagnosticSourceRange,
124 pub style: DiagnosticSnippetHighlightStyle,
126 pub description: Option<Cow<'a, str>>,
128}
129
130#[derive(Clone, Copy)]
131pub enum DiagnosticSnippetHighlightStyle {
132 Error,
134 #[allow(dead_code)]
135 Warning,
138 #[allow(dead_code)]
139 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
165fn line_text(source: &SourceTextInfo, line_number: usize) -> &str {
167 source.line_text(line_number - 1)
168}
169
170fn line_number(source: &SourceTextInfo, pos: DiagnosticSourcePos) -> usize {
173 source.line_index(pos.pos(source)) + 1
174}
175
176pub trait Diagnostic {
177 fn level(&self) -> DiagnosticLevel;
179
180 fn code(&self) -> Cow<'_, str>;
182
183 fn message(&self) -> Cow<'_, str>;
185
186 fn location(&self) -> DiagnosticLocation<'_>;
188
189 fn snippet(&self) -> Option<DiagnosticSnippet<'_>>;
191
192 fn hint(&self) -> Option<Cow<'_, str>>;
194
195 fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>>;
197
198 fn info(&self) -> Cow<'_, [Cow<'_, str>]>;
199
200 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
218const 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
238fn 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
274fn 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
395fn 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
519fn 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 _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 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; 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}