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 pub const PHP85: PHPVersion = PHPVersion::new(8, 5, 0);
66
67 pub const LATEST: PHPVersion = PHPVersion::PHP84;
76
77 pub const NEXT: PHPVersion = PHPVersion::PHP85;
87
88 #[inline]
104 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
105 Self((major << 16) | (minor << 8) | patch)
106 }
107
108 #[inline]
123 pub const fn from_version_id(version_id: u32) -> Self {
124 Self(version_id)
125 }
126
127 #[inline]
138 pub const fn major(&self) -> u32 {
139 self.0 >> 16
140 }
141
142 #[inline]
153 pub const fn minor(&self) -> u32 {
154 (self.0 >> 8) & 0xff
155 }
156
157 #[inline]
168 pub const fn patch(&self) -> u32 {
169 self.0 & 0xff
170 }
171
172 pub const fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
187 self.0 >= ((major << 16) | (minor << 8) | patch)
188 }
189
190 pub const fn is_supported(&self, feature: Feature) -> bool {
207 match feature {
208 Feature::NullableTypeHint
209 | Feature::IterableTypeHint
210 | Feature::VoidTypeHint
211 | Feature::ClassLikeConstantVisibilityModifiers
212 | Feature::CatchUnionType => self.0 >= 0x07_01_00,
213 Feature::TrailingCommaInListSyntax
214 | Feature::ParameterTypeWidening
215 | Feature::AllUnicodeScalarCodePointsInMbSubstituteCharacter => self.0 >= 0x07_02_00,
216 Feature::ListReferenceAssignment | Feature::TrailingCommaInFunctionCalls => self.0 >= 0x07_03_00,
217 Feature::NullCoalesceAssign
218 | Feature::ParameterContravariance
219 | Feature::ReturnCovariance
220 | Feature::PregUnmatchedAsNull
221 | Feature::ArrowFunctions
222 | Feature::NumericLiteralSeparator
223 | Feature::TypedProperties => self.0 >= 0x070400,
224 Feature::NonCapturingCatches
225 | Feature::NativeUnionTypes
226 | Feature::LessOverridenParametersWithVariadic
227 | Feature::ThrowExpression
228 | Feature::ClassConstantOnExpression
229 | Feature::PromotedProperties
230 | Feature::NamedArguments
231 | Feature::ThrowsTypeErrorForInternalFunctions
232 | Feature::ThrowsValueErrorForInternalFunctions
233 | Feature::HHPrintfSpecifier
234 | Feature::StricterRoundFunctions
235 | Feature::ThrowsOnInvalidMbStringEncoding
236 | Feature::WarnsAboutFinalPrivateMethods
237 | Feature::CastsNumbersToStringsOnLooseComparison
238 | Feature::NonNumericStringAndIntegerIsFalseOnLooseComparison
239 | Feature::AbstractTraitMethods
240 | Feature::StaticReturnTypeHint
241 | Feature::AccessClassOnObject
242 | Feature::Attributes
243 | Feature::MixedTypeHint
244 | Feature::MatchExpression
245 | Feature::NullSafeOperator
246 | Feature::TrailingCommaInClosureUseList
247 | Feature::FalseCompoundTypeHint
248 | Feature::NullCompoundTypeHint
249 | Feature::CatchOptionalVariable => self.0 >= 0x08_00_00,
250 Feature::FinalConstants
251 | Feature::ReadonlyProperties
252 | Feature::Enums
253 | Feature::PureIntersectionTypes
254 | Feature::TentativeReturnTypes
255 | Feature::NeverTypeHint
256 | Feature::ClosureCreation
257 | Feature::ArrayUnpackingWithStringKeys
258 | Feature::SerializableRequiresMagicMethods => self.0 >= 0x08_01_00,
259 Feature::ConstantsInTraits
260 | Feature::StrSplitReturnsEmptyArray
261 | Feature::DisjunctiveNormalForm
262 | Feature::ReadonlyClasses
263 | Feature::NeverReturnTypeInArrowFunction
264 | Feature::PregCaptureOnlyNamedGroups
265 | Feature::TrueTypeHint
266 | Feature::FalseTypeHint
267 | Feature::NullTypeHint => self.0 >= 0x08_02_00,
268 Feature::JsonValidate
269 | Feature::TypedClassLikeConstants
270 | Feature::DateTimeExceptions
271 | Feature::OverrideAttribute
272 | Feature::DynamicClassConstantAccess
273 | Feature::ReadonlyAnonymousClasses => self.0 >= 0x08_03_00,
274 Feature::AsymmetricVisibility
275 | Feature::LazyObjects
276 | Feature::HighlightStringDoesNotReturnFalse
277 | Feature::PropertyHooks
278 | Feature::NewWithoutParentheses
279 | Feature::DeprecatedAttribute => self.0 >= 0x08_04_00,
280 Feature::ClosureInConstantExpressions
281 | Feature::ConstantAttributes
282 | Feature::NoDiscardAttribute
283 | Feature::VoidCast
284 | Feature::AsymmetricVisibilityForStaticProperties
285 | Feature::ClosureCreationInConstantExpressions
286 | Feature::PipeOperator => self.0 >= 0x08_05_00,
287 Feature::CallableInstanceMethods
288 | Feature::LegacyConstructor
289 | Feature::UnsetCast
290 | Feature::CaseInsensitiveConstantNames
291 | Feature::ArrayFunctionsReturnNullWithNonArray
292 | Feature::SubstrReturnFalseInsteadOfEmptyString
293 | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
294 | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
295 | Feature::NumericStringValidArgInMbSubstituteCharacter => self.0 < 0x08_00_00,
296 Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
297 Feature::PassNoneEncodings => self.0 < 0x07_03_00,
298 _ => true,
299 }
300 }
301
302 pub const fn is_deprecated(&self, feature: Feature) -> bool {
318 match feature {
319 Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
320 Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
321 Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
322 Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
323 Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
324 _ => false,
325 }
326 }
327
328 pub const fn to_version_id(&self) -> u32 {
341 self.0
342 }
343}
344
345impl std::fmt::Display for PHPVersion {
346 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
347 write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
348 }
349}
350
351impl Serialize for PHPVersion {
352 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
353 where
354 S: Serializer,
355 {
356 serializer.serialize_str(&self.to_string())
357 }
358}
359
360impl<'de> Deserialize<'de> for PHPVersion {
361 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
362 where
363 D: Deserializer<'de>,
364 {
365 let s = String::deserialize(deserializer)?;
366
367 s.parse().map_err(serde::de::Error::custom)
368 }
369}
370
371impl FromStr for PHPVersion {
372 type Err = ParsingError;
373
374 fn from_str(s: &str) -> Result<Self, Self::Err> {
375 if s.is_empty() {
376 return Err(ParsingError::InvalidFormat);
377 }
378
379 let parts = s.split('.').collect::<Vec<_>>();
380 match parts.len() {
381 1 => {
382 let major = parts[0].parse()?;
383
384 Ok(Self::new(major, 0, 0))
385 }
386 2 => {
387 let major = parts[0].parse()?;
388 let minor = parts[1].parse()?;
389
390 Ok(Self::new(major, minor, 0))
391 }
392 3 => {
393 let major = parts[0].parse()?;
394 let minor = parts[1].parse()?;
395 let patch = parts[2].parse()?;
396
397 Ok(Self::new(major, minor, patch))
398 }
399 _ => Err(ParsingError::InvalidFormat),
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn test_version() {
410 let version = PHPVersion::new(7, 4, 0);
411 assert_eq!(version.major(), 7);
412 assert_eq!(version.minor(), 4);
413 assert_eq!(version.patch(), 0);
414 }
415
416 #[test]
417 fn test_display() {
418 let version = PHPVersion::new(7, 4, 0);
419 assert_eq!(version.to_string(), "7.4.0");
420 }
421
422 #[test]
423 fn test_from_str_single_segment() {
424 let v: PHPVersion = "7".parse().unwrap();
425 assert_eq!(v.major(), 7);
426 assert_eq!(v.minor(), 0);
427 assert_eq!(v.patch(), 0);
428 assert_eq!(v.to_string(), "7.0.0");
429 }
430
431 #[test]
432 fn test_from_str_two_segments() {
433 let v: PHPVersion = "7.4".parse().unwrap();
434 assert_eq!(v.major(), 7);
435 assert_eq!(v.minor(), 4);
436 assert_eq!(v.patch(), 0);
437 assert_eq!(v.to_string(), "7.4.0");
438 }
439
440 #[test]
441 fn test_from_str_three_segments() {
442 let v: PHPVersion = "8.1.2".parse().unwrap();
443 assert_eq!(v.major(), 8);
444 assert_eq!(v.minor(), 1);
445 assert_eq!(v.patch(), 2);
446 assert_eq!(v.to_string(), "8.1.2");
447 }
448
449 #[test]
450 fn test_from_str_invalid() {
451 let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
452 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
453
454 let err = "".parse::<PHPVersion>().unwrap_err();
455 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
456
457 let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
458 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
459
460 let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
461 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
462
463 let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
464 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
465 }
466
467 #[test]
468 fn test_is_supported_features_before_8() {
469 let v_7_4_0 = PHPVersion::new(7, 4, 0);
470
471 assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
472 assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
473
474 assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
475 assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
476 }
477
478 #[test]
479 fn test_is_supported_features_8_0_0() {
480 let v_8_0_0 = PHPVersion::new(8, 0, 0);
481
482 assert!(v_8_0_0.is_supported(Feature::NamedArguments));
483 assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
484 }
485
486 #[test]
487 fn test_is_deprecated_features() {
488 let v_7_4_0 = PHPVersion::new(7, 4, 0);
489 assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
490 assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
491
492 let v_8_0_0 = PHPVersion::new(8, 0, 0);
493 assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
494 assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
495
496 let v_8_2_0 = PHPVersion::new(8, 2, 0);
497 assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
498 }
499
500 #[test]
501 fn test_serde_serialize() {
502 let v_7_4_0 = PHPVersion::new(7, 4, 0);
503 let json = serde_json::to_string(&v_7_4_0).unwrap();
504 assert_eq!(json, "\"7.4.0\"");
505 }
506
507 #[test]
508 fn test_serde_deserialize() {
509 let json = "\"7.4.0\"";
510 let v: PHPVersion = serde_json::from_str(json).unwrap();
511 assert_eq!(v.major(), 7);
512 assert_eq!(v.minor(), 4);
513 assert_eq!(v.patch(), 0);
514
515 let json = "\"7.4\"";
516 let v: PHPVersion = serde_json::from_str(json).unwrap();
517 assert_eq!(v.major(), 7);
518 assert_eq!(v.minor(), 4);
519 assert_eq!(v.patch(), 0);
520 }
521
522 #[test]
523 fn test_serde_round_trip() {
524 let original = PHPVersion::new(8, 1, 5);
525 let serialized = serde_json::to_string(&original).unwrap();
526 let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
527 assert_eq!(original, deserialized);
528 assert_eq!(serialized, "\"8.1.5\"");
529 }
530}