use crate::config::FootprintThresholds;
use cargo_metadata::{DependencyKind, Metadata, Package, PackageId};
use std::collections::{BTreeMap, HashSet};
pub fn estimate_footprint(
package_id: &PackageId,
metadata: &Metadata,
thresholds: &FootprintThresholds,
) -> (f32, Vec<String>) {
let mut warnings = Vec::new();
let transitive_count = count_transitive_deps(package_id, metadata);
let package = metadata.packages.iter()
.find(|p| &p.id == package_id);
let mut footprint_score = 0.0;
let dep_score = calculate_dep_count_score(transitive_count);
footprint_score += dep_score * 0.4;
if let Some(pkg) = package {
let feature_score = calculate_feature_score(&pkg.features);
footprint_score += feature_score * 0.3;
let build_dep_score = calculate_build_dep_score(pkg);
footprint_score += build_dep_score * 0.3;
}
if let Some(max_transitive) = thresholds.max_transitive_deps {
if transitive_count > max_transitive {
warnings.push(format!(
"High number of transitive dependencies: {} (threshold: {})",
transitive_count, max_transitive
));
}
}
if let Some(max_footprint) = thresholds.max_footprint_risk {
if footprint_score > max_footprint {
warnings.push(format!(
"High footprint risk: {:.2} (threshold: {:.2})",
footprint_score, max_footprint
));
}
}
(footprint_score, warnings)
}
fn count_transitive_deps(package_id: &PackageId, metadata: &Metadata) -> u32 {
let Some(resolve) = &metadata.resolve else {
return 0;
};
let mut visited = HashSet::new();
let mut to_visit = vec![package_id.clone()];
while let Some(current_id) = to_visit.pop() {
if !visited.insert(current_id.clone()) {
continue;
}
if let Some(node) = resolve.nodes.iter().find(|n| n.id == current_id) {
for dep in &node.deps {
to_visit.push(dep.pkg.clone());
}
}
}
visited.len().saturating_sub(1) as u32
}
fn calculate_dep_count_score(count: u32) -> f32 {
match count {
0..=5 => 0.1,
6..=10 => 0.2,
11..=20 => 0.4,
21..=50 => 0.6,
51..=100 => 0.8,
_ => 1.0,
}
}
fn calculate_feature_score(features: &BTreeMap<String, Vec<String>>) -> f32 {
let feature_count = features.len();
match feature_count {
0..=3 => 0.1,
4..=8 => 0.3,
9..=15 => 0.5,
16..=30 => 0.7,
_ => 1.0,
}
}
fn calculate_build_dep_score(package: &Package) -> f32 {
let build_deps_count = package.dependencies.iter()
.filter(|dep| matches!(dep.kind, DependencyKind::Build))
.count();
match build_deps_count {
0 => 0.0,
1..=2 => 0.3,
3..=5 => 0.6,
_ => 1.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dep_count_score() {
assert!(calculate_dep_count_score(3) < 0.2);
assert!(calculate_dep_count_score(15) > 0.3);
assert!(calculate_dep_count_score(150) > 0.9);
}
#[test]
fn test_feature_score() {
let mut features = BTreeMap::new();
assert!(calculate_feature_score(&features) < 0.2);
for i in 0..10 {
features.insert(format!("feature{}", i), vec![]);
}
assert!(calculate_feature_score(&features) > 0.4);
}
}