SwitchAnalyzer::analyze()   F
last analyzed

Complexity

Conditions 40
Paths 17317

Size

Total Lines 206

Duplication

Lines 12
Ratio 5.83 %

Importance

Changes 0
Metric Value
cc 40
nc 17317
nop 3
dl 12
loc 206
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

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\Statements\Expression\ExpressionIdentifier;
8
use Psalm\Internal\Analyzer\StatementsAnalyzer;
9
use Psalm\Context;
10
use Psalm\Internal\Scope\SwitchScope;
11
use Psalm\Type;
12
use Psalm\Type\Algebra;
13
use Psalm\Type\Reconciler;
14
use function count;
15
use function in_array;
16
use function array_merge;
17
18
/**
19
 * @internal
20
 */
21
class SwitchAnalyzer
22
{
23
    /**
24
     * @param   StatementsAnalyzer               $statements_analyzer
25
     * @param   PhpParser\Node\Stmt\Switch_     $stmt
26
     * @param   Context                         $context
27
     *
28
     * @return  false|null
29
     */
30
    public static function analyze(
31
        StatementsAnalyzer $statements_analyzer,
32
        PhpParser\Node\Stmt\Switch_ $stmt,
33
        Context $context
34
    ) {
35
        $codebase = $statements_analyzer->getCodebase();
36
37
        $context->inside_conditional = true;
38
        if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->cond, $context) === false) {
39
            return false;
40
        }
41
        $context->inside_conditional = false;
42
43
        $switch_var_id = ExpressionIdentifier::getArrayVarId(
44
            $stmt->cond,
45
            null,
46
            $statements_analyzer
47
        );
48
49
        if (!$switch_var_id
50
            && ($stmt->cond instanceof PhpParser\Node\Expr\FuncCall
51
                || $stmt->cond instanceof PhpParser\Node\Expr\MethodCall
52
                || $stmt->cond instanceof PhpParser\Node\Expr\StaticCall
53
            )
54
        ) {
55
            $switch_var_id = '$__tmp_switch__' . (int) $stmt->cond->getAttribute('startFilePos');
56
57
            $condition_type = $statements_analyzer->node_data->getType($stmt->cond) ?: Type::getMixed();
58
59
            $context->vars_in_scope[$switch_var_id] = $condition_type;
60
        }
61
62
        $original_context = clone $context;
63
64
        // the last statement always breaks, by default
65
        $last_case_exit_type = 'break';
66
67
        $case_exit_types = new \SplFixedArray(count($stmt->cases));
68
69
        $has_default = false;
70
71
        $case_action_map = [];
72
73
        $config = \Psalm\Config::getInstance();
74
75
        // create a map of case statement -> ultimate exit type
76
        for ($i = count($stmt->cases) - 1; $i >= 0; --$i) {
77
            $case = $stmt->cases[$i];
78
79
            $case_actions = $case_action_map[$i] = ScopeAnalyzer::getFinalControlActions(
80
                $case->stmts,
81
                $statements_analyzer->node_data,
82
                $config->exit_functions,
83
                ['switch']
84
            );
85
86
            if (!in_array(ScopeAnalyzer::ACTION_NONE, $case_actions, true)) {
87
                if ($case_actions === [ScopeAnalyzer::ACTION_END]) {
88
                    $last_case_exit_type = 'return_throw';
89
                } elseif ($case_actions === [ScopeAnalyzer::ACTION_CONTINUE]) {
90
                    $last_case_exit_type = 'continue';
91
                } elseif (in_array(ScopeAnalyzer::ACTION_LEAVE_SWITCH, $case_actions, true)) {
92
                    $last_case_exit_type = 'break';
93
                }
94
            }
95
96
            $case_exit_types[$i] = $last_case_exit_type;
97
        }
98
99
        $switch_scope = new SwitchScope();
100
101
        $was_caching_assertions = $statements_analyzer->node_data->cache_assertions;
102
103
        $statements_analyzer->node_data->cache_assertions = false;
104
105
        for ($i = 0, $l = count($stmt->cases); $i < $l; $i++) {
106
            $case = $stmt->cases[$i];
107
108
            /** @var string */
109
            $case_exit_type = $case_exit_types[$i];
110
111
            $case_actions = $case_action_map[$i];
112
113
            if (!$case->cond) {
114
                $has_default = true;
115
            }
116
117
            if (SwitchCaseAnalyzer::analyze(
118
                $statements_analyzer,
119
                $codebase,
120
                $stmt,
121
                $switch_var_id,
122
                $case,
123
                $context,
124
                $original_context,
125
                $case_exit_type,
126
                $case_actions,
127
                $i === $l - 1,
128
                $switch_scope
129
            ) === false
130
            ) {
131
                return false;
132
            }
133
        }
