InstancePropertyAssignmentAnalyzer::analyze()   F
last analyzed

Complexity

Conditions 194
Paths > 20000

Size

Total Lines 1023

Duplication

Lines 242
Ratio 23.66 %

Importance

Changes 0
Metric Value
cc 194
nc 1292084
nop 7
dl 242
loc 1023
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\Assignment;
3
4
use PhpParser;
5
use PhpParser\Node\Expr\PropertyFetch;
6
use PhpParser\Node\Stmt\PropertyProperty;
7
use Psalm\Internal\Analyzer\ClassAnalyzer;
8
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
9
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
10
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
11
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
12
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\InstancePropertyFetchAnalyzer;
13
use Psalm\Internal\Analyzer\StatementsAnalyzer;
14
use Psalm\Internal\Analyzer\TypeAnalyzer;
15
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
16
use Psalm\CodeLocation;
17
use Psalm\Context;
18
use Psalm\Issue\DeprecatedProperty;
19
use Psalm\Issue\ImplicitToStringCast;
20
use Psalm\Issue\InaccessibleProperty;
21
use Psalm\Issue\InternalProperty;
22
use Psalm\Issue\InvalidPropertyAssignment;
23
use Psalm\Issue\InvalidPropertyAssignmentValue;
24
use Psalm\Issue\LoopInvalidation;
25
use Psalm\Issue\MixedAssignment;
26
use Psalm\Issue\MixedPropertyAssignment;
27
use Psalm\Issue\MixedPropertyTypeCoercion;
28
use Psalm\Issue\NoInterfaceProperties;
29
use Psalm\Issue\NullPropertyAssignment;
30
use Psalm\Issue\PossiblyFalsePropertyAssignmentValue;
31
use Psalm\Issue\PossiblyInvalidPropertyAssignment;
32
use Psalm\Issue\PossiblyInvalidPropertyAssignmentValue;
33
use Psalm\Issue\PossiblyNullPropertyAssignment;
34
use Psalm\Issue\PossiblyNullPropertyAssignmentValue;
35
use Psalm\Issue\PropertyTypeCoercion;
36
use Psalm\Issue\UndefinedClass;
37
use Psalm\Issue\UndefinedPropertyAssignment;
38
use Psalm\Issue\UndefinedMagicPropertyAssignment;
39
use Psalm\Issue\UndefinedThisPropertyAssignment;
40
use Psalm\IssueBuffer;
41
use Psalm\Type;
42
use Psalm\Type\Atomic\TNamedObject;
43
use Psalm\Type\Atomic\TNull;
44
use Psalm\Type\Atomic\TObject;
45
use function count;
46
use function in_array;
47
use function strtolower;
48
use function explode;
49
use Psalm\Internal\Taint\TaintNode;
50
51
/**
52
 * @internal
53
 */
54
class InstancePropertyAssignmentAnalyzer
55
{
56
    /**
57
     * @param   StatementsAnalyzer               $statements_analyzer
58
     * @param   PropertyFetch|PropertyProperty  $stmt
59
     * @param   string                          $prop_name
60
     * @param   PhpParser\Node\Expr|null        $assignment_value
61
     * @param   Type\Union                      $assignment_value_type
62
     * @param   Context                         $context
63
     * @param   bool                            $direct_assignment whether the variable is assigned explicitly
64
     *
65
     * @return  false|null
66
     */
67
    public static function analyze(
68
        StatementsAnalyzer $statements_analyzer,
69
        $stmt,
70
        $prop_name,
71
        $assignment_value,
72
        Type\Union $assignment_value_type,
73
        Context $context,
74
        $direct_assignment = true
75
    ) {
76
        $class_property_types = [];
77
78
        $codebase = $statements_analyzer->getCodebase();
79
80
        $property_exists = false;
81
82
        $property_ids = [];
83
84
        if ($stmt instanceof PropertyProperty) {
85
            if (!$context->self || !$stmt->default) {
86
                return null;
87
            }
88
89
            $property_id = $context->self . '::$' . $prop_name;
90
            $property_ids[] = $property_id;
91
92
            $property_exists = true;
93
94
            try {
95
                $class_property_type = $codebase->properties->getPropertyType(
96
                    $property_id,
97
                    true,
98
                    $statements_analyzer,
99
                    $context
100
                );
101
            } catch (\UnexpectedValueException $e) {
102
                return false;
103
            }
104
105
            if ($class_property_type) {
106
                $class_storage = $codebase->classlike_storage_provider->get($context->self);
107
108
                $class_property_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
109
                    $codebase,
110
                    clone $class_property_type,
111
                    $class_storage->name,
112
                    $class_storage->name,
113
                    $class_storage->parent_class
114
                );
115
            }
116
117
            $class_property_types[] = $class_property_type ?: Type::getMixed();
118
119
            $var_id = '$this->' . $prop_name;
120
        } else {
121
            if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->var, $context) === false) {
122
                return false;
123
            }
124
125
            $lhs_type = $statements_analyzer->node_data->getType($stmt->var);
126
127
            if ($lhs_type === null) {
128
                return null;
129
            }
130
131
            $lhs_var_id = ExpressionIdentifier::getVarId(
132
                $stmt->var,
133
                $statements_analyzer->getFQCLN(),
134
                $statements_analyzer
135
            );
136
137
            $var_id = ExpressionIdentifier::getVarId(
138
                $stmt,
139
                $statements_analyzer->getFQCLN(),
140
                $statements_analyzer
141
            );
