mir_analyzer/
php_version.rs1use 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 pub const fn cache_byte(self) -> u8 {
36 (self.major << 4) | (self.minor & 0x0F)
37 }
38
39 pub fn includes_symbol(self, since: Option<&str>, removed: Option<&str>) -> bool {
49 let parse_php = |s: &str| -> Option<PhpVersion> {
50 let v = s.parse::<PhpVersion>().ok()?;
51 if v.major() >= 4 && v.major() <= PhpVersion::LATEST.major() {
55 Some(v)
56 } else {
57 None
58 }
59 };
60 if let Some(s) = since.and_then(parse_php) {
61 if self < s {
62 return false;
63 }
64 }
65 if let Some(r) = removed.and_then(parse_php) {
66 if self >= r {
67 return false;
68 }
69 }
70 true
71 }
72}
73
74impl Default for PhpVersion {
75 fn default() -> Self {
76 Self::LATEST
77 }
78}
79
80impl fmt::Display for PhpVersion {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(f, "{}.{}", self.major, self.minor)
83 }
84}
85
86#[derive(Debug, thiserror::Error)]
87#[error("invalid PHP version `{0}`: expected `MAJOR.MINOR` (e.g. `8.2`)")]
88pub struct ParsePhpVersionError(pub String);
89
90impl FromStr for PhpVersion {
91 type Err = ParsePhpVersionError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 let mut parts = s.trim().split('.');
95 let major = parts
96 .next()
97 .and_then(|p| p.parse::<u8>().ok())
98 .ok_or_else(|| ParsePhpVersionError(s.to_string()))?;
99 let minor = match parts.next() {
100 Some(p) => p
101 .parse::<u8>()
102 .map_err(|_| ParsePhpVersionError(s.to_string()))?,
103 None => 0,
104 };
105 Ok(Self::new(major, minor))
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn parses_major_minor() {
116 assert_eq!("8.2".parse::<PhpVersion>().unwrap(), PhpVersion::new(8, 2));
117 }
118
119 #[test]
120 fn parses_major_minor_patch() {
121 assert_eq!(
122 "8.3.7".parse::<PhpVersion>().unwrap(),
123 PhpVersion::new(8, 3)
124 );
125 }
126
127 #[test]
128 fn parses_major_only() {
129 assert_eq!("7".parse::<PhpVersion>().unwrap(), PhpVersion::new(7, 0));
130 }
131
132 #[test]
133 fn rejects_garbage() {
134 assert!("x.y".parse::<PhpVersion>().is_err());
135 assert!("8.x".parse::<PhpVersion>().is_err());
136 assert!("".parse::<PhpVersion>().is_err());
137 }
138
139 #[test]
140 fn ordered_by_major_then_minor() {
141 assert!(PhpVersion::new(8, 1) < PhpVersion::new(8, 2));
142 assert!(PhpVersion::new(7, 4) < PhpVersion::new(8, 0));
143 }
144
145 #[test]
146 fn displays_as_major_dot_minor() {
147 assert_eq!(PhpVersion::new(8, 3).to_string(), "8.3");
148 }
149
150 #[test]
151 fn includes_symbol_respects_since() {
152 assert!(!PhpVersion::new(7, 4).includes_symbol(Some("8.0"), None));
153 assert!(PhpVersion::new(8, 0).includes_symbol(Some("8.0"), None));
154 assert!(PhpVersion::new(8, 5).includes_symbol(Some("8.0"), None));
155 }
156
157 #[test]
158 fn includes_symbol_respects_removed() {
159 assert!(PhpVersion::new(7, 4).includes_symbol(None, Some("8.0")));
160 assert!(!PhpVersion::new(8, 0).includes_symbol(None, Some("8.0")));
161 assert!(!PhpVersion::new(8, 5).includes_symbol(None, Some("8.0")));
162 }
163
164 #[test]
165 fn includes_symbol_ignores_library_versions() {
166 assert!(PhpVersion::new(8, 5).includes_symbol(Some("9.12"), None));
169 assert!(PhpVersion::new(8, 5).includes_symbol(Some("1.17"), None));
172 assert!(PhpVersion::new(8, 5).includes_symbol(Some("12.0"), None));
173 }
174
175 #[test]
176 fn includes_symbol_ignores_garbage() {
177 assert!(PhpVersion::new(8, 5).includes_symbol(Some("PECL"), None));
178 assert!(PhpVersion::new(8, 5).includes_symbol(Some(""), None));
179 }
180}