1use std::path::PathBuf;
28
29use lattice_protocol::error_list::{ErrorEntry, ErrorSeverity};
30
31#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct CompilationLocation {
37 pub path: PathBuf,
38 pub line: u32,
40 pub col: u32,
43}
44
45pub fn parse_location_line(line: &str) -> Option<CompilationLocation> {
60 let (path, line0, col0) = crate::parsers::match_location_line(line)?;
61 Some(CompilationLocation {
62 path,
63 line: line0,
64 col: col0,
65 })
66}
67
68pub fn match_severity(line: &str) -> Option<ErrorSeverity> {
77 crate::parsers::match_severity(line)
78}
79
80pub fn scan_location_lines(base_line: u32, text: &str) -> Vec<(u32, u32, u32)> {
107 text.lines()
108 .enumerate()
109 .filter_map(|(i, line)| {
110 let (start, end) = location_path_byte_range(line)?;
111 Some((base_line + i as u32, start as u32, end as u32))
112 })
113 .collect()
114}
115
116fn location_path_byte_range(line: &str) -> Option<(usize, usize)> {
120 let loc = parse_location_line(line)?;
121 let path_str = loc.path.to_str()?;
122 let byte_start = line.find(path_str)?;
123 let byte_end = byte_start + path_str.len();
124 Some((byte_start, byte_end))
125}
126
127pub fn scan_severities(base_line: u32, text: &str) -> Vec<(u32, ErrorSeverity)> {
132 text.lines()
133 .enumerate()
134 .filter_map(|(i, line)| match_severity(line).map(|sev| (base_line + i as u32, sev)))
135 .collect()
136}
137
138pub trait CompilationParser: Send {
150 fn feed(&mut self, line: &str) -> Vec<ErrorEntry>;
152}
153
154pub struct ParserRegistry {
161 parsers: Vec<Box<dyn CompilationParser>>,
162}
163
164impl ParserRegistry {
165 pub fn new() -> Self {
167 Self {
168 parsers: Vec::new(),
169 }
170 }
171
172 pub fn with_builtins() -> Self {
175 let mut registry = Self::new();
176 registry.register(Box::new(crate::parsers::CargoRustcParser::new()));
177 registry.register(Box::new(crate::parsers::GnuStyleParser::new()));
178 registry.register(Box::new(crate::parsers::TestPanicParser::new()));
179 registry.register(Box::new(crate::parsers::GeneralParser::new()));
180 registry
181 }
182
183 pub fn register(&mut self, parser: Box<dyn CompilationParser>) {
193 self.parsers.push(parser);
194 }
195
196 pub fn register_before_catch_all(&mut self, parser: Box<dyn CompilationParser>) {
204 let at = self.parsers.len().saturating_sub(1);
205 self.parsers.insert(at, parser);
206 }
207
208 pub fn feed(&mut self, line: &str) -> Vec<ErrorEntry> {
215 let mut out: Vec<ErrorEntry> = Vec::new();
216 for parser in &mut self.parsers {
217 for entry in parser.feed(line) {
218 let dup = out.iter().any(|existing| {
219 existing.path == entry.path
220 && existing.line == entry.line
221 && existing.col == entry.col
222 });
223 if !dup {
224 out.push(entry);
225 }
226 }
227 }
228 out
229 }
230}
231
232impl Default for ParserRegistry {
233 fn default() -> Self {
234 Self::with_builtins()
235 }
236}
237
238#[cfg(test)]
239mod cm6_tests {
240 use super::*;
241
242 struct FakePlugin;
244 impl CompilationParser for FakePlugin {
245 fn feed(&mut self, line: &str) -> Vec<ErrorEntry> {
246 line.strip_prefix("QQ ")
247 .map(|rest| {
248 vec![ErrorEntry {
249 path: std::path::PathBuf::from(rest),
250 line: 0,
251 col: 0,
252 severity: ErrorSeverity::Error,
253 message: "from the plugin".into(),
254 }]
255 })
256 .unwrap_or_default()
257 }
258 }
259
260 struct ClaimsMainRs;
263 impl CompilationParser for ClaimsMainRs {
264 fn feed(&mut self, line: &str) -> Vec<ErrorEntry> {
265 if line.contains("main.rs:10:5") {
266 vec![ErrorEntry {
267 path: std::path::PathBuf::from("main.rs"),
268 line: 9,
269 col: 4,
270 severity: ErrorSeverity::Warning,
271 message: "the plugin's richer message".into(),
272 }]
273 } else {
274 Vec::new()
275 }
276 }
277 }
278
279 #[test]
280 fn a_registered_plugin_parser_contributes_entries() {
281 let mut r = ParserRegistry::with_builtins();
282 r.register_before_catch_all(Box::new(FakePlugin));
283 let got = r.feed("QQ weird/format.q");
284 assert_eq!(got.len(), 1, "got {got:?}");
285 assert_eq!(got[0].message, "from the plugin");
286 }
287
288 #[test]
289 fn a_plugin_parser_beats_the_catch_all_for_the_same_location() {
290 let mut r = ParserRegistry::with_builtins();
294 r.register_before_catch_all(Box::new(ClaimsMainRs));
295 let got = r.feed("something main.rs:10:5 something");
296 assert_eq!(got.len(), 1, "deduped by location: {got:?}");
297 assert_eq!(
298 got[0].message, "the plugin's richer message",
299 "the plugin's entry must win over the catch-all's salvage"
300 );
301 assert_eq!(got[0].severity, ErrorSeverity::Warning);
302 }
303
304 #[test]
305 fn a_plugin_parser_does_not_displace_a_format_specific_native() {
306 let mut r = ParserRegistry::with_builtins();
309 r.register_before_catch_all(Box::new(ClaimsMainRs));
310 let got = r.feed("main.rs:10:5: error: real gnu-style diagnostic");
311 assert_eq!(got.len(), 1);
312 assert_eq!(
313 got[0].message, "real gnu-style diagnostic",
314 "the gnu parser understands this line better than the plugin: {got:?}"
315 );
316 }
317
318 #[test]
319 fn a_silent_plugin_parser_costs_nothing() {
320 let mut r = ParserRegistry::with_builtins();
321 r.register_before_catch_all(Box::new(FakePlugin));
322 assert!(r.feed(" Compiling foo v0.1.0").is_empty());
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use lattice_protocol::error_list::ErrorSeverity;
330 use std::path::PathBuf;
331
332 fn parse_block(block: &str) -> Vec<ErrorEntry> {
335 let mut registry = ParserRegistry::with_builtins();
336 let mut out = Vec::new();
337 for line in block.lines() {
338 out.extend(registry.feed(line));
339 }
340 out
341 }
342
343 #[test]
344 fn cargo_multiline_error_block_yields_one_zero_based_entry() {
345 let block = "\
346Compiling foo v0.1.0 (/tmp/foo)
347error[E0308]: mismatched types
348 --> src/foo.rs:12:9
349 |
35012 | let x: u32 = \"s\";
351 | --- ^^^ expected `u32`, found `&str`
352 |
353error: aborting due to 1 previous error
354";
355 let entries = parse_block(block);
356 assert_eq!(entries.len(), 1, "one located diagnostic, got {entries:?}");
357 let e = &entries[0];
358 assert_eq!(e.path, PathBuf::from("src/foo.rs"));
359 assert_eq!(e.line, 11, "rustc 1-based 12 → 0-based 11");
360 assert_eq!(e.col, 8, "rustc 1-based 9 → 0-based 8");
361 assert_eq!(e.severity, ErrorSeverity::Error);
362 assert_eq!(e.message, "mismatched types");
363 }
364
365 #[test]
366 fn cargo_warning_block_yields_warning_severity() {
367 let block = "\
368warning: unused variable: `y`
369 --> src/lib.rs:3:5
370";
371 let entries = parse_block(block);
372 assert_eq!(entries.len(), 1);
373 assert_eq!(entries[0].severity, ErrorSeverity::Warning);
374 assert_eq!(entries[0].line, 2);
375 assert_eq!(entries[0].col, 4);
376 assert_eq!(entries[0].message, "unused variable: `y`");
377 }
378
379 #[test]
380 fn gnu_full_form_yields_correct_entry() {
381 let entries = parse_block("main.c:10:5: error: 'foo' undeclared\n");
382 assert_eq!(entries.len(), 1);
383 let e = &entries[0];
384 assert_eq!(e.path, PathBuf::from("main.c"));
385 assert_eq!(e.line, 9);
386 assert_eq!(e.col, 4);
387 assert_eq!(e.severity, ErrorSeverity::Error);
388 assert_eq!(e.message, "'foo' undeclared");
389 }
390
391 #[test]
392 fn gnu_warning_and_note_severities_map() {
393 let warn = parse_block("a.c:1:1: warning: w\n");
394 assert_eq!(warn[0].severity, ErrorSeverity::Warning);
395 let note = parse_block("a.c:2:2: note: n\n");
396 assert_eq!(note[0].severity, ErrorSeverity::Note);
397 let fatal = parse_block("a.c:3:3: fatal error: f\n");
399 assert_eq!(fatal[0].severity, ErrorSeverity::Error);
400 }
401
402 #[test]
403 fn gnu_short_form_line_only_defaults_to_info() {
404 let entries = parse_block("Makefile:42: missing separator\n");
405 assert_eq!(entries.len(), 1);
406 let e = &entries[0];
407 assert_eq!(e.path, PathBuf::from("Makefile"));
408 assert_eq!(e.line, 41);
409 assert_eq!(e.col, 0, "short form has no column → 0");
410 assert_eq!(e.severity, ErrorSeverity::Info);
411 assert_eq!(e.message, "missing separator");
412 }
413
414 #[test]
415 fn non_matching_lines_yield_no_entries_and_no_panic() {
416 let block = "\
417 Compiling something
418 Finished dev [unoptimized] target(s) in 1.23s
419just some prose with a colon: not a location
420==== running 3 tests ====
421";
422 assert!(parse_block(block).is_empty());
423 }
424
425 #[test]
426 fn cargo_location_line_is_not_double_counted_by_gnu() {
427 let block = "\
430error[E0433]: failed to resolve
431 --> src/a.rs:7:13
432";
433 let entries = parse_block(block);
434 assert_eq!(
435 entries.len(),
436 1,
437 "cargo block yields exactly one entry, got {entries:?}"
438 );
439 assert_eq!(entries[0].path, PathBuf::from("src/a.rs"));
440 }
441
442 #[test]
443 fn parse_location_line_matches_cargo_arrow() {
444 let loc = parse_location_line(" --> src/foo.rs:12:9").expect("cargo location");
445 assert_eq!(loc.path, PathBuf::from("src/foo.rs"));
446 assert_eq!(loc.line, 11, "rustc 1-based 12 → 0-based 11");
447 assert_eq!(loc.col, 8, "rustc 1-based 9 → 0-based 8");
448 }
449
450 #[test]
451 fn parse_location_line_matches_gnu_full() {
452 let loc = parse_location_line("main.c:10:5: error: x").expect("gnu location");
453 assert_eq!(loc.path, PathBuf::from("main.c"));
454 assert_eq!(loc.line, 9, "gnu 1-based 10 → 0-based 9");
455 assert_eq!(loc.col, 4, "gnu 1-based 5 → 0-based 4");
456 }
457
458 #[test]
459 fn parse_location_line_rejects_plain_text() {
460 assert_eq!(parse_location_line("just some prose, not a location"), None);
461 assert_eq!(parse_location_line(" Compiling foo v0.1.0"), None);
462 }
463
464 #[test]
467 fn match_severity_maps_cargo_headers() {
468 assert_eq!(
469 match_severity("error[E0308]: mismatched types"),
470 Some(ErrorSeverity::Error)
471 );
472 assert_eq!(
473 match_severity("error: aborting due to 1 previous error"),
474 Some(ErrorSeverity::Error)
475 );
476 assert_eq!(
477 match_severity("warning: unused variable: `y`"),
478 Some(ErrorSeverity::Warning)
479 );
480 }
481
482 #[test]
483 fn match_severity_maps_gnu_full_form() {
484 assert_eq!(
485 match_severity("main.c:10:5: error: 'foo' undeclared"),
486 Some(ErrorSeverity::Error)
487 );
488 assert_eq!(
489 match_severity("a.c:1:1: warning: w"),
490 Some(ErrorSeverity::Warning)
491 );
492 assert_eq!(
493 match_severity("a.c:2:2: note: n"),
494 Some(ErrorSeverity::Note)
495 );
496 assert_eq!(
497 match_severity("a.c:3:3: fatal error: f"),
498 Some(ErrorSeverity::Error)
499 );
500 }
501
502 #[test]
503 fn match_severity_none_on_non_severity_lines() {
504 assert_eq!(match_severity(" Compiling foo v0.1.0"), None);
507 assert_eq!(match_severity(" --> src/foo.rs:12:9"), None);
508 assert_eq!(match_severity("Makefile:42: missing separator"), None);
509 assert_eq!(
510 match_severity("just some prose with a colon: not a location"),
511 None
512 );
513 assert_eq!(match_severity(""), None);
514 }
515
516 #[test]
517 fn scan_severities_yields_absolute_line_numbers() {
518 let block = "\
521Compiling foo
522error[E0308]: mismatched types
523 --> src/foo.rs:12:9
524warning: unused
525";
526 assert_eq!(
527 scan_severities(0, block),
528 vec![(1, ErrorSeverity::Error), (3, ErrorSeverity::Warning)]
529 );
530 }
531
532 #[test]
533 fn scan_severities_respects_base_line_offset() {
534 assert_eq!(
535 scan_severities(10, "error: boom\n"),
536 vec![(10, ErrorSeverity::Error)]
537 );
538 assert_eq!(
540 scan_severities(5, "x\nmain.c:1:1: warning: w\n"),
541 vec![(6, ErrorSeverity::Warning)]
542 );
543 }
544
545 #[test]
546 fn scan_severities_empty_and_plain() {
547 assert!(scan_severities(0, "").is_empty());
548 assert!(scan_severities(0, "Compiling\nFinished\n").is_empty());
549 }
550
551 #[test]
554 fn scan_location_lines_matches_cargo_arrow_and_gnu() {
555 let block = "\
556Compiling foo v0.1.0
557 --> src/foo.rs:12:9
558warning: unused
559main.c:10:5: error: x
560plain prose
561";
562 let locs = scan_location_lines(0, block);
563 assert_eq!(locs.len(), 2, "two location lines expected");
564 assert_eq!(locs[0].0, 1, "line 1 = cargo `-->`");
565 assert_eq!(locs[1].0, 3, "line 3 = gnu full-form location");
566 }
567
568 #[test]
569 fn scan_location_lines_respects_base_line_offset() {
570 assert_eq!(
571 scan_location_lines(10, " --> src/a.rs:1:1\n"),
572 vec![(10, 6, 14)]
573 );
574 assert_eq!(
575 scan_location_lines(5, "x\nmain.c:3:3: error: e\n"),
576 vec![(6, 0, 6)]
577 );
578 }
579
580 #[test]
581 fn scan_location_lines_empty_and_plain() {
582 assert!(scan_location_lines(0, "").is_empty());
583 assert!(scan_location_lines(0, "Compiling\nFinished\n").is_empty());
584 assert!(scan_location_lines(0, "just some prose\n").is_empty());
585 }
586}