142
143
            if ($var_id) {
144
                $context->assigned_var_ids[$var_id] = true;
145
146
                if ($direct_assignment && isset($context->protected_var_ids[$var_id])) {
147
                    if (IssueBuffer::accepts(
148
                        new LoopInvalidation(
149
                            'Variable ' . $var_id . ' has already been assigned in a for/foreach loop',
150
                            new CodeLocation($statements_analyzer->getSource(), $stmt->var)
151
                        ),
152
                        $statements_analyzer->getSuppressedIssues()
153
                    )) {
154
                        // fall through
155
                    }
156
                }
157
            }
158
159
            if ($lhs_type->hasMixed()) {
160 View Code Duplication
                if (!$context->collect_initializations
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
161
                    && !$context->collect_mutations
162
                    && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
163
                    && (!(($parent_source = $statements_analyzer->getSource())
164
                            instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
165
                        || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
166
                ) {
167
                    $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
168
                }
169
170 View Code Duplication
                if ($stmt->name instanceof PhpParser\Node\Identifier) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
171
                    $codebase->analyzer->addMixedMemberName(
172
                        '$' . $stmt->name->name,
173
                        $context->calling_method_id ?: $statements_analyzer->getFileName()
174
                    );
175
                }
176
177
                if (IssueBuffer::accepts(
178
                    new MixedPropertyAssignment(
179
                        $lhs_var_id . ' of type mixed cannot be assigned to',
180
                        new CodeLocation($statements_analyzer->getSource(), $stmt->var)
181
                    ),
182
                    $statements_analyzer->getSuppressedIssues()
183
                )) {
184
                    return false;
185
                }
186
187
                return null;
188
            }
189
190 View Code Duplication
            if (!$context->collect_initializations
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
191
                && !$context->collect_mutations
192
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
193
                && (!(($parent_source = $statements_analyzer->getSource())
194
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
195
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
196
            ) {
197
                $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
198
            }
199
200
            if ($lhs_type->isNull()) {
201
                if (IssueBuffer::accepts(
202
                    new NullPropertyAssignment(
203
                        $lhs_var_id . ' of type null cannot be assigned to',
204
                        new CodeLocation($statements_analyzer->getSource(), $stmt->var)
205
                    ),
206
                    $statements_analyzer->getSuppressedIssues()
207
                )) {
208
                    return false;
209
                }
210
211
                return null;
212
            }
213
214
            if ($lhs_type->isNullable() && !$lhs_type->ignore_nullable_issues) {
215
                if (IssueBuffer::accepts(
216
                    new PossiblyNullPropertyAssignment(
217
                        $lhs_var_id . ' with possibly null type \'' . $lhs_type . '\' cannot be assigned to',
218
                        new CodeLocation($statements_analyzer->getSource(), $stmt->var)
219
                    ),
220
                    $statements_analyzer->getSuppressedIssues()
221
                )) {
222
                    return false;
223
                }
224
            }
225
226
            $has_regular_setter = false;
227
228
            $invalid_assignment_types = [];
229
230
            $has_valid_assignment_type = false;
231
232
            $lhs_atomic_types = $lhs_type->getAtomicTypes();
233
234
            while ($lhs_atomic_types) {
235
                $lhs_type_part = \array_pop($lhs_atomic_types);
236
237
                if ($lhs_type_part instanceof Type\Atomic\TTemplateParam) {
238
                    $lhs_atomic_types = \array_merge(
239
                        $lhs_atomic_types,
240
                        $lhs_type_part->as->getAtomicTypes()
241
                    );
242
243
                    continue;
244
                }
245
246
                if ($lhs_type_part instanceof TNull) {
247
                    continue;
248
                }
249
250
                if ($lhs_type_part instanceof Type\Atomic\TFalse
251
                    && $lhs_type->ignore_falsable_issues
252
                    && count($lhs_type->getAtomicTypes()) > 1
253
                ) {
254
                    continue;
255
                }
256
257
                if (!$lhs_type_part instanceof TObject && !$lhs_type_part instanceof TNamedObject) {
258
                    $invalid_assignment_types[] = (string)$lhs_type_part;
259
260
                    continue;
261
                }
262
263
                $has_valid_assignment_type = true;
264
265
                // stdClass and SimpleXMLElement are special cases where we cannot infer the return types
266
                // but we don't want to throw an error
267
                // Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164
268
                if ($lhs_type_part instanceof TObject ||
269
                    (
270
                        in_array(
271
                            strtolower($lhs_type_part->value),
272
                            ['stdclass', 'simplexmlelement', 'dateinterval', 'domdocument', 'domnode'],
273
                            true
274
                        )
275
                    )
276
                ) {
277
                    if ($var_id) {
278
                        if ($lhs_type_part instanceof TNamedObject &&
279
                            strtolower($lhs_type_part->value) === 'stdclass'
280
                        ) {
281
                            $context->vars_in_scope[$var_id] = $assignment_value_type;
282
                        } else {
283
                            $context->vars_in_scope[$var_id] = Type::getMixed();
284
                        }
285
                    }
286
287
                    return null;
288
                }
289
290
                if (ExpressionAnalyzer::isMock($lhs_type_part->value)) {
291
                    if ($var_id) {
292
                        $context->vars_in_scope[$var_id] = Type::getMixed();
293
                    }
294
295
                    return null;
296
                }
297
298
                $intersection_types = $lhs_type_part->getIntersectionTypes() ?: [];
299
300
                $fq_class_name = $lhs_type_part->value;
301
302
                $override_property_visibility = false;
303
304
                $class_exists = false;
305
                $interface_exists = false;
306
307
                if (!$codebase->classExists($lhs_type_part->value)) {
308 View Code Duplication
                    if ($codebase->interfaceExists($lhs_type_part->value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309
                        $interface_exists = true;
310
                        $interface_storage = $codebase->classlike_storage_provider->get(
311
                            strtolower($lhs_type_part->value)
312
                        );
313
314
                        $override_property_visibility = $interface_storage->override_property_visibility;
315
316
                        foreach ($intersection_types as $intersection_type) {
317
                            if ($intersection_type instanceof TNamedObject
318
                                && $codebase->classExists($intersection_type->value)
319
                            ) {
320
                                $fq_class_name = $intersection_type->value;
321
                                $class_exists = true;
322
                                break;
323
                            }
324
                        }
325
326
                        if (!$class_exists) {
327
                            if (IssueBuffer::accepts(
328
                                new NoInterfaceProperties(
329
                                    'Interfaces cannot have properties',
330
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
331
                                    $lhs_type_part->value
332
                                ),
333
                                $statements_analyzer->getSuppressedIssues()
334
                            )) {
335
                                return null;
336
                            }
337
338
                            if (!$codebase->methods->methodExists(
339
                                new \Psalm\Internal\MethodIdentifier(
340
                                    $fq_class_name,
341
                                    '__set'
342
                                )
343
                            )) {
344
                                return null;
345
                            }
346
                        }
347
                    }
348
349
                    if (!$class_exists && !$interface_exists) {
350
                        if (IssueBuffer::accepts(
351
                            new UndefinedClass(
352
                                'Cannot set properties of undefined class ' . $lhs_type_part->value,
353
                                new CodeLocation($statements_analyzer->getSource(), $stmt),
354
                                $lhs_type_part->value
355
                            ),
356
                            $statements_analyzer->getSuppressedIssues()
357
                        )) {
358
                            // fall through
359
                        }
360
361
                        return null;
362
                    }
363
                } else {
364
                    $class_exists = true;
365
                }
366
367
                $property_id = $fq_class_name . '::$' . $prop_name;
368
                $property_ids[] = $property_id;
369
370
                $has_magic_setter = false;
371
372
                $set_method_id = new \Psalm\Internal\MethodIdentifier($fq_class_name, '__set');
373
374
                if ((!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)
375
                        || ($lhs_var_id !== '$this'
376
                            && $fq_class_name !== $context->self
377
                            && ClassLikeAnalyzer::checkPropertyVisibility(
378
                                $property_id,
379
                                $context,
380
                                $statements_analyzer,
381
                                new CodeLocation($statements_analyzer->getSource(), $stmt),
382
                                $statements_analyzer->getSuppressedIssues(),
383
                                false
384
                            ) !== true)
385
                    )
386
                    && $codebase->methods->methodExists(
387
                        $set_method_id,
388
                        $context->calling_method_id,
389
                        $codebase->collect_locations
390
                            ? new CodeLocation($statements_analyzer->getSource(), $stmt)
391
                            : null,
392
                        !$context->collect_initializations
393
                            && !$context->collect_mutations
394
                            ? $statements_analyzer
395
                            : null,
396
                        $statements_analyzer->getFilePath()
397
                    )
398
                ) {
399
                    $has_magic_setter = true;
400
                    $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
401
402
                    if ($var_id) {
403
                        if (isset($class_storage->pseudo_property_set_types['$' . $prop_name])) {
404
                            $class_property_types[] =
405
                                clone $class_storage->pseudo_property_set_types['$' . $prop_name];
406
407
                            $has_regular_setter = true;
408
                            $property_exists = true;
409
410
                            if (!$context->collect_initializations) {
411
                                self::taintProperty(
412
                                    $statements_analyzer,
413
                                    $stmt,
414
                                    $property_id,
415
                                    $class_storage,
416
                                    $assignment_value_type,
417
                                    $context
418
                                );
419
                            }
420
421
                            continue;
422
                        }
423
                    }
424
425
                    if ($assignment_value) {
426
                        if ($var_id) {
427
                            $context->removeVarFromConflictingClauses(
428
                                $var_id,
429
                                Type::getMixed(),
430
                                $statements_analyzer
431
                            );
432
433
                            unset($context->vars_in_scope[$var_id]);
434
                        }
435
436
                        $old_data_provider = $statements_analyzer->node_data;
437
438
                        $statements_analyzer->node_data = clone $statements_analyzer->node_data;
439
440
                        $fake_method_call = new PhpParser\Node\Expr\MethodCall(
441
                            $stmt->var,
442
                            new PhpParser\Node\Identifier('__set', $stmt->name->getAttributes()),
443
                            [
444
                                new PhpParser\Node\Arg(
445
                                    new PhpParser\Node\Scalar\String_(
446
                                        $prop_name,
447
                                        $stmt->name->getAttributes()
448
                                    )
449
                                ),
450
                                new PhpParser\Node\Arg(
451
                                    $assignment_value
452
                                )
453
                            ]
454
                        );
455
456
                        $suppressed_issues = $statements_analyzer->getSuppressedIssues();
457
458
                        if (!in_array('PossiblyNullReference', $suppressed_issues, true)) {
459
                            $statements_analyzer->addSuppressedIssues(['PossiblyNullReference']);
460
                        }
461
462
                        \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
463
                            $statements_analyzer,
464
                            $fake_method_call,
465
                            $context,
466
                            false
467
                        );
468
469
                        if (!in_array('PossiblyNullReference', $suppressed_issues, true)) {
470
                            $statements_analyzer->removeSuppressedIssues(['PossiblyNullReference']);
471
                        }
472
473
                        $statements_analyzer->node_data = $old_data_provider;
474
                    }
475
476
                    /*
477
                     * If we have an explicit list of all allowed magic properties on the class, and we're
478
                     * not in that list, fall through
479
                     */
480
                    if (!$var_id || !$class_storage->sealed_properties) {
481
                        if (!$context->collect_initializations) {
482
                            self::taintProperty(
483
                                $statements_analyzer,
484
                                $stmt,
485
                                $property_id,
486
                                $class_storage,
487
                                $assignment_value_type,
488
                                $context
489
                            );
490
                        }
491
492
                        continue;
493
                    }
494
495
                    if (!$class_exists) {
496
                        if (IssueBuffer::accepts(
497
                            new UndefinedMagicPropertyAssignment(
498
                                'Magic instance property ' . $property_id . ' is not defined',
499
                                new CodeLocation($statements_analyzer->getSource(), $stmt),
500
                                $property_id
501
                            ),
502
                            $statements_analyzer->getSuppressedIssues()
503
                        )) {
504
                            // fall through
505
                        }
506
                    }
507
                }
508
509
                if (!$class_exists) {
510
                    continue;
511
                }
512
513
                $has_regular_setter = true;
514
515
                if ($stmt->var instanceof PhpParser\Node\Expr\Variable
516
                    && $stmt->var->name === 'this'
517
                    && $context->self
518
                ) {
519
                    $self_property_id = $context->self . '::$' . $prop_name;
520
521
                    if ($self_property_id !== $property_id
522
                        && $codebase->properties->propertyExists(
523
                            $self_property_id,
524
                            false,
525
                            $statements_analyzer,
526
                            $context
527
                        )
528
                    ) {
529
                        $property_id = $self_property_id;
530
                    }
531
                }
532
533
                if ($codebase->taint && !$context->collect_initializations) {
534
                    $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
535
536
                    self::taintProperty(
537
                        $statements_analyzer,
538
                        $stmt,
539
                        $property_id,
540
                        $class_storage,
541
                        $assignment_value_type,
542
                        $context
543
                    );
544
                }
545
546
                if (!$codebase->properties->propertyExists(
547
                    $property_id,
548
                    false,
549
                    $statements_analyzer,
550
                    $context,
551
                    new CodeLocation($statements_analyzer->getSource(), $stmt)
552
                )) {
553
                    if ($stmt->var instanceof PhpParser\Node\Expr\Variable && $stmt->var->name === 'this') {
554
                        // if this is a proper error, we'll see it on the first pass
555
                        if ($context->collect_mutations) {
556
                            continue;
557
                        }
558
559
                        if (IssueBuffer::accepts(
560
                            new UndefinedThisPropertyAssignment(
561
                                'Instance property ' . $property_id . ' is not defined',
562
                                new CodeLocation($statements_analyzer->getSource(), $stmt),
563
                                $property_id
564
                            ),
565
                            $statements_analyzer->getSuppressedIssues()
566
                        )) {
567
                            // fall through
568
                        }
569 View Code Duplication
                    } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
570
                        if ($has_magic_setter) {
571
                            if (IssueBuffer::accepts(
572
                                new UndefinedMagicPropertyAssignment(
573
                                    'Magic instance property ' . $property_id . ' is not defined',
574
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
575
                                    $property_id
576
                                ),
577
                                $statements_analyzer->getSuppressedIssues()
578
                            )) {
579
                                // fall through
580
                            }
581
                        } else {
582
                            if (IssueBuffer::accepts(
583
                                new UndefinedPropertyAssignment(
584
                                    'Instance property ' . $property_id . ' is not defined',
585
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
586
                                    $property_id
587
                                ),
588
                                $statements_analyzer->getSuppressedIssues()
589
                            )) {
590
                                // fall through
591
                            }
592
                        }
593
                    }
594
595
                    continue;
596
                }
597
598 View Code Duplication
                if ($codebase->store_node_types
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
599
                    && !$context->collect_initializations
600
                    && !$context->collect_mutations
601
                ) {
602
                    $codebase->analyzer->addNodeReference(
603
                        $statements_analyzer->getFilePath(),
604
                        $stmt->name,
605
                        $property_id
606
                    );
607
                }
608
609
                $property_exists = true;
610
611
                if (!$override_property_visibility) {
612
                    if (!$context->collect_mutations) {
613 View Code Duplication
                        if (ClassLikeAnalyzer::checkPropertyVisibility(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
614
                            $property_id,
615
                            $context,
616
                            $statements_analyzer,
617
                            new CodeLocation($statements_analyzer->getSource(), $stmt),
618
                            $statements_analyzer->getSuppressedIssues()
619
                        ) === false) {
620
                            return false;
621
                        }
622 View Code Duplication
                    } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
623
                        if (ClassLikeAnalyzer::checkPropertyVisibility(
624
                            $property_id,
625
                            $context,
626
                            $statements_analyzer,
627
                            new CodeLocation($statements_analyzer->getSource(), $stmt),
628
                            $statements_analyzer->getSuppressedIssues(),
629
                            false
630
                        ) !== true) {
631
                            continue;
632
                        }
633
                    }
634
                }
635
636
                $declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
637
                    $property_id,
638
                    false
639
                );
