SwitchCaseAnalyzer   F
last analyzed

Complexity

Total Complexity 116

Size/Duplication

Total Lines 717
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 36

Importance

Changes 0
Metric Value
dl 0
loc 717
rs 1.883
c 0
b 0
f 0
wmc 116
lcom 1
cbo 36

4 Methods

Rating   Name   Duplication   Size   Complexity  
F analyze() 0 463 74
F handleNonReturningCase() 0 168 31
A simplifyCaseEqualityExpression() 0 31 3
B getOptionsFromNestedOr() 0 35 8

How to fix   Complexity   

Complex Class

Complex classes like SwitchCaseAnalyzer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SwitchCaseAnalyzer, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Block;
3
4
use PhpParser;
5
use Psalm\Codebase;
6
use Psalm\Internal\Analyzer\AlgebraAnalyzer;
7
use Psalm\Internal\Analyzer\ScopeAnalyzer;
8
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
9
use Psalm\Internal\Analyzer\StatementsAnalyzer;
10
use Psalm\CodeLocation;
11
use Psalm\Context;
12
use Psalm\Issue\ContinueOutsideLoop;
13
use Psalm\Issue\ParadoxicalCondition;
14
use Psalm\IssueBuffer;
15
use Psalm\Internal\Scope\CaseScope;
16
use Psalm\Internal\Scope\SwitchScope;
17
use Psalm\Type;
18
use Psalm\Type\Algebra;
19
use Psalm\Type\Reconciler;
20
use function count;
21
use function in_array;
22
use function array_merge;
23
use function is_string;
24
use function substr;
25
use function array_intersect_key;
26
use function array_diff_key;
27
28
/**
29
 * @internal
30
 */
