Skip to content

Commit 8a25d14

Browse files
phpstan-botclaude
authored andcommitted
Compare constant scalars refined by accessory types against other union members in TypeCombinator::union()
- `TypeCombinator::union()` buckets every type whose `isConstantScalarValue()` is yes into per-class scalar groups that are never compared with each other. An intersection such as `'Foo'&hasMethod(m1)` (produced by `method_exists()` / `property_exists()` on a constant class-string) landed in the `IntersectionType` bucket, so `'Foo'|('Foo'&hasMethod(m1))` was never collapsed to `'Foo'`. - After N `method_exists($class, $method)` checks (e.g. a foreach over a constant array being unrolled) the type of `$class` grew to 2^N union members, making analysis time exponential. - Such intersections now stay with the regular types, where `compareTypesInUnion()` absorbs them into the plain constant scalar or into a less refined intersection. - Ported the same change to the native mirror in `turbo-ext/src/TypeCombinator.cpp`. - Probed analogous constructs: `property_exists()` on a constant class-string had the same problem and is fixed by the same change (covered by the test); enum cases, objects and integers passed to `method_exists()`/`property_exists()` already collapsed correctly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4ef9ab7 commit 8a25d14

5 files changed

Lines changed: 64 additions & 3 deletions

File tree

‎phpstan-baseline.neon‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1678,7 +1678,7 @@ parameters:
16781678
-
16791679
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
16801680
identifier: phpstanApi.instanceofType
1681-
count: 7
1681+
count: 8
16821682
path: src/Type/TypeCombinator.php
16831683

16841684
-

‎src/Type/TypeCombinator.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,10 @@ public static function doUnion(Type ...$types): Type
420420
$enumCaseTypes = [];
421421
$integerRangeTypes = [];
422422
for ($i = 0; $i < $typesCount; $i++) {
423-
if ($types[$i]->isConstantScalarValue()->yes()) {
423+
// a constant scalar refined by an accessory type ('Foo'&hasMethod(bar))
424+
// stays with the other types so it is compared against the plain
425+
// constant scalars and other refinements of the same value
426+
if ($types[$i]->isConstantScalarValue()->yes() && !$types[$i] instanceof IntersectionType) {
424427
$type = $types[$i];
425428
$scalarTypes[get_class($type)][$type->describe(VerbosityLevel::cache())] = $type;
426429
unset($types[$i]);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15591;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
}
10+
11+
function test(): void
12+
{
13+
$class = Foo::class;
14+
foreach (['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12'] as $method) {
15+
if (method_exists($class, $method)) {
16+
}
17+
}
18+
assertType("'Bug15591\\\\Foo'", $class);
19+
}
20+
21+
function testStraightLine(): void
22+
{
23+
$class = Foo::class;
24+
if (method_exists($class, 'm1')) {
25+
assertType("'Bug15591\\\\Foo'&hasMethod(m1)", $class);
26+
}
27+
assertType("'Bug15591\\\\Foo'", $class);
28+
if (method_exists($class, 'm2')) {
29+
assertType("'Bug15591\\\\Foo'&hasMethod(m2)", $class);
30+
}
31+
assertType("'Bug15591\\\\Foo'", $class);
32+
}
33+
34+
function testProperty(): void
35+
{
36+
$class = Foo::class;
37+
if (property_exists($class, 'p1')) {
38+
}
39+
if (property_exists($class, 'p2')) {
40+
}
41+
assertType("'Bug15591\\\\Foo'", $class);
42+
}

‎tests/bench/data/bug-15591.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace MethodExistsLoopRepro;
4+
5+
class Foo
6+
{
7+
}
8+
9+
function test(): void
10+
{
11+
$class = Foo::class;
12+
foreach (['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12'] as $method) {
13+
if (method_exists($class, $method)) {
14+
}
15+
}
16+
}

‎turbo-ext/src/TypeCombinator.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,7 @@ class TypeCombinator
10401040
zval *type = types[i].raw();
10411041
zend_long isConstantScalarValue = callTrinary(type, PT_LC("isconstantscalarvalue"));
10421042
PT_FAIL_IF_NEG(isConstantScalarValue);
1043-
if (isConstantScalarValue == PT_TRI_YES) {
1043+
if (isConstantScalarValue == PT_TRI_YES && !isInstance(type, pt_ce_intersection_type)) {
10441044
zv::Val description = describe(type, cacheLevel);
10451045
zend_string *key = describedKey(description);
10461046
if (UNEXPECTED(key == NULL)) return zv::Val();

0 commit comments

Comments
 (0)