1use std::collections::{HashMap, HashSet};
25
26#[derive(Debug, Clone)]
28pub struct LicenseInfo {
29 pub spdx_id: String,
31 pub name: String,
33 pub compatibility: LicenseCompatibility,
35 pub notes: Vec<String>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
41pub enum LicenseCompatibility {
42 Compatible,
44 CompatibleWithAttribution,
46 CompatibleCopyleft,
48 ReviewRequired,
50 Incompatible,
52 Unknown,
54}
55
56pub struct LicenseChecker {
58 project_license: String,
60 compatibility_matrix: HashMap<String, HashMap<String, LicenseCompatibility>>,
62 problematic_licenses: HashSet<String>,
64}
65
66impl LicenseChecker {
67 pub fn new(project_license: &str) -> Self {
69 let mut checker = Self {
70 project_license: project_license.to_string(),
71 compatibility_matrix: HashMap::new(),
72 problematic_licenses: HashSet::new(),
73 };
74 checker.init_compatibility_matrix();
75 checker.init_problematic_licenses();
76 checker
77 }
78
79 fn init_compatibility_matrix(&mut self) {
81 let mut apache_compat = HashMap::new();
83 apache_compat.insert("MIT".to_string(), LicenseCompatibility::Compatible);
84 apache_compat.insert("BSD-2-Clause".to_string(), LicenseCompatibility::Compatible);
85 apache_compat.insert("BSD-3-Clause".to_string(), LicenseCompatibility::Compatible);
86 apache_compat.insert("ISC".to_string(), LicenseCompatibility::Compatible);
87 apache_compat.insert("Apache-2.0".to_string(), LicenseCompatibility::Compatible);
88 apache_compat.insert("GPL-2.0".to_string(), LicenseCompatibility::Incompatible);
89 apache_compat.insert(
90 "GPL-3.0".to_string(),
91 LicenseCompatibility::CompatibleCopyleft,
92 );
93 apache_compat.insert(
94 "LGPL-2.1".to_string(),
95 LicenseCompatibility::CompatibleWithAttribution,
96 );
97 apache_compat.insert(
98 "LGPL-3.0".to_string(),
99 LicenseCompatibility::CompatibleWithAttribution,
100 );
101 apache_compat.insert(
102 "MPL-2.0".to_string(),
103 LicenseCompatibility::CompatibleWithAttribution,
104 );
105 self.compatibility_matrix
106 .insert("Apache-2.0".to_string(), apache_compat);
107
108 let mut mit_compat = HashMap::new();
110 mit_compat.insert("MIT".to_string(), LicenseCompatibility::Compatible);
111 mit_compat.insert("BSD-2-Clause".to_string(), LicenseCompatibility::Compatible);
112 mit_compat.insert("BSD-3-Clause".to_string(), LicenseCompatibility::Compatible);
113 mit_compat.insert("ISC".to_string(), LicenseCompatibility::Compatible);
114 mit_compat.insert("Apache-2.0".to_string(), LicenseCompatibility::Compatible);
115 mit_compat.insert("GPL-2.0".to_string(), LicenseCompatibility::Incompatible);
116 mit_compat.insert(
117 "GPL-3.0".to_string(),
118 LicenseCompatibility::CompatibleCopyleft,
119 );
120 mit_compat.insert(
121 "LGPL-2.1".to_string(),
122 LicenseCompatibility::CompatibleWithAttribution,
123 );
124 mit_compat.insert(
125 "LGPL-3.0".to_string(),
126 LicenseCompatibility::CompatibleWithAttribution,
127 );
128 mit_compat.insert(
129 "MPL-2.0".to_string(),
130 LicenseCompatibility::CompatibleWithAttribution,
131 );
132 self.compatibility_matrix
133 .insert("MIT".to_string(), mit_compat);
134
135 let mut gpl3_compat = HashMap::new();
137 gpl3_compat.insert("MIT".to_string(), LicenseCompatibility::Compatible);
138 gpl3_compat.insert("BSD-2-Clause".to_string(), LicenseCompatibility::Compatible);
139 gpl3_compat.insert("BSD-3-Clause".to_string(), LicenseCompatibility::Compatible);
140 gpl3_compat.insert("ISC".to_string(), LicenseCompatibility::Compatible);
141 gpl3_compat.insert("Apache-2.0".to_string(), LicenseCompatibility::Compatible);
142 gpl3_compat.insert("GPL-2.0".to_string(), LicenseCompatibility::Incompatible);
143 gpl3_compat.insert("GPL-3.0".to_string(), LicenseCompatibility::Compatible);
144 gpl3_compat.insert("LGPL-2.1".to_string(), LicenseCompatibility::Compatible);
145 gpl3_compat.insert("LGPL-3.0".to_string(), LicenseCompatibility::Compatible);
146 gpl3_compat.insert("MPL-2.0".to_string(), LicenseCompatibility::Compatible);
147 self.compatibility_matrix
148 .insert("GPL-3.0".to_string(), gpl3_compat);
149 }
150
151 fn init_problematic_licenses(&mut self) {
153 self.problematic_licenses.insert("AGPL-1.0".to_string());
154 self.problematic_licenses.insert("AGPL-3.0".to_string());
155 self.problematic_licenses.insert("GPL-2.0-only".to_string());
156 self.problematic_licenses.insert("SSPL-1.0".to_string());
157 self.problematic_licenses
158 .insert("Commons-Clause".to_string());
159 self.problematic_licenses.insert("BUSL-1.1".to_string());
160 }
161
162 pub fn check_compatibility(&self, dependency_license: &str) -> LicenseCompatibility {
164 if self.problematic_licenses.contains(dependency_license) {
166 return LicenseCompatibility::ReviewRequired;
167 }
168
169 if let Some(project_matrix) = self.compatibility_matrix.get(&self.project_license) {
171 project_matrix
172 .get(dependency_license)
173 .copied()
174 .unwrap_or(LicenseCompatibility::Unknown)
175 } else {
176 LicenseCompatibility::Unknown
177 }
178 }
179
180 pub fn generate_license_report(
182 &self,
183 dependencies: &HashMap<String, DependencyInfo>,
184 ) -> LicenseReport {
185 let mut compatible = Vec::new();
186 let mut requires_attribution = Vec::new();
187 let mut copyleft = Vec::new();
188 let mut needs_review = Vec::new();
189 let mut incompatible = Vec::new();
190 let mut unknown = Vec::new();
191
192 for dep in dependencies.values() {
193 let dep_summary = LicenseDependencySummary {
194 name: dep.name.clone(),
195 version: dep.version.clone(),
196 license: dep.license.clone(),
197 };
198
199 match dep.license.compatibility {
200 LicenseCompatibility::Compatible => compatible.push(dep_summary),
201 LicenseCompatibility::CompatibleWithAttribution => {
202 requires_attribution.push(dep_summary)
203 }
204 LicenseCompatibility::CompatibleCopyleft => copyleft.push(dep_summary),
205 LicenseCompatibility::ReviewRequired => needs_review.push(dep_summary),
206 LicenseCompatibility::Incompatible => incompatible.push(dep_summary),
207 LicenseCompatibility::Unknown => unknown.push(dep_summary),
208 }
209 }
210
211 LicenseReport {
212 project_license: self.project_license.clone(),
213 compatible,
214 requires_attribution,
215 copyleft,
216 needs_review,
217 incompatible,
218 unknown,
219 }
220 }
221
222 pub fn generate_attribution_text(
224 &self,
225 dependencies: &HashMap<String, DependencyInfo>,
226 ) -> String {
227 let mut attribution = String::new();
228 attribution.push_str("# Third-Party Licenses\n\n");
229 attribution.push_str("This software includes the following third-party components:\n\n");
230
231 for dep in dependencies.values() {
232 if matches!(
233 dep.license.compatibility,
234 LicenseCompatibility::CompatibleWithAttribution
235 | LicenseCompatibility::CompatibleCopyleft
236 ) {
237 attribution.push_str(&format!(
238 "## {}\n\nVersion: {}\nLicense: {} ({})\n\n",
239 dep.name, dep.version, dep.license.name, dep.license.spdx_id
240 ));
241
242 if !dep.license.notes.is_empty() {
243 attribution.push_str("Notes:\n");
244 for note in &dep.license.notes {
245 attribution.push_str(&format!("- {note}\n"));
246 }
247 attribution.push('\n');
248 }
249 }
250 }
251
252 attribution
253 }
254}
255
256#[derive(Debug, Clone)]
258pub struct LicenseDependencySummary {
259 pub name: String,
260 pub version: String,
261 pub license: LicenseInfo,
262}
263
264#[derive(Debug, Clone)]
266pub struct LicenseReport {
267 pub project_license: String,
268 pub compatible: Vec<LicenseDependencySummary>,
269 pub requires_attribution: Vec<LicenseDependencySummary>,
270 pub copyleft: Vec<LicenseDependencySummary>,
271 pub needs_review: Vec<LicenseDependencySummary>,
272 pub incompatible: Vec<LicenseDependencySummary>,
273 pub unknown: Vec<LicenseDependencySummary>,
274}
275
276impl LicenseReport {
277 pub fn has_issues(&self) -> bool {
279 !self.incompatible.is_empty() || !self.needs_review.is_empty() || !self.unknown.is_empty()
280 }
281
282 pub fn issue_summary(&self) -> String {
284 if !self.has_issues() {
285 return "No license compatibility issues found.".to_string();
286 }
287
288 let mut summary = String::new();
289 summary.push_str("License Compatibility Issues:\n");
290
291 if !self.incompatible.is_empty() {
292 summary.push_str(&format!(
293 "- {} incompatible dependencies\n",
294 self.incompatible.len()
295 ));
296 }
297
298 if !self.needs_review.is_empty() {
299 summary.push_str(&format!(
300 "- {} dependencies need review\n",
301 self.needs_review.len()
302 ));
303 }
304
305 if !self.unknown.is_empty() {
306 summary.push_str(&format!(
307 "- {} dependencies with unknown licenses\n",
308 self.unknown.len()
309 ));
310 }
311
312 summary
313 }
314
315 pub fn detailed_report(&self) -> String {
317 let mut report = String::new();
318 report.push_str(&format!(
319 "License Compatibility Report (Project License: {})\n\n",
320 self.project_license
321 ));
322
323 if !self.compatible.is_empty() {
324 report.push_str(&format!("✅ Compatible ({}):\n", self.compatible.len()));
325 for dep in &self.compatible {
326 report.push_str(&format!(
327 " - {} ({}) - {}\n",
328 dep.name, dep.version, dep.license.spdx_id
329 ));
330 }
331 report.push('\n');
332 }
333
334 if !self.requires_attribution.is_empty() {
335 report.push_str(&format!(
336 "📝 Requires Attribution ({}):\n",
337 self.requires_attribution.len()
338 ));
339 for dep in &self.requires_attribution {
340 report.push_str(&format!(
341 " - {} ({}) - {}\n",
342 dep.name, dep.version, dep.license.spdx_id
343 ));
344 }
345 report.push('\n');
346 }
347
348 if !self.copyleft.is_empty() {
349 report.push_str(&format!("⚠️ Copyleft ({}):\n", self.copyleft.len()));
350 for dep in &self.copyleft {
351 report.push_str(&format!(
352 " - {} ({}) - {}\n",
353 dep.name, dep.version, dep.license.spdx_id
354 ));
355 }
356 report.push('\n');
357 }
358
359 if !self.needs_review.is_empty() {
360 report.push_str(&format!("🔍 Needs Review ({}):\n", self.needs_review.len()));
361 for dep in &self.needs_review {
362 report.push_str(&format!(
363 " - {} ({}) - {}\n",
364 dep.name, dep.version, dep.license.spdx_id
365 ));
366 }
367 report.push('\n');
368 }
369
370 if !self.incompatible.is_empty() {
371 report.push_str(&format!("❌ Incompatible ({}):\n", self.incompatible.len()));
372 for dep in &self.incompatible {
373 report.push_str(&format!(
374 " - {} ({}) - {}\n",
375 dep.name, dep.version, dep.license.spdx_id
376 ));
377 }
378 report.push('\n');
379 }
380
381 if !self.unknown.is_empty() {
382 report.push_str(&format!("❓ Unknown ({}):\n", self.unknown.len()));
383 for dep in &self.unknown {
384 report.push_str(&format!(
385 " - {} ({}) - {}\n",
386 dep.name, dep.version, dep.license.spdx_id
387 ));
388 }
389 report.push('\n');
390 }
391
392 report
393 }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
402pub enum DependencyCategory {
403 Essential,
405 Optional,
407 Development,
409 Redundant,
411 Heavy,
413 Security,
415}
416
417#[derive(Debug, Clone)]
419pub struct DependencyInfo {
420 pub name: String,
422 pub version: String,
424 pub category: DependencyCategory,
426 pub optional: bool,
428 pub features: Vec<String>,
430 pub compile_time_impact: CompileTimeImpact,
432 pub binary_size_impact: BinarySizeImpact,
434 pub use_case: String,
436 pub alternatives: Vec<String>,
438 pub transitive_deps: usize,
440 pub security_notes: Vec<String>,
442 pub license: LicenseInfo,
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
448pub enum CompileTimeImpact {
449 Minimal,
450 Low,
451 Medium,
452 High,
453 VeryHigh,
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
458pub enum BinarySizeImpact {
459 Minimal,
461 Low,
463 Medium,
465 High,
467 VeryHigh,
469}
470
471pub struct DependencyAudit {
477 dependencies: HashMap<String, DependencyInfo>,
478 recommendations: Vec<DependencyRecommendation>,
479}
480
481impl DependencyAudit {
482 pub fn new() -> Self {
484 let mut audit = Self {
485 dependencies: HashMap::new(),
486 recommendations: Vec::new(),
487 };
488 audit.populate_current_dependencies();
489 audit.generate_recommendations();
490 audit
491 }
492
493 fn populate_current_dependencies(&mut self) {
495 let license_checker = LicenseChecker::new("Apache-2.0");
496
497 self.add_dependency(DependencyInfo {
499 name: "numrs2".to_string(),
500 version: "workspace".to_string(),
501 category: DependencyCategory::Essential,
502 optional: false,
503 features: vec![],
504 compile_time_impact: CompileTimeImpact::Medium,
505 binary_size_impact: BinarySizeImpact::Medium,
506 use_case: "Numerical computing and array operations".to_string(),
507 alternatives: vec!["ndarray".to_string()],
508 transitive_deps: 15,
509 security_notes: vec!["Internal workspace dependency".to_string()],
510 license: LicenseInfo {
511 spdx_id: "Apache-2.0".to_string(),
512 name: "Apache License 2.0".to_string(),
513 compatibility: license_checker.check_compatibility("Apache-2.0"),
514 notes: vec!["Internal workspace dependency".to_string()],
515 },
516 });
517
518 self.add_dependency(DependencyInfo {
519 name: "scirs2".to_string(),
520 version: "workspace".to_string(),
521 category: DependencyCategory::Essential,
522 optional: false,
523 features: vec![],
524 compile_time_impact: CompileTimeImpact::High,
525 binary_size_impact: BinarySizeImpact::High,
526 use_case: "Scientific computing algorithms".to_string(),
527 alternatives: vec!["scirust".to_string()],
528 transitive_deps: 25,
529 security_notes: vec!["Internal workspace dependency".to_string()],
530 license: LicenseInfo {
531 spdx_id: "Apache-2.0".to_string(),
532 name: "Apache License 2.0".to_string(),
533 compatibility: license_checker.check_compatibility("Apache-2.0"),
534 notes: vec!["Internal workspace dependency".to_string()],
535 },
536 });
537
538 self.add_dependency(DependencyInfo {
539 name: "ndarray".to_string(),
540 version: "workspace".to_string(),
541 category: DependencyCategory::Essential,
542 optional: false,
543 features: vec![],
544 compile_time_impact: CompileTimeImpact::Medium,
545 binary_size_impact: BinarySizeImpact::Medium,
546 use_case: "N-dimensional array support".to_string(),
547 alternatives: vec!["nalgebra".to_string()],
548 transitive_deps: 10,
549 security_notes: vec!["Well-maintained, widely used".to_string()],
550 license: LicenseInfo {
551 spdx_id: "MIT".to_string(),
552 name: "MIT License".to_string(),
553 compatibility: license_checker.check_compatibility("MIT"),
554 notes: vec!["Permissive license".to_string()],
555 },
556 });
557
558 self.add_dependency(DependencyInfo {
559 name: "num-traits".to_string(),
560 version: "workspace".to_string(),
561 category: DependencyCategory::Essential,
562 optional: false,
563 features: vec![],
564 compile_time_impact: CompileTimeImpact::Minimal,
565 binary_size_impact: BinarySizeImpact::Minimal,
566 use_case: "Numeric trait abstractions".to_string(),
567 alternatives: vec![],
568 transitive_deps: 2,
569 security_notes: vec!["Minimal, trait-only crate".to_string()],
570 license: LicenseInfo {
571 spdx_id: "MIT".to_string(),
572 name: "MIT License".to_string(),
573 compatibility: license_checker.check_compatibility("MIT"),
574 notes: vec![],
575 },
576 });
577
578 self.add_dependency(DependencyInfo {
579 name: "thiserror".to_string(),
580 version: "workspace".to_string(),
581 category: DependencyCategory::Essential,
582 optional: false,
583 features: vec![],
584 compile_time_impact: CompileTimeImpact::Low,
585 binary_size_impact: BinarySizeImpact::Minimal,
586 use_case: "Error handling macros".to_string(),
587 alternatives: vec!["anyhow".to_string(), "manual impl".to_string()],
588 transitive_deps: 3,
589 security_notes: vec!["Proc-macro only, minimal runtime".to_string()],
590 license: LicenseInfo {
591 spdx_id: "MIT".to_string(),
592 name: "MIT License".to_string(),
593 compatibility: license_checker.check_compatibility("MIT"),
594 notes: vec![],
595 },
596 });
597
598 self.add_dependency(DependencyInfo {
600 name: "serde".to_string(),
601 version: "workspace".to_string(),
602 category: DependencyCategory::Optional,
603 optional: true,
604 features: vec!["derive".to_string()],
605 compile_time_impact: CompileTimeImpact::Medium,
606 binary_size_impact: BinarySizeImpact::Low,
607 use_case: "Serialization support".to_string(),
608 alternatives: vec!["bincode".to_string(), "manual".to_string()],
609 transitive_deps: 8,
610 security_notes: vec!["Popular, well-audited".to_string()],
611 license: LicenseInfo {
612 spdx_id: "MIT".to_string(),
613 name: "MIT License".to_string(),
614 compatibility: license_checker.check_compatibility("MIT"),
615 notes: vec![],
616 },
617 });
618
619 self.add_dependency(DependencyInfo {
620 name: "rayon".to_string(),
621 version: "workspace".to_string(),
622 category: DependencyCategory::Essential,
623 optional: false,
624 features: vec![],
625 compile_time_impact: CompileTimeImpact::Medium,
626 binary_size_impact: BinarySizeImpact::Medium,
627 use_case: "Parallel processing".to_string(),
628 alternatives: vec!["std::thread".to_string(), "tokio".to_string()],
629 transitive_deps: 12,
630 security_notes: vec!["Well-maintained, thread-safe".to_string()],
631 license: LicenseInfo {
632 spdx_id: "MIT".to_string(),
633 name: "MIT License".to_string(),
634 compatibility: license_checker.check_compatibility("MIT"),
635 notes: vec![],
636 },
637 });
638
639 self.add_dependency(DependencyInfo {
641 name: "heavy-test-dep".to_string(),
642 version: "1.0".to_string(),
643 category: DependencyCategory::Heavy,
644 optional: false,
645 features: vec![],
646 compile_time_impact: CompileTimeImpact::VeryHigh,
647 binary_size_impact: BinarySizeImpact::VeryHigh,
648 use_case: "Test heavy dependency for recommendations".to_string(),
649 alternatives: vec!["lighter-alternative".to_string()],
650 transitive_deps: 40,
651 security_notes: vec!["Heavy test dependency".to_string()],
652 license: LicenseInfo {
653 spdx_id: "GPL-2.0".to_string(),
654 name: "GNU General Public License v2.0".to_string(),
655 compatibility: license_checker.check_compatibility("GPL-2.0"),
656 notes: vec!["Copyleft license - may require legal review".to_string()],
657 },
658 });
659
660 self.add_dependency(DependencyInfo {
661 name: "polars".to_string(),
662 version: "0.42".to_string(),
663 category: DependencyCategory::Heavy,
664 optional: true,
665 features: vec!["lazy".to_string()],
666 compile_time_impact: CompileTimeImpact::VeryHigh,
667 binary_size_impact: BinarySizeImpact::VeryHigh,
668 use_case: "DataFrame operations".to_string(),
669 alternatives: vec!["custom impl".to_string(), "arrow".to_string()],
670 transitive_deps: 50,
671 security_notes: vec!["Large dependency tree".to_string()],
672 license: LicenseInfo {
673 spdx_id: "MIT".to_string(),
674 name: "MIT License".to_string(),
675 compatibility: license_checker.check_compatibility("MIT"),
676 notes: vec!["Large transitive dependency tree".to_string()],
677 },
678 });
679
680 self.add_dependency(DependencyInfo {
681 name: "arrow".to_string(),
682 version: "53".to_string(),
683 category: DependencyCategory::Heavy,
684 optional: true,
685 features: vec![],
686 compile_time_impact: CompileTimeImpact::High,
687 binary_size_impact: BinarySizeImpact::High,
688 use_case: "Columnar data format".to_string(),
689 alternatives: vec!["custom format".to_string()],
690 transitive_deps: 30,
691 security_notes: vec!["Apache project, actively maintained".to_string()],
692 license: LicenseInfo {
693 spdx_id: "Apache-2.0".to_string(),
694 name: "Apache License 2.0".to_string(),
695 compatibility: license_checker.check_compatibility("Apache-2.0"),
696 notes: vec!["Apache Software Foundation project".to_string()],
697 },
698 });
699
700 self.add_dependency(DependencyInfo {
702 name: "proc-macro2".to_string(),
703 version: "workspace".to_string(),
704 category: DependencyCategory::Redundant,
705 optional: false,
706 features: vec![],
707 compile_time_impact: CompileTimeImpact::Low,
708 binary_size_impact: BinarySizeImpact::Minimal,
709 use_case: "Proc macro support".to_string(),
710 alternatives: vec!["remove macros".to_string()],
711 transitive_deps: 5,
712 security_notes: vec!["May not be directly needed".to_string()],
713 license: LicenseInfo {
714 spdx_id: "MIT".to_string(),
715 name: "MIT License".to_string(),
716 compatibility: LicenseCompatibility::Compatible,
717 notes: vec![],
718 },
719 });
720
721 self.add_dependency(DependencyInfo {
722 name: "quote".to_string(),
723 version: "workspace".to_string(),
724 category: DependencyCategory::Redundant,
725 optional: false,
726 features: vec![],
727 compile_time_impact: CompileTimeImpact::Low,
728 binary_size_impact: BinarySizeImpact::Minimal,
729 use_case: "Quote tokens for proc macros".to_string(),
730 alternatives: vec!["remove macros".to_string()],
731 transitive_deps: 3,
732 security_notes: vec!["May not be directly needed".to_string()],
733 license: LicenseInfo {
734 spdx_id: "MIT".to_string(),
735 name: "MIT License".to_string(),
736 compatibility: LicenseCompatibility::Compatible,
737 notes: vec![],
738 },
739 });
740
741 self.add_dependency(DependencyInfo {
742 name: "syn".to_string(),
743 version: "workspace".to_string(),
744 category: DependencyCategory::Redundant,
745 optional: false,
746 features: vec![],
747 compile_time_impact: CompileTimeImpact::Medium,
748 binary_size_impact: BinarySizeImpact::Low,
749 use_case: "Parse Rust syntax for proc macros".to_string(),
750 alternatives: vec!["remove macros".to_string()],
751 transitive_deps: 10,
752 security_notes: vec!["May not be directly needed".to_string()],
753 license: LicenseInfo {
754 spdx_id: "MIT".to_string(),
755 name: "MIT License".to_string(),
756 compatibility: LicenseCompatibility::Compatible,
757 notes: vec![],
758 },
759 });
760
761 self.add_dependency(DependencyInfo {
763 name: "criterion".to_string(),
764 version: "workspace".to_string(),
765 category: DependencyCategory::Development,
766 optional: false,
767 features: vec![],
768 compile_time_impact: CompileTimeImpact::High,
769 binary_size_impact: BinarySizeImpact::Medium,
770 use_case: "Benchmarking".to_string(),
771 alternatives: vec!["manual timing".to_string()],
772 transitive_deps: 20,
773 security_notes: vec!["Development only".to_string()],
774 license: LicenseInfo {
775 spdx_id: "MIT".to_string(),
776 name: "MIT License".to_string(),
777 compatibility: LicenseCompatibility::Compatible,
778 notes: vec!["Development dependency".to_string()],
779 },
780 });
781
782 self.add_dependency(DependencyInfo {
783 name: "proptest".to_string(),
784 version: "workspace".to_string(),
785 category: DependencyCategory::Development,
786 optional: false,
787 features: vec![],
788 compile_time_impact: CompileTimeImpact::Medium,
789 binary_size_impact: BinarySizeImpact::Low,
790 use_case: "Property-based testing".to_string(),
791 alternatives: vec!["quickcheck".to_string(), "manual tests".to_string()],
792 transitive_deps: 15,
793 security_notes: vec!["Development only".to_string()],
794 license: LicenseInfo {
795 spdx_id: "MIT".to_string(),
796 name: "MIT License".to_string(),
797 compatibility: LicenseCompatibility::Compatible,
798 notes: vec!["Development dependency".to_string()],
799 },
800 });
801 }
802
803 fn add_dependency(&mut self, dep: DependencyInfo) {
805 self.dependencies.insert(dep.name.clone(), dep);
806 }
807
808 fn generate_recommendations(&mut self) {
810 for dep in self.dependencies.values() {
812 if dep.category == DependencyCategory::Heavy && !dep.optional {
813 self.recommendations.push(DependencyRecommendation {
814 dependency: dep.name.clone(),
815 action: RecommendationAction::MakeOptional,
816 reason: "Large dependency should be feature-gated".to_string(),
817 impact: RecommendationImpact::High,
818 effort: ImplementationEffort::Low,
819 });
820 }
821
822 if dep.category == DependencyCategory::Redundant {
823 self.recommendations.push(DependencyRecommendation {
824 dependency: dep.name.clone(),
825 action: RecommendationAction::Remove,
826 reason: "Dependency may not be directly used".to_string(),
827 impact: RecommendationImpact::Medium,
828 effort: ImplementationEffort::Medium,
829 });
830 }
831
832 if dep.compile_time_impact >= CompileTimeImpact::High && dep.optional {
833 self.recommendations.push(DependencyRecommendation {
834 dependency: dep.name.clone(),
835 action: RecommendationAction::OptimizeFeatures,
836 reason: "High compile time impact should use minimal features".to_string(),
837 impact: RecommendationImpact::Medium,
838 effort: ImplementationEffort::Low,
839 });
840 }
841 }
842
843 self.recommendations.push(DependencyRecommendation {
845 dependency: "multiple".to_string(),
846 action: RecommendationAction::ConsolidateAlternatives,
847 reason: "Multiple proc-macro dependencies could be consolidated".to_string(),
848 impact: RecommendationImpact::Low,
849 effort: ImplementationEffort::High,
850 });
851 }
852
853 pub fn dependencies(&self) -> &HashMap<String, DependencyInfo> {
855 &self.dependencies
856 }
857
858 pub fn dependencies_by_category(&self, category: DependencyCategory) -> Vec<&DependencyInfo> {
860 self.dependencies
861 .values()
862 .filter(|dep| dep.category == category)
863 .collect()
864 }
865
866 pub fn recommendations(&self) -> &[DependencyRecommendation] {
868 &self.recommendations
869 }
870
871 pub fn generate_report(&self) -> DependencyReport {
873 let total_deps = self.dependencies.len();
874 let optional_deps = self.dependencies.values().filter(|d| d.optional).count();
875 let essential_deps = self
876 .dependencies_by_category(DependencyCategory::Essential)
877 .len();
878 let heavy_deps = self
879 .dependencies_by_category(DependencyCategory::Heavy)
880 .len();
881
882 let total_transitive = self.dependencies.values().map(|d| d.transitive_deps).sum();
883
884 let high_impact_recommendations = self
885 .recommendations
886 .iter()
887 .filter(|r| r.impact >= RecommendationImpact::High)
888 .count();
889
890 DependencyReport {
891 total_dependencies: total_deps,
892 optional_dependencies: optional_deps,
893 essential_dependencies: essential_deps,
894 heavy_dependencies: heavy_deps,
895 total_transitive_dependencies: total_transitive,
896 high_impact_recommendations,
897 recommendations: self.recommendations.clone(),
898 dependency_breakdown: self.generate_breakdown(),
899 }
900 }
901
902 fn generate_breakdown(&self) -> HashMap<DependencyCategory, Vec<String>> {
904 let mut breakdown = HashMap::new();
905
906 for dep in self.dependencies.values() {
907 breakdown
908 .entry(dep.category)
909 .or_insert_with(Vec::new)
910 .push(dep.name.clone());
911 }
912
913 breakdown
914 }
915}
916
917impl Default for DependencyAudit {
918 fn default() -> Self {
919 Self::new()
920 }
921}
922
923#[derive(Debug, Clone)]
929pub struct DependencyRecommendation {
930 pub dependency: String,
932 pub action: RecommendationAction,
934 pub reason: String,
936 pub impact: RecommendationImpact,
938 pub effort: ImplementationEffort,
940}
941
942#[derive(Debug, Clone, PartialEq, Eq)]
944pub enum RecommendationAction {
945 Remove,
947 MakeOptional,
949 OptimizeFeatures,
951 ReplaceWithAlternative(String),
953 ConsolidateAlternatives,
955 UpdateVersion,
957 MoveToDevDeps,
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
963pub enum RecommendationImpact {
964 Low,
966 Medium,
968 High,
970 VeryHigh,
972}
973
974#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
976pub enum ImplementationEffort {
977 Low,
979 Medium,
981 High,
983 VeryHigh,
985}
986
987#[derive(Debug, Clone)]
993pub struct DependencyReport {
994 pub total_dependencies: usize,
996 pub optional_dependencies: usize,
998 pub essential_dependencies: usize,
1000 pub heavy_dependencies: usize,
1002 pub total_transitive_dependencies: usize,
1004 pub high_impact_recommendations: usize,
1006 pub recommendations: Vec<DependencyRecommendation>,
1008 pub dependency_breakdown: HashMap<DependencyCategory, Vec<String>>,
1010}
1011
1012impl DependencyReport {
1013 pub fn summary(&self) -> String {
1015 format!(
1016 "Dependency Audit Summary:\n\
1017 - Total dependencies: {}\n\
1018 - Essential: {}\n\
1019 - Optional: {}\n\
1020 - Heavy: {}\n\
1021 - Estimated transitive deps: {}\n\
1022 - High-impact recommendations: {}",
1023 self.total_dependencies,
1024 self.essential_dependencies,
1025 self.optional_dependencies,
1026 self.heavy_dependencies,
1027 self.total_transitive_dependencies,
1028 self.high_impact_recommendations
1029 )
1030 }
1031
1032 pub fn detailed_recommendations(&self) -> String {
1034 let mut output = String::new();
1035 output.push_str("Dependency Optimization Recommendations:\n\n");
1036
1037 for (i, rec) in self.recommendations.iter().enumerate() {
1038 output.push_str(&format!(
1039 "{}. {} ({})\n\
1040 Action: {:?}\n\
1041 Reason: {}\n\
1042 Impact: {:?}, Effort: {:?}\n\n",
1043 i + 1,
1044 rec.dependency,
1045 match rec.impact {
1046 RecommendationImpact::VeryHigh => "🔴 Very High",
1047 RecommendationImpact::High => "🟡 High",
1048 RecommendationImpact::Medium => "🟠 Medium",
1049 RecommendationImpact::Low => "🟢 Low",
1050 },
1051 rec.action,
1052 rec.reason,
1053 rec.impact,
1054 rec.effort
1055 ));
1056 }
1057
1058 output
1059 }
1060
1061 pub fn generate_cargo_optimizations(&self) -> String {
1063 let mut optimizations = String::new();
1064 optimizations.push_str("# Recommended Cargo.toml optimizations:\n\n");
1065
1066 optimizations.push_str("# Feature-gate heavy dependencies:\n");
1067 optimizations.push_str("[dependencies]\n");
1068 optimizations.push_str("polars = { version = \"0.42\", optional = true, default-features = false, features = [\"lazy\"] }\n");
1069 optimizations
1070 .push_str("arrow = { version = \"53\", optional = true, default-features = false }\n");
1071 optimizations.push_str("arrow-ipc = { version = \"53\", optional = true }\n");
1072 optimizations.push_str("arrow-csv = { version = \"53\", optional = true }\n\n");
1073
1074 optimizations.push_str("# Minimize features for heavy dependencies:\n");
1075 optimizations.push_str("[features]\n");
1076 optimizations.push_str("dataframes = [\"polars\"]\n");
1077 optimizations.push_str("arrow = [\"dep:arrow\", \"dep:arrow-ipc\", \"dep:arrow-csv\"]\n");
1078 optimizations.push_str("full = [\"dataframes\", \"arrow\", \"serde\"]\n\n");
1079
1080 optimizations.push_str("# Profile optimizations:\n");
1081 optimizations.push_str("[profile.dev]\n");
1082 optimizations.push_str("opt-level = 1 # Faster dev builds\n\n");
1083
1084 optimizations.push_str("[profile.release]\n");
1085 optimizations.push_str("codegen-units = 1 # Better optimization\n");
1086 optimizations.push_str("lto = true # Link-time optimization\n");
1087
1088 optimizations
1089 }
1090}
1091
1092pub fn calculate_metrics(audit: &DependencyAudit) -> DependencyMetrics {
1098 let deps = audit.dependencies();
1099
1100 let total_compile_time: u32 = deps
1101 .values()
1102 .map(|d| match d.compile_time_impact {
1103 CompileTimeImpact::Minimal => 1,
1104 CompileTimeImpact::Low => 3,
1105 CompileTimeImpact::Medium => 10,
1106 CompileTimeImpact::High => 20,
1107 CompileTimeImpact::VeryHigh => 40,
1108 })
1109 .sum();
1110
1111 let total_binary_size: u32 = deps
1112 .values()
1113 .map(|d| match d.binary_size_impact {
1114 BinarySizeImpact::Minimal => 1,
1115 BinarySizeImpact::Low => 5,
1116 BinarySizeImpact::Medium => 15,
1117 BinarySizeImpact::High => 50,
1118 BinarySizeImpact::VeryHigh => 200,
1119 })
1120 .sum();
1121
1122 DependencyMetrics {
1123 estimated_compile_time_seconds: total_compile_time,
1124 estimated_binary_size_mb: total_binary_size,
1125 dependency_depth: deps.values().map(|d| d.transitive_deps).max().unwrap_or(0),
1126 optimization_potential: audit.recommendations().len(),
1127 }
1128}
1129
1130#[derive(Debug, Clone)]
1132pub struct DependencyMetrics {
1133 pub estimated_compile_time_seconds: u32,
1135 pub estimated_binary_size_mb: u32,
1137 pub dependency_depth: usize,
1139 pub optimization_potential: usize,
1141}
1142
1143pub fn generate_dependency_graph(audit: &DependencyAudit) -> String {
1145 let mut graph = String::new();
1146 graph.push_str("digraph dependencies {\n");
1147 graph.push_str(" rankdir=TB;\n");
1148 graph.push_str(" node [shape=box];\n\n");
1149
1150 for dep in audit.dependencies().values() {
1152 let color = match dep.category {
1153 DependencyCategory::Essential => "lightblue",
1154 DependencyCategory::Optional => "lightgreen",
1155 DependencyCategory::Development => "lightyellow",
1156 DependencyCategory::Heavy => "lightcoral",
1157 DependencyCategory::Redundant => "lightgray",
1158 DependencyCategory::Security => "pink",
1159 };
1160
1161 graph.push_str(&format!(
1162 " \"{}\" [fillcolor={}, style=filled];\n",
1163 dep.name, color
1164 ));
1165 }
1166
1167 graph.push_str("}\n");
1168 graph
1169}
1170
1171#[derive(Debug)]
1177pub struct DependencyUpdater {
1178 config: UpdaterConfig,
1179 current_versions: HashMap<String, String>,
1180 latest_versions: HashMap<String, String>,
1181}
1182
1183#[derive(Debug, Clone)]
1185pub struct UpdaterConfig {
1186 pub allow_major_updates: bool,
1188 pub allow_minor_updates: bool,
1190 pub allow_patch_updates: bool,
1192 pub excluded_packages: HashSet<String>,
1194 pub version_constraints: HashMap<String, String>,
1196 pub check_security_advisories: bool,
1198}
1199
1200impl Default for UpdaterConfig {
1201 fn default() -> Self {
1202 Self {
1203 allow_major_updates: false,
1204 allow_minor_updates: true,
1205 allow_patch_updates: true,
1206 excluded_packages: HashSet::new(),
1207 version_constraints: HashMap::new(),
1208 check_security_advisories: true,
1209 }
1210 }
1211}
1212
1213#[derive(Debug, Clone)]
1215pub struct UpdateRecommendation {
1216 pub package_name: String,
1217 pub current_version: String,
1218 pub latest_version: String,
1219 pub update_type: UpdateType,
1220 pub security_advisory: Option<SecurityAdvisory>,
1221 pub breaking_changes: Vec<String>,
1222 pub priority: UpdatePriority,
1223}
1224
1225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1227pub enum UpdateType {
1228 Major,
1229 Minor,
1230 Patch,
1231}
1232
1233#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1235pub enum UpdatePriority {
1236 Critical, High, Medium, Low, }
1241
1242#[derive(Debug, Clone)]
1244pub struct SecurityAdvisory {
1245 pub id: String,
1246 pub title: String,
1247 pub severity: String,
1248 pub affected_versions: String,
1249 pub patched_versions: String,
1250 pub description: String,
1251}
1252
1253impl DependencyUpdater {
1254 pub fn new() -> Self {
1256 Self::with_config(UpdaterConfig::default())
1257 }
1258
1259 pub fn with_config(config: UpdaterConfig) -> Self {
1261 Self {
1262 config,
1263 current_versions: HashMap::new(),
1264 latest_versions: HashMap::new(),
1265 }
1266 }
1267
1268 pub fn check_for_updates(&mut self) -> Result<Vec<UpdateRecommendation>, String> {
1270 self.load_current_dependencies()?;
1272
1273 self.fetch_latest_versions()?;
1275
1276 let mut recommendations = Vec::new();
1278
1279 for (package, current_version) in &self.current_versions {
1280 if self.config.excluded_packages.contains(package) {
1281 continue;
1282 }
1283
1284 if let Some(latest_version) = self.latest_versions.get(package) {
1285 if let Some(recommendation) =
1286 self.analyze_update(package, current_version, latest_version)?
1287 {
1288 recommendations.push(recommendation);
1289 }
1290 }
1291 }
1292
1293 recommendations.sort_by(|a, b| b.priority.cmp(&a.priority));
1295
1296 Ok(recommendations)
1297 }
1298
1299 pub fn generate_update_script(&self, recommendations: &[UpdateRecommendation]) -> String {
1301 let mut script = String::new();
1302 script.push_str("#!/bin/bash\n");
1303 script.push_str("# Automated dependency update script generated by sklears\n\n");
1304
1305 for rec in recommendations {
1306 if self.should_auto_update(rec) {
1307 script.push_str(&format!(
1308 "echo \"Updating {} from {} to {}\"\n",
1309 rec.package_name, rec.current_version, rec.latest_version
1310 ));
1311 script.push_str(&format!(
1312 "cargo update -p {}:{}\n",
1313 rec.package_name, rec.latest_version
1314 ));
1315 } else {
1316 script.push_str(&format!(
1317 "# Manual review required for {}: {} -> {} ({})\n",
1318 rec.package_name,
1319 rec.current_version,
1320 rec.latest_version,
1321 match rec.update_type {
1322 UpdateType::Major => "major version change",
1323 UpdateType::Minor => "minor version change",
1324 UpdateType::Patch => "patch version change",
1325 }
1326 ));
1327 }
1328 }
1329
1330 script.push_str("\necho \"Update process complete. Running tests...\"\n");
1331 script.push_str("cargo test\n");
1332 script.push_str("cargo clippy -- -D warnings\n");
1333
1334 script
1335 }
1336
1337 fn load_current_dependencies(&mut self) -> Result<(), String> {
1339 self.current_versions
1342 .insert("ndarray".to_string(), "0.15.6".to_string());
1343 self.current_versions
1344 .insert("serde".to_string(), "1.0.193".to_string());
1345 self.current_versions
1346 .insert("rayon".to_string(), "1.8.0".to_string());
1347 self.current_versions
1348 .insert("criterion".to_string(), "0.5.1".to_string());
1349 Ok(())
1350 }
1351
1352 fn fetch_latest_versions(&mut self) -> Result<(), String> {
1354 self.latest_versions
1357 .insert("ndarray".to_string(), "0.15.7".to_string());
1358 self.latest_versions
1359 .insert("serde".to_string(), "1.0.195".to_string());
1360 self.latest_versions
1361 .insert("rayon".to_string(), "1.8.1".to_string());
1362 self.latest_versions
1363 .insert("criterion".to_string(), "0.5.1".to_string());
1364 Ok(())
1365 }
1366
1367 fn analyze_update(
1369 &self,
1370 package: &str,
1371 current: &str,
1372 latest: &str,
1373 ) -> Result<Option<UpdateRecommendation>, String> {
1374 if current == latest {
1375 return Ok(None);
1376 }
1377
1378 let update_type = self.determine_update_type(current, latest)?;
1379
1380 let allowed = match update_type {
1382 UpdateType::Major => self.config.allow_major_updates,
1383 UpdateType::Minor => self.config.allow_minor_updates,
1384 UpdateType::Patch => self.config.allow_patch_updates,
1385 };
1386
1387 if !allowed {
1388 return Ok(None);
1389 }
1390
1391 let security_advisory = self.check_security_advisory(package, current);
1393
1394 let priority = if security_advisory.is_some() {
1395 UpdatePriority::Critical
1396 } else {
1397 match update_type {
1398 UpdateType::Major => UpdatePriority::Low,
1399 UpdateType::Minor => UpdatePriority::Medium,
1400 UpdateType::Patch => UpdatePriority::High,
1401 }
1402 };
1403
1404 Ok(Some(UpdateRecommendation {
1405 package_name: package.to_string(),
1406 current_version: current.to_string(),
1407 latest_version: latest.to_string(),
1408 update_type,
1409 security_advisory,
1410 breaking_changes: self.get_breaking_changes(package, current, latest),
1411 priority,
1412 }))
1413 }
1414
1415 fn determine_update_type(&self, current: &str, latest: &str) -> Result<UpdateType, String> {
1417 let current_parts: Vec<u32> = current.split('.').map(|s| s.parse().unwrap_or(0)).collect();
1418 let latest_parts: Vec<u32> = latest.split('.').map(|s| s.parse().unwrap_or(0)).collect();
1419
1420 if current_parts.len() < 3 || latest_parts.len() < 3 {
1421 return Err("Invalid version format".to_string());
1422 }
1423
1424 if latest_parts[0] > current_parts[0] {
1425 Ok(UpdateType::Major)
1426 } else if latest_parts[1] > current_parts[1] {
1427 Ok(UpdateType::Minor)
1428 } else {
1429 Ok(UpdateType::Patch)
1430 }
1431 }
1432
1433 fn check_security_advisory(&self, package: &str, version: &str) -> Option<SecurityAdvisory> {
1435 if package == "serde" && version == "1.0.100" {
1438 Some(SecurityAdvisory {
1439 id: "RUSTSEC-2023-0001".to_string(),
1440 title: "Simulated security vulnerability".to_string(),
1441 severity: "Medium".to_string(),
1442 affected_versions: "< 1.0.150".to_string(),
1443 patched_versions: ">= 1.0.150".to_string(),
1444 description: "This is a simulated security advisory for demonstration".to_string(),
1445 })
1446 } else {
1447 None
1448 }
1449 }
1450
1451 fn get_breaking_changes(&self, _package: &str, _current: &str, _latest: &str) -> Vec<String> {
1453 Vec::new()
1455 }
1456
1457 fn should_auto_update(&self, recommendation: &UpdateRecommendation) -> bool {
1459 match recommendation.update_type {
1460 UpdateType::Major => false, UpdateType::Minor => recommendation.breaking_changes.is_empty(),
1462 UpdateType::Patch => true,
1463 }
1464 }
1465
1466 pub fn generate_update_report(&self, recommendations: &[UpdateRecommendation]) -> String {
1468 let mut report = String::new();
1469 report.push_str("Dependency Update Report\n");
1470 report.push_str("========================\n\n");
1471
1472 let critical_count = recommendations
1473 .iter()
1474 .filter(|r| r.priority == UpdatePriority::Critical)
1475 .count();
1476 let high_count = recommendations
1477 .iter()
1478 .filter(|r| r.priority == UpdatePriority::High)
1479 .count();
1480 let medium_count = recommendations
1481 .iter()
1482 .filter(|r| r.priority == UpdatePriority::Medium)
1483 .count();
1484 let low_count = recommendations
1485 .iter()
1486 .filter(|r| r.priority == UpdatePriority::Low)
1487 .count();
1488
1489 report.push_str("Summary:\n");
1490 report.push_str(&format!("- Critical updates: {critical_count}\n"));
1491 report.push_str(&format!("- High priority updates: {high_count}\n"));
1492 report.push_str(&format!("- Medium priority updates: {medium_count}\n"));
1493 report.push_str(&format!("- Low priority updates: {low_count}\n\n"));
1494
1495 for rec in recommendations {
1496 report.push_str(&format!("Package: {}\n", rec.package_name));
1497 report.push_str(&format!("Current Version: {}\n", rec.current_version));
1498 report.push_str(&format!("Latest Version: {}\n", rec.latest_version));
1499 report.push_str(&format!("Update Type: {:?}\n", rec.update_type));
1500 report.push_str(&format!("Priority: {:?}\n", rec.priority));
1501
1502 if let Some(ref advisory) = rec.security_advisory {
1503 report.push_str(&format!(
1504 "⚠️ Security Advisory: {} - {}\n",
1505 advisory.id, advisory.title
1506 ));
1507 report.push_str(&format!(" Severity: {}\n", advisory.severity));
1508 }
1509
1510 if !rec.breaking_changes.is_empty() {
1511 report.push_str("Breaking Changes:\n");
1512 for change in &rec.breaking_changes {
1513 report.push_str(&format!(" - {change}\n"));
1514 }
1515 }
1516
1517 report.push('\n');
1518 }
1519
1520 report
1521 }
1522}
1523
1524impl Default for DependencyUpdater {
1525 fn default() -> Self {
1526 Self::new()
1527 }
1528}
1529
1530#[allow(non_snake_case)]
1531#[cfg(test)]
1532mod tests {
1533 use super::*;
1534
1535 #[test]
1536 fn test_dependency_audit_creation() {
1537 let audit = DependencyAudit::new();
1538 assert!(!audit.dependencies().is_empty());
1539 assert!(!audit.recommendations().is_empty());
1540 }
1541
1542 #[test]
1543 fn test_dependency_categories() {
1544 let audit = DependencyAudit::new();
1545
1546 let essential = audit.dependencies_by_category(DependencyCategory::Essential);
1547 let optional = audit.dependencies_by_category(DependencyCategory::Optional);
1548 let heavy = audit.dependencies_by_category(DependencyCategory::Heavy);
1549
1550 assert!(!essential.is_empty());
1551 assert!(!optional.is_empty());
1552 assert!(!heavy.is_empty());
1553 }
1554
1555 #[test]
1556 fn test_report_generation() {
1557 let audit = DependencyAudit::new();
1558 let report = audit.generate_report();
1559
1560 assert!(report.total_dependencies > 0);
1561 assert!(report.essential_dependencies > 0);
1562 assert!(!report.summary().is_empty());
1563 assert!(!report.detailed_recommendations().is_empty());
1564 }
1565
1566 #[test]
1567 fn test_metrics_calculation() {
1568 let audit = DependencyAudit::new();
1569 let metrics = calculate_metrics(&audit);
1570
1571 assert!(metrics.estimated_compile_time_seconds > 0);
1572 assert!(metrics.estimated_binary_size_mb > 0);
1573 assert!(metrics.dependency_depth > 0);
1574 }
1575
1576 #[test]
1577 fn test_cargo_optimizations() {
1578 let audit = DependencyAudit::new();
1579 let report = audit.generate_report();
1580 let optimizations = report.generate_cargo_optimizations();
1581
1582 assert!(optimizations.contains("optional = true"));
1583 assert!(optimizations.contains("[features]"));
1584 assert!(optimizations.contains("[profile"));
1585 }
1586
1587 #[test]
1588 fn test_dependency_graph() {
1589 let audit = DependencyAudit::new();
1590 let graph = generate_dependency_graph(&audit);
1591
1592 assert!(graph.contains("digraph dependencies"));
1593 assert!(graph.contains("lightblue")); assert!(graph.contains("lightcoral")); }
1596
1597 #[test]
1598 fn test_recommendations() {
1599 let audit = DependencyAudit::new();
1600 let recommendations = audit.recommendations();
1601
1602 assert!(recommendations
1604 .iter()
1605 .any(|r| matches!(r.action, RecommendationAction::MakeOptional)));
1606
1607 assert!(recommendations
1609 .iter()
1610 .any(|r| matches!(r.action, RecommendationAction::Remove)));
1611 }
1612
1613 #[test]
1614 fn test_license_checker() {
1615 let checker = LicenseChecker::new("Apache-2.0");
1616
1617 assert_eq!(
1619 checker.check_compatibility("MIT"),
1620 LicenseCompatibility::Compatible
1621 );
1622 assert_eq!(
1623 checker.check_compatibility("BSD-3-Clause"),
1624 LicenseCompatibility::Compatible
1625 );
1626 assert_eq!(
1627 checker.check_compatibility("Apache-2.0"),
1628 LicenseCompatibility::Compatible
1629 );
1630
1631 assert_eq!(
1633 checker.check_compatibility("GPL-2.0"),
1634 LicenseCompatibility::Incompatible
1635 );
1636
1637 assert_eq!(
1639 checker.check_compatibility("LGPL-2.1"),
1640 LicenseCompatibility::CompatibleWithAttribution
1641 );
1642 assert_eq!(
1643 checker.check_compatibility("MPL-2.0"),
1644 LicenseCompatibility::CompatibleWithAttribution
1645 );
1646
1647 assert_eq!(
1649 checker.check_compatibility("GPL-3.0"),
1650 LicenseCompatibility::CompatibleCopyleft
1651 );
1652
1653 assert_eq!(
1655 checker.check_compatibility("UNKNOWN-LICENSE"),
1656 LicenseCompatibility::Unknown
1657 );
1658
1659 assert_eq!(
1661 checker.check_compatibility("AGPL-3.0"),
1662 LicenseCompatibility::ReviewRequired
1663 );
1664 }
1665
1666 #[test]
1667 fn test_license_report_generation() {
1668 let audit = DependencyAudit::new();
1669 let checker = LicenseChecker::new("Apache-2.0");
1670 let license_report = checker.generate_license_report(audit.dependencies());
1671
1672 assert!(!license_report.compatible.is_empty());
1674
1675 let has_issues = license_report.has_issues();
1677
1678 let detailed = license_report.detailed_report();
1680 assert!(detailed.contains("License Compatibility Report"));
1681
1682 if has_issues {
1683 let summary = license_report.issue_summary();
1684 assert!(summary.contains("License Compatibility Issues"));
1685 }
1686 }
1687
1688 #[test]
1689 fn test_attribution_text_generation() {
1690 let audit = DependencyAudit::new();
1691 let checker = LicenseChecker::new("Apache-2.0");
1692 let attribution = checker.generate_attribution_text(audit.dependencies());
1693
1694 assert!(attribution.contains("Third-Party Licenses"));
1695 assert!(attribution.contains("This software includes"));
1696 }
1697
1698 #[test]
1699 fn test_mit_license_compatibility() {
1700 let checker = LicenseChecker::new("MIT");
1701
1702 assert_eq!(
1704 checker.check_compatibility("MIT"),
1705 LicenseCompatibility::Compatible
1706 );
1707 assert_eq!(
1708 checker.check_compatibility("BSD-2-Clause"),
1709 LicenseCompatibility::Compatible
1710 );
1711 assert_eq!(
1712 checker.check_compatibility("Apache-2.0"),
1713 LicenseCompatibility::Compatible
1714 );
1715
1716 assert_eq!(
1718 checker.check_compatibility("GPL-2.0"),
1719 LicenseCompatibility::Incompatible
1720 );
1721 }
1722
1723 #[test]
1724 fn test_dependency_updater_creation() {
1725 let updater = DependencyUpdater::new();
1726 assert_eq!(updater.config.allow_minor_updates, true);
1727 assert_eq!(updater.config.allow_major_updates, false);
1728 assert_eq!(updater.config.allow_patch_updates, true);
1729 }
1730
1731 #[test]
1732 fn test_update_recommendations() {
1733 let mut updater = DependencyUpdater::new();
1734 let recommendations = updater.check_for_updates().unwrap();
1735
1736 assert!(!recommendations.is_empty());
1738
1739 for i in 1..recommendations.len() {
1741 assert!(recommendations[i - 1].priority >= recommendations[i].priority);
1742 }
1743 }
1744
1745 #[test]
1746 fn test_update_script_generation() {
1747 let mut updater = DependencyUpdater::new();
1748 let recommendations = updater.check_for_updates().unwrap();
1749 let script = updater.generate_update_script(&recommendations);
1750
1751 assert!(script.contains("#!/bin/bash"));
1752 assert!(script.contains("cargo update"));
1753 assert!(script.contains("cargo test"));
1754 }
1755
1756 #[test]
1757 fn test_update_report_generation() {
1758 let mut updater = DependencyUpdater::new();
1759 let recommendations = updater.check_for_updates().unwrap();
1760 let report = updater.generate_update_report(&recommendations);
1761
1762 assert!(report.contains("Dependency Update Report"));
1763 assert!(report.contains("Summary:"));
1764 assert!(report.contains("Package:"));
1765 }
1766
1767 #[test]
1768 fn test_update_type_determination() {
1769 let updater = DependencyUpdater::new();
1770
1771 assert_eq!(
1772 updater.determine_update_type("1.0.0", "2.0.0").unwrap(),
1773 UpdateType::Major
1774 );
1775 assert_eq!(
1776 updater.determine_update_type("1.0.0", "1.1.0").unwrap(),
1777 UpdateType::Minor
1778 );
1779 assert_eq!(
1780 updater.determine_update_type("1.0.0", "1.0.1").unwrap(),
1781 UpdateType::Patch
1782 );
1783 }
1784
1785 #[test]
1786 fn test_gpl3_license_compatibility() {
1787 let checker = LicenseChecker::new("GPL-3.0");
1788
1789 assert_eq!(
1791 checker.check_compatibility("MIT"),
1792 LicenseCompatibility::Compatible
1793 );
1794 assert_eq!(
1795 checker.check_compatibility("Apache-2.0"),
1796 LicenseCompatibility::Compatible
1797 );
1798 assert_eq!(
1799 checker.check_compatibility("GPL-3.0"),
1800 LicenseCompatibility::Compatible
1801 );
1802
1803 assert_eq!(
1805 checker.check_compatibility("GPL-2.0"),
1806 LicenseCompatibility::Incompatible
1807 );
1808 }
1809}