31
class SwitchCaseAnalyzer
32
{
33
    /**
34
     * @param ?string $switch_var_id
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
35
     * @return null|false
36
     */
37
    public static function analyze(
38
        StatementsAnalyzer $statements_analyzer,
39
        Codebase $codebase,
40
        PhpParser\Node\Stmt\Switch_ $stmt,
41
        $switch_var_id,
42
        PhpParser\Node\Stmt\Case_ $case,
43
        Context $context,
44
        Context $original_context,
45
        string $case_exit_type,
46
        array $case_actions,
47
        bool $is_last,
48
        SwitchScope $switch_scope
49
    ) {
50
        // has a return/throw at end
51
        $has_ending_statements = $case_actions === [ScopeAnalyzer::ACTION_END];
52
        $has_leaving_statements = $has_ending_statements
53
            || (count($case_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $case_actions, true));
54
55
        $case_context = clone $original_context;
56
57
        if ($codebase->alter_code) {
58
            $case_context->branch_point = $case_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
59
        }
60
61
        $case_context->parent_context = $context;
62
        $case_scope = $case_context->case_scope = new CaseScope($case_context);
63
64
        $case_equality_expr = null;
65
66
        $old_node_data = $statements_analyzer->node_data;
67
68
        $fake_switch_condition = false;
69
70
        if ($switch_var_id && substr($switch_var_id, 0, 15) === '$__tmp_switch__') {
71
            $switch_condition = new PhpParser\Node\Expr\Variable(
72
                substr($switch_var_id, 1),
73
                $stmt->cond->getAttributes()
74
            );
75
76
            $fake_switch_condition = true;
77
        } else {
78
            $switch_condition = $stmt->cond;
79
        }
80
81
        if ($case->cond) {
82
            $was_inside_conditional = $case_context->inside_conditional;
83
            $case_context->inside_conditional = true;
84
85
            if (ExpressionAnalyzer::analyze($statements_analyzer, $case->cond, $case_context) === false) {
86
                /** @psalm-suppress PossiblyNullPropertyAssignmentValue */
87
                $case_scope->parent_context = null;
88
                $case_context->case_scope = null;
89
                $case_context->parent_context = null;
90
91
                return false;
92
            }
93
94
            if (!$was_inside_conditional) {
95
                $case_context->inside_conditional = false;
96
            }
97
98
            $statements_analyzer->node_data = clone $statements_analyzer->node_data;
99
100
            $traverser = new PhpParser\NodeTraverser;
101
            $traverser->addVisitor(
102
                new \Psalm\Internal\PhpVisitor\ConditionCloningVisitor(
103
                    $statements_analyzer->node_data
104
                )
105
            );
106
107
            /** @var PhpParser\Node\Expr */
108
            $switch_condition = $traverser->traverse([$switch_condition])[0];
109
110
            if ($fake_switch_condition) {
111
                $statements_analyzer->node_data->setType(
112
                    $switch_condition,
0 ignored issues
show
Documentation introduced by
$switch_condition is of type object<PhpParser\Node>, but the function expects a object<PhpParser\Node\Ex...rser\Node\Stmt\Return_>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
113
                    $case_context->vars_in_scope[$switch_var_id] ?? Type::getMixed()
114
                );
115
            }
116
117
            if ($switch_condition instanceof PhpParser\Node\Expr\Variable
118
                && is_string($switch_condition->name)
119
                && isset($context->vars_in_scope['$' . $switch_condition->name])
120
            ) {
121
                $switch_var_type = $context->vars_in_scope['$' . $switch_condition->name];
122
123
                $type_statements = [];
124
125
                foreach ($switch_var_type->getAtomicTypes() as $type) {
126
                    if ($type instanceof Type\Atomic\GetClassT) {
127
                        $type_statements[] = new PhpParser\Node\Expr\FuncCall(
128
                            new PhpParser\Node\Name(['get_class']),
129
                            [
130
                                new PhpParser\Node\Arg(
131
                                    new PhpParser\Node\Expr\Variable(substr($type->typeof, 1))
132
                                ),
133
                            ]
134
                        );
135
                    } elseif ($type instanceof Type\Atomic\GetTypeT) {
136
                        $type_statements[] = new PhpParser\Node\Expr\FuncCall(
137
                            new PhpParser\Node\Name(['gettype']),
138
                            [
139
                                new PhpParser\Node\Arg(
140
                                    new PhpParser\Node\Expr\Variable(substr($type->typeof, 1))
141
                                ),
142
                            ]
143
                        );
144
                    } else {
145
                        $type_statements = null;
146
                        break;
147
                    }
148
                }
149
150
                if ($type_statements && count($type_statements) === 1) {
151
                    $switch_condition = $type_statements[0];
152
153
                    if ($fake_switch_condition) {
154
                        $statements_analyzer->node_data->setType(
155
                            $switch_condition,
156
                            $case_context->vars_in_scope[$switch_var_id] ?? Type::getMixed()
157
                        );
158
                    }
159
                }
160
            }
161
162
            if (($switch_condition_type = $statements_analyzer->node_data->getType($switch_condition))
163
                && ($case_cond_type = $statements_analyzer->node_data->getType($case->cond))
164
                && (($switch_condition_type->isString() && $case_cond_type->isString())
165
                    || ($switch_condition_type->isInt() && $case_cond_type->isInt())
166
                    || ($switch_condition_type->isFloat() && $case_cond_type->isFloat())
167
                )
168
            ) {
169
                $case_equality_expr = new PhpParser\Node\Expr\BinaryOp\Identical(
170
                    $switch_condition,
171
                    $case->cond,
172
                    $case->cond->getAttributes()
173
                );
174
            } else {
175
                $case_equality_expr = new PhpParser\Node\Expr\BinaryOp\Equal(
176
                    $switch_condition,
177
                    $case->cond,
178
                    $case->cond->getAttributes()
179
                );
180
            }
181
        }
182
183
        $continue_case_equality_expr = false;
184
185
        if ($case->stmts) {
186
            $case_stmts = array_merge($switch_scope->leftover_statements, $case->stmts);
187
        } else {
188
            $continue_case_equality_expr = count($switch_scope->leftover_statements) === 1;
189
            $case_stmts = $switch_scope->leftover_statements;
190
        }
191
192
        if (!$has_leaving_statements && !$is_last) {
193
            if (!$case_equality_expr) {
194
                $case_equality_expr = new PhpParser\Node\Expr\FuncCall(
195
                    new PhpParser\Node\Name\FullyQualified(['rand']),
196
                    [
197
                        new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(0)),
198
                        new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(1)),
199
                    ],
200
                    $case->getAttributes()
201
                );
202
            }
203
204
            $switch_scope->leftover_case_equality_expr = $switch_scope->leftover_case_equality_expr
205
                ? new PhpParser\Node\Expr\BinaryOp\BooleanOr(
206
                    $switch_scope->leftover_case_equality_expr,
207
                    $case_equality_expr,
208
                    $case->cond ? $case->cond->getAttributes() : $case->getAttributes()
209
                )
210
                : $case_equality_expr;
211
212
            if ($continue_case_equality_expr
213
                && $switch_scope->leftover_statements[0] instanceof PhpParser\Node\Stmt\If_
214
            ) {
215
                $case_if_stmt = $switch_scope->leftover_statements[0];
216
                $case_if_stmt->cond = $switch_scope->leftover_case_equality_expr;
217
            } else {
218
                $case_if_stmt = new PhpParser\Node\Stmt\If_(
219
                    $switch_scope->leftover_case_equality_expr,
220
                    ['stmts' => $case_stmts]
221
                );
222
223
                $switch_scope->leftover_statements = [$case_if_stmt];
224
            }
225
226
            /** @psalm-suppress PossiblyNullPropertyAssignmentValue */
227
            $case_scope->parent_context = null;
228
            $case_context->case_scope = null;
229
            $case_context->parent_context = null;
230
231
            $statements_analyzer->node_data = $old_node_data;
232
233
            return;
234
        }
