Skip to content
Open
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
7 changes: 3 additions & 4 deletions src/PhpDoc/ConstExprNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,13 @@ public function resolve(ConstExprNode $node, NameScope $nameScope): Type
if (!$classReflection->hasConstant($node->name)) {
return new ErrorType();
}
if ($classReflection->isEnum() && $classReflection->hasEnumCase($node->name)) {
return new EnumCaseObjectType($classReflection->getName(), $node->name);
}

$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($node->name);
if ($reflectionConstant === false) {
return new ErrorType();
}
if ($reflectionConstant->isEnumCase()) {
return new EnumCaseObjectType($classReflection->getName(), $node->name);
}
$declaringClass = $reflectionConstant->getDeclaringClass();

return $this->initializerExprTypeResolver->getType(
Expand Down
12 changes: 7 additions & 5 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,9 @@ private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameS
if ($reflectionConstant === false) {
return new ErrorType();
}
if ($reflectionConstant->isEnumCase()) {
return new EnumCaseObjectType($classReflection->getName(), $constantName);
}
$declaringClass = $reflectionConstant->getDeclaringClass();

return $this->initializerExprTypeResolver->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($declaringClass->getName(), $declaringClass->getFileName() ?: null));
Expand Down Expand Up @@ -1380,7 +1383,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
continue;
}

if ($classReflection->isEnum() && $classReflection->hasEnumCase($classConstantName)) {
if ($reflectionConstant->isEnumCase()) {
$constantTypes[] = new EnumCaseObjectType($classReflection->getName(), $classConstantName);
continue;
}
Expand Down Expand Up @@ -1409,14 +1412,13 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
return new ErrorType();
}

if ($classReflection->isEnum() && $classReflection->hasEnumCase($constantName)) {
return new EnumCaseObjectType($classReflection->getName(), $constantName);
}

$reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
if ($reflectionConstant === false) {
return new ErrorType();
}
if ($reflectionConstant->isEnumCase()) {
return new EnumCaseObjectType($classReflection->getName(), $constantName);
}
$declaringClass = $reflectionConstant->getDeclaringClass();

return $this->initializerExprTypeResolver->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($declaringClass->getName(), $declaringClass->getFileName() ?: null));
Expand Down
13 changes: 7 additions & 6 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2747,7 +2747,13 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
continue;
}

if ($constantClassReflection->isEnum() && $constantClassReflection->hasEnumCase($constantName)) {
$reflectionConstant = $constantClassReflection->getNativeReflection()->getReflectionConstant($constantName);
if ($reflectionConstant === false) {
continue;
}

// enums on PHP < 8.0 runtime are reflected through the generic class adapter, isEnum() is false there
if ($reflectionConstant->isEnumCase()) {
$types[] = new EnumCaseObjectType($constantClassReflection->getName(), $constantName);
continue;
}
Expand All @@ -2766,11 +2772,6 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
$this->currentlyResolvingClassConstant[$resolvingName] = true;

if (!$isObject) {
$reflectionConstant = $constantClassReflection->getNativeReflection()->getReflectionConstant($constantName);
if ($reflectionConstant === false) {
unset($this->currentlyResolvingClassConstant[$resolvingName]);
continue;
}
$reflectionConstantDeclaringClass = $reflectionConstant->getDeclaringClass();
$constantType = $this->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($reflectionConstantDeclaringClass->getName(), $reflectionConstantDeclaringClass->getFileName() ?: null));
$nativeType = null;
Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Type\Constant\ConstantStringType;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\RequiresPhp;
use function array_filter;
use function array_map;
use function array_unique;
use function array_values;
Expand Down Expand Up @@ -1808,6 +1809,18 @@ public function testBug15331(): void
$this->assertNoErrors($errors);
}

public function testBug15380(): void
{
// crash
$errors = $this->runAnalyse(__DIR__ . '/data/bug-15380.php');
$errors = array_values(array_filter($errors, static fn (Error $error): bool => $error->getIdentifier() !== 'enum.caseOutsideOfEnum'));
$this->assertCount(2, $errors);
$this->assertSame('Method Bug15380\\Foo::doFoo() has parameter $c with no value type specified in iterable type array.', $errors[0]->getMessage());
$this->assertSame(24, $errors[0]->getLine());
$this->assertSame('PHPDoc tag @param for parameter $c contains unresolvable type.', $errors[1]->getMessage());
$this->assertSame(24, $errors[1]->getLine());
}

