LoopAnalyzer::analyze()   F
last analyzed

Complexity

Conditions 92
Paths > 20000

Size

Total Lines 558

Duplication

Lines 71
Ratio 12.72 %

Importance

Changes 0
Metric Value
cc 92
nc 243760
nop 8
dl 71
loc 558
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Block;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\ScopeAnalyzer;
6
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
7
use Psalm\Internal\Analyzer\StatementsAnalyzer;
8
use Psalm\Internal\Clause;
9
use Psalm\CodeLocation;
10
use Psalm\Config;
11
use Psalm\Context;
12
use Psalm\IssueBuffer;
13
use Psalm\Internal\Scope\LoopScope;
14
use Psalm\Type;
15
use Psalm\Type\Algebra;
16
use Psalm\Type\Reconciler;
17
use function array_merge;
18
use function array_keys;
19
use function array_unique;
20
use function array_intersect_key;
21
use function in_array;
22
23
/**
24
 * @internal
25
 */
26
class LoopAnalyzer
27
{
28
    /**
29
     * Checks an array of statements in a loop
30
     *
31
     * @param  array<PhpParser\Node\Stmt>   $stmts
32
     * @param  PhpParser\Node\Expr[]        $pre_conditions
33
     * @param  PhpParser\Node\Expr[]        $post_expressions
34
     * @param  Context                      loop_scope->loop_context
35
     * @param  Context                      $loop_scope->loop_parent_context
0 ignored issues
show
Documentation introduced by
There is no parameter named $loop_scope->loop_parent_context. Did you maybe mean $loop_scope?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
36
     * @param  bool                         $is_do
37
     *
38
     * @return false|null
39
     */
40
    public static function analyze(
41
        StatementsAnalyzer $statements_analyzer,
42
        array $stmts,
43
        array $pre_conditions,
44
        array $post_expressions,
45
        LoopScope $loop_scope,
46
        Context &$inner_context = null,
47
        bool $is_do = false,
48
        bool $always_enters_loop = false
49
    ) {
50
        $traverser = new PhpParser\NodeTraverser;
51
52
        $assignment_mapper = new \Psalm\Internal\PhpVisitor\AssignmentMapVisitor($loop_scope->loop_context->self);
53
        $traverser->addVisitor($assignment_mapper);
54
55
        $traverser->traverse(array_merge($stmts, $post_expressions));
56
57
        $assignment_map = $assignment_mapper->getAssignmentMap();
58
59
        $assignment_depth = 0;
60
61
        $asserted_var_ids = [];
62
63
        $pre_condition_clauses = [];
64
65
        $original_protected_var_ids = $loop_scope->loop_parent_context->protected_var_ids;
66
67
        $codebase = $statements_analyzer->getCodebase();
68
69
        $inner_do_context = null;
70
71
        if ($pre_conditions) {
72
            foreach ($pre_conditions as $i => $pre_condition) {
73
                $pre_condition_clauses[$i] = Algebra::getFormula(
74
                    \spl_object_id($pre_condition),
75
                    $pre_condition,
76
                    $loop_scope->loop_context->self,
77
                    $statements_analyzer,
78
                    $codebase
79
                );
80
            }
81
        } else {
82
            $asserted_var_ids = Context::getNewOrUpdatedVarIds(
83
                $loop_scope->loop_parent_context,
84
                $loop_scope->loop_context
85
            );
86
        }
87
88
        $final_actions = ScopeAnalyzer::getFinalControlActions(
89
            $stmts,
90
            $statements_analyzer->node_data,
91
            Config::getInstance()->exit_functions,
92
            $loop_scope->loop_context->break_types
93
        );
94
95
        $does_always_break = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
96
97
        if ($assignment_map) {
98
            $first_var_id = array_keys($assignment_map)[0];
99
100
            $assignment_depth = self::getAssignmentMapDepth($first_var_id, $assignment_map);
101
        }
102
103
        $loop_scope->loop_context->parent_context = $loop_scope->loop_parent_context;
104
105
        $pre_outer_context = $loop_scope->loop_parent_context;
106
107
        if ($assignment_depth === 0 || $does_always_break) {
108
            $inner_context = clone $loop_scope->loop_context;
109
110
            foreach ($inner_context->vars_in_scope as $context_var_id => $context_type) {
111
                $inner_context->vars_in_scope[$context_var_id] = clone $context_type;
112
            }
113
114
            $inner_context->loop_scope = $loop_scope;
115
116
            $inner_context->parent_context = $loop_scope->loop_context;
117
            $old_referenced_var_ids = $inner_context->referenced_var_ids;
118
            $inner_context->referenced_var_ids = [];
119
120
            foreach ($pre_conditions as $condition_offset => $pre_condition) {
121
                self::applyPreConditionToLoopContext(
122
                    $statements_analyzer,
123
                    $pre_condition,
124
                    $pre_condition_clauses[$condition_offset],
125
                    $inner_context,
126
                    $loop_scope->loop_parent_context,
127
                    $is_do
128
                );
129
            }
130
131
            $inner_context->protected_var_ids = $loop_scope->protected_var_ids;
132
133
            $statements_analyzer->analyze($stmts, $inner_context);
134
            self::updateLoopScopeContexts($loop_scope, $loop_scope->loop_parent_context);
135
136
            foreach ($post_expressions as $post_expression) {
137
                if (ExpressionAnalyzer::analyze(
138
                    $statements_analyzer,
139
                    $post_expression,
140
                    $loop_scope->loop_context
141
                ) === false
142
                ) {
143
                    return false;
144
                }
145
            }
146
147
            $new_referenced_var_ids = $inner_context->referenced_var_ids;
148
            $inner_context->referenced_var_ids = $old_referenced_var_ids + $inner_context->referenced_var_ids;
149
150
            $loop_scope->loop_parent_context->vars_possibly_in_scope = array_merge(
151
                $inner_context->vars_possibly_in_scope,
152
                $loop_scope->loop_parent_context->vars_possibly_in_scope
153
            );
154
        } else {
155
            $pre_outer_context = clone $loop_scope->loop_parent_context;
156
157
            $analyzer = $statements_analyzer->getCodebase()->analyzer;
158
159
            $original_mixed_counts = $analyzer->getMixedCountsForFile($statements_analyzer->getFilePath());
160
161
            $pre_condition_vars_in_scope = $loop_scope->loop_context->vars_in_scope;
162
163
            IssueBuffer::startRecording();
164
165 View Code Duplication
            if (!$is_do) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
166
                foreach ($pre_conditions as $condition_offset => $pre_condition) {
167
                    $asserted_var_ids = array_merge(
168
                        self::applyPreConditionToLoopContext(
169
                            $statements_analyzer,
170
                            $pre_condition,
171
                            $pre_condition_clauses[$condition_offset],
172
                            $loop_scope->loop_context,
173
                            $loop_scope->loop_parent_context,
174
                            $is_do
175
                        ),
176
                        $asserted_var_ids
177
                    );
178
                }
179
            }
180
181
            // record all the vars that existed before we did the first pass through the loop
182
            $pre_loop_context = clone $loop_scope->loop_context;
183
184
            $inner_context = clone $loop_scope->loop_context;
185
186
            foreach ($inner_context->vars_in_scope as $context_var_id => $context_type) {
187
                $inner_context->vars_in_scope[$context_var_id] = clone $context_type;
188
            }
189
190
            $inner_context->parent_context = $loop_scope->loop_context;
191
            $inner_context->loop_scope = $loop_scope;
192
193
            $old_referenced_var_ids = $inner_context->referenced_var_ids;
194
            $inner_context->referenced_var_ids = [];
195
196
            $inner_context->protected_var_ids = $loop_scope->protected_var_ids;
197
198
            $statements_analyzer->analyze($stmts, $inner_context);
199
200
            self::updateLoopScopeContexts($loop_scope, $pre_outer_context);
201
202
            $inner_context->protected_var_ids = $original_protected_var_ids;
203
204 View Code Duplication
            if ($is_do) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
205
                $inner_do_context = clone $inner_context;
206
207
                foreach ($pre_conditions as $condition_offset => $pre_condition) {
208
                    $asserted_var_ids = array_merge(
209
                        self::applyPreConditionToLoopContext(
210
                            $statements_analyzer,
211
                            $pre_condition,
212
                            $pre_condition_clauses[$condition_offset],
213
                            $inner_context,
214
                            $loop_scope->loop_parent_context,
215
                            $is_do
216
                        ),
217
                        $asserted_var_ids
218
                    );
219
                }
220
            }
221
222
            $asserted_var_ids = array_unique($asserted_var_ids);
223
224
            foreach ($post_expressions as $post_expression) {
225
                if (ExpressionAnalyzer::analyze($statements_analyzer, $post_expression, $inner_context) === false) {
226
                    return false;
227
                }
228
            }
229
230
            /**
231
             * @var array<string, bool>
232
             */
233
            $new_referenced_var_ids = $inner_context->referenced_var_ids;
234
            $inner_context->referenced_var_ids = array_intersect_key(
235
                $old_referenced_var_ids,
236
                $inner_context->referenced_var_ids
237
            );
238
239
            $recorded_issues = IssueBuffer::clearRecordingLevel();
240
            IssueBuffer::stopRecording();
241
242
            for ($i = 0; $i < $assignment_depth; ++$i) {
243
                $vars_to_remove = [];
244
245
                $loop_scope->iteration_count++;
246
247
                $has_changes = false;
248
249
                // reset the $inner_context to what it was before we started the analysis,
250
                // but union the types with what's in the loop scope
251
252
                foreach ($inner_context->vars_in_scope as $var_id => $type) {
253
                    if (in_array($var_id, $asserted_var_ids, true)) {
254
                        // set the vars to whatever the while/foreach loop expects them to be
255
                        if (!isset($pre_loop_context->vars_in_scope[$var_id])
256
                            || !$type->equals($pre_loop_context->vars_in_scope[$var_id])
257
                        ) {
258
                            $has_changes = true;
259
                        }
260
                    } elseif (isset($pre_outer_context->vars_in_scope[$var_id])) {
261
                        if (!$type->equals($pre_outer_context->vars_in_scope[$var_id])) {
262
                            $has_changes = true;
263
264
                            // widen the foreach context type with the initial context type
265
                            $inner_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
266
                                $inner_context->vars_in_scope[$var_id],
267
                                $pre_outer_context->vars_in_scope[$var_id]
268
                            );
269
270
                            // if there's a change, invalidate related clauses
271
                            $pre_loop_context->removeVarFromConflictingClauses($var_id);
272
                        }
273
274
                        if (isset($loop_scope->loop_context->vars_in_scope[$var_id])
275
                            && !$type->equals($loop_scope->loop_context->vars_in_scope[$var_id])
276
                        ) {
277
                            $has_changes = true;
278
279
                            // widen the foreach context type with the initial context type
280
                            $inner_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
281
                                $inner_context->vars_in_scope[$var_id],
282
                                $loop_scope->loop_context->vars_in_scope[$var_id]
283
                            );
284
285
                            // if there's a change, invalidate related clauses
286
                            $pre_loop_context->removeVarFromConflictingClauses($var_id);
287
                        }
288
                    } else {
289
                        // give an opportunity to redeemed UndefinedVariable issues
290
                        if ($recorded_issues) {
291
                            $has_changes = true;
292
                        }
293
294
                        // if we're in a do block we don't want to remove vars before evaluating
295
                        // the where conditional
296
                        if (!$is_do) {
297
                            $vars_to_remove[] = $var_id;
298
                        }
299
                    }
300
                }