235
236
        if ($switch_scope->leftover_case_equality_expr) {
237
            $case_or_default_equality_expr = $case_equality_expr;
238
239
            if (!$case_or_default_equality_expr) {
240
                $case_or_default_equality_expr = new PhpParser\Node\Expr\FuncCall(
241
                    new PhpParser\Node\Name\FullyQualified(['rand']),
242
                    [
243
                        new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(0)),
244
                        new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(1)),
245
                    ],
246
                    $case->getAttributes()
247
                );
248
            }
249
250
            $case_equality_expr = new PhpParser\Node\Expr\BinaryOp\BooleanOr(
251
                $switch_scope->leftover_case_equality_expr,
252
                $case_or_default_equality_expr,
253
                $case_or_default_equality_expr->getAttributes()
254
            );
255
        }
256
257
        if ($case_equality_expr
258
            && $switch_condition instanceof PhpParser\Node\Expr\Variable
259
            && is_string($switch_condition->name)
260
            && isset($context->vars_in_scope['$' . $switch_condition->name])
261
        ) {
262
            $new_case_equality_expr = self::simplifyCaseEqualityExpression(
263
                $case_equality_expr,
264
                $switch_condition
265
            );
266
267
            if ($new_case_equality_expr) {
268
                ExpressionAnalyzer::analyze(
269
                    $statements_analyzer,
270
                    $new_case_equality_expr->args[1]->value,
271
                    $case_context
272
                );
273
274
                $case_equality_expr = $new_case_equality_expr;
275
            }
276
        }
277
278
        $case_context->break_types[] = 'switch';
279
280
        $switch_scope->leftover_statements = [];
281
        $switch_scope->leftover_case_equality_expr = null;
282
283
        $case_clauses = [];
284
285
        if ($case_equality_expr) {
286
            $case_clauses = Algebra::getFormula(
287
                \spl_object_id($case_equality_expr),
288
                $case_equality_expr,
289
                $context->self,
290
                $statements_analyzer,
291
                $codebase,
292
                false,
293
                false
294
            );
295
        }
296
297
        if ($switch_scope->negated_clauses && count($switch_scope->negated_clauses) < 50) {
298
            $entry_clauses = Algebra::simplifyCNF(
299
                array_merge(
300
                    $original_context->clauses,
301
                    $switch_scope->negated_clauses
302
                )
303
            );
304
        } else {
305
            $entry_clauses = $original_context->clauses;
306
        }
307
308
        if ($case_clauses && $case->cond) {
309
            // this will see whether any of the clauses in set A conflict with the clauses in set B
310
            AlgebraAnalyzer::checkForParadox(
311
                $entry_clauses,
312
                $case_clauses,
313
                $statements_analyzer,
314
                $case->cond,
315
                []
316
            );
317
318
            if (count($entry_clauses) + count($case_clauses) < 50) {
319
                $case_context->clauses = Algebra::simplifyCNF(array_merge($entry_clauses, $case_clauses));
320
            } else {
321
                $case_context->clauses = array_merge($entry_clauses, $case_clauses);
322
            }
323
        } else {
324
            $case_context->clauses = $entry_clauses;
325
        }
