diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 3df31e38aff..229df8daee6 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -10,6 +10,7 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\OutOfClassScope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; @@ -1272,22 +1273,7 @@ public function getConstant(string $name): ClassConstantReflection } $fileName = $declaringClass->getFileName(); $phpDocType = null; - $currentResolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc( - $declaringClass->getName(), - $name, - ); - if ($currentResolvedPhpDoc === null) { - if ($reflectionConstant->getDocComment() !== false) { - $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $fileName, - $declaringClass->getName(), - null, - null, - $reflectionConstant->getDocComment(), - ); - } - - } + $currentResolvedPhpDoc = $this->findConstantResolvedPhpDoc($reflectionConstant); $nativeType = null; if ($reflectionConstant->getType() !== null) { @@ -1311,18 +1297,7 @@ public function getConstant(string $name): ClassConstantReflection } $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); - $varTags = $resolvedPhpDoc->getVarTags(); - if (isset($varTags[0]) && count($varTags) === 1) { - $varTag = $varTags[0]; - if ($varTag->isExplicit() || $nativeType === null || $nativeType->isSuperTypeOf($varTag->getType())->yes()) { - $phpDocType = TemplateTypeHelper::resolveTemplateTypes( - $varTag->getType(), - $declaringClass->getActiveTemplateTypeMap(), - $declaringClass->getCallSiteVarianceMap(), - TemplateTypeVariance::createInvariant(), - ); - } - } + $phpDocType = self::resolveConstantVarPhpDocType($resolvedPhpDoc, $nativeType, $declaringClass); } $this->constants[$name] = new RealClassClassConstantReflection( @@ -1342,6 +1317,93 @@ public function getConstant(string $name): ClassConstantReflection return $this->constants[$name]; } + /** + * Returns the @var PHPDoc type of a class constant, if any. + * Does not walk ancestors, so it's safe to call from class-constant + * type resolution (which re-enters via @extends>). + * + * @internal + */ + public function getConstantPhpDocType(string $name): ?Type + { + $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); + if ($reflectionConstant === false) { + return null; + } + + $resolvedPhpDoc = $this->findConstantResolvedPhpDoc($reflectionConstant); + if ($resolvedPhpDoc === null) { + return null; + } + + $nativeType = null; + if ($reflectionConstant->getType() !== null) { + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType()); + } + + $declaringClassName = $reflectionConstant->getDeclaringClass()->getName(); + if ($declaringClassName === $this->getName()) { + $declaringClass = $this; + } else { + $declaringClass = $this->getAncestorWithClassName($declaringClassName); + if ($declaringClass === null) { + return null; + } + } + + return self::resolveConstantVarPhpDocType($resolvedPhpDoc, $nativeType, $declaringClass); + } + + private function findConstantResolvedPhpDoc(ReflectionClassConstant $reflectionConstant): ?ResolvedPhpDocBlock + { + $declaringClassName = $reflectionConstant->getDeclaringClass()->getName(); + + $resolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc( + $declaringClassName, + $reflectionConstant->getName(), + ); + if ($resolvedPhpDoc !== null) { + return $resolvedPhpDoc; + } + + $docComment = $reflectionConstant->getDocComment(); + if ($docComment === false) { + return null; + } + + return $this->fileTypeMapper->getResolvedPhpDoc( + $reflectionConstant->getDeclaringClass()->getFileName() ?: null, + $declaringClassName, + null, + null, + $docComment, + ); + } + + private static function resolveConstantVarPhpDocType( + ResolvedPhpDocBlock $resolvedPhpDoc, + ?Type $nativeType, + self $declaringClass, + ): ?Type + { + $varTags = $resolvedPhpDoc->getVarTags(); + if (!isset($varTags[0]) || count($varTags) !== 1) { + return null; + } + + $varTag = $varTags[0]; + if (!$varTag->isExplicit() && $nativeType !== null && !$nativeType->isSuperTypeOf($varTag->getType())->yes()) { + return null; + } + + return TemplateTypeHelper::resolveTemplateTypes( + $varTag->getType(), + $declaringClass->getActiveTemplateTypeMap(), + $declaringClass->getCallSiteVarianceMap(), + TemplateTypeVariance::createInvariant(), + ); + } + public function hasTraitUse(string $traitName): bool { return in_array($traitName, $this->getTraitNames(), true); diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index de9649776c0..8a9fa17b3aa 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2544,13 +2544,12 @@ function (Type $type, callable $traverse): Type { if ($reflectionConstant->getType() !== null) { $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $constantClassReflection); } - $phpDocType = $constantClassReflection->getConstant($constantName)->getPhpDocType(); $types[] = $this->constantResolver->resolveClassConstantType( $constantClassReflection->getName(), $constantName, $constantType, $nativeType, - $phpDocType, + $constantClassReflection->getConstantPhpDocType($constantName), ); unset($this->currentlyResolvingClassConstant[$resolvingName]); continue; @@ -2579,7 +2578,7 @@ function (Type $type, callable $traverse): Type { $constantName, $constantType, $nativeType, - $constantReflection->getPhpDocType(), + $constantClassReflection->getConstantPhpDocType($constantName), ); unset($this->currentlyResolvingClassConstant[$resolvingName]); $types[] = $constantType; diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 9949f3f9a25..6826995d4db 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1536,6 +1536,13 @@ public function testBugInfiniteLoopOnFileTypeMapper(): void $this->assertCount(0, $errors); } + #[RequiresPhp('>= 8.3.0')] + public function testBug14501(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-14501.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return list diff --git a/tests/PHPStan/Analyser/data/bug-14501.php b/tests/PHPStan/Analyser/data/bug-14501.php new file mode 100644 index 00000000000..b9aaa50c2eb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-14501.php @@ -0,0 +1,36 @@ +> */ +final class Bug14501Sub extends Bug14501Base +{ + public const string ATTR_A = 'A'; + public const string ATTR_B = 'B'; + + public const array ATTRIBUTES = [ + self::ATTR_A, + self::ATTR_B, + ]; + + public function check(string $attribute): bool + { + return $attribute === self::ATTR_A; + } +}