palladium/errors/
reporter.rs1use super::{Diagnostic, DiagnosticLevel, Span, Suggestion};
5use std::cmp::{max, min};
6use std::fs;
7
8pub struct ErrorReporter {
9 source_file: String,
10 source_content: String,
11}
12
13impl ErrorReporter {
14 pub fn new(source_file: String) -> std::io::Result<Self> {
15 let source_content = fs::read_to_string(&source_file)?;
16 Ok(Self {
17 source_file,
18 source_content,
19 })
20 }
21
22 pub fn report(&self, diagnostic: &Diagnostic) {
23 let (level_color, level_text) = match diagnostic.level {
25 DiagnosticLevel::Error => ("31", "error"),
26 DiagnosticLevel::Warning => ("33", "warning"),
27 DiagnosticLevel::Info => ("36", "info"),
28 DiagnosticLevel::Help => ("32", "help"),
29 };
30
31 eprintln!(
32 "\x1b[1;{}m{}\x1b[0m\x1b[1m: {}\x1b[0m",
33 level_color, level_text, diagnostic.message
34 );
35
36 if let Some(span) = diagnostic.span {
38 self.show_source_snippet_with_context(span, diagnostic.context_lines);
39 }
40
41 for note in &diagnostic.notes {
43 eprintln!("\x1b[1;36m = note:\x1b[0m {}", note);
44 }
45
46 for suggestion in &diagnostic.suggestions {
48 self.show_suggestion(suggestion);
49 }
50
51 eprintln!(); }
53
54 #[allow(dead_code)]
55 fn show_source_snippet(&self, span: Span) {
56 self.show_source_snippet_with_context(span, 0);
57 }
58
59 fn show_source_snippet_with_context(&self, span: Span, context_lines: usize) {
60 eprintln!(
62 "\x1b[1;34m --> \x1b[0m{}:{}:{}",
63 self.source_file, span.line, span.column
64 );
65
66 let lines: Vec<&str> = self.source_content.lines().collect();
67 if span.line == 0 || span.line > lines.len() {
68 return;
69 }
70
71 let start_line = max(1, span.line.saturating_sub(context_lines));
73 let end_line = min(lines.len(), span.line + context_lines);
74
75 let line_num_width = end_line.to_string().len();
77
78 eprintln!("{}\x1b[1;34m |\x1b[0m", " ".repeat(line_num_width));
79
80 for line_num in start_line..=end_line {
82 let line_text = lines[line_num - 1];
83
84 if line_num == span.line {
85 eprintln!(
87 "\x1b[1;34m{:>width$} |\x1b[0m {}",
88 line_num,
89 line_text,
90 width = line_num_width
91 );
92
93 let mut indicator = String::new();
95 indicator.push_str(&" ".repeat(line_num_width));
96 indicator.push_str(" \x1b[1;34m|\x1b[0m ");
97
98 for (i, ch) in line_text.chars().enumerate() {
100 if i < span.column.saturating_sub(1) {
101 if ch == '\t' {
102 indicator.push('\t');
103 } else {
104 indicator.push(' ');
105 }
106 } else {
107 break;
108 }
109 }
110
111 indicator.push_str("\x1b[1;31m^");
113 let error_len = if span.end > span.start {
114 span.end - span.start
115 } else {
116 self.estimate_token_length(&line_text[span.column.saturating_sub(1)..])
118 .max(1)
119 };
120
121 for _ in 1..error_len {
122 indicator.push('~');
123 }
124 indicator.push_str("\x1b[0m");
125
126 eprintln!("{}", indicator);
127 } else {
128 eprintln!(
130 "\x1b[2;34m{:>width$} |\x1b[0m\x1b[2m {}\x1b[0m",
131 line_num,
132 line_text,
133 width = line_num_width
134 );
135 }
136 }
137
138 eprintln!("{}\x1b[1;34m |\x1b[0m", " ".repeat(line_num_width));
139 }
140
141 fn show_suggestion(&self, suggestion: &Suggestion) {
142 eprintln!("\x1b[1;32m = help:\x1b[0m {}", suggestion.message);
143
144 if let Some(ref replacement) = suggestion.replacement {
145 if let Some(span) = suggestion.span {
146 let lines: Vec<&str> = self.source_content.lines().collect();
148 if span.line > 0 && span.line <= lines.len() {
149 let line = lines[span.line - 1];
150 let mut fixed_line = String::new();
151
152 if span.column > 1 {
154 fixed_line.push_str(&line[..span.column - 1]);
155 }
156 fixed_line.push_str(replacement);
157 if span.end < line.len() {
158 fixed_line.push_str(&line[span.end..]);
159 }
160
161 eprintln!("\x1b[1;32m Suggested fix:\x1b[0m");
162 eprintln!("\x1b[32m {}\x1b[0m", fixed_line);
163 }
164 }
165 }
166 }
167
168 fn estimate_token_length(&self, text: &str) -> usize {
169 let mut len = 0;
171 for ch in text.chars() {
172 if ch.is_alphanumeric() || ch == '_' {
173 len += 1;
174 } else if len > 0 {
175 break;
176 } else if !ch.is_whitespace() {
177 return 1;
179 }
180 }
181 len
182 }
183}
184
185pub struct DiagnosticBuilder;
187
188impl DiagnosticBuilder {
189 pub fn type_mismatch(expected: &str, found: &str, span: Span) -> Diagnostic {
190 Diagnostic::error(format!(
191 "type mismatch: expected {}, found {}",
192 expected, found
193 ))
194 .with_span(span)
195 .with_note("types must match exactly")
196 .with_context_lines(2)
197 }
198
199 pub fn undefined_variable(name: &str, span: Span) -> Diagnostic {
200 Diagnostic::error(format!("undefined variable: {}", name))
201 .with_span(span)
202 .with_note("variables must be declared before use")
203 .with_suggestion(
204 format!("did you mean to declare it? Try: let {} = ...;", name),
205 None,
206 )
207 .with_context_lines(3)
208 }
209
210 pub fn missing_semicolon(span: Span) -> Diagnostic {
211 Diagnostic::error("expected ';' after statement")
212 .with_span(span)
213 .with_note("each statement must end with a semicolon")
214 .with_suggestion(
215 "add a semicolon at the end of this line",
216 Some(";".to_string()),
217 )
218 }
219
220 pub fn wrong_arg_count(func: &str, expected: usize, found: usize, span: Span) -> Diagnostic {
221 let msg = if expected == 1 {
222 format!(
223 "function '{}' expects {} argument, but {} were provided",
224 func, expected, found
225 )
226 } else {
227 format!(
228 "function '{}' expects {} arguments, but {} were provided",
229 func, expected, found
230 )
231 };
232
233 let mut diag = Diagnostic::error(msg)
234 .with_span(span)
235 .with_note(format!("function signature: {}(...)", func))
236 .with_context_lines(2);
237
238 if found < expected {
239 diag = diag.with_suggestion(
240 format!(
241 "add {} more argument{}",
242 expected - found,
243 if expected - found == 1 { "" } else { "s" }
244 ),
245 None,
246 );
247 } else {
248 diag = diag.with_suggestion(
249 format!(
250 "remove {} argument{}",
251 found - expected,
252 if found - expected == 1 { "" } else { "s" }
253 ),
254 None,
255 );
256 }
257
258 diag
259 }
260}