326
327
        $reconcilable_if_types = Algebra::getTruthsFromFormula($case_context->clauses);
328
329
        // if the if has an || in the conditional, we cannot easily reason about it
330
        if ($reconcilable_if_types) {
331
            $changed_var_ids = [];
332
333
            $suppressed_issues = $statements_analyzer->getSuppressedIssues();
334
335
            if (!in_array('RedundantCondition', $suppressed_issues, true)) {
336
                $statements_analyzer->addSuppressedIssues(['RedundantCondition']);
337
            }
338
339
            if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
340
                $statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']);
341
            }
342
343
            $case_vars_in_scope_reconciled =
344
                Reconciler::reconcileKeyedTypes(
345
                    $reconcilable_if_types,
346
                    [],
347
                    $case_context->vars_in_scope,
348
                    $changed_var_ids,
349
                    $case->cond && $switch_var_id ? [$switch_var_id => true] : [],
350
                    $statements_analyzer,
351
                    [],
352
                    $case_context->inside_loop,
353
                    new CodeLocation(
354
                        $statements_analyzer->getSource(),
355
                        $case->cond ? $case->cond : $case,
356
                        $context->include_location
357
                    )
358
                );
359
360
            if (!in_array('RedundantCondition', $suppressed_issues, true)) {
361
                $statements_analyzer->removeSuppressedIssues(['RedundantCondition']);
362
            }
363
364
            if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
365
                $statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
366
            }
367
368
            $case_context->vars_in_scope = $case_vars_in_scope_reconciled;
369
            foreach ($reconcilable_if_types as $var_id => $_) {
370
                $case_context->vars_possibly_in_scope[$var_id] = true;
371
            }
372
373
            if ($changed_var_ids) {
374
                $case_context->clauses = Context::removeReconciledClauses($case_context->clauses, $changed_var_ids)[0];
375
            }
376
        }
377
378
        if ($case_clauses) {
379
            $switch_scope->negated_clauses = array_merge(
380
                $switch_scope->negated_clauses,
381
                Algebra::negateFormula($case_clauses)
382
            );
383
        }
384
385
        $pre_possibly_assigned_var_ids = $case_context->possibly_assigned_var_ids;
386
        $case_context->possibly_assigned_var_ids = [];
387
388
        $pre_assigned_var_ids = $case_context->assigned_var_ids;
389
        $case_context->assigned_var_ids = [];
390
391
        $statements_analyzer->analyze($case_stmts, $case_context);
392
393
        $traverser = new PhpParser\NodeTraverser;
394
        $traverser->addVisitor(
395
            new \Psalm\Internal\PhpVisitor\TypeMappingVisitor(
396
                $statements_analyzer->node_data,
397
                $old_node_data
398
            )
399
        );
400
401
        $traverser->traverse([$case]);
402
403
        $statements_analyzer->node_data = $old_node_data;
404
405
        /** @var array<string, bool> */
406
        $new_case_assigned_var_ids = $case_context->assigned_var_ids;
407
        $case_context->assigned_var_ids = $pre_assigned_var_ids + $new_case_assigned_var_ids;
408
409
        /** @var array<string, bool> */
410
        $new_case_possibly_assigned_var_ids = $case_context->possibly_assigned_var_ids;
411
        $case_context->possibly_assigned_var_ids =
412
            $pre_possibly_assigned_var_ids + $new_case_possibly_assigned_var_ids;
413
414
        $context->referenced_var_ids = array_merge(
415
            $context->referenced_var_ids,
416
            $case_context->referenced_var_ids
417
        );
418
419
        if ($case_exit_type !== 'return_throw') {
420
            if (self::handleNonReturningCase(
421
                $statements_analyzer,
422
                $switch_var_id,
423
                $case,
424
                $context,
425
                $case_context,
426
                $original_context,
427
                $new_case_assigned_var_ids,
428
                $new_case_possibly_assigned_var_ids,
429
                $case_exit_type,
430
                $switch_scope,
431
                $case_scope
432
            ) === false) {
433
                /** @psalm-suppress PossiblyNullPropertyAssignmentValue */
434
                $case_scope->parent_context = null;
435
                $case_context->case_scope = null;
436
                $case_context->parent_context = null;
437
438
                return false;
439
            }
440
        }