640
641 View Code Duplication
                if ($codebase->properties_to_rename) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
642
                    $declaring_property_id = strtolower($declaring_property_class) . '::$' . $prop_name;
643
644
                    foreach ($codebase->properties_to_rename as $original_property_id => $new_property_name) {
645
                        if ($declaring_property_id === $original_property_id) {
646
                            $file_manipulations = [
647
                                new \Psalm\FileManipulation(
648
                                    (int) $stmt->name->getAttribute('startFilePos'),
649
                                    (int) $stmt->name->getAttribute('endFilePos') + 1,
650
                                    $new_property_name
651
                                )
652
                            ];
653
654
                            \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
655
                                $statements_analyzer->getFilePath(),
656
                                $file_manipulations
657
                            );
658
                        }
659
                    }
660
                }
661
662
                $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
663
664
                if (isset($declaring_class_storage->properties[$prop_name])) {
665
                    $property_storage = $declaring_class_storage->properties[$prop_name];
666
667
                    if ($property_storage->deprecated) {
668
                        if (IssueBuffer::accepts(
669
                            new DeprecatedProperty(
670
                                $property_id . ' is marked deprecated',
671
                                new CodeLocation($statements_analyzer->getSource(), $stmt),
672
                                $property_id
673
                            ),
674
                            $statements_analyzer->getSuppressedIssues()
675
                        )) {
676
                            // fall through
677
                        }
678
                    }
679
680 View Code Duplication
                    if ($property_storage->psalm_internal && $context->self) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
681
                        if (! NamespaceAnalyzer::isWithin($context->self, $property_storage->psalm_internal)) {
682
                            if (IssueBuffer::accepts(
683
                                new InternalProperty(
684
                                    $property_id . ' is marked internal to ' . $property_storage->psalm_internal,
685
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
686
                                    $property_id
687
                                ),
688
                                $statements_analyzer->getSuppressedIssues()
689
                            )) {
690
                                // fall through
691
                            }
692
                        }
693
                    }
