1use voltaic_diag::Diagnostic;
2
3use voltaic_diag::DiagnosticCode;
4
5use voltaic_diag::SourceMap;
6
7use voltaic_diag::Span;
8
9pub fn remap(
10 message: &RustcMessage,
11 map: &SourceMap,
12 vlt_path: &str,
13 vlt_source: &str,
14 vrs_path: &str,
15 vrs_source: &str,
16) -> Diagnostic {
17 let code = DiagnosticCode::Foreign(
18 message
19 .code
20 .clone()
21 .unwrap_or_else(|| message.level.clone()),
22 );
23
24 let severity = if message.level == "warning" {
25 voltaic_diag::Severity::Warning
26 } else {
27 voltaic_diag::Severity::Error
28 };
29
30 match map.origin(message.line) {
31 Some(origin) => {
32 let shifted = origin.map_col(message.col);
33 Diagnostic {
34 code,
35 severity,
36 message: message.message.clone(),
37 span: narrow(origin.span, vlt_source, origin.src_line, shifted),
38 related: Vec::new(),
39 help: None,
40 path: vlt_path.to_string(),
41 source: vlt_source.to_string(),
42 }
43 }
44 None => {
45 let offset = generated_offset(vrs_source, message.line, message.col);
46 let note = format!(
47 "this is on a line Voltaic generated, so it has no place in your source; see {}:{}:{}",
48 vrs_path, message.line, message.col
49 );
50 Diagnostic {
51 code,
52 severity,
53 message: message.message.clone(),
54 span: (offset, offset + 1),
55 related: Vec::new(),
56 help: Some(note),
57 path: vrs_path.to_string(),
58 source: vrs_source.to_string(),
59 }
60 }
61 }
62}
63
64#[cfg(test)]
65const VLT: &str = "add = (a: i32, b: i32): i32 ->\n a + \"x\"\n";
66
67#[cfg(test)]
68const VRS: &str = "fn add(a: i32, b: i32) -> i32 {\n a + \"x\"\n}\n";
69
70#[cfg(test)]
71fn map_of() -> SourceMap {
72 SourceMap::new(
73 "p.vlt",
74 vec![
75 Some(voltaic_diag::Origin {
76 span: (0, 30),
77 src_line: 1,
78 src_col: 1,
79 gen_col: 1,
80 }),
81 Some(voltaic_diag::Origin {
82 span: (33, 40),
83 src_line: 2,
84 src_col: 3,
85 gen_col: 5,
86 }),
87 None,
88 ],
89 )
90}
91
92#[cfg(test)]
93fn message_at(line: usize, col: usize) -> RustcMessage {
94 RustcMessage {
95 path: "p.vrs".to_string(),
96 line,
97 col,
98 level: "error".to_string(),
99 code: Some("E0277".to_string()),
100 message: "cannot add `&str` to `i32`".to_string(),
101 }
102}
103
104#[cfg(test)]
105fn run(m: &RustcMessage) -> voltaic_diag::Diagnostic {
106 remap(m, &map_of(), "p.vlt", VLT, "p.vrs", VRS)
107}
108
109pub(crate) fn char_offset(source: &str, line: usize, col: usize) -> Option<usize> {
110 if line == 0 || col == 0 {
111 return None;
112 }
113 let mut offset = 0;
114 for pair in source.lines().enumerate() {
115 let n = pair.0;
116 let text = pair.1;
117 if n + 1 == line {
118 if text.chars().count() < col - 1 {
119 return None;
120 }
121 let within: usize = text.chars().take(col - 1).map(char::len_utf8).sum();
122 return Some(offset + within);
123 }
124 offset += text.len() + 1;
125 }
126 None
127}
128
129#[cfg(test)]
130const SRC_CHAR_OFFSET: &str = "ab\n cd\n";
131
132pub(crate) fn generated_offset(source: &str, line: usize, col: usize) -> usize {
133 char_offset(source, line, col).unwrap_or_else(|| source.len().saturating_sub(1))
134}
135
136#[cfg(test)]
137const SRC_GENERATED_OFFSET: &str = "fn main() {\n}\n";
138
139pub(crate) fn narrow(span: Span, source: &str, line: usize, col: usize) -> Span {
140 let Some(offset) = char_offset(source, line, col) else {
141 return span;
142 };
143 if offset < span.0 || offset >= span.1 {
144 return span;
145 }
146 let n = source.len();
147 let width = source[offset..n].chars().next().map_or(1, char::len_utf8);
148 (offset, offset + width)
149}
150
151#[cfg(test)]
152const SRC_NARROW: &str = "add = (a: i32, b: i32): i32 ->\n a + \"x\"\n";
153
154#[cfg(test)]
155const SPAN: (usize, usize) = (33, 40);
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct RustcMessage {
159 pub path: String,
160 pub line: usize,
161 pub col: usize,
162 pub level: String,
163 pub code: Option<String>,
164 pub message: String,
165}
166
167impl RustcMessage {
168 pub fn parse(line: &str) -> Option<Self> {
169 let head = line.split_once(": ")?;
170 let position = head.0;
171 let rest = head.1;
172
173 let tail = position.rsplit_once(':')?;
174 let col: usize = tail.1.parse().ok()?;
175 let stem = tail.0.rsplit_once(':')?;
176 let line_no: usize = stem.1.parse().ok()?;
177
178 let labelled = rest.split_once(": ")?;
179 let label = labelled.0;
180 let message = labelled.1;
181 let bracketed = label.split_once('[');
182 let level = bracketed.as_ref().map_or(label, |parts| parts.0);
183 let code = bracketed
184 .as_ref()
185 .map(|parts| parts.1.trim_end_matches(']').to_string());
186 if level != "error" && level != "warning" {
187 return None;
188 }
189
190 Some(RustcMessage {
191 path: stem.0.to_string(),
192 line: line_no,
193 col,
194 level: level.to_string(),
195 code,
196 message: message.to_string(),
197 })
198 }
199
200 pub fn is_error(&self) -> bool {
201 self.level == "error"
202 }
203}
204
205#[cfg(test)]
206mod __vlt_tests {
207 use super::*;
208
209 #[test]
210 fn narrows_to_the_exact_character_on_a_verbatim_line() {
211 let d = run(&message_at(2, 7));
212 assert_eq!(d.path, "p.vlt");
213 assert_eq!(&VLT[d.span.0..d.span.1], "+");
214 assert_eq!(d.code.text(), "E0277");
215 assert_eq!(
216 d.message, "cannot add `&str` to `i32`",
217 "rustc's message passes through"
218 );
219 assert!(d.help.is_none());
220 }
221
222 #[test]
223 fn underlines_the_whole_span_when_the_column_misses() {
224 let d = run(&message_at(2, 99));
225 assert_eq!(d.span, (33, 40));
226 assert_eq!(&VLT[d.span.0..d.span.1], "a + \"x\"");
227 }
228
229 #[test]
230 fn reports_a_synthetic_line_against_the_generated_file() {
231 let d = run(&message_at(3, 1));
232 assert_eq!(d.path, "p.vrs");
233 assert_eq!(d.source, VRS);
234 let help = d.help.expect("a synthetic line should explain itself");
235 assert!(help.contains("Voltaic generated"), "{}", help);
236 assert!(help.contains("p.vrs:3:1"), "{}", help);
237 }
238
239 #[test]
240 fn reports_an_unmapped_line_against_the_generated_file() {
241 let d = run(&message_at(99, 1));
242 assert_eq!(d.path, "p.vrs");
243 }
244
245 #[test]
246 fn falls_back_to_the_level_when_rustc_gave_no_code() {
247 let mut m = message_at(2, 7);
248 m.code = None;
249 m.level = "warning".to_string();
250 assert_eq!(run(&m).code.text(), "warning");
251 }
252
253 #[test]
254 fn resolves_a_position() {
255 assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 1), Some(0));
256 assert_eq!(char_offset(SRC_CHAR_OFFSET, 2, 3), Some(5));
257 assert_eq!(&SRC_CHAR_OFFSET[5..7], "cd");
258 }
259
260 #[test]
261 fn allows_the_position_just_past_a_line() {
262 assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 3), Some(2));
263 }
264
265 #[test]
266 fn declines_a_column_past_the_line() {
267 assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 9), None);
268 }
269
270 #[test]
271 fn declines_a_line_past_the_text() {
272 assert_eq!(char_offset(SRC_CHAR_OFFSET, 9, 1), None);
273 }
274
275 #[test]
276 fn declines_a_zero_line_or_column() {
277 assert_eq!(char_offset(SRC_CHAR_OFFSET, 0, 1), None);
278 assert_eq!(char_offset(SRC_CHAR_OFFSET, 1, 0), None);
279 }
280
281 #[test]
282 fn counts_characters_and_returns_bytes() {
283 assert_eq!(char_offset("é x\n", 1, 2), Some(2));
284 assert_eq!(&"é x\n"[2..3], " ");
285 }
286
287 #[test]
288 fn resolves_a_real_position() {
289 assert_eq!(generated_offset(SRC_GENERATED_OFFSET, 2, 1), 12);
290 assert_eq!(SRC_GENERATED_OFFSET[12..13].chars().next(), Some('}'));
291 }
292
293 #[test]
294 fn clamps_a_position_past_the_end() {
295 assert_eq!(
296 generated_offset(SRC_GENERATED_OFFSET, 99, 99),
297 SRC_GENERATED_OFFSET.len() - 1
298 );
299 }
300
301 #[test]
302 fn survives_an_empty_source() {
303 assert_eq!(generated_offset("", 1, 1), 0);
304 }
305
306 #[test]
307 fn narrows_to_one_character_inside_the_span() {
308 let got = narrow(SPAN, SRC_NARROW, 2, 5);
309 assert_eq!(&SRC_NARROW[got.0..got.1], "+");
310 }
311
312 #[test]
313 fn keeps_the_span_when_the_column_is_past_the_line() {
314 assert_eq!(narrow(SPAN, SRC_NARROW, 2, 99), SPAN);
315 }
316
317 #[test]
318 fn keeps_the_span_when_the_position_is_elsewhere() {
319 assert_eq!(narrow(SPAN, SRC_NARROW, 1, 1), SPAN);
320 }
321
322 #[test]
323 fn keeps_the_span_when_the_line_does_not_exist() {
324 assert_eq!(narrow(SPAN, SRC_NARROW, 99, 1), SPAN);
325 }
326
327 #[test]
328 fn narrows_to_a_whole_multibyte_character() {
329 let src = "x : ()\nx =\n é\n";
330 let span = (13, 15);
331 assert_eq!(&src[span.0..span.1], "é", "the premise of this test");
332 let got = narrow(span, src, 3, 3);
333 assert_eq!(&src[got.0..got.1], "é");
334 }
335
336 #[test]
337 fn parses_a_coded_error() {
338 let m = RustcMessage::parse(
339 ".tmp/typeerr.vrs:6:24: error[E0308]: mismatched types: expected `i32`, found `&str`",
340 )
341 .expect("should parse");
342 assert_eq!(m.path, ".tmp/typeerr.vrs");
343 assert_eq!((m.line, m.col), (6, 24));
344 assert_eq!(m.level, "error");
345 assert_eq!(m.code.as_deref(), Some("E0308"));
346 assert_eq!(
347 m.message, "mismatched types: expected `i32`, found `&str`",
348 "the message keeps rustc's own colons"
349 );
350 assert!(m.is_error());
351 }
352
353 #[test]
354 fn keeps_a_message_containing_a_colon() {
355 let m = RustcMessage::parse(".tmp/tail.vrs:2:7: error[E0277]: cannot add `&str` to `i32`: no implementation for `i32 + &str`").expect("should parse");
356 assert_eq!(
357 m.message,
358 "cannot add `&str` to `i32`: no implementation for `i32 + &str`"
359 );
360 }
361
362 #[test]
363 fn parses_an_uncoded_warning() {
364 let m =
365 RustcMessage::parse("main.vrs:3:9: warning: unused variable: `x`").expect("should parse");
366 assert_eq!(m.level, "warning");
367 assert_eq!(m.code, None);
368 assert_eq!(m.message, "unused variable: `x`");
369 assert!(!m.is_error());
370 }
371
372 #[test]
373 fn declines_the_summary_line() {
374 assert_eq!(
375 RustcMessage::parse("error: aborting due to 1 previous error"),
376 None
377 );
378 }
379
380 #[test]
381 fn declines_an_unpositioned_note() {
382 assert_eq!(
383 RustcMessage::parse("note: run with `RUST_BACKTRACE=1`"),
384 None
385 );
386 assert_eq!(RustcMessage::parse(""), None);
387 }
388
389 #[test]
390 fn survives_a_colon_in_the_path() {
391 let m = RustcMessage::parse("C:\\out\\main.vrs:6:24: error[E0308]: mismatched types")
392 .expect("should parse");
393 assert_eq!(m.path, "C:\\out\\main.vrs");
394 assert_eq!((m.line, m.col), (6, 24));
395 }
396
397 #[test]
398 fn declines_an_unknown_level() {
399 assert_eq!(RustcMessage::parse("main.vrs:1:1: help: try this"), None);
400 }
401}