ForeachAnalyzer::handleIterable()   F
last analyzed

Complexity

Conditions 55
Paths 5344

Size

Total Lines 259

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 55
nc 5344
nop 8
dl 0
loc 259
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\Codebase;
6
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
7
use Psalm\Internal\Analyzer\CommentAnalyzer;
8
use Psalm\Internal\Analyzer\ScopeAnalyzer;
9
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
10
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
11
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
12
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer;
13
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer;
14
use Psalm\Internal\Analyzer\StatementsAnalyzer;
15
use Psalm\Internal\Analyzer\TypeAnalyzer;
16
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
17
use Psalm\CodeLocation;
18
use Psalm\Context;
19
use Psalm\Exception\DocblockParseException;
20
use Psalm\Issue\InvalidDocblock;
21
use Psalm\Issue\InvalidIterator;
22
use Psalm\Issue\NullIterator;
23
use Psalm\Issue\PossiblyFalseIterator;
24
use Psalm\Issue\PossiblyInvalidIterator;
25
use Psalm\Issue\PossiblyNullIterator;
26
use Psalm\Issue\PossibleRawObjectIteration;
27
use Psalm\Issue\RawObjectIteration;
28
use Psalm\Issue\UnnecessaryVarAnnotation;
29
use Psalm\IssueBuffer;
30
use Psalm\Internal\Scope\LoopScope;
31
use Psalm\Type;
32
use function is_string;
33
use function in_array;
34
use function array_merge;
35
use function array_intersect_key;
36
use function array_values;
37
use function strtolower;
38
use function array_map;
39
use function array_search;
40
use function array_keys;
41
42
/**
43
 * @internal
44
 */
45
class ForeachAnalyzer
46
{
47
    /**
48
     * @param   StatementsAnalyzer               $statements_analyzer
49
     * @param   PhpParser\Node\Stmt\Foreach_    $stmt
50
     * @param   Context                         $context
51
     *
52
     * @return  false|null
53
     */
54
    public static function analyze(
55
        StatementsAnalyzer $statements_analyzer,
56
        PhpParser\Node\Stmt\Foreach_ $stmt,
57
        Context $context
58
    ) {
59
        $var_comments = [];
60
61
        $doc_comment = $stmt->getDocComment();
62
63
        $codebase = $statements_analyzer->getCodebase();
64
65
        if ($doc_comment) {
66
            try {
67
                $var_comments = CommentAnalyzer::getTypeFromComment(
68
                    $doc_comment,
69
                    $statements_analyzer->getSource(),
70
                    $statements_analyzer->getSource()->getAliases(),
71
                    $statements_analyzer->getTemplateTypeMap() ?: []
72
                );
73
            } catch (DocblockParseException $e) {
74
                if (IssueBuffer::accepts(
75
                    new InvalidDocblock(
76
                        (string)$e->getMessage(),
77
                        new CodeLocation($statements_analyzer, $stmt)
78
                    )
79
                )) {
80
                    // fall through
81
                }
82
            }
83
        }
84
85
        $safe_var_ids = [];
86
87
        if ($stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) {
88
            $safe_var_ids['$' . $stmt->keyVar->name] = true;
89
        }
90
91
        if ($stmt->valueVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->valueVar->name)) {
92
            $safe_var_ids['$' . $stmt->valueVar->name] = true;
93
        } elseif ($stmt->valueVar instanceof PhpParser\Node\Expr\List_) {
94
            foreach ($stmt->valueVar->items as $list_item) {
95
                if (!$list_item) {
96
                    continue;
97
                }
98
99
                $list_item_key = $list_item->key;
100
                $list_item_value = $list_item->value;
101
102
                if ($list_item_value instanceof PhpParser\Node\Expr\Variable && is_string($list_item_value->name)) {
103
                    $safe_var_ids['$' . $list_item_value->name] = true;
104
                }
105
106
                if ($list_item_key instanceof PhpParser\Node\Expr\Variable && is_string($list_item_key->name)) {
107
                    $safe_var_ids['$' . $list_item_key->name] = true;
108
                }
109
            }
110
        }
111
112
        foreach ($var_comments as $var_comment) {
113
            if (!$var_comment->var_id || !$var_comment->type) {
114
                continue;
115
            }
116
117
            if (isset($safe_var_ids[$var_comment->var_id])) {
118
                continue;
119
            }
120
121
            $comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
122
                $codebase,
123
                $var_comment->type,
124
                $context->self,
125
                $context->self,
126
                $statements_analyzer->getParentFQCLN()
127
            );
128
129
            $type_location = null;
130
131
            if ($var_comment->type_start
132
                && $var_comment->type_end
133
                && $var_comment->line_number
134
            ) {
135
                $type_location = new CodeLocation\DocblockTypeLocation(
136
                    $statements_analyzer,
137
                    $var_comment->type_start,
138
                    $var_comment->type_end,
139
                    $var_comment->line_number
140
                );
141
142
                if ($codebase->alter_code) {
143
                    $codebase->classlikes->handleDocblockTypeInMigration(
144
                        $codebase,
145
                        $statements_analyzer,
146
                        $comment_type,
147
                        $type_location,
148
                        $context->calling_method_id
149
                    );
150
                }
151
            }
152
153
            if (isset($context->vars_in_scope[$var_comment->var_id])
154
                || VariableFetchAnalyzer::isSuperGlobal($var_comment->var_id)
155
            ) {
156
                if ($codebase->find_unused_variables
157
                    && $doc_comment
158
                    && $type_location
159
                    && isset($context->vars_in_scope[$var_comment->var_id])
160
                    && $context->vars_in_scope[$var_comment->var_id]->getId() === $comment_type->getId()
161
                    && !$comment_type->isMixed()
162
                ) {
163
                    $project_analyzer = $statements_analyzer->getProjectAnalyzer();
164
165
                    if ($codebase->alter_code
166
                        && isset($project_analyzer->getIssuesToFix()['UnnecessaryVarAnnotation'])
167
                    ) {
168
                        FileManipulationBuffer::addVarAnnotationToRemove($type_location);
169
                    } elseif (IssueBuffer::accepts(
170
                        new UnnecessaryVarAnnotation(
171
                            'The @var ' . $comment_type . ' annotation for '
172
                                . $var_comment->var_id . ' is unnecessary',
173
                            $type_location
174
                        ),
175
                        [],
176
                        true
177
                    )) {
178
                        // fall through
179
                    }
180
                }
181
182
                $context->vars_in_scope[$var_comment->var_id] = $comment_type;
183
            }
184
        }
