diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index a31e70433e..e80d956b36 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -301,6 +301,14 @@ private function createProperty( ); } + if ($resolvedPhpDoc === null && $constructorName !== null) { + $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty( + $declaringClassReflection, + $propertyName, + null, + ); + } + if ($resolvedPhpDoc !== null) { $varTags = $resolvedPhpDoc->getVarTags(); if (isset($varTags[0]) && count($varTags) === 1) { @@ -326,14 +334,12 @@ private function createProperty( $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); } - if ($phpDocType === null) { - if (isset($constructorName)) { - $resolvedConstructorPhpDoc = $declaringClassReflection->getConstructor()->getResolvedPhpDoc(); - if ($resolvedConstructorPhpDoc !== null) { - $paramTags = $resolvedConstructorPhpDoc->getParamTags(); - if (isset($paramTags[$propertyReflection->getName()])) { - $phpDocType = $paramTags[$propertyReflection->getName()]->getType(); - } + if ($constructorName !== null && ($phpDocType === null || $docComment === null)) { + $resolvedConstructorPhpDoc = $declaringClassReflection->getConstructor()->getResolvedPhpDoc(); + if ($resolvedConstructorPhpDoc !== null) { + $paramTags = $resolvedConstructorPhpDoc->getParamTags(); + if (isset($paramTags[$propertyReflection->getName()])) { + $phpDocType = $paramTags[$propertyReflection->getName()]->getType(); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14564.php b/tests/PHPStan/Analyser/nsrt/bug-14564.php new file mode 100644 index 0000000000..128088db03 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14564.php @@ -0,0 +1,215 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug14564; + +use function PHPStan\Testing\assertType; + +interface A { + /** @var array */ + public array $test { get; } + + /** @var non-empty-string */ + public string $nonEmptyString { get; } + + /** @var int<1,max> */ + public int $positive { get; } +} + +// Regular properties inherit PHPDoc types from interface +class B implements A { + + public array $test = [ 1 ]; + + public string $nonEmptyString = ''; + + public int $positive = -1; + +} + +// Promoted properties inherit from interface +class C implements A { + + public function __construct( + + public array $test, + + public string $nonEmptyString, + + public int $positive, + + ) { } + +} + +function test(C $c): void { + assertType('array', $c->test); + assertType('non-empty-string', $c->nonEmptyString); + assertType('int<1, max>', $c->positive); +} + +// Explicit @var on promoted property +class D implements A { + + public function __construct( + + /** @var array */ + public array $test, + + /** @var non-empty-string */ + public string $nonEmptyString, + + /** @var int<1,max> */ + public int $positive, + + ) { } + +} + +function test2(D $d): void { + assertType('array', $d->test); + assertType('non-empty-string', $d->nonEmptyString); + assertType('int<1, max>', $d->positive); +} + +// Promoted properties inherit from parent class +class ParentClass { + /** @var array */ + public array $items; + + /** @var non-empty-string */ + public string $name; +} + +class ChildWithPromoted extends ParentClass { + + public function __construct( + public array $items, + public string $name, + ) { } + +} + +function test3(ChildWithPromoted $c): void { + assertType('array', $c->items); + assertType('non-empty-string', $c->name); +} + +// Inheritance from abstract class +abstract class AbstractBase { + /** @var array */ + public array $data; +} + +class ConcreteWithPromoted extends AbstractBase { + + public function __construct( + public array $data, + ) { } + +} + +function test4(ConcreteWithPromoted $c): void { + assertType('array', $c->data); +} + +// Multi-level inheritance +class Middle extends AbstractBase { +} + +class GrandchildWithPromoted extends Middle { + + public function __construct( + public array $data, + ) { } + +} + +function test5(GrandchildWithPromoted $g): void { + assertType('array', $g->data); +} + +// Constructor @param overrides inherited type +class WithParam implements A { + + /** + * @param list $test + * @param non-empty-string $nonEmptyString + * @param positive-int $positive + */ + public function __construct( + public array $test, + public string $nonEmptyString, + public int $positive, + ) { } + +} + +function test6(WithParam $w): void { + assertType('list', $w->test); + assertType('non-empty-string', $w->nonEmptyString); + assertType('int<1, max>', $w->positive); +} + +// Explicit @var on promoted property overrides inherited type +class WithVar implements A { + + public function __construct( + /** @var list */ + public array $test, + /** @var non-empty-string */ + public string $nonEmptyString, + /** @var int<1,max> */ + public int $positive, + ) { } + +} + +function test7(WithVar $w): void { + assertType('list', $w->test); + assertType('non-empty-string', $w->nonEmptyString); + assertType('int<1, max>', $w->positive); +} + +// Generic interface +/** + * @template T + */ +interface Container { + /** @var T */ + public mixed $value { get; } +} + +/** @implements Container */ +class StringContainer implements Container { + + public function __construct( + public mixed $value, + ) { } + +} + +function test8(StringContainer $c): void { + assertType('string', $c->value); +} + +// Private property in parent should not conflict with child's property +class FooWithPrivate { + /** @var array */ + private array $items = []; +} + +class BarWithPromoted extends FooWithPrivate { + + public function __construct( + public array $items, + ) { + parent::__construct(); + } + +} + +function test9(BarWithPromoted $b): void { + assertType('array', $b->items); +} diff --git a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php index 4324a66839..27b31fa5df 100644 --- a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -345,4 +345,11 @@ public function testFixMissingOverrideAttribute(): void $this->fix(__DIR__ . '/data/property-override-attr-missing.php', __DIR__ . '/data/property-override-attr-missing.php.fixed'); } + #[RequiresPhp('>= 8.4.0')] + public function testBug14564(): void + { + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14564.php'], []); + } + }