1use std::str::FromStr;
2
3use serde::Deserialize;
4use serde::Deserializer;
5use serde::Serialize;
6use serde::Serializer;
7
8use crate::error::ParsingError;
9use crate::feature::Feature;
10
11pub mod error;
12pub mod feature;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30#[repr(transparent)]
31pub struct PHPVersion(u32);
32
33impl PHPVersion {
34 pub const PHP70: PHPVersion = PHPVersion::new(7, 0, 0);
36
37 pub const PHP71: PHPVersion = PHPVersion::new(7, 1, 0);
39
40 pub const PHP72: PHPVersion = PHPVersion::new(7, 2, 0);
42
43 pub const PHP73: PHPVersion = PHPVersion::new(7, 3, 0);
45
46 pub const PHP74: PHPVersion = PHPVersion::new(7, 4, 0);
48
49 pub const PHP80: PHPVersion = PHPVersion::new(8, 0, 0);
51
52 pub const PHP81: PHPVersion = PHPVersion::new(8, 1, 0);
54
55 pub const PHP82: PHPVersion = PHPVersion::new(8, 2, 0);
57
58 pub const PHP83: PHPVersion = PHPVersion::new(8, 3, 0);
60
61 pub const PHP84: PHPVersion = PHPVersion::new(8, 4, 0);
63
64 #[inline]
80 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
81 Self((major << 16) | (minor << 8) | patch)
82 }
83
84 #[inline]
99 pub const fn from_version_id(version_id: u32) -> Self {
100 Self(version_id)
101 }
102
103 #[inline]
114 pub const fn major(&self) -> u32 {
115 self.0 >> 16
116 }
117
118 #[inline]
129 pub const fn minor(&self) -> u32 {
130 (self.0 >> 8) & 0xff
131 }
132
133 #[inline]
144 pub const fn patch(&self) -> u32 {
145 self.0 & 0xff
146 }
147
148 pub const fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
163 self.0 >= ((major << 16) | (minor << 8) | patch)
164 }
165
166 pub const fn is_supported(&self, feature: Feature) -> bool {
183 match feature {
184 Feature::NullableTypeHint
185 | Feature::IterableTypeHint
186 | Feature::VoidTypeHint
187 | Feature::ClassLikeConstantVisibilityModifiers
188 | Feature::CatchUnionType => self.0 >= 0x07_01_00,
189 Feature::TrailingCommaInListSyntax
190 | Feature::ParameterTypeWidening
191 | Feature::AllUnicodeScalarCodePointsInMbSubstituteCharacter => self.0 >= 0x07_02_00,
192 Feature::ListReferenceAssignment | Feature::TrailingCommaInFunctionCalls => self.0 >= 0x07_03_00,
193 Feature::NullCoalesceAssign
194 | Feature::ParameterContravariance
195 | Feature::ReturnCovariance
196 | Feature::PregUnmatchedAsNull
197 | Feature::ArrowFunctions
198 | Feature::NumericLiteralSeparator
199 | Feature::TypedProperties => self.0 >= 0x070400,
200 Feature::NonCapturingCatches
201 | Feature::NativeUnionTypes
202 | Feature::LessOverridenParametersWithVariadic
203 | Feature::ThrowExpression
204 | Feature::ClassConstantOnExpression
205 | Feature::PromotedProperties
206 | Feature::NamedArguments
207 | Feature::ThrowsTypeErrorForInternalFunctions
208 | Feature::ThrowsValueErrorForInternalFunctions
209 | Feature::HHPrintfSpecifier
210 | Feature::StricterRoundFunctions
211 | Feature::ThrowsOnInvalidMbStringEncoding
212 | Feature::WarnsAboutFinalPrivateMethods
213 | Feature::CastsNumbersToStringsOnLooseComparison
214 | Feature::NonNumericStringAndIntegerIsFalseOnLooseComparison
215 | Feature::AbstractTraitMethods
216 | Feature::StaticReturnTypeHint
217 | Feature::AccessClassOnObject
218 | Feature::Attribute
219 | Feature::MixedTypeHint
220 | Feature::MatchExpression
221 | Feature::NullSafeOperator
222 | Feature::TrailingCommaInClosureUseList
223 | Feature::FalseCompoundTypeHint
224 | Feature::NullCompoundTypeHint
225 | Feature::CatchOptionalVariable => self.0 >= 0x08_00_00,
226 Feature::FinalConstants
227 | Feature::ReadonlyProperties
228 | Feature::Enums
229 | Feature::PureIntersectionTypes
230 | Feature::TentativeReturnTypes
231 | Feature::NeverTypeHint
232 | Feature::ClosureCreation
233 | Feature::ArrayUnpackingWithStringKeys
234 | Feature::SerializableRequiresMagicMethods => self.0 >= 0x08_01_00,
235 Feature::ConstantsInTraits
236 | Feature::StrSplitReturnsEmptyArray
237 | Feature::DisjunctiveNormalForm
238 | Feature::ReadonlyClasses
239 | Feature::NeverReturnTypeInArrowFunction
240 | Feature::PregCaptureOnlyNamedGroups
241 | Feature::TrueTypeHint
242 | Feature::FalseTypeHint
243 | Feature::NullTypeHint => self.0 >= 0x08_02_00,
244 Feature::JsonValidate
245 | Feature::TypedClassLikeConstants
246 | Feature::DateTimeExceptions
247 | Feature::OverrideAttribute
248 | Feature::DynamicClassConstantAccess
249 | Feature::ReadonlyAnonymousClasses => self.0 >= 0x08_03_00,
250 Feature::AsymmetricVisibility
251 | Feature::LazyObjects
252 | Feature::HighlightStringDoesNotReturnFalse
253 | Feature::PropertyHooks
254 | Feature::NewWithoutParentheses => self.0 >= 0x08_04_00,
255 Feature::ClosureInConstantExpressions | Feature::ConstantAttribute => self.0 >= 0x08_05_00,
256 Feature::CallableInstanceMethods
257 | Feature::LegacyConstructor
258 | Feature::UnsetCast
259 | Feature::CaseInsensitiveConstantNames
260 | Feature::ArrayFunctionsReturnNullWithNonArray
261 | Feature::SubstrReturnFalseInsteadOfEmptyString
262 | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
263 | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
264 | Feature::NumericStringValidArgInMbSubstituteCharacter
265 | Feature::ShortOpenTag => self.0 < 0x08_00_00,
266 Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
267 Feature::PassNoneEncodings => self.0 < 0x07_03_00,
268 _ => true,
269 }
270 }
271
272 pub const fn is_deprecated(&self, feature: Feature) -> bool {
288 match feature {
289 Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
290 Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
291 Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
292 Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
293 Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
294 _ => false,
295 }
296 }
297
298 pub const fn to_version_id(&self) -> u32 {
311 self.0
312 }
313}
314
315impl std::fmt::Display for PHPVersion {
316 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
317 write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
318 }
319}
320
321impl Serialize for PHPVersion {
322 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
323 where
324 S: Serializer,
325 {
326 serializer.serialize_str(&self.to_string())
327 }
328}
329
330impl<'de> Deserialize<'de> for PHPVersion {
331 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332 where
333 D: Deserializer<'de>,
334 {
335 let s = String::deserialize(deserializer)?;
336
337 s.parse().map_err(serde::de::Error::custom)
338 }
339}
340
341impl FromStr for PHPVersion {
342 type Err = ParsingError;
343
344 fn from_str(s: &str) -> Result<Self, Self::Err> {
345 if s.is_empty() {
346 return Err(ParsingError::InvalidFormat);
347 }
348
349 let parts = s.split('.').collect::<Vec<_>>();
350 match parts.len() {
351 1 => {
352 let major = parts[0].parse()?;
353
354 Ok(Self::new(major, 0, 0))
355 }
356 2 => {
357 let major = parts[0].parse()?;
358 let minor = parts[1].parse()?;
359
360 Ok(Self::new(major, minor, 0))
361 }
362 3 => {
363 let major = parts[0].parse()?;
364 let minor = parts[1].parse()?;
365 let patch = parts[2].parse()?;
366
367 Ok(Self::new(major, minor, patch))
368 }
369 _ => Err(ParsingError::InvalidFormat),
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_version() {
380 let version = PHPVersion::new(7, 4, 0);
381 assert_eq!(version.major(), 7);
382 assert_eq!(version.minor(), 4);
383 assert_eq!(version.patch(), 0);
384 }
385
386 #[test]
387 fn test_display() {
388 let version = PHPVersion::new(7, 4, 0);
389 assert_eq!(version.to_string(), "7.4.0");
390 }
391
392 #[test]
393 fn test_from_str_single_segment() {
394 let v: PHPVersion = "7".parse().unwrap();
395 assert_eq!(v.major(), 7);
396 assert_eq!(v.minor(), 0);
397 assert_eq!(v.patch(), 0);
398 assert_eq!(v.to_string(), "7.0.0");
399 }
400
401 #[test]
402 fn test_from_str_two_segments() {
403 let v: PHPVersion = "7.4".parse().unwrap();
404 assert_eq!(v.major(), 7);
405 assert_eq!(v.minor(), 4);
406 assert_eq!(v.patch(), 0);
407 assert_eq!(v.to_string(), "7.4.0");
408 }
409
410 #[test]
411 fn test_from_str_three_segments() {
412 let v: PHPVersion = "8.1.2".parse().unwrap();
413 assert_eq!(v.major(), 8);
414 assert_eq!(v.minor(), 1);
415 assert_eq!(v.patch(), 2);
416 assert_eq!(v.to_string(), "8.1.2");
417 }
418
419 #[test]
420 fn test_from_str_invalid() {
421 let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
422 assert_eq!(format!("{}", err), "Invalid version format, expected 'major.minor.patch'.");
423
424 let err = "".parse::<PHPVersion>().unwrap_err();
425 assert_eq!(format!("{}", err), "Invalid version format, expected 'major.minor.patch'.");
426
427 let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
428 assert_eq!(format!("{}", err), "Failed to parse integer component of version: invalid digit found in string.");
429
430 let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
431 assert_eq!(format!("{}", err), "Failed to parse integer component of version: invalid digit found in string.");
432
433 let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
434 assert_eq!(format!("{}", err), "Failed to parse integer component of version: invalid digit found in string.");
435 }
436
437 #[test]
438 fn test_is_supported_features_before_8() {
439 let v_7_4_0 = PHPVersion::new(7, 4, 0);
440
441 assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
442 assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
443
444 assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
445 assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
446 }
447
448 #[test]
449 fn test_is_supported_features_8_0_0() {
450 let v_8_0_0 = PHPVersion::new(8, 0, 0);
451
452 assert!(v_8_0_0.is_supported(Feature::NamedArguments));
453 assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
454 }
455
456 #[test]
457 fn test_is_deprecated_features() {
458 let v_7_4_0 = PHPVersion::new(7, 4, 0);
459 assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
460 assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
461
462 let v_8_0_0 = PHPVersion::new(8, 0, 0);
463 assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
464 assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
465
466 let v_8_2_0 = PHPVersion::new(8, 2, 0);
467 assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
468 }
469
470 #[test]
471 fn test_serde_serialize() {
472 let v_7_4_0 = PHPVersion::new(7, 4, 0);
473 let json = serde_json::to_string(&v_7_4_0).unwrap();
474 assert_eq!(json, "\"7.4.0\"");
475 }
476
477 #[test]
478 fn test_serde_deserialize() {
479 let json = "\"7.4.0\"";
480 let v: PHPVersion = serde_json::from_str(json).unwrap();
481 assert_eq!(v.major(), 7);
482 assert_eq!(v.minor(), 4);
483 assert_eq!(v.patch(), 0);
484
485 let json = "\"7.4\"";
486 let v: PHPVersion = serde_json::from_str(json).unwrap();
487 assert_eq!(v.major(), 7);
488 assert_eq!(v.minor(), 4);
489 assert_eq!(v.patch(), 0);
490 }
491
492 #[test]
493 fn test_serde_round_trip() {
494 let original = PHPVersion::new(8, 1, 5);
495 let serialized = serde_json::to_string(&original).unwrap();
496 let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
497 assert_eq!(original, deserialized);
498 assert_eq!(serialized, "\"8.1.5\"");
499 }
500}