Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@ public function getEnumCases(): array
foreach ($this->reflection->getCases() as $case) {
$valueType = null;
if ($case instanceof ReflectionEnumBackedCase && $case->hasBackingValue()) {
$valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), $initializerExprContext);
$valueType = $this->initializerExprTypeResolver->getEnumCaseValueType($this->getName(), $case->getName(), $case->getValueExpression(), $initializerExprContext);
}
$caseName = $case->getName();
$attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName()));
Expand All @@ -1080,7 +1080,7 @@ public function getEnumCase(string $name): EnumCaseReflection
$case = $this->reflection->getCase($name);
$valueType = null;
if ($case instanceof ReflectionEnumBackedCase && $case->hasBackingValue()) {
$valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this));
$valueType = $this->initializerExprTypeResolver->getEnumCaseValueType($this->getName(), $name, $case->getValueExpression(), InitializerExprContext::fromClassReflection($this));
}

$attributes = $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(), InitializerExprContext::fromClass($this->getName(), $this->getFileName()));
Expand Down
19 changes: 19 additions & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,25 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
return new MixedType();
}

/**
* Resolves the backing value of an enum case. A read of the case while
* its value is being resolved gives mixed - like a class constant does - to prevent endless recursions.
*/
public function getEnumCaseValueType(string $className, string $caseName, Expr $valueExpr, InitializerExprContext $context): Type
{
$resolvingName = sprintf('%s::%s', $className, $caseName);
if (array_key_exists($resolvingName, $this->currentlyResolvingClassConstant)) {
return new MixedType();
}

$this->currentlyResolvingClassConstant[$resolvingName] = true;
try {
return $this->getType($valueExpr, $context);
} finally {
unset($this->currentlyResolvingClassConstant[$resolvingName]);
}
}

/**
* @param callable(Expr): Type $getTypeCallback
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '55df31e';
public const EXPECTED_EXTENSION_VERSION = '1cb5faf';

private static bool $active = false;

Expand Down
62 changes: 62 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15458.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php // lint >= 8.2

namespace Bug15458Types;

use function PHPStan\Testing\assertType;

enum E: string
{
case A = self::A->value;
}

enum IntBacked: int
{
case A = self::A->value;
}

enum Mutual: string
{
case A = self::B->value;
case B = self::A->value;
}

enum CrossA: string
{
case A = CrossB::B->value;
}

enum CrossB: string
{
case B = CrossA::A->value;
}

enum Concat: string
{
case A = self::A->value . 'x';
}

enum ForwardReference: string
{
case A = self::B->value . 'a';
case B = 'b';
}

class ConstantConcat
{
const A = self::A . 'x';
}

function (Mutual $mutual, ForwardReference $forwardReference): void {
assertType('mixed', E::A->value);
assertType('mixed', IntBacked::A->value);
assertType('mixed', Mutual::A->value);
assertType('mixed', Mutual::B->value);
assertType('mixed', $mutual->value);
assertType('mixed', CrossA::A->value);
assertType('mixed', CrossB::B->value);
assertType('non-falsy-string', Concat::A->value);
assertType('non-falsy-string', ConstantConcat::A);
assertType("'ba'", ForwardReference::A->value);
assertType("'b'", ForwardReference::B->value);
assertType("'b'|'ba'", $forwardReference->value);
};
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,35 @@ public function testBug11891(): void
]);
}

#[RequiresPhp('>= 8.2.0')]
public function testBug15458(): void
{
$this->analyse([__DIR__ . '/data/bug-15458.php'], [
[
'Enum case Bug15458\E::A value mixed does not match the "string" type.',
7,
],
[
'Enum case Bug15458\IntBacked::A value mixed does not match the "int" type.',
12,
],
[
'Enum case Bug15458\Mutual::A value mixed does not match the "string" type.',
17,
],
[
'Enum case Bug15458\Mutual::B value mixed does not match the "string" type.',
18,
],
[
'Enum case Bug15458\CrossA::A value mixed does not match the "string" type.',
23,
],
[
'Enum case Bug15458\CrossB::B value mixed does not match the "string" type.',
28,
],
]);
}

}
40 changes: 40 additions & 0 deletions tests/PHPStan/Rules/Classes/data/bug-15458.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php // lint >= 8.2

namespace Bug15458;

enum E: string
{
case A = self::A->value;
}

enum IntBacked: int
{
case A = self::A->value;
}

enum Mutual: string
{
case A = self::B->value;
case B = self::A->value;
}

enum CrossA: string
{
case A = CrossB::B->value;
}

enum CrossB: string
{
case B = CrossA::A->value;
}

enum Concat: string
{
case A = self::A->value . 'x';
}

enum ForwardReference: string
{
case A = self::B->value . 'a';
case B = 'b';
}
28 changes: 20 additions & 8 deletions turbo-ext/src/ClassReflection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2168,10 +2168,12 @@ class ClassReflection
zv::Arr cases = zv::Arr::create(countOf(reflectionCases.ref()));
if (reflectionCases.ref().isArray()) {
for (auto entry : zv::ArrRef(reflectionCases.raw())) {
zv::Val valueType = enumCaseValueType(entry.value(), initializerExprContext.raw());
if (UNEXPECTED(valueType.isUndef())) return zv::Val();
zv::Val caseName = callOn(entry.value(), PT_LC("getname"), 0, NULL);
if (UNEXPECTED(caseName.isUndef())) return zv::Val();
zend_string *caseNameStr = zval_get_string(caseName.raw());
zv::Val valueType = enumCaseValueType(entry.value(), caseNameStr, initializerExprContext.raw());
zend_string_release(caseNameStr);
if (UNEXPECTED(valueType.isUndef())) return zv::Val();
zv::Val attributes = enumCaseAttributes(entry.value());
if (UNEXPECTED(attributes.isUndef())) return zv::Val();
zv::Val caseReflection = newEnumCaseReflection(entry.value(), valueType.ref(), attributes.ref());
Expand Down Expand Up @@ -2228,7 +2230,7 @@ class ClassReflection
ZVAL_STR(&arg, name);
zv::Val case_ = reflectionCall(PT_LC("getcase"), 1, &arg);
if (UNEXPECTED(case_.isUndef())) return zv::Val();
zv::Val valueType = enumCaseValueType(case_.ref(), NULL);
zv::Val valueType = enumCaseValueType(case_.ref(), name, NULL);
if (UNEXPECTED(valueType.isUndef())) return zv::Val();
zv::Val attributes = enumCaseAttributes(case_.ref());
if (UNEXPECTED(attributes.isUndef())) return zv::Val();
Expand All @@ -2237,10 +2239,11 @@ class ClassReflection
}

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

return initializerExprType(valueExpression.raw(), context);
zv::Val className = getName();
if (UNEXPECTED(className.isUndef())) return zv::Val();
zend_object *object = service(PT_CR_PROP_INITIALIZER_EXPR_TYPE_RESOLVER, "initializerExprTypeResolver");
if (UNEXPECTED(object == NULL)) return zv::Val();
zval resolver;
ZVAL_OBJ(&resolver, object);
zend_string *classNameStr = zval_get_string(className.raw());
zv::Val valueType = pt_initializer_expr_type_resolver_get_enum_case_value_type(&resolver, classNameStr, caseName, valueExpression.raw(), context);
zend_string_release(classNameStr);
return valueType;
}

/* $this->attributeReflectionFactory->fromNativeReflection($case->getAttributes(),
Expand Down
40 changes: 40 additions & 0 deletions turbo-ext/src/InitializerExprTypeResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4838,6 +4838,25 @@ class InitializerExprTypeResolver
return getClassConstFetchTypeByReflection(class_, constantName, classReflection.raw(), getTypeCallback);
}

/* getEnumCaseValueType(): the case value resolved under the
* currentlyResolvingClassConstant guard, mixed on re-entry */
zv::Val getEnumCaseValueType(zend_string *className, zend_string *caseName, zval *valueExpr, zval *context) const
{
zend_string *resolvingName = zend_string_concat3(ZSTR_VAL(className), ZSTR_LEN(className), "::", 2, ZSTR_VAL(caseName), ZSTR_LEN(caseName));
zv::Val resolvingNameHold = zv::Val::adoptString(resolvingName);
if (zend_symtable_exists(Z_ARRVAL_P(prop(slots::currentlyResolvingClassConstant)), resolvingName)) {
return newMixedType();
}

zval trueValue = {};
ZVAL_TRUE(&trueValue);
writeArrayProperty(slots::currentlyResolvingClassConstant, resolvingName, &trueValue);
zv::Val type = getType(valueExpr, context);
/* the twin's finally: released with an exception pending too */
unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName);
return type;
}

