Skip to content

Commit bfca0a6

Browse files
authored
Stop the recursion when an enum case value reads itself (#6731)
2 parents 4a8e20f + 5230ed7 commit bfca0a6

10 files changed

Lines changed: 488 additions & 272 deletions

File tree

‎src/Reflection/ClassReflection.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,7 +1053,7 @@ public function getEnumCases(): array
10531053
foreach ($this->reflection->getCases() as $case) {
10541054
$valueType = null;
10551055
if ($case instanceof ReflectionEnumBackedCase && $case->hasBackingValue()) {
1056-
$valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), $initializerExprContext);
1056+
$valueType = $this->initializerExprTypeResolver->getEnumCaseValueType($this->getName(), $case->getName(), $case->getValueExpression(), $initializerExprContext);
10571057
}
10581058
$caseName = $case->getName();
10591059
$attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName()));
@@ -1080,7 +1080,7 @@ public function getEnumCase(string $name): EnumCaseReflection
10801080
$case = $this->reflection->getCase($name);
10811081
$valueType = null;
10821082
if ($case instanceof ReflectionEnumBackedCase && $case->hasBackingValue()) {
1083-
$valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this));
1083+
$valueType = $this->initializerExprTypeResolver->getEnumCaseValueType($this->getName(), $name, $case->getValueExpression(), InitializerExprContext::fromClassReflection($this));
10841084
}
10851085

10861086
$attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName()));

‎src/Reflection/InitializerExprTypeResolver.php‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,25 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
514514
return new MixedType();
515515
}
516516

517+
/**
518+
* Resolves the backing value of an enum case. A read of the case while
519+
* its value is being resolved gives mixed - like a class constant does - to prevent endless recursions.
520+
*/
521+
public function getEnumCaseValueType(string $className, string $caseName, Expr $valueExpr, InitializerExprContext $context): Type
522+
{
523+
$resolvingName = sprintf('%s::%s', $className, $caseName);
524+
if (array_key_exists($resolvingName, $this->currentlyResolvingClassConstant)) {
525+
return new MixedType();
526+
}
527+
528+
$this->currentlyResolvingClassConstant[$resolvingName] = true;
529+
try {
530+
return $this->getType($valueExpr, $context);
531+
} finally {
532+
unset($this->currentlyResolvingClassConstant[$resolvingName]);
533+
}
534+
}
535+
517536
/**
518537
* @param callable(Expr): Type $getTypeCallback
519538
*/

‎src/Turbo/TurboExtensionEnabler.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
final class TurboExtensionEnabler
3434
{
3535

36-
public const EXPECTED_EXTENSION_VERSION = '55df31e';
36+
public const EXPECTED_EXTENSION_VERSION = '1cb5faf';
3737

3838
private static bool $active = false;
3939

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php // lint >= 8.2
2+
3+
namespace Bug15458Types;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
enum E: string
8+
{
9+
case A = self::A->value;
10+
}
11+
12+
enum IntBacked: int
13+
{
14+
case A = self::A->value;
15+
}
16+
17+
enum Mutual: string
18+
{
19+
case A = self::B->value;
20+
case B = self::A->value;
21+
}
22+
23+
enum CrossA: string
24+
{
25+
case A = CrossB::B->value;
26+
}
27+
28+
enum CrossB: string
29+
{
30+
case B = CrossA::A->value;
31+
}
32+
33+
enum Concat: string
34+
{
35+
case A = self::A->value . 'x';
36+
}
37+
38+
enum ForwardReference: string
39+
{
40+
case A = self::B->value . 'a';
41+
case B = 'b';
42+
}
43+
44+
class ConstantConcat
45+
{
46+
const A = self::A . 'x';
47+
}
48+
49+
function (Mutual $mutual, ForwardReference $forwardReference): void {
50+
assertType('mixed', E::A->value);
51+
assertType('mixed', IntBacked::A->value);
52+
assertType('mixed', Mutual::A->value);
53+
assertType('mixed', Mutual::B->value);
54+
assertType('mixed', $mutual->value);
55+
assertType('mixed', CrossA::A->value);
56+
assertType('mixed', CrossB::B->value);
57+
assertType('non-falsy-string', Concat::A->value);
58+
assertType('non-falsy-string', ConstantConcat::A);
59+
assertType("'ba'", ForwardReference::A->value);
60+
assertType("'b'", ForwardReference::B->value);
61+
assertType("'b'|'ba'", $forwardReference->value);
62+
};

‎tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,35 @@ public function testBug11891(): void
190190
]);
191191
}
192192

