Skip to content

Commit 81e06a5

Browse files
phpstan-botstaabmclaude
authored
Faster HasOffsetType, HasOffsetValueType intersection (phpstan#6195)
Co-authored-by: staabm <120441+staabm@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent c0c39ea commit 81e06a5

2 files changed

Lines changed: 99 additions & 1 deletion

File tree

src/Type/TypeCombinator.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2084,7 +2084,21 @@ public static function doIntersect(Type ...$types): Type
20842084
$accessoryBaseTypes = null;
20852085
break;
20862086
}
2087-
$accessoryBaseTypes[] = $type->getDefaultBaseType();
2087+
// Accessory types share their default base type: every string accessory
2088+
// returns `string`, hasOffset() and hasOffsetValue() both return
2089+
// `array|ArrayAccess`. Adding the same base type again narrows nothing -
2090+
// intersection is idempotent - but the intersect() below distributes
2091+
// `A & (B | C)` one union at a time, so n copies of `array|ArrayAccess`
2092+
// would cost 2^n recursive calls before the duplicates are recognized at
2093+
// the leaves. That is why isset() with many offsets used to grow
2094+
// exponentially: each offset contributes one hasOffset().
2095+
$baseType = $type->getDefaultBaseType();
2096+
foreach ($accessoryBaseTypes as $addedBaseType) {
2097+
if ($addedBaseType->equals($baseType)) {
2098+
continue 2;
2099+
}
2100+
}
2101+
$accessoryBaseTypes[] = $baseType;
20882102
}
20892103
if ($accessoryBaseTypes !== null) {
20902104
// Accessory types never stand alone — supply the base type they refine.

tests/bench/data/bug-15061.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15061;
4+
5+
/**
6+
* Every isset() subject narrows the array to hasOffset(), and intersecting n of them
7+
* used to cost 2^n TypeCombinator::intersect() calls: each hasOffset() contributes the
8+
* same `array|ArrayAccess` default base type, and those n identical unions were
9+
* distributed over each other one at a time.
10+
*
11+
* @phpstan-type FooEntity array{
12+
* a?: string,
13+
* b?: string,
14+
* c?: string,
15+
* d?: string,
16+
* e?: string,
17+
* f?: string,
18+
* g?: string,
19+
* h?: string,
20+
* i?: string,
21+
* j?: string,
22+
* k?: string,
23+
* l?: string,
24+
* m?: string,
25+
* n?: string,
26+
* o?: string,
27+
* p?: string,
28+
* q?: string,
29+
* r?: string,
30+
* s?: string,
31+
* t?: string,
32+
* u?: string,
33+
* v?: string,
34+
* w?: string,
35+
* x?: string,
36+
* y?: string,
37+
* z?: string,
38+
* }
39+
*/
40+
final class TestClass
41+
{
42+
43+
public function __invoke(): void
44+
{
45+
/** @var array<string, FooEntity> $entities */
46+
$entities = [];
47+
48+
foreach ($entities as $entity) {
49+
$ok = isset(
50+
$entity['a'],
51+
$entity['b'],
52+
$entity['c'],
53+
$entity['d'],
54+
$entity['e'],
55+
$entity['f'],
56+
$entity['g'],
57+
$entity['h'],
58+
$entity['i'],
59+
$entity['j'],
60+
$entity['k'],
61+
$entity['l'],
62+
$entity['m'],
63+
$entity['n'],
64+
$entity['o'],
65+
$entity['p'],
66+
$entity['q'],
67+
$entity['r'],
68+
$entity['s'],
69+
$entity['t'],
70+
$entity['u'],
71+
$entity['v'],
72+
$entity['w'],
73+
$entity['x'],
74+
$entity['y'],
75+
$entity['z'],
76+
);
77+
78+
if (!$ok) {
79+
continue;
80+
}
81+
}
82+
}
83+
84+
}

0 commit comments

Comments
 (0)