AssignmentAnalyzer   F
last analyzed

Complexity

Total Complexity 295

Size/Duplication

Total Lines 1351
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 61

Importance

Changes 0
Metric Value
dl 0
loc 1351
rs 0.8
c 0
b 0
f 0
wmc 295
lcom 1
cbo 61

4 Methods

Rating   Name   Duplication   Size   Complexity  
F analyze() 0 925 205
F analyzeAssignmentOperation() 0 242 63
B analyzeAssignmentRef() 0 43 5
F assignByRefParam() 0 104 22

How to fix   Complexity   

Complex Class

Complex classes like AssignmentAnalyzer 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 AssignmentAnalyzer, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\CommentAnalyzer;
6
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
7
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\ArrayAssignmentAnalyzer;
8
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
9
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\StaticPropertyAssignmentAnalyzer;
10
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
11
use Psalm\Internal\Analyzer\StatementsAnalyzer;
12
use Psalm\Internal\Analyzer\TypeAnalyzer;
13
use Psalm\CodeLocation;
14
use Psalm\Context;
15
use Psalm\Exception\DocblockParseException;
16
use Psalm\Exception\IncorrectDocblockException;
17
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
18
use Psalm\Issue\AssignmentToVoid;
19
use Psalm\Issue\ImpureByReferenceAssignment;
20
use Psalm\Issue\ImpurePropertyAssignment;
21
use Psalm\Issue\InvalidArrayAccess;
22
use Psalm\Issue\InvalidArrayOffset;
23
use Psalm\Issue\InvalidDocblock;
24
use Psalm\Issue\InvalidScope;
25
use Psalm\Issue\LoopInvalidation;
26
use Psalm\Issue\MissingDocblockType;
27
use Psalm\Issue\MixedAssignment;
28
use Psalm\Issue\MixedArrayAccess;
29
use Psalm\Issue\NoValue;
30
use Psalm\Issue\PossiblyInvalidArrayAccess;
31
use Psalm\Issue\PossiblyNullArrayAccess;
32
use Psalm\Issue\PossiblyUndefinedArrayOffset;
33
use Psalm\Issue\ReferenceConstraintViolation;
34
use Psalm\Issue\UnnecessaryVarAnnotation;
35
use Psalm\IssueBuffer;
36
use Psalm\Type;
37
use function is_string;
38
use function strpos;
39
use function strtolower;
40
use function substr;
41
42
/**
43
 * @internal
44
 */
45
class AssignmentAnalyzer
46
{
47
    /**
48
     * @param  StatementsAnalyzer        $statements_analyzer
49
     * @param  PhpParser\Node\Expr      $assign_var
50
     * @param  PhpParser\Node\Expr|null $assign_value  This has to be null to support list destructuring
51
     * @param  Type\Union|null          $assign_value_type
52
     * @param  Context                  $context
53
     * @param  ?PhpParser\Comment\Doc   $doc_comment
0 ignored issues
show
Documentation introduced by
The doc-type ?PhpParser\Comment\Doc could not be parsed: Unknown type name "?PhpParser\Comment\Doc" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
54
     *
55
     * @return false|Type\Union
56
     */
57
    public static function analyze(
58
        StatementsAnalyzer $statements_analyzer,
59
        PhpParser\Node\Expr $assign_var,
60
        $assign_value,
61
        $assign_value_type,
62
        Context $context,
63
        ?PhpParser\Comment\Doc $doc_comment
64
    ) {
65
        $var_id = ExpressionIdentifier::getVarId(
66
            $assign_var,
67
            $statements_analyzer->getFQCLN(),
68
            $statements_analyzer
69
        );
70
71
        // gets a variable id that *may* contain array keys
72
        $array_var_id = ExpressionIdentifier::getArrayVarId(
73
            $assign_var,
74
            $statements_analyzer->getFQCLN(),
75
            $statements_analyzer
76
        );
77
78
        $var_comments = [];
79
        $comment_type = null;
80
        $comment_type_location = null;
81
82
        $was_in_assignment = $context->inside_assignment;
83
84
        $context->inside_assignment = true;
85
86
        $codebase = $statements_analyzer->getCodebase();
87
88
        $removed_taints = [];
89
90
        if ($doc_comment) {
91
            $file_path = $statements_analyzer->getRootFilePath();
92
93
            $file_storage_provider = $codebase->file_storage_provider;
94
95
            $file_storage = $file_storage_provider->get($file_path);
96
97
            $template_type_map = $statements_analyzer->getTemplateTypeMap();
98
99
            try {
100
                $var_comments = CommentAnalyzer::getTypeFromComment(
101
                    $doc_comment,
102
                    $statements_analyzer->getSource(),
103
                    $statements_analyzer->getAliases(),
104
                    $template_type_map,
105
                    $file_storage->type_aliases
106
                );
107
            } catch (IncorrectDocblockException $e) {
108
                if (IssueBuffer::accepts(
109
                    new MissingDocblockType(
110
                        (string)$e->getMessage(),
111
                        new CodeLocation($statements_analyzer->getSource(), $assign_var)
112
                    )
113
                )) {
114
                    // fall through
115
                }
116
            } catch (DocblockParseException $e) {
117
                if (IssueBuffer::accepts(
118
                    new InvalidDocblock(
119
                        (string)$e->getMessage(),
120
                        new CodeLocation($statements_analyzer->getSource(), $assign_var)
121
                    )
122
                )) {
123
                    // fall through
124
                }
125
            }
126
127
            foreach ($var_comments as $var_comment) {
128
                if ($var_comment->removed_taints) {
129
                    $removed_taints = $var_comment->removed_taints;
130
                }
131
132
                if (!$var_comment->type) {
133
                    continue;
134
                }
135
136
                try {
137
                    $var_comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
138
                        $codebase,
139
                        $var_comment->type,
140
                        $context->self,
141
                        $context->self,
142
                        $statements_analyzer->getParentFQCLN()
143
                    );
144
145
                    $var_comment_type->setFromDocblock();
146
147
                    $var_comment_type->check(
148
                        $statements_analyzer,
149
                        new CodeLocation($statements_analyzer->getSource(), $assign_var),
150
                        $statements_analyzer->getSuppressedIssues(),
151
                        [],
152
                        false,
153
                        false,
154
                        false,
155
                        $context->calling_method_id
156
                    );
157
158
                    $type_location = null;
159
160
                    if ($var_comment->type_start
161
                        && $var_comment->type_end
162
                        && $var_comment->line_number
163
                    ) {
164
                        $type_location = new CodeLocation\DocblockTypeLocation(
165
                            $statements_analyzer,
166
                            $var_comment->type_start,
167
                            $var_comment->type_end,
168
                            $var_comment->line_number
169
                        );
170
171
                        if ($codebase->alter_code) {
172
                            $codebase->classlikes->handleDocblockTypeInMigration(
173
                                $codebase,
174
                                $statements_analyzer,
175
                                $var_comment_type,
176
                                $type_location,
177
                                $context->calling_method_id
178
                            );
179
                        }
180
                    }
181
182
                    if (!$var_comment->var_id || $var_comment->var_id === $var_id) {
183
                        $comment_type = $var_comment_type;
184
                        $comment_type_location = $type_location;
185
                        continue;
186
                    }
187
188
                    if ($codebase->find_unused_variables
189
                        && $type_location
190
                        && isset($context->vars_in_scope[$var_comment->var_id])
191
                        && $context->vars_in_scope[$var_comment->var_id]->getId() === $var_comment_type->getId()
192
                        && !$var_comment_type->isMixed()
193
                    ) {
194
                        $project_analyzer = $statements_analyzer->getProjectAnalyzer();
195
196
                        if ($codebase->alter_code
197
                            && isset($project_analyzer->getIssuesToFix()['UnnecessaryVarAnnotation'])
198
                        ) {
199
                            FileManipulationBuffer::addVarAnnotationToRemove($type_location);
200
                        } elseif (IssueBuffer::accepts(
201
                            new UnnecessaryVarAnnotation(
202
                                'The @var ' . $var_comment_type . ' annotation for '
203
                                    . $var_comment->var_id . ' is unnecessary',
204
                                $type_location
205
                            ),
206
                            $statements_analyzer->getSuppressedIssues(),
207
                            true
208
                        )) {
209
                            // fall through
210
                        }
211
                    }
212
213
                    $parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes ?? [];
214
                    $var_comment_type->parent_nodes = $parent_nodes;
215
216
                    $context->vars_in_scope[$var_comment->var_id] = $var_comment_type;
217
                } catch (\UnexpectedValueException $e) {
218
                    if (IssueBuffer::accepts(
219
                        new InvalidDocblock(
220
                            (string)$e->getMessage(),
221
                            new CodeLocation($statements_analyzer->getSource(), $assign_var)
222
                        )
223
                    )) {
224
                        // fall through
225
                    }
226
                }
227
            }
228
        }
229
230
        if ($array_var_id) {
231
            unset($context->referenced_var_ids[$array_var_id]);
232
            $context->assigned_var_ids[$array_var_id] = true;
233
            $context->possibly_assigned_var_ids[$array_var_id] = true;
234
        }
235
236
        if ($assign_value) {
237
            if ($var_id && $assign_value instanceof PhpParser\Node\Expr\Closure) {
238
                foreach ($assign_value->uses as $closure_use) {
239
                    if ($closure_use->byRef
240
                        && is_string($closure_use->var->name)
241
                        && $var_id === '$' . $closure_use->var->name
242
                    ) {
243
                        $context->vars_in_scope[$var_id] = Type::getClosure();
244
                        $context->vars_possibly_in_scope[$var_id] = true;
245
                    }
246
                }
247
            }
248
249
            if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_value, $context) === false) {
250
                if ($var_id) {
251
                    if ($array_var_id) {
252
                        $context->removeDescendents($array_var_id, null, $assign_value_type);
253
                    }
254
255
                    // if we're not exiting immediately, make everything mixed
256
                    $context->vars_in_scope[$var_id] = $comment_type ?: Type::getMixed();
257
                }
258
259
                return false;
260
            }
