ArrayFetchAnalyzer   F
last analyzed

Complexity

Total Complexity 353

Size/Duplication

Total Lines 1560
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 63

Importance

Changes 0
Metric Value
dl 0
loc 1560
rs 0.8
c 0
b 0
f 0
wmc 353
lcom 1
cbo 63

6 Methods

Rating   Name   Duplication   Size   Complexity  
F analyze() 0 240 58
B taintArrayFetch() 0 45 11
F getArrayAccessTypeGivenOffset() 0 1120 251
B checkLiteralIntArrayOffset() 0 48 11
C checkLiteralStringArrayOffset() 0 48 12
B replaceOffsetTypeWithInts() 0 40 10

How to fix   Complexity   

Complex Class

Complex classes like ArrayFetchAnalyzer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArrayFetchAnalyzer, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\Fetch;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
6
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
7
use Psalm\Internal\Analyzer\StatementsAnalyzer;
8
use Psalm\Internal\Analyzer\TypeAnalyzer;
9
use Psalm\CodeLocation;
10
use Psalm\Context;
11
use Psalm\Issue\EmptyArrayAccess;
12
use Psalm\Issue\InvalidArrayAccess;
13
use Psalm\Issue\InvalidArrayAssignment;
14
use Psalm\Issue\InvalidArrayOffset;
15
use Psalm\Issue\MixedArrayAccess;
16
use Psalm\Issue\MixedArrayAssignment;
17
use Psalm\Issue\MixedArrayOffset;
18
use Psalm\Issue\MixedStringOffsetAssignment;
19
use Psalm\Issue\MixedArrayTypeCoercion;
20
use Psalm\Issue\NullArrayAccess;
21
use Psalm\Issue\NullArrayOffset;
22
use Psalm\Issue\PossiblyInvalidArrayAccess;
23
use Psalm\Issue\PossiblyInvalidArrayAssignment;
24
use Psalm\Issue\PossiblyInvalidArrayOffset;
25
use Psalm\Issue\PossiblyNullArrayAccess;
26
use Psalm\Issue\PossiblyNullArrayAssignment;
27
use Psalm\Issue\PossiblyNullArrayOffset;
28
use Psalm\Issue\PossiblyUndefinedArrayOffset;
29
use Psalm\Issue\PossiblyUndefinedIntArrayOffset;
30
use Psalm\Issue\PossiblyUndefinedStringArrayOffset;
31
use Psalm\IssueBuffer;
32
use Psalm\Type;
33
use Psalm\Type\Atomic\ObjectLike;
34
use Psalm\Type\Atomic\TArray;
35
use Psalm\Type\Atomic\TArrayKey;
36
use Psalm\Type\Atomic\TClassStringMap;
37
use Psalm\Type\Atomic\TEmpty;
38
use Psalm\Type\Atomic\TLiteralInt;
39
use Psalm\Type\Atomic\TLiteralString;
40
use Psalm\Type\Atomic\TTemplateParam;
41
use Psalm\Type\Atomic\TInt;
42
use Psalm\Type\Atomic\TList;
43
use Psalm\Type\Atomic\TMixed;
44
use Psalm\Type\Atomic\TNamedObject;
45
use Psalm\Type\Atomic\TNonEmptyArray;
46
use Psalm\Type\Atomic\TNonEmptyList;
47
use Psalm\Type\Atomic\TNull;
48
use Psalm\Type\Atomic\TSingleLetter;
49
use Psalm\Type\Atomic\TString;
50
use function array_values;
51
use function array_keys;
52
use function count;
53
use function array_pop;
54
use function implode;
55
use function strlen;
56
use function strtolower;
57
use function in_array;
58
use function is_int;
59
use function preg_match;
60
use Psalm\Internal\Type\TemplateResult;
61
62
/**
63
 * @internal
64
 */
65
class ArrayFetchAnalyzer
66
{
67
    public static function analyze(
68
        StatementsAnalyzer $statements_analyzer,
69
        PhpParser\Node\Expr\ArrayDimFetch $stmt,
70
        Context $context
71
    ) : bool {
72
        $array_var_id = ExpressionIdentifier::getArrayVarId(
73
            $stmt->var,
74
            $statements_analyzer->getFQCLN(),
75
            $statements_analyzer
76
        );
77
78
        if ($stmt->dim && ExpressionAnalyzer::analyze($statements_analyzer, $stmt->dim, $context) === false) {
79
            return false;
80
        }
81
82
        $keyed_array_var_id = ExpressionIdentifier::getArrayVarId(
83
            $stmt,
84
            $statements_analyzer->getFQCLN(),
85
            $statements_analyzer
86
        );
87
88
        $dim_var_id = null;
89
        $new_offset_type = null;
90
91
        if ($stmt->dim) {
92
            $used_key_type = $statements_analyzer->node_data->getType($stmt->dim) ?: Type::getMixed();
93
94
            $dim_var_id = ExpressionIdentifier::getArrayVarId(
95
                $stmt->dim,
96
                $statements_analyzer->getFQCLN(),
97
                $statements_analyzer
98
            );
99
        } else {
100
            $used_key_type = Type::getInt();
101
        }
102
103
        if (ExpressionAnalyzer::analyze(
104
            $statements_analyzer,
105
            $stmt->var,
106
            $context
107
        ) === false) {
108
            return false;
109
        }
110
111
        $stmt_var_type = $statements_analyzer->node_data->getType($stmt->var);
112
113
        $codebase = $statements_analyzer->getCodebase();
114
115
        if ($keyed_array_var_id
116
            && $context->hasVariable($keyed_array_var_id)
117
            && !$context->vars_in_scope[$keyed_array_var_id]->possibly_undefined
118
            && $stmt_var_type
119
            && !$stmt_var_type->hasClassStringMap()
120
        ) {
121
            $stmt_type = clone $context->vars_in_scope[$keyed_array_var_id];
122
123
            $statements_analyzer->node_data->setType(
124
                $stmt,
125
                $stmt_type
126
            );
127
128
            self::taintArrayFetch(
129
                $statements_analyzer,
130
                $stmt->var,
131
                $keyed_array_var_id,
132
                $stmt_type,
133
                $used_key_type
134
            );
135
136
            return true;
137
        }
138
139
        $can_store_result = false;
140
141
        if ($stmt_var_type) {
142
            if ($stmt_var_type->isNull()) {
143
                if (!$context->inside_isset) {
144
                    if (IssueBuffer::accepts(
145
                        new NullArrayAccess(
146
                            'Cannot access array value on null variable ' . $array_var_id,
147
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
148
                        ),
149
                        $statements_analyzer->getSuppressedIssues()
150
                    )) {
151
                        // fall through
152
                    }
153
                }
154
155
                if ($stmt_type = $statements_analyzer->node_data->getType($stmt)) {
156
                    $statements_analyzer->node_data->setType(
157
                        $stmt,
158
                        Type::combineUnionTypes($stmt_type, Type::getNull())
159
                    );
160
                } else {
161
                    $statements_analyzer->node_data->setType($stmt, Type::getNull());
162
                }
163
164
                return true;
165
            }
166
167
            $stmt_type = self::getArrayAccessTypeGivenOffset(
168
                $statements_analyzer,
169
                $stmt,
170
                $stmt_var_type,
171
                $used_key_type,
172
                false,
173
                $array_var_id,
174
                $context,
175
                null
176
            );
177
178
            if ($stmt->dim && $stmt_var_type->hasArray()) {
179
                /**
180
                 * @psalm-suppress PossiblyUndefinedStringArrayOffset
181
                 * @var TArray|ObjectLike|TList|Type\Atomic\TClassStringMap
182
                 */
183
                $array_type = $stmt_var_type->getAtomicTypes()['array'];
184
185
                if ($array_type instanceof Type\Atomic\TClassStringMap) {
186
                    $array_value_type = Type::getMixed();
187
                } elseif ($array_type instanceof TArray) {
188
                    $array_value_type = $array_type->type_params[1];
189
                } elseif ($array_type instanceof TList) {
190
                    $array_value_type = $array_type->type_param;
191
                } else {
192
                    $array_value_type = $array_type->getGenericValueType();
193
                }
194
195
                if ($context->inside_assignment || !$array_value_type->isMixed()) {
196
                    $can_store_result = true;
197
                }
198
            }
199
200
            $statements_analyzer->node_data->setType($stmt, $stmt_type);
201
202
            if ($context->inside_isset
203
                && $stmt->dim
204
                && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))
205
                && $stmt_var_type->hasArray()
206
                && ($stmt->var instanceof PhpParser\Node\Expr\ClassConstFetch
207
                    || $stmt->var instanceof PhpParser\Node\Expr\ConstFetch)
208
            ) {
209
                /**
210
                 * @psalm-suppress PossiblyUndefinedStringArrayOffset
211
                 * @var TArray|ObjectLike|TList
212
                 */
213
                $array_type = $stmt_var_type->getAtomicTypes()['array'];
214
215
                if ($array_type instanceof TArray) {
216
                    $const_array_key_type = $array_type->type_params[0];
217
                } elseif ($array_type instanceof TList) {
218
                    $const_array_key_type = Type::getInt();
219
                } else {
220
                    $const_array_key_type = $array_type->getGenericKeyType();
221
                }
222
223
                if ($dim_var_id
224
                    && !$const_array_key_type->hasMixed()
225
                    && !$stmt_dim_type->hasMixed()
226
                ) {
227
                    $new_offset_type = clone $stmt_dim_type;
228
                    $const_array_key_atomic_types = $const_array_key_type->getAtomicTypes();
229
230
                    foreach ($new_offset_type->getAtomicTypes() as $offset_key => $offset_atomic_type) {
231
                        if ($offset_atomic_type instanceof TString
232
                            || $offset_atomic_type instanceof TInt
233
                        ) {
234
                            if (!isset($const_array_key_atomic_types[$offset_key])
235
                                && !TypeAnalyzer::isContainedBy(
236
                                    $codebase,
237
                                    new Type\Union([$offset_atomic_type]),
238
                                    $const_array_key_type
239
                                )
240
                            ) {
241
                                $new_offset_type->removeType($offset_key);
242
                            }
243
                        } elseif (!TypeAnalyzer::isContainedBy(
244
                            $codebase,
245
                            $const_array_key_type,
246
                            new Type\Union([$offset_atomic_type])
247
                        )) {
248
                            $new_offset_type->removeType($offset_key);
249
                        }
250
                    }
251
                }
252
            }
253
        }
