TryAnalyzer::analyze()   F
last analyzed

Complexity

Conditions 98
Paths > 20000

Size

Total Lines 495

Duplication

Lines 43
Ratio 8.69 %

Importance

Changes 0
Metric Value
cc 98
nc 4294967295
nop 3
dl 43
loc 495
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\ClassLikeAnalyzer;
6
use Psalm\Internal\Analyzer\ScopeAnalyzer;
7
use Psalm\Internal\Analyzer\StatementsAnalyzer;
8
use Psalm\CodeLocation;
9
use Psalm\Context;
10
use Psalm\Issue\InvalidCatch;
11
use Psalm\IssueBuffer;
12
use Psalm\Type;
13
use Psalm\Type\Atomic\TNamedObject;
14
use Psalm\Type\Union;
15
use function in_array;
16
use function array_merge;
17
use function array_intersect_key;
18
use function array_diff_key;
19
use function is_string;
20
use function strtolower;
21
use function array_map;
22
use function version_compare;
23
use const PHP_VERSION;
24
25
/**
26
 * @internal
27
 */
28
class TryAnalyzer
29
{
30
    /**
31
     * @param   StatementsAnalyzer               $statements_analyzer
32
     * @param   PhpParser\Node\Stmt\TryCatch    $stmt
33
     * @param   Context                         $context
34
     *
35
     * @return  false|null
36
     */
37
    public static function analyze(
38
        StatementsAnalyzer $statements_analyzer,
39
        PhpParser\Node\Stmt\TryCatch $stmt,
40
        Context $context
41
    ) {
42
        $catch_actions = [];
43
        $all_catches_leave = true;
44
45
        $codebase = $statements_analyzer->getCodebase();
46
47
        /** @var int $i */
48
        foreach ($stmt->catches as $i => $catch) {
49
            $catch_actions[$i] = ScopeAnalyzer::getFinalControlActions(
50
                $catch->stmts,
51
                $statements_analyzer->node_data,
52
                $codebase->config->exit_functions
53
            );
54
            $all_catches_leave = $all_catches_leave && !in_array(ScopeAnalyzer::ACTION_NONE, $catch_actions[$i], true);
55
        }
56
57
        $existing_thrown_exceptions = $context->possibly_thrown_exceptions;
58
59
        /**
60
         * @var array<string, array<array-key, CodeLocation>>
61
         */
62
        $context->possibly_thrown_exceptions = [];
63
64
        $old_context = clone $context;
65
66
        if ($all_catches_leave && !$stmt->finally) {
67
            $try_context = $context;
68
        } else {
69
            $try_context = clone $context;
70
71
            if ($codebase->alter_code) {
72
                $try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
73
            }
74
        }
75
76
        $assigned_var_ids = $try_context->assigned_var_ids;
77
        $context->assigned_var_ids = [];
78
79
        $old_referenced_var_ids = $try_context->referenced_var_ids;
80
        $old_unreferenced_vars = $try_context->unreferenced_vars;
81
82
        $newly_unreferenced_vars = [];
83
84
        if ($statements_analyzer->analyze($stmt->stmts, $context) === false) {
85
            return false;
86
        }
87
88
        $context->has_returned = false;
89
90
        $stmt_control_actions = ScopeAnalyzer::getFinalControlActions(
91
            $stmt->stmts,
92
            $statements_analyzer->node_data,
93
            $codebase->config->exit_functions,
94
            $context->break_types
95
        );
96
97
        /** @var array<string, bool> */
98
        $newly_assigned_var_ids = $context->assigned_var_ids;
99
100
        $context->assigned_var_ids = array_merge(
101
            $assigned_var_ids,
102
            $newly_assigned_var_ids
103
        );
104
105
        $possibly_referenced_var_ids = array_merge(
106
            $context->referenced_var_ids,
107
            $old_referenced_var_ids
108
        );
109
110
        if ($try_context !== $context) {
111
            foreach ($context->vars_in_scope as $var_id => $type) {
112
                if (!isset($try_context->vars_in_scope[$var_id])) {
113
                    $try_context->vars_in_scope[$var_id] = clone $type;
114
                    $try_context->vars_in_scope[$var_id]->from_docblock = true;
115
                } else {
116
                    $try_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
117
                        $try_context->vars_in_scope[$var_id],
118
                        $type
119
                    );
120
                }
121
            }
122
123
            $try_context->vars_possibly_in_scope = $context->vars_possibly_in_scope;
124
            $try_context->possibly_thrown_exceptions = $context->possibly_thrown_exceptions;
125
126
            $context->referenced_var_ids = array_intersect_key(
127
                $try_context->referenced_var_ids,
128
                $context->referenced_var_ids
129
            );
130
131
            if ($codebase->find_unused_variables) {
132
                $newly_unreferenced_vars = array_merge(
133
                    $newly_unreferenced_vars,
134
                    array_diff_key(
135
                        $context->unreferenced_vars,
136
                        $old_unreferenced_vars
137
                    )
138
                );
139
            }
140
        }
141
142
        $try_leaves_loop = $context->loop_scope
143
            && $context->loop_scope->final_actions
144
            && !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true);
