Rust · 4508 bytes Raw Blame History
1 //! Diagnostic rendering: file:line:col header + source line + caret,
2 //! optionally colorised when stderr is a TTY.
3 //!
4 //! Sprint 32 #5 / #506. Output format mirrors gfortran / clang so
5 //! editors and IDEs can parse it with their existing regexes.
6
7 use std::io::IsTerminal;
8
9 use crate::lexer::Span;
10
11 /// Severity of a rendered diagnostic.
12 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13 pub enum Level {
14 Error,
15 Warning,
16 Note,
17 }
18
19 impl Level {
20 fn label(self) -> &'static str {
21 match self {
22 Level::Error => "error",
23 Level::Warning => "warning",
24 Level::Note => "note",
25 }
26 }
27 fn color(self) -> &'static str {
28 // ANSI SGR colour codes; matched to clang's defaults.
29 match self {
30 Level::Error => "\x1b[31;1m", // bright red
31 Level::Warning => "\x1b[33;1m", // bright yellow
32 Level::Note => "\x1b[34;1m", // bright blue
33 }
34 }
35 }
36
37 /// True when the given fd looks like a TTY and ANSI escapes are
38 /// safe to emit. Honours NO_COLOR and CLICOLOR_FORCE per the
39 /// no-color.org convention so users / CI can override.
40 fn use_color() -> bool {
41 if std::env::var_os("NO_COLOR").is_some() {
42 return false;
43 }
44 if std::env::var_os("CLICOLOR_FORCE")
45 .map(|v| v != "0")
46 .unwrap_or(false)
47 {
48 return true;
49 }
50 std::io::stderr().is_terminal()
51 }
52
53 const ANSI_RESET: &str = "\x1b[0m";
54 const ANSI_BOLD: &str = "\x1b[1m";
55
56 /// Render a single diagnostic to stderr, gfortran-style:
57 /// file:line:col: error: message
58 /// 12 | print *, xyz
59 /// | ^^^
60 ///
61 /// `source` is the full original source text — needed to extract the
62 /// line for the gutter view. `span_len` controls how wide the caret
63 /// underline is (defaults to a single `^` when zero).
64 pub fn render(file: &str, source: &str, span: Span, level: Level, message: &str, span_len: usize) {
65 let color = use_color();
66 let (col_start, reset) = if color {
67 (level.color(), ANSI_RESET)
68 } else {
69 ("", "")
70 };
71 let bold = if color { ANSI_BOLD } else { "" };
72
73 eprintln!(
74 "{bold}{file}:{line}:{col}:{reset} {col_start}{label}:{reset} {bold}{msg}{reset}",
75 bold = bold,
76 file = file,
77 line = span.start.line,
78 col = span.start.col,
79 reset = reset,
80 col_start = col_start,
81 label = level.label(),
82 msg = message,
83 );
84
85 if let Some((gutter, line_text, caret_indent, caret_len)) = snippet_for(source, span, span_len)
86 {
87 let blue = if color { "\x1b[34m" } else { "" };
88 let gutter_width = gutter.to_string().len().max(5);
89 let caret_gutter = " ".repeat(gutter_width);
90 eprintln!(
91 "{blue}{gutter:>width$} |{reset} {line}",
92 gutter = gutter,
93 width = gutter_width,
94 reset = reset,
95 blue = blue,
96 line = line_text
97 );
98 let mut caret = String::new();
99 for _ in 0..caret_indent {
100 caret.push(' ');
101 }
102 for _ in 0..caret_len.max(1) {
103 caret.push('^');
104 }
105 eprintln!(
106 "{blue}{caret_gutter} |{reset} {col_start}{caret}{reset}",
107 blue = blue,
108 caret_gutter = caret_gutter,
109 reset = reset,
110 col_start = col_start,
111 caret = caret
112 );
113 }
114 }
115
116 /// Pull the requested source line out of `source` for display.
117 /// Returns `(line_number, line_text, caret_indent, caret_len)`.
118 /// caret_indent is in display columns assuming a tab is replaced by
119 /// 4 spaces, matching how the line_text is emitted.
120 fn snippet_for(source: &str, span: Span, span_len: usize) -> Option<(u32, String, usize, usize)> {
121 if span.start.line == 0 {
122 return None;
123 }
124 let target = span.start.line as usize;
125 let raw_line = source.lines().nth(target.saturating_sub(1))?;
126 let display_line: String = raw_line.replace('\t', " ");
127 // span.start.col is 1-based. Convert tabs in the prefix to the
128 // same 4-space expansion so the caret lines up.
129 let prefix_chars = span.start.col.saturating_sub(1) as usize;
130 let mut indent = 0usize;
131 for (i, ch) in raw_line.chars().enumerate() {
132 if i >= prefix_chars {
133 break;
134 }
135 indent += if ch == '\t' { 4 } else { 1 };
136 }
137 let caret_len = if span_len == 0 { 1 } else { span_len };
138 Some((span.start.line, display_line, indent, caret_len))
139 }
140