441
442
        // augment the information with data from break statements
443
        if ($case_scope->break_vars !== null) {
444
            if ($switch_scope->possibly_redefined_vars === null) {
445
                $switch_scope->possibly_redefined_vars = array_intersect_key(
446
                    $case_scope->break_vars,
447
                    $context->vars_in_scope
448
                );
449
            } else {
450
                foreach ($case_scope->break_vars as $var_id => $type) {
451
                    if (isset($context->vars_in_scope[$var_id])) {
452
                        if (!isset($switch_scope->possibly_redefined_vars[$var_id])) {
453
                            $switch_scope->possibly_redefined_vars[$var_id] = clone $type;
454
                        } else {
455
                            $switch_scope->possibly_redefined_vars[$var_id] = Type::combineUnionTypes(
456
                                clone $type,
457
                                $switch_scope->possibly_redefined_vars[$var_id]
458
                            );
459
                        }
460
                    }
461
                }
462
            }
463
464
            if ($switch_scope->new_vars_in_scope !== null) {
465
                foreach ($switch_scope->new_vars_in_scope as $var_id => $type) {
466
                    if (isset($case_scope->break_vars[$var_id])) {
467
                        if (!isset($case_context->vars_in_scope[$var_id])) {
468
                            unset($switch_scope->new_vars_in_scope[$var_id]);
469
                        } else {
470
                            $switch_scope->new_vars_in_scope[$var_id] = Type::combineUnionTypes(
471
                                clone $case_scope->break_vars[$var_id],
472
                                $type
473
                            );
474
                        }
475
                    } else {
476
                        unset($switch_scope->new_vars_in_scope[$var_id]);
477
                    }
478
                }
479
            }
480
481
            if ($switch_scope->redefined_vars !== null) {
482
                foreach ($switch_scope->redefined_vars as $var_id => $type) {
483
                    if (isset($case_scope->break_vars[$var_id])) {
484
                        $switch_scope->redefined_vars[$var_id] = Type::combineUnionTypes(
485
                            clone $case_scope->break_vars[$var_id],
486
                            $type
487
                        );
488
                    } else {
489
                        unset($switch_scope->redefined_vars[$var_id]);
490
                    }
491
                }
492
            }
493
        }
494
495
        /** @psalm-suppress PossiblyNullPropertyAssignmentValue */
496
        $case_scope->parent_context = null;
497
        $case_context->case_scope = null;
498
        $case_context->parent_context = null;
499
    }
500
501
    /**
502
     * @param ?string $switch_var_id
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
503
     * @param array<string, bool> $new_case_assigned_var_ids
504
     * @param array<string, bool> $new_case_possibly_assigned_var_ids
505
     * @return null|false
506
     */