185
186
        $context->inside_assignment = true;
187
        if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
188
            return false;
189
        }
190
        $context->inside_assignment = false;
191
192
        $key_type = null;
193
        $value_type = null;
194
        $always_non_empty_array = true;
195
196
        $var_id = ExpressionIdentifier::getVarId(
197
            $stmt->expr,
198
            $statements_analyzer->getFQCLN(),
199
            $statements_analyzer
200
        );
201
202
        if ($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) {
203
            $iterator_type = $stmt_expr_type;
204
        } elseif ($var_id && $context->hasVariable($var_id, $statements_analyzer)) {
205
            $iterator_type = $context->vars_in_scope[$var_id];
206
        } else {
207
            $iterator_type = null;
208
        }
209
210
        if ($iterator_type) {
211
            if (self::checkIteratorType(
212
                $statements_analyzer,
213
                $stmt,
214
                $iterator_type,
215
                $codebase,
216
                $context,
217
                $key_type,
218
                $value_type,
219
                $always_non_empty_array
220
            ) === false
221
            ) {
222
                return false;
223
            }
224
        }
225
226
        $foreach_context = clone $context;
227
228
        foreach ($foreach_context->vars_in_scope as $context_var_id => $context_type) {
229
            $foreach_context->vars_in_scope[$context_var_id] = clone $context_type;
230
        }
231
232
        $foreach_context->inside_loop = true;
233
        $foreach_context->break_types[] = 'loop';
234
235
        if ($codebase->alter_code) {
236
            $foreach_context->branch_point =
237
                $foreach_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
238
        }
239
240
        if ($stmt->keyVar && $stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) {
241
            $key_var_id = '$' . $stmt->keyVar->name;
242
            $foreach_context->vars_in_scope[$key_var_id] = $key_type ?: Type::getMixed();
243
            $foreach_context->vars_possibly_in_scope[$key_var_id] = true;
244
245
            $location = new CodeLocation($statements_analyzer, $stmt->keyVar);
246
247
            if ($codebase->find_unused_variables && !isset($foreach_context->byref_constraints[$key_var_id])) {
248
                $foreach_context->unreferenced_vars[$key_var_id] = [$location->getHash() => $location];
249
                unset($foreach_context->referenced_var_ids[$key_var_id]);
250
            }
251
252
            if (!$statements_analyzer->hasVariable($key_var_id)) {
253
                $statements_analyzer->registerVariable(
254
                    $key_var_id,
255
                    $location,
256
                    $foreach_context->branch_point
257
                );
258
            } else {
259
                $statements_analyzer->registerVariableAssignment(
260
                    $key_var_id,
261
                    $location
262
                );
263
            }
264
265
            if ($stmt->byRef && $codebase->find_unused_variables) {
266
                $statements_analyzer->registerVariableUses([$location->getHash() => $location]);
267
            }
268
        }
269
270
        if ($codebase->find_unused_variables
271
            && $stmt->byRef
272
            && $stmt->valueVar instanceof PhpParser\Node\Expr\Variable
273
            && is_string($stmt->valueVar->name)
274
        ) {
275
            $foreach_context->byref_constraints['$' . $stmt->valueVar->name]
276
                = new \Psalm\Internal\ReferenceConstraint($value_type);
277
        }
278
279
        AssignmentAnalyzer::analyze(
280
            $statements_analyzer,
281
            $stmt->valueVar,
282
            null,
283
            $value_type ?: Type::getMixed(),
284
            $foreach_context,
285
            $doc_comment
286
        );
287
288
        foreach ($var_comments as $var_comment) {
289
            if (!$var_comment->var_id || !$var_comment->type) {
290
                continue;
291
            }
292
293
            $comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
294
                $codebase,
295
                $var_comment->type,
296
                $context->self,
297
                $context->self,
298
                $statements_analyzer->getParentFQCLN()
299
            );