193+
#[RequiresPhp('>= 8.2.0')]
194+
public function testBug15458(): void
195+
{
196+
$this->analyse([__DIR__ . '/data/bug-15458.php'], [
197+
[
198+
'Enum case Bug15458\E::A value mixed does not match the "string" type.',
199+
7,
200+
],
201+
[
202+
'Enum case Bug15458\IntBacked::A value mixed does not match the "int" type.',
203+
12,
204+
],
205+
[
206+
'Enum case Bug15458\Mutual::A value mixed does not match the "string" type.',
207+
17,
208+
],
209+
[
210+
'Enum case Bug15458\Mutual::B value mixed does not match the "string" type.',
211+
18,
212+
],
213+
[
214+
'Enum case Bug15458\CrossA::A value mixed does not match the "string" type.',
215+
23,
216+
],
217+
[
218+
'Enum case Bug15458\CrossB::B value mixed does not match the "string" type.',
219+
28,
220+
],
221+
]);
222+
}
223+
193224
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php // lint >= 8.2
2+
3+
namespace Bug15458;
4+
5+
enum E: string
6+
{
7+
case A = self::A->value;
8+
}
9+
10+
enum IntBacked: int
11+
{
12+
case A = self::A->value;
13+
}
14+
15+
enum Mutual: string
16+
{
17+
case A = self::B->value;
18+
case B = self::A->value;
19+
}
20+
21+
enum CrossA: string
22+
{
23+
case A = CrossB::B->value;
24+
}
25+
26+
enum CrossB: string
27+
{
28+
case B = CrossA::A->value;
29+
}
30+
31+
enum Concat: string
32+
{
33+
case A = self::A->value . 'x';
34+
}
35+
36+
enum ForwardReference: string
37+
{
38+
case A = self::B->value . 'a';
39+
case B = 'b';
40+
}

‎turbo-ext/src/ClassReflection.cpp‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,10 +2168,12 @@ class ClassReflection
21682168
zv::Arr cases = zv::Arr::create(countOf(reflectionCases.ref()));
21692169
if (reflectionCases.ref().isArray()) {
21702170
for (auto entry : zv::ArrRef(reflectionCases.raw())) {
2171-
zv::Val valueType = enumCaseValueType(entry.value(), initializerExprContext.raw());
2172-
if (UNEXPECTED(valueType.isUndef())) return zv::Val();
21732171
zv::Val caseName = callOn(entry.value(), PT_LC("getname"), 0, NULL);
21742172
if (UNEXPECTED(caseName.isUndef())) return zv::Val();
2173+
zend_string *caseNameStr = zval_get_string(caseName.raw());
2174+
zv::Val valueType = enumCaseValueType(entry.value(), caseNameStr, initializerExprContext.raw());
2175+
zend_string_release(caseNameStr);
2176+
if (UNEXPECTED(valueType.isUndef())) return zv::Val();
21752177
zv::Val attributes = enumCaseAttributes(entry.value());
21762178
if (UNEXPECTED(attributes.isUndef())) return zv::Val();
21772179
zv::Val caseReflection = newEnumCaseReflection(entry.value(), valueType.ref(), attributes.ref());
@@ -2228,7 +2230,7 @@ class ClassReflection
22282230
ZVAL_STR(&arg, name);
22292231
zv::Val case_ = reflectionCall(PT_LC("getcase"), 1, &arg);
22302232
if (UNEXPECTED(case_.isUndef())) return zv::Val();
2231-
zv::Val valueType = enumCaseValueType(case_.ref(), NULL);
2233+
zv::Val valueType = enumCaseValueType(case_.ref(), name, NULL);
22322234
if (UNEXPECTED(valueType.isUndef())) return zv::Val();
22332235
zv::Val attributes = enumCaseAttributes(case_.ref());
22342236
if (UNEXPECTED(attributes.isUndef())) return zv::Val();
@@ -2237,10 +2239,11 @@ class ClassReflection
22372239
}
22382240

22392241
/* $case instanceof ReflectionEnumBackedCase && $case->hasBackingValue()
2240-
* ? $this->initializerExprTypeResolver->getType($case->getValueExpression(),
2241-
* $context) : null — $context NULL where the twin builds it inside the
2242-
* branch (getEnumCase()), the loop's shared one otherwise */
2243-
zv::Val enumCaseValueType(zv::Ref case_, zval *context)
2242+
* ? $this->initializerExprTypeResolver->getEnumCaseValueType($this->getName(),
2243+
* $caseName, $case->getValueExpression(), $context) : null — $context NULL
2244+
* where the twin builds it inside the branch (getEnumCase()), the loop's
2245+
* shared one otherwise */
2246+
zv::Val enumCaseValueType(zv::Ref case_, zend_string *caseName, zval *context)
22442247
{
22452248
bool isBackedCase;
22462249
if (UNEXPECTED(!pt_type_instanceof(case_.raw(), PT_CLASS_REFLECTION_ENUM_BACKED_CASE, isBackedCase))) return zv::Val();
@@ -2258,7 +2261,16 @@ class ClassReflection
22582261
context = ownContext.raw();
22592262
}
22602263

2261-
return initializerExprType(valueExpression.raw(), context);
2264+
zv::Val className = getName();
2265+
if (UNEXPECTED(className.isUndef())) return zv::Val();
2266+
zend_object *object = service(PT_CR_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, "initializerExprTypeResolver");
2267+
if (UNEXPECTED(object == NULL)) return zv::Val();
2268+
zval resolver;
2269+
ZVAL_OBJ(&resolver, object);
2270+
zend_string *classNameStr = zval_get_string(className.raw());
2271+
zv::Val valueType = pt_initializer_expr_type_resolver_get_enum_case_value_type(&resolver, classNameStr, caseName, valueExpression.raw(), context);
2272+
zend_string_release(classNameStr);
2273+
return valueType;
22622274
}
22632275

