use std::collections::BTreeMap;
use cargo_lock::{Dependency, Lockfile, Name, Package, SourceId};
use crate::Violation;
pub fn compute_violations(baseline: &Lockfile, current: &Lockfile) -> Vec<Violation> {
let baseline_packages = highest_version_packages(&baseline.packages);
let current_packages = highest_version_packages(¤t.packages);
let mut violations = Vec::new();
for (name, baseline_package) in &baseline_packages {
let Some(current_package) = current_packages.get(name).copied() else {
continue;
};
if current_package.version < baseline_package.version {
violations.push(Violation::PackageVersionRegressed {
name: name.to_string(),
baseline_version: baseline_package.version.clone(),
current_version: current_package.version.clone(),
baseline_source: baseline_package.source.as_ref().map(ToString::to_string),
current_source: current_package.source.as_ref().map(ToString::to_string),
});
}
let baseline_deps = highest_version_registry_deps(baseline_package);
let current_deps = highest_version_registry_deps(current_package);
for (dep_name, baseline_dep) in &baseline_deps {
let Some(current_dep) = current_deps.get(dep_name).copied() else {
continue;
};
if current_dep.version < baseline_dep.version {
violations.push(Violation::DependencyEdgeRegressed {
package_name: name.to_string(),
baseline_package_version: baseline_package.version.clone(),
current_package_version: current_package.version.clone(),
dependency_name: dep_name.to_string(),
baseline_dependency_version: baseline_dep.version.clone(),
current_dependency_version: current_dep.version.clone(),
});
}
}
}
violations
}
fn highest_version_packages(packages: &[Package]) -> BTreeMap<&Name, &Package> {
let mut by_name: BTreeMap<&Name, &Package> = BTreeMap::new();
for package in packages {
by_name
.entry(&package.name)
.and_modify(|highest| {
if package.version > highest.version {
*highest = package;
}
})
.or_insert(package);
}
by_name
}
fn highest_version_registry_deps(package: &Package) -> BTreeMap<&Name, &Dependency> {
let mut by_name: BTreeMap<&Name, &Dependency> = BTreeMap::new();
for dep in package
.dependencies
.iter()
.filter(|dep| is_default_registry_dep(dep))
{
by_name
.entry(&dep.name)
.and_modify(|highest| {
if dep.version > highest.version {
*highest = dep;
}
})
.or_insert(dep);
}
by_name
}
fn is_default_registry_dep(dep: &Dependency) -> bool {
dep.source
.as_ref()
.is_some_and(SourceId::is_default_registry)
}