300
301
            $foreach_context->vars_in_scope[$var_comment->var_id] = $comment_type;
302
        }
303
304
        $loop_scope = new LoopScope($foreach_context, $context);
305
306
        $loop_scope->protected_var_ids = $context->protected_var_ids;
307
308
        LoopAnalyzer::analyze(
309
            $statements_analyzer,
310
            $stmt->stmts,
311
            [],
312
            [],
313
            $loop_scope,
314
            $inner_loop_context,
315
            false,
316
            $always_non_empty_array
317
        );
318
319
        if (!$inner_loop_context) {
320
            throw new \UnexpectedValueException('There should be an inner loop context');
321
        }
322
323
        $foreach_context->loop_scope = null;
324
325
        $context->vars_possibly_in_scope = array_merge(
326
            $foreach_context->vars_possibly_in_scope,
327
            $context->vars_possibly_in_scope
328
        );
329
330
        $context->referenced_var_ids = array_intersect_key(
331
            $foreach_context->referenced_var_ids,
332
            $context->referenced_var_ids
333
        );
334
335
        if ($context->collect_exceptions) {
336
            $context->mergeExceptions($foreach_context);
337
        }
338
339
        if ($codebase->find_unused_variables) {
340
            foreach ($foreach_context->unreferenced_vars as $var_id => $locations) {
341
                if (isset($context->unreferenced_vars[$var_id])) {
342
                    $context->unreferenced_vars[$var_id] += $locations;
343
                } else {
344
                    $context->unreferenced_vars[$var_id] = $locations;
345
                }
346
            }
347
        }
348
349
        return null;
350
    }
351
352
    /**
353
     * @param  ?Type\Union  $key_type
0 ignored issues
show
Documentation introduced by
The doc-type ?Type\Union could not be parsed: Unknown type name "?Type\Union" 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...
354
     * @param  ?Type\Union  $value_type
0 ignored issues
show
Documentation introduced by
The doc-type ?Type\Union could not be parsed: Unknown type name "?Type\Union" 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...
355
     * @return false|null
356
     */