261
        }
262
263
        if ($comment_type && $comment_type_location) {
264
            $temp_assign_value_type = $assign_value_type
265
                ? $assign_value_type
266
                : ($assign_value ? $statements_analyzer->node_data->getType($assign_value) : null);
267
268
            if ($codebase->find_unused_variables
269
                && $temp_assign_value_type
270
                && $array_var_id
271
                && $temp_assign_value_type->getId() === $comment_type->getId()
272
                && !$comment_type->isMixed()
273
            ) {
274
                if ($codebase->alter_code
275
                    && isset($statements_analyzer->getProjectAnalyzer()->getIssuesToFix()['UnnecessaryVarAnnotation'])
276
                ) {
277
                    FileManipulationBuffer::addVarAnnotationToRemove($comment_type_location);
278
                } elseif (IssueBuffer::accepts(
279
                    new UnnecessaryVarAnnotation(
280
                        'The @var ' . $comment_type . ' annotation for '
281
                            . $array_var_id . ' is unnecessary',
282
                        $comment_type_location
283
                    )
284
                )) {
285
                    // fall through
286
                }
287
            }
288
289
            $assign_value_type = $comment_type;
290
            $assign_value_type->parent_nodes = $temp_assign_value_type->parent_nodes ?? [];
291
        } elseif (!$assign_value_type) {
292
            $assign_value_type = $assign_value
293
                ? ($statements_analyzer->node_data->getType($assign_value) ?: Type::getMixed())
294
                : Type::getMixed();
295
        }
296
297
        if ($array_var_id && isset($context->vars_in_scope[$array_var_id])) {
298
            if ($context->vars_in_scope[$array_var_id]->by_ref) {
299
                if ($context->mutation_free) {
300
                    if (IssueBuffer::accepts(
301
                        new ImpureByReferenceAssignment(
302
                            'Variable ' . $array_var_id . ' cannot be assigned to as it is passed by reference',
303
                            new CodeLocation($statements_analyzer->getSource(), $assign_var)
304
                        )
305
                    )) {
306
                        // fall through
307
                    }
308
                }
309
310
                $assign_value_type->by_ref = true;
311
            }
312
313
            // removes dependent vars from $context
314
            $context->removeDescendents(
315
                $array_var_id,
316
                $context->vars_in_scope[$array_var_id],
317
                $assign_value_type,
318
                $statements_analyzer
319
            );
320
        } else {
321
            $root_var_id = ExpressionIdentifier::getRootVarId(
322
                $assign_var,
323
                $statements_analyzer->getFQCLN(),
324
                $statements_analyzer
325
            );
326
327
            if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) {
328
                $context->removeVarFromConflictingClauses(
329
                    $root_var_id,
330
                    $context->vars_in_scope[$root_var_id],
331
                    $statements_analyzer
332
                );
333
            }
334
        }
335
336
        $codebase = $statements_analyzer->getCodebase();
