From 85b8086b06a89663bfa4676ec59ffc1b28ef2902 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 22 May 2025 12:41:32 +0200 Subject: [PATCH 1/2] Fix var php doc inheritance for constant --- src/PhpDoc/PhpDocNodeResolver.php | 4 +-- src/PhpDoc/ResolvedPhpDocBlock.php | 2 +- src/PhpDoc/Tag/VarTag.php | 14 ++++++++-- src/Reflection/ClassReflection.php | 12 ++++++--- ...patibleClassConstantPhpDocTypeRuleTest.php | 9 +++++++ tests/PHPStan/Rules/PhpDoc/data/bug-10911.php | 27 +++++++++++++++++++ 6 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/bug-10911.php diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 02fed04bcc..91aaf7037c 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -76,9 +76,9 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar } if ($tagValue->variableName !== '') { $variableName = substr($tagValue->variableName, 1); - $resolved[$variableName] = new VarTag($type); + $resolved[$variableName] = new VarTag($type, true); } else { - $varTag = new VarTag($type); + $varTag = new VarTag($type, true); $tagResolved[] = $varTag; } } diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index ed45d60e48..771490e7c9 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -887,7 +887,7 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array { foreach ($parent->getVarTags() as $key => $parentVarTag) { - return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock, TemplateTypeVariance::createInvariant())]; + return [$key => self::resolveTemplateTypeInTag($parentVarTag->toImplicit(), $phpDocBlock, TemplateTypeVariance::createInvariant())]; } return null; diff --git a/src/PhpDoc/Tag/VarTag.php b/src/PhpDoc/Tag/VarTag.php index 0d93daeac8..b1ae823ea0 100644 --- a/src/PhpDoc/Tag/VarTag.php +++ b/src/PhpDoc/Tag/VarTag.php @@ -11,7 +11,7 @@ class VarTag implements TypedTag { - public function __construct(private Type $type) + public function __construct(private Type $type, private bool $isExplicit) { } @@ -25,7 +25,17 @@ public function getType(): Type */ public function withType(Type $type): TypedTag { - return new self($type); + return new self($type, $this->isExplicit); + } + + public function isExplicit(): bool + { + return $this->isExplicit; + } + + public function toImplicit(): self + { + return new self($this->type, false); } } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index d031cb8816..c00bd778cb 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1094,10 +1094,6 @@ public function getConstant(string $name): ClassConstantReflection $isDeprecated = $resolvedPhpDoc->isDeprecated(); $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); - $varTags = $resolvedPhpDoc->getVarTags(); - if (isset($varTags[0]) && count($varTags) === 1) { - $phpDocType = $varTags[0]->getType(); - } $nativeType = null; if ($reflectionConstant->getType() !== null) { @@ -1106,6 +1102,14 @@ public function getConstant(string $name): ClassConstantReflection $nativeType = $this->signatureMapProvider->getClassConstantMetadata($declaringClass->getName(), $name)['nativeType']; } + $varTags = $resolvedPhpDoc->getVarTags(); + if (isset($varTags[0]) && count($varTags) === 1) { + $varTag = $varTags[0]; + if ($varTag->isExplicit() || $nativeType->isSuperTypeOf($varTag->getType())->yes()) { + $phpDocType = $varTag->getType(); + } + } + $this->constants[$name] = new ClassConstantReflection( $this->initializerExprTypeResolver, $declaringClass, diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php index a66be60a2d..91a74e428b 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php @@ -50,4 +50,13 @@ public function testNativeType(): void ]); } + public function testBug10911(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/bug-10911.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10911.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10911.php new file mode 100644 index 0000000000..7cce5e8b88 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10911.php @@ -0,0 +1,27 @@ += 8.3 + +namespace Bug10911; + +abstract class Model +{ + /** + * The name of the "created at" column. + * + * @var string|null + */ + const CREATED_AT = 'created_at'; + + /** + * The name of the "updated at" column. + * + * @var string|null + */ + const UPDATED_AT = 'updated_at'; +} + +class TestModel extends Model +{ + const string CREATED_AT = 'data_criacao'; + const string UPDATED_AT = 'data_alteracao'; + const DELETED_AT = null; +} From 048cedf1f8075b091158c57be690d23e86f0ee98 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 22 May 2025 12:47:19 +0200 Subject: [PATCH 2/2] Fix --- src/Reflection/ClassReflection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index c00bd778cb..23f1432c3c 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1105,7 +1105,7 @@ public function getConstant(string $name): ClassConstantReflection $varTags = $resolvedPhpDoc->getVarTags(); if (isset($varTags[0]) && count($varTags) === 1) { $varTag = $varTags[0]; - if ($varTag->isExplicit() || $nativeType->isSuperTypeOf($varTag->getType())->yes()) { + if ($varTag->isExplicit() || $nativeType === null || $nativeType->isSuperTypeOf($varTag->getType())->yes()) { $phpDocType = $varTag->getType(); } }