357
    public static function checkIteratorType(
358
        StatementsAnalyzer $statements_analyzer,
359
        PhpParser\Node\Stmt\Foreach_ $stmt,
360
        Type\Union $iterator_type,
361
        Codebase $codebase,
362
        Context $context,
363
        &$key_type,
364
        &$value_type,
365
        bool &$always_non_empty_array
366
    ) {
367
        if ($iterator_type->isNull()) {
368
            if (IssueBuffer::accepts(
369
                new NullIterator(
370
                    'Cannot iterate over null',
371
                    new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
372
                ),
373
                $statements_analyzer->getSuppressedIssues()
374
            )) {
375
                return false;
376
            }
377
        } elseif ($iterator_type->isNullable() && !$iterator_type->ignore_nullable_issues) {
378
            if (IssueBuffer::accepts(
379
                new PossiblyNullIterator(
380
                    'Cannot iterate over nullable var ' . $iterator_type,
381
                    new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
382
                ),
383
                $statements_analyzer->getSuppressedIssues()
384
            )) {
385
                return false;
386
            }
387
        } elseif ($iterator_type->isFalsable() && !$iterator_type->ignore_falsable_issues) {
388
            if (IssueBuffer::accepts(
389
                new PossiblyFalseIterator(
390
                    'Cannot iterate over falsable var ' . $iterator_type,
391
                    new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
392
                ),
393
                $statements_analyzer->getSuppressedIssues()
394
            )) {
395
                return false;
396
            }
397
        }
398
399
        $has_valid_iterator = false;
400
        $invalid_iterator_types = [];
401
        $raw_object_types = [];
402
403
        foreach ($iterator_type->getAtomicTypes() as $iterator_atomic_type) {
404
            if ($iterator_atomic_type instanceof Type\Atomic\TTemplateParam) {
405
                $iterator_atomic_type = array_values($iterator_atomic_type->as->getAtomicTypes())[0];
406
            }
407
408
            // if it's an empty array, we cannot iterate over it
409
            if ($iterator_atomic_type instanceof Type\Atomic\TArray
410
                && $iterator_atomic_type->type_params[1]->isEmpty()
411
            ) {
412
                $always_non_empty_array = false;
413
                $has_valid_iterator = true;
414
                continue;
415
            }
416
417
            if ($iterator_atomic_type instanceof Type\Atomic\TNull
418
                || $iterator_atomic_type instanceof Type\Atomic\TFalse
419
            ) {
420
                $always_non_empty_array = false;
421
                continue;
422
            }
423
424
            if ($iterator_atomic_type instanceof Type\Atomic\TArray
425
                || $iterator_atomic_type instanceof Type\Atomic\ObjectLike
426
                || $iterator_atomic_type instanceof Type\Atomic\TList
427
            ) {
428
                if ($iterator_atomic_type instanceof Type\Atomic\ObjectLike) {
429
                    if (!$iterator_atomic_type->sealed) {
430
                        $always_non_empty_array = false;
431
                    }
432
                    $iterator_atomic_type = $iterator_atomic_type->getGenericArrayType();
433
                } elseif ($iterator_atomic_type instanceof Type\Atomic\TList) {
434
                    if (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyList) {
435
                        $always_non_empty_array = false;
436
                    }
437
438
                    $iterator_atomic_type = new Type\Atomic\TArray([
439
                        Type::getInt(),
440
                        $iterator_atomic_type->type_param
441
                    ]);
442
                } elseif (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyArray) {
443
                    $always_non_empty_array = false;
444
                }
445
446
                if (!$value_type) {
447
                    $value_type = clone $iterator_atomic_type->type_params[1];
448
                } else {
449
                    $value_type = Type::combineUnionTypes($value_type, $iterator_atomic_type->type_params[1]);
450
                }
451
452
                ArrayFetchAnalyzer::taintArrayFetch(
453
                    $statements_analyzer,
454
                    $stmt->expr,
455
                    null,
456
                    $value_type,
457
                    Type::getMixed()
458
                );
459
460
                $key_type_part = $iterator_atomic_type->type_params[0];
461
462
                if (!$key_type) {
463
                    $key_type = $key_type_part;
464
                } else {
465
                    $key_type = Type::combineUnionTypes($key_type, $key_type_part);
466
                }
467
468
                ArrayFetchAnalyzer::taintArrayFetch(
469
                    $statements_analyzer,
470
                    $stmt->expr,
471
                    null,
472
                    $key_type,
473
                    Type::getMixed()
474
                );
475
476
                $has_valid_iterator = true;
477
                continue;
478
            }
479
480
            $always_non_empty_array = false;
481
482
            if ($iterator_atomic_type instanceof Type\Atomic\Scalar ||
483
                $iterator_atomic_type instanceof Type\Atomic\TVoid
484
            ) {
485
                $invalid_iterator_types[] = $iterator_atomic_type->getKey();
486
487
                $value_type = Type::getMixed();
488
            } elseif ($iterator_atomic_type instanceof Type\Atomic\TObject ||
489
                $iterator_atomic_type instanceof Type\Atomic\TMixed ||
490
                $iterator_atomic_type instanceof Type\Atomic\TEmpty
491
            ) {
492
                $has_valid_iterator = true;
493
                $value_type = Type::getMixed();
494
495
                ArrayFetchAnalyzer::taintArrayFetch(
496
                    $statements_analyzer,
497
                    $stmt->expr,
498
                    null,
499
                    $value_type,
500
                    Type::getMixed()
501
                );
502
            } elseif ($iterator_atomic_type instanceof Type\Atomic\TIterable) {
503
                if ($iterator_atomic_type->extra_types) {
504
                    $iterator_atomic_type_copy = clone $iterator_atomic_type;
505
                    $iterator_atomic_type_copy->extra_types = [];
506
                    $iterator_atomic_types = [$iterator_atomic_type_copy];
507
                    $iterator_atomic_types = array_merge(
508
                        $iterator_atomic_types,
509
                        $iterator_atomic_type->extra_types
510
                    );
511
                } else {
512
                    $iterator_atomic_types = [$iterator_atomic_type];
513
                }
514
515
                $intersection_value_type = null;
516
                $intersection_key_type = null;
517
518
                foreach ($iterator_atomic_types as $iat) {
519
                    if (!$iat instanceof Type\Atomic\TIterable) {
520
                        continue;
521
                    }
522
523
                    $value_type_part = $iat->type_params[1];
524
                    $key_type_part = $iat->type_params[0];
525
526
                    if (!$intersection_value_type) {
527
                        $intersection_value_type = $value_type_part;
528
                    } else {
529
                        $intersection_value_type = Type::intersectUnionTypes(
530
                            $intersection_value_type,
531
                            $value_type_part,
532
                            $codebase
533
                        ) ?: Type::getMixed();
534
                    }
535
536
                    if (!$intersection_key_type) {
537
                        $intersection_key_type = $key_type_part;
538
                    } else {
539
                        $intersection_key_type = Type::intersectUnionTypes(
540
                            $intersection_key_type,
541
                            $key_type_part,
542
                            $codebase
543
                        ) ?: Type::getMixed();
544
                    }
545
                }
546
547
                if (!$intersection_value_type || !$intersection_key_type) {
548
                    throw new \UnexpectedValueException('Should not happen');
549
                }
550
551
                if (!$value_type) {
552
                    $value_type = $intersection_value_type;
553
                } else {
554
                    $value_type = Type::combineUnionTypes($value_type, $intersection_value_type);
555
                }
556
557
                if (!$key_type) {
558
                    $key_type = $intersection_key_type;
559
                } else {
560
                    $key_type = Type::combineUnionTypes($key_type, $intersection_key_type);
561
                }
562
563
                $has_valid_iterator = true;
564
            } elseif ($iterator_atomic_type instanceof Type\Atomic\TNamedObject) {
565
                if ($iterator_atomic_type->value !== 'Traversable' &&
566
                    $iterator_atomic_type->value !== $statements_analyzer->getClassName()
567
                ) {
568
                    if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
569
                        $statements_analyzer,
570
                        $iterator_atomic_type->value,
571
                        new CodeLocation($statements_analyzer->getSource(), $stmt->expr),
572
                        $context->self,
573
                        $context->calling_method_id,
574
                        $statements_analyzer->getSuppressedIssues()
575
                    ) === false) {
576
                        return false;
577
                    }
578
                }
579
580
                if (TypeAnalyzer::isAtomicContainedBy(
581
                    $codebase,
582
                    $iterator_atomic_type,
583
                    new Type\Atomic\TIterable([Type::getMixed(), Type::getMixed()])
584
                )) {
585
                    self::handleIterable(
586
                        $statements_analyzer,
587
                        $iterator_atomic_type,
588
                        $stmt->expr,
589
                        $codebase,
590
                        $context,
591
                        $key_type,
592
                        $value_type,
593
                        $has_valid_iterator
594
                    );
595
                } else {
596
                    $raw_object_types[] = $iterator_atomic_type->value;
597
                }
598
            }
