Skip to content

Commit 61a0d9f

Browse files
AJenboondrejmirtes
authored andcommitted
enum::tryFrom() and enum::from() takes IntBackedEnum and
StringBackedEnum in to account
1 parent c2befd8 commit 61a0d9f

8 files changed

Lines changed: 314 additions & 2 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection\Php;
4+
5+
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
6+
use PHPStan\Reflection\Assertions;
7+
use PHPStan\Reflection\ClassMemberReflection;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Reflection\ExtendedFunctionVariant;
10+
use PHPStan\Reflection\ExtendedMethodReflection;
11+
use PHPStan\Reflection\ExtendedParameterReflection;
12+
use PHPStan\Reflection\ExtendedParametersAcceptor;
13+
use PHPStan\TrinaryLogic;
14+
use PHPStan\Turbo\ReferencedByTurboExtension;
15+
use PHPStan\Type\Type;
16+
use function array_map;
17+
18+
/**
19+
* from() and tryFrom() of a backed enum. Reflection declares their parameter
20+
* as int|string, but PHP only accepts the enum's backing type.
21+
*/
22+
#[ReferencedByTurboExtension(key: 'backedEnumFromMethodReflection')]
23+
final class BackedEnumFromMethodReflection implements ExtendedMethodReflection
24+
{
25+
26+
/** @var list<ExtendedParametersAcceptor>|null */
27+
private ?array $variants = null;
28+
29+
public function __construct(private ExtendedMethodReflection $method, private Type $backingType)
30+
{
31+
}
32+
33+
public function getDeclaringClass(): ClassReflection
34+
{
35+
return $this->method->getDeclaringClass();
36+
}
37+
38+
public function isStatic(): bool
39+
{
40+
return $this->method->isStatic();
41+
}
42+
43+
public function isPrivate(): bool
44+
{
45+
return $this->method->isPrivate();
46+
}
47+
48+
public function isPublic(): bool
49+
{
50+
return $this->method->isPublic();
51+
}
52+
53+
public function getDocComment(): ?string
54+
{
55+
return $this->method->getDocComment();
56+
}
57+
58+
public function getName(): string
59+
{
60+
return $this->method->getName();
61+
}
62+
63+
public function getPrototype(): ClassMemberReflection
64+
{
65+
return $this->method->getPrototype();
66+
}
67+
68+
public function getVariants(): array
69+
{
70+
return $this->variants ??= array_map(fn (ExtendedParametersAcceptor $variant): ExtendedParametersAcceptor => new ExtendedFunctionVariant(
71+
$variant->getTemplateTypeMap(),
72+
$variant->getResolvedTemplateTypeMap(),
73+
array_map(fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter(
74+
$parameter->getName(),
75+
$this->backingType,
76+
$parameter->isOptional(),
77+
$parameter->passedByReference(),
78+
$parameter->isVariadic(),
79+
$parameter->getDefaultValue(),
80+
$this->backingType,
81+
$this->backingType,
82+
$parameter->getOutType(),
83+
$parameter->isImmediatelyInvokedCallable(),
84+
$parameter->getClosureThisType(),
85+
$parameter->getAttributes(),
86+
$parameter->getAllowedConstants(),
87+
$parameter->isPureUnlessCallableIsImpureParameter(),
88+
$parameter->isPureUnlessParameterPassedParameter(),
89+
), $variant->getParameters()),
90+
$variant->isVariadic(),
91+
$variant->getReturnType(),
92+
$variant->getPhpDocReturnType(),
93+
$variant->getNativeReturnType(),
94+
$variant->getCallSiteVarianceMap(),
95+
), $this->method->getVariants());
96+
}
97+
98+
public function getOnlyVariant(): ExtendedParametersAcceptor
99+
{
100+
return $this->getVariants()[0];
101+
}
102+
103+
public function getNamedArgumentsVariants(): ?array
104+
{
105+
return null;
106+
}
107+
108+
public function isDeprecated(): TrinaryLogic
109+
{
110+
return $this->method->isDeprecated();
111+
}
112+
113+
public function getDeprecatedDescription(): ?string
114+
{
115+
return $this->method->getDeprecatedDescription();
116+
}
117+
118+
public function isFinal(): TrinaryLogic
119+
{
120+
return $this->method->isFinal();
121+
}
122+
123+
public function isFinalByKeyword(): TrinaryLogic
124+
{
125+
return $this->method->isFinalByKeyword();
126+
}
127+
128+
public function isInternal(): TrinaryLogic
129+
{
130+
return $this->method->isInternal();
131+
}
132+
133+
public function isBuiltin(): TrinaryLogic|bool
134+
{
135+
return $this->method->isBuiltin();
136+
}
137+
138+
public function getThrowType(): ?Type
139+
{
140+
return $this->method->getThrowType();
141+
}
142+
143+
public function hasSideEffects(): TrinaryLogic
144+
{
145+
return $this->method->hasSideEffects();
146+
}
147+
148+
public function getAsserts(): Assertions
149+
{
150+
return $this->method->getAsserts();
151+
}
152+
153+
public function acceptsNamedArguments(): TrinaryLogic
154+
{
155+
return $this->method->acceptsNamedArguments();
156+
}
157+
158+
public function getSelfOutType(): ?Type
159+
{
160+
return $this->method->getSelfOutType();
161+
}
162+
163+
public function returnsByReference(): TrinaryLogic
164+
{
165+
return $this->method->returnsByReference();
166+
}
167+
168+
public function isAbstract(): TrinaryLogic|bool
169+
{
170+
return $this->method->isAbstract();
171+
}
172+
173+
public function isPure(): TrinaryLogic
174+
{
175+
return $this->method->isPure();
176+
}
177+
178+
public function getPureUnlessCallableIsImpureParameters(): array
179+
{
180+
return $this->method->getPureUnlessCallableIsImpureParameters();
181+
}
182+
183+
public function getPureUnlessParameterPassedParameters(): array
184+
{
185+
return $this->method->getPureUnlessParameterPassedParameters();
186+
}
187+
188+
public function getAttributes(): array
189+
{
190+
return $this->method->getAttributes();
191+
}
192+
193+
public function mustUseReturnValue(): TrinaryLogic
194+
{
195+
return $this->method->mustUseReturnValue();
196+
}
197+
198+
public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
199+
{
200+
return $this->method->getResolvedPhpDoc();
201+
}
202+
203+
}

