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_terminal::colors;
11use unicode_width::UnicodeWidthStr;
12
13use crate::ModuleSpecifier;
14use crate::SourcePos;
15use crate::SourceRange;
16use crate::SourceRanged;
17use crate::SourceTextInfo;
18
19pub enum DiagnosticLevel {
20 Error,
21 Warning,
22}
23
24#[derive(Clone, Copy, Debug)]
25pub struct DiagnosticSourceRange {
26 pub start: DiagnosticSourcePos,
27 pub end: DiagnosticSourcePos,
28}
29
30#[derive(Clone, Copy, Debug)]
31pub enum DiagnosticSourcePos {
32 SourcePos(SourcePos),
33 ByteIndex(usize),
34 LineAndCol {
35 line: usize,
37 column: usize,
39 },
40}
41
42impl DiagnosticSourcePos {
43 fn pos(&self, source: &SourceTextInfo) -> SourcePos {
44 match self {
45 DiagnosticSourcePos::SourcePos(pos) => *pos,
46 DiagnosticSourcePos::ByteIndex(index) => source.range().start() + *index,
47 DiagnosticSourcePos::LineAndCol { line, column } => {
48 source.line_start(*line) + *column
49 }
50 }
51 }
52}
53
54#[derive(Clone, Debug)]
55pub enum DiagnosticLocation<'a> {
56 Path { path: PathBuf },
58 Module {
60 specifier: Cow<'a, ModuleSpecifier>,
62 },
63 ModulePosition {
69 specifier: Cow<'a, ModuleSpecifier>,
71 source_pos: DiagnosticSourcePos,
73 text_info: Cow<'a, SourceTextInfo>,
74 },
75}
76
77impl DiagnosticLocation<'_> {
78 fn position(&self) -> Option<(usize, usize)> {
87 match self {
88 DiagnosticLocation::Path { .. } => None,
89 DiagnosticLocation::Module { .. } => None,
90 DiagnosticLocation::ModulePosition {
91 specifier: _specifier,
92 source_pos,
93 text_info,
94 } => {
95 let pos = source_pos.pos(text_info);
96 let line_index = text_info.line_index(pos);
97 let line_start_pos = text_info.line_start(line_index);
98 let content =
100 text_info.range_text(&SourceRange::new(line_start_pos, pos));
101 let line = line_index + 1;
102 let column = content.encode_utf16().count() + 1;
103 Some((line, column))
104 }
105 }
106 }
107}
108
109pub struct DiagnosticSnippet<'a> {
110 pub source: Cow<'a, crate::SourceTextInfo>,
112 pub highlights: Vec<DiagnosticSnippetHighlight<'a>>,
115}
116
117#[derive(Clone)]
118pub struct DiagnosticSnippetHighlight<'a> {
119 pub range: DiagnosticSourceRange,
121 pub style: DiagnosticSnippetHighlightStyle,
123 pub description: Option<Cow<'a, str>>,
125}
126
127#[derive(Clone, Copy)]
128pub enum DiagnosticSnippetHighlightStyle {
129 Error,
131 #[allow(dead_code)]
132 Warning,
135 #[allow(dead_code)]
136 Hint,
139}
140
141impl DiagnosticSnippetHighlightStyle {
142 fn style_underline(
143 &self,
144 s: impl std::fmt::Display,
145 ) -> impl std::fmt::Display {
146 match self {
147 DiagnosticSnippetHighlightStyle::Error => colors::red_bold(s),
148 DiagnosticSnippetHighlightStyle::Warning => colors::yellow_bold(s),
149 DiagnosticSnippetHighlightStyle::Hint => colors::intense_blue(s),
150 }
151 }
152
153 fn underline_char(&self) -> char {
154 match self {
155 DiagnosticSnippetHighlightStyle::Error => '^',
156 DiagnosticSnippetHighlightStyle::Warning => '^',
157 DiagnosticSnippetHighlightStyle::Hint => '-',
158 }
159 }
160}
161
162fn line_text(source: &SourceTextInfo, line_number: usize) -> &str {
164 source.line_text(line_number - 1)
165}
166
167fn line_number(source: &SourceTextInfo, pos: DiagnosticSourcePos) -> usize {
170 source.line_index(pos.pos(source)) + 1
171}
172
173pub trait Diagnostic {
174 fn level(&self) -> DiagnosticLevel;
176
177 fn code(&self) -> Cow<'_, str>;
179
180 fn message(&self) -> Cow<'_, str>;
182
183 fn location(&self) -> DiagnosticLocation<'_>;
185
186 fn snippet(&self) -> Option<DiagnosticSnippet<'_>>;
188
189 fn hint(&self) -> Option<Cow<'_, str>>;
191
192 fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>>;
194
195 fn info(&self) -> Cow<'_, [Cow<'_, str>]>;
196
197 fn docs_url(&self) -> Option<Cow<'_, str>>;
199
200 fn display(&self) -> DiagnosticDisplay<'_, Self> {
201 DiagnosticDisplay { diagnostic: self }
202 }
203}
204
205struct RepeatingCharFmt(char, usize);
206impl fmt::Display for RepeatingCharFmt {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 for _ in 0..self.1 {
209 f.write_char(self.0)?;
210 }
211 Ok(())
212 }
213}
214
215const TAB_WIDTH: usize = 2;
218
219struct ReplaceTab<'a>(&'a str);
220impl fmt::Display for ReplaceTab<'_> {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 let mut written = 0;
223 for (i, c) in self.0.char_indices() {
224 if c == '\t' {
225 self.0[written..i].fmt(f)?;
226 RepeatingCharFmt(' ', TAB_WIDTH).fmt(f)?;
227 written = i + 1;
228 }
229 }
230 self.0[written..].fmt(f)?;
231 Ok(())
232 }
233}
234
235fn display_width(str: &str) -> usize {
257 let num_tabs = str.chars().filter(|c| *c == '\t').count();
258 str.width_cjk() + num_tabs * TAB_WIDTH - num_tabs
259}
260
261pub struct DiagnosticDisplay<'a, T: Diagnostic + ?Sized> {
262 diagnostic: &'a T,
263}
264
265impl<T: Diagnostic + ?Sized> Display for DiagnosticDisplay<'_, T> {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 print_diagnostic(f, self.diagnostic)
268 }
269}
270
271fn print_diagnostic(
284 io: &mut dyn std::fmt::Write,
285 diagnostic: &(impl Diagnostic + ?Sized),
286) -> Result<(), std::fmt::Error> {
287 match diagnostic.level() {
288 DiagnosticLevel::Error => {
289 write!(
290 io,
291 "{}",
292 colors::red_bold(format_args!("error[{}]", diagnostic.code()))
293 )?;
294 }
295 DiagnosticLevel::Warning => {
296 write!(
297 io,
298 "{}",
299 colors::yellow_bold(format_args!("warning[{}]", diagnostic.code()))
300 )?;
301 }
302 }
303
304 writeln!(io, ": {}", colors::bold(diagnostic.message()))?;
305
306 let mut max_line_number_digits = 1;
307 if let Some(snippet) = diagnostic.snippet() {
308 for highlight in snippet.highlights.iter() {
309 let last_line = line_number(&snippet.source, highlight.range.end);
310 max_line_number_digits =
311 max_line_number_digits.max(last_line.ilog10() + 1);
312 }
313 }
314
315 if let Some(snippet) = diagnostic.snippet_fixed() {
316 for highlight in snippet.highlights.iter() {
317 let last_line = line_number(&snippet.source, highlight.range.end);
318 max_line_number_digits =
319 max_line_number_digits.max(last_line.ilog10() + 1);
320 }
321 }
322
323 let location = diagnostic.location();
324 write!(
325 io,
326 "{}{}",
327 RepeatingCharFmt(' ', max_line_number_digits as usize),
328 colors::intense_blue("-->"),
329 )?;
330 match &location {
331 DiagnosticLocation::Path { path } => {
332 write!(io, " {}", colors::cyan(path.display()))?;
333 }
334 DiagnosticLocation::Module { specifier }
335 | DiagnosticLocation::ModulePosition { specifier, .. } => {
336 if let Some(path) = specifier_to_file_path(specifier) {
337 write!(io, " {}", colors::cyan(path.display()))?;
338 } else {
339 write!(io, " {}", colors::cyan(specifier.as_str()))?;
340 }
341 }
342 }
343 if let Some((line, column)) = location.position() {
344 write!(
345 io,
346 "{}",
347 colors::yellow(format_args!(":{}:{}", line, column))
348 )?;
349 }
350
351 if diagnostic.snippet().is_some()
352 || diagnostic.hint().is_some()
353 || diagnostic.snippet_fixed().is_some()
354 || !diagnostic.info().is_empty()
355 || diagnostic.docs_url().is_some()
356 {
357 writeln!(io)?;
358 }
359
360 if let Some(snippet) = diagnostic.snippet() {
361 print_snippet(io, &snippet, max_line_number_digits)?;
362 };
363
364 if let Some(hint) = diagnostic.hint() {
365 write!(
366 io,
367 "{} {} ",
368 RepeatingCharFmt(' ', max_line_number_digits as usize),
369 colors::intense_blue("=")
370 )?;
371 writeln!(io, "{}: {}", colors::bold("hint"), hint)?;
372 }
373
374 if let Some(snippet) = diagnostic.snippet_fixed() {
375 print_snippet(io, &snippet, max_line_number_digits)?;
376 }
377
378 if !diagnostic.info().is_empty() || diagnostic.docs_url().is_some() {
379 writeln!(io)?;
380 }
381
382 for info in diagnostic.info().iter() {
383 writeln!(io, " {}: {}", colors::intense_blue("info"), info)?;
384 }
385 if let Some(docs_url) = diagnostic.docs_url() {
386 writeln!(io, " {}: {}", colors::intense_blue("docs"), docs_url)?;
387 }
388
389 Ok(())
390}
391
392fn print_snippet(
394 io: &mut dyn std::fmt::Write,
395 snippet: &DiagnosticSnippet<'_>,
396 max_line_number_digits: u32,
397) -> Result<(), std::fmt::Error> {
398 let DiagnosticSnippet { source, highlights } = snippet;
399
400 fn print_padded(
401 io: &mut dyn std::fmt::Write,
402 text: impl std::fmt::Display,
403 padding: u32,
404 ) -> Result<(), std::fmt::Error> {
405 for _ in 0..padding {
406 write!(io, " ")?;
407 }
408 write!(io, "{}", text)?;
409 Ok(())
410 }
411
412 let mut lines_to_show = HashMap::<usize, Vec<usize>>::new();
413 let mut highlights_info = Vec::new();
414 for (i, highlight) in highlights.iter().enumerate() {
415 let start_line_number = line_number(source, highlight.range.start);
416 let end_line_number = line_number(source, highlight.range.end);
417 highlights_info.push((start_line_number, end_line_number));
418 for line_number in start_line_number..=end_line_number {
419 lines_to_show.entry(line_number).or_default().push(i);
420 }
421 }
422
423 let mut lines_to_show = lines_to_show.into_iter().collect::<Vec<_>>();
424 lines_to_show.sort();
425
426 print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
427 writeln!(io)?;
428 let mut previous_line_number = None;
429 let mut previous_line_empty = false;
430 for (line_number, highlight_indexes) in lines_to_show {
431 if previous_line_number.is_some()
432 && previous_line_number == Some(line_number - 1)
433 && !previous_line_empty
434 {
435 print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
436 writeln!(io)?;
437 }
438
439 print_padded(
440 io,
441 colors::intense_blue(format_args!("{} | ", line_number)),
442 max_line_number_digits - line_number.ilog10() - 1,
443 )?;
444
445 let line_start_pos = source.line_start(line_number - 1);
446 let line_end_pos = source.line_end(line_number - 1);
447 let line_text = line_text(source, line_number);
448 writeln!(io, "{}", ReplaceTab(line_text))?;
449 previous_line_empty = false;
450
451 let mut wrote_description = false;
452 for highlight_index in highlight_indexes {
453 let highlight = &highlights[highlight_index];
454 let (start_line_number, end_line_number) =
455 highlights_info[highlight_index];
456
457 let padding_width;
458 let highlight_width;
459 if start_line_number == end_line_number {
460 padding_width = display_width(source.range_text(&SourceRange::new(
461 line_start_pos,
462 highlight.range.start.pos(source),
463 )));
464 highlight_width = display_width(source.range_text(&SourceRange::new(
465 highlight.range.start.pos(source),
466 highlight.range.end.pos(source),
467 )));
468 } else if start_line_number == line_number {
469 padding_width = display_width(source.range_text(&SourceRange::new(
470 line_start_pos,
471 highlight.range.start.pos(source),
472 )));
473 highlight_width = display_width(source.range_text(&SourceRange::new(
474 highlight.range.start.pos(source),
475 line_end_pos,
476 )));
477 } else if end_line_number == line_number {
478 padding_width = 0;
479 highlight_width = display_width(source.range_text(&SourceRange::new(
480 line_start_pos,
481 highlight.range.end.pos(source),
482 )));
483 } else {
484 padding_width = 0;
485 highlight_width = display_width(line_text);
486 }
487
488 let underline =
489 RepeatingCharFmt(highlight.style.underline_char(), highlight_width);
490 print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
491 write!(io, "{}", RepeatingCharFmt(' ', padding_width))?;
492 write!(io, "{}", highlight.style.style_underline(underline))?;
493
494 if line_number == end_line_number
495 && let Some(description) = &highlight.description
496 {
497 write!(io, " {}", highlight.style.style_underline(description))?;
498 wrote_description = true;
499 }
500
501 writeln!(io)?;
502 }
503
504 if wrote_description {
505 print_padded(io, colors::intense_blue(" | "), max_line_number_digits)?;
506 writeln!(io)?;
507 previous_line_empty = true;
508 }
509
510 previous_line_number = Some(line_number);
511 }
512
513 Ok(())
514}
515
516fn specifier_to_file_path(specifier: &ModuleSpecifier) -> Option<PathBuf> {
520 fn to_file_path_if_not_wasm(_specifier: &ModuleSpecifier) -> Option<PathBuf> {
521 #[cfg(target_arch = "wasm32")]
522 {
523 None
524 }
525 #[cfg(not(target_arch = "wasm32"))]
526 {
527 _specifier.to_file_path().ok()
529 }
530 }
531
532 if specifier.scheme() != "file" {
533 None
534 } else if cfg!(windows) {
535 match to_file_path_if_not_wasm(specifier) {
536 Some(path) => Some(path),
537 None => {
538 if specifier.scheme() == "file"
542 && specifier.host().is_none()
543 && specifier.port().is_none()
544 && specifier.path_segments().is_some()
545 {
546 let path_str = specifier.path();
547 match String::from_utf8(
548 percent_encoding::percent_decode(path_str.as_bytes()).collect(),
549 ) {
550 Ok(path_str) => Some(PathBuf::from(path_str)),
551 Err(_) => None,
552 }
553 } else {
554 None
555 }
556 }
557 }
558 } else {
559 to_file_path_if_not_wasm(specifier)
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use std::borrow::Cow;
566
567 use super::*;
568 use crate::ModuleSpecifier;
569 use crate::SourceTextInfo;
570
571 #[test]
572 fn test_display_width() {
573 assert_eq!(display_width("abc"), 3);
574 assert_eq!(display_width("\t"), 2);
575 assert_eq!(display_width("\t\t123"), 7);
576 assert_eq!(display_width("π"), 2);
577 assert_eq!(display_width("ππ"), 4);
578 assert_eq!(display_width("π§βπ¦°"), 2);
579 }
580
581 #[test]
582 fn test_position_in_file_from_text_info_simple() {
583 let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap();
584 let text_info = SourceTextInfo::new("foo\nbar\nbaz".into());
585 let pos = text_info.line_start(1);
586 let location = DiagnosticLocation::ModulePosition {
587 specifier: Cow::Borrowed(&specifier),
588 source_pos: DiagnosticSourcePos::SourcePos(pos),
589 text_info: Cow::Owned(text_info),
590 };
591 let position = location.position().unwrap();
592 assert_eq!(position, (2, 1))
593 }
594
595 #[test]
596 fn test_position_in_file_from_text_info_emoji() {
597 let specifier: ModuleSpecifier = "file:///dev/test.ts".parse().unwrap();
598 let text_info = SourceTextInfo::new("π§βπ¦°text".into());
599 let pos = text_info.line_start(0) + 11; let location = DiagnosticLocation::ModulePosition {
601 specifier: Cow::Borrowed(&specifier),
602 source_pos: DiagnosticSourcePos::SourcePos(pos),
603 text_info: Cow::Owned(text_info),
604 };
605 let position = location.position().unwrap();
606 assert_eq!(position, (1, 6))
607 }
608
609 #[test]
610 fn test_specifier_to_file_path() {
611 run_success_test("file:///", "/");
612 run_success_test("file:///test", "/test");
613 run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt");
614 run_success_test(
615 "file:///dir/test%20test/test.txt",
616 "/dir/test test/test.txt",
617 );
618
619 fn run_success_test(specifier: &str, expected_path: &str) {
620 let result =
621 specifier_to_file_path(&ModuleSpecifier::parse(specifier).unwrap())
622 .unwrap();
623 assert_eq!(result, PathBuf::from(expected_path));
624 }
625 }
626}