Skip to main content

rez_lsp_server/resolver/
conflict_detector.rs

1//! Conflict detection for dependency resolution.
2
3use std::collections::HashMap;
4use tracing::debug;
5
6use crate::core::{DependencyConflict, Package, Requirement, Version, VersionConstraint};
7
8/// Detects conflicts in package requirements.
9pub struct ConflictDetector {
10    /// Available packages indexed by name
11    packages: HashMap<String, Vec<Package>>,
12}
13
14impl ConflictDetector {
15    /// Create a new conflict detector.
16    pub fn new() -> Self {
17        Self {
18            packages: HashMap::new(),
19        }
20    }
21
22    /// Set the available packages for conflict detection.
23    pub fn set_packages(&mut self, packages: HashMap<String, Vec<Package>>) {
24        self.packages = packages;
25    }
26
27    /// Detect conflicts in a set of requirements.
28    pub fn detect_conflicts(&self, requirements: &[Requirement]) -> Vec<DependencyConflict> {
29        let mut conflicts = Vec::new();
30
31        // Group requirements by package name
32        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        // Check each package for conflicts
41        for (package_name, reqs) in package_requirements {
42            conflicts.extend(self.check_package_conflicts(&package_name, &reqs));
43        }
44
45        conflicts
46    }
47
48    /// Check conflicts for a specific package.
49    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; // No conflicts possible with single requirement
58        }
59
60        debug!(
61            "Checking conflicts for package '{}' with {} requirements",
62            package_name,
63            requirements.len()
64        );
65
66        // Get available versions for this package
67        let available_versions = match self.packages.get(package_name) {
68            Some(versions) => versions,
69            None => {
70                // Package not found - this is a different kind of error
71                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        // Check if any version can satisfy all requirements
80        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            // No version satisfies all requirements - this is a conflict
87            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            // If no version satisfies all requirements, don't check for other conflicts
101            return conflicts;
102        }
103
104        // Check for explicit conflict requirements
105        for req in requirements {
106            if req.conflict {
107                // This is a conflict requirement - check if it would exclude valid versions
108                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    /// Check if a version satisfies all requirements.
131    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                // Conflict requirements should NOT be satisfied
139                if req.constraint.satisfies(version) {
140                    return false;
141                }
142            } else {
143                // Normal requirements should be satisfied
144                if !req.constraint.satisfies(version) {
145                    return false;
146                }
147            }
148        }
149        true
150    }
151
152    /// Check if two constraints are mutually exclusive.
153    #[allow(dead_code)]
154    fn are_constraints_mutually_exclusive(
155        &self,
156        constraint1: &VersionConstraint,
157        constraint2: &VersionConstraint,
158        available_versions: &[Package],
159    ) -> bool {
160        // Check if there's any version that satisfies both constraints
161        for package in available_versions {
162            if constraint1.satisfies(&package.version) && constraint2.satisfies(&package.version) {
163                return false; // Found a version that satisfies both
164            }
165        }
166        true // No version satisfies both constraints
167    }
168
169    /// Get detailed conflict analysis.
170    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/// Analysis result for conflicts.
203#[derive(Debug, Clone)]
204pub struct ConflictAnalysis {
205    /// Total number of requirements analyzed
206    pub total_requirements: usize,
207    /// Total number of unique packages
208    pub total_packages: usize,
209    /// Detected conflicts
210    pub conflicts: Vec<DependencyConflict>,
211    /// Whether any conflicts were found
212    pub has_conflicts: bool,
213    /// Severity of conflicts
214    pub severity: ConflictSeverity,
215}
216
217/// Severity levels for conflicts.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum ConflictSeverity {
220    /// No conflicts
221    None,
222    /// Minor conflicts (1 conflict)
223    Minor,
224    /// Moderate conflicts (2-3 conflicts)
225    Moderate,
226    /// Severe conflicts (4+ conflicts)
227    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}