Skip to content

Commit 669f01c

Browse files
ondrejmirtesclaude
authored andcommitted
Fix slow analysis on large AND condition chains
Apply the same optimization from 7294ca2 (BooleanOr falsey scope) to BooleanAnd truthy scope. For left-associative chains like A && B && C && D, filterByTruthyValue on the full expression caused O(N^2) re-processing through TypeSpecifier at each nesting level. Since the right side is already processed in leftResult->getTruthyScope(), the left's narrowing is already in the scope — only filter by the right operand. Reduces O(N^2) to O(N). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 70b1ee0 commit 669f01c

File tree

3 files changed

+122
-3
lines changed

3 files changed

+122
-3
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3640,23 +3640,24 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
36403640
$this->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage);
36413641
} elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) {
36423642
$leftResult = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());
3643-
$rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getTruthyScope(), $storage, $nodeCallback, $context);
3643+
$leftTruthyScope = $leftResult->getTruthyScope();
3644+
$rightResult = $this->processExprNode($stmt, $expr->right, $leftTruthyScope, $storage, $nodeCallback, $context);
36443645
$rightExprType = $rightResult->getScope()->getType($expr->right);
36453646
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
36463647
$leftMergedWithRightScope = $leftResult->getFalseyScope();
36473648
} else {
36483649
$leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
36493650
}
36503651

3651-
$this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $storage, $context);
3652+
$this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftTruthyScope), $scope, $storage, $context);
36523653

36533654
$result = new ExpressionResult(
36543655
$leftMergedWithRightScope,
36553656
$leftResult->hasYield() || $rightResult->hasYield(),
36563657
$leftResult->isAlwaysTerminating(),
36573658
array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
36583659
array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
3659-
static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr),
3660+
static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr->right),
36603661
static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr),
36613662
);
36623663
return $result;

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,6 +1638,12 @@ public function testBug14207(): void
16381638
$this->assertNoErrors($errors);
16391639
}
16401640

1641+
public function testBug14207And(): void
1642+
{
1643+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-14207-and.php');
1644+
$this->assertNoErrors($errors);
1645+
}
1646+
16411647
public function testBug13945(): void
16421648
{
16431649
$errors = $this->runAnalyse(__DIR__ . '/data/bug-13945.php');
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14207And;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
public static function is_not_special(string $tag_name): bool {
10+
$x = (
11+
'ADDRESS' !== $tag_name &&
12+
'APPLET' !== $tag_name &&
13+
'AREA' !== $tag_name &&
14+
'ARTICLE' !== $tag_name &&
15+
'ASIDE' !== $tag_name &&
16+
'BASE' !== $tag_name &&
17+
'BASEFONT' !== $tag_name &&
18+
'BGSOUND' !== $tag_name &&
19+
'BLOCKQUOTE' !== $tag_name &&
20+
'BODY' !== $tag_name &&
21+
'BR' !== $tag_name &&
22+
'BUTTON' !== $tag_name &&
23+
'CAPTION' !== $tag_name &&
24+
'CENTER' !== $tag_name &&
25+
'COL' !== $tag_name &&
26+
'COLGROUP' !== $tag_name &&
27+
'DD' !== $tag_name &&
28+
'DETAILS' !== $tag_name &&
29+
'DIR' !== $tag_name &&
30+
'DIV' !== $tag_name &&
31+
'DL' !== $tag_name &&
32+
'DT' !== $tag_name &&
33+
'EMBED' !== $tag_name &&
34+
'FIELDSET' !== $tag_name &&
35+
'FIGCAPTION' !== $tag_name &&
36+
'FIGURE' !== $tag_name &&
37+
'FOOTER' !== $tag_name &&
38+
'FORM' !== $tag_name &&
39+
'FRAME' !== $tag_name &&
40+
'FRAMESET' !== $tag_name &&
41+
'H1' !== $tag_name &&
42+
'H2' !== $tag_name &&
43+
'H3' !== $tag_name &&
44+
'H4' !== $tag_name &&
45+
'H5' !== $tag_name &&
46+
'H6' !== $tag_name &&
47+
'HEAD' !== $tag_name &&
48+
'HEADER' !== $tag_name &&
49+
'HGROUP' !== $tag_name &&
50+
'HR' !== $tag_name &&
51+
'HTML' !== $tag_name &&
52+
'IFRAME' !== $tag_name &&
53+
'IMG' !== $tag_name &&
54+
'INPUT' !== $tag_name &&
55+
'KEYGEN' !== $tag_name &&
56+
'LI' !== $tag_name &&
57+
'LINK' !== $tag_name &&
58+
'LISTING' !== $tag_name &&
59+
'MAIN' !== $tag_name &&
60+
'MARQUEE' !== $tag_name &&
61+
'MENU' !== $tag_name &&
62+
'META' !== $tag_name &&
63+
'NAV' !== $tag_name &&
64+
'NOEMBED' !== $tag_name &&
65+
'NOFRAMES' !== $tag_name &&
66+
'NOSCRIPT' !== $tag_name &&
67+
'OBJECT' !== $tag_name &&
68+
'OL' !== $tag_name &&
69+
'P' !== $tag_name &&
70+
'PARAM' !== $tag_name &&
71+
'PLAINTEXT' !== $tag_name &&
72+
'PRE' !== $tag_name &&
73+
'SCRIPT' !== $tag_name &&
74+
'SEARCH' !== $tag_name &&
75+
'SECTION' !== $tag_name &&
76+
'SELECT' !== $tag_name &&
77+
'SOURCE' !== $tag_name &&
78+
'STYLE' !== $tag_name &&
79+
'SUMMARY' !== $tag_name &&
80+
'TABLE' !== $tag_name &&
81+
'TBODY' !== $tag_name &&
82+
'TD' !== $tag_name &&
83+
'TEMPLATE' !== $tag_name &&
84+
'TEXTAREA' !== $tag_name &&
85+
'TFOOT' !== $tag_name &&
86+
'TH' !== $tag_name &&
87+
'THEAD' !== $tag_name &&
88+
'TITLE' !== $tag_name &&
89+
'TR' !== $tag_name &&
90+
'TRACK' !== $tag_name &&
91+
'UL' !== $tag_name &&
92+
'WBR' !== $tag_name &&
93+
'XMP' !== $tag_name &&
94+
'a1' !== $tag_name &&
95+
'a2' !== $tag_name &&
96+
'a3' !== $tag_name &&
97+
'a4' !== $tag_name &&
98+
'a5' !== $tag_name &&
99+
'a6' !== $tag_name &&
100+
'a7' !== $tag_name &&
101+
'a8' !== $tag_name &&
102+
'a9' !== $tag_name
103+
);
104+
105+
assertType('bool', $x);
106+
if ($x) {
107+
assertType('string', $tag_name);
108+
}
109+
110+
return $x;
111+
}
112+
}

0 commit comments

Comments
 (0)