145
146
        if (!$all_catches_leave) {
147
            foreach ($newly_assigned_var_ids as $assigned_var_id => $_) {
148
                $context->removeVarFromConflictingClauses($assigned_var_id);
149
            }
150
        } else {
151
            foreach ($newly_assigned_var_ids as $assigned_var_id => $_) {
152
                $try_context->removeVarFromConflictingClauses($assigned_var_id);
153
            }
154
        }
155
156
        // at this point we have two contexts – $context, in which it is assumed that everything was fine,
157
        // and $try_context - which allows all variables to have the union of the values before and after
158
        // the try was applied
159
        $original_context = clone $try_context;
160
161
        $issues_to_suppress = [
162
            'RedundantCondition',
163
            'RedundantConditionGivenDocblockType',
164
            'TypeDoesNotContainNull',
165
            'TypeDoesNotContainType',
166
        ];
167
168
        $definitely_newly_assigned_var_ids = $newly_assigned_var_ids;
169
170
        /** @var int $i */
171
        foreach ($stmt->catches as $i => $catch) {
172
            $catch_context = clone $original_context;
173
            $catch_context->has_returned = false;
174
175
            foreach ($catch_context->vars_in_scope as $var_id => $type) {
176
                if (!isset($old_context->vars_in_scope[$var_id])) {
177
                    $type = clone $type;
178
                    $type->possibly_undefined_from_try = true;
179
                    $catch_context->vars_in_scope[$var_id] = $type;
180
                } else {
181
                    $catch_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
182
                        $type,
183
                        $old_context->vars_in_scope[$var_id]
184
                    );
185
                }
186
187 View Code Duplication
                if (isset($old_context->unreferenced_vars[$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...
188
                    if (!isset($catch_context->unreferenced_vars[$var_id])) {
189
                        $catch_context->unreferenced_vars[$var_id] = $old_context->unreferenced_vars[$var_id];
190
                    } else {
191
                        $catch_context->unreferenced_vars[$var_id] += $old_context->unreferenced_vars[$var_id];
192
                    }
193
                }
194
            }
195
196
            $fq_catch_classes = [];
197
198
            if (!$catch->types) {
199
                throw new \UnexpectedValueException('Very bad');
200
            }
201
202
            foreach ($catch->types as $catch_type) {
203
                $fq_catch_class = ClassLikeAnalyzer::getFQCLNFromNameObject(
204
                    $catch_type,
205
                    $statements_analyzer->getAliases()
206
                );
207
208
                $fq_catch_class = $codebase->classlikes->getUnAliasedName($fq_catch_class);
209
210
                if ($codebase->alter_code && $fq_catch_class) {
211
                    $codebase->classlikes->handleClassLikeReferenceInMigration(
212
                        $codebase,
213
                        $statements_analyzer,
214
                        $catch_type,
215
                        $fq_catch_class,
216
                        $context->calling_method_id
217
                    );
218
                }
219
220
                if ($original_context->check_classes) {
221
                    if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
222
                        $statements_analyzer,
223
                        $fq_catch_class,
224
                        new CodeLocation($statements_analyzer->getSource(), $catch_type, $context->include_location),
225
                        $context->self,
226
                        $context->calling_method_id,
227
                        $statements_analyzer->getSuppressedIssues(),
228
                        false
229
                    ) === false) {
230
                        return false;
231
                    }
232
                }
233
234
                if (($codebase->classExists($fq_catch_class)
235
                        && strtolower($fq_catch_class) !== 'exception'
236
                        && !($codebase->classExtends($fq_catch_class, 'Exception')
237
                            || $codebase->classImplements($fq_catch_class, 'Throwable')))
238
                    || ($codebase->interfaceExists($fq_catch_class)
239
                        && strtolower($fq_catch_class) !== 'throwable'
240
                        && !$codebase->interfaceExtends($fq_catch_class, 'Throwable'))
241
                ) {
242
                    if (IssueBuffer::accepts(
243
                        new InvalidCatch(
244
                            'Class/interface ' . $fq_catch_class . ' cannot be caught',
245
                            new CodeLocation($statements_analyzer->getSource(), $stmt),
246
                            $fq_catch_class
247
                        ),
248
                        $statements_analyzer->getSuppressedIssues()
249
                    )) {
250
                        return false;
251
                    }
252
                }
253
254
                $fq_catch_classes[] = $fq_catch_class;
255
            }