694
695
                    if ($property_storage->internal && $context->self) {
696
                        if (! NamespaceAnalyzer::nameSpaceRootsMatch($context->self, $declaring_property_class)) {
697
                            if (IssueBuffer::accepts(
698
                                new InternalProperty(
699
                                    $property_id . ' is marked internal',
700
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
701
                                    $property_id
702
                                ),
703
                                $statements_analyzer->getSuppressedIssues()
704
                            )) {
705
                                // fall through
706
                            }
707
                        }
708
                    }
709
710
                    // prevents writing to readonly properties
711
                    if ($property_storage->readonly) {
712
                        $appearing_property_class = $codebase->properties->getAppearingClassForProperty(
713
                            $property_id,
714
                            true
715
                        );
716
717
                        $stmt_var_type = $statements_analyzer->node_data->getType($stmt->var);
718
719
                        $property_var_pure_compatible = $stmt_var_type
720
                            && $stmt_var_type->reference_free
721
                            && $stmt_var_type->allow_mutations;
722
723
                        if ($appearing_property_class) {
724
                            $can_set_property = $context->self
725
                                && $context->calling_method_id
726
                                && ($appearing_property_class === $context->self
727
                                    || $codebase->classExtends($context->self, $appearing_property_class))
728
                                && (\strpos($context->calling_method_id, '::__construct')
729
                                    || \strpos($context->calling_method_id, '::unserialize')
730
                                    || \strpos($context->calling_method_id, '::__unserialize')
731
                                    || $property_storage->allow_private_mutation
732
                                    || $property_var_pure_compatible);
733
734
                            if (!$can_set_property) {
735
                                if (IssueBuffer::accepts(
736
                                    new InaccessibleProperty(
737
                                        $property_id . ' is marked readonly',
738
                                        new CodeLocation($statements_analyzer->getSource(), $stmt)
739
                                    ),
740
                                    $statements_analyzer->getSuppressedIssues()
741
                                )) {
742
                                    // fall through
743
                                }
744
                            } elseif ($declaring_class_storage->mutation_free) {
745
                                $visitor = new \Psalm\Internal\TypeVisitor\ImmutablePropertyAssignmentVisitor(
746
                                    $statements_analyzer,
747
                                    $stmt
748
                                );
749
750
                                $visitor->traverse($assignment_value_type);
751
                            }
752
                        }
753
                    }
