Skip to content

Commit 9adae6c

Browse files
committed
Fix performance problem with nested BooleanOr and BooleanAnd
1 parent 1b0c6a0 commit 9adae6c

File tree

3 files changed

+212
-8
lines changed

3 files changed

+212
-8
lines changed

src/Analyser/MutatingScope.php

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@
143143
class MutatingScope implements Scope
144144
{
145145

146+
private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4;
147+
146148
/** @var Type[] */
147149
private array $resolvedTypes = [];
148150

@@ -768,10 +770,14 @@ private function resolveType(string $exprString, Expr $node): Type
768770
return new ConstantBooleanType(false);
769771
}
770772

771-
$noopCallback = static function (): void {
772-
};
773-
$leftResult = $this->nodeScopeResolver->processExprNode($node->left, $this, $noopCallback, ExpressionContext::createDeep());
774-
$rightBooleanType = $leftResult->getTruthyScope()->getType($node->right)->toBoolean();
773+
if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) {
774+
$noopCallback = static function (): void {
775+
};
776+
$leftResult = $this->nodeScopeResolver->processExprNode($node->left, $this, $noopCallback, ExpressionContext::createDeep());
777+
$rightBooleanType = $leftResult->getTruthyScope()->getType($node->right)->toBoolean();
778+
} else {
779+
$rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean();
780+
}
775781

776782
if ($rightBooleanType->isFalse()->yes()) {
777783
return new ConstantBooleanType(false);
@@ -796,10 +802,14 @@ private function resolveType(string $exprString, Expr $node): Type
796802
return new ConstantBooleanType(true);
797803
}
798804

799-
$noopCallback = static function (): void {
800-
};
801-
$leftResult = $this->nodeScopeResolver->processExprNode($node->left, $this, $noopCallback, ExpressionContext::createDeep());
802-
$rightBooleanType = $leftResult->getFalseyScope()->getType($node->right)->toBoolean();
805+
if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) {
806+
$noopCallback = static function (): void {
807+
};
808+
$leftResult = $this->nodeScopeResolver->processExprNode($node->left, $this, $noopCallback, ExpressionContext::createDeep());
809+
$rightBooleanType = $leftResult->getFalseyScope()->getType($node->right)->toBoolean();
810+
} else {
811+
$rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean();
812+
}
803813