256
257
            if ($catch_context->collect_exceptions) {
258
                foreach ($fq_catch_classes as $fq_catch_class) {
259
                    $fq_catch_class_lower = strtolower($fq_catch_class);
260
261
                    foreach ($catch_context->possibly_thrown_exceptions as $exception_fqcln => $_) {
262
                        $exception_fqcln_lower = strtolower($exception_fqcln);
263
264
                        if ($exception_fqcln_lower === $fq_catch_class_lower
265
                            || ($codebase->classExists($exception_fqcln)
266
                                && $codebase->classExtendsOrImplements($exception_fqcln, $fq_catch_class))
267
                            || ($codebase->interfaceExists($exception_fqcln)
268
                                && $codebase->interfaceExtends($exception_fqcln, $fq_catch_class))
269
                        ) {
270
                            unset($original_context->possibly_thrown_exceptions[$exception_fqcln]);
271
                            unset($context->possibly_thrown_exceptions[$exception_fqcln]);
272
                            unset($catch_context->possibly_thrown_exceptions[$exception_fqcln]);
273
                        }
274
                    }
275
                }
276
277
                /**
278
                 * @var array<string, array<array-key, CodeLocation>>
279
                 */
280
                $catch_context->possibly_thrown_exceptions = [];
281
            }
282
283
            // discard all clauses because crazy stuff may have happened in try block
284
            $catch_context->clauses = [];
285
286
            /** @psalm-suppress RedundantConditionGivenDocblockType */
287
            if ($catch->var && is_string($catch->var->name)) {
288
                $catch_var_id = '$' . $catch->var->name;
289
290
                $catch_context->vars_in_scope[$catch_var_id] = new Union(
291
                    array_map(
292
                        /**
293
                         * @param string $fq_catch_class
294
                         *
295
                         * @return Type\Atomic
296
                         */
297
                        function ($fq_catch_class) use ($codebase) {
298
                            $catch_class_type = new TNamedObject($fq_catch_class);
299
300
                            if (version_compare(PHP_VERSION, '7.0.0dev', '>=')
301
                                && strtolower($fq_catch_class) !== 'throwable'
302
                                && $codebase->interfaceExists($fq_catch_class)
303
                                && !$codebase->interfaceExtends($fq_catch_class, 'Throwable')
304
                            ) {
305
                                $catch_class_type->addIntersectionType(new TNamedObject('Throwable'));
306
                            }
307
308
                            return $catch_class_type;
309
                        },
310
                        $fq_catch_classes
311
                    )
312
                );
313
314
                $catch_context->vars_possibly_in_scope[$catch_var_id] = true;
315
316
                if (!$statements_analyzer->hasVariable($catch_var_id)) {
317
                    $location = new CodeLocation(
318
                        $statements_analyzer,
319
                        $catch->var,
320
                        $context->include_location
321
                    );
322
                    $statements_analyzer->registerVariable(
323
                        $catch_var_id,
324
                        $location,
325
                        $try_context->branch_point
326
                    );
327
                    $catch_context->unreferenced_vars[$catch_var_id] = [$location->getHash() => $location];
328
                }
329
330
                // this registers the variable to avoid unfair deadcode issues
331
                $catch_context->hasVariable($catch_var_id, $statements_analyzer);
332
            }