301
302
                $inner_context->has_returned = false;
303
304
                if ($codebase->find_unused_variables) {
305
                    foreach ($inner_context->unreferenced_vars as $var_id => $locations) {
306
                        if (!isset($pre_outer_context->vars_in_scope[$var_id])) {
307
                            $loop_scope->unreferenced_vars[$var_id] = $locations;
308
                            unset($inner_context->unreferenced_vars[$var_id]);
309
                        }
310
                    }
311
                }
312
313
                $loop_scope->loop_parent_context->vars_possibly_in_scope = array_merge(
314
                    $inner_context->vars_possibly_in_scope,
315
                    $loop_scope->loop_parent_context->vars_possibly_in_scope
316
                );
317
318
                // if there are no changes to the types, no need to re-examine
319
                if (!$has_changes) {
320
                    break;
321
                }
322
323
                if ($codebase->find_unused_variables) {
324
                    foreach ($loop_scope->possibly_unreferenced_vars as $var_id => $locations) {
325
                        if (isset($inner_context->unreferenced_vars[$var_id])) {
326
                            $inner_context->unreferenced_vars[$var_id] += $locations;
327
                        } else {
328
                            $inner_context->unreferenced_vars[$var_id] = $locations;
329
                        }
330
                    }
331
                }
332
333
                // remove vars that were defined in the foreach
