use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct LicenseInfo {
pub spdx_id: String,
pub name: String,
pub compatibility: LicenseCompatibility,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LicenseCompatibility {
Compatible,
CompatibleWithAttribution,
CompatibleCopyleft,
ReviewRequired,
Incompatible,
Unknown,
}
pub struct LicenseChecker {
project_license: String,
compatibility_matrix: HashMap<String, HashMap<String, LicenseCompatibility>>,
problematic_licenses: HashSet<String>,
}
impl LicenseChecker {
pub fn new(project_license: &str) -> Self {
let mut checker = Self {
project_license: project_license.to_string(),
compatibility_matrix: HashMap::new(),
problematic_licenses: HashSet::new(),
};
checker.init_compatibility_matrix();
checker.init_problematic_licenses();
checker
}
fn init_compatibility_matrix(&mut self) {
let mut apache_compat = HashMap::new();
apache_compat.insert("MIT".to_string(), LicenseCompatibility::Compatible);
apache_compat.insert("BSD-2-Clause".to_string(), LicenseCompatibility::Compatible);
apache_compat.insert("BSD-3-Clause".to_string(), LicenseCompatibility::Compatible);
apache_compat.insert("ISC".to_string(), LicenseCompatibility::Compatible);
apache_compat.insert("Apache-2.0".to_string(), LicenseCompatibility::Compatible);
apache_compat.insert("GPL-2.0".to_string(), LicenseCompatibility::Incompatible);
apache_compat.insert(
"GPL-3.0".to_string(),
LicenseCompatibility::CompatibleCopyleft,
);
apache_compat.insert(
"LGPL-2.1".to_string(),
LicenseCompatibility::CompatibleWithAttribution,
);
apache_compat.insert(
"LGPL-3.0".to_string(),
LicenseCompatibility::CompatibleWithAttribution,
);
apache_compat.insert(
"MPL-2.0".to_string(),
LicenseCompatibility::CompatibleWithAttribution,
);
self.compatibility_matrix
.insert("Apache-2.0".to_string(), apache_compat);
let mut mit_compat = HashMap::new();
mit_compat.insert("MIT".to_string(), LicenseCompatibility::Compatible);
mit_compat.insert("BSD-2-Clause".to_string(), LicenseCompatibility::Compatible);
mit_compat.insert("BSD-3-Clause".to_string(), LicenseCompatibility::Compatible);
mit_compat.insert("ISC".to_string(), LicenseCompatibility::Compatible);
mit_compat.insert("Apache-2.0".to_string(), LicenseCompatibility::Compatible);
mit_compat.insert("GPL-2.0".to_string(), LicenseCompatibility::Incompatible);
mit_compat.insert(
"GPL-3.0".to_string(),
LicenseCompatibility::CompatibleCopyleft,
);
mit_compat.insert(
"LGPL-2.1".to_string(),
LicenseCompatibility::CompatibleWithAttribution,
);
mit_compat.insert(
"LGPL-3.0".to_string(),
LicenseCompatibility::CompatibleWithAttribution,
);
mit_compat.insert(
"MPL-2.0".to_string(),
LicenseCompatibility::CompatibleWithAttribution,
);
self.compatibility_matrix
.insert("MIT".to_string(), mit_compat);
let mut gpl3_compat = HashMap::new();
gpl3_compat.insert("MIT".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("BSD-2-Clause".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("BSD-3-Clause".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("ISC".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("Apache-2.0".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("GPL-2.0".to_string(), LicenseCompatibility::Incompatible);
gpl3_compat.insert("GPL-3.0".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("LGPL-2.1".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("LGPL-3.0".to_string(), LicenseCompatibility::Compatible);
gpl3_compat.insert("MPL-2.0".to_string(), LicenseCompatibility::Compatible);
self.compatibility_matrix
.insert("GPL-3.0".to_string(), gpl3_compat);
}
fn init_problematic_licenses(&mut self) {
self.problematic_licenses.insert("AGPL-1.0".to_string());
self.problematic_licenses.insert("AGPL-3.0".to_string());
self.problematic_licenses.insert("GPL-2.0-only".to_string());
self.problematic_licenses.insert("SSPL-1.0".to_string());
self.problematic_licenses
.insert("Commons-Clause".to_string());
self.problematic_licenses.insert("BUSL-1.1".to_string());
}
pub fn check_compatibility(&self, dependency_license: &str) -> LicenseCompatibility {
if self.problematic_licenses.contains(dependency_license) {
return LicenseCompatibility::ReviewRequired;
}
if let Some(project_matrix) = self.compatibility_matrix.get(&self.project_license) {
project_matrix
.get(dependency_license)
.copied()
.unwrap_or(LicenseCompatibility::Unknown)
} else {
LicenseCompatibility::Unknown
}
}
pub fn generate_license_report(
&self,
dependencies: &HashMap<String, DependencyInfo>,
) -> LicenseReport {
let mut compatible = Vec::new();
let mut requires_attribution = Vec::new();
let mut copyleft = Vec::new();
let mut needs_review = Vec::new();
let mut incompatible = Vec::new();
let mut unknown = Vec::new();
for dep in dependencies.values() {
let dep_summary = LicenseDependencySummary {
name: dep.name.clone(),
version: dep.version.clone(),
license: dep.license.clone(),
};
match dep.license.compatibility {
LicenseCompatibility::Compatible => compatible.push(dep_summary),
LicenseCompatibility::CompatibleWithAttribution => {
requires_attribution.push(dep_summary)
}
LicenseCompatibility::CompatibleCopyleft => copyleft.push(dep_summary),
LicenseCompatibility::ReviewRequired => needs_review.push(dep_summary),
LicenseCompatibility::Incompatible => incompatible.push(dep_summary),
LicenseCompatibility::Unknown => unknown.push(dep_summary),
}
}
LicenseReport {
project_license: self.project_license.clone(),
compatible,
requires_attribution,
copyleft,
needs_review,
incompatible,
unknown,
}
}
pub fn generate_attribution_text(
&self,
dependencies: &HashMap<String, DependencyInfo>,
) -> String {
let mut attribution = String::new();
attribution.push_str("# Third-Party Licenses\n\n");
attribution.push_str("This software includes the following third-party components:\n\n");
for dep in dependencies.values() {
if matches!(
dep.license.compatibility,
LicenseCompatibility::CompatibleWithAttribution
| LicenseCompatibility::CompatibleCopyleft
) {
attribution.push_str(&format!(
"## {}\n\nVersion: {}\nLicense: {} ({})\n\n",
dep.name, dep.version, dep.license.name, dep.license.spdx_id
));
if !dep.license.notes.is_empty() {
attribution.push_str("Notes:\n");
for note in &dep.license.notes {
attribution.push_str(&format!("- {note}\n"));
}
attribution.push('\n');
}
}
}
attribution
}
}
#[derive(Debug, Clone)]
pub struct LicenseDependencySummary {
pub name: String,
pub version: String,
pub license: LicenseInfo,
}
#[derive(Debug, Clone)]
pub struct LicenseReport {
pub project_license: String,
pub compatible: Vec<LicenseDependencySummary>,
pub requires_attribution: Vec<LicenseDependencySummary>,
pub copyleft: Vec<LicenseDependencySummary>,
pub needs_review: Vec<LicenseDependencySummary>,
pub incompatible: Vec<LicenseDependencySummary>,
pub unknown: Vec<LicenseDependencySummary>,
}
impl LicenseReport {
pub fn has_issues(&self) -> bool {
!self.incompatible.is_empty() || !self.needs_review.is_empty() || !self.unknown.is_empty()
}
pub fn issue_summary(&self) -> String {
if !self.has_issues() {
return "No license compatibility issues found.".to_string();
}
let mut summary = String::new();
summary.push_str("License Compatibility Issues:\n");
if !self.incompatible.is_empty() {
summary.push_str(&format!(
"- {} incompatible dependencies\n",
self.incompatible.len()
));
}
if !self.needs_review.is_empty() {
summary.push_str(&format!(
"- {} dependencies need review\n",
self.needs_review.len()
));
}
if !self.unknown.is_empty() {
summary.push_str(&format!(
"- {} dependencies with unknown licenses\n",
self.unknown.len()
));
}
summary
}
pub fn detailed_report(&self) -> String {
let mut report = String::new();
report.push_str(&format!(
"License Compatibility Report (Project License: {})\n\n",
self.project_license
));
if !self.compatible.is_empty() {
report.push_str(&format!("✅ Compatible ({}):\n", self.compatible.len()));
for dep in &self.compatible {
report.push_str(&format!(
" - {} ({}) - {}\n",
dep.name, dep.version, dep.license.spdx_id
));
}
report.push('\n');
}
if !self.requires_attribution.is_empty() {
report.push_str(&format!(
"📝 Requires Attribution ({}):\n",
self.requires_attribution.len()
));
for dep in &self.requires_attribution {
report.push_str(&format!(
" - {} ({}) - {}\n",
dep.name, dep.version, dep.license.spdx_id
));
}
report.push('\n');
}
if !self.copyleft.is_empty() {
report.push_str(&format!("⚠️ Copyleft ({}):\n", self.copyleft.len()));
for dep in &self.copyleft {
report.push_str(&format!(
" - {} ({}) - {}\n",
dep.name, dep.version, dep.license.spdx_id
));
}
report.push('\n');
}
if !self.needs_review.is_empty() {
report.push_str(&format!("🔍 Needs Review ({}):\n", self.needs_review.len()));
for dep in &self.needs_review {
report.push_str(&format!(
" - {} ({}) - {}\n",
dep.name, dep.version, dep.license.spdx_id
));
}
report.push('\n');
}
if !self.incompatible.is_empty() {
report.push_str(&format!("❌ Incompatible ({}):\n", self.incompatible.len()));
for dep in &self.incompatible {
report.push_str(&format!(
" - {} ({}) - {}\n",
dep.name, dep.version, dep.license.spdx_id
));
}
report.push('\n');
}
if !self.unknown.is_empty() {
report.push_str(&format!("❓ Unknown ({}):\n", self.unknown.len()));
for dep in &self.unknown {
report.push_str(&format!(
" - {} ({}) - {}\n",
dep.name, dep.version, dep.license.spdx_id
));
}
report.push('\n');
}
report
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DependencyCategory {
Essential,
Optional,
Development,
Redundant,
Heavy,
Security,
}
#[derive(Debug, Clone)]
pub struct DependencyInfo {
pub name: String,
pub version: String,
pub category: DependencyCategory,
pub optional: bool,
pub features: Vec<String>,
pub compile_time_impact: CompileTimeImpact,
pub binary_size_impact: BinarySizeImpact,
pub use_case: String,
pub alternatives: Vec<String>,
pub transitive_deps: usize,
pub security_notes: Vec<String>,
pub license: LicenseInfo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CompileTimeImpact {
Minimal,
Low,
Medium,
High,
VeryHigh,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BinarySizeImpact {
Minimal,
Low,
Medium,
High,
VeryHigh,
}
pub struct DependencyAudit {
dependencies: HashMap<String, DependencyInfo>,
recommendations: Vec<DependencyRecommendation>,
}
impl DependencyAudit {
pub fn new() -> Self {
let mut audit = Self {
dependencies: HashMap::new(),
recommendations: Vec::new(),
};
audit.populate_current_dependencies();
audit.generate_recommendations();
audit
}
fn populate_current_dependencies(&mut self) {
let license_checker = LicenseChecker::new("Apache-2.0");
self.add_dependency(DependencyInfo {
name: "numrs2".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Essential,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Medium,
binary_size_impact: BinarySizeImpact::Medium,
use_case: "Numerical computing and array operations".to_string(),
alternatives: vec!["ndarray".to_string()],
transitive_deps: 15,
security_notes: vec!["Internal workspace dependency".to_string()],
license: LicenseInfo {
spdx_id: "Apache-2.0".to_string(),
name: "Apache License 2.0".to_string(),
compatibility: license_checker.check_compatibility("Apache-2.0"),
notes: vec!["Internal workspace dependency".to_string()],
},
});
self.add_dependency(DependencyInfo {
name: "scirs2".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Essential,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::High,
binary_size_impact: BinarySizeImpact::High,
use_case: "Scientific computing algorithms".to_string(),
alternatives: vec!["scirust".to_string()],
transitive_deps: 25,
security_notes: vec!["Internal workspace dependency".to_string()],
license: LicenseInfo {
spdx_id: "Apache-2.0".to_string(),
name: "Apache License 2.0".to_string(),
compatibility: license_checker.check_compatibility("Apache-2.0"),
notes: vec!["Internal workspace dependency".to_string()],
},
});
self.add_dependency(DependencyInfo {
name: "ndarray".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Essential,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Medium,
binary_size_impact: BinarySizeImpact::Medium,
use_case: "N-dimensional array support".to_string(),
alternatives: vec!["nalgebra".to_string()],
transitive_deps: 10,
security_notes: vec!["Well-maintained, widely used".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: license_checker.check_compatibility("MIT"),
notes: vec!["Permissive license".to_string()],
},
});
self.add_dependency(DependencyInfo {
name: "num-traits".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Essential,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Minimal,
binary_size_impact: BinarySizeImpact::Minimal,
use_case: "Numeric trait abstractions".to_string(),
alternatives: vec![],
transitive_deps: 2,
security_notes: vec!["Minimal, trait-only crate".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: license_checker.check_compatibility("MIT"),
notes: vec![],
},
});
self.add_dependency(DependencyInfo {
name: "thiserror".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Essential,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Low,
binary_size_impact: BinarySizeImpact::Minimal,
use_case: "Error handling macros".to_string(),
alternatives: vec!["anyhow".to_string(), "manual impl".to_string()],
transitive_deps: 3,
security_notes: vec!["Proc-macro only, minimal runtime".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: license_checker.check_compatibility("MIT"),
notes: vec![],
},
});
self.add_dependency(DependencyInfo {
name: "serde".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Optional,
optional: true,
features: vec!["derive".to_string()],
compile_time_impact: CompileTimeImpact::Medium,
binary_size_impact: BinarySizeImpact::Low,
use_case: "Serialization support".to_string(),
alternatives: vec!["bincode".to_string(), "manual".to_string()],
transitive_deps: 8,
security_notes: vec!["Popular, well-audited".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: license_checker.check_compatibility("MIT"),
notes: vec![],
},
});
self.add_dependency(DependencyInfo {
name: "rayon".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Essential,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Medium,
binary_size_impact: BinarySizeImpact::Medium,
use_case: "Parallel processing".to_string(),
alternatives: vec!["std::thread".to_string(), "tokio".to_string()],
transitive_deps: 12,
security_notes: vec!["Well-maintained, thread-safe".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: license_checker.check_compatibility("MIT"),
notes: vec![],
},
});
self.add_dependency(DependencyInfo {
name: "heavy-test-dep".to_string(),
version: "1.0".to_string(),
category: DependencyCategory::Heavy,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::VeryHigh,
binary_size_impact: BinarySizeImpact::VeryHigh,
use_case: "Test heavy dependency for recommendations".to_string(),
alternatives: vec!["lighter-alternative".to_string()],
transitive_deps: 40,
security_notes: vec!["Heavy test dependency".to_string()],
license: LicenseInfo {
spdx_id: "GPL-2.0".to_string(),
name: "GNU General Public License v2.0".to_string(),
compatibility: license_checker.check_compatibility("GPL-2.0"),
notes: vec!["Copyleft license - may require legal review".to_string()],
},
});
self.add_dependency(DependencyInfo {
name: "arrow".to_string(),
version: "53".to_string(),
category: DependencyCategory::Heavy,
optional: true,
features: vec![],
compile_time_impact: CompileTimeImpact::High,
binary_size_impact: BinarySizeImpact::High,
use_case: "Columnar data format".to_string(),
alternatives: vec!["custom format".to_string()],
transitive_deps: 30,
security_notes: vec!["Apache project, actively maintained".to_string()],
license: LicenseInfo {
spdx_id: "Apache-2.0".to_string(),
name: "Apache License 2.0".to_string(),
compatibility: license_checker.check_compatibility("Apache-2.0"),
notes: vec!["Apache Software Foundation project".to_string()],
},
});
self.add_dependency(DependencyInfo {
name: "proc-macro2".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Redundant,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Low,
binary_size_impact: BinarySizeImpact::Minimal,
use_case: "Proc macro support".to_string(),
alternatives: vec!["remove macros".to_string()],
transitive_deps: 5,
security_notes: vec!["May not be directly needed".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: LicenseCompatibility::Compatible,
notes: vec![],
},
});
self.add_dependency(DependencyInfo {
name: "quote".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Redundant,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Low,
binary_size_impact: BinarySizeImpact::Minimal,
use_case: "Quote tokens for proc macros".to_string(),
alternatives: vec!["remove macros".to_string()],
transitive_deps: 3,
security_notes: vec!["May not be directly needed".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: LicenseCompatibility::Compatible,
notes: vec![],
},
});
self.add_dependency(DependencyInfo {
name: "syn".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Redundant,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Medium,
binary_size_impact: BinarySizeImpact::Low,
use_case: "Parse Rust syntax for proc macros".to_string(),
alternatives: vec!["remove macros".to_string()],
transitive_deps: 10,
security_notes: vec!["May not be directly needed".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: LicenseCompatibility::Compatible,
notes: vec![],
},
});
self.add_dependency(DependencyInfo {
name: "criterion".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Development,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::High,
binary_size_impact: BinarySizeImpact::Medium,
use_case: "Benchmarking".to_string(),
alternatives: vec!["manual timing".to_string()],
transitive_deps: 20,
security_notes: vec!["Development only".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: LicenseCompatibility::Compatible,
notes: vec!["Development dependency".to_string()],
},
});
self.add_dependency(DependencyInfo {
name: "proptest".to_string(),
version: "workspace".to_string(),
category: DependencyCategory::Development,
optional: false,
features: vec![],
compile_time_impact: CompileTimeImpact::Medium,
binary_size_impact: BinarySizeImpact::Low,
use_case: "Property-based testing".to_string(),
alternatives: vec!["quickcheck".to_string(), "manual tests".to_string()],
transitive_deps: 15,
security_notes: vec!["Development only".to_string()],
license: LicenseInfo {
spdx_id: "MIT".to_string(),
name: "MIT License".to_string(),
compatibility: LicenseCompatibility::Compatible,
notes: vec!["Development dependency".to_string()],
},
});
}
fn add_dependency(&mut self, dep: DependencyInfo) {
self.dependencies.insert(dep.name.clone(), dep);
}
fn generate_recommendations(&mut self) {
for dep in self.dependencies.values() {
if dep.category == DependencyCategory::Heavy && !dep.optional {
self.recommendations.push(DependencyRecommendation {
dependency: dep.name.clone(),
action: RecommendationAction::MakeOptional,
reason: "Large dependency should be feature-gated".to_string(),
impact: RecommendationImpact::High,
effort: ImplementationEffort::Low,
});
}
if dep.category == DependencyCategory::Redundant {
self.recommendations.push(DependencyRecommendation {
dependency: dep.name.clone(),
action: RecommendationAction::Remove,
reason: "Dependency may not be directly used".to_string(),
impact: RecommendationImpact::Medium,
effort: ImplementationEffort::Medium,
});
}
if dep.compile_time_impact >= CompileTimeImpact::High && dep.optional {
self.recommendations.push(DependencyRecommendation {
dependency: dep.name.clone(),
action: RecommendationAction::OptimizeFeatures,
reason: "High compile time impact should use minimal features".to_string(),
impact: RecommendationImpact::Medium,
effort: ImplementationEffort::Low,
});
}
}
self.recommendations.push(DependencyRecommendation {
dependency: "multiple".to_string(),
action: RecommendationAction::ConsolidateAlternatives,
reason: "Multiple proc-macro dependencies could be consolidated".to_string(),
impact: RecommendationImpact::Low,
effort: ImplementationEffort::High,
});
}
pub fn dependencies(&self) -> &HashMap<String, DependencyInfo> {
&self.dependencies
}
pub fn dependencies_by_category(&self, category: DependencyCategory) -> Vec<&DependencyInfo> {
self.dependencies
.values()
.filter(|dep| dep.category == category)
.collect()
}
pub fn recommendations(&self) -> &[DependencyRecommendation] {
&self.recommendations
}
pub fn generate_report(&self) -> DependencyReport {
let total_deps = self.dependencies.len();
let optional_deps = self.dependencies.values().filter(|d| d.optional).count();
let essential_deps = self
.dependencies_by_category(DependencyCategory::Essential)
.len();
let heavy_deps = self
.dependencies_by_category(DependencyCategory::Heavy)
.len();
let total_transitive = self.dependencies.values().map(|d| d.transitive_deps).sum();
let high_impact_recommendations = self
.recommendations
.iter()
.filter(|r| r.impact >= RecommendationImpact::High)
.count();
DependencyReport {
total_dependencies: total_deps,
optional_dependencies: optional_deps,
essential_dependencies: essential_deps,
heavy_dependencies: heavy_deps,
total_transitive_dependencies: total_transitive,
high_impact_recommendations,
recommendations: self.recommendations.clone(),
dependency_breakdown: self.generate_breakdown(),
}
}
fn generate_breakdown(&self) -> HashMap<DependencyCategory, Vec<String>> {
let mut breakdown = HashMap::new();
for dep in self.dependencies.values() {
breakdown
.entry(dep.category)
.or_insert_with(Vec::new)
.push(dep.name.clone());
}
breakdown
}
}
impl Default for DependencyAudit {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DependencyRecommendation {
pub dependency: String,
pub action: RecommendationAction,
pub reason: String,
pub impact: RecommendationImpact,
pub effort: ImplementationEffort,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecommendationAction {
Remove,
MakeOptional,
OptimizeFeatures,
ReplaceWithAlternative(String),
ConsolidateAlternatives,
UpdateVersion,
MoveToDevDeps,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RecommendationImpact {
Low,
Medium,
High,
VeryHigh,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ImplementationEffort {
Low,
Medium,
High,
VeryHigh,
}
#[derive(Debug, Clone)]
pub struct DependencyReport {
pub total_dependencies: usize,
pub optional_dependencies: usize,
pub essential_dependencies: usize,
pub heavy_dependencies: usize,
pub total_transitive_dependencies: usize,
pub high_impact_recommendations: usize,
pub recommendations: Vec<DependencyRecommendation>,
pub dependency_breakdown: HashMap<DependencyCategory, Vec<String>>,
}
impl DependencyReport {
pub fn summary(&self) -> String {
format!(
"Dependency Audit Summary:\n\
- Total dependencies: {}\n\
- Essential: {}\n\
- Optional: {}\n\
- Heavy: {}\n\
- Estimated transitive deps: {}\n\
- High-impact recommendations: {}",
self.total_dependencies,
self.essential_dependencies,
self.optional_dependencies,
self.heavy_dependencies,
self.total_transitive_dependencies,
self.high_impact_recommendations
)
}
pub fn detailed_recommendations(&self) -> String {
let mut output = String::new();
output.push_str("Dependency Optimization Recommendations:\n\n");
for (i, rec) in self.recommendations.iter().enumerate() {
output.push_str(&format!(
"{}. {} ({})\n\
Action: {:?}\n\
Reason: {}\n\
Impact: {:?}, Effort: {:?}\n\n",
i + 1,
rec.dependency,
match rec.impact {
RecommendationImpact::VeryHigh => "🔴 Very High",
RecommendationImpact::High => "🟡 High",
RecommendationImpact::Medium => "🟠 Medium",
RecommendationImpact::Low => "🟢 Low",
},
rec.action,
rec.reason,
rec.impact,
rec.effort
));
}
output
}
pub fn generate_cargo_optimizations(&self) -> String {
let mut optimizations = String::new();
optimizations.push_str("# Recommended Cargo.toml optimizations:\n\n");
optimizations.push_str("# Feature-gate heavy dependencies:\n");
optimizations.push_str("[dependencies]\n");
optimizations
.push_str("arrow = { version = \"53\", optional = true, default-features = false }\n");
optimizations.push_str("arrow-ipc = { version = \"53\", optional = true }\n");
optimizations.push_str("arrow-csv = { version = \"53\", optional = true }\n\n");
optimizations.push_str("# Minimize features for heavy dependencies:\n");
optimizations.push_str("[features]\n");
optimizations.push_str("arrow = [\"dep:arrow\", \"dep:arrow-ipc\", \"dep:arrow-csv\"]\n");
optimizations.push_str("full = [\"dataframes\", \"arrow\", \"serde\"]\n\n");
optimizations.push_str("# Profile optimizations:\n");
optimizations.push_str("[profile.dev]\n");
optimizations.push_str("opt-level = 1 # Faster dev builds\n\n");
optimizations.push_str("[profile.release]\n");
optimizations.push_str("codegen-units = 1 # Better optimization\n");
optimizations.push_str("lto = true # Link-time optimization\n");
optimizations
}
}
pub fn calculate_metrics(audit: &DependencyAudit) -> DependencyMetrics {
let deps = audit.dependencies();
let total_compile_time: u32 = deps
.values()
.map(|d| match d.compile_time_impact {
CompileTimeImpact::Minimal => 1,
CompileTimeImpact::Low => 3,
CompileTimeImpact::Medium => 10,
CompileTimeImpact::High => 20,
CompileTimeImpact::VeryHigh => 40,
})
.sum();
let total_binary_size: u32 = deps
.values()
.map(|d| match d.binary_size_impact {
BinarySizeImpact::Minimal => 1,
BinarySizeImpact::Low => 5,
BinarySizeImpact::Medium => 15,
BinarySizeImpact::High => 50,
BinarySizeImpact::VeryHigh => 200,
})
.sum();
DependencyMetrics {
estimated_compile_time_seconds: total_compile_time,
estimated_binary_size_mb: total_binary_size,
dependency_depth: deps.values().map(|d| d.transitive_deps).max().unwrap_or(0),
optimization_potential: audit.recommendations().len(),
}
}
#[derive(Debug, Clone)]
pub struct DependencyMetrics {
pub estimated_compile_time_seconds: u32,
pub estimated_binary_size_mb: u32,
pub dependency_depth: usize,
pub optimization_potential: usize,
}
pub fn generate_dependency_graph(audit: &DependencyAudit) -> String {
let mut graph = String::new();
graph.push_str("digraph dependencies {\n");
graph.push_str(" rankdir=TB;\n");
graph.push_str(" node [shape=box];\n\n");
for dep in audit.dependencies().values() {
let color = match dep.category {
DependencyCategory::Essential => "lightblue",
DependencyCategory::Optional => "lightgreen",
DependencyCategory::Development => "lightyellow",
DependencyCategory::Heavy => "lightcoral",
DependencyCategory::Redundant => "lightgray",
DependencyCategory::Security => "pink",
};
graph.push_str(&format!(
" \"{}\" [fillcolor={}, style=filled];\n",
dep.name, color
));
}
graph.push_str("}\n");
graph
}
#[derive(Debug)]
pub struct DependencyUpdater {
config: UpdaterConfig,
current_versions: HashMap<String, String>,
latest_versions: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct UpdaterConfig {
pub allow_major_updates: bool,
pub allow_minor_updates: bool,
pub allow_patch_updates: bool,
pub excluded_packages: HashSet<String>,
pub version_constraints: HashMap<String, String>,
pub check_security_advisories: bool,
}
impl Default for UpdaterConfig {
fn default() -> Self {
Self {
allow_major_updates: false,
allow_minor_updates: true,
allow_patch_updates: true,
excluded_packages: HashSet::new(),
version_constraints: HashMap::new(),
check_security_advisories: true,
}
}
}
#[derive(Debug, Clone)]
pub struct UpdateRecommendation {
pub package_name: String,
pub current_version: String,
pub latest_version: String,
pub update_type: UpdateType,
pub security_advisory: Option<SecurityAdvisory>,
pub breaking_changes: Vec<String>,
pub priority: UpdatePriority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateType {
Major,
Minor,
Patch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum UpdatePriority {
Critical, High, Medium, Low, }
#[derive(Debug, Clone)]
pub struct SecurityAdvisory {
pub id: String,
pub title: String,
pub severity: String,
pub affected_versions: String,
pub patched_versions: String,
pub description: String,
}
impl DependencyUpdater {
pub fn new() -> Self {
Self::with_config(UpdaterConfig::default())
}
pub fn with_config(config: UpdaterConfig) -> Self {
Self {
config,
current_versions: HashMap::new(),
latest_versions: HashMap::new(),
}
}
pub fn check_for_updates(&mut self) -> Result<Vec<UpdateRecommendation>, String> {
self.load_current_dependencies()?;
self.fetch_latest_versions()?;
let mut recommendations = Vec::new();
for (package, current_version) in &self.current_versions {
if self.config.excluded_packages.contains(package) {
continue;
}
if let Some(latest_version) = self.latest_versions.get(package) {
if let Some(recommendation) =
self.analyze_update(package, current_version, latest_version)?
{
recommendations.push(recommendation);
}
}
}
recommendations.sort_by_key(|r| std::cmp::Reverse(r.priority));
Ok(recommendations)
}
pub fn generate_update_script(&self, recommendations: &[UpdateRecommendation]) -> String {
let mut script = String::new();
script.push_str("#!/bin/bash\n");
script.push_str("# Automated dependency update script generated by sklears\n\n");
for rec in recommendations {
if self.should_auto_update(rec) {
script.push_str(&format!(
"echo \"Updating {} from {} to {}\"\n",
rec.package_name, rec.current_version, rec.latest_version
));
script.push_str(&format!(
"cargo update -p {}:{}\n",
rec.package_name, rec.latest_version
));
} else {
script.push_str(&format!(
"# Manual review required for {}: {} -> {} ({})\n",
rec.package_name,
rec.current_version,
rec.latest_version,
match rec.update_type {
UpdateType::Major => "major version change",
UpdateType::Minor => "minor version change",
UpdateType::Patch => "patch version change",
}
));
}
}
script.push_str("\necho \"Update process complete. Running tests...\"\n");
script.push_str("cargo test\n");
script.push_str("cargo clippy -- -D warnings\n");
script
}
fn load_current_dependencies(&mut self) -> Result<(), String> {
self.current_versions
.insert("ndarray".to_string(), "0.15.6".to_string());
self.current_versions
.insert("serde".to_string(), "1.0.193".to_string());
self.current_versions
.insert("rayon".to_string(), "1.8.0".to_string());
self.current_versions
.insert("criterion".to_string(), "0.5.1".to_string());
Ok(())
}
fn fetch_latest_versions(&mut self) -> Result<(), String> {
self.latest_versions
.insert("ndarray".to_string(), "0.15.7".to_string());
self.latest_versions
.insert("serde".to_string(), "1.0.195".to_string());
self.latest_versions
.insert("rayon".to_string(), "1.8.1".to_string());
self.latest_versions
.insert("criterion".to_string(), "0.5.1".to_string());
Ok(())
}
fn analyze_update(
&self,
package: &str,
current: &str,
latest: &str,
) -> Result<Option<UpdateRecommendation>, String> {
if current == latest {
return Ok(None);
}
let update_type = self.determine_update_type(current, latest)?;
let allowed = match update_type {
UpdateType::Major => self.config.allow_major_updates,
UpdateType::Minor => self.config.allow_minor_updates,
UpdateType::Patch => self.config.allow_patch_updates,
};
if !allowed {
return Ok(None);
}
let security_advisory = self.check_security_advisory(package, current);
let priority = if security_advisory.is_some() {
UpdatePriority::Critical
} else {
match update_type {
UpdateType::Major => UpdatePriority::Low,
UpdateType::Minor => UpdatePriority::Medium,
UpdateType::Patch => UpdatePriority::High,
}
};
Ok(Some(UpdateRecommendation {
package_name: package.to_string(),
current_version: current.to_string(),
latest_version: latest.to_string(),
update_type,
security_advisory,
breaking_changes: self.get_breaking_changes(package, current, latest),
priority,
}))
}
fn determine_update_type(&self, current: &str, latest: &str) -> Result<UpdateType, String> {
let current_parts: Vec<u32> = current.split('.').map(|s| s.parse().unwrap_or(0)).collect();
let latest_parts: Vec<u32> = latest.split('.').map(|s| s.parse().unwrap_or(0)).collect();
if current_parts.len() < 3 || latest_parts.len() < 3 {
return Err("Invalid version format".to_string());
}
if latest_parts[0] > current_parts[0] {
Ok(UpdateType::Major)
} else if latest_parts[1] > current_parts[1] {
Ok(UpdateType::Minor)
} else {
Ok(UpdateType::Patch)
}
}
fn check_security_advisory(&self, package: &str, version: &str) -> Option<SecurityAdvisory> {
if package == "serde" && version == "1.0.100" {
Some(SecurityAdvisory {
id: "RUSTSEC-2023-0001".to_string(),
title: "Simulated security vulnerability".to_string(),
severity: "Medium".to_string(),
affected_versions: "< 1.0.150".to_string(),
patched_versions: ">= 1.0.150".to_string(),
description: "This is a simulated security advisory for demonstration".to_string(),
})
} else {
None
}
}
fn get_breaking_changes(&self, _package: &str, _current: &str, _latest: &str) -> Vec<String> {
Vec::new()
}
fn should_auto_update(&self, recommendation: &UpdateRecommendation) -> bool {
match recommendation.update_type {
UpdateType::Major => false, UpdateType::Minor => recommendation.breaking_changes.is_empty(),
UpdateType::Patch => true,
}
}
pub fn generate_update_report(&self, recommendations: &[UpdateRecommendation]) -> String {
let mut report = String::new();
report.push_str("Dependency Update Report\n");
report.push_str("========================\n\n");
let critical_count = recommendations
.iter()
.filter(|r| r.priority == UpdatePriority::Critical)
.count();
let high_count = recommendations
.iter()
.filter(|r| r.priority == UpdatePriority::High)
.count();
let medium_count = recommendations
.iter()
.filter(|r| r.priority == UpdatePriority::Medium)
.count();
let low_count = recommendations
.iter()
.filter(|r| r.priority == UpdatePriority::Low)
.count();
report.push_str("Summary:\n");
report.push_str(&format!("- Critical updates: {critical_count}\n"));
report.push_str(&format!("- High priority updates: {high_count}\n"));
report.push_str(&format!("- Medium priority updates: {medium_count}\n"));
report.push_str(&format!("- Low priority updates: {low_count}\n\n"));
for rec in recommendations {
report.push_str(&format!("Package: {}\n", rec.package_name));
report.push_str(&format!("Current Version: {}\n", rec.current_version));
report.push_str(&format!("Latest Version: {}\n", rec.latest_version));
report.push_str(&format!("Update Type: {:?}\n", rec.update_type));
report.push_str(&format!("Priority: {:?}\n", rec.priority));
if let Some(ref advisory) = rec.security_advisory {
report.push_str(&format!(
"⚠️ Security Advisory: {} - {}\n",
advisory.id, advisory.title
));
report.push_str(&format!(" Severity: {}\n", advisory.severity));
}
if !rec.breaking_changes.is_empty() {
report.push_str("Breaking Changes:\n");
for change in &rec.breaking_changes {
report.push_str(&format!(" - {change}\n"));
}
}
report.push('\n');
}
report
}
}
impl Default for DependencyUpdater {
fn default() -> Self {
Self::new()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dependency_audit_creation() {
let audit = DependencyAudit::new();
assert!(!audit.dependencies().is_empty());
assert!(!audit.recommendations().is_empty());
}
#[test]
fn test_dependency_categories() {
let audit = DependencyAudit::new();
let essential = audit.dependencies_by_category(DependencyCategory::Essential);
let optional = audit.dependencies_by_category(DependencyCategory::Optional);
let heavy = audit.dependencies_by_category(DependencyCategory::Heavy);
assert!(!essential.is_empty());
assert!(!optional.is_empty());
assert!(!heavy.is_empty());
}
#[test]
fn test_report_generation() {
let audit = DependencyAudit::new();
let report = audit.generate_report();
assert!(report.total_dependencies > 0);
assert!(report.essential_dependencies > 0);
assert!(!report.summary().is_empty());
assert!(!report.detailed_recommendations().is_empty());
}
#[test]
fn test_metrics_calculation() {
let audit = DependencyAudit::new();
let metrics = calculate_metrics(&audit);
assert!(metrics.estimated_compile_time_seconds > 0);
assert!(metrics.estimated_binary_size_mb > 0);
assert!(metrics.dependency_depth > 0);
}
#[test]
fn test_cargo_optimizations() {
let audit = DependencyAudit::new();
let report = audit.generate_report();
let optimizations = report.generate_cargo_optimizations();
assert!(optimizations.contains("optional = true"));
assert!(optimizations.contains("[features]"));
assert!(optimizations.contains("[profile"));
}
#[test]
fn test_dependency_graph() {
let audit = DependencyAudit::new();
let graph = generate_dependency_graph(&audit);
assert!(graph.contains("digraph dependencies"));
assert!(graph.contains("lightblue")); assert!(graph.contains("lightcoral")); }
#[test]
fn test_recommendations() {
let audit = DependencyAudit::new();
let recommendations = audit.recommendations();
assert!(recommendations
.iter()
.any(|r| matches!(r.action, RecommendationAction::MakeOptional)));
assert!(recommendations
.iter()
.any(|r| matches!(r.action, RecommendationAction::Remove)));
}
#[test]
fn test_license_checker() {
let checker = LicenseChecker::new("Apache-2.0");
assert_eq!(
checker.check_compatibility("MIT"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("BSD-3-Clause"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("Apache-2.0"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("GPL-2.0"),
LicenseCompatibility::Incompatible
);
assert_eq!(
checker.check_compatibility("LGPL-2.1"),
LicenseCompatibility::CompatibleWithAttribution
);
assert_eq!(
checker.check_compatibility("MPL-2.0"),
LicenseCompatibility::CompatibleWithAttribution
);
assert_eq!(
checker.check_compatibility("GPL-3.0"),
LicenseCompatibility::CompatibleCopyleft
);
assert_eq!(
checker.check_compatibility("UNKNOWN-LICENSE"),
LicenseCompatibility::Unknown
);
assert_eq!(
checker.check_compatibility("AGPL-3.0"),
LicenseCompatibility::ReviewRequired
);
}
#[test]
fn test_license_report_generation() {
let audit = DependencyAudit::new();
let checker = LicenseChecker::new("Apache-2.0");
let license_report = checker.generate_license_report(audit.dependencies());
assert!(!license_report.compatible.is_empty());
let has_issues = license_report.has_issues();
let detailed = license_report.detailed_report();
assert!(detailed.contains("License Compatibility Report"));
if has_issues {
let summary = license_report.issue_summary();
assert!(summary.contains("License Compatibility Issues"));
}
}
#[test]
fn test_attribution_text_generation() {
let audit = DependencyAudit::new();
let checker = LicenseChecker::new("Apache-2.0");
let attribution = checker.generate_attribution_text(audit.dependencies());
assert!(attribution.contains("Third-Party Licenses"));
assert!(attribution.contains("This software includes"));
}
#[test]
fn test_mit_license_compatibility() {
let checker = LicenseChecker::new("MIT");
assert_eq!(
checker.check_compatibility("MIT"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("BSD-2-Clause"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("Apache-2.0"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("GPL-2.0"),
LicenseCompatibility::Incompatible
);
}
#[test]
fn test_dependency_updater_creation() {
let updater = DependencyUpdater::new();
assert!(updater.config.allow_minor_updates);
assert!(!updater.config.allow_major_updates);
assert!(updater.config.allow_patch_updates);
}
#[test]
fn test_update_recommendations() {
let mut updater = DependencyUpdater::new();
let recommendations = updater
.check_for_updates()
.expect("check_for_updates should succeed");
assert!(!recommendations.is_empty());
for i in 1..recommendations.len() {
assert!(recommendations[i - 1].priority >= recommendations[i].priority);
}
}
#[test]
fn test_update_script_generation() {
let mut updater = DependencyUpdater::new();
let recommendations = updater
.check_for_updates()
.expect("check_for_updates should succeed");
let script = updater.generate_update_script(&recommendations);
assert!(script.contains("#!/bin/bash"));
assert!(script.contains("cargo update"));
assert!(script.contains("cargo test"));
}
#[test]
fn test_update_report_generation() {
let mut updater = DependencyUpdater::new();
let recommendations = updater
.check_for_updates()
.expect("check_for_updates should succeed");
let report = updater.generate_update_report(&recommendations);
assert!(report.contains("Dependency Update Report"));
assert!(report.contains("Summary:"));
assert!(report.contains("Package:"));
}
#[test]
fn test_update_type_determination() {
let updater = DependencyUpdater::new();
assert_eq!(
updater
.determine_update_type("1.0.0", "2.0.0")
.expect("determine_update_type should succeed"),
UpdateType::Major
);
assert_eq!(
updater
.determine_update_type("1.0.0", "1.1.0")
.expect("determine_update_type should succeed"),
UpdateType::Minor
);
assert_eq!(
updater
.determine_update_type("1.0.0", "1.0.1")
.expect("determine_update_type should succeed"),
UpdateType::Patch
);
}
#[test]
fn test_gpl3_license_compatibility() {
let checker = LicenseChecker::new("GPL-3.0");
assert_eq!(
checker.check_compatibility("MIT"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("Apache-2.0"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("GPL-3.0"),
LicenseCompatibility::Compatible
);
assert_eq!(
checker.check_compatibility("GPL-2.0"),
LicenseCompatibility::Incompatible
);
}
}