333
334
            $suppressed_issues = $statements_analyzer->getSuppressedIssues();
335
336
            foreach ($issues_to_suppress as $issue_to_suppress) {
337
                if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
338
                    $statements_analyzer->addSuppressedIssues([$issue_to_suppress]);
339
                }
340
            }
341
342
            $old_catch_assigned_var_ids = $catch_context->referenced_var_ids;
343
344
            $catch_context->assigned_var_ids = [];
345
346
            $statements_analyzer->analyze($catch->stmts, $catch_context);
347
348
            // recalculate in case there's a no-return clause
349
            $catch_actions[$i] = ScopeAnalyzer::getFinalControlActions(
350
                $catch->stmts,
351
                $statements_analyzer->node_data,
352
                $codebase->config->exit_functions,
353
                $context->break_types
354
            );
355
356
            foreach ($issues_to_suppress as $issue_to_suppress) {
357
                if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
358
                    $statements_analyzer->removeSuppressedIssues([$issue_to_suppress]);
359
                }
360
            }
361
362
            /** @var array<string, bool> */
363
            $new_catch_assigned_var_ids = $catch_context->assigned_var_ids;
364
365
            $catch_context->assigned_var_ids += $old_catch_assigned_var_ids;
366
367
            $context->referenced_var_ids = array_intersect_key(
368
                $catch_context->referenced_var_ids,
369
                $context->referenced_var_ids
370
            );
371
372
            $possibly_referenced_var_ids = array_merge(
373
                $catch_context->referenced_var_ids,
374
                $possibly_referenced_var_ids
375
            );
376
377
            if ($codebase->find_unused_variables && $catch_actions[$i] !== [ScopeAnalyzer::ACTION_END]) {
378
                $newly_unreferenced_vars = array_merge(
379
                    $newly_unreferenced_vars,
380
                    array_diff_key(
381
                        $catch_context->unreferenced_vars,
382
                        $old_unreferenced_vars
383
                    )
384
                );
385
386
                foreach ($catch_context->unreferenced_vars as $var_id => $locations) {
387
                    if (!isset($old_unreferenced_vars[$var_id])
388
                        && (isset($context->unreferenced_vars[$var_id])
389
                            || isset($newly_assigned_var_ids[$var_id]))
390
                    ) {
391
                        $statements_analyzer->registerVariableUses($locations);
392
                    } elseif (isset($old_unreferenced_vars[$var_id])
393
                        && $old_unreferenced_vars[$var_id] !== $locations
394
                    ) {
395
                        $statements_analyzer->registerVariableUses($locations);
396
                    } elseif (isset($newly_unreferenced_vars[$var_id])) {
397
                        $context->unreferenced_vars[$var_id] = $newly_unreferenced_vars[$var_id];
398
                    }
399
                }
400
            }
401
402
            if ($catch_context->collect_exceptions) {
403
                $context->mergeExceptions($catch_context);
404
            }
405
406
            if ($catch_actions[$i] !== [ScopeAnalyzer::ACTION_END]
407
                && $catch_actions[$i] !== [ScopeAnalyzer::ACTION_CONTINUE]
408
                && $catch_actions[$i] !== [ScopeAnalyzer::ACTION_BREAK]
409
            ) {
410
                $definitely_newly_assigned_var_ids = array_intersect_key(
411
                    $new_catch_assigned_var_ids,
412
                    $definitely_newly_assigned_var_ids
413
                );
414
415
                foreach ($catch_context->vars_in_scope as $var_id => $type) {
416
                    if ($stmt_control_actions === [ScopeAnalyzer::ACTION_END]) {
417
                        $context->vars_in_scope[$var_id] = $type;
418
                    } elseif (isset($context->vars_in_scope[$var_id])
419
                        && !$context->vars_in_scope[$var_id]->equals($type)
420
                    ) {
421
                        $context->vars_in_scope[$var_id] = Type::combineUnionTypes(
422
                            $context->vars_in_scope[$var_id],
423
                            $type
424
                        );
425
                    }
426
                }
427
428
                $context->vars_possibly_in_scope = array_merge(
429
                    $catch_context->vars_possibly_in_scope,
430
                    $context->vars_possibly_in_scope
431
                );
432
            } else {
433
                if ($stmt->finally) {
434
                    $context->vars_possibly_in_scope = array_merge(
435
                        $catch_context->vars_possibly_in_scope,
436
                        $context->vars_possibly_in_scope
437
                    );
438
                }
439
            }