337
338
        if ($assign_value_type->hasMixed()) {
339
            $root_var_id = ExpressionIdentifier::getRootVarId(
340
                $assign_var,
341
                $statements_analyzer->getFQCLN(),
342
                $statements_analyzer
343
            );
344
345
            if (!$context->collect_initializations
346
                && !$context->collect_mutations
347
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
348
                && (!(($parent_source = $statements_analyzer->getSource())
349
                            instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
350
                        || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
351
            ) {
352
                $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
353
            }
354
355
            if (!$assign_var instanceof PhpParser\Node\Expr\PropertyFetch
356
                && !strpos($root_var_id ?? '', '->')
357
                && !$comment_type
358
                && substr($var_id ?? '', 0, 2) !== '$_'
359
            ) {
360
                if (IssueBuffer::accepts(
361
                    new MixedAssignment(
362
                        $var_id
363
                            ? 'Unable to determine the type that ' . $var_id . ' is being assigned to'
364
                            : 'Unable to determine the type of this assignment',
365
                        new CodeLocation($statements_analyzer->getSource(), $assign_var)
366
                    ),
367
                    $statements_analyzer->getSuppressedIssues()
368
                )) {
369
                    // fall through
370
                }
371
            }
372
        } else {
373
            if (!$context->collect_initializations
374
                && !$context->collect_mutations
375
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
376
                && (!(($parent_source = $statements_analyzer->getSource())
377
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
378
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
379
            ) {
380
                $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
381
            }
382
383
            if ($var_id
384
                && isset($context->byref_constraints[$var_id])
385
                && ($outer_constraint_type = $context->byref_constraints[$var_id]->type)
386
            ) {
387
                if (!TypeAnalyzer::isContainedBy(
388
                    $codebase,
389
                    $assign_value_type,
390
                    $outer_constraint_type,
391
                    $assign_value_type->ignore_nullable_issues,
392
                    $assign_value_type->ignore_falsable_issues
393
                )
394
                ) {
395
                    if (IssueBuffer::accepts(
396
                        new ReferenceConstraintViolation(
397
                            'Variable ' . $var_id . ' is limited to values of type '
398
                                . $context->byref_constraints[$var_id]->type
399
                                . ' because it is passed by reference, '
400
                                . $assign_value_type->getId() . ' type found',
401
                            new CodeLocation($statements_analyzer->getSource(), $assign_var)
402
                        ),
403
                        $statements_analyzer->getSuppressedIssues()
404
                    )) {
405
                        // fall through
406
                    }
407
                }
408
            }
409
        }
410
411
        if ($var_id === '$this' && IssueBuffer::accepts(
412
            new InvalidScope(
413
                'Cannot re-assign ' . $var_id,
414
                new CodeLocation($statements_analyzer->getSource(), $assign_var)
415
            ),
416
            $statements_analyzer->getSuppressedIssues()
417
        )) {
418
            return false;
419
        }
420
421
        if (isset($context->protected_var_ids[$var_id])) {
422
            if (IssueBuffer::accepts(
423
                new LoopInvalidation(
424
                    'Variable ' . $var_id . ' has already been assigned in a for/foreach loop',
425
                    new CodeLocation($statements_analyzer->getSource(), $assign_var)
426
                ),
427
                $statements_analyzer->getSuppressedIssues()
428
            )) {
429
                // fall through
430
            }
431
        }
432
433
        if ($assign_var instanceof PhpParser\Node\Expr\Variable) {
434
            if (is_string($assign_var->name)) {
435
                if ($var_id) {
436
                    $context->vars_in_scope[$var_id] = $assign_value_type;
437
                    $context->vars_possibly_in_scope[$var_id] = true;
438
439
                    $location = new CodeLocation($statements_analyzer, $assign_var);
440
441
                    if ($codebase->find_unused_variables) {
442
                        $context->unreferenced_vars[$var_id] = [$location->getHash() => $location];
443
                    }
444
445
                    if (!$statements_analyzer->hasVariable($var_id)) {
446
                        $statements_analyzer->registerVariable(
447
                            $var_id,
448
                            $location,
449
                            $context->branch_point
450
                        );
451
                    } else {
452
                        $statements_analyzer->registerVariableAssignment(
453
                            $var_id,
454
                            $location
455
                        );
456
                    }
457
458
                    if ($codebase->store_node_types
459
                        && !$context->collect_initializations
460
                        && !$context->collect_mutations
461
                    ) {
462
                        $location = new CodeLocation($statements_analyzer, $assign_var);
463
                        $codebase->analyzer->addNodeReference(
464
                            $statements_analyzer->getFilePath(),
465
                            $assign_var,
466
                            $location->raw_file_start
467
                                . '-' . $location->raw_file_end
468
                                . ':' . $assign_value_type->getId()
469
                        );
470
                    }
471
472
                    if (isset($context->byref_constraints[$var_id]) || $assign_value_type->by_ref) {
473
                        $statements_analyzer->registerVariableUses([$location->getHash() => $location]);
474
                    }
475
                }
476
            } else {
477
                if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->name, $context) === false) {
478
                    return false;
479
                }
480
            }
481
        } elseif ($assign_var instanceof PhpParser\Node\Expr\List_
482
            || $assign_var instanceof PhpParser\Node\Expr\Array_
483
        ) {
484
            if (!$assign_value_type->hasArray()
485
                && !$assign_value_type->isMixed()
486
                && !$assign_value_type->hasArrayAccessInterface($codebase)
487
            ) {
488
                if (IssueBuffer::accepts(
489
                    new InvalidArrayOffset(
490
                        'Cannot destructure non-array of type ' . $assign_value_type->getId(),
491
                        new CodeLocation($statements_analyzer->getSource(), $assign_var)
492
                    ),
493
                    $statements_analyzer->getSuppressedIssues()
494
                )) {
495
                    // fall through
496
                }
497
            }
498
499
            $can_be_empty = true;
500
501
            foreach ($assign_var->items as $offset => $assign_var_item) {
502
                // $assign_var_item can be null e.g. list($a, ) = ['a', 'b']
503
                if (!$assign_var_item) {
504
                    continue;
505
                }
506
507
                $var = $assign_var_item->value;
508
509
                if ($assign_value instanceof PhpParser\Node\Expr\Array_
510
                    && $statements_analyzer->node_data->getType($assign_var_item->value)
511
                ) {
512
                    self::analyze(
513
                        $statements_analyzer,
514
                        $var,
515
                        $assign_var_item->value,
516
                        null,
517
                        $context,
518
                        $doc_comment
519
                    );
520
521
                    continue;
522
                }
523
524
                $list_var_id = ExpressionIdentifier::getArrayVarId(
525
                    $var,
526
                    $statements_analyzer->getFQCLN(),
527
                    $statements_analyzer
528
                );
529
530
                $new_assign_type = null;
531
                $assigned = false;
532
                $has_null = false;
533
534
                foreach ($assign_value_type->getAtomicTypes() as $assign_value_atomic_type) {
535
                    if ($assign_value_atomic_type instanceof Type\Atomic\ObjectLike
536
                        && !$assign_var_item->key
537
                    ) {
538
                        // if object-like has int offsets
539
                        if (isset($assign_value_atomic_type->properties[$offset])) {
540
                            $offset_type = $assign_value_atomic_type->properties[(string)$offset];
541
542
                            if ($offset_type->possibly_undefined) {
543
                                if (IssueBuffer::accepts(
544
                                    new PossiblyUndefinedArrayOffset(
545
                                        'Possibly undefined array key',
546
                                        new CodeLocation($statements_analyzer->getSource(), $var)
547
                                    ),
548
                                    $statements_analyzer->getSuppressedIssues()
549
                                )) {
550
                                    // fall through
551
                                }
552
553
                                $offset_type = clone $offset_type;
554
                                $offset_type->possibly_undefined = false;
555
                            }
556
557
                            self::analyze(
558
                                $statements_analyzer,
559
                                $var,
560
                                null,
561
                                $offset_type,
562
                                $context,
563
                                $doc_comment
564
                            );
565
566
                            $assigned = true;
567
568
                            continue;
569
                        }
570
571
                        if ($assign_value_atomic_type->sealed) {
572
                            if (IssueBuffer::accepts(
573
                                new InvalidArrayOffset(
574
                                    'Cannot access value with offset ' . $offset,
575
                                    new CodeLocation($statements_analyzer->getSource(), $var)
576
                                ),
577
                                $statements_analyzer->getSuppressedIssues()
578
                            )) {
579
                                // fall through
580
                            }
581
                        }
582
                    }
583
584
                    if ($assign_value_atomic_type instanceof Type\Atomic\TMixed) {
585
                        if (IssueBuffer::accepts(
586
                            new MixedArrayAccess(
587
                                'Cannot access array value on mixed variable ' . $array_var_id,
588
                                new CodeLocation($statements_analyzer->getSource(), $var)
589
                            ),
590
                            $statements_analyzer->getSuppressedIssues()
591
                        )) {
592
                            // fall through
593
                        }
594
                    } elseif ($assign_value_atomic_type instanceof Type\Atomic\TNull) {
595
                        $has_null = true;
596
597
                        if (IssueBuffer::accepts(
598
                            new PossiblyNullArrayAccess(
599
                                'Cannot access array value on null variable ' . $array_var_id,
600
                                new CodeLocation($statements_analyzer->getSource(), $var)
601
                            ),
602
                            $statements_analyzer->getSuppressedIssues()
603
                        )
604
                        ) {
605
                            // do nothing
606
                        }
607
                    } elseif (!$assign_value_atomic_type instanceof Type\Atomic\TArray
608
                        && !$assign_value_atomic_type instanceof Type\Atomic\ObjectLike
609
                        && !$assign_value_atomic_type instanceof Type\Atomic\TList
610
                        && !$assign_value_type->hasArrayAccessInterface($codebase)
611
                    ) {
612
                        if ($assign_value_type->hasArray()) {
613
                            if (($assign_value_atomic_type instanceof Type\Atomic\TFalse
614
                                    && $assign_value_type->ignore_falsable_issues)
615
                                || ($assign_value_atomic_type instanceof Type\Atomic\TNull
616
                                    && $assign_value_type->ignore_nullable_issues)
617
                            ) {
618
                                // do nothing
619
                            } elseif (IssueBuffer::accepts(
620
                                new PossiblyInvalidArrayAccess(
621
                                    'Cannot access array value on non-array variable '
622
                                        . $array_var_id . ' of type ' . $assign_value_atomic_type->getId(),
623
                                    new CodeLocation($statements_analyzer->getSource(), $var)
624
                                ),
625
                                $statements_analyzer->getSuppressedIssues()
626
                            )
627
                            ) {
628
                                // do nothing
629
                            }
630
                        } else {
631
                            if (IssueBuffer::accepts(
632
                                new InvalidArrayAccess(
633
                                    'Cannot access array value on non-array variable '
634
                                        . $array_var_id . ' of type ' . $assign_value_atomic_type->getId(),
635
                                    new CodeLocation($statements_analyzer->getSource(), $var)
636
                                ),
637
                                $statements_analyzer->getSuppressedIssues()
638
                            )
639
                            ) {
640
                                // do nothing
641
                            }
642
                        }
643
                    }
644
645
                    if ($var instanceof PhpParser\Node\Expr\List_
646
                        || $var instanceof PhpParser\Node\Expr\Array_
647
                    ) {
648
                        if ($assign_value_atomic_type instanceof Type\Atomic\ObjectLike) {
649
                            $assign_value_atomic_type = $assign_value_atomic_type->getGenericArrayType();
650
                        }
651
652
                        if ($assign_value_atomic_type instanceof Type\Atomic\TList) {
653
                            $assign_value_atomic_type = new Type\Atomic\TArray([
654
                                Type::getInt(),
655
                                $assign_value_atomic_type->type_param
656
                            ]);
657
                        }
658
659
                        self::analyze(
660
                            $statements_analyzer,
661
                            $var,
662
                            null,
663
                            $assign_value_atomic_type instanceof Type\Atomic\TArray
664
                                ? clone $assign_value_atomic_type->type_params[1]
665
                                : Type::getMixed(),
666
                            $context,
667
                            $doc_comment
668
                        );
669
670
                        continue;
671
                    }
672
673
                    if ($list_var_id) {
674
                        $context->vars_possibly_in_scope[$list_var_id] = true;
675
                        $context->assigned_var_ids[$list_var_id] = true;
676
                        $context->possibly_assigned_var_ids[$list_var_id] = true;
677
678
                        $already_in_scope = isset($context->vars_in_scope[$list_var_id]);
679
680
                        if (strpos($list_var_id, '-') === false && strpos($list_var_id, '[') === false) {
681
                            $location = new CodeLocation($statements_analyzer, $var);
682
683
                            if ($codebase->find_unused_variables) {
684
                                $context->unreferenced_vars[$list_var_id] = [$location->getHash() => $location];
685
                            }
686
687
                            if (!$statements_analyzer->hasVariable($list_var_id)) {
688
                                $statements_analyzer->registerVariable(
689
                                    $list_var_id,
690
                                    $location,
691
                                    $context->branch_point
692
                                );
693
                            } else {
694
                                $statements_analyzer->registerVariableAssignment(
695
                                    $list_var_id,
696
                                    $location
697
                                );
698
                            }
699
700
                            if (isset($context->byref_constraints[$list_var_id])) {
701
                                $statements_analyzer->registerVariableUses([$location->getHash() => $location]);
702
                            }
703
                        }
704
705
                        if ($assign_value_atomic_type instanceof Type\Atomic\TArray) {
706
                            $new_assign_type = clone $assign_value_atomic_type->type_params[1];
707
708
                            $can_be_empty = !$assign_value_atomic_type instanceof Type\Atomic\TNonEmptyArray;
709
                        } elseif ($assign_value_atomic_type instanceof Type\Atomic\TList) {
710
                            $new_assign_type = clone $assign_value_atomic_type->type_param;
711
712
                            $can_be_empty = !$assign_value_atomic_type instanceof Type\Atomic\TNonEmptyList;
713
                        } elseif ($assign_value_atomic_type instanceof Type\Atomic\ObjectLike) {
714
                            if ($assign_var_item->key
715
                                && ($assign_var_item->key instanceof PhpParser\Node\Scalar\String_
716
                                    || $assign_var_item->key instanceof PhpParser\Node\Scalar\LNumber)
717
                                && isset($assign_value_atomic_type->properties[$assign_var_item->key->value])
718
                            ) {
719
                                $new_assign_type =
720
                                    clone $assign_value_atomic_type->properties[$assign_var_item->key->value];
721
722
                                if ($new_assign_type->possibly_undefined) {
723
                                    if (IssueBuffer::accepts(
724
                                        new PossiblyUndefinedArrayOffset(
725
                                            'Possibly undefined array key',
726
                                            new CodeLocation($statements_analyzer->getSource(), $var)
727
                                        ),
728
                                        $statements_analyzer->getSuppressedIssues()
729
                                    )) {
730
                                        // fall through
731
                                    }
732
733
                                    $new_assign_type->possibly_undefined = false;
734
                                }
735
                            }
736
737
                            $can_be_empty = !$assign_value_atomic_type->sealed;
738
                        } elseif ($assign_value_atomic_type->hasArrayAccessInterface($codebase)) {
739
                            ForeachAnalyzer::getKeyValueParamsForTraversableObject(
740
                                $assign_value_atomic_type,
741
                                $codebase,
742
                                $array_access_key_type,
743
                                $array_access_value_type
744
                            );
745
746
                            $new_assign_type = $array_access_value_type;
747
                        }
748
749
                        if ($already_in_scope) {
750
                            // removes dependennt vars from $context
751
                            $context->removeDescendents(
752
                                $list_var_id,
753
                                $context->vars_in_scope[$list_var_id],
754
                                $new_assign_type,
755
                                $statements_analyzer
756
                            );
757
                        }
758
                    }
759
                }
760
761
                if (!$assigned) {
762
                    foreach ($var_comments as $var_comment) {
763
                        if (!$var_comment->type) {
764
                            continue;
765
                        }
766
767
                        try {
768
                            if ($var_comment->var_id === $list_var_id) {
769
                                $var_comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
770
                                    $codebase,
771
                                    $var_comment->type,
772
                                    $context->self,
773
                                    $context->self,
774
                                    $statements_analyzer->getParentFQCLN()
775
                                );
776
777
                                $var_comment_type->setFromDocblock();
778
779
                                $new_assign_type = $var_comment_type;
780
                                break;
781
                            }
782
                        } catch (\UnexpectedValueException $e) {
783
                            if (IssueBuffer::accepts(
784
                                new InvalidDocblock(
785
                                    (string)$e->getMessage(),
786
                                    new CodeLocation($statements_analyzer->getSource(), $assign_var)
787
                                )
788
                            )) {
789
                                // fall through
790
                            }
791
                        }
792
                    }
793
794
                    if ($list_var_id) {
795
                        $context->vars_in_scope[$list_var_id] = $new_assign_type ?: Type::getMixed();
796
797
                        if (($context->error_suppressing && ($offset || $can_be_empty))
798
                            || $has_null
799
                        ) {
800
                            $context->vars_in_scope[$list_var_id]->addType(new Type\Atomic\TNull);
801
                        }
802
                    }
803
                }
804
            }
805
        } elseif ($assign_var instanceof PhpParser\Node\Expr\ArrayDimFetch) {
806
            ArrayAssignmentAnalyzer::analyze(
807
                $statements_analyzer,
808
                $assign_var,
809
                $context,
810
                $assign_value,
811
                $assign_value_type
812
            );
813
        } elseif ($assign_var instanceof PhpParser\Node\Expr\PropertyFetch) {
814
            if (!$assign_var->name instanceof PhpParser\Node\Identifier) {
815
                // this can happen when the user actually means to type $this-><autocompleted>, but there's
816
                // a variable on the next line
817
                if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->var, $context) === false) {
818
                    return false;
819
                }
820
821
                if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->name, $context) === false) {
822
                    return false;
823
                }