254
255
        if ($keyed_array_var_id
256
            && $context->hasVariable($keyed_array_var_id, $statements_analyzer)
257
            && (!($stmt_type = $statements_analyzer->node_data->getType($stmt)) || $stmt_type->isVanillaMixed())
258
        ) {
259
            $statements_analyzer->node_data->setType($stmt, $context->vars_in_scope[$keyed_array_var_id]);
260
        }
261
262
        if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))) {
263
            $stmt_type = Type::getMixed();
264
            $statements_analyzer->node_data->setType($stmt, $stmt_type);
265
        } else {
266
            if ($stmt_type->possibly_undefined
267
                && !$context->inside_isset
268
                && !$context->inside_unset
269
                && ($stmt_var_type && !$stmt_var_type->hasMixed())
270
            ) {
271
                if (IssueBuffer::accepts(
272
                    new PossiblyUndefinedArrayOffset(
273
                        'Possibly undefined array key ' . $keyed_array_var_id,
274
                        new CodeLocation($statements_analyzer->getSource(), $stmt)
275
                    ),
276
                    $statements_analyzer->getSuppressedIssues()
277
                )) {
278
                    // fall through
279
                }
280
            }
281
282
            $stmt_type->possibly_undefined = false;
283
        }
284
285
        if ($context->inside_isset && $dim_var_id && $new_offset_type && $new_offset_type->getAtomicTypes()) {
286
            $context->vars_in_scope[$dim_var_id] = $new_offset_type;
287
        }
288
289
        if ($keyed_array_var_id && !$context->inside_isset && $can_store_result) {
290
            $context->vars_in_scope[$keyed_array_var_id] = $stmt_type;
291
            $context->vars_possibly_in_scope[$keyed_array_var_id] = true;
292
293
            // reference the variable too
294
            $context->hasVariable($keyed_array_var_id, $statements_analyzer);
295
        }
296
297
        self::taintArrayFetch(
298
            $statements_analyzer,
299
            $stmt->var,
300
            $keyed_array_var_id,
301
            $stmt_type,
302
            $used_key_type
303
        );
304
305
        return true;
306
    }
307
308
    public static function taintArrayFetch(
309
        StatementsAnalyzer $statements_analyzer,
310
        PhpParser\Node\Expr $var,
311
        ?string $keyed_array_var_id,
312
        Type\Union $stmt_type,
313
        Type\Union $offset_type
314
    ) : void {
315
        $codebase = $statements_analyzer->getCodebase();
316
317
        if ($codebase->taint
318
            && ($stmt_var_type = $statements_analyzer->node_data->getType($var))
319
            && $stmt_var_type->parent_nodes
320
            && $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
321
        ) {
322
            if (\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())) {
323
                $stmt_var_type->parent_nodes = [];
324
                return;
325
            }
326
327
            $var_location = new CodeLocation($statements_analyzer->getSource(), $var);
328
329
            $new_parent_node = \Psalm\Internal\Taint\TaintNode::getForAssignment(
330
                $keyed_array_var_id ?: 'array-fetch',
331
                $var_location
332
            );
333
334
            $codebase->taint->addTaintNode($new_parent_node);
335
336
            $dim_value = $offset_type->isSingleStringLiteral()
337
                ? $offset_type->getSingleStringLiteral()->value
338
                : ($offset_type->isSingleIntLiteral()
339
                    ? $offset_type->getSingleIntLiteral()->value
340
                    : null);
341
342
            foreach ($stmt_var_type->parent_nodes as $parent_node) {
343
                $codebase->taint->addPath(
344
                    $parent_node,
345
                    $new_parent_node,
346
                    'array-fetch' . ($dim_value !== null ? '-\'' . $dim_value . '\'' : '')
347
                );
348
            }
349
350
            $stmt_type->parent_nodes = [$new_parent_node];
351
        }
352
    }
353
354
    /**
355
     * @param  Type\Union $array_type
356
     * @param  Type\Union $offset_type
357
     * @param  bool       $in_assignment
358
     * @param  null|string    $array_var_id
359
     *
360
     * @return Type\Union
361
     */
