Skip to content

Commit

Permalink
Backed enums - dynamic return type extension for from() and tryFrom()
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Mar 10, 2023
1 parent 8e4a487 commit edcaaba
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\BackedEnumFromMethodDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension

-
class: PHPStan\Type\Php\Base64DecodeDynamicFunctionReturnTypeExtension
tags:
Expand Down
85 changes: 85 additions & 0 deletions src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use BackedEnum;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\Enum\EnumCaseObjectType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;

class BackedEnumFromMethodDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{

public function getClass(): string
{
return BackedEnum::class;
}

public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->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);
}

}
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
54 changes: 54 additions & 0 deletions tests/PHPStan/Analyser/data/enum-from.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php // lint >= 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));
}

}

0 comments on commit edcaaba

Please sign in to comment.