Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Keep an empty array next to an oversized array in a union
reduceArrays() leaves empty constant arrays out when it generalizes to an
oversized array, but a union of an array that already is oversized with
array{} went through processArrayTypes(), which folded array{} into it:
the oversized accessory then rejected [] (a false positive for T inferred
from both) and non-empty-array accepted the union (a missed error).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv
  • Loading branch information
ondrejmirtes and claude committed Sep 27, 2026
commit 71cb8ca943f507a43b3ebebf847eee8e445ccc67
38 changes: 38 additions & 0 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,28 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array
return $commonAccessoryTypes;
}

/**
* @param Type[] $arrayTypes
*/
private static function findEmptyArrayNextToOversizedArray(array $arrayTypes): ?Type
{
if (count($arrayTypes) < 2) {
return null;
}

$emptyArray = null;
$hasOversizedArray = false;
foreach ($arrayTypes as $arrayType) {
if ($arrayType->isOversizedArray()->yes()) {
$hasOversizedArray = true;
} elseif ($emptyArray === null && $arrayType->isConstantArray()->yes() && $arrayType->isIterableAtLeastOnce()->no()) {
$emptyArray = $arrayType;
}
}

return $hasOversizedArray ? $emptyArray : null;
}

/**
* @param list<Type> $arrayTypes
* @return Type[]
Expand All @@ -1047,6 +1069,22 @@ private static function processArrayTypes(array $arrayTypes): array
return [];
}

$emptyArray = self::findEmptyArrayNextToOversizedArray($arrayTypes);
if ($emptyArray !== null) {
// like reduceArrays() does when it generalizes to an oversized array:
// folded into it, the empty array would be lost to the oversized
// accessory, which accepts only arrays that hold something
$otherArrayTypes = [];
foreach ($arrayTypes as $arrayType) {
if ($arrayType->isConstantArray()->yes() && $arrayType->isIterableAtLeastOnce()->no()) {
continue;
}
$otherArrayTypes[] = $arrayType;
}

return [$emptyArray, ...self::processArrayTypes($otherArrayTypes)];
}

$accessoryTypes = self::processArrayAccessoryTypes($arrayTypes);

if (count($arrayTypes) === 1) {
Expand Down
273 changes: 273 additions & 0 deletions tests/PHPStan/Analyser/nsrt/oversized-array-union-with-empty-array.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?php declare(strict_types = 1);

namespace OversizedArrayUnionWithEmptyArray;

use function PHPStan\Testing\assertType;

function doFoo(bool $flag): void
{
$big = [
'K0' => 'v0',
'K1' => 'v1',
'K2' => 'v2',
'K3' => 'v3',
'K4' => 'v4',
'K5' => 'v5',
'K6' => 'v6',
'K7' => 'v7',
'K8' => 'v8',
'K9' => 'v9',
'K10' => 'v10',
'K11' => 'v11',
'K12' => 'v12',
'K13' => 'v13',
'K14' => 'v14',
'K15' => 'v15',
'K16' => 'v16',
'K17' => 'v17',
'K18' => 'v18',
'K19' => 'v19',
'K20' => 'v20',
'K21' => 'v21',
'K22' => 'v22',
'K23' => 'v23',
'K24' => 'v24',
'K25' => 'v25',
'K26' => 'v26',
'K27' => 'v27',
'K28' => 'v28',
'K29' => 'v29',
'K30' => 'v30',
'K31' => 'v31',
'K32' => 'v32',
'K33' => 'v33',
'K34' => 'v34',
'K35' => 'v35',
'K36' => 'v36',
'K37' => 'v37',
'K38' => 'v38',
'K39' => 'v39',
'K40' => 'v40',
'K41' => 'v41',
'K42' => 'v42',
'K43' => 'v43',
'K44' => 'v44',
'K45' => 'v45',
'K46' => 'v46',
'K47' => 'v47',
'K48' => 'v48',
'K49' => 'v49',
'K50' => 'v50',
'K51' => 'v51',
'K52' => 'v52',
'K53' => 'v53',
'K54' => 'v54',
'K55' => 'v55',
'K56' => 'v56',
'K57' => 'v57',
'K58' => 'v58',
'K59' => 'v59',
'K60' => 'v60',
'K61' => 'v61',
'K62' => 'v62',
'K63' => 'v63',
'K64' => 'v64',
'K65' => 'v65',
'K66' => 'v66',
'K67' => 'v67',
'K68' => 'v68',
'K69' => 'v69',
'K70' => 'v70',
'K71' => 'v71',
'K72' => 'v72',
'K73' => 'v73',
'K74' => 'v74',
'K75' => 'v75',
'K76' => 'v76',
'K77' => 'v77',
'K78' => 'v78',
'K79' => 'v79',
'K80' => 'v80',
'K81' => 'v81',
'K82' => 'v82',
'K83' => 'v83',
'K84' => 'v84',
'K85' => 'v85',
'K86' => 'v86',
'K87' => 'v87',
'K88' => 'v88',
'K89' => 'v89',
'K90' => 'v90',
'K91' => 'v91',
'K92' => 'v92',
'K93' => 'v93',
'K94' => 'v94',
'K95' => 'v95',
'K96' => 'v96',
'K97' => 'v97',
'K98' => 'v98',
'K99' => 'v99',
'K100' => 'v100',
'K101' => 'v101',
'K102' => 'v102',
'K103' => 'v103',
'K104' => 'v104',
'K105' => 'v105',
'K106' => 'v106',
'K107' => 'v107',
'K108' => 'v108',
'K109' => 'v109',
'K110' => 'v110',
'K111' => 'v111',
'K112' => 'v112',
'K113' => 'v113',
'K114' => 'v114',
'K115' => 'v115',
'K116' => 'v116',
'K117' => 'v117',
'K118' => 'v118',
'K119' => 'v119',
'K120' => 'v120',
'K121' => 'v121',
'K122' => 'v122',
'K123' => 'v123',
'K124' => 'v124',
'K125' => 'v125',
'K126' => 'v126',
'K127' => 'v127',
'K128' => 'v128',
'K129' => 'v129',
'K130' => 'v130',
'K131' => 'v131',
'K132' => 'v132',
'K133' => 'v133',
'K134' => 'v134',
'K135' => 'v135',
'K136' => 'v136',
'K137' => 'v137',
'K138' => 'v138',
'K139' => 'v139',
'K140' => 'v140',
'K141' => 'v141',
'K142' => 'v142',
'K143' => 'v143',
'K144' => 'v144',
'K145' => 'v145',
'K146' => 'v146',
'K147' => 'v147',
'K148' => 'v148',
'K149' => 'v149',
'K150' => 'v150',
'K151' => 'v151',
'K152' => 'v152',
'K153' => 'v153',
'K154' => 'v154',
'K155' => 'v155',
'K156' => 'v156',
'K157' => 'v157',
'K158' => 'v158',
'K159' => 'v159',
'K160' => 'v160',
'K161' => 'v161',
'K162' => 'v162',
'K163' => 'v163',
'K164' => 'v164',
'K165' => 'v165',
'K166' => 'v166',
'K167' => 'v167',
'K168' => 'v168',
'K169' => 'v169',
'K170' => 'v170',
'K171' => 'v171',
'K172' => 'v172',
'K173' => 'v173',
'K174' => 'v174',
'K175' => 'v175',
'K176' => 'v176',
'K177' => 'v177',
'K178' => 'v178',
'K179' => 'v179',
'K180' => 'v180',
'K181' => 'v181',
'K182' => 'v182',
'K183' => 'v183',
'K184' => 'v184',
'K185' => 'v185',
'K186' => 'v186',
'K187' => 'v187',
'K188' => 'v188',
'K189' => 'v189',
'K190' => 'v190',
'K191' => 'v191',
'K192' => 'v192',
'K193' => 'v193',
'K194' => 'v194',
'K195' => 'v195',
'K196' => 'v196',
'K197' => 'v197',
'K198' => 'v198',
'K199' => 'v199',
'K200' => 'v200',
'K201' => 'v201',
'K202' => 'v202',
'K203' => 'v203',
'K204' => 'v204',
'K205' => 'v205',
'K206' => 'v206',
'K207' => 'v207',
'K208' => 'v208',
'K209' => 'v209',
'K210' => 'v210',
'K211' => 'v211',
'K212' => 'v212',
'K213' => 'v213',
'K214' => 'v214',
'K215' => 'v215',
'K216' => 'v216',
'K217' => 'v217',
'K218' => 'v218',
'K219' => 'v219',
'K220' => 'v220',
'K221' => 'v221',
'K222' => 'v222',
'K223' => 'v223',
'K224' => 'v224',
'K225' => 'v225',
'K226' => 'v226',
'K227' => 'v227',
'K228' => 'v228',
'K229' => 'v229',
'K230' => 'v230',
'K231' => 'v231',
'K232' => 'v232',
'K233' => 'v233',
'K234' => 'v234',
'K235' => 'v235',
'K236' => 'v236',
'K237' => 'v237',
'K238' => 'v238',
'K239' => 'v239',
'K240' => 'v240',
'K241' => 'v241',
'K242' => 'v242',
'K243' => 'v243',
'K244' => 'v244',
'K245' => 'v245',
'K246' => 'v246',
'K247' => 'v247',
'K248' => 'v248',
'K249' => 'v249',
'K250' => 'v250',
'K251' => 'v251',
'K252' => 'v252',
'K253' => 'v253',
'K254' => 'v254',
'K255' => 'v255',
'K256' => 'v256',
'K257' => 'v257',
'K258' => 'v258',
'K259' => 'v259',
];
assertType("array{}|(non-empty-array<literal-string&non-falsy-string&uppercase-string, literal-string&lowercase-string&non-falsy-string>&oversized-array)", $flag ? $big : []);
assertType("array{}|(non-empty-array<literal-string&non-falsy-string&uppercase-string, literal-string&lowercase-string&non-falsy-string>&oversized-array)", $flag ? [] : $big);
}
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3268,4 +3268,15 @@ public function testRoundModePhp84(): void
]);
}

public function testOversizedArrayUnionWithEmptyArray(): void
{
$this->analyse([__DIR__ . '/data/oversized-array-union-with-empty-array.php'], [
[
'Parameter #1 $a of function OversizedArrayUnionWithEmptyArrayRule\\nonEmpty expects non-empty-array<string, string>, array{}|(non-empty-array<literal-string&non-falsy-string, literal-string&non-falsy-string>) given.',
286,
'array{}|(non-empty-array<literal-string&non-falsy-string, literal-string&non-falsy-string>) might be empty.',
],
]);
}

}
Loading