‎src/Reflection/Php/PhpClassReflectionExtension.php‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
use function count;
7676
use function explode;
7777
use function implode;
78+
use function in_array;
7879
use function is_array;
7980
use function sprintf;
8081
use function strtolower;
@@ -795,12 +796,21 @@ private function createMethod(
795796
);
796797
}
797798

798-
return $this->createUserlandMethodReflection(
799+
$method = $this->createUserlandMethodReflection(
799800
$declaringClass,
800801
$declaringClass,
801802
$methodReflection,
802803
$this->findMethodTrait($methodReflection),
803804
);
805+
806+
if (in_array(strtolower($methodReflection->getName()), ['from', 'tryfrom'], true)) {
807+
$backedEnumType = $declaringClass->getBackedEnumType();
808+
if ($backedEnumType !== null) {
809+
return new BackedEnumFromMethodReflection($method, $backedEnumType);
810+
}
811+
}
812+
813+
return $method;
804814
}
805815

806816
public function createUserlandMethodReflection(ClassReflection $fileDeclaringClass, ClassReflection $actualDeclaringClass, ReflectionMethod $methodReflection, ?string $declaringTraitName): PhpMethodReflection
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11421Types;
6+
7+
use function PHPStan\Testing\assertNativeType;
8+
use function PHPStan\Testing\assertType;
9+
10+
enum NumberEnum: int
11+
{
12+
case One = 1;
13+
}
14+
15+
enum WordEnum: string
16+
{
17+
case One = 'one';
18+
}
19+
20+
function (): void {
21+
assertType('Closure(int): Bug11421Types\NumberEnum', NumberEnum::from(...));
22+
assertNativeType('Closure(int): mixed', NumberEnum::from(...));
23+
assertType('Closure(int): (Bug11421Types\NumberEnum|null)', NumberEnum::tryFrom(...));
24+
assertType('Closure(string): Bug11421Types\WordEnum', WordEnum::from(...));
25+
assertType('Closure(string): (Bug11421Types\WordEnum|null)', WordEnum::tryFrom(...));
26+
};

‎tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,4 +1132,28 @@ public function testBug15251(): void
11321132
]);
11331133
}
11341134

