Skip to content

Commit 64495a4

Browse files
staabmphpstan-bot
authored andcommitted
Fix slow analysis on return with large OR condition chains
- Changed BooleanOr falsey scope computation to avoid reprocessing the entire chain through TypeSpecifier at each level - Instead of filterByFalseyValue on the full expression, only filter by the right operand since the left's narrowing is already in the scope - Reduces O(N^2) work to O(N) for chains of N conditions - New regression test in tests/PHPStan/Analyser/nsrt/bug-14207.php - Updated bug-7156 test assertion for equivalent type description Closes phpstan/phpstan#14207
1 parent 2681e50 commit 64495a4

File tree

3 files changed

+129
-4
lines changed

3 files changed

+129
-4
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3660,15 +3660,16 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
36603660
return $result;
36613661
} elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) {
36623662
$leftResult = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());
3663-
$rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $storage, $nodeCallback, $context);
3663+
$leftFalseyScope = $leftResult->getFalseyScope();
3664+
$rightResult = $this->processExprNode($stmt, $expr->right, $leftFalseyScope, $storage, $nodeCallback, $context);
36643665
$rightExprType = $rightResult->getScope()->getType($expr->right);
36653666
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
36663667
$leftMergedWithRightScope = $leftResult->getTruthyScope();
36673668
} else {
36683669
$leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
36693670
}
36703671

3671-
$this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $storage, $context);
3672+
$this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftFalseyScope), $scope, $storage, $context);
36723673

36733674
return new ExpressionResult(
36743675
$leftMergedWithRightScope,
@@ -3677,7 +3678,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
36773678
array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
36783679
array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
36793680
static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr),
3680-
static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr),
3681+
static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr->right),
36813682
);
36823683
} elseif ($expr instanceof Coalesce) {
36833684
$nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14207;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class WP_HTML_Token
8+
{
9+
public string $namespace;
10+
public string $node_name;
11+
}
12+
13+
class HelloWorld
14+
{
15+
/**
16+
* @param WP_HTML_Token|string $tag_name
17+
*/
18+
public static function is_special( $tag_name ): bool {
19+
if ( is_string( $tag_name ) ) {
20+
$tag_name = strtoupper( $tag_name );
21+
} else {
22+
$tag_name = 'html' === $tag_name->namespace
23+
? strtoupper( $tag_name->node_name )
24+
: "{$tag_name->namespace} {$tag_name->node_name}";
25+
}
26+
27+
assertType('non-falsy-string|uppercase-string', $tag_name);
28+
29+
return (
30+
'ADDRESS' === $tag_name ||
31+
'APPLET' === $tag_name ||
32+
'AREA' === $tag_name ||
33+
'ARTICLE' === $tag_name ||
34+
'ASIDE' === $tag_name ||
35+
'BASE' === $tag_name ||
36+
'BASEFONT' === $tag_name ||
37+
'BGSOUND' === $tag_name ||
38+
'BLOCKQUOTE' === $tag_name ||
39+
'BODY' === $tag_name ||
40+
'BR' === $tag_name ||
41+
'BUTTON' === $tag_name ||
42+
'CAPTION' === $tag_name ||
43+
'CENTER' === $tag_name ||
44+
'COL' === $tag_name ||
45+
'COLGROUP' === $tag_name ||
46+
'DD' === $tag_name ||
47+
'DETAILS' === $tag_name ||
48+
'DIR' === $tag_name ||
49+
'DIV' === $tag_name ||
50+
'DL' === $tag_name ||
51+
'DT' === $tag_name ||
52+
'EMBED' === $tag_name ||
53+
'FIELDSET' === $tag_name ||
54+
'FIGCAPTION' === $tag_name ||
55+
'FIGURE' === $tag_name ||
56+
'FOOTER' === $tag_name ||
57+
'FORM' === $tag_name ||
58+
'FRAME' === $tag_name ||
59+
'FRAMESET' === $tag_name ||
60+
'H1' === $tag_name ||
61+
'H2' === $tag_name ||
62+
'H3' === $tag_name ||
63+
'H4' === $tag_name ||
64+
'H5' === $tag_name ||
65+
'H6' === $tag_name ||
66+
'HEAD' === $tag_name ||
67+
'HEADER' === $tag_name ||
68+
'HGROUP' === $tag_name ||
69+
'HR' === $tag_name ||
70+
'HTML' === $tag_name ||
71+
'IFRAME' === $tag_name ||
72+
'IMG' === $tag_name ||
73+
'INPUT' === $tag_name ||
74+
'KEYGEN' === $tag_name ||
75+
'LI' === $tag_name ||
76+
'LINK' === $tag_name ||
77+
'LISTING' === $tag_name ||
78+
'MAIN' === $tag_name ||
79+
'MARQUEE' === $tag_name ||
80+
'MENU' === $tag_name ||
81+
'META' === $tag_name ||
82+
'NAV' === $tag_name ||
83+
'NOEMBED' === $tag_name ||
84+
'NOFRAMES' === $tag_name ||
85+
'NOSCRIPT' === $tag_name ||
86+
'OBJECT' === $tag_name ||
87+
'OL' === $tag_name ||
88+
'P' === $tag_name ||
89+
'PARAM' === $tag_name ||
90+
'PLAINTEXT' === $tag_name ||
91+
'PRE' === $tag_name ||
92+
'SCRIPT' === $tag_name ||
93+
'SEARCH' === $tag_name ||
94+
'SECTION' === $tag_name ||
95+
'SELECT' === $tag_name ||
96+
'SOURCE' === $tag_name ||
97+
'STYLE' === $tag_name ||
98+
'SUMMARY' === $tag_name ||
99+
'TABLE' === $tag_name ||
100+
'TBODY' === $tag_name ||
101+
'TD' === $tag_name ||
102+
'TEMPLATE' === $tag_name ||
103+
'TEXTAREA' === $tag_name ||
104+
'TFOOT' === $tag_name ||
105+
'TH' === $tag_name ||
106+
'THEAD' === $tag_name ||
107+
'TITLE' === $tag_name ||
108+
'TR' === $tag_name ||
109+
'TRACK' === $tag_name ||
110+
'UL' === $tag_name ||
111+
'WBR' === $tag_name ||
112+
'XMP' === $tag_name ||
113+
'math MI' === $tag_name ||
114+
'math MO' === $tag_name ||
115+
'math MN' === $tag_name ||
116+
'math MS' === $tag_name ||
117+
'math MTEXT' === $tag_name ||
118+
'math ANNOTATION-XML' === $tag_name ||
119+
'svg DESC' === $tag_name ||
120+
'svg FOREIGNOBJECT' === $tag_name ||
121+
'svg TITLE' === $tag_name
122+
);
123+
}
124+
}

tests/PHPStan/Rules/Functions/data/bug-7156.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function foobar2(mixed $data): void
3232
throw new \RuntimeException();
3333
}
3434

35-
assertType("non-empty-array<mixed, mixed>&hasOffsetValue('value', string)", $data);
35+
assertType("non-empty-array&hasOffsetValue('value', string)", $data);
3636

3737
foo($data);
3838
}

0 commit comments

Comments
 (0)