From 12a7c9916e03c013c6ebe4c58984d4c3a2d46489 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Fri, 23 Oct 2026 15:53:11 +0200 Subject: [PATCH] enum::tryFrom() and enum::from() takes IntBackedEnum and StringBackedEnum in to account --- .../Php/BackedEnumFromMethodReflection.php | 203 ++++++++++++++++++ .../Php/PhpClassReflectionExtension.php | 12 +- tests/PHPStan/Analyser/nsrt/bug-11421.php | 26 +++ .../Methods/CallStaticMethodsRuleTest.php | 24 +++ .../PHPStan/Rules/Methods/data/bug-11421.php | 34 +++ turbo-ext/src/PhpClassReflectionExtension.cpp | 15 +- turbo-ext/src/support.cpp | 1 + turbo-ext/src/support.h | 1 + 8 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/Reflection/Php/BackedEnumFromMethodReflection.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11421.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-11421.php diff --git a/src/Reflection/Php/BackedEnumFromMethodReflection.php b/src/Reflection/Php/BackedEnumFromMethodReflection.php new file mode 100644 index 00000000000..0faea145a09 --- /dev/null +++ b/src/Reflection/Php/BackedEnumFromMethodReflection.php @@ -0,0 +1,203 @@ +|null */ + private ?array $variants = null; + + public function __construct(private ExtendedMethodReflection $method, private Type $backingType) + { + } + + public function getDeclaringClass(): ClassReflection + { + return $this->method->getDeclaringClass(); + } + + public function isStatic(): bool + { + return $this->method->isStatic(); + } + + public function isPrivate(): bool + { + return $this->method->isPrivate(); + } + + public function isPublic(): bool + { + return $this->method->isPublic(); + } + + public function getDocComment(): ?string + { + return $this->method->getDocComment(); + } + + public function getName(): string + { + return $this->method->getName(); + } + + public function getPrototype(): ClassMemberReflection + { + return $this->method->getPrototype(); + } + + public function getVariants(): array + { + return $this->variants ??= array_map(fn (ExtendedParametersAcceptor $variant): ExtendedParametersAcceptor => new ExtendedFunctionVariant( + $variant->getTemplateTypeMap(), + $variant->getResolvedTemplateTypeMap(), + array_map(fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( + $parameter->getName(), + $this->backingType, + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $this->backingType, + $this->backingType, + $parameter->getOutType(), + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType(), + $parameter->getAttributes(), + $parameter->getAllowedConstants(), + $parameter->isPureUnlessCallableIsImpureParameter(), + $parameter->isPureUnlessParameterPassedParameter(), + ), $variant->getParameters()), + $variant->isVariadic(), + $variant->getReturnType(), + $variant->getPhpDocReturnType(), + $variant->getNativeReturnType(), + $variant->getCallSiteVarianceMap(), + ), $this->method->getVariants()); + } + + public function getOnlyVariant(): ExtendedParametersAcceptor + { + return $this->getVariants()[0]; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; + } + + public function isDeprecated(): TrinaryLogic + { + return $this->method->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->method->getDeprecatedDescription(); + } + + public function isFinal(): TrinaryLogic + { + return $this->method->isFinal(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return $this->method->isFinalByKeyword(); + } + + public function isInternal(): TrinaryLogic + { + return $this->method->isInternal(); + } + + public function isBuiltin(): TrinaryLogic|bool + { + return $this->method->isBuiltin(); + } + + public function getThrowType(): ?Type + { + return $this->method->getThrowType(); + } + + public function hasSideEffects(): TrinaryLogic + { + return $this->method->hasSideEffects(); + } + + public function getAsserts(): Assertions + { + return $this->method->getAsserts(); + } + + public function acceptsNamedArguments(): TrinaryLogic + { + return $this->method->acceptsNamedArguments(); + } + + public function getSelfOutType(): ?Type + { + return $this->method->getSelfOutType(); + } + + public function returnsByReference(): TrinaryLogic + { + return $this->method->returnsByReference(); + } + + public function isAbstract(): TrinaryLogic|bool + { + return $this->method->isAbstract(); + } + + public function isPure(): TrinaryLogic + { + return $this->method->isPure(); + } + + public function getPureUnlessCallableIsImpureParameters(): array + { + return $this->method->getPureUnlessCallableIsImpureParameters(); + } + + public function getPureUnlessParameterPassedParameters(): array + { + return $this->method->getPureUnlessParameterPassedParameters(); + } + + public function getAttributes(): array + { + return $this->method->getAttributes(); + } + + public function mustUseReturnValue(): TrinaryLogic + { + return $this->method->mustUseReturnValue(); + } + + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->method->getResolvedPhpDoc(); + } + +} diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 20f6500bc82..15deda7bf68 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -75,6 +75,7 @@ use function count; use function explode; use function implode; +use function in_array; use function is_array; use function sprintf; use function strtolower; @@ -795,12 +796,21 @@ private function createMethod( ); } - return $this->createUserlandMethodReflection( + $method = $this->createUserlandMethodReflection( $declaringClass, $declaringClass, $methodReflection, $this->findMethodTrait($methodReflection), ); + + if (in_array(strtolower($methodReflection->getName()), ['from', 'tryfrom'], true)) { + $backedEnumType = $declaringClass->getBackedEnumType(); + if ($backedEnumType !== null) { + return new BackedEnumFromMethodReflection($method, $backedEnumType); + } + } + + return $method; } public function createUserlandMethodReflection(ClassReflection $fileDeclaringClass, ClassReflection $actualDeclaringClass, ReflectionMethod $methodReflection, ?string $declaringTraitName): PhpMethodReflection diff --git a/tests/PHPStan/Analyser/nsrt/bug-11421.php b/tests/PHPStan/Analyser/nsrt/bug-11421.php new file mode 100644 index 00000000000..27ca5dda64f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11421.php @@ -0,0 +1,26 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug11421Types; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +enum NumberEnum: int +{ + case One = 1; +} + +enum WordEnum: string +{ + case One = 'one'; +} + +function (): void { + assertType('Closure(int): Bug11421Types\NumberEnum', NumberEnum::from(...)); + assertNativeType('Closure(int): mixed', NumberEnum::from(...)); + assertType('Closure(int): (Bug11421Types\NumberEnum|null)', NumberEnum::tryFrom(...)); + assertType('Closure(string): Bug11421Types\WordEnum', WordEnum::from(...)); + assertType('Closure(string): (Bug11421Types\WordEnum|null)', WordEnum::tryFrom(...)); +}; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index fbed93ba6c9..33f7859302b 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1132,4 +1132,28 @@ public function testBug15251(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug11421(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-11421.php'], [ + [ + 'Parameter #1 $value of static method Bug11421\\NumberEnum::from() expects int, string given.', + 18, + ], + [ + 'Parameter #1 $value of static method Bug11421\\WordEnum::from() expects string, int given.', + 21, + ], + [ + 'Parameter #1 $value of static method Bug11421\\NumberEnum::tryFrom() expects int, string given.', + 24, + ], + [ + 'Parameter #1 $value of static method Bug11421\\WordEnum::tryFrom() expects string, int given.', + 27, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-11421.php b/tests/PHPStan/Rules/Methods/data/bug-11421.php new file mode 100644 index 00000000000..99739b19477 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11421.php @@ -0,0 +1,34 @@ += 8.1 + +namespace Bug11421; + +enum NumberEnum: int +{ + case One = 1; + case Two = 2; +} + +enum WordEnum: string +{ + case One = 'one'; + case Two = 'two'; +} + +$word = 'one'; +NumberEnum::from($word); + +$number = 1; +WordEnum::from($number); + +function (string $s, int $i, \BackedEnum $e): void { + NumberEnum::tryFrom($s); + NumberEnum::from($i); + NumberEnum::tryFrom($i); + WordEnum::tryFrom($i); + WordEnum::from($s); + WordEnum::tryFrom($s); + $e::from($s); + $e::tryFrom($i); + $from = NumberEnum::from(...); + $from($s); +}; diff --git a/turbo-ext/src/PhpClassReflectionExtension.cpp b/turbo-ext/src/PhpClassReflectionExtension.cpp index b883e2ca518..fcec125fe6e 100644 --- a/turbo-ext/src/PhpClassReflectionExtension.cpp +++ b/turbo-ext/src/PhpClassReflectionExtension.cpp @@ -1573,7 +1573,20 @@ class PhpClassReflectionExtension zv::Val methodTrait = findMethodTrait(methodReflection); if (UNEXPECTED(methodTrait.isUndef())) return zv::Val(); - return createUserlandMethodReflection(declaringClass, declaringClass, methodReflection, methodTrait.raw()); + zv::Val method = createUserlandMethodReflection(declaringClass, declaringClass, methodReflection, methodTrait.raw()); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + + zv::Str loweredName = zv::Str::adopt(zend_string_tolower(methodNameStr.get())); + if (zend_string_equals_literal(loweredName.get(), "from") || zend_string_equals_literal(loweredName.get(), "tryfrom")) { + zv::Val backedEnumType = call(declaringClass, PT_LC("getbackedenumtype")); + if (UNEXPECTED(backedEnumType.isUndef())) return zv::Val(); + if (Z_TYPE_P(backedEnumType.raw()) != IS_NULL) { + zv::Args args{method.raw(), backedEnumType.raw()}; + return pt_type_new(PT_CLASS_BACKED_ENUM_FROM_METHOD_REFLECTION, 2, args); + } + } + + return method; } /* diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 4a23023cf17..5f9b626eecc 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -165,6 +165,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_ALWAYS_REMEMBERED_EXPR */ {"alwaysRememberedExpr", "PHPStan\\Node\\Expr\\AlwaysRememberedExpr"}, /* PT_CLASS_NATIVE_METHOD_REFLECTION */ {"nativeMethodReflection", "PHPStan\\Reflection\\Native\\NativeMethodReflection"}, /* PT_CLASS_ENUM_CASES_METHOD_REFLECTION */ {"enumCasesMethodReflection", "PHPStan\\Reflection\\Php\\EnumCasesMethodReflection"}, + /* PT_CLASS_BACKED_ENUM_FROM_METHOD_REFLECTION */ {"backedEnumFromMethodReflection", "PHPStan\\Reflection\\Php\\BackedEnumFromMethodReflection"}, /* PT_CLASS_PRIVATE_PROPERTY_ATTRIBUTE */ {"privatePropertyAttribute", "PHPStan\\Reflection\\Attribute\\PrivateProperty"}, /* PT_CLASS_PROTECTED_PROPERTY_ATTRIBUTE */ {"protectedPropertyAttribute", "PHPStan\\Reflection\\Attribute\\ProtectedProperty"}, /* PT_CLASS_ADAPTER_REFLECTION_METHOD */ {"adapterReflectionMethod", "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod"}, diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index b8606239ab0..68ca6589bdb 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -236,6 +236,7 @@ enum { PT_CLASS_ALWAYS_REMEMBERED_EXPR, PT_CLASS_NATIVE_METHOD_REFLECTION, PT_CLASS_ENUM_CASES_METHOD_REFLECTION, + PT_CLASS_BACKED_ENUM_FROM_METHOD_REFLECTION, PT_CLASS_PRIVATE_PROPERTY_ATTRIBUTE, PT_CLASS_PROTECTED_PROPERTY_ATTRIBUTE, PT_CLASS_ADAPTER_REFLECTION_METHOD,