diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 7dd05c682c..f5d2f09b71 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -893,14 +893,6 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } $phpDocParameterTypes[$paramName] = $paramTag->getType(); } - foreach ($phpDocParameterTypes as $paramName => $paramType) { - $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( - $paramType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap(), - $phpDocBlockClassReflection->getCallSiteVarianceMap(), - TemplateTypeVariance::createContravariant(), - ); - } foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( $paramOutTag->getType(), @@ -916,22 +908,6 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure ??= $resolvedPhpDoc->isPure(); - if ($isPure === null) { - $classResolvedPhpDoc = $phpDocBlockClassReflection->getResolvedPhpDoc(); - if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) { - if ( - strtolower($methodReflection->getName()) === '__construct' - || ( - ($phpDocReturnType === null || !$phpDocReturnType->isVoid()->yes()) - && !$nativeReturnType->isVoid()->yes() - ) - ) { - $isPure = true; - } - } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) { - $isPure = false; - } - } $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; @@ -940,6 +916,32 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } } + if ($isPure === null) { + $classResolvedPhpDoc = $phpDocBlockClassReflection->getResolvedPhpDoc(); + if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) { + if ( + strtolower($methodReflection->getName()) === '__construct' + || ( + ($phpDocReturnType === null || !$phpDocReturnType->isVoid()->yes()) + && !$nativeReturnType->isVoid()->yes() + ) + ) { + $isPure = true; + } + } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) { + $isPure = false; + } + } + + foreach ($phpDocParameterTypes as $paramName => $paramType) { + $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramType, + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ); + } + return $this->methodReflectionFactory->create( $actualDeclaringClass, $declaringTrait, diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 43e79cf13f..9cc2feed08 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -646,4 +646,15 @@ public function testConstantParameterCheckInstantiation(): void ]); } + public function testBug14138(): void + { + $this->analyse([__DIR__ . '/data/bug-14138.php'], [ + [ + 'Parameter #1 $data of class Bug14138\Foo constructor expects array{foo: int, bar: int}, array{foo: 1} given.', + 36, + "Array does not have offset 'bar'.", + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-14138.php b/tests/PHPStan/Rules/Classes/data/bug-14138.php new file mode 100644 index 0000000000..185e16b309 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-14138.php @@ -0,0 +1,37 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug14138; + +/** + * @template T of array + */ +abstract class AbstractApiData +{ + public function __construct( + /** @var T */ + protected array $data + ) {} + + /** + * @return T + */ + public function getData(): array + { + return $this->data; + } +} + + +/** + * @extends AbstractApiData + */ +class Foo extends AbstractApiData {} + +function testing(): void { + $a = new Foo(["foo" => 1]); +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index 0aa586868e..9c1a95adc9 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -279,6 +279,13 @@ public function testAllMethodsArePure(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug14138Pure(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14138-pure.php'], []); + } + public function testBug12382(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Pure/data/bug-14138-pure.php b/tests/PHPStan/Rules/Pure/data/bug-14138-pure.php new file mode 100644 index 0000000000..0c4f666819 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-14138-pure.php @@ -0,0 +1,27 @@ += 8.0 + +namespace Bug14138Pure; + +/** + * @phpstan-all-methods-pure + */ +class PureClassWithPromotedProps +{ + public function __construct( + protected int $value + ) {} + + public function getValue(): int + { + return $this->value; + } +} + +class TestCaller +{ + /** @phpstan-pure */ + public function callPureConstructor(): PureClassWithPromotedProps + { + return new PureClassWithPromotedProps(1); + } +}