824
            }
825
826
            if ($assign_var->name instanceof PhpParser\Node\Identifier) {
827
                $prop_name = $assign_var->name->name;
828
            } elseif (($assign_var_name_type = $statements_analyzer->node_data->getType($assign_var->name))
829
                && $assign_var_name_type->isSingleStringLiteral()
830
            ) {
831
                $prop_name = $assign_var_name_type->getSingleStringLiteral()->value;
832
            } else {
833
                $prop_name = null;
834
            }
835
836
            if ($prop_name) {
837
                InstancePropertyAssignmentAnalyzer::analyze(
838
                    $statements_analyzer,
839
                    $assign_var,
840
                    $prop_name,
841
                    $assign_value,
842
                    $assign_value_type,
843
                    $context
844
                );
845
            } else {
846
                if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->var, $context) === false) {
847
                    return false;
848
                }
849
850
                if (($assign_var_type = $statements_analyzer->node_data->getType($assign_var->var))
851
                    && !$context->ignore_variable_property
852
                ) {
853
                    $stmt_var_type = $assign_var_type;
854
855
                    if ($stmt_var_type->hasObjectType()) {
856
                        foreach ($stmt_var_type->getAtomicTypes() as $type) {
857
                            if ($type instanceof Type\Atomic\TNamedObject) {
858
                                $codebase->analyzer->addMixedMemberName(
859
                                    strtolower($type->value) . '::$',
860
                                    $context->calling_method_id ?: $statements_analyzer->getFileName()
861
                                );
862
                            }
863
                        }
864
                    }
865
                }
