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
33#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, PartialOrd, Deserialize, Serialize, Default)]
49pub struct PHPVersionRange {
50 pub min: Option<PHPVersion>,
51 pub max: Option<PHPVersion>,
52}
53
54impl PHPVersion {
55 pub const PHP70: PHPVersion = PHPVersion::new(7, 0, 0);
57
58 pub const PHP71: PHPVersion = PHPVersion::new(7, 1, 0);
60
61 pub const PHP72: PHPVersion = PHPVersion::new(7, 2, 0);
63
64 pub const PHP73: PHPVersion = PHPVersion::new(7, 3, 0);
66
67 pub const PHP74: PHPVersion = PHPVersion::new(7, 4, 0);
69
70 pub const PHP80: PHPVersion = PHPVersion::new(8, 0, 0);
72
73 pub const PHP81: PHPVersion = PHPVersion::new(8, 1, 0);
75
76 pub const PHP82: PHPVersion = PHPVersion::new(8, 2, 0);
78
79 pub const PHP83: PHPVersion = PHPVersion::new(8, 3, 0);
81
82 pub const PHP84: PHPVersion = PHPVersion::new(8, 4, 0);
84
85 pub const PHP85: PHPVersion = PHPVersion::new(8, 5, 0);
87
88 pub const LATEST: PHPVersion = PHPVersion::PHP84;
97
98 pub const NEXT: PHPVersion = PHPVersion::PHP85;
108
109 #[inline]
125 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
126 Self((major << 16) | (minor << 8) | patch)
127 }
128
129 #[inline]
144 pub const fn from_version_id(version_id: u32) -> Self {
145 Self(version_id)
146 }
147
148 #[inline]
159 pub const fn major(&self) -> u32 {
160 self.0 >> 16
161 }
162
163 #[inline]
174 pub const fn minor(&self) -> u32 {
175 (self.0 >> 8) & 0xff
176 }
177
178 #[inline]
189 pub const fn patch(&self) -> u32 {
190 self.0 & 0xff
191 }
192
193 pub const fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
208 self.0 >= ((major << 16) | (minor << 8) | patch)
209 }
210
211 pub const fn is_supported(&self, feature: Feature) -> bool {
228 match feature {
229 Feature::NullableTypeHint
230 | Feature::IterableTypeHint
231 | Feature::VoidTypeHint
232 | Feature::ClassLikeConstantVisibilityModifiers
233 | Feature::CatchUnionType => self.0 >= 0x07_01_00,
234 Feature::TrailingCommaInListSyntax
235 | Feature::ParameterTypeWidening
236 | Feature::AllUnicodeScalarCodePointsInMbSubstituteCharacter => self.0 >= 0x07_02_00,
237 Feature::ListReferenceAssignment | Feature::TrailingCommaInFunctionCalls => self.0 >= 0x07_03_00,
238 Feature::NullCoalesceAssign
239 | Feature::ParameterContravariance
240 | Feature::ReturnCovariance
241 | Feature::PregUnmatchedAsNull
242 | Feature::ArrowFunctions
243 | Feature::NumericLiteralSeparator
244 | Feature::TypedProperties => self.0 >= 0x070400,
245 Feature::NonCapturingCatches
246 | Feature::NativeUnionTypes
247 | Feature::LessOverriddenParametersWithVariadic
248 | Feature::ThrowExpression
249 | Feature::ClassConstantOnExpression
250 | Feature::PromotedProperties
251 | Feature::NamedArguments
252 | Feature::ThrowsTypeErrorForInternalFunctions
253 | Feature::ThrowsValueErrorForInternalFunctions
254 | Feature::HHPrintfSpecifier
255 | Feature::StricterRoundFunctions
256 | Feature::ThrowsOnInvalidMbStringEncoding
257 | Feature::WarnsAboutFinalPrivateMethods
258 | Feature::CastsNumbersToStringsOnLooseComparison
259 | Feature::NonNumericStringAndIntegerIsFalseOnLooseComparison
260 | Feature::AbstractTraitMethods
261 | Feature::StaticReturnTypeHint
262 | Feature::AccessClassOnObject
263 | Feature::Attributes
264 | Feature::MixedTypeHint
265 | Feature::MatchExpression
266 | Feature::NullSafeOperator
267 | Feature::TrailingCommaInClosureUseList
268 | Feature::FalseCompoundTypeHint
269 | Feature::NullCompoundTypeHint
270 | Feature::CatchOptionalVariable => self.0 >= 0x08_00_00,
271 Feature::FinalConstants
272 | Feature::ReadonlyProperties
273 | Feature::Enums
274 | Feature::PureIntersectionTypes
275 | Feature::TentativeReturnTypes
276 | Feature::NeverTypeHint
277 | Feature::ClosureCreation
278 | Feature::ArrayUnpackingWithStringKeys
279 | Feature::SerializableRequiresMagicMethods => self.0 >= 0x08_01_00,
280 Feature::ConstantsInTraits
281 | Feature::StrSplitReturnsEmptyArray
282 | Feature::DisjunctiveNormalForm
283 | Feature::ReadonlyClasses
284 | Feature::NeverReturnTypeInArrowFunction
285 | Feature::PregCaptureOnlyNamedGroups
286 | Feature::TrueTypeHint
287 | Feature::FalseTypeHint
288 | Feature::NullTypeHint => self.0 >= 0x08_02_00,
289 Feature::JsonValidate
290 | Feature::TypedClassLikeConstants
291 | Feature::DateTimeExceptions
292 | Feature::OverrideAttribute
293 | Feature::DynamicClassConstantAccess
294 | Feature::ReadonlyAnonymousClasses => self.0 >= 0x08_03_00,
295 Feature::AsymmetricVisibility
296 | Feature::LazyObjects
297 | Feature::HighlightStringDoesNotReturnFalse
298 | Feature::PropertyHooks
299 | Feature::NewWithoutParentheses
300 | Feature::DeprecatedAttribute => self.0 >= 0x08_04_00,
301 Feature::ClosureInConstantExpressions
302 | Feature::ConstantAttributes
303 | Feature::NoDiscardAttribute
304 | Feature::VoidCast
305 | Feature::AsymmetricVisibilityForStaticProperties
306 | Feature::ClosureCreationInConstantExpressions
307 | Feature::PipeOperator => self.0 >= 0x08_05_00,
308 Feature::CallableInstanceMethods
309 | Feature::LegacyConstructor
310 | Feature::UnsetCast
311 | Feature::CaseInsensitiveConstantNames
312 | Feature::ArrayFunctionsReturnNullWithNonArray
313 | Feature::SubstrReturnFalseInsteadOfEmptyString
314 | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
315 | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
316 | Feature::NumericStringValidArgInMbSubstituteCharacter => self.0 < 0x08_00_00,
317 Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
318 Feature::PassNoneEncodings => self.0 < 0x07_03_00,
319 _ => true,
320 }
321 }
322
323 pub const fn is_deprecated(&self, feature: Feature) -> bool {
339 match feature {
340 Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
341 Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
342 Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
343 Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
344 Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
345 _ => false,
346 }
347 }
348
349 pub const fn to_version_id(&self) -> u32 {
362 self.0
363 }
364}
365
366impl PHPVersionRange {
367 pub const PHP7: PHPVersionRange = Self::between(PHPVersion::new(7, 0, 0), PHPVersion::new(7, 99, 99));
369
370 pub const PHP8: PHPVersionRange = Self::between(PHPVersion::new(8, 0, 0), PHPVersion::new(8, 99, 99));
372
373 pub const fn any() -> Self {
375 Self { min: None, max: None }
376 }
377
378 pub const fn until(version: PHPVersion) -> Self {
380 Self { min: None, max: Some(version) }
381 }
382
383 pub const fn from(version: PHPVersion) -> Self {
385 Self { min: Some(version), max: None }
386 }
387
388 pub const fn between(min: PHPVersion, max: PHPVersion) -> Self {
390 Self { min: Some(min), max: Some(max) }
391 }
392
393 #[inline]
395 pub const fn includes(&self, version: PHPVersion) -> bool {
396 if let Some(min) = self.min
397 && version.0 < min.0
398 {
399 return false;
400 }
401
402 if let Some(max) = self.max
403 && version.0 > max.0
404 {
405 return false;
406 }
407
408 true
409 }
410}
411
412impl std::default::Default for PHPVersion {
413 fn default() -> Self {
414 Self::LATEST
415 }
416}
417
418impl std::fmt::Display for PHPVersion {
419 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
420 write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
421 }
422}
423
424impl Serialize for PHPVersion {
425 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
426 where
427 S: Serializer,
428 {
429 serializer.serialize_str(&self.to_string())
430 }
431}
432
433impl<'de> Deserialize<'de> for PHPVersion {
434 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
435 where
436 D: Deserializer<'de>,
437 {
438 let s = String::deserialize(deserializer)?;
439
440 s.parse().map_err(serde::de::Error::custom)
441 }
442}
443
444impl FromStr for PHPVersion {
445 type Err = ParsingError;
446
447 fn from_str(s: &str) -> Result<Self, Self::Err> {
448 if s.is_empty() {
449 return Err(ParsingError::InvalidFormat);
450 }
451
452 let parts = s.split('.').collect::<Vec<_>>();
453 match parts.len() {
454 1 => {
455 let major = parts[0].parse()?;
456
457 Ok(Self::new(major, 0, 0))
458 }
459 2 => {
460 let major = parts[0].parse()?;
461 let minor = parts[1].parse()?;
462
463 Ok(Self::new(major, minor, 0))
464 }
465 3 => {
466 let major = parts[0].parse()?;
467 let minor = parts[1].parse()?;
468 let patch = parts[2].parse()?;
469
470 Ok(Self::new(major, minor, patch))
471 }
472 _ => Err(ParsingError::InvalidFormat),
473 }
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_version() {
483 let version = PHPVersion::new(7, 4, 0);
484 assert_eq!(version.major(), 7);
485 assert_eq!(version.minor(), 4);
486 assert_eq!(version.patch(), 0);
487 }
488
489 #[test]
490 fn test_display() {
491 let version = PHPVersion::new(7, 4, 0);
492 assert_eq!(version.to_string(), "7.4.0");
493 }
494
495 #[test]
496 fn test_from_str_single_segment() {
497 let v: PHPVersion = "7".parse().unwrap();
498 assert_eq!(v.major(), 7);
499 assert_eq!(v.minor(), 0);
500 assert_eq!(v.patch(), 0);
501 assert_eq!(v.to_string(), "7.0.0");
502 }
503
504 #[test]
505 fn test_from_str_two_segments() {
506 let v: PHPVersion = "7.4".parse().unwrap();
507 assert_eq!(v.major(), 7);
508 assert_eq!(v.minor(), 4);
509 assert_eq!(v.patch(), 0);
510 assert_eq!(v.to_string(), "7.4.0");
511 }
512
513 #[test]
514 fn test_from_str_three_segments() {
515 let v: PHPVersion = "8.1.2".parse().unwrap();
516 assert_eq!(v.major(), 8);
517 assert_eq!(v.minor(), 1);
518 assert_eq!(v.patch(), 2);
519 assert_eq!(v.to_string(), "8.1.2");
520 }
521
522 #[test]
523 fn test_from_str_invalid() {
524 let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
525 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
526
527 let err = "".parse::<PHPVersion>().unwrap_err();
528 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
529
530 let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
531 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
532
533 let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
534 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
535
536 let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
537 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
538 }
539
540 #[test]
541 fn test_is_supported_features_before_8() {
542 let v_7_4_0 = PHPVersion::new(7, 4, 0);
543
544 assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
545 assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
546
547 assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
548 assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
549 }
550
551 #[test]
552 fn test_is_supported_features_8_0_0() {
553 let v_8_0_0 = PHPVersion::new(8, 0, 0);
554
555 assert!(v_8_0_0.is_supported(Feature::NamedArguments));
556 assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
557 }
558
559 #[test]
560 fn test_is_deprecated_features() {
561 let v_7_4_0 = PHPVersion::new(7, 4, 0);
562 assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
563 assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
564
565 let v_8_0_0 = PHPVersion::new(8, 0, 0);
566 assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
567 assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
568
569 let v_8_2_0 = PHPVersion::new(8, 2, 0);
570 assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
571 }
572
573 #[test]
574 fn test_serde_serialize() {
575 let v_7_4_0 = PHPVersion::new(7, 4, 0);
576 let json = serde_json::to_string(&v_7_4_0).unwrap();
577 assert_eq!(json, "\"7.4.0\"");
578 }
579
580 #[test]
581 fn test_serde_deserialize() {
582 let json = "\"7.4.0\"";
583 let v: PHPVersion = serde_json::from_str(json).unwrap();
584 assert_eq!(v.major(), 7);
585 assert_eq!(v.minor(), 4);
586 assert_eq!(v.patch(), 0);
587
588 let json = "\"7.4\"";
589 let v: PHPVersion = serde_json::from_str(json).unwrap();
590 assert_eq!(v.major(), 7);
591 assert_eq!(v.minor(), 4);
592 assert_eq!(v.patch(), 0);
593 }
594
595 #[test]
596 fn test_serde_round_trip() {
597 let original = PHPVersion::new(8, 1, 5);
598 let serialized = serde_json::to_string(&original).unwrap();
599 let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
600 assert_eq!(original, deserialized);
601 assert_eq!(serialized, "\"8.1.5\"");
602 }
603}