Skip to main content

pro_core/pep/
specifier.rs

1//! Version specifier parsing and range conversion
2//!
3//! Parses PEP 440 version specifiers like ">=1.0,<2.0" and converts
4//! them to pubgrub ranges for dependency resolution.
5
6use std::str::FromStr;
7
8use pubgrub::range::Range;
9use pubgrub::version::Version as PubgrubVersion;
10
11use crate::pep::pep440::Version;
12use crate::Error;
13
14/// Helper to create a version that is the next possible version after the given one
15fn next_version(v: &Version) -> Version {
16    PubgrubVersion::bump(v)
17}
18
19/// Version comparison operator
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Operator {
22    /// `==` - Exact match
23    Equal,
24    /// `!=` - Exclusion
25    NotEqual,
26    /// `<` - Less than
27    LessThan,
28    /// `<=` - Less than or equal
29    LessThanOrEqual,
30    /// `>` - Greater than
31    GreaterThan,
32    /// `>=` - Greater than or equal
33    GreaterThanOrEqual,
34    /// `~=` - Compatible release
35    Compatible,
36    /// `===` - Arbitrary equality (string match)
37    ArbitraryEqual,
38}
39
40impl Operator {
41    /// Parse an operator from a string prefix
42    fn parse(s: &str) -> Option<(Self, usize)> {
43        if s.starts_with("===") {
44            Some((Operator::ArbitraryEqual, 3))
45        } else if s.starts_with("==") {
46            Some((Operator::Equal, 2))
47        } else if s.starts_with("!=") {
48            Some((Operator::NotEqual, 2))
49        } else if s.starts_with("~=") {
50            Some((Operator::Compatible, 2))
51        } else if s.starts_with("<=") {
52            Some((Operator::LessThanOrEqual, 2))
53        } else if s.starts_with(">=") {
54            Some((Operator::GreaterThanOrEqual, 2))
55        } else if s.starts_with('<') {
56            Some((Operator::LessThan, 1))
57        } else if s.starts_with('>') {
58            Some((Operator::GreaterThan, 1))
59        } else {
60            None
61        }
62    }
63}
64
65/// A single version specifier (operator + version)
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct VersionSpecifier {
68    /// The comparison operator
69    pub operator: Operator,
70    /// The version to compare against
71    pub version: Version,
72    /// Whether this is a wildcard match (e.g., `==1.0.*`)
73    pub wildcard: bool,
74}
75
76impl VersionSpecifier {
77    /// Create a new version specifier
78    pub fn new(operator: Operator, version: Version) -> Self {
79        Self {
80            operator,
81            version,
82            wildcard: false,
83        }
84    }
85
86    /// Parse a single version specifier
87    pub fn parse(s: &str) -> Result<Self, Error> {
88        let s = s.trim();
89
90        let (operator, op_len) =
91            Operator::parse(s).ok_or_else(|| Error::InvalidSpecifier(s.to_string()))?;
92
93        let version_str = s[op_len..].trim();
94
95        // Check for wildcard
96        let (version_str, wildcard) = if let Some(stripped) = version_str.strip_suffix(".*") {
97            (stripped, true)
98        } else if let Some(stripped) = version_str.strip_suffix('*') {
99            (stripped, true)
100        } else {
101            (version_str, false)
102        };
103
104        let version = Version::parse(version_str)?;
105
106        Ok(Self {
107            operator,
108            version,
109            wildcard,
110        })
111    }
112
113    /// Check if a version satisfies this specifier
114    pub fn contains(&self, v: &Version) -> bool {
115        match self.operator {
116            Operator::Equal => {
117                if self.wildcard {
118                    self.matches_prefix(v)
119                } else {
120                    v == &self.version
121                }
122            }
123            Operator::NotEqual => {
124                if self.wildcard {
125                    !self.matches_prefix(v)
126                } else {
127                    v != &self.version
128                }
129            }
130            Operator::LessThan => v < &self.version,
131            Operator::LessThanOrEqual => v <= &self.version,
132            Operator::GreaterThan => v > &self.version,
133            Operator::GreaterThanOrEqual => v >= &self.version,
134            Operator::Compatible => {
135                // ~=X.Y is equivalent to >=X.Y,==X.*
136                if self.version.release.len() < 2 {
137                    v >= &self.version
138                } else {
139                    let mut upper = self.version.clone();
140                    // Bump the second-to-last release segment
141                    let len = upper.release.len();
142                    if len >= 2 {
143                        upper.release[len - 2] += 1;
144                        upper.release.truncate(len - 1);
145                        upper.pre = None;
146                        upper.post = None;
147                        upper.dev = None;
148                        upper.local = None;
149                    }
150                    v >= &self.version && v < &upper
151                }
152            }
153            Operator::ArbitraryEqual => {
154                // String comparison - compare string representations
155                v.to_string() == self.version.to_string()
156            }
157        }
158    }
159
160    /// Check if version matches the prefix (for wildcard matching)
161    fn matches_prefix(&self, v: &Version) -> bool {
162        // Epochs must match
163        if v.epoch != self.version.epoch {
164            return false;
165        }
166
167        // Check release prefix
168        for (i, seg) in self.version.release.iter().enumerate() {
169            if v.release.get(i) != Some(seg) {
170                return false;
171            }
172        }
173
174        true
175    }
176
177    /// Convert to a pubgrub range
178    pub fn to_range(&self) -> Range<Version> {
179        match self.operator {
180            Operator::Equal => {
181                if self.wildcard {
182                    // ==1.0.* means >=1.0.0,<1.1.0
183                    let mut upper = self.version.clone();
184                    if let Some(last) = upper.release.last_mut() {
185                        *last += 1;
186                    }
187                    Range::between(self.version.clone(), upper)
188                } else {
189                    // Exact match
190                    Range::exact(self.version.clone())
191                }
192            }
193            Operator::NotEqual => {
194                // For pubgrub, != means everything except this version
195                Range::exact(self.version.clone()).negate()
196            }
197            Operator::LessThan => Range::strictly_lower_than(self.version.clone()),
198            Operator::LessThanOrEqual => {
199                // <= v means < v.bump()
200                Range::strictly_lower_than(next_version(&self.version))
201            }
202            Operator::GreaterThan => {
203                // > v means >= v.bump()
204                Range::higher_than(next_version(&self.version))
205            }
206            Operator::GreaterThanOrEqual => Range::higher_than(self.version.clone()),
207            Operator::Compatible => {
208                // ~=X.Y.Z is >=X.Y.Z,<X.(Y+1).0
209                if self.version.release.len() < 2 {
210                    Range::higher_than(self.version.clone())
211                } else {
212                    let mut upper = self.version.clone();
213                    let len = upper.release.len();
214                    upper.release[len - 2] += 1;
215                    upper.release.truncate(len - 1);
216                    upper.pre = None;
217                    upper.post = None;
218                    upper.dev = None;
219                    upper.local = None;
220                    Range::between(self.version.clone(), upper)
221                }
222            }
223            Operator::ArbitraryEqual => {
224                // Treat as exact match
225                Range::exact(self.version.clone())
226            }
227        }
228    }
229}
230
231impl FromStr for VersionSpecifier {
232    type Err = Error;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        Self::parse(s)
236    }
237}
238
239/// A collection of version specifiers (e.g., ">=1.0,<2.0")
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct VersionSpecifiers(pub Vec<VersionSpecifier>);
242
243impl VersionSpecifiers {
244    /// Create empty specifiers (matches all versions)
245    pub fn any() -> Self {
246        Self(vec![])
247    }
248
249    /// Parse a comma-separated list of version specifiers
250    pub fn parse(s: &str) -> Result<Self, Error> {
251        let s = s.trim();
252
253        if s.is_empty() {
254            return Ok(Self::any());
255        }
256
257        let specifiers: Result<Vec<_>, _> = s
258            .split(',')
259            .map(|part| VersionSpecifier::parse(part.trim()))
260            .collect();
261
262        Ok(Self(specifiers?))
263    }
264
265    /// Check if a version satisfies all specifiers
266    pub fn contains(&self, v: &Version) -> bool {
267        self.0.iter().all(|spec| spec.contains(v))
268    }
269
270    /// Check if this matches any version
271    pub fn is_any(&self) -> bool {
272        self.0.is_empty()
273    }
274
275    /// Convert to a pubgrub range by intersecting all specifier ranges
276    pub fn to_pubgrub_range(&self) -> Range<Version> {
277        if self.0.is_empty() {
278            return Range::any();
279        }
280
281        let mut result = Range::any();
282
283        for spec in &self.0 {
284            let range = spec.to_range();
285            result = result.intersection(&range);
286        }
287
288        result
289    }
290}
291
292impl FromStr for VersionSpecifiers {
293    type Err = Error;
294
295    fn from_str(s: &str) -> Result<Self, Self::Err> {
296        Self::parse(s)
297    }
298}
299
300impl std::fmt::Display for VersionSpecifiers {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        let parts: Vec<String> = self
303            .0
304            .iter()
305            .map(|spec| {
306                let op = match spec.operator {
307                    Operator::Equal => "==",
308                    Operator::NotEqual => "!=",
309                    Operator::LessThan => "<",
310                    Operator::LessThanOrEqual => "<=",
311                    Operator::GreaterThan => ">",
312                    Operator::GreaterThanOrEqual => ">=",
313                    Operator::Compatible => "~=",
314                    Operator::ArbitraryEqual => "===",
315                };
316                let wildcard = if spec.wildcard { ".*" } else { "" };
317                format!("{}{}{}", op, spec.version, wildcard)
318            })
319            .collect();
320        write!(f, "{}", parts.join(","))
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_parse_single_specifier() {
330        let spec = VersionSpecifier::parse(">=1.0").unwrap();
331        assert_eq!(spec.operator, Operator::GreaterThanOrEqual);
332        assert_eq!(spec.version.release, vec![1, 0]);
333    }
334
335    #[test]
336    fn test_parse_multiple_specifiers() {
337        let specs = VersionSpecifiers::parse(">=1.0,<2.0").unwrap();
338        assert_eq!(specs.0.len(), 2);
339        assert_eq!(specs.0[0].operator, Operator::GreaterThanOrEqual);
340        assert_eq!(specs.0[1].operator, Operator::LessThan);
341    }
342
343    #[test]
344    fn test_parse_wildcard() {
345        let spec = VersionSpecifier::parse("==1.0.*").unwrap();
346        assert_eq!(spec.operator, Operator::Equal);
347        assert!(spec.wildcard);
348        assert_eq!(spec.version.release, vec![1, 0]);
349    }
350
351    #[test]
352    fn test_contains_ge() {
353        let spec = VersionSpecifier::parse(">=1.0").unwrap();
354        assert!(spec.contains(&Version::parse("1.0").unwrap()));
355        assert!(spec.contains(&Version::parse("1.5").unwrap()));
356        assert!(spec.contains(&Version::parse("2.0").unwrap()));
357        assert!(!spec.contains(&Version::parse("0.9").unwrap()));
358    }
359
360    #[test]
361    fn test_contains_lt() {
362        let spec = VersionSpecifier::parse("<2.0").unwrap();
363        assert!(spec.contains(&Version::parse("1.0").unwrap()));
364        assert!(spec.contains(&Version::parse("1.9.9").unwrap()));
365        assert!(!spec.contains(&Version::parse("2.0").unwrap()));
366        assert!(!spec.contains(&Version::parse("2.1").unwrap()));
367    }
368
369    #[test]
370    fn test_contains_range() {
371        let specs = VersionSpecifiers::parse(">=1.0,<2.0").unwrap();
372        assert!(specs.contains(&Version::parse("1.0").unwrap()));
373        assert!(specs.contains(&Version::parse("1.5").unwrap()));
374        assert!(!specs.contains(&Version::parse("0.9").unwrap()));
375        assert!(!specs.contains(&Version::parse("2.0").unwrap()));
376    }
377
378    #[test]
379    fn test_contains_compatible() {
380        let spec = VersionSpecifier::parse("~=1.4.2").unwrap();
381        assert!(spec.contains(&Version::parse("1.4.2").unwrap()));
382        assert!(spec.contains(&Version::parse("1.4.5").unwrap()));
383        assert!(!spec.contains(&Version::parse("1.5.0").unwrap()));
384        assert!(!spec.contains(&Version::parse("1.4.1").unwrap()));
385    }
386
387    #[test]
388    fn test_contains_wildcard() {
389        let spec = VersionSpecifier::parse("==1.0.*").unwrap();
390        assert!(spec.contains(&Version::parse("1.0.0").unwrap()));
391        assert!(spec.contains(&Version::parse("1.0.5").unwrap()));
392        assert!(!spec.contains(&Version::parse("1.1.0").unwrap()));
393    }
394
395    #[test]
396    fn test_to_pubgrub_range_ge() {
397        let spec = VersionSpecifier::parse(">=1.0").unwrap();
398        let range = spec.to_range();
399        assert!(range.contains(&Version::parse("1.0").unwrap()));
400        assert!(range.contains(&Version::parse("2.0").unwrap()));
401        assert!(!range.contains(&Version::parse("0.9").unwrap()));
402    }
403
404    #[test]
405    fn test_to_pubgrub_range_lt() {
406        let spec = VersionSpecifier::parse("<2.0").unwrap();
407        let range = spec.to_range();
408        assert!(range.contains(&Version::parse("1.0").unwrap()));
409        assert!(!range.contains(&Version::parse("2.0").unwrap()));
410    }
411
412    #[test]
413    fn test_to_pubgrub_range_combined() {
414        let specs = VersionSpecifiers::parse(">=1.0,<2.0").unwrap();
415        let range = specs.to_pubgrub_range();
416        assert!(range.contains(&Version::parse("1.0").unwrap()));
417        assert!(range.contains(&Version::parse("1.5").unwrap()));
418        assert!(!range.contains(&Version::parse("0.9").unwrap()));
419        assert!(!range.contains(&Version::parse("2.0").unwrap()));
420    }
421
422    #[test]
423    fn test_any_specifiers() {
424        let specs = VersionSpecifiers::any();
425        assert!(specs.is_any());
426        assert!(specs.contains(&Version::parse("1.0.0").unwrap()));
427        assert!(specs.contains(&Version::parse("999.999.999").unwrap()));
428    }
429
430    #[test]
431    fn test_display() {
432        let specs = VersionSpecifiers::parse(">=1.0,<2.0").unwrap();
433        assert_eq!(specs.to_string(), ">=1.0,<2.0");
434    }
435}