362
    public static function getArrayAccessTypeGivenOffset(
363
        StatementsAnalyzer $statements_analyzer,
364
        PhpParser\Node\Expr\ArrayDimFetch $stmt,
365
        Type\Union $array_type,
366
        Type\Union $offset_type,
367
        bool $in_assignment,
368
        ?string $array_var_id,
369
        Context $context,
370
        PhpParser\Node\Expr $assign_value = null,
371
        Type\Union $replacement_type = null
372
    ) {
373
        $codebase = $statements_analyzer->getCodebase();
374
375
        $has_array_access = false;
376
        $non_array_types = [];
377
378
        $has_valid_offset = false;
379
        $expected_offset_types = [];
380
381
        $key_values = [];
382
383
        if ($stmt->dim instanceof PhpParser\Node\Scalar\String_
384
            || $stmt->dim instanceof PhpParser\Node\Scalar\LNumber
385
        ) {
386
            $key_values[] = $stmt->dim->value;
387
        } elseif ($stmt->dim && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))) {
388
            $string_literals = $stmt_dim_type->getLiteralStrings();
389
            $int_literals = $stmt_dim_type->getLiteralInts();
390
391
            $all_atomic_types = $stmt_dim_type->getAtomicTypes();
392
393
            if (count($string_literals) + count($int_literals) === count($all_atomic_types)) {
394
                foreach ($string_literals as $string_literal) {
395
                    $key_values[] = $string_literal->value;
396
                }
397
398
                foreach ($int_literals as $int_literal) {
399
                    $key_values[] = $int_literal->value;
400
                }
401
            }
402
        }
403
404
        $array_access_type = null;
405
406
        if ($offset_type->isNull()) {
407
            if (IssueBuffer::accepts(
408
                new NullArrayOffset(
409
                    'Cannot access value on variable ' . $array_var_id . ' using null offset',
410
                    new CodeLocation($statements_analyzer->getSource(), $stmt)
411
                ),
412
                $statements_analyzer->getSuppressedIssues()
413
            )) {
414
                // fall through
415
            }
416
417
            if ($in_assignment) {
418
                $offset_type->removeType('null');
419
                $offset_type->addType(new TLiteralInt(0));
420
            }
421
        }
422
423
        if ($offset_type->isNullable() && !$context->inside_isset) {
424
            if (!$offset_type->ignore_nullable_issues) {
425
                if (IssueBuffer::accepts(
426
                    new PossiblyNullArrayOffset(
427
                        'Cannot access value on variable ' . $array_var_id
428
                            . ' using possibly null offset ' . $offset_type,
429
                        new CodeLocation($statements_analyzer->getSource(), $stmt->var)
430
                    ),
431
                    $statements_analyzer->getSuppressedIssues()
432
                )) {
433
                    // fall through
434
                }
435
            }
436
437
            if ($in_assignment) {
438
                $offset_type->removeType('null');
439
440
                if (!$offset_type->ignore_nullable_issues) {
441
                    $offset_type->addType(new TLiteralInt(0));
442
                }
443
            }
444
        }
