use core::ops::{Range, RangeInclusive};
use camino::Utf8Path;
use super::Guards;
use super::messages::{CompilerMessage, Span, cargo_message};
use crate::schema::{Guard, Position};
use crate::{HashMap, HashSet};
#[expect(clippy::too_many_lines, reason = "the attribution tiers share one parsed diagnostic walk")]
pub(super) fn blame(stdout: &str, root: &Utf8Path, guards: &Guards) -> HashMap<u32, String> {
let mut blamed: HashMap<u32, String> = HashMap::default();
let mut by_path: HashMap<&Utf8Path, Vec<(u32, &Guard)>> = HashMap::default();
for (ordinal, (file, guard)) in guards {
by_path.entry(file.as_path()).or_default().push((*ordinal, guard));
}
for line in stdout.lines() {
let Some(message) = cargo_message(line) else {
continue;
};
if message.reason != "compiler-message" {
continue;
}
let Some(diagnostic) = message.message else {
continue;
};
if diagnostic.level != "error" {
continue;
}
let primary: Vec<&Span<'_>> = diagnostic.spans.iter().filter(|span| span.is_primary).collect();
let considered = if primary.is_empty() {
diagnostic.spans.iter().collect()
} else {
primary
};
let mut exact = HashSet::default();
let mut enclosing: Option<(u32, HashSet<u32>)> = None;
let mut contained: Option<(u32, HashSet<u32>)> = None;
for span in considered {
let Some(file_name) = span.file_name.as_deref() else {
continue;
};
let relative = Utf8Path::new(file_name)
.strip_prefix(root.as_str())
.unwrap_or_else(|_ignored| Utf8Path::new(file_name));
let Some(reported) = position_range(span) else {
continue;
};
let matched = by_path.get(relative).map(Vec::as_slice).unwrap_or_default();
let scanned;
let here = if matched.is_empty() {
scanned = by_path
.iter()
.filter(|(file, _found)| file_name.ends_with(file.as_str()))
.flat_map(|(_file, found)| found.iter().copied())
.collect::<Vec<_>>();
scanned.as_slice()
} else {
matched
};
for (ordinal, guard) in here.iter().copied() {
if guard.mutated.as_ref().is_some_and(|mutated| covers(mutated, &reported)) {
let _ = exact.insert(ordinal);
} else if covers(&guard.site, &reported) {
let width = guard.site.end.line().saturating_sub(guard.site.start.line());
match &mut enclosing {
Some((best, ordinals)) if *best == width => {
let _ = ordinals.insert(ordinal);
}
Some((best, _ordinals)) if *best < width => {}
_ => enclosing = Some((width, HashSet::from_iter([ordinal]))),
}
} else if covers(&reported, &guard.site) {
let width = reported.end.line().saturating_sub(reported.start.line());
match &mut contained {
Some((best, ordinals)) if *best == width => {
let _ = ordinals.insert(ordinal);
}
Some((best, _ordinals)) if *best < width => {}
_ => contained = Some((width, HashSet::from_iter([ordinal]))),
}
}
}
}
if exact.is_empty() && enclosing.is_none() && contained.is_none() {
for span in diagnostic.spans.iter().filter(|span| !span.is_primary) {
let Some(file_name) = span.file_name.as_deref() else {
continue;
};
let relative = Utf8Path::new(file_name)
.strip_prefix(root.as_str())
.unwrap_or_else(|_ignored| Utf8Path::new(file_name));
let Some(reported) = position_range(span) else {
continue;
};
let matched = by_path.get(relative).map(Vec::as_slice).unwrap_or_default();
for (ordinal, guard) in matched.iter().copied() {
if guard.mutated.as_ref().is_some_and(|mutated| overlaps(mutated, &reported)) {
let _ = exact.insert(ordinal);
}
}
}
}
let ordinals = if exact.is_empty() {
enclosing.or(contained).map(|(_width, ordinals)| ordinals)
} else {
Some(exact)
};
let ordinals = ordinals.or_else(|| diverted(&diagnostic, root, &by_path));
if let Some(ordinals) = ordinals {
let code = diagnostic.code.as_ref().map_or("", |code| code.code.as_ref());
for ordinal in ordinals {
let _ = blamed.entry(ordinal).or_insert_with(|| code.to_owned());
}
}
}
blamed
}
pub(super) const FLOW_SENSITIVE: &[&str] = &[
"E0381", "E0382", "E0383", "E0384", "E0499", "E0502", "E0503", "E0505", "E0506", "E0507", "E0508", "E0509", "E0510", "E0594", "E0596", "E0597", "E0716", ];
pub(super) fn diverted(
diagnostic: &CompilerMessage<'_>,
root: &Utf8Path,
by_path: &HashMap<&Utf8Path, Vec<(u32, &Guard)>>,
) -> Option<HashSet<u32>> {
let code = diagnostic.code.as_ref()?;
if !FLOW_SENSITIVE.contains(&code.code.as_ref()) {
return None;
}
let mut deletions = HashSet::default();
let mut all = HashSet::default();
for (file, region) in regions(diagnostic) {
let relative = Utf8Path::new(file.as_str())
.strip_prefix(root.as_str())
.unwrap_or_else(|_ignored| Utf8Path::new(file.as_str()));
let here = by_path.get(relative).map_or_else(
|| {
by_path
.iter()
.filter(|(known, _found)| file.ends_with(known.as_str()))
.flat_map(|(_known, found)| found.iter().copied())
.collect::<Vec<_>>()
},
Clone::clone,
);
for (ordinal, guard) in here {
if !region.contains(&guard.site.start.line()) {
continue;
}
let _ = all.insert(ordinal);
if guard.mutated.is_none() {
let _ = deletions.insert(ordinal);
}
}
}
if !deletions.is_empty() {
return Some(deletions);
}
if all.is_empty() { None } else { Some(all) }
}
pub(super) fn regions(diagnostic: &CompilerMessage<'_>) -> HashMap<String, RangeInclusive<u32>> {
let mut spread: HashMap<String, RangeInclusive<u32>> = HashMap::default();
let mut pending = vec![diagnostic];
while let Some(node) = pending.pop() {
for span in &node.spans {
let (Some(file), Some(from), Some(to)) = (span.file_name.as_deref(), span.line_start, span.line_end) else {
continue;
};
let from = clamped(from);
let to = clamped(to);
let _widened = spread
.entry(file.to_owned())
.and_modify(|known| *known = (*known.start()).min(from)..=(*known.end()).max(to))
.or_insert(from..=to);
}
pending.extend(&node.children);
}
spread
}
pub(super) fn clamped(number: u64) -> u32 {
u32::try_from(number).unwrap_or(u32::MAX)
}
pub(super) fn position_range(span: &Span<'_>) -> Option<Range<Position>> {
let at = |line: Option<u64>, column: Option<u64>| Position::new(clamped(line?), clamped(column?));
Some(at(span.line_start, span.column_start)?..at(span.line_end, span.column_end)?)
}
pub(super) fn covers(range: &Range<Position>, reported: &Range<Position>) -> bool {
range.start <= reported.start && range.end >= reported.end
}
fn overlaps(left: &Range<Position>, right: &Range<Position>) -> bool {
left.start < right.end && right.start < left.end
}