334
                foreach ($vars_to_remove as $var_id) {
335
                    unset($inner_context->vars_in_scope[$var_id]);
336
                }
337
338
                $inner_context->clauses = $pre_loop_context->clauses;
339
340
                $analyzer->setMixedCountsForFile($statements_analyzer->getFilePath(), $original_mixed_counts);
341
                IssueBuffer::startRecording();
342
343
                foreach ($pre_loop_context->vars_in_scope as $var_id => $_) {
344
                    if (!isset($pre_condition_vars_in_scope[$var_id])
345
                        && isset($inner_context->vars_in_scope[$var_id])
346
                        && \strpos($var_id, '->') === false
347
                        && \strpos($var_id, '[') === false
348
                    ) {
349
                        $inner_context->vars_in_scope[$var_id]->possibly_undefined = true;
350
                    }
351
                }
352
353
                if (!$is_do) {
354
                    foreach ($pre_conditions as $condition_offset => $pre_condition) {
355
                        self::applyPreConditionToLoopContext(
356
                            $statements_analyzer,
357
                            $pre_condition,
358
                            $pre_condition_clauses[$condition_offset],
359
                            $inner_context,
360
                            $loop_scope->loop_parent_context,
361
                            false
362
                        );
363
                    }
364
                }
365
366
                foreach ($asserted_var_ids as $var_id) {
367
                    if (!isset($inner_context->vars_in_scope[$var_id])
368
                        || $inner_context->vars_in_scope[$var_id]->getId()
369
                            !== $pre_loop_context->vars_in_scope[$var_id]->getId()
370
                        || $inner_context->vars_in_scope[$var_id]->from_docblock
371
                            !== $pre_loop_context->vars_in_scope[$var_id]->from_docblock
372
                    ) {
373
                        $inner_context->vars_in_scope[$var_id] = clone $pre_loop_context->vars_in_scope[$var_id];
374
                    }
375
                }
