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
296 | Feature::ShortOpenTag => self.0 < 0x08_00_00,
297 Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
298 Feature::PassNoneEncodings => self.0 < 0x07_03_00,
299 _ => true,
300 }
301 }
302
303 pub const fn is_deprecated(&self, feature: Feature) -> bool {
319 match feature {
320 Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
321 Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
322 Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
323 Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
324 Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
325 _ => false,
326 }
327 }
328
329 pub const fn to_version_id(&self) -> u32 {
342 self.0
343 }
344}
345
346impl std::fmt::Display for PHPVersion {
347 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
348 write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
349 }
350}
351
352impl Serialize for PHPVersion {
353 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
354 where
355 S: Serializer,
356 {
357 serializer.serialize_str(&self.to_string())
358 }
359}
360
361impl<'de> Deserialize<'de> for PHPVersion {
362 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
363 where
364 D: Deserializer<'de>,
365 {
366 let s = String::deserialize(deserializer)?;
367
368 s.parse().map_err(serde::de::Error::custom)
369 }
370}
371
372impl FromStr for PHPVersion {
373 type Err = ParsingError;
374
375 fn from_str(s: &str) -> Result<Self, Self::Err> {
376 if s.is_empty() {
377 return Err(ParsingError::InvalidFormat);
378 }
379
380 let parts = s.split('.').collect::<Vec<_>>();
381 match parts.len() {
382 1 => {
383 let major = parts[0].parse()?;
384
385 Ok(Self::new(major, 0, 0))
386 }
387 2 => {
388 let major = parts[0].parse()?;
389 let minor = parts[1].parse()?;
390
391 Ok(Self::new(major, minor, 0))
392 }
393 3 => {
394 let major = parts[0].parse()?;
395 let minor = parts[1].parse()?;
396 let patch = parts[2].parse()?;
397
398 Ok(Self::new(major, minor, patch))
399 }
400 _ => Err(ParsingError::InvalidFormat),
401 }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_version() {
411 let version = PHPVersion::new(7, 4, 0);
412 assert_eq!(version.major(), 7);
413 assert_eq!(version.minor(), 4);
414 assert_eq!(version.patch(), 0);
415 }
416
417 #[test]
418 fn test_display() {
419 let version = PHPVersion::new(7, 4, 0);
420 assert_eq!(version.to_string(), "7.4.0");
421 }
422
423 #[test]
424 fn test_from_str_single_segment() {
425 let v: PHPVersion = "7".parse().unwrap();
426 assert_eq!(v.major(), 7);
427 assert_eq!(v.minor(), 0);
428 assert_eq!(v.patch(), 0);
429 assert_eq!(v.to_string(), "7.0.0");
430 }
431
432 #[test]
433 fn test_from_str_two_segments() {
434 let v: PHPVersion = "7.4".parse().unwrap();
435 assert_eq!(v.major(), 7);
436 assert_eq!(v.minor(), 4);
437 assert_eq!(v.patch(), 0);
438 assert_eq!(v.to_string(), "7.4.0");
439 }
440
441 #[test]
442 fn test_from_str_three_segments() {
443 let v: PHPVersion = "8.1.2".parse().unwrap();
444 assert_eq!(v.major(), 8);
445 assert_eq!(v.minor(), 1);
446 assert_eq!(v.patch(), 2);
447 assert_eq!(v.to_string(), "8.1.2");
448 }
449
450 #[test]
451 fn test_from_str_invalid() {
452 let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
453 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
454
455 let err = "".parse::<PHPVersion>().unwrap_err();
456 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
457
458 let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
459 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
460
461 let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
462 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
463
464 let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
465 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
466 }
467
468 #[test]
469 fn test_is_supported_features_before_8() {
470 let v_7_4_0 = PHPVersion::new(7, 4, 0);
471
472 assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
473 assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
474
475 assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
476 assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
477 }
478
479 #[test]
480 fn test_is_supported_features_8_0_0() {
481 let v_8_0_0 = PHPVersion::new(8, 0, 0);
482
483 assert!(v_8_0_0.is_supported(Feature::NamedArguments));
484 assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
485 }
486
487 #[test]
488 fn test_is_deprecated_features() {
489 let v_7_4_0 = PHPVersion::new(7, 4, 0);
490 assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
491 assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
492
493 let v_8_0_0 = PHPVersion::new(8, 0, 0);
494 assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
495 assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
496
497 let v_8_2_0 = PHPVersion::new(8, 2, 0);
498 assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
499 }
500
501 #[test]
502 fn test_serde_serialize() {
503 let v_7_4_0 = PHPVersion::new(7, 4, 0);
504 let json = serde_json::to_string(&v_7_4_0).unwrap();
505 assert_eq!(json, "\"7.4.0\"");
506 }
507
508 #[test]
509 fn test_serde_deserialize() {
510 let json = "\"7.4.0\"";
511 let v: PHPVersion = serde_json::from_str(json).unwrap();
512 assert_eq!(v.major(), 7);
513 assert_eq!(v.minor(), 4);
514 assert_eq!(v.patch(), 0);
515
516 let json = "\"7.4\"";
517 let v: PHPVersion = serde_json::from_str(json).unwrap();
518 assert_eq!(v.major(), 7);
519 assert_eq!(v.minor(), 4);
520 assert_eq!(v.patch(), 0);
521 }
522
523 #[test]
524 fn test_serde_round_trip() {
525 let original = PHPVersion::new(8, 1, 5);
526 let serialized = serde_json::to_string(&original).unwrap();
527 let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
528 assert_eq!(original, deserialized);
529 assert_eq!(serialized, "\"8.1.5\"");
530 }
531}