22642276
/* $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(),

‎turbo-ext/src/InitializerExprTypeResolver.cpp‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4838,6 +4838,25 @@ class InitializerExprTypeResolver
48384838
return getClassConstFetchTypeByReflection(class_, constantName, classReflection.raw(), getTypeCallback);
48394839
}
48404840

4841+
/* getEnumCaseValueType(): the case value resolved under the
4842+
* currentlyResolvingClassConstant guard, mixed on re-entry */
4843+
zv::Val getEnumCaseValueType(zend_string *className, zend_string *caseName, zval *valueExpr, zval *context) const
4844+
{
4845+
zend_string *resolvingName = zend_string_concat3(ZSTR_VAL(className), ZSTR_LEN(className), "::", 2, ZSTR_VAL(caseName), ZSTR_LEN(caseName));
4846+
zv::Val resolvingNameHold = zv::Val::adoptString(resolvingName);
4847+
if (zend_symtable_exists(Z_ARRVAL_P(prop(slots::currentlyResolvingClassConstant)), resolvingName)) {
4848+
return newMixedType();
4849+
}
4850+
4851+
zval trueValue = {};
4852+
ZVAL_TRUE(&trueValue);
4853+
writeArrayProperty(slots::currentlyResolvingClassConstant, resolvingName, &trueValue);
4854+
zv::Val type = getType(valueExpr, context);
4855+
/* the twin's finally: released with an exception pending too */
4856+
unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName);
4857+
return type;
4858+
}
4859+
48414860
/* }}} */
48424861

48434862
private:
@@ -5336,6 +5355,19 @@ zv::Val pt_initializer_expr_type_resolver_get_type(zval *resolver, zval *expr, z
53365355
return callResolverMethod(site, resolver, PT_LC("gettype"), 2, argv);
53375356
}
53385357

5358+
zv::Val pt_initializer_expr_type_resolver_get_enum_case_value_type(zval *resolver, zend_string *className, zend_string *caseName, zval *valueExpr, zval *context)
5359+
{
5360+
if (EXPECTED(isNativeResolver(resolver) && Z_TYPE_P(valueExpr) == IS_OBJECT)) {
5361+
return InitializerExprTypeResolver(Z_OBJ_P(resolver)).getEnumCaseValueType(className, caseName, valueExpr, context);
5362+
}
5363+
static pt_method_site site;
5364+
zval classNameZv, caseNameZv;
5365+
ZVAL_STR(&classNameZv, className);
5366+
ZVAL_STR(&caseNameZv, caseName);
5367+
zv::Args argv{&classNameZv, &caseNameZv, valueExpr, context};
5368+
return callResolverMethod(site, resolver, PT_LC("getenumcasevaluetype"), 4, argv);
5369+
}
5370+
53395371
zv::Val pt_initializer_expr_type_resolver_get_binary_op_type(zval *resolver, pt_ietr_binary_operator op, zval *left, zval *right, const pt_ietr_get_type &getTypeCallback)
53405372
{
53415373
if (EXPECTED(isNativeResolver(resolver))) {
@@ -5641,6 +5673,14 @@ PT_MINIT_REGISTRATION(pt_register_initializer_expr_type_resolver)
56415673
PT_RETURN_VAL(IETR_THIS.getType(expr, context));
56425674
});
56435675

5676+
cls.method(sigs::getEnumCaseValueType, [](INTERNAL_FUNCTION_PARAMETERS) {
5677+
zend_string *className, *caseName;
5678+
zval *valueExpr, *context;
5679+
if (!zp::parse<zp::Str, zp::Str, zp::Obj, zp::Obj>(execute_data, className, caseName, valueExpr, context)) RETURN_THROWS();
5680+
if (UNEXPECTED(!ietrArgExpr(valueExpr, 3) || !ietrArgContext(context, 4))) RETURN_THROWS();
5681+
PT_RETURN_VAL(IETR_THIS.getEnumCaseValueType(className, caseName, valueExpr, context));
5682+
});
5683+
56445684
cls.method(sigs::getConcatType, IETR_OPERATOR_GLUE(self.getConcatType(left, right, getTypeCallback)));
56455685

56465686
cls.method(sigs::resolveConcatType, [](INTERNAL_FUNCTION_PARAMETERS) {

0 commit comments

Comments
 (0)