376
377
                $inner_context->clauses = $pre_loop_context->clauses;
378
379
                $inner_context->protected_var_ids = $loop_scope->protected_var_ids;
380
381
                $traverser = new PhpParser\NodeTraverser;
382
383
                $traverser->addVisitor(
384
                    new \Psalm\Internal\PhpVisitor\NodeCleanerVisitor(
385
                        $statements_analyzer->node_data
386
                    )
387
                );
388
                $traverser->traverse($stmts);
389
390
                $statements_analyzer->analyze($stmts, $inner_context);
391
392
                self::updateLoopScopeContexts($loop_scope, $pre_outer_context);
393
394
                $inner_context->protected_var_ids = $original_protected_var_ids;
395
396 View Code Duplication
                if ($is_do) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
397
                    $inner_do_context = clone $inner_context;
398
399
                    foreach ($pre_conditions as $condition_offset => $pre_condition) {
400
                        self::applyPreConditionToLoopContext(
401
                            $statements_analyzer,
402
                            $pre_condition,
403
                            $pre_condition_clauses[$condition_offset],
404
                            $inner_context,
405
                            $loop_scope->loop_parent_context,
406
                            $is_do
407
                        );
408
                    }
409
                }
410
411
                foreach ($post_expressions as $post_expression) {
412
                    if (ExpressionAnalyzer::analyze($statements_analyzer, $post_expression, $inner_context) === false) {
413
                        return false;
414
                    }
415
                }
416
417
                $recorded_issues = IssueBuffer::clearRecordingLevel();
418
419
                IssueBuffer::stopRecording();
420
            }
421
422
            if ($recorded_issues) {
423
                foreach ($recorded_issues as $recorded_issue) {
424
                    // if we're not in any loops then this will just result in the issue being emitted
425
                    IssueBuffer::bubbleUp($recorded_issue);
426
                }
427
            }
428
        }
429
430
        $does_sometimes_break = in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true);
431
        $does_always_break = $loop_scope->final_actions === [ScopeAnalyzer::ACTION_BREAK];