866
            }
867
868
            if ($var_id) {
869
                $context->vars_possibly_in_scope[$var_id] = true;
870
            }
871
872
            $property_var_pure_compatible = $statements_analyzer->node_data->isPureCompatible($assign_var->var);
873
874
            // prevents writing to any properties in a mutation-free context
875
            if (($context->mutation_free || $context->external_mutation_free)
876
                && !$property_var_pure_compatible
877
                && !$context->collect_mutations
878
                && !$context->collect_initializations
879
            ) {
880
                if (IssueBuffer::accepts(
881
                    new ImpurePropertyAssignment(
882
                        'Cannot assign to a property from a mutation-free context',
883
                        new CodeLocation($statements_analyzer, $assign_var)
884
                    ),
885
                    $statements_analyzer->getSuppressedIssues()
886
                )) {
887
                    // fall through
888
                }
889
            }
890
        } elseif ($assign_var instanceof PhpParser\Node\Expr\StaticPropertyFetch &&
891
            $assign_var->class instanceof PhpParser\Node\Name
892
        ) {
893
            if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var, $context) === false) {
894
                return false;
895
            }
896
897
            if ($context->check_classes) {
898
                StaticPropertyAssignmentAnalyzer::analyze(
899
                    $statements_analyzer,
900
                    $assign_var,
901
                    $assign_value,
902
                    $assign_value_type,
903
                    $context
904
                );
905
            }