/* }}} */

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

zv::Val pt_initializer_expr_type_resolver_get_enum_case_value_type(zval *resolver, zend_string *className, zend_string *caseName, zval *valueExpr, zval *context)
{
if (EXPECTED(isNativeResolver(resolver) && Z_TYPE_P(valueExpr) == IS_OBJECT)) {
return InitializerExprTypeResolver(Z_OBJ_P(resolver)).getEnumCaseValueType(className, caseName, valueExpr, context);
}
static pt_method_site site;
zval classNameZv, caseNameZv;
ZVAL_STR(&classNameZv, className);
ZVAL_STR(&caseNameZv, caseName);
zv::Args argv{&classNameZv, &caseNameZv, valueExpr, context};
return callResolverMethod(site, resolver, PT_LC("getenumcasevaluetype"), 4, argv);
}

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)
{
if (EXPECTED(isNativeResolver(resolver))) {
Expand Down Expand Up @@ -5641,6 +5673,14 @@ PT_MINIT_REGISTRATION(pt_register_initializer_expr_type_resolver)
PT_RETURN_VAL(IETR_THIS.getType(expr, context));
});

cls.method(sigs::getEnumCaseValueType, [](INTERNAL_FUNCTION_PARAMETERS) {
zend_string *className, *caseName;
zval *valueExpr, *context;
if (!zp::parse<zp::Str, zp::Str, zp::Obj, zp::Obj>(execute_data, className, caseName, valueExpr, context)) RETURN_THROWS();
if (UNEXPECTED(!ietrArgExpr(valueExpr, 3) || !ietrArgContext(context, 4))) RETURN_THROWS();
PT_RETURN_VAL(IETR_THIS.getEnumCaseValueType(className, caseName, valueExpr, context));
});

cls.method(sigs::getConcatType, IETR_OPERATOR_GLUE(self.getConcatType(left, right, getTypeCallback)));

cls.method(sigs::resolveConcatType, [](INTERNAL_FUNCTION_PARAMETERS) {
Expand Down
Loading
Loading