1use std::collections::HashMap;
4use tracing::debug;
5
6use crate::core::{DependencyConflict, Package, Requirement, Version, VersionConstraint};
7
8pub struct ConflictDetector {
10 packages: HashMap<String, Vec<Package>>,
12}
13
14impl ConflictDetector {
15 pub fn new() -> Self {
17 Self {
18 packages: HashMap::new(),
19 }
20 }
21
22 pub fn set_packages(&mut self, packages: HashMap<String, Vec<Package>>) {
24 self.packages = packages;
25 }
26
27 pub fn detect_conflicts(&self, requirements: &[Requirement]) -> Vec<DependencyConflict> {
29 let mut conflicts = Vec::new();
30
31 let mut package_requirements: HashMap<String, Vec<&Requirement>> = HashMap::new();
33 for req in requirements {
34 package_requirements
35 .entry(req.name.clone())
36 .or_default()
37 .push(req);
38 }
39
40 for (package_name, reqs) in package_requirements {
42 conflicts.extend(self.check_package_conflicts(&package_name, &reqs));
43 }
44
45 conflicts
46 }
47
48 fn check_package_conflicts(
50 &self,
51 package_name: &str,
52 requirements: &[&Requirement],
53 ) -> Vec<DependencyConflict> {
54 let mut conflicts = Vec::new();
55
56 if requirements.len() <= 1 {
57 return conflicts; }
59
60 debug!(
61 "Checking conflicts for package '{}' with {} requirements",
62 package_name,
63 requirements.len()
64 );
65
66 let available_versions = match self.packages.get(package_name) {
68 Some(versions) => versions,
69 None => {
70 return vec![DependencyConflict {
72 package: package_name.to_string(),
73 requirements: requirements.iter().map(|&r| r.clone()).collect(),
74 description: format!("Package '{}' not found", package_name),
75 }];
76 }
77 };
78
79 let satisfying_versions: Vec<&Package> = available_versions
81 .iter()
82 .filter(|pkg| self.version_satisfies_all_requirements(&pkg.version, requirements))
83 .collect();
84
85 if satisfying_versions.is_empty() {
86 conflicts.push(DependencyConflict {
88 package: package_name.to_string(),
89 requirements: requirements.iter().map(|&r| r.clone()).collect(),
90 description: format!(
91 "No version of '{}' satisfies all requirements: {}",
92 package_name,
93 requirements
94 .iter()
95 .map(|r| r.to_string())
96 .collect::<Vec<_>>()
97 .join(", ")
98 ),
99 });
100 return conflicts;
102 }
103
104 for req in requirements {
106 if req.conflict {
107 let excluded_versions: Vec<&Package> = available_versions
109 .iter()
110 .filter(|pkg| req.constraint.satisfies(&pkg.version))
111 .collect();
112
113 if !excluded_versions.is_empty() {
114 conflicts.push(DependencyConflict {
115 package: package_name.to_string(),
116 requirements: vec![(*req).clone()],
117 description: format!(
118 "Conflict requirement '{}' excludes {} available version(s)",
119 req,
120 excluded_versions.len()
121 ),
122 });
123 }
124 }
125 }
126
127 conflicts
128 }
129
130 fn version_satisfies_all_requirements(
132 &self,
133 version: &Version,
134 requirements: &[&Requirement],
135 ) -> bool {
136 for req in requirements {
137 if req.conflict {
138 if req.constraint.satisfies(version) {
140 return false;
141 }
142 } else {
143 if !req.constraint.satisfies(version) {
145 return false;
146 }
147 }
148 }
149 true
150 }
151
152 #[allow(dead_code)]
154 fn are_constraints_mutually_exclusive(
155 &self,
156 constraint1: &VersionConstraint,
157 constraint2: &VersionConstraint,
158 available_versions: &[Package],
159 ) -> bool {
160 for package in available_versions {
162 if constraint1.satisfies(&package.version) && constraint2.satisfies(&package.version) {
163 return false; }
165 }
166 true }
168
169 pub fn analyze_conflicts(&self, requirements: &[Requirement]) -> ConflictAnalysis {
171 let conflicts = self.detect_conflicts(requirements);
172 let total_packages = requirements
173 .iter()
174 .map(|r| &r.name)
175 .collect::<std::collections::HashSet<_>>()
176 .len();
177
178 ConflictAnalysis {
179 total_requirements: requirements.len(),
180 total_packages,
181 conflicts: conflicts.clone(),
182 has_conflicts: !conflicts.is_empty(),
183 severity: if conflicts.is_empty() {
184 ConflictSeverity::None
185 } else if conflicts.len() == 1 {
186 ConflictSeverity::Minor
187 } else if conflicts.len() <= 3 {
188 ConflictSeverity::Moderate
189 } else {
190 ConflictSeverity::Severe
191 },
192 }
193 }
194}
195
196impl Default for ConflictDetector {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct ConflictAnalysis {
205 pub total_requirements: usize,
207 pub total_packages: usize,
209 pub conflicts: Vec<DependencyConflict>,
211 pub has_conflicts: bool,
213 pub severity: ConflictSeverity,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum ConflictSeverity {
220 None,
222 Minor,
224 Moderate,
226 Severe,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::core::{Package, Version, VersionConstraint};
234 use std::collections::HashMap;
235 use std::path::PathBuf;
236
237 fn create_test_package(name: &str, version: &str) -> Package {
238 Package {
239 name: name.to_string(),
240 version: Version::new(version),
241 description: Some(format!("Test package {}", name)),
242 authors: vec!["Test Author".to_string()],
243 requires: vec![],
244 tools: vec![],
245 variants: vec![],
246 path: PathBuf::from("/test"),
247 metadata: HashMap::new(),
248 }
249 }
250
251 #[test]
252 fn test_no_conflicts() {
253 let mut detector = ConflictDetector::new();
254
255 let mut packages = HashMap::new();
256 packages.insert(
257 "python".to_string(),
258 vec![
259 create_test_package("python", "3.7.0"),
260 create_test_package("python", "3.8.0"),
261 create_test_package("python", "3.9.0"),
262 ],
263 );
264 detector.set_packages(packages);
265
266 let requirements = vec![Requirement::new(
267 "python",
268 VersionConstraint::GreaterEqual(Version::new("3.7")),
269 )];
270
271 let conflicts = detector.detect_conflicts(&requirements);
272 assert!(conflicts.is_empty());
273 }
274
275 #[test]
276 fn test_version_conflict() {
277 let mut detector = ConflictDetector::new();
278
279 let mut packages = HashMap::new();
280 packages.insert(
281 "python".to_string(),
282 vec![
283 create_test_package("python", "3.7.0"),
284 create_test_package("python", "3.8.0"),
285 create_test_package("python", "3.9.0"),
286 ],
287 );
288 detector.set_packages(packages);
289
290 let requirements = vec![
291 Requirement::new("python", VersionConstraint::Exact(Version::new("3.7.0"))),
292 Requirement::new("python", VersionConstraint::Exact(Version::new("3.9.0"))),
293 ];
294
295 let conflicts = detector.detect_conflicts(&requirements);
296 assert!(!conflicts.is_empty());
297 assert_eq!(conflicts.len(), 1);
298 }
299
300 #[test]
301 fn test_conflict_analysis() {
302 let mut detector = ConflictDetector::new();
303
304 let mut packages = HashMap::new();
305 packages.insert(
306 "python".to_string(),
307 vec![create_test_package("python", "3.9.0")],
308 );
309 detector.set_packages(packages);
310
311 let requirements = vec![
312 Requirement::new("python", VersionConstraint::Exact(Version::new("3.7.0"))),
313 Requirement::new("python", VersionConstraint::Exact(Version::new("3.9.0"))),
314 ];
315
316 let analysis = detector.analyze_conflicts(&requirements);
317 assert!(analysis.has_conflicts);
318 assert_eq!(analysis.severity, ConflictSeverity::Minor);
319 assert_eq!(analysis.total_requirements, 2);
320 assert_eq!(analysis.total_packages, 1);
321 }
322}