Skip to content

Commit 332a6f1

Browse files
committed
fixed error on Enum type declaration and MyEnum|NoEnum and also support MyEnum1|MyEnum2
1 parent 452a803 commit 332a6f1

6 files changed

+446
-181
lines changed

src/EnumDynamicReturnTypeExtension.php

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,25 @@
1111
use PHPStan\Reflection\ParametersAcceptorSelector;
1212
use PHPStan\ShouldNotHappenException;
1313
use PHPStan\Type\ArrayType;
14+
use PHPStan\Type\Constant\ConstantArrayType;
15+
use PHPStan\Type\ConstantTypeHelper;
1416
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15-
use PHPStan\Type\IntegerType;
1617
use PHPStan\Type\Type;
1718
use PHPStan\Type\TypeCombinator;
1819

1920
class EnumDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
2021
{
2122
/**
22-
* Buffer of known return types of Enum::getValues()
23-
* @var Type[]
24-
* @phpstan-var array<class-string<Enum>, Type>
23+
* Buffer of all types of enumeration values
24+
* @phpstan-var array<class-string<Enum>, Type[]>
2525
*/
26-
private $enumValuesTypeBuffer = [];
26+
private $enumValueTypesBuffer = [];
27+
28+
/**
29+
* Buffer of all types of enumeration ordinals
30+
* @phpstan-var array<class-string<Enum>, Type[]>
31+
*/
32+
private $enumOrdinalTypesBuffer = [];
2733

2834
public function getClass(): string
2935
{
@@ -40,45 +46,87 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
4046
return in_array(strtolower($methodReflection->getName()), $supportedMethods, true);
4147
}
4248

43-
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
44-
{
45-
$enumType = $scope->getType($methodCall->var);
46-
$methodName = $methodReflection->getName();
47-
$methodClasses = $enumType->getReferencedClasses();
48-
if (count($methodClasses) !== 1) {
49-
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
49+
public function getTypeFromMethodCall(
50+
MethodReflection $methodReflection,
51+
MethodCall $methodCall,
52+
Scope $scope
53+
): Type {
54+
$callType = $scope->getType($methodCall->var);
55+
$callClasses = $callType->getReferencedClasses();
56+
$methodName = strtolower($methodReflection->getName());
57+
$returnTypes = [];
58+
foreach ($callClasses as $callClass) {
59+
if (!is_subclass_of($callClass, Enum::class, true)) {
60+
$returnTypes[] = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())
61+
->getReturnType();
62+
} else {
63+
switch ($methodName) {
64+
case 'getvalue':
65+
$returnTypes[] = $this->enumGetValueReturnType($callClass);
66+
break;
67+
case 'getvalues':
68+
$returnTypes[] = $this->enumGetValuesReturnType($callClass);
69+
break;
70+
default:
71+
throw new ShouldNotHappenException("Method {$methodName} is not supported");
72+
}
73+
}
5074
}
5175

52-
$enumeration = $methodClasses[0];
53-
54-
switch (strtolower($methodName)) {
55-
case 'getvalue':
56-
return $this->getEnumValuesType($enumeration, $scope);
57-
case 'getvalues':
58-
return new ArrayType(
59-
new IntegerType(),
60-
$this->getEnumValuesType($enumeration, $scope)
61-
);
62-
default:
63-
throw new ShouldNotHappenException("Method {$methodName} is not supported");
64-
}
76+
return TypeCombinator::union(...$returnTypes);
6577
}
6678

6779
/**
68-
* Returns union type of all values of an enumeration
69-
* @phpstan-param class-string<Enum> $enumClass
80+
* Returns types of all values of an enumeration
81+
* @phpstan-param class-string<Enum> $enumeration
82+
* @return Type[]
7083
*/
71-
private function getEnumValuesType(string $enumeration, Scope $scope): Type
84+
private function enumValueTypes(string $enumeration): array
7285
{
73-
if (isset($this->enumValuesTypeBuffer[$enumeration])) {
74-
return $this->enumValuesTypeBuffer[$enumeration];
86+
if (isset($this->enumValueTypesBuffer[$enumeration])) {
87+
return $this->enumValueTypesBuffer[$enumeration];
7588
}
7689

7790
$values = array_values($enumeration::getConstants());
78-
$types = array_map(function ($value) use ($scope): Type {
79-
return $scope->getTypeFromValue($value);
80-
}, $values);
91+
$types = array_map([ConstantTypeHelper::class, 'getTypeFromValue'], $values);
92+
93+
return $this->enumValueTypesBuffer[$enumeration] = $types;
94+
}
95+
96+
/**
97+
* Returns types of all ordinals of an enumeration
98+
* @phpstan-param class-string<Enum> $enumeration
99+
* @return Type[]
100+
*/
101+
private function enumOrdinalTypes(string $enumeration): array
102+
{
103+
if (isset($this->enumOrdinalTypesBuffer[$enumeration])) {
104+
return $this->enumOrdinalTypesBuffer[$enumeration];
105+
}
81106

82-
return $this->enumValuesTypeBuffer[$enumeration] = TypeCombinator::union(...$types);
107+
$ordinals = array_keys($enumeration::getOrdinals());
108+
$types = array_map([ConstantTypeHelper::class, 'getTypeFromValue'], $ordinals);
109+
110+
return $this->enumOrdinalTypesBuffer[$enumeration] = $types;
111+
}
112+
113+
/**
114+
* Returns return type of Enum::getValue()
115+
* @phpstan-param class-string<Enum> $enumeration
116+
*/
117+
private function enumGetValueReturnType(string $enumeration): Type
118+
{
119+
return TypeCombinator::union(...$this->enumValueTypes($enumeration));
120+
}
121+
122+
/**
123+
* Returns return type of Enum::getValues()
124+
* @phpstan-param class-string<Enum> $enumeration
125+
*/
126+
private function enumGetValuesReturnType(string $enumeration): ArrayType
127+
{
128+
$keyTypes = $this->enumOrdinalTypes($enumeration);
129+
$valueTypes = $this->enumValueTypes($enumeration);
130+
return new ConstantArrayType($keyTypes, $valueTypes, count($keyTypes));
83131
}
84132
}

tests/Assets/BigStrEnum.php

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,34 @@
88

99
class BigStrEnum extends Enum
1010
{
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';
11+
const C_01 = '01';
12+
const C_02 = '02';
13+
const C_03 = '03';
14+
const C_04 = '04';
15+
const C_05 = '05';
16+
const C_06 = '06';
17+
const C_07 = '07';
18+
const C_08 = '08';
19+
const C_09 = '09';
20+
const C_10 = '10';
21+
const C_11 = '11';
22+
const C_12 = '12';
23+
const C_13 = '13';
24+
const C_14 = '14';
25+
const C_15 = '15';
26+
const C_16 = '16';
27+
const C_17 = '17';
28+
const C_18 = '18';
29+
const C_19 = '19';
30+
const C_20 = '20';
31+
const C_21 = '21';
32+
const C_22 = '22';
33+
const C_23 = '23';
34+
const C_24 = '24';
35+
const C_25 = '25';
36+
const C_26 = '26';
37+
const C_27 = '27';
38+
const C_28 = '28';
39+
const C_29 = '29';
40+
const C_30 = '30';
2041
}

tests/Assets/NotAnEnum.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ class NotAnEnum
1010
private const PRIVATE_STR = 'private str';
1111
protected const PROTECTED_STR = 'protected str';
1212
public const PUBLIC_STR = 'public str';
13+
14+
public function getValue(): string {return __FUNCTION__; }
15+
public function getValues(): array { return []; }
1316
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MabeEnumPHPStanTest;
6+
7+
use MabeEnum\Enum;
8+
use MabeEnumPHPStan\EnumDynamicReturnTypeExtension;
9+
10+
final class EnumDynamicReturnTypeExtensionGetValueTest extends ExtensionTestCase
11+
{
12+
/** @var EnumDynamicReturnTypeExtension */
13+
private $extension;
14+
15+
private $defaultReturnType = 'array|bool|float|int|string|null';
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
$this->extension = new EnumDynamicReturnTypeExtension();
22+
23+
// Version < 3.x did not support array values
24+
if (method_exists(Enum::class, 'getByName')) {
25+
$this->defaultReturnType = 'bool|float|int|string|null';
26+
}
27+
}
28+
29+
public function testNullType(): void
30+
{
31+
$code = <<<'CODE'
32+
<?php
33+
function f(MabeEnumPHPStanTest\Assets\NullTypeEnum $enum) {
34+
die;
35+
}
36+
CODE;
37+
38+
$this->processCode($code, '$enum->getValue()', 'null', $this->extension);
39+
}
40+
41+
public function testBoolType(): void
42+
{
43+
$code = <<<'CODE'
44+
<?php
45+
function f(MabeEnumPHPStanTest\Assets\BoolTypeEnum $enum) {
46+
die;
47+
}
48+
CODE;
49+
50+
$this->processCode($code, '$enum->getValue()', 'bool', $this->extension);
51+
}
52+
53+
public function testStringType(): void
54+
{
55+
$code = <<<'CODE'
56+
<?php
57+
function f(MabeEnumPHPStanTest\Assets\StrTypeEnum $enum) {
58+
die;
59+
}
60+
CODE;
61+
62+
$this->processCode($code, '$enum->getValue()', "'str1'|'str2'", $this->extension);
63+
}
64+
65+
public function testIntType(): void
66+
{
67+
$code = <<<'CODE'
68+
<?php
69+
function f(MabeEnumPHPStanTest\Assets\IntTypeEnum $enum) {
70+
die;
71+
}
72+
CODE;
73+
74+
$this->processCode($code, '$enum->getValue()', '0|1', $this->extension);
75+
}
76+
77+
public function testFloatType(): void
78+
{
79+
$code = <<<'CODE'
80+
<?php
81+
function f(MabeEnumPHPStanTest\Assets\FloatTypeEnum $enum) {
82+
die;
83+
}
84+
CODE;
85+
86+
$this->processCode($code, '$enum->getValue()', '1.1|1.2', $this->extension);
87+
}
88+
89+
public function testArrayType(): void
90+
{
91+
$code = <<<'CODE'
92+
<?php
93+
function f(MabeEnumPHPStanTest\Assets\ArrayTypeEnum $enum) {
94+
die;
95+
}
96+
CODE;
97+
98+
$this->processCode($code, '$enum->getValue()', 'array(array())', $this->extension);
99+
}
100+
101+
public function testAllTypes(): void
102+
{
103+
$code = <<<'CODE'
104+
<?php
105+
function f(MabeEnumPHPStanTest\Assets\AllTypeEnum $enum) {
106+
die;
107+
}
108+
CODE;
109+
110+
$this->processCode(
111+
$code,
112+
'$enum->getValue()',
113+
"1|1.1|'str'|array(null, true, 1, 1.1, 'str', array())|true|null",
114+
$this->extension
115+
);
116+
}
117+
118+
public function testGeneralizedTypes(): void
119+
{
120+
$code = <<<'CODE'
121+
<?php
122+
function f(MabeEnumPHPStanTest\Assets\BigStrEnum $enum) {
123+
die;
124+
}
125+
CODE;
126+
127+
$this->processCode($code, '$enum->getValue()', 'string', $this->extension);
128+
}
129+
130+
public function testBaseEnum(): void
131+
{
132+
$code = <<<'CODE'
133+
<?php
134+
function f(MabeEnum\Enum $enum) {
135+
die;
136+
}
137+
CODE;
138+
139+
$this->processCode($code, '$enum->getValue()', $this->defaultReturnType, $this->extension);
140+
}
141+
142+
public function testUnionEnum(): void
143+
{
144+
$code = <<<'CODE'
145+
<?php
146+
use MabeEnumPHPStanTest\Assets\IntTypeEnum;
147+
use MabeEnumPHPStanTest\Assets\StrTypeEnum;
148+
149+
/** @param IntTypeEnum|StrTypeEnum $enum */
150+
function f($enum) {
151+
die;
152+
}
153+
CODE;
154+
155+
$this->processCode($code, '$enum->getValue()', "0|1|'str1'|'str2'", $this->extension);
156+
}
157+
158+
public function testEnumAndNonEnum(): void
159+
{
160+
$code = <<<'CODE'
161+
<?php
162+
use MabeEnumPHPStanTest\Assets\IntTypeEnum;
163+
use MabeEnumPHPStanTest\Assets\NotAnEnum;
164+
165+
/** @param IntTypeEnum|NotAnEnum $enum */
166+
function f($enum) {
167+
die;
168+
}
169+
CODE;
170+
171+
$this->processCode($code, '$enum->getValue()', $this->defaultReturnType, $this->extension);
172+
}
173+
}

0 commit comments

Comments
 (0)