diff --git a/src/PhpDoc/ConstExprNodeResolver.php b/src/PhpDoc/ConstExprNodeResolver.php index 7d1b0ad824f..4842fbfc91d 100644 --- a/src/PhpDoc/ConstExprNodeResolver.php +++ b/src/PhpDoc/ConstExprNodeResolver.php @@ -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( diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 4c52e21fe2a..eb451b15470 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -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)); @@ -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; } @@ -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)); diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 7440726aa83..c2efde88888 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -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; } @@ -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; diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index ac5201a9242..a3cbe76ed33 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -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; @@ -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 { diff --git a/tests/PHPStan/Analyser/data/bug-15380.php b/tests/PHPStan/Analyser/data/bug-15380.php new file mode 100644 index 00000000000..c89a18e95be --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-15380.php @@ -0,0 +1,42 @@ += 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(); + } + +} diff --git a/turbo-ext/src/InitializerExprTypeResolver.cpp b/turbo-ext/src/InitializerExprTypeResolver.cpp index 43e5e58952a..5b65cdcc653 100644 --- a/turbo-ext/src/InitializerExprTypeResolver.cpp +++ b/turbo-ext/src/InitializerExprTypeResolver.cpp @@ -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"}; @@ -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; } } @@ -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)); @@ -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 */