From 09fbc92c415a2b9789463b3ca8fa7c73cc3475bd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jul 2024 13:10:16 +0200 Subject: [PATCH] Optimize array_map with many arrays --- .../ArrayMapFunctionReturnTypeExtension.php | 38 +-- src/Type/TypeCombinator.php | 2 +- .../Analyser/AnalyserIntegrationTest.php | 10 + tests/PHPStan/Analyser/data/bug-11297.php | 251 ++++++++++++++++++ 4 files changed, 285 insertions(+), 16 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-11297.php diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 50841093a8..af0f49d231 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -66,23 +66,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantArrays = $arrayType->getConstantArrays(); if (count($constantArrays) > 0) { $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $returnedArrayBuilder->setOffsetValueType( - $keyType, - $valueType, - $constantArray->isOptionalKey($i), - ); + $totalCount = TypeCombinator::countConstantArrayValueTypes($constantArrays) * TypeCombinator::countConstantArrayValueTypes([$valueType]); + if ($totalCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + foreach ($constantArrays as $constantArray) { + $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $returnedArrayBuilder->setOffsetValueType( + $keyType, + $valueType, + $constantArray->isOptionalKey($i), + ); + } + $returnedArray = $returnedArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $returnedArray = AccessoryArrayListType::intersectWith($returnedArray); + } + $arrayTypes[] = $returnedArray; } - $returnedArray = $returnedArrayBuilder->getArray(); - if ($constantArray->isList()->yes()) { - $returnedArray = AccessoryArrayListType::intersectWith($returnedArray); - } - $arrayTypes[] = $returnedArray; - } - $mappedArrayType = TypeCombinator::union(...$arrayTypes); + $mappedArrayType = TypeCombinator::union(...$arrayTypes); + } else { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + $arrayType->getIterableKeyType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)); + } } elseif ($arrayType->isArray()->yes()) { $mappedArrayType = TypeCombinator::intersect(new ArrayType( $arrayType->getIterableKeyType(), diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index d31d6ff6af..88d04af91f 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -826,7 +826,7 @@ private static function optimizeConstantArrays(array $types): array /** * @param Type[] $types */ - private static function countConstantArrayValueTypes(array $types): int + public static function countConstantArrayValueTypes(array $types): int { $constantArrayValuesCount = 0; foreach ($types as $type) { diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 0b57adc405..442e5863fe 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1406,6 +1406,16 @@ public function testBug11292(): void $this->assertNoErrors($errors); } + public function testBug11297(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11297.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/data/bug-11297.php b/tests/PHPStan/Analyser/data/bug-11297.php new file mode 100644 index 0000000000..5bc767581c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11297.php @@ -0,0 +1,251 @@ += 8.1 + +namespace Bug11297; + +class ClassA { + /** @param array $array */ + public static function doSomething(string $string, array $array): void + { + } +} + +enum Icon: string +{ + case CASE1 = 'case1'; + case CASE2 = 'case2'; + case CASE3 = 'case3'; + case CASE4 = 'case4'; + case CASE5 = 'case5'; + case CASE6 = 'case6'; + case CASE7 = 'case7'; + case CASE8 = 'case8'; + case CASE9 = 'case9'; + case CASE10 = 'case10'; + case CASE11 = 'case11'; + case CASE12 = 'case12'; + case CASE13 = 'case13'; + case CASE14 = 'case14'; + case CASE15 = 'case15'; + case CASE16 = 'case16'; + case CASE17 = 'case17'; + case CASE18 = 'case18'; + case CASE19 = 'case19'; + case CASE20 = 'case20'; + case CASE21 = 'case21'; + case CASE22 = 'case22'; + case CASE23 = 'case23'; + case CASE24 = 'case24'; + case CASE25 = 'case25'; + case CASE26 = 'case26'; + case CASE27 = 'case27'; + case CASE28 = 'case28'; + case CASE29 = 'case29'; + case CASE30 = 'case30'; + case CASE31 = 'case31'; + case CASE32 = 'case32'; + case CASE33 = 'case33'; + case CASE34 = 'case34'; + case CASE35 = 'case35'; + case CASE36 = 'case36'; + case CASE37 = 'case37'; + case CASE38 = 'case38'; + case CASE39 = 'case39'; + case CASE40 = 'case40'; + case CASE41 = 'case41'; + case CASE42 = 'case42'; + case CASE43 = 'case43'; + case CASE44 = 'case44'; + case CASE45 = 'case45'; + case CASE46 = 'case46'; + case CASE47 = 'case47'; + case CASE48 = 'case48'; + case CASE49 = 'case49'; + case CASE50 = 'case50'; + case CASE51 = 'case51'; + case CASE52 = 'case52'; + case CASE53 = 'case53'; + case CASE54 = 'case54'; + case CASE55 = 'case55'; + case CASE56 = 'case56'; + case CASE57 = 'case57'; + case CASE58 = 'case58'; + case CASE59 = 'case59'; + case CASE60 = 'case60'; + case CASE61 = 'case61'; + case CASE62 = 'case62'; + case CASE63 = 'case63'; + case CASE64 = 'case64'; + case CASE65 = 'case65'; + case CASE66 = 'case66'; + case CASE67 = 'case67'; + case CASE68 = 'case68'; + case CASE69 = 'case69'; + case CASE70 = 'case70'; + case CASE71 = 'case71'; + case CASE72 = 'case72'; + case CASE73 = 'case73'; + case CASE74 = 'case74'; + case CASE75 = 'case75'; + case CASE76 = 'case76'; + case CASE77 = 'case77'; + case CASE78 = 'case78'; + case CASE79 = 'case79'; + case CASE80 = 'case80'; + case CASE81 = 'case81'; + case CASE82 = 'case82'; + case CASE83 = 'case83'; + case CASE84 = 'case84'; + case CASE85 = 'case85'; + case CASE86 = 'case86'; + case CASE87 = 'case87'; + case CASE88 = 'case88'; + case CASE89 = 'case89'; + case CASE90 = 'case90'; + case CASE91 = 'case91'; + case CASE92 = 'case92'; + case CASE93 = 'case93'; + case CASE94 = 'case94'; + case CASE95 = 'case95'; + case CASE96 = 'case96'; + case CASE97 = 'case97'; + case CASE98 = 'case98'; + case CASE99 = 'case99'; + case CASE100 = 'case100'; + case CASE101 = 'case101'; + case CASE102 = 'case102'; + case CASE103 = 'case103'; + case CASE104 = 'case104'; + case CASE105 = 'case105'; + case CASE106 = 'case106'; + case CASE107 = 'case107'; + case CASE108 = 'case108'; + case CASE109 = 'case109'; + case CASE110 = 'case110'; + case CASE111 = 'case111'; + case CASE112 = 'case112'; + case CASE113 = 'case113'; + case CASE114 = 'case114'; + case CASE115 = 'case115'; + case CASE116 = 'case116'; + case CASE117 = 'case117'; + case CASE118 = 'case118'; + case CASE119 = 'case119'; + case CASE120 = 'case120'; + case CASE121 = 'case121'; + case CASE122 = 'case122'; + case CASE123 = 'case123'; + case CASE124 = 'case124'; + case CASE125 = 'case125'; + case CASE126 = 'case126'; + case CASE127 = 'case127'; + case CASE128 = 'case128'; + case CASE129 = 'case129'; + case CASE130 = 'case130'; + case CASE131 = 'case131'; + case CASE132 = 'case132'; + case CASE133 = 'case133'; + case CASE134 = 'case134'; + case CASE135 = 'case135'; + case CASE136 = 'case136'; + case CASE137 = 'case137'; + case CASE138 = 'case138'; + case CASE139 = 'case139'; + case CASE140 = 'case140'; + case CASE141 = 'case141'; + case CASE142 = 'case142'; + case CASE143 = 'case143'; + case CASE144 = 'case144'; + case CASE145 = 'case145'; + case CASE146 = 'case146'; + case CASE147 = 'case147'; + case CASE148 = 'case148'; + case CASE149 = 'case149'; + case CASE150 = 'case150'; + case CASE151 = 'case151'; + case CASE152 = 'case152'; + case CASE153 = 'case153'; + case CASE154 = 'case154'; + case CASE155 = 'case155'; + case CASE156 = 'case156'; + case CASE157 = 'case157'; + case CASE158 = 'case158'; + case CASE159 = 'case159'; + case CASE160 = 'case160'; + case CASE161 = 'case161'; + case CASE162 = 'case162'; + case CASE163 = 'case163'; + case CASE164 = 'case164'; + case CASE165 = 'case165'; + case CASE166 = 'case166'; + case CASE167 = 'case167'; + case CASE168 = 'case168'; + case CASE169 = 'case169'; + case CASE170 = 'case170'; + case CASE171 = 'case171'; + case CASE172 = 'case172'; + case CASE173 = 'case173'; + case CASE174 = 'case174'; + case CASE175 = 'case175'; + case CASE176 = 'case176'; + case CASE177 = 'case177'; + case CASE178 = 'case178'; + case CASE179 = 'case179'; + case CASE180 = 'case180'; + case CASE181 = 'case181'; + case CASE182 = 'case182'; + case CASE183 = 'case183'; + case CASE184 = 'case184'; + case CASE185 = 'case185'; + case CASE186 = 'case186'; + case CASE187 = 'case187'; + case CASE188 = 'case188'; + case CASE189 = 'case189'; + case CASE190 = 'case190'; + case CASE191 = 'case191'; + case CASE192 = 'case192'; + case CASE193 = 'case193'; + case CASE194 = 'case194'; + case CASE195 = 'case195'; + case CASE196 = 'case196'; + case CASE197 = 'case197'; + case CASE198 = 'case198'; + case CASE199 = 'case199'; + case CASE200 = 'case200'; + + public function getFileIdentifier(): string + { + return match ($this) { + default => $this->value + }; + } + + public function getBackendLabelIdentifier(): string + { + return 'foo:' . $this->getLocallangIdentifier(); + } + + private function getLocallangIdentifier(): string + { + return 'foo.icon.' . $this->value; + } +} + +(static function (string $table): void { + /** + * Add TCA for top bar field in pages. + */ + ClassA::doSomething($table, [ + 'foo' => [ + 'config' => [ + 'items' => [['', ''], ...array_map( + static fn (Icon $icon): array => [ + $icon->getBackendLabelIdentifier(), + $icon->value, + 'foo/' . $icon->getFileIdentifier() . '.svg', + ], + Icon::cases(), + )], + ], + ], + ]); +})('foo');