804814
if ($rightBooleanType->isTrue()->yes()) {
805815
return new ConstantBooleanType(true);
@@ -4707,6 +4717,20 @@ private function compareVariableTypeHolders(array $variableTypeHolders, array $o
47074717
return true;
47084718
}
47094719

4720+
private function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int
4721+
{
4722+
while (
4723+
$expr instanceof BinaryOp\BooleanOr
4724+
|| $expr instanceof BinaryOp\LogicalOr
4725+
|| $expr instanceof BinaryOp\BooleanAnd
4726+
|| $expr instanceof BinaryOp\LogicalAnd
4727+
) {
4728+
return $this->getBooleanExpressionDepth($expr->left, $depth + 1);
4729+
}
4730+
4731+
return $depth;
4732+
}
4733+
47104734
/** @api */
47114735
public function canAccessProperty(PropertyReflection $propertyReflection): bool
47124736
{

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,12 @@ public function testBug9428(): void
12171217
$this->assertNoErrors($errors);
12181218
}
12191219

1220+
public function testBug9690(): void
1221+
{
1222+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-9690.php');
1223+
$this->assertNoErrors($errors);
1224+
}
1225+
12201226
/**
12211227
* @param string[]|null $allAnalysedFiles
12221228
* @return Error[]
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
namespace Bug9690;
4+
5+
class par {
6+
public static function isId(string $id): bool {
7+
return (bool) rand(0,1);
8+
}
9+
};
10+
11+
class a1 extends par {};
12+
class a2 extends par {};
13+
class a3 extends par {};
14+
class a4 extends par {};
15+
class a5 extends par {};
16+
class a6 extends par {};
17+
class a7 extends par {};
18+
class a8 extends par {};
19+
class a9 extends par {};
20+
class a10 extends par {};
21+
class a11 extends par {};
22+
class a12 extends par {};
23+
class a13 extends par {};
24+
class a14 extends par {};
25+
class a15 extends par {};
26+
class a16 extends par {};
27+
class a17 extends par {};
28+
class a18 extends par {};
29+
class a19 extends par {};
30+
class a20 extends par {};
31+
class a21 extends par {};
32+
class a22 extends par {};
33+
class a23 extends par {};
34+
class a24 extends par {};
35+
class a25 extends par {};
36+
class a26 extends par {};
37+
class a27 extends par {};
38+
class a28 extends par {};
39+
class a29 extends par {};
40+
class a30 extends par {};
41+
class a31 extends par {};
42+
class a32 extends par {};
43+
class a33 extends par {};
44+
class a34 extends par {};
45+
class a35 extends par {};
46+
class a36 extends par {};
47+
class a37 extends par {};
48+
class a38 extends par {};
49+
class a39 extends par {};
50+
class a40 extends par {};
51+
class a41 extends par {};
52+
class a42 extends par {};
53+
class a43 extends par {};
54+
class a44 extends par {};
55+
class a45 extends par {};
56+
class a46 extends par {};
57+
class a47 extends par {};
58+
class a48 extends par {};
59+
class a49 extends par {};
60+
class a50 extends par {};
61+
62+
class test {
63+
public static function isId(string $id): bool {
64+
return (
65+
a1::isId($id)
66+
|| a2::isId($id)
67+
|| a3::isId($id)
68+
|| a4::isId($id)
69+
|| a5::isId($id)
70+
|| a6::isId($id)
71+
|| a7::isId($id)
72+
|| a8::isId($id)
73+
|| a9::isId($id)
74+
|| a10::isId($id)
75+
|| a11::isId($id)
76+
|| a12::isId($id)
77+
|| a13::isId($id)
78+
|| a14::isId($id)
79+
|| a15::isId($id)
80+
|| a16::isId($id)
81+
|| a17::isId($id)
82+
|| a18::isId($id)
83+
|| a19::isId($id)
84+
|| a20::isId($id)
85+
|| a21::isId($id)
86+
|| a22::isId($id)
87+
|| a23::isId($id)
88+
|| a24::isId($id)
89+
|| a25::isId($id)
90+
|| a26::isId($id)
91+
|| a27::isId($id)
92+
|| a28::isId($id)
93+
|| a29::isId($id)
94+
|| a30::isId($id)
95+
|| a31::isId($id)
96+
|| a32::isId($id)
97+
|| a33::isId($id)
98+
|| a34::isId($id)
99+
|| a35::isId($id)
100+
|| a36::isId($id)
101+
|| a37::isId($id)
102+
|| a38::isId($id)
103+
|| a39::isId($id)
104+
|| a40::isId($id)
105+
|| a41::isId($id)
106+
|| a42::isId($id)
107+
|| a43::isId($id)
108+
|| a44::isId($id)
109+
|| a45::isId($id)
110+
|| a46::isId($id)
111+
|| a47::isId($id)
112+
|| a48::isId($id)
113+
|| a49::isId($id)
114+
|| a50::isId($id)
115+
);
116+
}
117+
}
118+
119+
class test2 {
120+
public static function isId(string $id): bool {
121+
return (
122+
a1::isId($id)
123+
&& a2::isId($id)
124+
&& a3::isId($id)
125+
&& a4::isId($id)
126+
&& a5::isId($id)
127+
&& a6::isId($id)
128+
&& a7::isId($id)
129+
&& a8::isId($id)
130+
&& a9::isId($id)
131+
&& a10::isId($id)
132+
&& a11::isId($id)
133+
&& a12::isId($id)
134+
&& a13::isId($id)
135+
&& a14::isId($id)
136+
&& a15::isId($id)
137+
&& a16::isId($id)
138+
&& a17::isId($id)
139+
&& a18::isId($id)
140+
&& a19::isId($id)
141+
&& a20::isId($id)
142+
&& a21::isId($id)
143+
&& a22::isId($id)
144+
&& a23::isId($id)
145+
&& a24::isId($id)
146+
&& a25::isId($id)
147+
&& a26::isId($id)
148+
&& a27::isId($id)
149+
&& a28::isId($id)
150+
&& a29::isId($id)
151+
&& a30::isId($id)
152+
&& a31::isId($id)
153+
&& a32::isId($id)
154+
&& a33::isId($id)
155+
&& a34::isId($id)
156+
&& a35::isId($id)
157+
&& a36::isId($id)
158+
&& a37::isId($id)
159+
&& a38::isId($id)
160+
&& a39::isId($id)
161+
&& a40::isId($id)
162+
&& a41::isId($id)
163+
&& a42::isId($id)
164+
&& a43::isId($id)
165+
&& a44::isId($id)
166+
&& a45::isId($id)
167+
&& a46::isId($id)
168+
&& a47::isId($id)
169+
&& a48::isId($id)
170+
&& a49::isId($id)
171+
&& a50::isId($id)
172+
);
173+
}
174+
}

0 commit comments

Comments
 (0)