906
907
            if ($var_id) {
908
                $context->vars_possibly_in_scope[$var_id] = true;
909
            }
910
        }
911
912
        if ($var_id && isset($context->vars_in_scope[$var_id])) {
913
            if ($context->vars_in_scope[$var_id]->isVoid()) {
914
                if (IssueBuffer::accepts(
915
                    new AssignmentToVoid(
916
                        'Cannot assign ' . $var_id . ' to type void',
917
                        new CodeLocation($statements_analyzer->getSource(), $assign_var)
918
                    ),
919
                    $statements_analyzer->getSuppressedIssues()
920
                )) {
921
                    // fall through
922
                }
923
924
                $context->vars_in_scope[$var_id] = Type::getNull();
925
926
                if (!$was_in_assignment) {
927
                    $context->inside_assignment = false;
928
                }
929
930
                return $context->vars_in_scope[$var_id];
931
            }
932
933
            if ($context->vars_in_scope[$var_id]->isNever()) {
934
                if (IssueBuffer::accepts(
935
                    new NoValue(
936
                        'This function or method call never returns output',
937
                        new CodeLocation($statements_analyzer->getSource(), $assign_var)
938
                    ),
939
                    $statements_analyzer->getSuppressedIssues()
940
                )) {
941
                    return false;
942
                }
943
944
                $context->vars_in_scope[$var_id] = Type::getEmpty();
945
946
                if (!$was_in_assignment) {
947
                    $context->inside_assignment = false;
948
                }
949
950
                return $context->vars_in_scope[$var_id];
951
            }
952
953
            if ($codebase->taint
954
                && $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
955
            ) {
956
                if ($context->vars_in_scope[$var_id]->parent_nodes) {
957
                    if (\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())) {
958
                        $context->vars_in_scope[$var_id]->parent_nodes = [];
959
                    } else {
960
                        $var_location = new CodeLocation($statements_analyzer->getSource(), $assign_var);
961
962
                        $new_parent_node = \Psalm\Internal\Taint\TaintNode::getForAssignment($var_id, $var_location);
963
964
                        $codebase->taint->addTaintNode($new_parent_node);
965
966
                        foreach ($context->vars_in_scope[$var_id]->parent_nodes as $parent_node) {
967
                            $codebase->taint->addPath($parent_node, $new_parent_node, '=', [], $removed_taints);
968
                        }
969
970
                        $context->vars_in_scope[$var_id]->parent_nodes = [$new_parent_node];
971
                    }
972
                }
973
            }
974
        }
975
976
        if (!$was_in_assignment) {
977
            $context->inside_assignment = false;
978
        }
979
980
        return $assign_value_type;
981
    }
982
983
    /**
984
     * @param   StatementsAnalyzer               $statements_analyzer
985
     * @param   PhpParser\Node\Expr\AssignOp    $stmt
986
     * @param   Context                         $context
987
     *
988
     * @return  bool
989
     */
990
    public static function analyzeAssignmentOperation(
991
        StatementsAnalyzer $statements_analyzer,
992
        PhpParser\Node\Expr\AssignOp $stmt,
993
        Context $context
994
    ) {
995
        $array_var_id = ExpressionIdentifier::getArrayVarId(
996
            $stmt->var,
997
            $statements_analyzer->getFQCLN(),
998
            $statements_analyzer
999
        );
1000
1001
        if ($stmt instanceof PhpParser\Node\Expr\AssignOp\Coalesce) {
1002
            $old_data_provider = $statements_analyzer->node_data;
1003
1004
            $statements_analyzer->node_data = clone $statements_analyzer->node_data;
1005
1006
            $fake_coalesce_expr = new PhpParser\Node\Expr\BinaryOp\Coalesce(
1007
                $stmt->var,
1008
                $stmt->expr,
1009
                $stmt->getAttributes()
1010
            );
1011
1012
            $fake_coalesce_type = AssignmentAnalyzer::analyze(
1013
                $statements_analyzer,
1014
                $stmt->var,
1015
                $fake_coalesce_expr,
1016
                null,
1017
                $context,
1018
                $stmt->getDocComment()
1019
            );
1020
1021
            $statements_analyzer->node_data = $old_data_provider;
1022
1023
            if ($fake_coalesce_type) {
1024
                if ($array_var_id) {
1025
                    $context->vars_in_scope[$array_var_id] = $fake_coalesce_type;
1026
                }
1027
1028
                $statements_analyzer->node_data->setType($stmt, $fake_coalesce_type);
1029
            }
1030
1031
            return true;
1032
        }
1033
1034
        $was_in_assignment = $context->inside_assignment;
1035
1036
        $context->inside_assignment = true;
1037
1038
        if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->var, $context) === false) {
1039
            return false;
1040
        }
1041
1042
        if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
1043
            return false;
1044
        }
1045
1046
        if ($array_var_id
1047
            && $context->mutation_free
1048
            && $stmt->var instanceof PhpParser\Node\Expr\PropertyFetch
1049
            && ($stmt_var_var_type = $statements_analyzer->node_data->getType($stmt->var->var))
1050
            && !$stmt_var_var_type->reference_free
1051
        ) {
1052
            if (IssueBuffer::accepts(
1053
                new ImpurePropertyAssignment(
1054
                    'Cannot assign to a property from a mutation-free context',
1055
                    new CodeLocation($statements_analyzer, $stmt->var)
1056
                ),
1057
                $statements_analyzer->getSuppressedIssues()
1058
            )) {
1059
                // fall through
1060
            }
1061
        }
1062
1063
        $codebase = $statements_analyzer->getCodebase();
1064
1065
        if ($array_var_id) {
1066
            $context->assigned_var_ids[$array_var_id] = true;
1067
            $context->possibly_assigned_var_ids[$array_var_id] = true;
1068
1069
            if ($codebase->find_unused_variables && $stmt->var instanceof PhpParser\Node\Expr\Variable) {
1070
                $location = new CodeLocation($statements_analyzer, $stmt->var);
1071
                $statements_analyzer->registerVariableAssignment(
1072
                    $array_var_id,
1073
                    $location
1074
                );
1075
                $context->unreferenced_vars[$array_var_id] = [$location->getHash() => $location];
1076
            }
1077
        }
