getArrayAccessTypeGivenOffset()   F
last analyzed

Complexity

Conditions 251
Paths > 20000

Size

Total Lines 1120

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 251
nc 4294967295
nop 9
dl 0
loc 1120
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\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