432
433
        if ($does_sometimes_break) {
434
            if ($loop_scope->possibly_redefined_loop_parent_vars !== null) {
435
                foreach ($loop_scope->possibly_redefined_loop_parent_vars as $var => $type) {
436
                    $loop_scope->loop_parent_context->vars_in_scope[$var] = Type::combineUnionTypes(
437
                        $type,
438
                        $loop_scope->loop_parent_context->vars_in_scope[$var]
439
                    );
440
                }
441
            }
442
        }
443
444
        foreach ($loop_scope->loop_parent_context->vars_in_scope as $var_id => $type) {
445
            if (!isset($loop_scope->loop_context->vars_in_scope[$var_id])) {
446
                continue;
447
            }
448
449 View Code Duplication
            if ($loop_scope->loop_context->vars_in_scope[$var_id]->getId() !== $type->getId()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
450
                $loop_scope->loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
451
                    $loop_scope->loop_parent_context->vars_in_scope[$var_id],
452
                    $loop_scope->loop_context->vars_in_scope[$var_id]
453
                );
454
455
                $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id);
456
            }
457
        }
458
459
        if (!$does_always_break) {
460
            foreach ($loop_scope->loop_parent_context->vars_in_scope as $var_id => $type) {
461
                if (!isset($inner_context->vars_in_scope[$var_id])) {
462
                    unset($loop_scope->loop_parent_context->vars_in_scope[$var_id]);
463
                    continue;
464
                }
465
466
                if ($inner_context->vars_in_scope[$var_id]->hasMixed()) {
467
                    $loop_scope->loop_parent_context->vars_in_scope[$var_id] =
468
                        $inner_context->vars_in_scope[$var_id];
469
                    $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id);
470
                    continue;
471
                }
472
473 View Code Duplication
                if ($inner_context->vars_in_scope[$var_id]->getId() !== $type->getId()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
474
                    $loop_scope->loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
475
                        $loop_scope->loop_parent_context->vars_in_scope[$var_id],
476
                        $inner_context->vars_in_scope[$var_id]
477
                    );
478
479
                    $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id);
480
                }
481
            }
482
        }
483
484
        if ($pre_conditions && $pre_condition_clauses && !ScopeAnalyzer::doesEverBreak($stmts)) {
485
            // if the loop contains an assertion and there are no break statements, we can negate that assertion
486
            // and apply it to the current context
487
488
            try {
489
                $negated_pre_condition_clauses = Algebra::negateFormula(array_merge(...$pre_condition_clauses));
490
            } catch (\Psalm\Exception\ComplicatedExpressionException $e) {
491
                $negated_pre_condition_clauses = [];
492
            }
493
494
            $negated_pre_condition_types = Algebra::getTruthsFromFormula($negated_pre_condition_clauses);
495
496
            if ($negated_pre_condition_types) {
497
                $changed_var_ids = [];
498
499
                $vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
500
                    $negated_pre_condition_types,
501
                    [],
502
                    $inner_context->vars_in_scope,
503
                    $changed_var_ids,
504
                    [],
505
                    $statements_analyzer,
506
                    [],
507
                    true,
508
                    new CodeLocation($statements_analyzer->getSource(), $pre_conditions[0])
509
                );
510
511
                foreach ($changed_var_ids as $var_id => $_) {
512
                    if (isset($vars_in_scope_reconciled[$var_id])
513
                        && isset($loop_scope->loop_parent_context->vars_in_scope[$var_id])
514
                    ) {
515
                        $loop_scope->loop_parent_context->vars_in_scope[$var_id] = $vars_in_scope_reconciled[$var_id];
516
                    }
517
518
                    $loop_scope->loop_parent_context->removeVarFromConflictingClauses($var_id);
519
                }
520
            }
521
        }
522
523
        $loop_scope->loop_context->referenced_var_ids = array_merge(
524
            array_intersect_key(
525
                $inner_context->referenced_var_ids,
526
                $pre_outer_context->vars_in_scope
527
            ),
528
            $loop_scope->loop_context->referenced_var_ids
529
        );