507
    private static function handleNonReturningCase(
508
        StatementsAnalyzer $statements_analyzer,
509
        $switch_var_id,
510
        PhpParser\Node\Stmt\Case_ $case,
511
        Context $context,
512
        Context $case_context,
513
        Context $original_context,
514
        array $new_case_assigned_var_ids,
515
        array $new_case_possibly_assigned_var_ids,
516
        string $case_exit_type,
517
        SwitchScope $switch_scope,
518
        CaseScope $case_scope
519
    ) {
520
        if (!$case->cond
521
            && $switch_var_id
522
            && isset($case_context->vars_in_scope[$switch_var_id])
523
            && $case_context->vars_in_scope[$switch_var_id]->isEmpty()
524
        ) {
525
            if (IssueBuffer::accepts(
526
                new ParadoxicalCondition(
527
                    'All possible case statements have been met, default is impossible here',
528
                    new CodeLocation($statements_analyzer->getSource(), $case)
529
                ),
530
                $statements_analyzer->getSuppressedIssues()
531
            )) {
532
                return false;
533
            }
534
        }
535
536
        // if we're leaving this block, add vars to outer for loop scope
537
        if ($case_exit_type === 'continue') {
538
            if (!$context->loop_scope) {
539
                if (IssueBuffer::accepts(
540
                    new ContinueOutsideLoop(
541
                        'Continue called when not in loop',
542
                        new CodeLocation($statements_analyzer->getSource(), $case)
543
                    )
544
                )) {
545
                    return false;
546
                }
547
            }
548
        } else {
549
            $case_redefined_vars = $case_context->getRedefinedVars($original_context->vars_in_scope);
550
551
            if ($switch_scope->possibly_redefined_vars === null) {
552
                $switch_scope->possibly_redefined_vars = $case_redefined_vars;
553
            } else {
554
                foreach ($case_redefined_vars as $var_id => $type) {
555
                    if (!isset($switch_scope->possibly_redefined_vars[$var_id])) {
556
                        $switch_scope->possibly_redefined_vars[$var_id] = clone $type;
557
                    } else {
558
                        $switch_scope->possibly_redefined_vars[$var_id] = Type::combineUnionTypes(
559
                            clone $type,
560
                            $switch_scope->possibly_redefined_vars[$var_id]
561
                        );
562
                    }
563
                }
564
            }
565
566
            if ($switch_scope->redefined_vars === null) {
567
                $switch_scope->redefined_vars = $case_redefined_vars;
568
            } else {
569
                foreach ($switch_scope->redefined_vars as $var_id => $type) {
570
                    if (!isset($case_redefined_vars[$var_id])) {
571
                        unset($switch_scope->redefined_vars[$var_id]);
572
                    } else {
573
                        $switch_scope->redefined_vars[$var_id] = Type::combineUnionTypes(
574
                            $type,
575
                            clone $case_redefined_vars[$var_id]
576
                        );
577
                    }
578
                }
579
            }
580
581
            $context_new_vars = array_diff_key($case_context->vars_in_scope, $context->vars_in_scope);
582
583
            if ($switch_scope->new_vars_in_scope === null) {
584
                $switch_scope->new_vars_in_scope = $context_new_vars;
585
                $switch_scope->new_vars_possibly_in_scope = array_diff_key(
586
                    $case_context->vars_possibly_in_scope,
587
                    $context->vars_possibly_in_scope
588
                );
589
            } else {
590
                foreach ($switch_scope->new_vars_in_scope as $new_var => $type) {
591
                    if (!$case_context->hasVariable($new_var, $statements_analyzer)) {
592
                        unset($switch_scope->new_vars_in_scope[$new_var]);
593
                    } else {
594
                        $switch_scope->new_vars_in_scope[$new_var] =
595
                            Type::combineUnionTypes(clone $case_context->vars_in_scope[$new_var], $type);
596
                    }
597
                }
598
599
                $switch_scope->new_vars_possibly_in_scope = array_merge(
600
                    array_diff_key(
601
                        $case_context->vars_possibly_in_scope,
602
                        $context->vars_possibly_in_scope
603
                    ),
604
                    $switch_scope->new_vars_possibly_in_scope
605
                );
606
            }
607
        }
608
609
        if ($context->collect_exceptions) {
610
            $context->mergeExceptions($case_context);
611
        }
612
613
        $codebase = $statements_analyzer->getCodebase();
614
615
        if ($codebase->find_unused_variables) {
616
            $switch_scope->new_possibly_assigned_var_ids =
617
                $switch_scope->new_possibly_assigned_var_ids + $new_case_possibly_assigned_var_ids;
618
619
            if ($switch_scope->new_assigned_var_ids === null) {
620
                $switch_scope->new_assigned_var_ids = $new_case_assigned_var_ids;
621
            } else {
622
                $switch_scope->new_assigned_var_ids = array_intersect_key(
623
                    $switch_scope->new_assigned_var_ids,
624
                    $new_case_assigned_var_ids
625
                );
626
            }
627
628
            foreach ($case_context->unreferenced_vars as $var_id => $locations) {
629
                if (!isset($original_context->unreferenced_vars[$var_id])) {
630
                    if (isset($switch_scope->new_unreferenced_vars[$var_id])) {
631
                        $switch_scope->new_unreferenced_vars[$var_id] += $locations;
632
                    } else {
633
                        $switch_scope->new_unreferenced_vars[$var_id] = $locations;
634
                    }
635
                } else {
636
                    $new_locations = array_diff_key(
637
                        $locations,
638
                        $original_context->unreferenced_vars[$var_id]
639
                    );
640
641
                    if ($new_locations) {
642
                        if (isset($switch_scope->new_unreferenced_vars[$var_id])) {
643
                            $switch_scope->new_unreferenced_vars[$var_id] += $locations;
644
                        } else {
645
                            $switch_scope->new_unreferenced_vars[$var_id] = $locations;
646
                        }
647
                    }
648
                }
649
            }
650
651
            foreach ($case_scope->unreferenced_vars as $var_id => $locations) {
652
                if (!isset($original_context->unreferenced_vars[$var_id])) {
653
                    if (isset($switch_scope->new_unreferenced_vars[$var_id])) {
654
                        $switch_scope->new_unreferenced_vars[$var_id] += $locations;
655
                    } else {
656
                        $switch_scope->new_unreferenced_vars[$var_id] = $locations;
657
                    }
658
                } else {
659
                    $new_locations = array_diff_key(
660
                        $locations,
661
                        $original_context->unreferenced_vars[$var_id]
662
                    );
663
664
                    if ($new_locations) {
665
                        if (isset($switch_scope->new_unreferenced_vars[$var_id])) {
666
                            $switch_scope->new_unreferenced_vars[$var_id] += $locations;
667
                        } else {
668
                            $switch_scope->new_unreferenced_vars[$var_id] = $locations;
669
                        }
670
                    }
671
                }
672
            }
673
        }
674
    }