599
        }
600
601
        if ($raw_object_types) {
602
            if ($has_valid_iterator) {
603
                if (IssueBuffer::accepts(
604
                    new PossibleRawObjectIteration(
605
                        'Possibly undesired iteration over regular object ' . \reset($raw_object_types),
606
                        new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
607
                    ),
608
                    $statements_analyzer->getSuppressedIssues()
609
                )) {
610
                    // fall through
611
                }
612
            } else {
613
                if (IssueBuffer::accepts(
614
                    new RawObjectIteration(
615
                        'Possibly undesired iteration over regular object ' . \reset($raw_object_types),
616
                        new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
617
                    ),
618
                    $statements_analyzer->getSuppressedIssues()
619
                )) {
620
                    // fall through
621
                }
622
            }
623
        }
624
625
        if ($invalid_iterator_types) {
626
            if ($has_valid_iterator) {
627
                if (IssueBuffer::accepts(
628
                    new PossiblyInvalidIterator(
629
                        'Cannot iterate over ' . $invalid_iterator_types[0],
630
                        new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
631
                    ),
632
                    $statements_analyzer->getSuppressedIssues()
633
                )) {
634
                    // fall through
635
                }
636
            } else {
637
                if (IssueBuffer::accepts(
638
                    new InvalidIterator(
639
                        'Cannot iterate over ' . $invalid_iterator_types[0],
640
                        new CodeLocation($statements_analyzer->getSource(), $stmt->expr)
641
                    ),
642
                    $statements_analyzer->getSuppressedIssues()
643
                )) {
644
                    // fall through
645
                }
646
            }
647
        }
648
    }
649
650
    /**
651
     * @param  ?Type\Union  $key_type
0 ignored issues
show
Documentation introduced by
The doc-type ?Type\Union could not be parsed: Unknown type name "?Type\Union" 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...
652
     * @param  ?Type\Union  $value_type
0 ignored issues
show
Documentation introduced by
The doc-type ?Type\Union could not be parsed: Unknown type name "?Type\Union" 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...
653
     * @return void
654
     */