754
755
                    if ($property_storage->getter_method) {
756
                        $getter_id = $lhs_var_id . '->' . $property_storage->getter_method . '()';
757
758
                        unset($context->vars_in_scope[$getter_id]);
759
                    }
760
                }
761
762
                $class_property_type = $codebase->properties->getPropertyType(
763
                    $property_id,
764
                    true,
765
                    $statements_analyzer,
766
                    $context
767
                );
768
769
                if (!$class_property_type
770
                    || (isset($declaring_class_storage->properties[$prop_name])
771
                        && !$declaring_class_storage->properties[$prop_name]->type_location)
772
                ) {
773
                    if (!$class_property_type) {
774
                        $class_property_type = Type::getMixed();
775
                    }
776
777
                    $source_analyzer = $statements_analyzer->getSource()->getSource();
778
779 View Code Duplication
                    if ($lhs_var_id === '$this'
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
780
                        && $source_analyzer instanceof ClassAnalyzer
781
                    ) {
782
                        if (isset($source_analyzer->inferred_property_types[$prop_name])) {
783
                            $source_analyzer->inferred_property_types[$prop_name] = Type::combineUnionTypes(
784
                                $assignment_value_type,
785
                                $source_analyzer->inferred_property_types[$prop_name]
786
                            );
787
                        } else {
788
                            $source_analyzer->inferred_property_types[$prop_name] = $assignment_value_type;
789
                        }
790
                    }
791
                }
