Skip to content

Commit be25773

Browse files
committed
Added dynamic return type extension for Enum::getValue()
1 parent 0896bd0 commit be25773

File tree

7 files changed

+216
-1
lines changed

7 files changed

+216
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
> It moves PHP closer to compiled languages in the sense that the correctness
1010
> of each line of the code can be checked before you run the actual line.
1111
12-
This PHPStan extension makes enumerator accessor methods known to PHPStan.
12+
This PHPStan extension makes enumerator accessor methods and enum possible values known to PHPStan.
1313

1414
## Install
1515

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ services:
22
- class: MabeEnumPHPStan\EnumMethodsClassReflectionExtension
33
tags:
44
- phpstan.broker.methodsClassReflectionExtension
5+
6+
- class: MabeEnumPHPStan\EnumGetValueDynamicReturnTypeExtension
7+
tags:
8+
- phpstan.broker.dynamicMethodReturnTypeExtension
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MabeEnumPHPStan;
6+
7+
use MabeEnum\Enum;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\ShouldNotHappenException;
13+
use PHPStan\Type\BooleanType;
14+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15+
use PHPStan\Type\FloatType;
16+
use PHPStan\Type\IntegerType;
17+
use PHPStan\Type\StringType;
18+
use PHPStan\Type\Type;
19+
use PHPStan\Type\TypeCombinator;
20+
use PHPStan\Type\VerbosityLevel;
21+
22+
class EnumGetValueDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
23+
{
24+
/** @var \PHPStan\Type\Type[] */
25+
private $enumTypes = [];
26+
27+
public function getClass(): string
28+
{
29+
return Enum::class;
30+
}
31+
32+
public function isMethodSupported(MethodReflection $methodReflection): bool
33+
{
34+
// version 1.x has visibility bug in getConstants()
35+
return method_exists(Enum::class, 'getValues') && $methodReflection->getName() === 'getValue';
36+
}
37+
38+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
39+
{
40+
$enumType = $scope->getType($methodCall->var);
41+
if (count($enumType->getReferencedClasses()) !== 1) {
42+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
43+
}
44+
45+
/** @var string $enumClass */
46+
$enumClass = $enumType->getReferencedClasses()[0];
47+
if (array_key_exists($enumClass, $this->enumTypes)) {
48+
return $this->enumTypes[$enumClass];
49+
}
50+
$types = array_map(function ($value) use ($scope): Type {
51+
return $scope->getTypeFromValue($value);
52+
}, self::getEnumValues($enumClass));
53+
54+
$this->enumTypes[$enumClass] = TypeCombinator::union(...$types);
55+
56+
return $this->enumTypes[$enumClass];
57+
}
58+
59+
/**
60+
* @phpstan-param class-string<Enum> $enumClass
61+
*/
62+
private static function getEnumValues(string $enumClass): array
63+
{
64+
if (method_exists($enumClass, 'getValues')) {
65+
return $enumClass::getValues();
66+
}
67+
68+
throw new \PHPStan\ShouldNotHappenException();
69+
}
70+
}

tests/Assets/BigStrEnum.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MabeEnumPHPStanTest\Assets;
6+
7+
use MabeEnum\Enum;
8+
9+
class BigStrEnum extends Enum
10+
{
11+
const C_1 = 'c1';
12+
const C_2 = 'c2';
13+
const C_3 = 'c3';
14+
const C_4 = 'c4';
15+
const C_5 = 'c5';
16+
const C_6 = 'c6';
17+
const C_7 = 'c7';
18+
const C_8 = 'c8';
19+
const C_9 = 'c9';
20+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MabeEnumPHPStanTest;
6+
7+
use MabeEnum\Enum;
8+
use MabeEnumPHPStan\EnumGetValueDynamicReturnTypeExtension;
9+
10+
final class EnumGetValueDynamicReturnTypeExtensionTest extends ExtensionTestCase
11+
{
12+
/**
13+
* @var \PHPStan\Broker\Broker
14+
*/
15+
protected $broker;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
if (!method_exists(Enum::class, 'getValues')) {
22+
self::markTestSkipped('Version 1.x is not supported.');
23+
}
24+
}
25+
26+
public function testConstantTypes(): void
27+
{
28+
$this->processFile(__DIR__ . '/data/get_value.php', '$strEnum->getValue()', "'no doc block'|'public str'|'str'", new EnumGetValueDynamicReturnTypeExtension());
29+
}
30+
31+
public function testGeneralizedTypes(): void
32+
{
33+
$this->processFile(__DIR__ . '/data/get_value.php', '$bigStrEnum->getValue()', "string", new EnumGetValueDynamicReturnTypeExtension());
34+
}
35+
}

