getKeyValueParamsForTraversableObject()   C
last analyzed

Complexity

Conditions 14
Paths 9

Size

Total Lines 93

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
nc 9
nop 4
dl 0
loc 93
rs 5.166
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\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