792
793
                if (!$class_property_type->isMixed()) {
794
                    $class_property_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
795
                        $codebase,
796
                        clone $class_property_type,
797
                        $fq_class_name,
798
                        $lhs_type_part,
799
                        $declaring_class_storage->parent_class
800
                    );
801
802
                    $class_property_type = \Psalm\Internal\Codebase\Methods::localizeType(
803
                        $codebase,
804
                        $class_property_type,
805
                        $fq_class_name,
806
                        $declaring_property_class
807
                    );
808
809
                    if ($lhs_type_part instanceof Type\Atomic\TGenericObject) {
810
                        $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
811
812
                        $class_property_type = InstancePropertyFetchAnalyzer::localizePropertyType(
813
                            $codebase,
814
                            $class_property_type,
815
                            $lhs_type_part,
816
                            $class_storage,
817
                            $declaring_class_storage
818
                        );
819
                    }
820
821
                    $assignment_value_type = \Psalm\Internal\Codebase\Methods::localizeType(
822
                        $codebase,
823
                        $assignment_value_type,
824
                        $fq_class_name,
825
                        $declaring_property_class
826
                    );
827
828
                    if (!$class_property_type->hasMixed() && $assignment_value_type->hasMixed()) {
829
                        if (IssueBuffer::accepts(
830
                            new MixedAssignment(
831
                                'Cannot assign' . ($var_id ? ' ' . $var_id . ' ' : ' ') . 'to a mixed type',
832
                                new CodeLocation($statements_analyzer->getSource(), $stmt)
833
                            ),
834
                            $statements_analyzer->getSuppressedIssues()
835
                        )) {
836
                            // fall through
837
                        }
838
                    }
839
                }
840
841
                $class_property_types[] = $class_property_type;
842
            }
843
844
            if ($invalid_assignment_types) {
845
                $invalid_assignment_type = $invalid_assignment_types[0];
846
847
                if (!$has_valid_assignment_type) {
848
                    if (IssueBuffer::accepts(
849
                        new InvalidPropertyAssignment(
850
                            $lhs_var_id . ' with non-object type \'' . $invalid_assignment_type .
851
                            '\' cannot treated as an object',
852
                            new CodeLocation($statements_analyzer->getSource(), $stmt->var)
853
                        ),
854
                        $statements_analyzer->getSuppressedIssues()
855
                    )) {
856
                        return false;
857
                    }
858
                } else {
859
                    if (IssueBuffer::accepts(
860
                        new PossiblyInvalidPropertyAssignment(
861
                            $lhs_var_id . ' with possible non-object type \'' . $invalid_assignment_type .
862
                            '\' cannot treated as an object',
863
                            new CodeLocation($statements_analyzer->getSource(), $stmt->var)
864
                        ),
865
                        $statements_analyzer->getSuppressedIssues()
866
                    )) {
867
                        return false;
868
                    }
869
                }
870
            }
871
872
            if (!$has_regular_setter) {
873
                return null;
874
            }
875
876
            if ($var_id) {
877
                if ($context->collect_initializations
878
                    && $lhs_var_id === '$this'
879
                ) {
880
                    $assignment_value_type->initialized_class = $context->self;
881
                }
882
883
                // because we don't want to be assigning for property declarations
884
                $context->vars_in_scope[$var_id] = $assignment_value_type;
885
            }
886
        }
887
888
        if (!$property_exists) {
889
            return null;
890
        }
891
892
        if ($assignment_value_type->hasMixed()) {
893
            return null;
894
        }
895
896
        $invalid_assignment_value_types = [];
897
898
        $has_valid_assignment_value_type = false;
899
900
        if ($codebase->store_node_types
901
            && !$context->collect_initializations
902
            && !$context->collect_mutations
903
            && count($class_property_types) === 1
904
        ) {
905
            $codebase->analyzer->addNodeType(
906
                $statements_analyzer->getFilePath(),
907
                $stmt->name,
908
                $class_property_types[0]->getId()
909
            );
910
        }