655
    public static function handleIterable(
656
        StatementsAnalyzer $statements_analyzer,
657
        Type\Atomic\TNamedObject $iterator_atomic_type,
658
        PhpParser\Node\Expr $foreach_expr,
659
        Codebase $codebase,
660
        Context $context,
661
        &$key_type,
662
        &$value_type,
663
        bool &$has_valid_iterator
664
    ) {
665
        if ($iterator_atomic_type->extra_types) {
666
            $iterator_atomic_type_copy = clone $iterator_atomic_type;
667
            $iterator_atomic_type_copy->extra_types = [];
668
            $iterator_atomic_types = [$iterator_atomic_type_copy];
669
            $iterator_atomic_types = array_merge($iterator_atomic_types, $iterator_atomic_type->extra_types);
670
        } else {
671
            $iterator_atomic_types = [$iterator_atomic_type];
672
        }
673
674
        foreach ($iterator_atomic_types as $iterator_atomic_type) {
675
            if ($iterator_atomic_type instanceof Type\Atomic\TTemplateParam
676
                || $iterator_atomic_type instanceof Type\Atomic\TObjectWithProperties
677
            ) {
678
                throw new \UnexpectedValueException('Shouldn’t get a generic param here');
679
            }
680
681
682
            $has_valid_iterator = true;
683
684
            if ($iterator_atomic_type instanceof Type\Atomic\TNamedObject
685
                && strtolower($iterator_atomic_type->value) === 'simplexmlelement'
686
            ) {
687
                if ($value_type) {
688
                    $value_type = Type::combineUnionTypes(
689
                        $value_type,
690
                        new Type\Union([clone $iterator_atomic_type])
691
                    );
692
                } else {
693
                    $value_type = new Type\Union([clone $iterator_atomic_type]);
694
                }
695
696
                if ($key_type) {
697
                    $key_type = Type::combineUnionTypes(
698
                        $key_type,
699
                        Type::getString()
700
                    );
701
                } else {
702
                    $key_type = Type::getString();
703
                }
704
            }
705
706
            if ($iterator_atomic_type instanceof Type\Atomic\TIterable
707
                || (strtolower($iterator_atomic_type->value) === 'traversable'
708
                    || $codebase->classImplements(
709
                        $iterator_atomic_type->value,
710
                        'Traversable'
711
                    ) ||
712
                    (
713
                        $codebase->interfaceExists($iterator_atomic_type->value)
714
                        && $codebase->interfaceExtends(
715
                            $iterator_atomic_type->value,
716
                            'Traversable'
717
                        )
718
                    ))
719
            ) {
720
                if (strtolower($iterator_atomic_type->value) === 'iteratoraggregate'
721
                    || $codebase->classImplements(
722
                        $iterator_atomic_type->value,
723
                        'IteratorAggregate'
724
                    )
725
                    || ($codebase->interfaceExists($iterator_atomic_type->value)
726
                        && $codebase->interfaceExtends(
727
                            $iterator_atomic_type->value,
728
                            'IteratorAggregate'
729
                        )
730
                    )
731
                ) {
732
                    $old_data_provider = $statements_analyzer->node_data;
733
734
                    $statements_analyzer->node_data = clone $statements_analyzer->node_data;
735
736
                    $fake_method_call = new PhpParser\Node\Expr\MethodCall(
737
                        $foreach_expr,
738
                        new PhpParser\Node\Identifier('getIterator', $foreach_expr->getAttributes())
739
                    );
740
741
                    $suppressed_issues = $statements_analyzer->getSuppressedIssues();
742
743
                    if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
744
                        $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
745
                    }
746
747
                    if (!in_array('PossiblyUndefinedMethod', $suppressed_issues, true)) {
748
                        $statements_analyzer->addSuppressedIssues(['PossiblyUndefinedMethod']);
749
                    }
750
751
                    \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
752
                        $statements_analyzer,
753
                        $fake_method_call,
754
                        $context
755
                    );
756
757
                    if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
758
                        $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
759
                    }
760
761
                    if (!in_array('PossiblyUndefinedMethod', $suppressed_issues, true)) {
762
                        $statements_analyzer->removeSuppressedIssues(['PossiblyUndefinedMethod']);
763
                    }
764
765
                    $iterator_class_type = $statements_analyzer->node_data->getType($fake_method_call) ?: null;
766
767
                    $statements_analyzer->node_data = $old_data_provider;
768
769
                    if ($iterator_class_type) {
770
                        foreach ($iterator_class_type->getAtomicTypes() as $array_atomic_type) {
771
                            $key_type_part = null;
772
                            $value_type_part = null;
773
774
                            if ($array_atomic_type instanceof Type\Atomic\TArray
775
                                || $array_atomic_type instanceof Type\Atomic\ObjectLike
776
                            ) {
777
                                if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
778
                                    $array_atomic_type = $array_atomic_type->getGenericArrayType();
779
                                }
780
781
                                $key_type_part = $array_atomic_type->type_params[0];
782
                                $value_type_part = $array_atomic_type->type_params[1];
783
                            } else {
784
                                if ($array_atomic_type instanceof Type\Atomic\TNamedObject
785
                                    && $codebase->classExists($array_atomic_type->value)
786
                                    && $codebase->classImplements(
787
                                        $array_atomic_type->value,
788
                                        'Traversable'
789
                                    )
790
                                ) {
791
                                    $generic_storage = $codebase->classlike_storage_provider->get(
792
                                        $array_atomic_type->value
793
                                    );
794
795
                                    // The collection might be an iterator, in which case
796
                                    // we want to call the iterator function
797
                                    /** @psalm-suppress PossiblyUndefinedStringArrayOffset */
798
                                    if (!isset($generic_storage->template_type_extends['Traversable'])
799
                                        || ($generic_storage
800
                                                ->template_type_extends['Traversable']['TKey']->isMixed()
801
                                            && $generic_storage
802
                                                ->template_type_extends['Traversable']['TValue']->isMixed())
803
                                    ) {
804
                                        self::handleIterable(
805
                                            $statements_analyzer,
806
                                            $array_atomic_type,
807
                                            $fake_method_call,
808
                                            $codebase,
809
                                            $context,
810
                                            $key_type,
811
                                            $value_type,
812
                                            $has_valid_iterator
813
                                        );
814
815
                                        continue;
816
                                    }
817
                                }
818
819
                                if ($array_atomic_type instanceof Type\Atomic\TIterable
820
                                    || ($array_atomic_type instanceof Type\Atomic\TNamedObject
821
                                        && ($array_atomic_type->value === 'Traversable'
822
                                            || ($codebase->classOrInterfaceExists($array_atomic_type->value)
823
                                                && $codebase->classImplements(
824
                                                    $array_atomic_type->value,
825
                                                    'Traversable'
826
                                                ))))
827
                                ) {
828
                                    self::getKeyValueParamsForTraversableObject(
829
                                        $array_atomic_type,
830
                                        $codebase,
831
                                        $key_type_part,
832
                                        $value_type_part
833
                                    );
834
                                }
835
                            }
836
837
                            if (!$key_type_part || !$value_type_part) {
838
                                break;
839
                            }
840
841
                            if (!$key_type) {
842
                                $key_type = $key_type_part;
843
                            } else {
844
                                $key_type = Type::combineUnionTypes($key_type, $key_type_part);
845
                            }
846
847
                            if (!$value_type) {
848
                                $value_type = $value_type_part;
849
                            } else {
850
                                $value_type = Type::combineUnionTypes($value_type, $value_type_part);
851
                            }
852
                        }
853
                    }
