SwitchAnalyzer   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 216
Duplicated Lines 5.56 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
dl 12
loc 216
rs 9.2
c 0
b 0
f 0
wmc 40
lcom 1
cbo 16

1 Method

Rating   Name   Duplication   Size   Complexity  
F analyze() 12 206 40

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SwitchAnalyzer 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 SwitchAnalyzer, 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\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