1135+
#[RequiresPhp('>= 8.1.0')]
1136+
public function testBug11421(): void
1137+
{
1138+
$this->checkThisOnly = false;
1139+
$this->analyse([__DIR__ . '/data/bug-11421.php'], [
1140+
[
1141+
'Parameter #1 $value of static method Bug11421\\NumberEnum::from() expects int, string given.',
1142+
18,
1143+
],
1144+
[
1145+
'Parameter #1 $value of static method Bug11421\\WordEnum::from() expects string, int given.',
1146+
21,
1147+
],
1148+
[
1149+
'Parameter #1 $value of static method Bug11421\\NumberEnum::tryFrom() expects int, string given.',
1150+
24,
1151+
],
1152+
[
1153+
'Parameter #1 $value of static method Bug11421\\WordEnum::tryFrom() expects string, int given.',
1154+
27,
1155+
],
1156+
]);
1157+
}
1158+
11351159
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace Bug11421;
4+
5+
enum NumberEnum: int
6+
{
7+
case One = 1;
8+
case Two = 2;
9+
}
10+
11+
enum WordEnum: string
12+
{
13+
case One = 'one';
14+
case Two = 'two';
15+
}
16+
17+
$word = 'one';
18+
NumberEnum::from($word);
19+
20+
$number = 1;
21+
WordEnum::from($number);
22+
23+
function (string $s, int $i, \BackedEnum $e): void {
24+
NumberEnum::tryFrom($s);
25+
NumberEnum::from($i);
26+
NumberEnum::tryFrom($i);
27+
WordEnum::tryFrom($i);
28+
WordEnum::from($s);
29+
WordEnum::tryFrom($s);
30+
$e::from($s);
31+
$e::tryFrom($i);
32+
$from = NumberEnum::from(...);
33+
$from($s);
34+
};

‎turbo-ext/src/PhpClassReflectionExtension.cpp‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1573,7 +1573,20 @@ class PhpClassReflectionExtension
15731573

15741574
zv::Val methodTrait = findMethodTrait(methodReflection);
15751575
if (UNEXPECTED(methodTrait.isUndef())) return zv::Val();
1576-
return createUserlandMethodReflection(declaringClass, declaringClass, methodReflection, methodTrait.raw());
1576+
zv::Val method = createUserlandMethodReflection(declaringClass, declaringClass, methodReflection, methodTrait.raw());
1577+
if (UNEXPECTED(method.isUndef())) return zv::Val();
1578+
1579+
zv::Str loweredName = zv::Str::adopt(zend_string_tolower(methodNameStr.get()));
1580+
if (zend_string_equals_literal(loweredName.get(), "from") || zend_string_equals_literal(loweredName.get(), "tryfrom")) {
1581+
zv::Val backedEnumType = call(declaringClass, PT_LC("getbackedenumtype"));
1582+
if (UNEXPECTED(backedEnumType.isUndef())) return zv::Val();
1583+
if (Z_TYPE_P(backedEnumType.raw()) != IS_NULL) {
1584+
zv::Args args{method.raw(), backedEnumType.raw()};
1585+
return pt_type_new(PT_CLASS_BACKED_ENUM_FROM_METHOD_REFLECTION, 2, args);
1586+
}
1587+
}
1588+
1589+
return method;
15771590
}
15781591

15791592
/*

‎turbo-ext/src/support.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = {
165165
/* PT_CLASS_ALWAYS_REMEMBERED_EXPR */ {"alwaysRememberedExpr", "PHPStan\\Node\\Expr\\AlwaysRememberedExpr"},
166166
/* PT_CLASS_NATIVE_METHOD_REFLECTION */ {"nativeMethodReflection", "PHPStan\\Reflection\\Native\\NativeMethodReflection"},
167167
/* PT_CLASS_ENUM_CASES_METHOD_REFLECTION */ {"enumCasesMethodReflection", "PHPStan\\Reflection\\Php\\EnumCasesMethodReflection"},
168+
/* PT_CLASS_BACKED_ENUM_FROM_METHOD_REFLECTION */ {"backedEnumFromMethodReflection", "PHPStan\\Reflection\\Php\\BackedEnumFromMethodReflection"},
168169
/* PT_CLASS_PRIVATE_PROPERTY_ATTRIBUTE */ {"privatePropertyAttribute", "PHPStan\\Reflection\\Attribute\\PrivateProperty"},
169170
/* PT_CLASS_PROTECTED_PROPERTY_ATTRIBUTE */ {"protectedPropertyAttribute", "PHPStan\\Reflection\\Attribute\\ProtectedProperty"},
170171
/* PT_CLASS_ADAPTER_REFLECTION_METHOD */ {"adapterReflectionMethod", "PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod"},

‎turbo-ext/src/support.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ enum {
236236
PT_CLASS_ALWAYS_REMEMBERED_EXPR,
237237
PT_CLASS_NATIVE_METHOD_REFLECTION,
238238
PT_CLASS_ENUM_CASES_METHOD_REFLECTION,
239+
PT_CLASS_BACKED_ENUM_FROM_METHOD_REFLECTION,
239240
PT_CLASS_PRIVATE_PROPERTY_ATTRIBUTE,
240241
PT_CLASS_PROTECTED_PROPERTY_ATTRIBUTE,
241242
PT_CLASS_ADAPTER_REFLECTION_METHOD,

0 commit comments

Comments
 (0)