diff --git a/conf/config.neon b/conf/config.neon index 53a58a7684..5a7dcf9802 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1292,6 +1292,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\BackedEnumFromMethodDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - class: PHPStan\Type\Php\Base64DecodeDynamicFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..f5f64e90b8 --- /dev/null +++ b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php @@ -0,0 +1,85 @@ +getName(), ['from', 'tryFrom'], true); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (!$methodReflection->getDeclaringClass()->isBackedEnum()) { + return null; + } + + $arguments = $methodCall->getArgs(); + if (count($arguments) < 1) { + return null; + } + + $valueType = $scope->getType($arguments[0]->value); + + $enumCases = $methodReflection->getDeclaringClass()->getEnumCases(); + if (count($enumCases) === 0) { + if ($methodReflection->getName() === 'tryFrom') { + return new NullType(); + } + + return null; + } + + if (count($valueType->getConstantScalarValues()) === 0) { + return null; + } + + $resultEnumCases = []; + foreach ($enumCases as $enumCase) { + if ($enumCase->getBackingValueType() === null) { + continue; + } + $enumCaseValues = $enumCase->getBackingValueType()->getConstantScalarValues(); + if (count($enumCaseValues) !== 1) { + continue; + } + + foreach ($valueType->getConstantScalarValues() as $value) { + if ($value === $enumCaseValues[0]) { + $resultEnumCases[] = new EnumCaseObjectType($enumCase->getDeclaringEnum()->getName(), $enumCase->getName(), $enumCase->getDeclaringEnum()); + break; + } + } + } + + if (count($resultEnumCases) === 0) { + if ($methodReflection->getName() === 'tryFrom') { + return new NullType(); + } + + return null; + } + + return TypeCombinator::union(...$resultEnumCases); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index f9eb7a9cfb..07ddd09089 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1210,6 +1210,7 @@ public function dataFileAsserts(): iterable if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8486.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9000.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-from.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php'); diff --git a/tests/PHPStan/Analyser/data/enum-from.php b/tests/PHPStan/Analyser/data/enum-from.php new file mode 100644 index 0000000000..d32587ffa6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-from.php @@ -0,0 +1,54 @@ += 8.1 + +namespace EnumFrom; + +use function PHPStan\Testing\assertType; + +enum FooIntegerEnum: int +{ + + case BAR = 1; + case BAZ = 2; + +} + +enum FooStringEnum: string +{ + + case BAR = 'bar'; + case BAZ = 'baz'; + +} + +class Foo +{ + + public function doFoo(): void + { + assertType('1', FooIntegerEnum::BAR->value); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::BAR); + + assertType('null', FooIntegerEnum::tryFrom(0)); + assertType(FooIntegerEnum::class, FooIntegerEnum::from(0)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::tryFrom(0 + 1)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::from(1 * FooIntegerEnum::BAR->value)); + + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(2)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(FooIntegerEnum::BAZ->value)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::from(FooIntegerEnum::BAZ->value)); + + assertType("'bar'", FooStringEnum::BAR->value); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::BAR); + + assertType('null', FooStringEnum::tryFrom('barz')); + assertType(FooStringEnum::class, FooStringEnum::from('barz')); + + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::tryFrom('ba' . 'r')); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::from(sprintf('%s%s', 'ba', 'r'))); + + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom('baz')); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom(FooStringEnum::BAZ->value)); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::from(FooStringEnum::BAZ->value)); + } + +}