Skip to content

Commit 617dfd0

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

File tree

7 files changed

+221
-1
lines changed

7 files changed

+221
-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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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') && (
36+
$methodReflection->getName() === 'getValue'
37+
|| $methodReflection->getName() === 'getValues'
38+
);
39+
}
40+
41+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
42+
{
43+
$enumType = $scope->getType($methodCall->var);
44+
if (count($enumType->getReferencedClasses()) !== 1) {
45+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
46+
}
47+
48+
/** @var string $enumClass */
49+
$enumClass = $enumType->getReferencedClasses()[0];
50+
if (array_key_exists($enumClass, $this->enumTypes)) {
51+
return $this->enumTypes[$enumClass];
52+
}
53+
$types = array_map(function ($value) use ($scope): Type {
54+
return $scope->getTypeFromValue($value);
55+
}, self::getEnumValues($enumClass));
56+
57+
$this->enumTypes[$enumClass] = TypeCombinator::union(...$types);
58+
59+
return $this->enumTypes[$enumClass];
60+
}
61+
62+
/**
63+
* @phpstan-param class-string<Enum> $enumClass
64+
*/
65+
private static function getEnumValues(string $enumClass): array
66+
{
67+
if (method_exists($enumClass, 'getValues')) {
68+
return $enumClass::getValues();
69+
}
70+
71+
throw new \PHPStan\ShouldNotHappenException();
72+
}
73+
}

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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
$this->processFile(__DIR__ . '/data/get_value.php', '$strEnum->getValues()', "'no doc block'|'public str'|'str'", new EnumGetValueDynamicReturnTypeExtension());
30+
}
31+
32+
public function testGeneralizedTypes(): void
33+
{
34+
$this->processFile(__DIR__ . '/data/get_value.php', '$bigStrEnum->getValue()', "string", new EnumGetValueDynamicReturnTypeExtension());
35+
$this->processFile(__DIR__ . '/data/get_value.php', '$bigStrEnum->getValues()', "string", new EnumGetValueDynamicReturnTypeExtension());
36+
}
37+
}

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)