440
441 View Code Duplication
            if ($stmt->finally) {
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...
442
                $suppressed_issues = $statements_analyzer->getSuppressedIssues();
443
444
                foreach ($issues_to_suppress as $issue_to_suppress) {
445
                    if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
446
                        $statements_analyzer->addSuppressedIssues([$issue_to_suppress]);
447
                    }
448
                }
449
450
                $catch_context->has_returned = false;
451
452
                $statements_analyzer->analyze($stmt->finally->stmts, $catch_context);
453
454
                foreach ($issues_to_suppress as $issue_to_suppress) {
455
                    if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
456
                        $statements_analyzer->removeSuppressedIssues([$issue_to_suppress]);
457
                    }
458
                }
459
            }
460
        }
461
462
        foreach ($definitely_newly_assigned_var_ids as $var_id => $_) {
463
            if (isset($context->vars_in_scope[$var_id])) {
464
                $new_type = clone $context->vars_in_scope[$var_id];
465
                $new_type->possibly_undefined_from_try = false;
466
                $context->vars_in_scope[$var_id] = $new_type;
467
            }
468
        }
469
470
        if ($context->loop_scope
471
            && !$try_leaves_loop
472
            && !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true)
473
        ) {
474
            $context->loop_scope->final_actions[] = ScopeAnalyzer::ACTION_NONE;
475
        }
476
477
        $newly_referenced_var_ids = array_diff_key(
478
            $context->referenced_var_ids,
479
            $old_referenced_var_ids
480
        );
481
482
        if ($codebase->find_unused_variables) {
483
            foreach ($old_unreferenced_vars as $var_id => $locations) {
484
                if ((isset($context->unreferenced_vars[$var_id]) && $context->unreferenced_vars[$var_id] !== $locations)
485
                    || (!isset($newly_referenced_var_ids[$var_id]) && isset($possibly_referenced_var_ids[$var_id]))
486
                ) {
487
                    $statements_analyzer->registerVariableUses($locations);
488
                }
489
            }
490
491
            $newly_unreferenced_vars = array_merge(
492
                $newly_unreferenced_vars,
493
                array_diff_key(
494
                    $try_context->unreferenced_vars,
495
                    $old_unreferenced_vars
496
                )
497
            );
498
499
            foreach ($newly_unreferenced_vars as $var_id => $locations) {
500
                if (!isset($context->unreferenced_vars[$var_id])) {
501
                    $context->unreferenced_vars[$var_id] = $locations;
502
                }
503
            }
504
        }
505
506 View Code Duplication
        if ($stmt->finally) {
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...
507
            $suppressed_issues = $statements_analyzer->getSuppressedIssues();
508
509
            foreach ($issues_to_suppress as $issue_to_suppress) {
510
                if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
511
                    $statements_analyzer->addSuppressedIssues([$issue_to_suppress]);
512
                }
513
            }
514
515
            $statements_analyzer->analyze($stmt->finally->stmts, $context);
516
517
            foreach ($issues_to_suppress as $issue_to_suppress) {
518
                if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
519
                    $statements_analyzer->removeSuppressedIssues([$issue_to_suppress]);
520
                }
521
            }
522
        }
523
524
        foreach ($existing_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
525
            foreach ($codelocations as $hash => $codelocation) {
526
                $context->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation;
527
            }
528
        }
529
530
        return null;
531
    }
532
}
533