445
446
        foreach ($array_type->getAtomicTypes() as $type_string => $type) {
447
            $original_type = $type;
448
449
            if ($type instanceof TMixed || $type instanceof TTemplateParam || $type instanceof TEmpty) {
450
                if (!$type instanceof TTemplateParam || $type->as->isMixed() || !$type->as->isSingle()) {
451
                    if (!$context->collect_initializations
452
                        && !$context->collect_mutations
453
                        && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
454
                        && (!(($parent_source = $statements_analyzer->getSource())
455
                                instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
456
                            || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
457
                    ) {
458
                        $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
459
                    }
460
461
                    if (!$context->inside_isset) {
462
                        if ($in_assignment) {
463
                            if (IssueBuffer::accepts(
464
                                new MixedArrayAssignment(
465
                                    'Cannot access array value on mixed variable ' . $array_var_id,
466
                                    new CodeLocation($statements_analyzer->getSource(), $stmt)
467
                                ),
468
                                $statements_analyzer->getSuppressedIssues()
469
                            )) {
470
                                // fall through
471
                            }
472
                        } else {
473
                            if (IssueBuffer::accepts(
474
                                new MixedArrayAccess(
475
                                    'Cannot access array value on mixed variable ' . $array_var_id,
476
                                    new CodeLocation($statements_analyzer->getSource(), $stmt)
477
                                ),
478
                                $statements_analyzer->getSuppressedIssues()
479
                            )) {
480
                                // fall through
481
                            }
482
                        }
483
                    }
484
485
                    $has_valid_offset = true;
486
                    if (!$array_access_type) {
487
                        $array_access_type = Type::getMixed(
488
                            $type instanceof TEmpty
489
                        );
490
                    } else {
491
                        $array_access_type = Type::combineUnionTypes(
492
                            $array_access_type,
493
                            Type::getMixed($type instanceof TEmpty)
494
                        );
495
                    }
496
497
                    continue;
498
                }
499
500
                $type = clone array_values($type->as->getAtomicTypes())[0];
501
            }
502
503
            if ($type instanceof TNull) {
504
                if ($array_type->ignore_nullable_issues) {
505
                    continue;
506
                }
507
508
                if ($in_assignment) {
509
                    if ($replacement_type) {
510
                        if ($array_access_type) {
511
                            $array_access_type = Type::combineUnionTypes($array_access_type, $replacement_type);
512
                        } else {
513
                            $array_access_type = clone $replacement_type;
514
                        }
515
                    } else {
516
                        if (IssueBuffer::accepts(
517
                            new PossiblyNullArrayAssignment(
518
                                'Cannot access array value on possibly null variable ' . $array_var_id .
519
                                    ' of type ' . $array_type,
520
                                new CodeLocation($statements_analyzer->getSource(), $stmt)
521
                            ),
522
                            $statements_analyzer->getSuppressedIssues()
523
                        )) {
524
                            // fall through
525
                        }
526
527
                        $array_access_type = new Type\Union([new TEmpty]);
528
                    }
529
                } else {
530
                    if (!$context->inside_isset) {
531
                        if (IssueBuffer::accepts(
532
                            new PossiblyNullArrayAccess(
533
                                'Cannot access array value on possibly null variable ' . $array_var_id .
534
                                    ' of type ' . $array_type,
535
                                new CodeLocation($statements_analyzer->getSource(), $stmt)
536
                            ),
537
                            $statements_analyzer->getSuppressedIssues()
538
                        )) {
539
                            // fall through
540
                        }
541
                    }
542
543
                    if ($array_access_type) {
544
                        $array_access_type = Type::combineUnionTypes($array_access_type, Type::getNull());
545
                    } else {
546
                        $array_access_type = Type::getNull();
547
                    }
548
                }
549
550
                continue;
551
            }
552
553
            if ($type instanceof TArray
554
                || $type instanceof ObjectLike
555
                || $type instanceof TList
556
                || $type instanceof TClassStringMap
557
            ) {
558
                $has_array_access = true;
559
560
                if ($in_assignment
561
                    && $type instanceof TArray
562
                    && (($type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty())
563
                        || ($type->type_params[1]->hasMixed()
564
                            && count($key_values) === 1
565
                            &&  \is_string($key_values[0])))
566
                ) {
567
                    $from_empty_array = $type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty();
568
569
                    if (count($key_values) === 1) {
570
                        $from_mixed_array = $type->type_params[1]->isMixed();
571
572
                        $previous_key_type = $type->type_params[0];
573
                        $previous_value_type = $type->type_params[1];
574
575
                        // ok, type becomes an ObjectLike
576
                        $array_type->removeType($type_string);
577
                        $type = new ObjectLike([
578
                            $key_values[0] => $from_mixed_array ? Type::getMixed() : Type::getEmpty()
579
                        ]);
580
581
                        $type->sealed = $from_empty_array;
582
583
                        if (!$from_empty_array) {
584
                            $type->previous_value_type = clone $previous_value_type;
585
                            $type->previous_key_type = clone $previous_key_type;
586
                        }
587
588
                        $array_type->addType($type);
589
                    } elseif (!$stmt->dim && $from_empty_array && $replacement_type) {
590
                        $array_type->removeType($type_string);
591
                        $array_type->addType(new Type\Atomic\TNonEmptyList($replacement_type));
592
                        continue;
593
                    }
594
                } elseif ($in_assignment
595
                    && $type instanceof ObjectLike
596
                    && $type->previous_value_type
597
                    && $type->previous_value_type->isMixed()
598
                    && count($key_values) === 1
599
                ) {
600
                    $type->properties[$key_values[0]] = Type::getMixed();
601
                }
602
603
                $offset_type = self::replaceOffsetTypeWithInts($offset_type);
604
605
                if ($type instanceof TList
606
                    && (($in_assignment && $stmt->dim)
607
                        || $original_type instanceof TTemplateParam
608
                        || !$offset_type->isInt())
609
                ) {
610
                    $type = new TArray([Type::getInt(), $type->type_param]);
611
                }
612
613
                if ($type instanceof TArray) {
614
                    // if we're assigning to an empty array with a key offset, refashion that array
615
                    if ($in_assignment) {
616
                        if ($type->type_params[0]->isEmpty()) {
617
                            $type->type_params[0] = $offset_type->isMixed()
618
                                ? Type::getArrayKey()
619
                                : $offset_type;
620
                        }
621
                    } elseif (!$type->type_params[0]->isEmpty()) {
622
                        $expected_offset_type = $type->type_params[0]->hasMixed()
623
                            ? new Type\Union([ new TArrayKey ])
624
                            : $type->type_params[0];
625
626
                        $templated_offset_type = null;
627
628
                        foreach ($offset_type->getAtomicTypes() as $offset_atomic_type) {
629
                            if ($offset_atomic_type instanceof TTemplateParam) {
630
                                $templated_offset_type = $offset_atomic_type;
631
                            }
632
                        }
633
634
                        $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
635
636
                        if ($original_type instanceof TTemplateParam && $templated_offset_type) {
637
                            foreach ($templated_offset_type->as->getAtomicTypes() as $offset_as) {
638
                                if ($offset_as instanceof Type\Atomic\TTemplateKeyOf
639
                                    && $offset_as->param_name === $original_type->param_name
640
                                    && $offset_as->defining_class === $original_type->defining_class
641
                                ) {
642
                                    /** @psalm-suppress PropertyTypeCoercion */
643
                                    $type->type_params[1] = new Type\Union([
644
                                        new Type\Atomic\TTemplateIndexedAccess(
645
                                            $offset_as->param_name,
646
                                            $templated_offset_type->param_name,
647
                                            $offset_as->defining_class
648
                                        )
649
                                    ]);
650
651
                                    $has_valid_offset = true;
652
                                }
653
                            }
654
                        } else {
655
                            $offset_type_contained_by_expected = TypeAnalyzer::isContainedBy(
656
                                $codebase,
657
                                $offset_type,
658
                                $expected_offset_type,
659
                                true,
660
                                $offset_type->ignore_falsable_issues,
661
                                $union_comparison_results
662
                            );
663
664
                            if ($codebase->config->ensure_array_string_offsets_exist
665
                                && $offset_type_contained_by_expected
666
                            ) {
667
                                self::checkLiteralStringArrayOffset(
668
                                    $offset_type,
669
                                    $expected_offset_type,
670
                                    $array_var_id,
671
                                    $stmt,
672
                                    $context,
673
                                    $statements_analyzer
674
                                );
675
                            }
676
677
                            if ($codebase->config->ensure_array_int_offsets_exist
678
                                && $offset_type_contained_by_expected
679
                            ) {
680
                                self::checkLiteralIntArrayOffset(
681
                                    $offset_type,
682
                                    $expected_offset_type,
683
                                    $array_var_id,
684
                                    $stmt,
685
                                    $context,
686
                                    $statements_analyzer
687
                                );
688
                            }
689
690
                            if ((!$offset_type_contained_by_expected
691
                                    && !$union_comparison_results->type_coerced_from_scalar)
692
                                || $union_comparison_results->to_string_cast
693
                            ) {
694
                                if ($union_comparison_results->type_coerced_from_mixed
695
                                    && !$offset_type->isMixed()
696
                                ) {
697
                                    if (IssueBuffer::accepts(
698
                                        new MixedArrayTypeCoercion(
699
                                            'Coercion from array offset type \'' . $offset_type->getId() . '\' '
700
                                                . 'to the expected type \'' . $expected_offset_type->getId() . '\'',
701
                                            new CodeLocation($statements_analyzer->getSource(), $stmt)
702
                                        ),
703
                                        $statements_analyzer->getSuppressedIssues()
704
                                    )) {
705
                                        // fall through
706
                                    }
707
                                } else {
708
                                    $expected_offset_types[] = $expected_offset_type->getId();
709
                                }
710
711
                                if (TypeAnalyzer::canExpressionTypesBeIdentical(
712
                                    $codebase,
713
                                    $offset_type,
714
                                    $expected_offset_type
715
                                )) {
716
                                    $has_valid_offset = true;
717
                                }
718
                            } else {
719
                                $has_valid_offset = true;
720
                            }
721
                        }
722
                    }
723
724
                    if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) {
725
                        $type->count++;
726
                    }
727
728
                    if ($in_assignment && $replacement_type) {
729
                        /** @psalm-suppress PropertyTypeCoercion */
730
                        $type->type_params[1] = Type::combineUnionTypes(
731
                            $type->type_params[1],
732
                            $replacement_type,
733
                            $codebase
734
                        );
735
                    }
736
737
                    if (!$array_access_type) {
738
                        $array_access_type = $type->type_params[1];
739
                    } else {
740
                        $array_access_type = Type::combineUnionTypes(
741
                            $array_access_type,
742
                            $type->type_params[1]
743
                        );
744
                    }
745
746
                    if ($array_access_type->isEmpty()
747
                        && !$array_type->hasMixed()
748
                        && !$in_assignment
749
                        && !$context->inside_isset
750
                    ) {
751
                        if (IssueBuffer::accepts(
752
                            new EmptyArrayAccess(
753
                                'Cannot access value on empty array variable ' . $array_var_id,
754
                                new CodeLocation($statements_analyzer->getSource(), $stmt)
755
                            ),
756
                            $statements_analyzer->getSuppressedIssues()
757
                        )) {
758
                            return Type::getMixed(true);
759
                        }
760
761
                        if (!IssueBuffer::isRecording()) {
762
                            $array_access_type = Type::getMixed(true);
763
                        }
764
                    }
765
                } elseif ($type instanceof TList) {
766
                    // if we're assigning to an empty array with a key offset, refashion that array
767
                    if (!$in_assignment) {
768
                        if (!$type instanceof TNonEmptyList
769
                            || (count($key_values) === 1
770
                                && is_int($key_values[0])
771
                                && $key_values[0] > 0
772
                                && $key_values[0] > ($type->count - 1))
773
                        ) {
774
                            $expected_offset_type = Type::getInt();
775
776
                            if ($codebase->config->ensure_array_int_offsets_exist) {
777
                                self::checkLiteralIntArrayOffset(
778
                                    $offset_type,
779
                                    $expected_offset_type,
780
                                    $array_var_id,
781
                                    $stmt,
782
                                    $context,
783
                                    $statements_analyzer
784
                                );
785
                            }
786
                        }
787
788
                        $has_valid_offset = true;
789
                    }
790
791
                    if ($in_assignment && $type instanceof Type\Atomic\TNonEmptyList && $type->count !== null) {
792
                        $type->count++;
793
                    }
794
795
                    if ($in_assignment && $replacement_type) {
796
                        $type->type_param = Type::combineUnionTypes(
797
                            $type->type_param,
798
                            $replacement_type,
799
                            $codebase
800
                        );
801
                    }
802
803
                    if (!$array_access_type) {
804
                        $array_access_type = $type->type_param;
805
                    } else {
806
                        $array_access_type = Type::combineUnionTypes(
807
                            $array_access_type,
808
                            $type->type_param
809
                        );
810
                    }
811
                } elseif ($type instanceof TClassStringMap) {
812
                    $offset_type_parts = array_values($offset_type->getAtomicTypes());
813
814
                    foreach ($offset_type_parts as $offset_type_part) {
815
                        if ($offset_type_part instanceof Type\Atomic\TClassString) {
816
                            if ($offset_type_part instanceof Type\Atomic\TTemplateParamClass) {
817
                                $template_result_get = new TemplateResult(
818
                                    [],
819
                                    [
820
                                        $type->param_name => [
821
                                            'class-string-map' => [
822
                                                new Type\Union([
823
                                                    new TTemplateParam(
824
                                                        $offset_type_part->param_name,
825
                                                        $offset_type_part->as_type
826
                                                            ? new Type\Union([$offset_type_part->as_type])
827
                                                            : Type::getObject(),
828
                                                        $offset_type_part->defining_class
829
                                                    )
830
                                                ])
831
                                            ]
832
                                        ]
833
                                    ]
834
                                );
835
836
                                $template_result_set = new TemplateResult(
837
                                    [],
838
                                    [
839
                                        $offset_type_part->param_name => [
840
                                            $offset_type_part->defining_class => [
841
                                                new Type\Union([
842
                                                    new TTemplateParam(
843
                                                        $type->param_name,
844
                                                        $type->as_type
845
                                                            ? new Type\Union([$type->as_type])
846
                                                            : Type::getObject(),
847
                                                        'class-string-map'
848
                                                    )
849
                                                ])
850
                                            ]
851
                                        ]
852
                                    ]
853
                                );
854
                            } else {
855
                                $template_result_get = new TemplateResult(
856
                                    [],
857
                                    [
858
                                        $type->param_name => [
859
                                            'class-string-map' => [
860
                                                new Type\Union([
861
                                                    $offset_type_part->as_type
862
                                                        ?: new Type\Atomic\TObject()
863
                                                ])
864
                                            ]
865
                                        ]
866
                                    ]
867
                                );
868
                                $template_result_set = new TemplateResult(
869
                                    [],
870
                                    []
871
                                );
872
                            }
873
874
                            $expected_value_param_get = clone $type->value_param;
875
876
                            $expected_value_param_get->replaceTemplateTypesWithArgTypes(
877
                                $template_result_get,
878
                                $codebase
879
                            );
880
881
                            if ($replacement_type) {
882
                                $expected_value_param_set = clone $type->value_param;
883
884
                                $replacement_type->replaceTemplateTypesWithArgTypes(
885
                                    $template_result_set,
886
                                    $codebase
887
                                );
888
889
                                $type->value_param = Type::combineUnionTypes(
890
                                    $replacement_type,
891
                                    $expected_value_param_set,
892
                                    $codebase
893
                                );
894
                            }
895
896
                            if (!$array_access_type) {
897
                                $array_access_type = $expected_value_param_get;
898
                            } else {
899
                                $array_access_type = Type::combineUnionTypes(
900
                                    $array_access_type,
901
                                    $expected_value_param_get,
902
                                    $codebase
903
                                );
904
                            }
905
                        }
906
                    }
907
                } else {
908
                    $generic_key_type = $type->getGenericKeyType();
909
910
                    if (!$stmt->dim && $type->sealed && $type->is_list) {
911
                        $key_values[] = count($type->properties);
912
                    }
913
914
                    if ($key_values) {
915
                        foreach ($key_values as $key_value) {
916
                            if (isset($type->properties[$key_value]) || $replacement_type) {
917
                                $has_valid_offset = true;
918
919
                                if ($replacement_type) {
920
                                    if (isset($type->properties[$key_value])) {
921
                                        $type->properties[$key_value] = Type::combineUnionTypes(
922
                                            $type->properties[$key_value],
923
                                            $replacement_type
924
                                        );
925
                                    } else {
926
                                        $type->properties[$key_value] = $replacement_type;
927
                                    }
928
                                }
929
930
                                if (!$array_access_type) {
931
                                    $array_access_type = clone $type->properties[$key_value];
932
                                } else {
933
                                    $array_access_type = Type::combineUnionTypes(
934
                                        $array_access_type,
935
                                        $type->properties[$key_value]
936
                                    );
937
                                }
938
                            } elseif ($in_assignment) {
939
                                $type->properties[$key_value] = new Type\Union([new TEmpty]);
940
941
                                if (!$array_access_type) {
942
                                    $array_access_type = clone $type->properties[$key_value];
943
                                } else {
944
                                    $array_access_type = Type::combineUnionTypes(
945
                                        $array_access_type,
946
                                        $type->properties[$key_value]
947
                                    );
948
                                }
949
                            } elseif ($type->previous_value_type) {
950
                                if ($codebase->config->ensure_array_string_offsets_exist) {
951
                                    self::checkLiteralStringArrayOffset(
952
                                        $offset_type,
953
                                        $type->getGenericKeyType(),
954
                                        $array_var_id,
955
                                        $stmt,
956
                                        $context,
957
                                        $statements_analyzer
958
                                    );
959
                                }
960
961
                                if ($codebase->config->ensure_array_int_offsets_exist) {
962
                                    self::checkLiteralIntArrayOffset(
963
                                        $offset_type,
964
                                        $type->getGenericKeyType(),
965
                                        $array_var_id,
966
                                        $stmt,
967
                                        $context,
968
                                        $statements_analyzer
969
                                    );
970
                                }
971
972
                                $type->properties[$key_value] = clone $type->previous_value_type;
973
974
                                $array_access_type = clone $type->previous_value_type;
975
                            } elseif ($array_type->hasMixed()) {
976
                                $has_valid_offset = true;
977
978
                                $array_access_type = Type::getMixed();
979
                            } else {
980
                                if ($type->sealed || !$context->inside_isset) {
981
                                    $object_like_keys = array_keys($type->properties);
982
983
                                    if (count($object_like_keys) === 1) {
984
                                        $expected_keys_string = '\'' . $object_like_keys[0] . '\'';
985
                                    } else {
986
                                        $last_key = array_pop($object_like_keys);
987
                                        $expected_keys_string = '\'' . implode('\', \'', $object_like_keys) .
988
                                            '\' or \'' . $last_key . '\'';
989
                                    }
990
991
                                    $expected_offset_types[] = $expected_keys_string;
992
                                }
993
994
                                $array_access_type = Type::getMixed();
995
                            }
996
                        }
997
                    } else {
998
                        $key_type = $generic_key_type->hasMixed()
999
                                ? Type::getArrayKey()
1000
                                : $generic_key_type;
1001
1002
                        $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
1003
1004
                        $is_contained = TypeAnalyzer::isContainedBy(
1005
                            $codebase,
1006
                            $offset_type,
1007
                            $key_type,
1008
                            true,
1009
                            $offset_type->ignore_falsable_issues,
1010
                            $union_comparison_results
1011
                        );
1012
1013
                        if ($context->inside_isset && !$is_contained) {
1014
                            $is_contained = TypeAnalyzer::isContainedBy(
1015
                                $codebase,
1016
                                $key_type,
1017
                                $offset_type,
1018
                                true,
1019
                                $offset_type->ignore_falsable_issues
1020
                            )
1021
                            || TypeAnalyzer::canBeContainedBy(
1022
                                $codebase,
1023
                                $offset_type,
1024
                                $key_type,
1025
                                true,
1026
                                $offset_type->ignore_falsable_issues
1027
                            );
1028
                        }
1029
1030
                        if (($is_contained
1031
                            || $union_comparison_results->type_coerced_from_scalar
1032
                            || $union_comparison_results->type_coerced_from_mixed
1033
                            || $in_assignment)
1034
                            && !$union_comparison_results->to_string_cast
1035
                        ) {
1036
                            if ($replacement_type) {
1037
                                $generic_params = Type::combineUnionTypes(
1038
                                    $type->getGenericValueType(),
1039
                                    $replacement_type
1040
                                );
1041
1042
                                $new_key_type = Type::combineUnionTypes(
1043
                                    $generic_key_type,
1044
                                    $offset_type->isMixed() ? Type::getArrayKey() : $offset_type
1045
                                );
1046
1047
                                $property_count = $type->sealed ? count($type->properties) : null;
1048
1049
                                if (!$stmt->dim && $property_count) {
1050
                                    ++$property_count;
1051
                                    $array_type->removeType($type_string);
1052
                                    $type = new TNonEmptyArray([
1053
                                        $new_key_type,
1054
                                        $generic_params,
1055
                                    ]);
1056
                                    $array_type->addType($type);
1057
                                    $type->count = $property_count;
1058
                                } else {
1059
                                    $array_type->removeType($type_string);
1060
                                    $type = new TArray([
1061
                                        $new_key_type,
1062
                                        $generic_params,
1063
                                    ]);
1064
                                    $array_type->addType($type);
1065
                                }
1066
1067
                                if (!$array_access_type) {
1068
                                    $array_access_type = clone $generic_params;
1069
                                } else {
1070
                                    $array_access_type = Type::combineUnionTypes(
1071
                                        $array_access_type,
1072
                                        $generic_params
1073
                                    );
1074
                                }
1075
                            } else {
1076
                                if (!$array_access_type) {
1077
                                    $array_access_type = $type->getGenericValueType();
1078
                                } else {
1079
                                    $array_access_type = Type::combineUnionTypes(
1080
                                        $array_access_type,
1081
                                        $type->getGenericValueType()
1082
                                    );
1083
                                }
1084
                            }
1085
1086
                            $has_valid_offset = true;
1087
                        } else {
1088
                            if (!$context->inside_isset
1089
                                || ($type->sealed && !$union_comparison_results->type_coerced)
1090
                            ) {
1091
                                $expected_offset_types[] = $generic_key_type->getId();
1092
                            }
1093
1094
                            $array_access_type = Type::getMixed();
1095
                        }
1096
                    }
1097
                }
1098
                continue;
1099
            }
1100
1101
            if ($type instanceof TString) {
1102
                if ($in_assignment && $replacement_type) {
1103
                    if ($replacement_type->hasMixed()) {
1104
                        if (!$context->collect_initializations
1105
                            && !$context->collect_mutations
1106
                            && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
1107
                            && (!(($parent_source = $statements_analyzer->getSource())
1108
                                    instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
1109
                                || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
1110
                        ) {
1111
                            $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
1112
                        }
1113
1114
                        if (IssueBuffer::accepts(
1115
                            new MixedStringOffsetAssignment(
1116
                                'Right-hand-side of string offset assignment cannot be mixed',
1117
                                new CodeLocation($statements_analyzer->getSource(), $stmt)
1118
                            ),
1119
                            $statements_analyzer->getSuppressedIssues()
1120
                        )) {
1121
                            // fall through
1122
                        }
1123
                    } else {
1124
                        if (!$context->collect_initializations
1125
                            && !$context->collect_mutations
1126
                            && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
1127
                            && (!(($parent_source = $statements_analyzer->getSource())
1128
                                    instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
1129
                                || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
1130
                        ) {
1131
                            $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
1132
                        }
1133
                    }
1134
                }
1135
1136
                if ($type instanceof TSingleLetter) {
1137
                    $valid_offset_type = Type::getInt(false, 0);
1138
                } elseif ($type instanceof TLiteralString) {
1139
                    if (!strlen($type->value)) {
1140
                        $valid_offset_type = Type::getEmpty();
1141
                    } elseif (strlen($type->value) < 10) {
1142
                        $valid_offsets = [];
1143
1144
                        for ($i = -strlen($type->value), $l = strlen($type->value); $i < $l; $i++) {
1145
                            $valid_offsets[] = new TLiteralInt($i);
1146
                        }
1147
1148
                        if (!$valid_offsets) {
1149
                            throw new \UnexpectedValueException('This is weird');
1150
                        }
1151
1152
                        $valid_offset_type = new Type\Union($valid_offsets);
1153
                    } else {
1154
                        $valid_offset_type = Type::getInt();
1155
                    }
1156
                } else {
1157
                    $valid_offset_type = Type::getInt();
1158
                }
1159
1160
                if (!TypeAnalyzer::isContainedBy(
1161
                    $codebase,
1162
                    $offset_type,
1163
                    $valid_offset_type,
1164
                    true
1165
                )) {
1166
                    $expected_offset_types[] = $valid_offset_type->getId();
1167
1168
                    $array_access_type = Type::getMixed();
1169
                } else {
1170
                    $has_valid_offset = true;
1171
1172
                    if (!$array_access_type) {
1173
                        $array_access_type = Type::getSingleLetter();
1174
                    } else {
1175
                        $array_access_type = Type::combineUnionTypes(
1176
                            $array_access_type,
1177
                            Type::getSingleLetter()
1178
                        );
1179
                    }
1180
                }
1181
1182
                continue;
1183
            }
1184
1185
            if (!$context->collect_initializations
1186
                && !$context->collect_mutations
1187
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
1188
                && (!(($parent_source = $statements_analyzer->getSource())
1189
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
1190
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
1191
            ) {
1192
                $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
1193
            }
1194
1195
            if ($type instanceof Type\Atomic\TFalse && $array_type->ignore_falsable_issues) {
1196
                continue;
1197
            }
1198
1199
            if ($type instanceof TNamedObject) {
1200
                if (strtolower($type->value) === 'simplexmlelement') {
1201
                    $call_array_access_type = new Type\Union([new TNamedObject('SimpleXMLElement')]);
1202
                } elseif (strtolower($type->value) === 'domnodelist' && $stmt->dim) {
1203
                    $old_data_provider = $statements_analyzer->node_data;
1204
1205
                    $statements_analyzer->node_data = clone $statements_analyzer->node_data;
1206
1207
                    $fake_method_call = new PhpParser\Node\Expr\MethodCall(
1208
                        $stmt->var,
1209
                        new PhpParser\Node\Identifier('item', $stmt->var->getAttributes()),
1210
                        [
1211
                            new PhpParser\Node\Arg($stmt->dim)
1212
                        ]
1213
                    );
1214
1215
                    $suppressed_issues = $statements_analyzer->getSuppressedIssues();
1216
1217
                    if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
1218
                        $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
1219
                    }
1220
1221
                    if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
1222
                        $statements_analyzer->addSuppressedIssues(['MixedMethodCall']);
1223
                    }
1224
1225
                    \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
1226
                        $statements_analyzer,
1227
                        $fake_method_call,
1228
                        $context
1229
                    );
1230
1231
                    if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
1232
                        $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
1233
                    }
1234
1235
                    if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
1236
                        $statements_analyzer->removeSuppressedIssues(['MixedMethodCall']);
1237
                    }
1238
1239
                    $call_array_access_type = $statements_analyzer->node_data->getType(
1240
                        $fake_method_call
1241
                    ) ?: Type::getMixed();
1242
1243
                    $statements_analyzer->node_data = $old_data_provider;
1244
                } else {
1245
                    $suppressed_issues = $statements_analyzer->getSuppressedIssues();
1246
1247
                    if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
1248
                        $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
1249
                    }
1250
1251
                    if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
1252
                        $statements_analyzer->addSuppressedIssues(['MixedMethodCall']);
1253
                    }
1254
1255
                    if ($in_assignment) {
1256
                        $old_node_data = $statements_analyzer->node_data;
1257
1258
                        $statements_analyzer->node_data = clone $statements_analyzer->node_data;
1259
1260
                        $fake_set_method_call = new PhpParser\Node\Expr\MethodCall(
1261
                            $stmt->var,
1262
                            new PhpParser\Node\Identifier('offsetSet', $stmt->var->getAttributes()),
1263
                            [
1264
                                new PhpParser\Node\Arg(
1265
                                    $stmt->dim
1266
                                        ? $stmt->dim
1267
                                        : new PhpParser\Node\Expr\ConstFetch(
1268
                                            new PhpParser\Node\Name('null'),
1269
                                            $stmt->var->getAttributes()
1270
                                        )
1271
                                ),
1272
                                new PhpParser\Node\Arg(
1273
                                    $assign_value
1274
                                        ?: new PhpParser\Node\Expr\ConstFetch(
1275
                                            new PhpParser\Node\Name('null'),
1276
                                            $stmt->var->getAttributes()
1277
                                        )
1278
                                ),
1279
                            ]
1280
                        );
1281
1282
                        \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
1283
                            $statements_analyzer,
1284
                            $fake_set_method_call,
1285
                            $context
1286
                        );
1287
1288
                        $statements_analyzer->node_data = $old_node_data;
1289
                    }
1290
1291
                    if ($stmt->dim) {
1292
                        $old_node_data = $statements_analyzer->node_data;
1293
1294
                        $statements_analyzer->node_data = clone $statements_analyzer->node_data;
1295
1296
                        $fake_get_method_call = new PhpParser\Node\Expr\MethodCall(
1297
                            $stmt->var,
1298
                            new PhpParser\Node\Identifier('offsetGet', $stmt->var->getAttributes()),
1299
                            [
1300
                                new PhpParser\Node\Arg(
1301
                                    $stmt->dim
1302
                                )
1303
                            ]
1304
                        );
1305
1306
                        \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
1307
                            $statements_analyzer,
1308
                            $fake_get_method_call,
1309
                            $context
1310
                        );
1311
1312
                        $call_array_access_type = $statements_analyzer->node_data->getType($fake_get_method_call)
1313
                            ?: Type::getMixed();
1314
1315
                        $statements_analyzer->node_data = $old_node_data;
1316
                    } else {
1317
                        $call_array_access_type = Type::getVoid();
1318
                    }
1319
1320
                    $has_array_access = true;
1321
1322
                    if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
1323
                        $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
1324
                    }
