Skip to main content

mir_analyzer/
php_version.rs

1//! Target PHP language version.
2//!
3//! Used by the analyzer and stub loader to make version-conditional decisions
4//! (e.g. filtering stub symbols by `@since`/`@removed` markers). The type is
5//! `Copy` and stores only major/minor — patch level is parsed but discarded,
6//! since language features track the minor release.
7use std::fmt;
8use std::str::FromStr;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct PhpVersion {
12    major: u8,
13    minor: u8,
14}
15
16impl PhpVersion {
17    pub const LATEST: PhpVersion = PhpVersion::new(8, 5);
18
19    pub const fn new(major: u8, minor: u8) -> Self {
20        Self { major, minor }
21    }
22
23    pub const fn major(self) -> u8 {
24        self.major
25    }
26
27    pub const fn minor(self) -> u8 {
28        self.minor
29    }
30}
31
32impl Default for PhpVersion {
33    fn default() -> Self {
34        Self::LATEST
35    }
36}
37
38impl fmt::Display for PhpVersion {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(f, "{}.{}", self.major, self.minor)
41    }
42}
43
44#[derive(Debug, thiserror::Error)]
45#[error("invalid PHP version `{0}`: expected `MAJOR.MINOR` (e.g. `8.2`)")]
46pub struct ParsePhpVersionError(pub String);
47
48impl FromStr for PhpVersion {
49    type Err = ParsePhpVersionError;
50
51    fn from_str(s: &str) -> Result<Self, Self::Err> {
52        let mut parts = s.trim().split('.');
53        let major = parts
54            .next()
55            .and_then(|p| p.parse::<u8>().ok())
56            .ok_or_else(|| ParsePhpVersionError(s.to_string()))?;
57        let minor = parts.next().and_then(|p| p.parse::<u8>().ok()).unwrap_or(0);
58        // Ignore any patch component — language features track the minor release.
59        Ok(Self::new(major, minor))
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn parses_major_minor() {
69        assert_eq!("8.2".parse::<PhpVersion>().unwrap(), PhpVersion::new(8, 2));
70    }
71
72    #[test]
73    fn parses_major_minor_patch() {
74        assert_eq!(
75            "8.3.7".parse::<PhpVersion>().unwrap(),
76            PhpVersion::new(8, 3)
77        );
78    }
79
80    #[test]
81    fn parses_major_only() {
82        assert_eq!("7".parse::<PhpVersion>().unwrap(), PhpVersion::new(7, 0));
83    }
84
85    #[test]
86    fn rejects_garbage() {
87        assert!("x.y".parse::<PhpVersion>().is_err());
88        assert!("".parse::<PhpVersion>().is_err());
89    }
90
91    #[test]
92    fn ordered_by_major_then_minor() {
93        assert!(PhpVersion::new(8, 1) < PhpVersion::new(8, 2));
94        assert!(PhpVersion::new(7, 4) < PhpVersion::new(8, 0));
95    }
96
97    #[test]
98    fn displays_as_major_dot_minor() {
99        assert_eq!(PhpVersion::new(8, 3).to_string(), "8.3");
100    }
101}