1use std::str::FromStr;
2
3use schemars::JsonSchema;
4use serde::Deserialize;
5use serde::Deserializer;
6use serde::Serialize;
7use serde::Serializer;
8
9use crate::error::ParsingError;
10use crate::feature::Feature;
11
12pub mod error;
13pub mod feature;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
31#[schemars(with = "String")]
32#[repr(transparent)]
33pub struct PHPVersion(u32);
34
35#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, PartialOrd, Deserialize, Serialize, Default, Hash, JsonSchema)]
51pub struct PHPVersionRange {
52 pub min: Option<PHPVersion>,
53 pub max: Option<PHPVersion>,
54}
55
56impl PHPVersion {
57 pub const PHP70: PHPVersion = PHPVersion::new(7, 0, 0);
59
60 pub const PHP71: PHPVersion = PHPVersion::new(7, 1, 0);
62
63 pub const PHP72: PHPVersion = PHPVersion::new(7, 2, 0);
65
66 pub const PHP73: PHPVersion = PHPVersion::new(7, 3, 0);
68
69 pub const PHP74: PHPVersion = PHPVersion::new(7, 4, 0);
71
72 pub const PHP80: PHPVersion = PHPVersion::new(8, 0, 0);
74
75 pub const PHP81: PHPVersion = PHPVersion::new(8, 1, 0);
77
78 pub const PHP82: PHPVersion = PHPVersion::new(8, 2, 0);
80
81 pub const PHP83: PHPVersion = PHPVersion::new(8, 3, 0);
83
84 pub const PHP84: PHPVersion = PHPVersion::new(8, 4, 0);
86
87 pub const PHP85: PHPVersion = PHPVersion::new(8, 5, 0);
89
90 pub const PHP86: PHPVersion = PHPVersion::new(8, 6, 0);
92
93 pub const LATEST: PHPVersion = PHPVersion::PHP85;
102
103 pub const NEXT: PHPVersion = PHPVersion::PHP86;
113
114 #[inline]
130 #[must_use]
131 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
132 Self((major << 16) | (minor << 8) | patch)
133 }
134
135 #[inline]
150 #[must_use]
151 pub const fn from_version_id(version_id: u32) -> Self {
152 Self(version_id)
153 }
154
155 #[inline]
166 #[must_use]
167 pub const fn major(&self) -> u32 {
168 self.0 >> 16
169 }
170
171 #[inline]
182 #[must_use]
183 pub const fn minor(&self) -> u32 {
184 (self.0 >> 8) & 0xff
185 }
186
187 #[inline]
198 #[must_use]
199 pub const fn patch(&self) -> u32 {
200 self.0 & 0xff
201 }
202
203 #[inline]
218 #[must_use]
219 pub const fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
220 self.0 >= ((major << 16) | (minor << 8) | patch)
221 }
222
223 #[inline]
240 #[must_use]
241 pub const fn is_supported(&self, feature: Feature) -> bool {
242 match feature {
243 Feature::NullableTypeHint
244 | Feature::IterableTypeHint
245 | Feature::VoidTypeHint
246 | Feature::ClassLikeConstantVisibilityModifiers
247 | Feature::CatchUnionType => self.0 >= 0x07_01_00,
248 Feature::TrailingCommaInListSyntax
249 | Feature::ParameterTypeWidening
250 | Feature::AllUnicodeScalarCodePointsInMbSubstituteCharacter => self.0 >= 0x07_02_00,
251 Feature::ListReferenceAssignment | Feature::TrailingCommaInFunctionCalls => self.0 >= 0x07_03_00,
252 Feature::NullCoalesceAssign
253 | Feature::ParameterContravariance
254 | Feature::ReturnCovariance
255 | Feature::PregUnmatchedAsNull
256 | Feature::ArrowFunctions
257 | Feature::NumericLiteralSeparator
258 | Feature::TypedProperties => self.0 >= 0x07_04_00,
259 Feature::NonCapturingCatches
260 | Feature::NativeUnionTypes
261 | Feature::LessOverriddenParametersWithVariadic
262 | Feature::ThrowExpression
263 | Feature::ClassConstantOnExpression
264 | Feature::PromotedProperties
265 | Feature::NamedArguments
266 | Feature::ThrowsTypeErrorForInternalFunctions
267 | Feature::ThrowsValueErrorForInternalFunctions
268 | Feature::HHPrintfSpecifier
269 | Feature::StricterRoundFunctions
270 | Feature::ThrowsOnInvalidMbStringEncoding
271 | Feature::WarnsAboutFinalPrivateMethods
272 | Feature::CastsNumbersToStringsOnLooseComparison
273 | Feature::NonNumericStringAndIntegerIsFalseOnLooseComparison
274 | Feature::AbstractTraitMethods
275 | Feature::StaticReturnTypeHint
276 | Feature::AccessClassOnObject
277 | Feature::Attributes
278 | Feature::MixedTypeHint
279 | Feature::MatchExpression
280 | Feature::NullSafeOperator
281 | Feature::TrailingCommaInClosureUseList
282 | Feature::FalseCompoundTypeHint
283 | Feature::NullCompoundTypeHint
284 | Feature::CatchOptionalVariable => self.0 >= 0x08_00_00,
285 Feature::FinalConstants
286 | Feature::ReadonlyProperties
287 | Feature::Enums
288 | Feature::PureIntersectionTypes
289 | Feature::TentativeReturnTypes
290 | Feature::NeverTypeHint
291 | Feature::ClosureCreation
292 | Feature::ArrayUnpackingWithStringKeys
293 | Feature::SerializableRequiresMagicMethods => self.0 >= 0x08_01_00,
294 Feature::ConstantsInTraits
295 | Feature::StrSplitReturnsEmptyArray
296 | Feature::DisjunctiveNormalForm
297 | Feature::ReadonlyClasses
298 | Feature::NeverReturnTypeInArrowFunction
299 | Feature::PregCaptureOnlyNamedGroups
300 | Feature::TrueTypeHint
301 | Feature::FalseTypeHint
302 | Feature::NullTypeHint => self.0 >= 0x08_02_00,
303 Feature::JsonValidate
304 | Feature::TypedClassLikeConstants
305 | Feature::DateTimeExceptions
306 | Feature::OverrideAttribute
307 | Feature::DynamicClassConstantAccess
308 | Feature::ReadonlyAnonymousClasses
309 | Feature::ReadonlyPropertyReinitializationInClone => self.0 >= 0x08_03_00,
310 Feature::AsymmetricVisibility
311 | Feature::LazyObjects
312 | Feature::HighlightStringDoesNotReturnFalse
313 | Feature::PropertyHooks
314 | Feature::NewWithoutParentheses
315 | Feature::DeprecatedAttribute => self.0 >= 0x08_04_00,
316 Feature::ClosureInConstantExpressions
317 | Feature::ConstantAttributes
318 | Feature::NoDiscardAttribute
319 | Feature::VoidCast
320 | Feature::CloneWith
321 | Feature::AsymmetricVisibilityForStaticProperties
322 | Feature::ClosureCreationInConstantExpressions
323 | Feature::PipeOperator => self.0 >= 0x08_05_00,
324 Feature::CallableInstanceMethods
325 | Feature::LegacyConstructor
326 | Feature::UnsetCast
327 | Feature::CaseInsensitiveConstantNames
328 | Feature::ArrayFunctionsReturnNullWithNonArray
329 | Feature::SubstrReturnFalseInsteadOfEmptyString
330 | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
331 | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
332 | Feature::NumericStringValidArgInMbSubstituteCharacter => self.0 < 0x08_00_00,
333 Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
334 Feature::PassNoneEncodings => self.0 < 0x07_03_00,
335 Feature::ImplicitlyNullableParameterTypes => self.0 < 0x09_00_00,
336 Feature::PartialFunctionApplication => self.0 >= 0x08_06_00,
337 _ => true,
338 }
339 }
340
341 #[inline]
357 #[must_use]
358 pub const fn is_deprecated(&self, feature: Feature) -> bool {
359 match feature {
360 Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
361 Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
362 Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
363 Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
364 Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
365 Feature::SwitchSemicolonSeparators => self.0 >= 0x08_05_00,
366 _ => false,
367 }
368 }
369
370 #[inline]
383 #[must_use]
384 pub const fn to_version_id(&self) -> u32 {
385 self.0
386 }
387}
388
389impl PHPVersionRange {
390 pub const PHP7: PHPVersionRange = Self::between(PHPVersion::new(7, 0, 0), PHPVersion::new(7, 99, 99));
392
393 pub const PHP8: PHPVersionRange = Self::between(PHPVersion::new(8, 0, 0), PHPVersion::new(8, 99, 99));
395
396 #[inline]
398 #[must_use]
399 pub const fn any() -> Self {
400 Self { min: None, max: None }
401 }
402
403 #[inline]
405 #[must_use]
406 pub const fn until(version: PHPVersion) -> Self {
407 Self { min: None, max: Some(version) }
408 }
409
410 #[inline]
412 #[must_use]
413 pub const fn from(version: PHPVersion) -> Self {
414 Self { min: Some(version), max: None }
415 }
416
417 #[inline]
419 #[must_use]
420 pub const fn between(min: PHPVersion, max: PHPVersion) -> Self {
421 Self { min: Some(min), max: Some(max) }
422 }
423
424 #[inline]
426 #[must_use]
427 pub const fn includes(&self, version: PHPVersion) -> bool {
428 if let Some(min) = self.min
429 && version.0 < min.0
430 {
431 return false;
432 }
433
434 if let Some(max) = self.max
435 && version.0 > max.0
436 {
437 return false;
438 }
439
440 true
441 }
442}
443
444impl std::default::Default for PHPVersion {
445 #[inline]
446 fn default() -> Self {
447 Self::LATEST
448 }
449}
450
451impl std::fmt::Display for PHPVersion {
452 #[inline]
453 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
454 write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
455 }
456}
457
458impl Serialize for PHPVersion {
459 #[inline]
460 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
461 where
462 S: Serializer,
463 {
464 serializer.serialize_str(&self.to_string())
465 }
466}
467
468impl<'de> Deserialize<'de> for PHPVersion {
469 #[inline]
470 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
471 where
472 D: Deserializer<'de>,
473 {
474 let s = String::deserialize(deserializer)?;
475
476 s.parse().map_err(serde::de::Error::custom)
477 }
478}
479
480impl FromStr for PHPVersion {
481 type Err = ParsingError;
482
483 #[inline]
484 fn from_str(s: &str) -> Result<Self, Self::Err> {
485 if s.is_empty() {
486 return Err(ParsingError::InvalidFormat);
487 }
488
489 let parts = s.split('.').collect::<Vec<_>>();
490 match parts.len() {
491 1 => {
492 let major = parts[0].parse()?;
493
494 Ok(Self::new(major, 0, 0))
495 }
496 2 => {
497 let major = parts[0].parse()?;
498 let minor = parts[1].parse()?;
499
500 Ok(Self::new(major, minor, 0))
501 }
502 3 => {
503 let major = parts[0].parse()?;
504 let minor = parts[1].parse()?;
505 let patch = parts[2].parse()?;
506
507 Ok(Self::new(major, minor, patch))
508 }
509 _ => Err(ParsingError::InvalidFormat),
510 }
511 }
512}
513
514#[cfg(test)]
515#[allow(clippy::unwrap_used)]
516mod tests {
517 use super::*;
518
519 #[test]
520 fn test_version() {
521 let version = PHPVersion::new(7, 4, 0);
522 assert_eq!(version.major(), 7);
523 assert_eq!(version.minor(), 4);
524 assert_eq!(version.patch(), 0);
525 }
526
527 #[test]
528 fn test_display() {
529 let version = PHPVersion::new(7, 4, 0);
530 assert_eq!(version.to_string(), "7.4.0");
531 }
532
533 #[test]
534 fn test_from_str_single_segment() {
535 let v: PHPVersion = "7".parse().unwrap();
536 assert_eq!(v.major(), 7);
537 assert_eq!(v.minor(), 0);
538 assert_eq!(v.patch(), 0);
539 assert_eq!(v.to_string(), "7.0.0");
540 }
541
542 #[test]
543 fn test_from_str_two_segments() {
544 let v: PHPVersion = "7.4".parse().unwrap();
545 assert_eq!(v.major(), 7);
546 assert_eq!(v.minor(), 4);
547 assert_eq!(v.patch(), 0);
548 assert_eq!(v.to_string(), "7.4.0");
549 }
550
551 #[test]
552 fn test_from_str_three_segments() {
553 let v: PHPVersion = "8.1.2".parse().unwrap();
554 assert_eq!(v.major(), 8);
555 assert_eq!(v.minor(), 1);
556 assert_eq!(v.patch(), 2);
557 assert_eq!(v.to_string(), "8.1.2");
558 }
559
560 #[test]
561 fn test_from_str_invalid() {
562 let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
563 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
564
565 let err = "".parse::<PHPVersion>().unwrap_err();
566 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
567
568 let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
569 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
570
571 let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
572 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
573
574 let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
575 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
576 }
577
578 #[test]
579 fn test_is_supported_features_before_8() {
580 let v_7_4_0 = PHPVersion::new(7, 4, 0);
581
582 assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
583 assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
584
585 assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
586 assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
587 }
588
589 #[test]
590 fn test_is_supported_features_8_0_0() {
591 let v_8_0_0 = PHPVersion::new(8, 0, 0);
592
593 assert!(v_8_0_0.is_supported(Feature::NamedArguments));
594 assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
595 }
596
597 #[test]
598 fn test_is_deprecated_features() {
599 let v_7_4_0 = PHPVersion::new(7, 4, 0);
600 assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
601 assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
602
603 let v_8_0_0 = PHPVersion::new(8, 0, 0);
604 assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
605 assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
606
607 let v_8_2_0 = PHPVersion::new(8, 2, 0);
608 assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
609 }
610
611 #[test]
612 fn test_serde_serialize() {
613 let v_7_4_0 = PHPVersion::new(7, 4, 0);
614 let json = serde_json::to_string(&v_7_4_0).unwrap();
615 assert_eq!(json, "\"7.4.0\"");
616 }
617
618 #[test]
619 fn test_serde_deserialize() {
620 let json = "\"7.4.0\"";
621 let v: PHPVersion = serde_json::from_str(json).unwrap();
622 assert_eq!(v.major(), 7);
623 assert_eq!(v.minor(), 4);
624 assert_eq!(v.patch(), 0);
625
626 let json = "\"7.4\"";
627 let v: PHPVersion = serde_json::from_str(json).unwrap();
628 assert_eq!(v.major(), 7);
629 assert_eq!(v.minor(), 4);
630 assert_eq!(v.patch(), 0);
631 }
632
633 #[test]
634 fn test_serde_round_trip() {
635 let original = PHPVersion::new(8, 1, 5);
636 let serialized = serde_json::to_string(&original).unwrap();
637 let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
638 assert_eq!(original, deserialized);
639 assert_eq!(serialized, "\"8.1.5\"");
640 }
641}