Repository navigation
Expand file tree
/
Copy pathDoWhileHandler.php
More file actions
186 lines (170 loc) · 7.66 KB
/
Copy pathDoWhileHandler.php
File metadata and controls
186 lines (170 loc) · 7.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
<?php declare(strict_types = 1);
namespace PHPStan\Analyser\StmtHandler;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Do_;
use PHPStan\Analyser\ExpressionContext;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\InternalStatementResult;
use PHPStan\Analyser\LoopWrittenVariableNames;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\NoopNodeCallback;
use PHPStan\Analyser\RecordingNodeCallback;
use PHPStan\Analyser\StatementContext;
use PHPStan\Analyser\StmtHandler;
use PHPStan\Analyser\VariableFlow;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\DoWhileLoopConditionNode;
use function array_merge;
use function count;
/**
* @implements StmtHandler<Do_>
*/
#[AutowiredService]
final class DoWhileHandler implements StmtHandler
{
public function supports(Stmt $stmt): bool
{
return $stmt instanceof Do_;
}
public function processStmt(
NodeScopeResolver $nodeScopeResolver,
Stmt $stmt,
MutatingScope $scope,
ExpressionResultStorage $storage,
callable $nodeCallback,
StatementContext $context,
): InternalStatementResult
{
$finalScope = null;
$bodyScope = $scope;
$count = 0;
$hasYield = false;
$throwPoints = [];
$impurePoints = [];
$originalStorage = $storage;
$replayBodyRecording = null;
$replayPassStorage = null;
$replayPassResult = null;
$prevEntryScope = null;
if ($context->isTopLevel()) {
$bodyIsReplayable = $nodeScopeResolver->isReplayableConvergenceBody($stmt, $stmt->stmts);
do {
$prevScope = $bodyScope;
$bodyScope = $bodyScope->mergeWith($scope);
if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) {
// walking is deterministic in the entry scope - an unchanged entry
// reproduces the previous pass's exit (and repeats only idempotent
// merges into the final scope), so the verification walk is skipped
$bodyScope = $prevScope;
break;
}
$prevEntryScope = $bodyScope;
$storage = $originalStorage->duplicate();
$bodyRecording = $bodyIsReplayable ? new RecordingNodeCallback() : new NoopNodeCallback();
$scope->pushExpressionResultStorage($storage);
try {
$bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints();
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
$finalScope = $backEdgeScope === null ? $finalScope : $backEdgeScope->mergeWith($finalScope);
foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
$finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
}
// the candidate to replace the final body walk when this pass's
// entry turns out to be the fixpoint
if ($bodyRecording instanceof RecordingNodeCallback) {
$replayBodyRecording = $bodyRecording;
$replayPassStorage = $storage;
$replayPassResult = $bodyScopeResult;
}
if ($backEdgeScope === null) {
$bodyScope = $prevScope;
break;
}
$passCondResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $backEdgeScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false));
$bodyScope = $passCondResult->getTruthyScope();
} finally {
$scope->popExpressionResultStorage();
}
if ($bodyScope->equals($prevScope)) {
break;
}
if ($count >= NodeScopeResolver::GENERALIZE_AFTER_ITERATION) {
$bodyScope = $prevScope->generalizeWith(
$bodyScope,
LoopWrittenVariableNames::collect($stmt, VariableFlow::sequence($bodyScopeResult->getVariableFlow(), $passCondResult->getVariableFlow())),
);
}
$count++;
} while ($count < NodeScopeResolver::LOOP_SCOPE_ITERATIONS);
$bodyScope = $bodyScope->mergeWith($scope);
}
$storage = $originalStorage;
if (
$replayBodyRecording !== null && $replayPassStorage !== null && $replayPassResult !== null
&& $bodyScope->equals($prevEntryScope)
) {
// the final body walk would repeat the recorded fixpoint pass exactly
// (same entry scope, deterministic walk) - adopt the pass's results
// and replay its emissions through the real callback instead; the
// condition walks below stay real
$originalStorage->mergeResults($replayPassStorage);
$nodeScopeResolver->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage, $scope);
$bodyScopeResult = $replayPassResult;
} else {
$bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
}
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
$backEdgeDead = $backEdgeScope === null;
$bodyScope = $backEdgeScope ?? $bodyScopeResult->getScope();
// the condition is processed once on the post-body scope; its result
// answers both the always-iterates check below and the falsey post-loop
// scope - the previous scope-based read here was a guaranteed storage
// miss (the condition was only ever stored into discarded convergence
// duplicates) that re-priced the condition on demand before this walk
$condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments()));
$alwaysIterates = false;
if ($context->isTopLevel()) {
$condBooleanType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean();
$alwaysIterates = $condBooleanType->isTrue()->yes();
}
if ($alwaysIterates || $backEdgeDead) {
$alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
} else {
$alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
}
$finalScope = $alwaysTerminating || $backEdgeDead ? $finalScope : $bodyScope->mergeWith($finalScope);
if ($finalScope === null) {
$finalScope = $scope;
}
if (!$alwaysTerminating && !$backEdgeDead) {
$hasYield = $condResult->hasYield();
$throwPoints = $condResult->getThrowPoints();
$impurePoints = $condResult->getImpurePoints();
$finalScope = $condResult->getFalseyScope();
}
// both emissions fire after the condition's final walk stored its
// results, so rule-side asks about the condition answer from the
// storage; the Do_ callback is deferred from processStmtNode()
$nodeScopeResolver->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints(), $bodyScopeResult->hasYield()), $bodyScope, $storage);
$nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $scope, $storage);
$breakExitPoints = $bodyScopeResult->getExitPointsByType(Break_::class);
if (count($breakExitPoints) > 0) {
$breakScope = $alwaysIterates || $backEdgeDead ? null : $finalScope;
foreach ($breakExitPoints as $breakExitPoint) {
$breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
}
$finalScope = $breakScope;
}
return new InternalStatementResult(
$finalScope,
hasYield: $bodyScopeResult->hasYield() || $hasYield,
isAlwaysTerminating: $alwaysTerminating,
exitPoints: $bodyScopeResult->getExitPointsForOuterLoop(),
throwPoints: array_merge($throwPoints, $bodyScopeResult->getThrowPoints()),
impurePoints: array_merge($impurePoints, $bodyScopeResult->getImpurePoints()),
variableFlow: VariableFlow::loop(null, $bodyScopeResult->getVariableFlow(), $condResult->getVariableFlow(), true, !$alwaysIterates, !$condResult->getType()->toBoolean()->isFalse()->yes()),
);
}
}