Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 15 additions & 1 deletion src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2084,7 +2084,21 @@ public static function doIntersect(Type ...$types): Type
$accessoryBaseTypes = null;
break;
}
$accessoryBaseTypes[] = $type->getDefaultBaseType();
// Accessory types share their default base type: every string accessory
// returns `string`, hasOffset() and hasOffsetValue() both return
// `array|ArrayAccess`. Adding the same base type again narrows nothing -
// intersection is idempotent - but the intersect() below distributes
// `A & (B | C)` one union at a time, so n copies of `array|ArrayAccess`
// would cost 2^n recursive calls before the duplicates are recognized at
// the leaves. That is why isset() with many offsets used to grow
// exponentially: each offset contributes one hasOffset().
$baseType = $type->getDefaultBaseType();
foreach ($accessoryBaseTypes as $addedBaseType) {
if ($addedBaseType->equals($baseType)) {
continue 2;
}
}
$accessoryBaseTypes[] = $baseType;
}
if ($accessoryBaseTypes !== null) {
// Accessory types never stand alone — supply the base type they refine.
Expand Down
84 changes: 84 additions & 0 deletions tests/bench/data/bug-15061.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php declare(strict_types = 1);

namespace Bug15061;

/**
* Every isset() subject narrows the array to hasOffset(), and intersecting n of them
* used to cost 2^n TypeCombinator::intersect() calls: each hasOffset() contributes the
* same `array|ArrayAccess` default base type, and those n identical unions were
* distributed over each other one at a time.
*
* @phpstan-type FooEntity array{
* a?: string,
* b?: string,
* c?: string,
* d?: string,
* e?: string,
* f?: string,
* g?: string,
* h?: string,
* i?: string,
* j?: string,
* k?: string,
* l?: string,
* m?: string,
* n?: string,
* o?: string,
* p?: string,
* q?: string,
* r?: string,
* s?: string,
* t?: string,
* u?: string,
* v?: string,
* w?: string,
* x?: string,
* y?: string,
* z?: string,
* }
*/
final class TestClass
{

public function __invoke(): void
{
/** @var array<string, FooEntity> $entities */
$entities = [];

foreach ($entities as $entity) {
$ok = isset(
$entity['a'],
$entity['b'],
$entity['c'],
$entity['d'],
$entity['e'],
$entity['f'],
$entity['g'],
$entity['h'],
$entity['i'],
$entity['j'],
$entity['k'],
$entity['l'],
$entity['m'],
$entity['n'],
$entity['o'],
$entity['p'],
$entity['q'],
$entity['r'],
$entity['s'],
$entity['t'],
$entity['u'],
$entity['v'],
$entity['w'],
$entity['x'],
$entity['y'],
$entity['z'],
);

if (!$ok) {
continue;
}
}
}

}
Loading