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