134
135
        $all_options_matched = $has_default;
136
137
        if (!$has_default && $switch_scope->negated_clauses && $switch_var_id) {
138
            $entry_clauses = Algebra::simplifyCNF(
139
                array_merge(
140
                    $original_context->clauses,
141
                    $switch_scope->negated_clauses
142
                )
143
            );
144
145
            $reconcilable_if_types = Algebra::getTruthsFromFormula($entry_clauses);
146
147
            // if the if has an || in the conditional, we cannot easily reason about it
148
            if ($reconcilable_if_types && isset($reconcilable_if_types[$switch_var_id])) {
149
                $changed_var_ids = [];
150
151
                $case_vars_in_scope_reconciled =
152
                    Reconciler::reconcileKeyedTypes(
153
                        $reconcilable_if_types,
154
                        [],
155
                        $original_context->vars_in_scope,
156
                        $changed_var_ids,
157
                        [],
158
                        $statements_analyzer,
159
                        [],
160
                        $original_context->inside_loop
161
                    );
162
163
                if (isset($case_vars_in_scope_reconciled[$switch_var_id])
164
                    && $case_vars_in_scope_reconciled[$switch_var_id]->isEmpty()
165
                ) {
166
                    $all_options_matched = true;
167
                }
168
            }
169
        }
170
171
        if ($was_caching_assertions) {
172
            $statements_analyzer->node_data->cache_assertions = true;
173
        }
174
175
        // only update vars if there is a default or all possible cases accounted for
176
        // if the default has a throw/return/continue, that should be handled above
177
        if ($all_options_matched) {
178
            if ($switch_scope->new_vars_in_scope) {
179
                $context->vars_in_scope = array_merge($context->vars_in_scope, $switch_scope->new_vars_in_scope);
180
            }
181
182
            if ($switch_scope->redefined_vars) {
183
                $context->vars_in_scope = array_merge($context->vars_in_scope, $switch_scope->redefined_vars);
184
            }
185
186 View Code Duplication
            if ($switch_scope->possibly_redefined_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...
187
                foreach ($switch_scope->possibly_redefined_vars as $var_id => $type) {
188
                    if (!isset($switch_scope->redefined_vars[$var_id])
189
                        && !isset($switch_scope->new_vars_in_scope[$var_id])
190
                    ) {
191
                        $context->vars_in_scope[$var_id] = Type::combineUnionTypes(
192
                            $type,
193
                            $context->vars_in_scope[$var_id]
194
                        );
195
                    }
196
                }
197
            }
198
199
            /** @psalm-suppress UndefinedPropertyAssignment */
200
            $stmt->allMatched = true;
201
        } elseif ($switch_scope->possibly_redefined_vars) {
202
            foreach ($switch_scope->possibly_redefined_vars as $var_id => $type) {
203
                $context->vars_in_scope[$var_id] = Type::combineUnionTypes($type, $context->vars_in_scope[$var_id]);
204
            }
205
        }
206
207
        if ($switch_scope->new_assigned_var_ids) {
208
            $context->assigned_var_ids += $switch_scope->new_assigned_var_ids;
209
        }
210
211
        if ($codebase->find_unused_variables) {
212
            foreach ($switch_scope->new_unreferenced_vars as $var_id => $locations) {
213
                if (($all_options_matched && isset($switch_scope->new_assigned_var_ids[$var_id]))
214
                    || !isset($context->vars_in_scope[$var_id])
215
                ) {
216
                    $context->unreferenced_vars[$var_id] = $locations;
217
                } elseif (isset($switch_scope->new_possibly_assigned_var_ids[$var_id])) {
218
                    if (!isset($context->unreferenced_vars[$var_id])) {
219
                        $context->unreferenced_vars[$var_id] = $locations;
220
                    } else {
221
                        $context->unreferenced_vars[$var_id] += $locations;
222
                    }
223
                } else {
224
                    $statements_analyzer->registerVariableUses($locations);
225
                }
226
            }
227
        }
228
229
        $context->vars_possibly_in_scope = array_merge(
230
            $context->vars_possibly_in_scope,
231
            $switch_scope->new_vars_possibly_in_scope
232
        );
233
234
        return null;
235
    }
236
}
237