854
                } elseif ($codebase->classImplements(
855
                    $iterator_atomic_type->value,
856
                    'Iterator'
857
                ) ||
858
                    (
859
                        $codebase->interfaceExists($iterator_atomic_type->value)
860
                        && $codebase->interfaceExtends(
861
                            $iterator_atomic_type->value,
862
                            'Iterator'
863
                        )
864
                    )
865
                ) {
866
                    $iterator_value_type = self::getFakeMethodCallType(
867
                        $statements_analyzer,
868
                        $foreach_expr,
869
                        $context,
870
                        'current'
871
                    );
872
873
                    $iterator_key_type = self::getFakeMethodCallType(
874
                        $statements_analyzer,
875
                        $foreach_expr,
876
                        $context,
877
                        'key'
878
                    );
879
880
                    if ($iterator_value_type && !$iterator_value_type->isMixed()) {
881
                        if (!$value_type) {
882
                            $value_type = $iterator_value_type;
883
                        } else {
884
                            $value_type = Type::combineUnionTypes($value_type, $iterator_value_type);
885
                        }
886
                    }
887
888
                    if ($iterator_key_type && !$iterator_key_type->isMixed()) {
889
                        if (!$key_type) {
890
                            $key_type = $iterator_key_type;
891
                        } else {
892
                            $key_type = Type::combineUnionTypes($key_type, $iterator_key_type);
893
                        }
894
                    }
895
                }
896
897
                if (!$key_type && !$value_type) {
898
                    self::getKeyValueParamsForTraversableObject(
899
                        $iterator_atomic_type,
900
                        $codebase,
901
                        $key_type,
902
                        $value_type
903
                    );
904
                }
905
906
                return;
907
            }
908
909
            if (!$codebase->classlikes->classOrInterfaceExists($iterator_atomic_type->value)) {
910
                return;
911
            }
912
        }
913
    }
914
915
    /**
916
     * @param  ?Type\Union  $key_type
0 ignored issues
show
Documentation introduced by
The doc-type ?Type\Union could not be parsed: Unknown type name "?Type\Union" 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...
917
     * @param  ?Type\Union  $value_type
0 ignored issues
show
Documentation introduced by
The doc-type ?Type\Union could not be parsed: Unknown type name "?Type\Union" 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...
918
     * @return void
919
     */
920
    public static function getKeyValueParamsForTraversableObject(
921
        Type\Atomic $iterator_atomic_type,
922
        Codebase $codebase,
923
        &$key_type,
924
        &$value_type
925
    ) {
926
        if ($iterator_atomic_type instanceof Type\Atomic\TIterable
927
            || ($iterator_atomic_type instanceof Type\Atomic\TGenericObject
928
                && strtolower($iterator_atomic_type->value) === 'traversable')
929
        ) {
930
            $value_type_part = $iterator_atomic_type->type_params[1];
931
932
            if (!$value_type) {
933
                $value_type = $value_type_part;
934
            } else {
935
                $value_type = Type::combineUnionTypes($value_type, $value_type_part);
936
            }
937
938
            $key_type_part = $iterator_atomic_type->type_params[0];
939
940
            if (!$key_type) {
941
                $key_type = $key_type_part;
942
            } else {
943
                $key_type = Type::combineUnionTypes($key_type, $key_type_part);
944
            }
945
            return;
946
        }
947
948
        if ($iterator_atomic_type instanceof Type\Atomic\TNamedObject
949
            && (
950
                $codebase->classImplements(
951
                    $iterator_atomic_type->value,
952
                    'Traversable'
953
                )
954
                || $codebase->interfaceExtends(
955
                    $iterator_atomic_type->value,
956
                    'Traversable'
957
                )
958
            )
959
        ) {
960
            $generic_storage = $codebase->classlike_storage_provider->get(
961
                $iterator_atomic_type->value
962
            );
963
964
            if (!isset($generic_storage->template_type_extends['Traversable'])) {
965
                return;
966
            }
967
968
            if ($generic_storage->template_types
969
                || $iterator_atomic_type instanceof Type\Atomic\TGenericObject
970
            ) {
971
                // if we're just being passed the non-generic class itself, assume
972
                // that it's inside the calling class
973
                $passed_type_params = $iterator_atomic_type instanceof Type\Atomic\TGenericObject
974
                    ? $iterator_atomic_type->type_params
975
                    : array_values(
976
                        array_map(
977
                            /** @param array<string, array{0:Type\Union}> $arr */
978
                            function (array $arr) use ($iterator_atomic_type) : Type\Union {
979
                                if (isset($arr[$iterator_atomic_type->value])) {
980
                                    return $arr[$iterator_atomic_type->value][0];
981
                                }
982
983
                                return Type::getMixed();
984
                            },
985
                            $generic_storage->template_types
986
                        )
987
                    );
988
            } else {
989
                $passed_type_params = null;
990
            }
991
992
            $key_type = self::getExtendedType(
993
                'TKey',
994
                'Traversable',
995
                $generic_storage->name,
996
                $generic_storage->template_type_extends,
997
                $generic_storage->template_types,
998
                $passed_type_params
999
            );
1000
1001
            $value_type = self::getExtendedType(
1002
                'TValue',
1003
                'Traversable',
1004
                $generic_storage->name,
1005
                $generic_storage->template_type_extends,
1006
                $generic_storage->template_types,
1007
                $passed_type_params
1008
            );
1009
1010
            return;
1011
        }
1012
    }