1325
1326
                    if (!in_array('MixedMethodCall', $suppressed_issues, true)) {
1327
                        $statements_analyzer->removeSuppressedIssues(['MixedMethodCall']);
1328
                    }
1329
                }
1330
1331
                if (!$array_access_type) {
1332
                    $array_access_type = $call_array_access_type;
1333
                } else {
1334
                    $array_access_type = Type::combineUnionTypes(
1335
                        $array_access_type,
1336
                        $call_array_access_type
1337
                    );
1338
                }
1339
            } elseif (!$array_type->hasMixed()) {
1340
                $non_array_types[] = (string)$type;
1341
            }
1342
        }
1343
1344
        if ($non_array_types) {
1345
            if ($has_array_access) {
1346
                if ($in_assignment) {
1347
                    if (IssueBuffer::accepts(
1348
                        new PossiblyInvalidArrayAssignment(
1349
                            'Cannot access array value on non-array variable ' .
1350
                            $array_var_id . ' of type ' . $non_array_types[0],
1351
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
1352
                        ),
1353
                        $statements_analyzer->getSuppressedIssues()
1354
                    )
1355
                    ) {
1356
                        // do nothing
1357
                    }
1358
                } elseif (!$context->inside_isset) {
1359
                    if (IssueBuffer::accepts(
1360
                        new PossiblyInvalidArrayAccess(
1361
                            'Cannot access array value on non-array variable ' .
1362
                            $array_var_id . ' of type ' . $non_array_types[0],
1363
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
1364
                        ),
1365
                        $statements_analyzer->getSuppressedIssues()
1366
                    )
1367
                    ) {
1368
                        // do nothing
1369
                    }
1370
                }
1371
            } else {
1372
                if ($in_assignment) {
1373
                    if (IssueBuffer::accepts(
1374
                        new InvalidArrayAssignment(
1375
                            'Cannot access array value on non-array variable ' .
1376
                            $array_var_id . ' of type ' . $non_array_types[0],
1377
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
1378
                        ),
1379
                        $statements_analyzer->getSuppressedIssues()
1380
                    )) {
1381
                        // fall through
1382
                    }
1383
                } else {
1384
                    if (IssueBuffer::accepts(
1385
                        new InvalidArrayAccess(
1386
                            'Cannot access array value on non-array variable ' .
1387
                            $array_var_id . ' of type ' . $non_array_types[0],
1388
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
1389
                        ),
1390
                        $statements_analyzer->getSuppressedIssues()
1391
                    )) {
1392
                        // fall through
1393
                    }
1394
                }
1395
1396
                $array_access_type = Type::getMixed();
1397
            }
1398
        }
