Skip to content

Commit c9460fa

Browse files
committed
Merge remote-tracking branch 'origin/2.1.x' into 2.2.x
2 parents d30c1bb + 79212f5 commit c9460fa

File tree

4 files changed

+142
-0
lines changed

4 files changed

+142
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PhpParser\Node\Expr\StaticCall;
2020
use PhpParser\Node\Expr\StaticPropertyFetch;
2121
use PhpParser\Node\Name;
22+
use PHPStan\Analyser\ExprHandler\BooleanAndHandler;
2223
use PHPStan\DependencyInjection\AutowiredService;
2324
use PHPStan\Node\Expr\AlwaysRememberedExpr;
2425
use PHPStan\Node\Expr\TypeExpr;
@@ -99,6 +100,8 @@ final class TypeSpecifier
99100

100101
private const MAX_ACCESSORIES_LIMIT = 8;
101102

103+
private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4;
104+
102105
/** @var MethodTypeSpecifyingExtension[][]|null */
103106
private ?array $methodTypeSpecifyingExtensionsByClass = null;
104107

@@ -731,6 +734,13 @@ public function specifyTypesInCondition(
731734
if (!$scope instanceof MutatingScope) {
732735
throw new ShouldNotHappenException();
733736
}
737+
738+
// For deep BooleanOr chains, flatten and process all arms at once
739+
// to avoid O(n^2) recursive filterByFalseyValue calls
740+
if (BooleanAndHandler::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) {
741+
return $this->specifyTypesForFlattenedBooleanOr($scope, $expr, $context);
742+
}
743+
734744
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
735745
$rightScope = $scope->filterByFalseyValue($expr->left);
736746
$rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
@@ -1967,6 +1977,60 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes
19671977
return [];
19681978
}
19691979

1980+
/**
1981+
* Flatten a deep BooleanOr chain into leaf expressions and process them
1982+
* without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n)
1983+
* for chains with many arms (e.g., 80+ === comparisons in ||).
1984+
*/
1985+
private function specifyTypesForFlattenedBooleanOr(
1986+
MutatingScope $scope,
1987+
BooleanOr|LogicalOr $expr,
1988+
TypeSpecifierContext $context,
1989+
): SpecifiedTypes
1990+
{
1991+
// Collect all leaf expressions from the chain
1992+
$arms = [];
1993+
$current = $expr;
1994+
while ($current instanceof BooleanOr || $current instanceof LogicalOr) {
1995+
$arms[] = $current->right;
1996+
$current = $current->left;
1997+
}
1998+
$arms[] = $current; // leftmost leaf
1999+
$arms = array_reverse($arms);
2000+
2001+
if ($context->false() || $context->falsey()) {
2002+
// Falsey: all arms are false → union all SpecifiedTypes
2003+
$result = new SpecifiedTypes([], []);
2004+
foreach ($arms as $arm) {
2005+
$armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2006+
$result = $result->unionWith($armTypes);
2007+
}
2008+
return $result->setRootExpr($expr);
2009+
}
2010+
2011+
// Truthy: at least one arm is true → intersect all normalized SpecifiedTypes
2012+
$armSpecifiedTypes = [];
2013+
foreach ($arms as $arm) {
2014+
$armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2015+
$armSpecifiedTypes[] = $armTypes->normalize($scope);
2016+
}
2017+
2018+
$types = $armSpecifiedTypes[0];
2019+
for ($i = 1; $i < count($armSpecifiedTypes); $i++) {
2020+
$types = $types->intersectWith($armSpecifiedTypes[$i]);
2021+
}
2022+
2023+
$result = new SpecifiedTypes(
2024+
$types->getSureTypes(),
2025+
$types->getSureNotTypes(),
2026+
);
2027+
if ($types->shouldOverwrite()) {
2028+
$result = $result->setAlwaysOverwriteTypes();
2029+
}
2030+
2031+
return $result->setRootExpr($expr);
2032+
}
2033+
19702034
/**
19712035
* @return array<string, ConditionalExpressionHolder[]>
19722036
*/

src/Type/Constant/ConstantFloatType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ public function toArrayKey(): Type
9999
return new ConstantIntegerType((int) $this->value);
100100
}
101101

102+
public function getFiniteTypes(): array
103+
{
104+
if (is_nan($this->value)) {
105+
return [];
106+
}
107+
108+
return [$this];
109+
}
110+
102111
public function generalize(GeneralizePrecision $precision): Type
103112
{
104113
return new FloatType();

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,4 +1193,15 @@ public function testInTrait(): void
11931193
]);
11941194
}
11951195

1196+
public function testBug11054(): void
1197+
{
1198+
$this->analyse([__DIR__ . '/data/bug-11054.php'], [
1199+
[
1200+
'Strict comparison using === between mixed and array{INF} will always evaluate to false.',
1201+
47,
1202+
'Type array{INF} has already been eliminated from mixed.',
1203+
],
1204+
]);
1205+
}
1206+
11961207
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11054;
4+
5+
class XXX {
6+
public const DEF_VALUE = [NAN];
7+
public const NO_VALUE = [NAN];
8+
}
9+
10+
class YYY
11+
{
12+
public const LOSSLESS = [NAN];
13+
14+
/**
15+
* @param string|int|mixed $substituteChar
16+
*/
17+
public static function convertEncoding (
18+
string $str,
19+
string $sourceEncoding,
20+
string $targetEncoding,
21+
$substituteChar = XXX::DEF_VALUE,
22+
$defValue = XXX::NO_VALUE
23+
): string {
24+
if ($substituteChar === XXX::DEF_VALUE) { // no error expected
25+
return mb_convert_encoding($str, $targetEncoding, $sourceEncoding);
26+
}
27+
28+
if ($substituteChar === self::LOSSLESS) { // no error expected
29+
return $str;
30+
}
31+
32+
return $str;
33+
}
34+
}
35+
36+
class InfTest {
37+
public const DEF_VALUE = [INF];
38+
public const NO_VALUE = [INF];
39+
40+
/** @param mixed $v */
41+
public static function test($v): void
42+
{
43+
if ($v === self::DEF_VALUE) { // no error expected
44+
return;
45+
}
46+
47+
if ($v === self::NO_VALUE) {} // error expected - INF === INF is true, so narrowing is correct
48+
}
49+
}
50+
51+
class SimpleTest {
52+
/** @param mixed $v */
53+
public static function test($v): void
54+
{
55+
if ($v === [NAN]) {} // no error expected
56+
if ($v === [INF]) {} // no error expected
57+
}
58+
}

0 commit comments

Comments
 (0)