911
912
        foreach ($class_property_types as $class_property_type) {
913
            if ($class_property_type->hasMixed()) {
914
                continue;
915
            }
916
917
            $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
918
919
            $type_match_found = TypeAnalyzer::isContainedBy(
920
                $codebase,
921
                $assignment_value_type,
922
                $class_property_type,
923
                true,
924
                true,
925
                $union_comparison_results
926
            );
927
928
            if ($type_match_found && $union_comparison_results->replacement_union_type) {
929
                if ($var_id) {
930
                    $context->vars_in_scope[$var_id] = $union_comparison_results->replacement_union_type;
931
                }
932
            }
933
934 View Code Duplication
            if ($union_comparison_results->type_coerced) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
935
                if ($union_comparison_results->type_coerced_from_mixed) {
936
                    if (IssueBuffer::accepts(
937
                        new MixedPropertyTypeCoercion(
938
                            $var_id . ' expects \'' . $class_property_type->getId() . '\', '
939
                                . ' parent type `' . $assignment_value_type->getId() . '` provided',
940
                            new CodeLocation(
941
                                $statements_analyzer->getSource(),
942
                                $assignment_value ?: $stmt,
943
                                $context->include_location
944
                            ),
945
                            $property_ids[0]
946
                        ),
947
                        $statements_analyzer->getSuppressedIssues()
948
                    )) {
949
                        // keep soldiering on
950
                    }
951
                } else {
952
                    if (IssueBuffer::accepts(
953
                        new PropertyTypeCoercion(
954
                            $var_id . ' expects \'' . $class_property_type->getId() . '\', '
955
                                . ' parent type \'' . $assignment_value_type->getId() . '\' provided',
956
                            new CodeLocation(
957
                                $statements_analyzer->getSource(),
958
                                $assignment_value ?: $stmt,
959
                                $context->include_location
960
                            ),
961
                            $property_ids[0]
962
                        ),
963
                        $statements_analyzer->getSuppressedIssues()
964
                    )) {
965
                        // keep soldiering on
966
                    }
967
                }
968
            }
969
970
            if ($union_comparison_results->to_string_cast) {
971
                if (IssueBuffer::accepts(
972
                    new ImplicitToStringCast(
973
                        $var_id . ' expects \'' . $class_property_type . '\', '
974
                            . '\'' . $assignment_value_type . '\' provided with a __toString method',
975
                        new CodeLocation(
976
                            $statements_analyzer->getSource(),
977
                            $assignment_value ?: $stmt,
978
                            $context->include_location
979
                        )
980
                    ),
981
                    $statements_analyzer->getSuppressedIssues()
982
                )) {
983
                    // fall through
984
                }
985
            }
986
987
            if (!$type_match_found && !$union_comparison_results->type_coerced) {
988
                if (TypeAnalyzer::canBeContainedBy(
989
                    $codebase,
990
                    $assignment_value_type,
991
                    $class_property_type,
992
                    true,
993
                    true
994
                )) {
995
                    $has_valid_assignment_value_type = true;
996
                }
997
998
                $invalid_assignment_value_types[] = $class_property_type->getId();
999
            } else {
1000
                $has_valid_assignment_value_type = true;
1001
            }
1002
1003
            if ($type_match_found) {
1004 View Code Duplication
                if (!$assignment_value_type->ignore_nullable_issues
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1005
                    && $assignment_value_type->isNullable()
1006
                    && !$class_property_type->isNullable()
1007
                ) {
1008
                    if (IssueBuffer::accepts(
1009
                        new PossiblyNullPropertyAssignmentValue(
1010
                            $var_id . ' with non-nullable declared type \'' . $class_property_type .
1011
                                '\' cannot be assigned nullable type \'' . $assignment_value_type . '\'',
1012
                            new CodeLocation(
1013
                                $statements_analyzer->getSource(),
1014
                                $assignment_value ?: $stmt,
1015
                                $context->include_location
1016
                            ),
1017
                            $property_ids[0]
1018
                        ),
1019
                        $statements_analyzer->getSuppressedIssues()
1020
                    )) {
1021
                        return false;
1022
                    }
1023
                }
1024
1025 View Code Duplication
                if (!$assignment_value_type->ignore_falsable_issues
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1026
                    && $assignment_value_type->isFalsable()
1027
                    && !$class_property_type->hasBool()
1028
                    && !$class_property_type->hasScalar()
1029
                ) {
1030
                    if (IssueBuffer::accepts(
1031
                        new PossiblyFalsePropertyAssignmentValue(
1032
                            $var_id . ' with non-falsable declared type \'' . $class_property_type .
1033
                                '\' cannot be assigned possibly false type \'' . $assignment_value_type . '\'',
1034
                            new CodeLocation(
1035
                                $statements_analyzer->getSource(),
1036
                                $assignment_value ?: $stmt,
1037
                                $context->include_location
1038
                            ),
1039
                            $property_ids[0]
1040
                        ),
1041
                        $statements_analyzer->getSuppressedIssues()
1042
                    )) {
1043
                        return false;
1044
                    }
1045
                }
1046
            }
1047
        }
1048
1049
        if ($invalid_assignment_value_types) {
1050
            $invalid_class_property_type = $invalid_assignment_value_types[0];
1051
1052
            if (!$has_valid_assignment_value_type) {
1053
                if (IssueBuffer::accepts(
1054
                    new InvalidPropertyAssignmentValue(
1055
                        $var_id . ' with declared type \'' . $invalid_class_property_type .
1056
                            '\' cannot be assigned type \'' . $assignment_value_type->getId() . '\'',
1057
                        new CodeLocation(
1058
                            $statements_analyzer->getSource(),
1059
                            $assignment_value ?: $stmt,
1060
                            $context->include_location
1061
                        ),
1062
                        $property_ids[0]
1063
                    ),
1064
                    $statements_analyzer->getSuppressedIssues()
1065
                )) {
1066
                    return false;
1067
                }
1068
            } else {
1069
                if (IssueBuffer::accepts(
1070
                    new PossiblyInvalidPropertyAssignmentValue(
1071
                        $var_id . ' with declared type \'' . $invalid_class_property_type .
1072
                            '\' cannot be assigned possibly different type \'' .
1073
                            $assignment_value_type->getId() . '\'',
1074
                        new CodeLocation(
1075
                            $statements_analyzer->getSource(),
1076
                            $assignment_value ?: $stmt,
1077
                            $context->include_location
1078
                        ),
1079
                        $property_ids[0]
1080
                    ),
1081
                    $statements_analyzer->getSuppressedIssues()
1082
                )) {
1083
                    return false;
1084
                }
1085
            }
1086
        }