#[RequiresPhp('>= 8.0.0')]
public function testBug15520(): void
{
Expand Down
42 changes: 42 additions & 0 deletions tests/PHPStan/Analyser/data/bug-15380.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php // lint >= 8.1

namespace Bug15380;

enum Suit
{
case Hearts;
case Spades;
}

/**
* @method void doMagic(Suit $suit = Suit::Hearts)
*/
class Foo
{

public const SUIT = Suit::Hearts;

/**
* @param Suit::Hearts $a
* @param Suit::* $b
* @param array{Suit::Hearts: int} $c
*/
public function doFoo(Suit $suit, $a, $b, array $c, Suit $d = Suit::Spades): void
{
echo round(1.5);
var_dump(Suit::Hearts, self::SUIT, $suit::Spades);
}

/**
* @param mixed[] $args
*/
public function __call(string $name, array $args): void
{
}

public function doBar(): void
{
$this->doMagic();
}

}
49 changes: 18 additions & 31 deletions turbo-ext/src/InitializerExprTypeResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ constexpr Method mGetParentClass{"getParentClass", PT_OP_GET_PARENT_CLASS};
constexpr Method mGetName{"getName", PT_OP_GET_NAME};
constexpr Method mIsFinal{"isFinal", PT_OP_IS_FINAL};
constexpr Method mHasConstant{"hasConstant"};
constexpr Method mHasEnumCase{"hasEnumCase"};
constexpr Method mIsEnumCase{"isEnumCase"};
constexpr Method mGetNativeReflection{"getNativeReflection"};
constexpr Method mGetReflectionConstant{"getReflectionConstant"};
constexpr Method mGetDeclaringClass{"getDeclaringClass"};
Expand Down Expand Up @@ -4691,22 +4691,25 @@ class InitializerExprTypeResolver
}
}

IETR_VAL(nativeReflection, callOn(constantClassReflection.raw(), mGetNativeReflection));
IETR_VAL(reflectionConstant, callOn(nativeReflection.raw(), mGetReflectionConstant, 1, constantName));
if (Z_TYPE_P(reflectionConstant.raw()) == IS_FALSE) {
continue;
}

// enums on PHP < 8.0 runtime are reflected through the generic class adapter, isEnum() is false there
{
bool isEnum;
if (UNEXPECTED(!classReflectionIsEnum(constantClassReflection.raw(), isEnum))) return zv::Val();
if (isEnum) {
IETR_VAL(hasEnumCase, callOn(constantClassReflection.raw(), mHasEnumCase, 1, constantName));
if (zend_is_true(hasEnumCase.raw())) {
IETR_VAL(reflectionName, classReflectionName(constantClassReflection.raw()));
if (UNEXPECTED(Z_TYPE_P(reflectionName.raw()) != IS_STRING)) {
pt_throw_should_not_happen();
return zv::Val();
}
zval enumCase;
if (UNEXPECTED(!pt_enum_case_object_type_new(&enumCase, Z_STR_P(reflectionName.raw()), constantNameString))) return zv::Val();
types.push(zv::Val::adopt(enumCase));
continue;
IETR_VAL(isEnumCase, callOn(reflectionConstant.raw(), mIsEnumCase));
if (zend_is_true(isEnumCase.raw())) {
IETR_VAL(reflectionName, classReflectionName(constantClassReflection.raw()));
if (UNEXPECTED(Z_TYPE_P(reflectionName.raw()) != IS_STRING)) {
pt_throw_should_not_happen();
return zv::Val();
}
zval enumCase;
if (UNEXPECTED(!pt_enum_case_object_type_new(&enumCase, Z_STR_P(reflectionName.raw()), constantNameString))) return zv::Val();
types.push(zv::Val::adopt(enumCase));
continue;
}
}

Expand Down Expand Up @@ -4734,12 +4737,6 @@ class InitializerExprTypeResolver
writeArrayProperty(slots::currentlyResolvingClassConstant, resolvingName, &trueValue);

if (!isObject) {
IETR_VAL(nativeReflection, callOn(constantClassReflection.raw(), mGetNativeReflection));
IETR_VAL(reflectionConstant, callOn(nativeReflection.raw(), mGetReflectionConstant, 1, constantName));
if (Z_TYPE_P(reflectionConstant.raw()) == IS_FALSE) {
unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName);
continue;
}
IETR_VAL(reflectionConstantDeclaringClass, callOn(reflectionConstant.raw(), mGetDeclaringClass));
IETR_VAL(valueExpression, callOn(reflectionConstant.raw(), mGetValueExpression));
IETR_VAL(declaringClassName, callOn(reflectionConstantDeclaringClass.raw(), mGetName));
Expand Down Expand Up @@ -5197,16 +5194,6 @@ class InitializerExprTypeResolver
return true;
}

/* $classReflection->isEnum(); false = pending exception */
static bool classReflectionIsEnum(zval *classReflection, bool &out)
{
if (EXPECTED(Z_TYPE_P(classReflection) == IS_OBJECT && Z_OBJCE_P(classReflection) == pt_ce_class_reflection)) return pt_class_reflection_is_enum(Z_OBJ_P(classReflection), out);
zv::Val result = callOn(classReflection, mIsEnum);
if (UNEXPECTED(result.isUndef())) return false;
out = zend_is_true(result.raw());
return true;
}

/* }}} */

/* {{{ more value helpers */
Expand Down
Loading