530
531
        if ($codebase->find_unused_variables) {
532
            foreach ($loop_scope->possibly_unreferenced_vars as $var_id => $locations) {
533
                if (isset($inner_context->unreferenced_vars[$var_id])) {
534
                    $inner_context->unreferenced_vars[$var_id] += $locations;
535
                }
536
            }
537
538
            foreach ($inner_context->unreferenced_vars as $var_id => $locations) {
539
                if (!isset($new_referenced_var_ids[$var_id])
540
                    || !isset($pre_outer_context->vars_in_scope[$var_id])
541
                    || $does_always_break
542
                ) {
543
                    if (!isset($loop_scope->loop_context->unreferenced_vars[$var_id])) {
544
                        $loop_scope->loop_context->unreferenced_vars[$var_id] = $locations;
545
                    } else {
546
                        $loop_scope->loop_context->unreferenced_vars[$var_id] += $locations;
547
                    }
548
                } else {
549
                    $statements_analyzer->registerVariableUses($locations);
550
                }
551
            }
552
553
            foreach ($loop_scope->unreferenced_vars as $var_id => $locations) {
554 View Code Duplication
                if (isset($loop_scope->referenced_var_ids[$var_id])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
555
                    $statements_analyzer->registerVariableUses($locations);
556
                } else {
557
                    if (!isset($loop_scope->loop_context->unreferenced_vars[$var_id])) {
558
                        $loop_scope->loop_context->unreferenced_vars[$var_id] = $locations;
559
                    } else {
560
                        $loop_scope->loop_context->unreferenced_vars[$var_id] += $locations;
561
                    }
562
                }
563
            }
564
        }
565
566
        if ($always_enters_loop) {
567
            foreach ($inner_context->vars_in_scope as $var_id => $type) {
568
                // if there are break statements in the loop it's not certain
569
                // that the loop has finished executing, so the assertions at the end
570
                // the loop in the while conditional may not hold
571
                if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)
572
                    || in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)
573
                ) {
574
                    if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
575
                        $loop_scope->loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
576
                            $type,
577
                            $loop_scope->possibly_defined_loop_parent_vars[$var_id]
578
                        );
579
                    }
580
                } else {
581
                    if ($codebase->find_unused_variables
582
                        && !isset($loop_scope->loop_parent_context->vars_in_scope[$var_id])
583
                        && isset($inner_context->unreferenced_vars[$var_id])
584
                    ) {
585
                        $loop_scope->loop_parent_context->unreferenced_vars[$var_id]
586
                            = $inner_context->unreferenced_vars[$var_id];
587
                    }
588
589
                    $loop_scope->loop_parent_context->vars_in_scope[$var_id] = $type;
590
                }
591
            }
592
        }
593
594
        if ($inner_do_context) {
595
            $inner_context = $inner_do_context;
596
        }
597
    }
598
599
    /**
600
     * @param  LoopScope $loop_scope
601
     * @param  Context   $pre_outer_context
602
     *
603
     * @return void
604
     */
605
    private static function updateLoopScopeContexts(
606
        LoopScope $loop_scope,
607
        Context $pre_outer_context
608
    ) {
609
        $updated_loop_vars = [];
610
611
        if (!in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)) {
612
            $loop_scope->loop_context->vars_in_scope = $pre_outer_context->vars_in_scope;
613
        } else {
614
            if ($loop_scope->redefined_loop_vars !== null) {
615
                foreach ($loop_scope->redefined_loop_vars as $var => $type) {
616
                    $loop_scope->loop_context->vars_in_scope[$var] = $type;
617
                    $updated_loop_vars[$var] = true;
618
                }
619
            }
620
621 View Code Duplication
            if ($loop_scope->possibly_redefined_loop_vars) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
622
                foreach ($loop_scope->possibly_redefined_loop_vars as $var => $type) {
623
                    if ($loop_scope->loop_context->hasVariable($var)
624
                        && !isset($updated_loop_vars[$var])
625
                    ) {
626
                        $loop_scope->loop_context->vars_in_scope[$var] = Type::combineUnionTypes(
627
                            $loop_scope->loop_context->vars_in_scope[$var],
628
                            $type
629
                        );
630
                    }
631
                }
632
            }
633
        }
634
635
        // merge vars possibly in scope at the end of each loop
636
        $loop_scope->loop_context->vars_possibly_in_scope = array_merge(
637
            $loop_scope->loop_context->vars_possibly_in_scope,
638
            $loop_scope->vars_possibly_in_scope
639
        );
640
    }
641
642
    /**
643
     * @param  PhpParser\Node\Expr $pre_condition
644
     * @param  array<int, Clause>  $pre_condition_clauses
645
     * @param  Context             $loop_context
646
     * @param  Context             $outer_context
647
     *
648
     * @return string[]
649
     */
