Skip to content

Commit 72b4de4

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 2d66c4e commit 72b4de4

File tree

4 files changed

+164
-4
lines changed

4 files changed

+164
-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);

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,12 @@ public function testBug13980(): void
16321632
$this->assertNoErrors($errors);
16331633
}
16341634

1635+
public function testBug14207(): void
1636+
{
1637+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-14207.php');
1638+
$this->assertNoErrors($errors);
1639+
}
1640+
16351641
public function testBug13945(): void
16361642
{
16371643
$errors = $this->runAnalyse(__DIR__ . '/data/bug-13945.php');
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14207;
4+
5+
use function assert;
6+
use function PHPStan\Testing\assertType;
7+
8+
class WP_HTML_Token {
9+
public string $namespace;
10+
public string $node_name;
11+
}
12+
13+
class HelloWorld
14+
{
15+
/**
16+
* Returns whether an element of a given name is in the HTML special category.
17+
*
18+
* @since 6.4.0
19+
*
20+
* @see https://html.spec.whatwg.org/#special
21+
*
22+
* @param WP_HTML_Token|string $tag_name Node to check, or only its name if in the HTML namespace.
23+
* @return bool Whether the element of the given name is in the special category.
24+
*/
25+
public static function is_special( $tag_name ): bool {
26+
if ( is_string( $tag_name ) ) {
27+
$tag_name = strtoupper( $tag_name );
28+
} else {
29+
$tag_name = 'html' === $tag_name->namespace
30+
? strtoupper( $tag_name->node_name )
31+
: "{$tag_name->namespace} {$tag_name->node_name}";
32+
}
33+
34+
assertType('non-falsy-string|uppercase-string', $tag_name);
35+
36+
$x = (
37+
'ADDRESS' === $tag_name ||
38+
'APPLET' === $tag_name ||
39+
'AREA' === $tag_name ||
40+
'ARTICLE' === $tag_name ||
41+
'ASIDE' === $tag_name ||
42+
'BASE' === $tag_name ||
43+
'BASEFONT' === $tag_name ||
44+
'BGSOUND' === $tag_name ||
45+
'BLOCKQUOTE' === $tag_name ||
46+
'BODY' === $tag_name ||
47+
'BR' === $tag_name ||
48+
'BUTTON' === $tag_name ||
49+
'CAPTION' === $tag_name ||
50+
'CENTER' === $tag_name ||
51+
'COL' === $tag_name ||
52+
'COLGROUP' === $tag_name ||
53+
'DD' === $tag_name ||
54+
'DETAILS' === $tag_name ||
55+
'DIR' === $tag_name ||
56+
'DIV' === $tag_name ||
57+
'DL' === $tag_name ||
58+
'DT' === $tag_name ||
59+
'EMBED' === $tag_name ||
60+
'FIELDSET' === $tag_name ||
61+
'FIGCAPTION' === $tag_name ||
62+
'FIGURE' === $tag_name ||
63+
'FOOTER' === $tag_name ||
64+
'FORM' === $tag_name ||
65+
'FRAME' === $tag_name ||
66+
'FRAMESET' === $tag_name ||
67+
'H1' === $tag_name ||
68+
'H2' === $tag_name ||
69+
'H3' === $tag_name ||
70+
'H4' === $tag_name ||
71+
'H5' === $tag_name ||
72+
'H6' === $tag_name ||
73+
'HEAD' === $tag_name ||
74+
'HEADER' === $tag_name ||
75+
'HGROUP' === $tag_name ||
76+
'HR' === $tag_name ||
77+
'HTML' === $tag_name ||
78+
'IFRAME' === $tag_name ||
79+
'IMG' === $tag_name ||
80+
'INPUT' === $tag_name ||
81+
'KEYGEN' === $tag_name ||
82+
'LI' === $tag_name ||
83+
'LINK' === $tag_name ||
84+
'LISTING' === $tag_name ||
85+
'MAIN' === $tag_name ||
86+
'MARQUEE' === $tag_name ||
87+
'MENU' === $tag_name ||
88+
'META' === $tag_name ||
89+
'NAV' === $tag_name ||
90+
'NOEMBED' === $tag_name ||
91+
'NOFRAMES' === $tag_name ||
92+
'NOSCRIPT' === $tag_name ||
93+
'OBJECT' === $tag_name ||
94+
'OL' === $tag_name ||
95+
'P' === $tag_name ||
96+
'PARAM' === $tag_name ||
97+
'PLAINTEXT' === $tag_name ||
98+
'PRE' === $tag_name ||
99+
'SCRIPT' === $tag_name ||
100+
'SEARCH' === $tag_name ||
101+
'SECTION' === $tag_name ||
102+
'SELECT' === $tag_name ||
103+
'SOURCE' === $tag_name ||
104+
'STYLE' === $tag_name ||
105+
'SUMMARY' === $tag_name ||
106+
'TABLE' === $tag_name ||
107+
'TBODY' === $tag_name ||
108+
'TD' === $tag_name ||
109+
'TEMPLATE' === $tag_name ||
110+
'TEXTAREA' === $tag_name ||
111+
'TFOOT' === $tag_name ||
112+
'TH' === $tag_name ||
113+
'THEAD' === $tag_name ||
114+
'TITLE' === $tag_name ||
115+
'TR' === $tag_name ||
116+
'TRACK' === $tag_name ||
117+
'UL' === $tag_name ||
118+
'WBR' === $tag_name ||
119+
'XMP' === $tag_name ||
120+
121+
// MathML.
122+
'math MI' === $tag_name ||
123+
'math MO' === $tag_name ||
124+
'math MN' === $tag_name ||
125+
'math MS' === $tag_name ||
126+
'math MTEXT' === $tag_name ||
127+
'math ANNOTATION-XML' === $tag_name ||
128+
129+
// SVG.
130+
'svg DESC' === $tag_name ||
131+
'svg FOREIGNOBJECT' === $tag_name ||
132+
'svg TITLE' === $tag_name ||
133+
134+
// some random stuff
135+
'a1' === $tag_name ||
136+
'a2' === $tag_name ||
137+
'a3' === $tag_name ||
138+
'a4' === $tag_name ||
139+
'a5' === $tag_name ||
140+
'a6' === $tag_name ||
141+
'a7' === $tag_name ||
142+
'a8' === $tag_name ||
143+
'a9' === $tag_name
144+
);
145+
146+
assertType('bool', $x);
147+
if ($x) {
148+
assertType("'a1'|'a2'|'a3'|'a4'|'a5'|'a6'|'a7'|'a8'|'a9'|'ADDRESS'|'APPLET'|'AREA'|'ARTICLE'|'ASIDE'|'BASE'|'BASEFONT'|'BGSOUND'|'BLOCKQUOTE'|'BODY'|'BR'|'BUTTON'|'CAPTION'|'CENTER'|'COL'|'COLGROUP'|'DD'|'DETAILS'|'DIR'|'DIV'|'DL'|'DT'|'EMBED'|'FIELDSET'|'FIGCAPTION'|'FIGURE'|'FOOTER'|'FORM'|'FRAME'|'FRAMESET'|'H1'|'H2'|'H3'|'H4'|'H5'|'H6'|'HEAD'|'HEADER'|'HGROUP'|'HR'|'HTML'|'IFRAME'|'IMG'|'INPUT'|'KEYGEN'|'LI'|'LINK'|'LISTING'|'MAIN'|'MARQUEE'|'math ANNOTATION-XML'|'math MI'|'math MN'|'math MO'|'math MS'|'math MTEXT'|'MENU'|'META'|'NAV'|'NOEMBED'|'NOFRAMES'|'NOSCRIPT'|'OBJECT'|'OL'|'P'|'PARAM'|'PLAINTEXT'|'PRE'|'SCRIPT'|'SEARCH'|'SECTION'|'SELECT'|'SOURCE'|'STYLE'|'SUMMARY'|'svg DESC'|'svg FOREIGNOBJECT'|'svg TITLE'|'TABLE'|'TBODY'|'TD'|'TEMPLATE'|'TEXTAREA'|'TFOOT'|'TH'|'THEAD'|'TITLE'|'TR'|'TRACK'|'UL'|'WBR'|'XMP'", $tag_name);
149+
}
150+
151+
return $x;
152+
}
153+
}

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)