1078
1079
        $stmt_var_type = $statements_analyzer->node_data->getType($stmt->var);
1080
        $stmt_var_type = $stmt_var_type ? clone $stmt_var_type: null;
1081
1082
        $stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr);
1083
        $result_type = null;
1084
1085
        if ($stmt instanceof PhpParser\Node\Expr\AssignOp\Plus
1086
            || $stmt instanceof PhpParser\Node\Expr\AssignOp\Minus
1087
            || $stmt instanceof PhpParser\Node\Expr\AssignOp\Mod
1088
            || $stmt instanceof PhpParser\Node\Expr\AssignOp\Mul
1089
            || $stmt instanceof PhpParser\Node\Expr\AssignOp\Pow
1090
        ) {
1091
            BinaryOp\NonDivArithmeticOpAnalyzer::analyze(
1092
                $statements_analyzer,
1093
                $statements_analyzer->node_data,
1094
                $stmt->var,
1095
                $stmt->expr,
1096
                $stmt,
1097
                $result_type,
1098
                $context
1099
            );
1100
1101
            if ($stmt->var instanceof PhpParser\Node\Expr\ArrayDimFetch) {
1102
                ArrayAssignmentAnalyzer::analyze(
1103
                    $statements_analyzer,
1104
                    $stmt->var,
1105
                    $context,
1106
                    $stmt->expr,
1107
                    $result_type ?: Type::getMixed($context->inside_loop)
1108
                );
1109
            } elseif ($result_type && $array_var_id) {
1110
                $context->vars_in_scope[$array_var_id] = $result_type;
1111
                $statements_analyzer->node_data->setType($stmt, clone $context->vars_in_scope[$array_var_id]);
1112
            }
1113
        } elseif ($stmt instanceof PhpParser\Node\Expr\AssignOp\Div
1114
            && $stmt_var_type
1115
            && $stmt_expr_type
1116
            && $stmt_var_type->hasDefinitelyNumericType()
1117
            && $stmt_expr_type->hasDefinitelyNumericType()
1118
            && $array_var_id
1119
        ) {
1120
            $context->vars_in_scope[$array_var_id] = Type::combineUnionTypes(Type::getFloat(), Type::getInt());
1121
            $statements_analyzer->node_data->setType($stmt, clone $context->vars_in_scope[$array_var_id]);
1122
        } elseif ($stmt instanceof PhpParser\Node\Expr\AssignOp\Concat) {
1123
            BinaryOp\ConcatAnalyzer::analyze(
1124
                $statements_analyzer,
1125
                $stmt->var,
1126
                $stmt->expr,
1127
                $context,
1128
                $result_type
1129
            );
1130
1131
            if ($result_type && $array_var_id) {
1132
                $context->vars_in_scope[$array_var_id] = $result_type;
1133
                $statements_analyzer->node_data->setType($stmt, clone $context->vars_in_scope[$array_var_id]);
1134
1135
                if ($codebase->taint
1136
                    && $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
1137
                    && !\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
1138
                ) {
1139
                    $stmt_left_type = $statements_analyzer->node_data->getType($stmt->var);
1140
                    $stmt_right_type = $statements_analyzer->node_data->getType($stmt->expr);
1141
1142
                    $var_location = new CodeLocation($statements_analyzer, $stmt);
1143
1144
                    $new_parent_node = \Psalm\Internal\Taint\TaintNode::getForAssignment($array_var_id, $var_location);
1145
                    $codebase->taint->addTaintNode($new_parent_node);
1146
1147
                    $result_type->parent_nodes = [$new_parent_node];
1148
1149
                    if ($stmt_left_type && $stmt_left_type->parent_nodes) {
1150
                        foreach ($stmt_left_type->parent_nodes as $parent_node) {
1151
                            $codebase->taint->addPath($parent_node, $new_parent_node, 'concat');
1152
                        }
1153
                    }
1154
1155
                    if ($stmt_right_type && $stmt_right_type->parent_nodes) {
1156
                        foreach ($stmt_right_type->parent_nodes as $parent_node) {
1157
                            $codebase->taint->addPath($parent_node, $new_parent_node, 'concat');
1158
                        }
1159
                    }
1160
                }
1161
            }
1162
        } elseif ($stmt_var_type
1163
            && $stmt_expr_type
1164
            && ($stmt_var_type->hasInt() || $stmt_expr_type->hasInt())
1165
            && ($stmt instanceof PhpParser\Node\Expr\AssignOp\BitwiseOr
1166
                || $stmt instanceof PhpParser\Node\Expr\AssignOp\BitwiseXor
1167
                || $stmt instanceof PhpParser\Node\Expr\AssignOp\BitwiseAnd
1168
                || $stmt instanceof PhpParser\Node\Expr\AssignOp\ShiftLeft
1169
                || $stmt instanceof PhpParser\Node\Expr\AssignOp\ShiftRight
1170
            )
1171
        ) {
1172
            BinaryOp\NonDivArithmeticOpAnalyzer::analyze(
1173
                $statements_analyzer,
1174
                $statements_analyzer->node_data,
1175
                $stmt->var,
1176
                $stmt->expr,
1177
                $stmt,
1178
                $result_type,
1179
                $context
1180
            );
1181
1182
            if ($result_type && $array_var_id) {
1183
                $context->vars_in_scope[$array_var_id] = $result_type;
1184
                $statements_analyzer->node_data->setType($stmt, clone $context->vars_in_scope[$array_var_id]);
1185
            }
1186
        }
1187
1188
        if ($array_var_id && isset($context->vars_in_scope[$array_var_id])) {
1189
            if ($result_type && $context->vars_in_scope[$array_var_id]->by_ref) {
1190
                $result_type->by_ref = true;
1191
            }
1192
1193
            // removes dependent vars from $context
1194
            $context->removeDescendents(
1195
                $array_var_id,
1196
                $context->vars_in_scope[$array_var_id],
1197
                $result_type,
1198
                $statements_analyzer
1199
            );
1200
        } else {
1201
            $root_var_id = ExpressionIdentifier::getRootVarId(
1202
                $stmt->var,
1203
                $statements_analyzer->getFQCLN(),
1204
                $statements_analyzer
1205
            );
1206
1207
            if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) {
1208
                $context->removeVarFromConflictingClauses(
1209
                    $root_var_id,
1210
                    $context->vars_in_scope[$root_var_id],
1211
                    $statements_analyzer
1212
                );
1213
            }