675
676
    private static function simplifyCaseEqualityExpression(
677
        PhpParser\Node\Expr $case_equality_expr,
678
        PhpParser\Node\Expr\Variable $var
679
    ) : ?PhpParser\Node\Expr\FuncCall {
680
        if ($case_equality_expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
681
            $nested_or_options = self::getOptionsFromNestedOr($case_equality_expr, $var);
682
683
            if ($nested_or_options) {
684
                return new PhpParser\Node\Expr\FuncCall(
685
                    new PhpParser\Node\Name\FullyQualified(['in_array']),
686
                    [
687
                        new PhpParser\Node\Arg(
688
                            $var
689
                        ),
690
                        new PhpParser\Node\Arg(
691
                            new PhpParser\Node\Expr\Array_(
692
                                $nested_or_options
693
                            )
694
                        ),
695
                        new PhpParser\Node\Arg(
696
                            new PhpParser\Node\Expr\ConstFetch(
697
                                new PhpParser\Node\Name\FullyQualified(['true'])
698
                            )
699
                        ),
700
                    ]
701
                );
702
            }
703
        }
704
705
        return null;
706
    }
707
708
    /**
709
     * @param array<PhpParser\Node\Expr\ArrayItem> $in_array_values
710
     * @return ?array<PhpParser\Node\Expr\ArrayItem>
0 ignored issues
show
Documentation introduced by
The doc-type ?array<PhpParser\Node\Expr\ArrayItem> could not be parsed: Unknown type name "?array" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
711
     */
712
    private static function getOptionsFromNestedOr(
713
        PhpParser\Node\Expr $case_equality_expr,
714
        PhpParser\Node\Expr\Variable $var,
715
        array $in_array_values = []
716
    ) : ?array {
717
        if ($case_equality_expr instanceof PhpParser\Node\Expr\BinaryOp\Identical
718
            && $case_equality_expr->left instanceof PhpParser\Node\Expr\Variable
719
            && $case_equality_expr->left->name === $var->name
720
        ) {
721
            $in_array_values[] = new PhpParser\Node\Expr\ArrayItem(
722
                $case_equality_expr->right
723
            );
724
725
            return $in_array_values;
726
        }
727
728
        if (!$case_equality_expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
729
            return null;
730
        }
731
732
        if (!$case_equality_expr->right instanceof PhpParser\Node\Expr\BinaryOp\Identical
733
            || !$case_equality_expr->right->left instanceof PhpParser\Node\Expr\Variable
734
            || $case_equality_expr->right->left->name !== $var->name
735
        ) {
736
            return null;
737
        }
738
739
        $in_array_values[] = new PhpParser\Node\Expr\ArrayItem($case_equality_expr->right->right);
740
741
        return self::getOptionsFromNestedOr(
742
            $case_equality_expr->left,
743
            $var,
744
            $in_array_values
745
        );
746
    }
747
}
748