From 1308c520e763e286809f01a4100af8ba156c1d34 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 30 Oct 2023 14:49:18 +0100 Subject: [PATCH] Fix crash when template types map and variances map are not of equal length --- src/Reflection/ClassReflection.php | 38 +++++++---- .../Analyser/AnalyserIntegrationTest.php | 10 +++ .../Analyser/data/bug-10049-recursive.php | 66 +++++++++++++++++++ 3 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-10049-recursive.php diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 2d768f7678..2755d387c3 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -241,21 +241,26 @@ public function getName(): string public function getDisplayName(bool $withTemplateTypes = true): string { - $name = $this->displayName; - if ( $withTemplateTypes === false || $this->resolvedTemplateTypeMap === null || count($this->resolvedTemplateTypeMap->getTypes()) === 0 ) { - return $name; + return $this->displayName; + } + + $templateTypes = []; + $variances = $this->getCallSiteVarianceMap()->getVariances(); + foreach ($this->getActiveTemplateTypeMap()->getTypes() as $name => $templateType) { + $variance = $variances[$name] ?? null; + if ($variance === null) { + continue; + } + + $templateTypes[] = TypeProjectionHelper::describe($templateType, $variance, VerbosityLevel::typeOnly()); } - return $name . '<' . implode(',', array_map( - static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::typeOnly()), - $this->getActiveTemplateTypeMap()->getTypes(), - $this->getCallSiteVarianceMap()->getVariances(), - )) . '>'; + return $this->displayName . '<' . implode(',', $templateTypes) . '>'; } public function getCacheKey(): string @@ -268,11 +273,18 @@ public function getCacheKey(): string $cacheKey = $this->displayName; if ($this->resolvedTemplateTypeMap !== null) { - $cacheKey .= '<' . implode(',', array_map( - static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::cache()), - $this->getActiveTemplateTypeMap()->getTypes(), - $this->getCallSiteVarianceMap()->getVariances(), - )) . '>'; + $templateTypes = []; + $variances = $this->getCallSiteVarianceMap()->getVariances(); + foreach ($this->getActiveTemplateTypeMap()->getTypes() as $name => $templateType) { + $variance = $variances[$name] ?? null; + if ($variance === null) { + continue; + } + + $templateTypes[] = TypeProjectionHelper::describe($templateType, $variance, VerbosityLevel::cache()); + } + + $cacheKey .= '<' . implode(',', $templateTypes) . '>'; } if ($this->extraCacheKey !== null) { diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index d52741e04d..6e149081f0 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1242,6 +1242,16 @@ public function testBug9994(): void $this->assertSame('Parameter #2 $callback of function array_filter expects callable(1|2|3|null): mixed, false given.', $errors[1]->getMessage()); } + public function testBug10049(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10049-recursive.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/data/bug-10049-recursive.php b/tests/PHPStan/Analyser/data/bug-10049-recursive.php new file mode 100644 index 0000000000..b0887157ba --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10049-recursive.php @@ -0,0 +1,66 @@ + + */ +abstract class SimpleEntity +{ + /** + * @param SimpleTable $table + */ + public function __construct(protected readonly SimpleTable $table) + { + } +} + +/** + * @template-covariant E of SimpleEntity + */ +class SimpleTable +{ + /** + * @template ENTITY of SimpleEntity + * + * @param class-string $className + * + * @return SimpleTable + */ + public static function table(string $className, string $name): SimpleTable + { + return new SimpleTable($className, $name); + } + + /** + * @param class-string $className + */ + private function __construct(readonly string $className, readonly string $table) + { + } +} + +/** + * @template-extends SimpleEntity + */ +class TestEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(TestEntity::class, 'testentity'); + parent::__construct($table); + } +} + + +/** + * @template-extends SimpleEntity + */ +class AnotherEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(AnotherEntity::class, 'anotherentity'); + parent::__construct($table); + } +}