tests/ExtensionTestCase.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MabeEnumPHPStanTest;
6+
7+
use PhpParser\Node;
8+
use PhpParser\PrettyPrinter\Standard;
9+
use PHPStan\Analyser\NodeScopeResolver;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Analyser\ScopeContext;
12+
use PHPStan\Broker\AnonymousClassNameHelper;
13+
use PHPStan\Cache\Cache;
14+
use PHPStan\File\FileHelper;
15+
use PHPStan\Node\VirtualNode;
16+
use PHPStan\PhpDoc\PhpDocNodeResolver;
17+
use PHPStan\PhpDoc\PhpDocStringResolver;
18+
use PHPStan\Testing\TestCase;
19+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
20+
use PHPStan\Type\FileTypeMapper;
21+
use PHPStan\Type\VerbosityLevel;
22+
23+
abstract class ExtensionTestCase extends TestCase
24+
{
25+
protected function processFile(
26+
string $file,
27+
string $expression,
28+
string $type,
29+
DynamicMethodReturnTypeExtension $extension
30+
): void
31+
{
32+
$broker = $this->createBroker([$extension]);
33+
$parser = $this->getParser();
34+
$currentWorkingDirectory = $this->getCurrentWorkingDirectory();
35+
$fileHelper = new FileHelper($currentWorkingDirectory);
36+
$typeSpecifier = $this->createTypeSpecifier(new Standard(), $broker);
37+
/** @var \PHPStan\PhpDoc\PhpDocStringResolver $phpDocStringResolver */
38+
$phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class);
39+
$resolver = new NodeScopeResolver(
40+
$broker,
41+
$parser,
42+
new FileTypeMapper(
43+
$parser,
44+
$phpDocStringResolver,
45+
self::getContainer()->getByType(PhpDocNodeResolver::class),
46+
$this->createMock(Cache::class),
47+
$this->createMock(AnonymousClassNameHelper::class)
48+
),
49+
$fileHelper,
50+
$typeSpecifier,
51+
true,
52+
true,
53+
true,
54+
[],
55+
[]
56+
);
57+
$resolver->setAnalysedFiles([$fileHelper->normalizePath($file)]);
58+
59+
$run = false;
60+
$resolver->processNodes(
61+
$parser->parseFile($file),
62+
$this->createScopeFactory($broker, $typeSpecifier)->create(ScopeContext::create($file)),
63+
function (Node $node, Scope $scope) use ($expression, $type, &$run): void {
64+
if ($node instanceof VirtualNode) {
65+
return;
66+
}
67+
if ((new Standard())->prettyPrint([$node]) !== 'die') {
68+
return;
69+
}
70+
/** @var \PhpParser\Node\Stmt\Expression $expNode */
71+
$expNode = $this->getParser()->parseString(sprintf('<?php %s;', $expression))[0];
72+
self::assertSame($type, $scope->getType($expNode->expr)->describe(VerbosityLevel::precise()));
73+
$run = true;
74+
}
75+
);
76+
self::assertTrue($run);
77+
}
78+
}

tests/data/get_value.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
use MabeEnumPHPStanTest\Assets\BigStrEnum;
4+
use MabeEnumPHPStanTest\Assets\StrEnum;
5+
6+
function f(StrEnum $strEnum, BigStrEnum $bigStrEnum): void {
7+
die;
8+
}

0 commit comments

Comments
 (0)