1087
1088
        return null;
1089
    }
1090
1091
    public static function analyzeStatement(
1092
        StatementsAnalyzer $statements_analyzer,
1093
        PhpParser\Node\Stmt\Property $stmt,
1094
        Context $context
1095
    ): void {
1096
        foreach ($stmt->props as $prop) {
1097
            if ($prop->default) {
1098
                ExpressionAnalyzer::analyze($statements_analyzer, $prop->default, $context);
1099
1100
                if ($prop_default_type = $statements_analyzer->node_data->getType($prop->default)) {
1101
                    if (self::analyze(
1102
                        $statements_analyzer,
1103
                        $prop,
1104
                        $prop->name->name,
1105
                        $prop->default,
1106
                        $prop_default_type,
1107
                        $context
1108
                    ) === false) {
1109
                        // fall through
1110
                    }
1111
                }
1112
            }
1113
        }
1114
    }
1115
1116
    private static function taintProperty(
1117
        StatementsAnalyzer $statements_analyzer,
1118
        PhpParser\Node\Expr\PropertyFetch $stmt,
1119
        string $property_id,
1120
        \Psalm\Storage\ClassLikeStorage $class_storage,
1121
        Type\Union $assignment_value_type,
1122
        Context $context
1123
    ) : void {
1124
        $codebase = $statements_analyzer->getCodebase();
1125
1126
        if (!$codebase->taint
1127
            || !$codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
1128
        ) {
1129
            return;
1130
        }
1131
1132
        $var_location = new CodeLocation($statements_analyzer->getSource(), $stmt->var);
1133
        $property_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
1134
1135
        if ($class_storage->specialize_instance) {
1136
            $var_id = ExpressionIdentifier::getArrayVarId(
1137
                $stmt->var,
1138
                null,
1139
                $statements_analyzer
1140
            );
1141
1142
            $var_property_id = ExpressionIdentifier::getArrayVarId(
1143
                $stmt,
1144
                null,
1145
                $statements_analyzer
1146
            );
1147
1148
            if ($var_id) {
1149
                if (\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())) {
1150
                    $context->vars_in_scope[$var_id]->parent_nodes = [];
1151
                    return;
1152
                }
1153
1154
                $var_node = TaintNode::getForAssignment(
1155
                    $var_id,
1156
                    $var_location
1157
                );
1158
1159
                $codebase->taint->addTaintNode($var_node);
1160
1161
                $property_node = TaintNode::getForAssignment(
1162
                    $var_property_id ?: $var_id . '->$property',
1163
                    $property_location
1164
                );
1165
1166
                $codebase->taint->addTaintNode($property_node);
1167
1168
                $codebase->taint->addPath(
1169
                    $property_node,
1170
                    $var_node,
1171
                    'property-assignment'
1172
                        . ($stmt->name instanceof PhpParser\Node\Identifier ? '-' . $stmt->name : '')
1173
                );
1174
1175
                if ($assignment_value_type->parent_nodes) {
1176
                    foreach ($assignment_value_type->parent_nodes as $parent_node) {
1177
                        $codebase->taint->addPath($parent_node, $property_node, '=');
1178
                    }
1179
                }
1180
1181
                $stmt_var_type = clone $context->vars_in_scope[$var_id];
1182
1183
                if ($context->vars_in_scope[$var_id]->parent_nodes) {
1184
                    foreach ($context->vars_in_scope[$var_id]->parent_nodes as $parent_node) {
1185
                        $codebase->taint->addPath($parent_node, $var_node, '=');
1186
                    }
1187
                }
1188
1189
                $stmt_var_type->parent_nodes = [$var_node];
1190
1191
                $context->vars_in_scope[$var_id] = $stmt_var_type;
1192
            }
1193
        } else {
1194
            if (\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())) {
1195
                $assignment_value_type->parent_nodes = [];
1196
                return;
1197
            }
1198
1199
1200
            $code_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
1201
1202
            $localized_property_node = new TaintNode(
1203
                $property_id . '-' . $code_location->file_name . ':' . $code_location->raw_file_start,
1204
                $property_id,
1205
                $code_location,
1206
                null
1207
            );
1208
1209
            $codebase->taint->addTaintNode($localized_property_node);
1210
1211
            $property_node = new TaintNode(
1212
                $property_id,
1213
                $property_id,
1214
                null,
1215
                null
1216
            );
1217
1218
            $codebase->taint->addTaintNode($property_node);
1219
1220
            $codebase->taint->addPath($localized_property_node, $property_node, 'property-assignment');
1221
1222
            if ($assignment_value_type->parent_nodes) {
1223
                foreach ($assignment_value_type->parent_nodes as $parent_node) {
1224
                    $codebase->taint->addPath($parent_node, $localized_property_node, '=');
1225
                }
1226
            }
1227
        }
1228
    }
1229
}
1230