650
    private static function applyPreConditionToLoopContext(
651
        StatementsAnalyzer $statements_analyzer,
652
        PhpParser\Node\Expr $pre_condition,
653
        array $pre_condition_clauses,
654
        Context $loop_context,
655
        Context $outer_context,
656
        bool $is_do
657
    ) {
658
        $pre_referenced_var_ids = $loop_context->referenced_var_ids;
659
        $loop_context->referenced_var_ids = [];
660
661
        $loop_context->inside_conditional = true;
662
663
        $suppressed_issues = $statements_analyzer->getSuppressedIssues();
664
665 View Code Duplication
        if ($is_do) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
666
            if (!in_array('RedundantCondition', $suppressed_issues, true)) {
667
                $statements_analyzer->addSuppressedIssues(['RedundantCondition']);
668
            }
669
            if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
670
                $statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']);
671
            }
672
            if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
673
                $statements_analyzer->addSuppressedIssues(['TypeDoesNotContainType']);
674
            }
675
        }
676
677
        if (ExpressionAnalyzer::analyze($statements_analyzer, $pre_condition, $loop_context) === false) {
678
            return [];
679
        }
680
681
        $loop_context->inside_conditional = false;
682
683
        $new_referenced_var_ids = $loop_context->referenced_var_ids;
684
        $loop_context->referenced_var_ids = array_merge($pre_referenced_var_ids, $new_referenced_var_ids);
685
686
        $asserted_var_ids = Context::getNewOrUpdatedVarIds($outer_context, $loop_context);
687
688
        $loop_context->clauses = Algebra::simplifyCNF(
689
            array_merge($outer_context->clauses, $pre_condition_clauses)
690
        );
691
692
        $active_while_types = [];
693
694
        $reconcilable_while_types = Algebra::getTruthsFromFormula(
695
            $loop_context->clauses,
696
            \spl_object_id($pre_condition),
697
            $new_referenced_var_ids
698
        );
699
700
        $changed_var_ids = [];
701
702
        if ($reconcilable_while_types) {
703
            $pre_condition_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
704
                $reconcilable_while_types,
705
                $active_while_types,
706
                $loop_context->vars_in_scope,
707
                $changed_var_ids,
708
                $new_referenced_var_ids,
709
                $statements_analyzer,
710
                [],
711
                true,
712
                new CodeLocation($statements_analyzer->getSource(), $pre_condition)
713
            );
714
715
            $loop_context->vars_in_scope = $pre_condition_vars_in_scope_reconciled;
716
        }
717
718 View Code Duplication
        if ($is_do) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
719
            if (!in_array('RedundantCondition', $suppressed_issues, true)) {
720
                $statements_analyzer->removeSuppressedIssues(['RedundantCondition']);
721
            }
722
            if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
723
                $statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
724
            }
725
            if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
726
                $statements_analyzer->removeSuppressedIssues(['TypeDoesNotContainType']);
727
            }
728
        }
729
730
        if ($is_do) {
731
            return [];
732
        }
733
734
        foreach ($asserted_var_ids as $var_id) {
735
            $loop_context->clauses = Context::filterClauses(
736
                $var_id,
737
                $loop_context->clauses,
738
                null,
739
                $statements_analyzer
740
            );
741
        }
742
743
        return $asserted_var_ids;
744
    }
745
746
    /**
747
     * @param  string                               $first_var_id
748
     * @param  array<string, array<string, bool>>   $assignment_map
749
     *
750
     * @return int
751
     */
752
    private static function getAssignmentMapDepth($first_var_id, array $assignment_map)
753
    {
754
        $max_depth = 0;
755
756
        $assignment_var_ids = $assignment_map[$first_var_id];
757
        unset($assignment_map[$first_var_id]);
758
759
        foreach ($assignment_var_ids as $assignment_var_id => $_) {
760
            $depth = 1;
761
762
            if (isset($assignment_map[$assignment_var_id])) {
763
                $depth = 1 + self::getAssignmentMapDepth($assignment_var_id, $assignment_map);
764
            }
765
766
            if ($depth > $max_depth) {
767
                $max_depth = $depth;
768
            }
769
        }
770
771
        return $max_depth;
772
    }
773
}
774