1399
1400
        if ($offset_type->hasMixed()) {
1401
            if (!$context->collect_initializations
1402
                && !$context->collect_mutations
1403
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
1404
                && (!(($parent_source = $statements_analyzer->getSource())
1405
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
1406
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
1407
            ) {
1408
                $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
1409
            }
1410
1411
            if (IssueBuffer::accepts(
1412
                new MixedArrayOffset(
1413
                    'Cannot access value on variable ' . $array_var_id . ' using mixed offset',
1414
                    new CodeLocation($statements_analyzer->getSource(), $stmt)
1415
                ),
1416
                $statements_analyzer->getSuppressedIssues()
1417
            )) {
1418
                // fall through
1419
            }
1420
        } else {
1421
            if (!$context->collect_initializations
1422
                && !$context->collect_mutations
1423
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
1424
                && (!(($parent_source = $statements_analyzer->getSource())
1425
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
1426
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
1427
            ) {
1428
                $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
1429
            }
1430
1431
            if ($expected_offset_types) {
1432
                $invalid_offset_type = $expected_offset_types[0];
1433
1434
                $used_offset = 'using a ' . $offset_type->getId() . ' offset';
1435
1436
                if ($key_values) {
1437
                    $used_offset = 'using offset value of '
1438
                        . (is_int($key_values[0]) ? $key_values[0] : '\'' . $key_values[0] . '\'');
1439
                }
1440
1441
                if ($has_valid_offset && $context->inside_isset) {
1442
                    // do nothing
1443
                } elseif ($has_valid_offset) {
1444
                    if (!$context->inside_unset) {
1445
                        if (IssueBuffer::accepts(
1446
                            new PossiblyInvalidArrayOffset(
1447
                                'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset
1448
                                    . ', expecting ' . $invalid_offset_type,
1449
                                new CodeLocation($statements_analyzer->getSource(), $stmt)
1450
                            ),
1451
                            $statements_analyzer->getSuppressedIssues()
1452
                        )) {
1453
                            // fall through
1454
                        }
1455
                    }
1456
                } else {
1457
                    if (IssueBuffer::accepts(
1458
                        new InvalidArrayOffset(
1459
                            'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset
1460
                                . ', expecting ' . $invalid_offset_type,
1461
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
1462
                        ),
1463
                        $statements_analyzer->getSuppressedIssues()
1464
                    )) {
1465
                        // fall through
1466
                    }
1467
                }
1468
            }
1469
        }
1470
1471
        if ($array_access_type === null) {
1472
            // shouldn’t happen, but don’t crash
1473
            return Type::getMixed();
1474
        }
1475
1476
        if ($in_assignment) {
1477
            $array_type->bustCache();
1478
        }
1479
1480
        return $array_access_type;
1481
    }
1482
1483
    private static function checkLiteralIntArrayOffset(
1484
        Type\Union $offset_type,
1485
        Type\Union $expected_offset_type,
1486
        ?string $array_var_id,
1487
        PhpParser\Node\Expr\ArrayDimFetch $stmt,
1488
        Context $context,
1489
        StatementsAnalyzer $statements_analyzer
1490
    ) : void {
1491
        if ($context->inside_isset || $context->inside_unset) {
1492
            return;
1493
        }
1494
1495
        if ($offset_type->hasLiteralInt()) {
1496
            $found_match = false;
1497
1498
            foreach ($offset_type->getAtomicTypes() as $offset_type_part) {
1499
                if ($array_var_id
1500
                    && $offset_type_part instanceof TLiteralInt
1501
                    && isset(
1502
                        $context->vars_in_scope[
1503
                            $array_var_id . '[' . $offset_type_part->value . ']'
1504
                        ]
1505
                    )
1506
                    && !$context->vars_in_scope[
1507
                            $array_var_id . '[' . $offset_type_part->value . ']'
1508
                        ]->possibly_undefined
1509
                ) {
1510
                    $found_match = true;
1511
                }
1512
            }
1513
1514
            if (!$found_match) {
1515
                if (IssueBuffer::accepts(
1516
                    new PossiblyUndefinedIntArrayOffset(
1517
                        'Possibly undefined array offset \''
1518
                            . $offset_type->getId() . '\' '
1519
                            . 'is risky given expected type \''
1520
                            . $expected_offset_type->getId() . '\'.'
1521
                            . ' Consider using isset beforehand.',
1522
                        new CodeLocation($statements_analyzer->getSource(), $stmt)
1523
                    ),
1524
                    $statements_analyzer->getSuppressedIssues()
1525
                )) {
1526
                    // fall through
1527
                }
1528
            }
1529
        }
1530
    }
1531
1532
    private static function checkLiteralStringArrayOffset(
1533
        Type\Union $offset_type,
1534
        Type\Union $expected_offset_type,
1535
        ?string $array_var_id,
1536
        PhpParser\Node\Expr\ArrayDimFetch $stmt,
1537
        Context $context,
1538
        StatementsAnalyzer $statements_analyzer
1539
    ) : void {
1540
        if ($context->inside_isset || $context->inside_unset) {
1541
            return;
1542
        }
1543
1544
        if ($offset_type->hasLiteralString() && !$expected_offset_type->hasLiteralClassString()) {
1545
            $found_match = false;
1546
1547
            foreach ($offset_type->getAtomicTypes() as $offset_type_part) {
1548
                if ($array_var_id
1549
                    && $offset_type_part instanceof TLiteralString
1550
                    && isset(
1551
                        $context->vars_in_scope[
1552
                            $array_var_id . '[\'' . $offset_type_part->value . '\']'
1553
                        ]
1554
                    )
1555
                    && !$context->vars_in_scope[
1556
                            $array_var_id . '[\'' . $offset_type_part->value . '\']'
1557
                        ]->possibly_undefined
1558
                ) {
1559
                    $found_match = true;
1560
                }
1561
            }
1562
1563
            if (!$found_match) {
1564
                if (IssueBuffer::accepts(
1565
                    new PossiblyUndefinedStringArrayOffset(
1566
                        'Possibly undefined array offset \''
1567
                            . $offset_type->getId() . '\' '
1568
                            . 'is risky given expected type \''
1569
                            . $expected_offset_type->getId() . '\'.'
1570
                            . ' Consider using isset beforehand.',
1571
                        new CodeLocation($statements_analyzer->getSource(), $stmt)
1572
                    ),
1573
                    $statements_analyzer->getSuppressedIssues()
1574
                )) {
1575
                    // fall through
1576
                }
1577
            }
1578
        }
1579
    }
1580
1581
    /**
1582
     * @return Type\Union
1583
     */
1584
    public static function replaceOffsetTypeWithInts(Type\Union $offset_type)
1585
    {
1586
        $offset_types = $offset_type->getAtomicTypes();
1587
1588
        $cloned = false;
1589
1590
        foreach ($offset_types as $key => $offset_type_part) {
1591
            if ($offset_type_part instanceof Type\Atomic\TLiteralString) {
1592
                if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) {
1593
                    if (!$cloned) {
1594
                        $offset_type = clone $offset_type;
1595
                        $cloned = true;
1596
                    }
1597
                    $offset_type->addType(new Type\Atomic\TLiteralInt((int) $offset_type_part->value));
1598
                    $offset_type->removeType($key);
1599
                }
1600
            } elseif ($offset_type_part instanceof Type\Atomic\TBool) {
1601
                if (!$cloned) {
1602
                    $offset_type = clone $offset_type;
1603
                    $cloned = true;
1604
                }
1605
1606
                if ($offset_type_part instanceof Type\Atomic\TFalse) {
1607
                    if (!$offset_type->ignore_falsable_issues) {
1608
                        $offset_type->addType(new Type\Atomic\TLiteralInt(0));
1609
                        $offset_type->removeType($key);
1610
                    }
1611
                } elseif ($offset_type_part instanceof Type\Atomic\TTrue) {
1612
                    $offset_type->addType(new Type\Atomic\TLiteralInt(1));
1613
                    $offset_type->removeType($key);
1614
                } else {
1615
                    $offset_type->addType(new Type\Atomic\TLiteralInt(0));
1616
                    $offset_type->addType(new Type\Atomic\TLiteralInt(1));
1617
                    $offset_type->removeType($key);
1618
                }
1619
            }
1620
        }
1621
1622
        return $offset_type;
1623
    }
1624
}
1625