1214
        }
1215
1216
        if ($stmt->var instanceof PhpParser\Node\Expr\ArrayDimFetch) {
1217
            ArrayAssignmentAnalyzer::analyze(
1218
                $statements_analyzer,
1219
                $stmt->var,
1220
                $context,
1221
                null,
1222
                $result_type ?: Type::getEmpty()
1223
            );
1224
        }
1225
1226
        if (!$was_in_assignment) {
1227
            $context->inside_assignment = false;
1228
        }
1229
1230
        return true;
1231
    }
1232
1233
    /**
1234
     * @param   StatementsAnalyzer               $statements_analyzer
1235
     * @param   PhpParser\Node\Expr\AssignRef   $stmt
1236
     * @param   Context                         $context
1237
     */
1238
    public static function analyzeAssignmentRef(
1239
        StatementsAnalyzer $statements_analyzer,
1240
        PhpParser\Node\Expr\AssignRef $stmt,
1241
        Context $context
1242
    ) : bool {
1243
        $assignment_type = self::analyze(
1244
            $statements_analyzer,
1245
            $stmt->var,
1246
            $stmt->expr,
1247
            null,
1248
            $context,
1249
            $stmt->getDocComment()
1250
        );
1251
1252
        if ($assignment_type === false) {
1253
            return false;
1254
        }
1255
1256
        $assignment_type->by_ref = true;
1257
1258
        $lhs_var_id = ExpressionIdentifier::getVarId(
1259
            $stmt->var,
1260
            $statements_analyzer->getFQCLN(),
1261
            $statements_analyzer
1262
        );
1263
1264
        $rhs_var_id = ExpressionIdentifier::getVarId(
1265
            $stmt->expr,
1266
            $statements_analyzer->getFQCLN(),
1267
            $statements_analyzer
1268
        );
1269
1270
        if ($lhs_var_id) {
1271
            $context->vars_in_scope[$lhs_var_id] = $assignment_type;
1272
            $context->hasVariable($lhs_var_id, $statements_analyzer);
1273
        }
1274
1275
        if ($rhs_var_id && !isset($context->vars_in_scope[$rhs_var_id])) {
1276
            $context->vars_in_scope[$rhs_var_id] = Type::getMixed();
1277
        }
1278
1279
        return true;
1280
    }
1281
1282
    /**
1283
     * @param  StatementsAnalyzer    $statements_analyzer
1284
     * @param  PhpParser\Node\Expr  $stmt
1285
     * @param  Type\Union           $by_ref_type
1286
     * @param  Context              $context
1287
     * @param  bool                 $constrain_type
1288
     *
1289
     * @return void
1290
     */
1291
    public static function assignByRefParam(
1292
        StatementsAnalyzer $statements_analyzer,
1293
        PhpParser\Node\Expr $stmt,
1294
        Type\Union $by_ref_type,
1295
        Type\Union $by_ref_out_type,
1296
        Context $context,
1297
        bool $constrain_type = true,
1298
        bool $prevent_null = false
1299
    ) {
1300
        if ($stmt instanceof PhpParser\Node\Expr\PropertyFetch && $stmt->name instanceof PhpParser\Node\Identifier) {
1301
            $prop_name = $stmt->name->name;
1302
1303
            InstancePropertyAssignmentAnalyzer::analyze(
1304
                $statements_analyzer,
1305
                $stmt,
1306
                $prop_name,
1307
                null,
1308
                $by_ref_out_type,
1309
                $context
1310
            );
1311
1312
            return;
1313
        }
1314
1315
        $var_id = ExpressionIdentifier::getVarId(
1316
            $stmt,
1317
            $statements_analyzer->getFQCLN(),
1318
            $statements_analyzer
1319
        );
1320
1321
        if ($var_id) {
1322
            if (!$by_ref_type->hasMixed() && $constrain_type) {
1323
                $context->byref_constraints[$var_id] = new \Psalm\Internal\ReferenceConstraint($by_ref_type);
1324
            }
1325
1326
            if (!$context->hasVariable($var_id, $statements_analyzer)) {
1327
                $context->vars_possibly_in_scope[$var_id] = true;
1328
1329
                if (!$statements_analyzer->hasVariable($var_id)) {
1330
                    $location = new CodeLocation($statements_analyzer, $stmt);
1331
                    $statements_analyzer->registerVariable($var_id, $location, null);
1332
1333
                    if ($constrain_type
1334
                        && $prevent_null
1335
                        && !$by_ref_type->isMixed()
1336
                        && !$by_ref_type->isNullable()
1337
                        && !strpos($var_id, '->')
1338
                        && !strpos($var_id, '::')
1339
                    ) {
1340
                        if (IssueBuffer::accepts(
1341
                            new \Psalm\Issue\NullReference(
1342
                                'Not expecting null argument passed by reference',
1343
                                new CodeLocation($statements_analyzer->getSource(), $stmt)
1344
                            ),
1345
                            $statements_analyzer->getSuppressedIssues()
1346
                        )) {
1347
                            // fall through
1348
                        }
1349
                    }
1350
1351
                    $codebase = $statements_analyzer->getCodebase();
1352
1353
                    if ($codebase->find_unused_variables) {
1354
                        $context->unreferenced_vars[$var_id] = [$location->getHash() => $location];
1355
                    }
1356
1357
                    $context->hasVariable($var_id, $statements_analyzer);
1358
                }
1359
            } elseif ($var_id === '$this') {
1360
                // don't allow changing $this
1361
                return;
1362
            } else {
1363
                $existing_type = $context->vars_in_scope[$var_id];
1364
1365
                // removes dependent vars from $context
1366
                $context->removeDescendents(
1367
                    $var_id,
1368
                    $existing_type,
1369
                    $by_ref_type,
1370
                    $statements_analyzer
1371
                );
1372
1373
                if ($existing_type->getId() !== 'array<empty, empty>') {
1374
                    $context->vars_in_scope[$var_id] = clone $by_ref_out_type;
1375
1376
                    if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))
1377
                        || $stmt_type->isEmpty()
1378
                    ) {
1379
                        $statements_analyzer->node_data->setType($stmt, clone $by_ref_type);
1380
                    }
1381
1382
                    return;
1383
                }
1384
            }
1385
1386
            $context->assigned_var_ids[$var_id] = true;
1387
1388
            $context->vars_in_scope[$var_id] = $by_ref_out_type;
1389
1390
            if (!($stmt_type = $statements_analyzer->node_data->getType($stmt)) || $stmt_type->isEmpty()) {
1391
                $statements_analyzer->node_data->setType($stmt, clone $by_ref_type);
1392
            }
1393
        }
1394
    }
1395
}
1396