Repository navigation
Expand file tree
/
Copy pathDoWhileHandler.php
More file actions
215 lines (198 loc) · 8.96 KB
/
Copy pathDoWhileHandler.php
File metadata and controls
215 lines (198 loc) · 8.96 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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?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\Dependency\Dependencies;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\DoWhileLoopConditionNode;
use PHPStan\Turbo\ShadowedByTurboExtension;
use function array_merge;
use function count;
/**
* @implements StmtHandler<Do_>
*/
#[AutowiredService]
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../../turbo-ext/src/DoWhileHandler.cpp')]
final class DoWhileHandler implements StmtHandler
{
public function __construct(
#[AutowiredParameter]
private bool $treatPhpDocTypesAsCertain,
)
{
}
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;
$previousPass = null;
$previousPassStorage = null;
if ($context->shouldConvergeLoops()) {
$bodyIsReplayable = $nodeScopeResolver->isReplayableConvergenceBody($stmt, $stmt->stmts);
// each pass after the first re-walks only the statements whose
// entry state differs from the previous pass's (StatementsHandler::processLoopPassStmtNodes())
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();
if ($previousPassStorage !== null) {
// the statements this pass replays answer from the results the previous pass stored
$storage->mergeResults($previousPassStorage);
}
$bodyRecording = new RecordingNodeCallback();
$scope->pushExpressionResultStorage($storage);
try {
$passRecord = null;
$bodyScopeResult = $nodeScopeResolver->processLoopPassStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterLoopPass()->withoutTemplateArgumentResolution(), $previousPass, $passRecord)->filterOutLoopExitPoints();
$previousPass = $passRecord;
$previousPassStorage = $storage;
$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 ($bodyIsReplayable) {
$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;
} elseif ($previousPass !== null && $previousPass[1] !== null) {
// the last pass stands in for the statements whose state it walked
// with - they replay through the real callback
$originalStorage->mergeResults($previousPassStorage);
$bodyScopeResult = $nodeScopeResolver->processLoopFinalStmtNodes($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context, $previousPass)->filterOutLoopExitPoints();
} 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 = ($this->treatPhpDocTypesAsCertain ? $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()),
// the body runs once, then may repeat or not whatever the condition says
variableFlow: VariableFlow::loop(null, $bodyScopeResult->getVariableFlow(), $condResult->getVariableFlow(), true, true),
dependencies: Dependencies::merge($bodyScopeResult->getDependencies(), $condResult->getDependencies()),
);
}
}