1013
1014
    private static function getFakeMethodCallType(
1015
        StatementsAnalyzer $statements_analyzer,
1016
        PhpParser\Node\Expr $foreach_expr,
1017
        Context $context,
1018
        string $method_name
1019
    ) : ?Type\Union {
1020
        $old_data_provider = $statements_analyzer->node_data;
1021
1022
        $statements_analyzer->node_data = clone $statements_analyzer->node_data;
1023
1024
        $fake_method_call = new PhpParser\Node\Expr\MethodCall(
1025
            $foreach_expr,
1026
            new PhpParser\Node\Identifier($method_name, $foreach_expr->getAttributes())
1027
        );
1028
1029
        $suppressed_issues = $statements_analyzer->getSuppressedIssues();
1030
1031
        if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
1032
            $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
1033
        }
1034
1035
        if (!in_array('PossiblyUndefinedMethod', $suppressed_issues, true)) {
1036
            $statements_analyzer->addSuppressedIssues(['PossiblyUndefinedMethod']);
1037
        }
1038
1039
        $was_inside_call = $context->inside_call;
1040
1041
        $context->inside_call = true;
1042
1043
        \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
1044
            $statements_analyzer,
1045
            $fake_method_call,
1046
            $context
1047
        );
1048
1049
        $context->inside_call = $was_inside_call;
1050
1051
        if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
1052
            $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
1053
        }
1054
1055
        if (!in_array('PossiblyUndefinedMethod', $suppressed_issues, true)) {
1056
            $statements_analyzer->removeSuppressedIssues(['PossiblyUndefinedMethod']);
1057
        }
1058
1059
        $iterator_class_type = $statements_analyzer->node_data->getType($fake_method_call) ?: null;
1060
1061
        $statements_analyzer->node_data = $old_data_provider;
1062
1063
        return $iterator_class_type;
1064
    }
1065
1066
    /**
1067
     * @param  string $template_name
1068
     * @param  array<string, array<int|string, Type\Union>>  $template_type_extends
1069
     * @param  array<string, array<string, array{Type\Union}>>  $class_template_types
1070
     * @param  array<int, Type\Union> $calling_type_params
1071
     * @return Type\Union|null
1072
     */
1073
    private static function getExtendedType(
1074
        string $template_name,
1075
        string $template_class,
1076
        string $calling_class,
1077
        array $template_type_extends,
1078
        array $class_template_types = null,
1079
        array $calling_type_params = null
1080
    ) {
1081
        if ($calling_class === $template_class) {
1082
            if (isset($class_template_types[$template_name]) && $calling_type_params) {
1083
                $offset = array_search($template_name, array_keys($class_template_types));
1084
1085
                if ($offset !== false && isset($calling_type_params[$offset])) {
1086
                    return $calling_type_params[$offset];
1087
                }
1088
            }
1089
1090
            return null;
1091
        }
1092
1093
        if (isset($template_type_extends[$template_class][$template_name])) {
1094
            $extended_type = $template_type_extends[$template_class][$template_name];
1095
1096
            $return_type = null;
1097
1098
            foreach ($extended_type->getAtomicTypes() as $extended_atomic_type) {
1099
                if (!$extended_atomic_type instanceof Type\Atomic\TTemplateParam) {
1100
                    if (!$return_type) {
1101
                        $return_type = $extended_type;
1102
                    } else {
1103
                        $return_type = Type::combineUnionTypes(
1104
                            $return_type,
1105
                            $extended_type
1106
                        );
1107
                    }
1108
1109
                    continue;
1110
                }
1111
1112
                $candidate_type = self::getExtendedType(
1113
                    $extended_atomic_type->param_name,
1114
                    $extended_atomic_type->defining_class,
1115
                    $calling_class,
1116
                    $template_type_extends,
1117
                    $class_template_types,
1118
                    $calling_type_params
1119
                );
1120
1121
                if ($candidate_type) {
1122
                    if (!$return_type) {
1123
                        $return_type = $candidate_type;
1124
                    } else {
1125
                        $return_type = Type::combineUnionTypes(
1126
                            $return_type,
1127
                            $candidate_type
1128
                        );
1129
                    }
1130
                }
1131
            }
1132
1133
            if ($return_type) {
1134
                return $return_type;
1135
            }
1136
        }
1137
1138
        return null;
1139
    }
1140
}
1141