From c381cc004ac38cc605bbd4996bb5c3e2fe858678 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:36:35 +0100 Subject: [PATCH] Fix phpstan/phpstan#14138: errors for argument array template types no longer reported (argument.type) (#5300) Co-authored-by: Claude Opus 4.6 --- .../Php/PhpClassReflectionExtension.php | 50 ++++++++++--------- .../Rules/Classes/InstantiationRuleTest.php | 12 +++++ .../PHPStan/Rules/Classes/data/bug-14138.php | 37 ++++++++++++++ .../PHPStan/Rules/Pure/PureMethodRuleTest.php | 7 +++ .../Rules/Pure/data/bug-14138-pure.php | 27 ++++++++++ 5 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-14138.php create mode 100644 tests/PHPStan/Rules/Pure/data/bug-14138-pure.php diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index e471943e87c..fccce2fa772 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -891,14 +891,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(), @@ -914,22 +906,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; @@ -938,6 +914,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 8558296e149..9232d2bc296 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -615,4 +615,16 @@ public function testBug11006(): void $this->analyse([__DIR__ . '/data/bug-11006.php'], []); } + #[RequiresPhp('>= 8.0')] + 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 00000000000..185e16b309a --- /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 0aa586868ed..9c1a95adc9f 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 00000000000..0c4f6668194 --- /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); + } +}