pub fn remap(raw: &str, src: &str, start: usize, count: usize, wrapped: bool) -> String {
let map_line = |n: usize| -> Option<usize> {
if n >= start && n < start + count {
Some(n - start + 1)
} else if wrapped && n + 1 == start {
Some(1)
} else if wrapped && n == start + count {
Some(count)
} else {
None
}
};
let basename = std::path::Path::new(src)
.file_name()
.and_then(|s| s.to_str());
let mut out = String::with_capacity(raw.len());
for line in raw.lines() {
if basename.is_some_and(|b| line.trim() == b) {
continue;
}
match split_location(line, src) {
Some((gen_line, rest)) if map_line(gen_line).is_some() => {
let mapped = map_line(gen_line).expect("checked in guard");
out.push_str(&format!("<input>:{mapped}{rest}"));
}
Some((_, rest)) => out.push_str(&format!("<session>{rest}")),
None => {
let scrubbed = line.replace(src, "<input>");
out.push_str(&remap_gutter(&scrubbed, start, count).unwrap_or(scrubbed));
}
}
out.push('\n');
}
out
}
fn remap_gutter(line: &str, start: usize, count: usize) -> Option<String> {
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
let digits: String = trimmed.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
return None;
}
let rest = &trimmed[digits.len()..];
if !rest.starts_with(" |") {
return None;
}
let n: usize = digits.parse().ok()?;
let shown = if n >= start && n < start + count {
format!("{:>w$}", n - start + 1, w = digits.len())
} else {
" ".repeat(digits.len())
};
Some(format!("{}{}{}", " ".repeat(indent), shown, rest))
}
pub fn only_new(text: &str) -> String {
filter_blocks(text, |header| header.starts_with("<input>:"))
}
pub fn drop_stale_warnings(text: &str) -> String {
filter_blocks(text, |header| {
let is_warning = header.contains("warning:") || header.contains("warning C");
header.starts_with("<input>:") || !is_warning
})
}
fn filter_blocks(text: &str, keep_header: impl Fn(&str) -> bool) -> String {
let mut out = String::new();
let mut keep = true;
let mut pending: Option<String> = None;
for line in text.lines() {
if is_group_banner(line) {
pending = Some(line.to_string());
continue;
}
if line.starts_with("<input>:") || line.starts_with("<session>") {
keep = keep_header(line);
}
if keep {
if let Some(p) = pending.take() {
out.push_str(&p);
out.push('\n');
}
out.push_str(line);
out.push('\n');
}
}
out
}
fn is_group_banner(line: &str) -> bool {
for tag in ["<input>: ", "<session>: "] {
if let Some(rest) = line.strip_prefix(tag) {
return !rest.starts_with(|c: char| c.is_ascii_digit());
}
}
false
}
fn split_location<'a>(line: &'a str, src: &str) -> Option<(usize, &'a str)> {
let rest = line.strip_prefix(src)?;
if let Some(after) = rest.strip_prefix(':') {
let end = after.find(|c: char| !c.is_ascii_digit())?;
let n: usize = after[..end].parse().ok()?;
return Some((n, &after[end..]));
}
if let Some(after) = rest.strip_prefix('(') {
let end = after.find(|c: char| !c.is_ascii_digit())?;
let n: usize = after[..end].parse().ok()?;
let tail = &after[end..];
let tail = match tail.strip_prefix(',') {
Some(col) => {
let e = col.find(|c: char| !c.is_ascii_digit())?;
&col[e..]
}
None => tail,
};
return Some((n, tail.strip_prefix(')')?));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_gnu_location_into_input_space() {
let raw = "/tmp/x/in.c:42:5: error: 'y' undeclared";
assert_eq!(
remap(raw, "/tmp/x/in.c", 40, 5, false).trim_end(),
"<input>:3:5: error: 'y' undeclared"
);
}
#[test]
fn maps_msvc_location() {
let raw = "C:\\t\\in.c(42): error C2065: 'y': undeclared identifier";
assert_eq!(
remap(raw, "C:\\t\\in.c", 42, 1, false).trim_end(),
"<input>:1: error C2065: 'y': undeclared identifier"
);
}
#[test]
fn labels_locations_outside_the_input() {
let raw = "/tmp/x/in.c:10:1: note: previous definition";
assert_eq!(
remap(raw, "/tmp/x/in.c", 40, 1, false).trim_end(),
"<session>:1: note: previous definition"
);
let raw = "/tmp/x/in.c:41:5: error: expected ';' before 'return'";
assert!(remap(raw, "/tmp/x/in.c", 40, 1, false).starts_with("<session>"));
}
#[test]
fn renumbers_the_excerpt_gutter_to_match_the_header() {
let raw = " 42 | int x = ;\n 43 | return 0;";
let got = remap(raw, "/tmp/x/in.c", 42, 1, false);
assert!(got.contains(" 1 | int x = ;"), "{got}");
assert!(got.contains(" | return 0;"), "{got}");
}
#[test]
fn wrapper_line_anchors_are_pulled_into_a_wrapped_input() {
let raw = "C:\\t\\in.c(41): warning C4018: '>': signed/unsigned mismatch";
assert_eq!(
remap(raw, "C:\\t\\in.c", 42, 1, true).trim_end(),
"<input>:1: warning C4018: '>': signed/unsigned mismatch"
);
let raw2 = "C:\\t\\in.c(43): error C2143: syntax error";
assert_eq!(
remap(raw2, "C:\\t\\in.c", 42, 1, true).trim_end(),
"<input>:1: error C2143: syntax error"
);
assert!(remap(raw, "C:\\t\\in.c", 42, 1, false).starts_with("<session>"));
}
#[test]
fn drops_msvc_bare_filename_lines() {
let raw = "input.c\n/tmp/x/input.c:42:1: warning: something";
let got = remap(raw, "/tmp/x/input.c", 42, 1, false);
assert!(!got.contains("input.c"), "{got}");
assert!(got.contains("<input>:1:1: warning"), "{got}");
}
#[test]
fn scrubs_temp_path_from_unlocated_lines() {
let raw = "cc1: warning while reading /tmp/x/in.c";
assert_eq!(
remap(raw, "/tmp/x/in.c", 1, 1, false).trim_end(),
"cc1: warning while reading <input>"
);
}
}