getMagicGetterOrSetterProperty()   F
last analyzed

Complexity

Conditions 24
Paths 60

Size

Total Lines 167

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
nc 60
nop 4
dl 0
loc 167
rs 3.3333
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\Call\Method;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
6
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
7
use Psalm\Internal\Analyzer\MethodAnalyzer;
8
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
9
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
10
use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentMapPopulator;
11
use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector;
12
use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsAnalyzer;
13
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
14
use Psalm\Internal\Analyzer\StatementsAnalyzer;
15
use Psalm\Internal\Analyzer\TypeAnalyzer;
16
use Psalm\Internal\Codebase\InternalCallMapHandler;
17
use Psalm\Codebase;
18
use Psalm\CodeLocation;
19
use Psalm\Context;
20
use Psalm\Internal\MethodIdentifier;
21
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
22
use Psalm\Issue\InvalidPropertyAssignmentValue;
23
use Psalm\Issue\MixedMethodCall;
24
use Psalm\Issue\MixedPropertyTypeCoercion;
25
use Psalm\Issue\PossiblyInvalidPropertyAssignmentValue;
26
use Psalm\Issue\PropertyTypeCoercion;
27
use Psalm\Issue\UndefinedThisPropertyAssignment;
28
use Psalm\Issue\UndefinedThisPropertyFetch;
29
use Psalm\IssueBuffer;
30
use Psalm\Storage\Assertion;
31
use Psalm\Type;
32
use Psalm\Type\Atomic\TNamedObject;
33
use function array_values;
34
use function array_shift;
35
use function get_class;
36
use function strtolower;
37
use function array_map;
38
use function array_merge;
39
use function explode;
40
use function in_array;
41
use function count;
42
43
class AtomicMethodCallAnalyzer extends CallAnalyzer
44
{
45
    /**
46
     * @param  StatementsAnalyzer             $statements_analyzer
47
     * @param  PhpParser\Node\Expr\MethodCall $stmt
48
     * @param  Codebase                       $codebase
49
     * @param  Context                        $context
50
     * @param  Type\Atomic\TNamedObject|Type\Atomic\TTemplateParam  $static_type
51
     * @param  ?string                        $lhs_var_id
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?string" 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...
52
     */
53
    public static function analyze(
54
        StatementsAnalyzer $statements_analyzer,
55
        PhpParser\Node\Expr\MethodCall $stmt,
56
        Codebase $codebase,
57
        Context $context,
58
        Type\Atomic $lhs_type_part,
59
        ?Type\Atomic $static_type,
60
        bool $is_intersection,
61
        $lhs_var_id,
62
        AtomicMethodCallAnalysisResult $result
63
    ) : void {
64
        $config = $codebase->config;
65
66
        if ($lhs_type_part instanceof Type\Atomic\TTemplateParam
67
            && !$lhs_type_part->as->isMixed()
68
        ) {
69
            $extra_types = $lhs_type_part->extra_types;
70
71
            $lhs_type_part = array_values(
72
                $lhs_type_part->as->getAtomicTypes()
73
            )[0];
74
75
            $lhs_type_part->from_docblock = true;
76
77
            if ($lhs_type_part instanceof TNamedObject) {
78
                $lhs_type_part->extra_types = $extra_types;
79
            } elseif ($lhs_type_part instanceof Type\Atomic\TObject && $extra_types) {
80
                $lhs_type_part = array_shift($extra_types);
81
                if ($extra_types) {
82
                    $lhs_type_part->extra_types = $extra_types;
83
                }
84
            }
85
86
            $result->has_mixed_method_call = true;
87
        }
88
89
        $source = $statements_analyzer->getSource();
90
91
        if (!$lhs_type_part instanceof TNamedObject) {
92
            self::handleInvalidClass(
93
                $statements_analyzer,
94
                $codebase,
95
                $stmt,
96
                $lhs_type_part,
97
                $lhs_var_id,
98
                $context,
99
                $is_intersection,
100
                $result
101
            );
102
103
            return;
104
        }
105
106
        if (!$context->collect_initializations
107
            && !$context->collect_mutations
108
            && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
109
            && (!(($parent_source = $statements_analyzer->getSource())
110
                    instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
111
                || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
112
        ) {
113
            $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
114
        }
115
116
        $result->has_valid_method_call_type = true;
117
118
        $fq_class_name = $lhs_type_part->value;
119
120
        $is_mock = ExpressionAnalyzer::isMock($fq_class_name);
121
122
        $result->has_mock = $result->has_mock || $is_mock;
123
124
        if ($fq_class_name === 'static') {
125
            $fq_class_name = (string) $context->self;
126
        }
127
128
        if ($is_mock ||
129
            $context->isPhantomClass($fq_class_name)
130
        ) {
131
            $result->return_type = Type::getMixed();
132
133
            ArgumentsAnalyzer::analyze(
134
                $statements_analyzer,
135
                $stmt->args,
136
                null,
137
                null,
138
                $context
139
            );
140
141
            return;
142
        }
143
144
        if ($lhs_var_id === '$this') {
145
            $does_class_exist = true;
146
        } else {
147
            $does_class_exist = ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
148
                $statements_analyzer,
149
                $fq_class_name,
150
                new CodeLocation($source, $stmt->var),
151
                $context->self,
152
                $context->calling_method_id,
153
                $statements_analyzer->getSuppressedIssues(),
154
                true,
155
                false,
156
                true,
157
                $lhs_type_part->from_docblock
158
            );
159
        }
160
161
        if (!$does_class_exist) {
162
            return;
163
        }
164
165
        $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
166
167
        $result->check_visibility = $result->check_visibility && !$class_storage->override_method_visibility;
168
169
        $intersection_types = $lhs_type_part->getIntersectionTypes();
170
171
        $all_intersection_return_type = null;
172
        $all_intersection_existent_method_ids = [];
173
174
        if ($intersection_types) {
175
            foreach ($intersection_types as $intersection_type) {
176
                $intersection_result = clone $result;
177
178
                /** @var ?Type\Union */
179
                $intersection_result->return_type = null;
180
181
                self::analyze(
182
                    $statements_analyzer,
183
                    $stmt,
184
                    $codebase,
185
                    $context,
186
                    $intersection_type,
187
                    $lhs_type_part,
188
                    true,
189
                    $lhs_var_id,
190
                    $intersection_result
191
                );
192
193
                $result->returns_by_ref = $intersection_result->returns_by_ref;
194
                $result->has_mock = $intersection_result->has_mock;
195
                $result->has_valid_method_call_type = $intersection_result->has_valid_method_call_type;
196
                $result->has_mixed_method_call = $intersection_result->has_mixed_method_call;
197
                $result->invalid_method_call_types = $intersection_result->invalid_method_call_types;
198
                $result->check_visibility = $intersection_result->check_visibility;
199
                $result->too_many_arguments = $intersection_result->too_many_arguments;
200
201
                $all_intersection_existent_method_ids = array_merge(
202
                    $all_intersection_existent_method_ids,
203
                    $intersection_result->existent_method_ids
204
                );
205
206
                if ($intersection_result->return_type) {
207
                    if (!$all_intersection_return_type || $all_intersection_return_type->isMixed()) {
208
                        $all_intersection_return_type = $intersection_result->return_type;
209
                    } else {
210
                        $all_intersection_return_type = Type::intersectUnionTypes(
211
                            $all_intersection_return_type,
212
                            $intersection_result->return_type,
213
                            $codebase
214
                        ) ?: Type::getMixed();
215
                    }
216
                }
217
            }
218
        }
219
220
        if (!$stmt->name instanceof PhpParser\Node\Identifier) {
221
            if (!$context->ignore_variable_method) {
222
                $codebase->analyzer->addMixedMemberName(
223
                    strtolower($fq_class_name) . '::',
224
                    $context->calling_method_id ?: $statements_analyzer->getFileName()
225
                );
226
            }
227
228
            $result->return_type = Type::getMixed();
229
            return;
230
        }
231
232
        $method_name_lc = strtolower($stmt->name->name);
233
234
        $method_id = new MethodIdentifier($fq_class_name, $method_name_lc);
235
        $cased_method_id = $fq_class_name . '::' . $stmt->name->name;
236
237
        $intersection_method_id = $intersection_types
238
            ? '(' . $lhs_type_part . ')'  . '::' . $stmt->name->name
239
            : null;
240
241
        $args = $stmt->args;
242
243
        $old_node_data = null;
244
245
        $naive_method_id = $method_id;
246
247
        $naive_method_exists = $codebase->methods->methodExists(
248
            $method_id,
249
            $context->calling_method_id,
250
            $codebase->collect_locations
251
                ? new CodeLocation($source, $stmt->name)
252
                : null,
253
            !$context->collect_initializations
254
                && !$context->collect_mutations
255
                ? $statements_analyzer
256
                : null,
257
            $statements_analyzer->getFilePath()
258
        );
259
260
        if (!$naive_method_exists
261
            && $class_storage->mixin instanceof Type\Atomic\TTemplateParam
262
            && $lhs_type_part instanceof Type\Atomic\TGenericObject
263
            && $class_storage->template_types
264
        ) {
265
            $param_position = \array_search(
266
                $class_storage->mixin->param_name,
267
                \array_keys($class_storage->template_types)
268
            );
269
270
            if ($param_position !== false
271
                && isset($lhs_type_part->type_params[$param_position])
272
            ) {
273
                if ($lhs_type_part->type_params[$param_position]->isSingle()) {
274
                    $lhs_type_part_new = array_values(
275
                        $lhs_type_part->type_params[$param_position]->getAtomicTypes()
276
                    )[0];
277
278
                    if ($lhs_type_part_new instanceof Type\Atomic\TNamedObject) {
279
                        $new_method_id = new MethodIdentifier(
280
                            $lhs_type_part_new->value,
281
                            $method_name_lc
282
                        );
283
284
                        $mixin_class_storage = $codebase->classlike_storage_provider->get($lhs_type_part_new->value);
285
286
                        if ($codebase->methods->methodExists(
287
                            $new_method_id,
288
                            $context->calling_method_id,
289
                            $codebase->collect_locations
290
                                ? new CodeLocation($source, $stmt->name)
291
                                : null,
292
                            !$context->collect_initializations
293
                                && !$context->collect_mutations
294
                                ? $statements_analyzer
295
                                : null,
296
                            $statements_analyzer->getFilePath()
297
                        )) {
298
                            $lhs_type_part = clone $lhs_type_part_new;
299
                            $class_storage = $mixin_class_storage;
300
301
                            $naive_method_exists = true;
302
                            $method_id = $new_method_id;
303
                        } elseif (isset($mixin_class_storage->pseudo_methods[$method_name_lc])) {
304
                            $lhs_type_part = clone $lhs_type_part_new;
305
                            $class_storage = $mixin_class_storage;
306
                            $method_id = $new_method_id;
307
                        }
308
                    }
309
                }
310
            }
311
        } elseif (!$naive_method_exists
312
            && $class_storage->mixin_declaring_fqcln
313
            && $class_storage->mixin instanceof Type\Atomic\TNamedObject
314
        ) {
315
            $new_method_id = new MethodIdentifier(
316
                $class_storage->mixin->value,
317
                $method_name_lc
318
            );
319
320
            if ($codebase->methods->methodExists(
321
                $new_method_id,
322
                $context->calling_method_id,
323
                $codebase->collect_locations
324
                    ? new CodeLocation($source, $stmt->name)
325
                    : null,
326
                !$context->collect_initializations
327
                    && !$context->collect_mutations
328
                    ? $statements_analyzer
329
                    : null,
330
                $statements_analyzer->getFilePath()
331
            )) {
332
                $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get(
333
                    $class_storage->mixin_declaring_fqcln
334
                );
335
336
                $mixin_class_template_params = ClassTemplateParamCollector::collect(
337
                    $codebase,
338
                    $mixin_declaring_class_storage,
339
                    $codebase->classlike_storage_provider->get($fq_class_name),
340
                    null,
341
                    $lhs_type_part,
342
                    $lhs_var_id
343
                );
344
345
                $lhs_type_part = clone $class_storage->mixin;
346
347
                $lhs_type_part->replaceTemplateTypesWithArgTypes(
348
                    new \Psalm\Internal\Type\TemplateResult([], $mixin_class_template_params ?: []),
349
                    $codebase
350
                );
351
352
                $lhs_type_expanded = \Psalm\Internal\Type\TypeExpander::expandUnion(
353
                    $codebase,
354
                    new Type\Union([$lhs_type_part]),
355
                    $mixin_declaring_class_storage->name,
356
                    $fq_class_name,
357
                    $class_storage->parent_class,
358
                    true,
359
                    false,
360
                    $class_storage->final
361
                );
362
363
                $new_lhs_type_part = array_values($lhs_type_expanded->getAtomicTypes())[0];
364
365
                if ($new_lhs_type_part instanceof Type\Atomic\TNamedObject) {
366
                    $lhs_type_part = $new_lhs_type_part;
367
                }
368
369
                $mixin_class_storage = $codebase->classlike_storage_provider->get($class_storage->mixin->value);
370
371
                $fq_class_name = $mixin_class_storage->name;
372
                $class_storage = $mixin_class_storage;
373
                $naive_method_exists = true;
374
                $method_id = $new_method_id;
375
            }
376
        }
377
378
        if (!$naive_method_exists
379
            || !MethodAnalyzer::isMethodVisible(
380
                $method_id,
381
                $context,
382
                $statements_analyzer->getSource()
383
            )
384
        ) {
385
            $interface_has_method = false;
386
387
            if ($class_storage->abstract && $class_storage->class_implements) {
388
                foreach ($class_storage->class_implements as $interface_fqcln_lc => $_) {
389
                    $interface_storage = $codebase->classlike_storage_provider->get($interface_fqcln_lc);
390
391
                    if (isset($interface_storage->methods[$method_name_lc])) {
392
                        $interface_has_method = true;
393
                        $fq_class_name = $interface_storage->name;
394
                        $method_id = new MethodIdentifier(
395
                            $fq_class_name,
396
                            $method_name_lc
397
                        );
398
                        break;
399
                    }
400
                }
401
            }
402
403
            if (!$interface_has_method
404
                && $codebase->methods->methodExists(
405
                    new MethodIdentifier($fq_class_name, '__call'),
406
                    $context->calling_method_id,
407
                    $codebase->collect_locations
408
                        ? new CodeLocation($source, $stmt->name)
409
                        : null,
410
                    !$context->collect_initializations
411
                        && !$context->collect_mutations
412
                        ? $statements_analyzer
413
                        : null,
414
                    $statements_analyzer->getFilePath()
415
                )
416
            ) {
417
                $new_call_context = MissingMethodCallHandler::handleMagicMethod(
418
                    $statements_analyzer,
419
                    $codebase,
420
                    $stmt,
421
                    $method_id,
422
                    $class_storage,
423
                    $context,
424
                    $config,
425
                    $all_intersection_return_type,
426
                    $result
427
                );
428
429
                if ($new_call_context) {
430
                    if ($method_id === $new_call_context->method_id) {
431
                        return;
432
                    }
433
434
                    $method_id = $new_call_context->method_id;
435
                    $args = $new_call_context->args;
436
                    $old_node_data = $statements_analyzer->node_data;
437
                } else {
438
                    return;
439
                }
440
            }
441
        }
442
443
        $source_source = $statements_analyzer->getSource();
444
445
        /**
446
         * @var \Psalm\Internal\Analyzer\ClassLikeAnalyzer|null
447
         */
448
        $classlike_source = $source_source->getSource();
449
        $classlike_source_fqcln = $classlike_source ? $classlike_source->getFQCLN() : null;
450
451
        if ($lhs_var_id === '$this'
452
            && $context->self
453
            && $classlike_source_fqcln
454
            && $fq_class_name !== $context->self
455
            && $codebase->methods->methodExists(
456
                new MethodIdentifier($context->self, $method_name_lc)
457
            )
458
        ) {
459
            $method_id = new MethodIdentifier($context->self, $method_name_lc);
460
            $cased_method_id = $context->self . '::' . $stmt->name->name;
461
            $fq_class_name = $context->self;
462
        }
463
464
        $is_interface = false;
465
466
        if ($codebase->interfaceExists($fq_class_name)) {
467
            $is_interface = true;
468
        }
469
470
        $source_method_id = $source instanceof FunctionLikeAnalyzer
471
            ? $source->getId()
472
            : null;
473
474
        $corrected_method_exists = ($naive_method_exists && $method_id === $naive_method_id)
475
            || ($method_id !== $naive_method_id
476
                && $codebase->methods->methodExists(
477
                    $method_id,
478
                    $context->calling_method_id,
479
                    $codebase->collect_locations && $method_id !== $source_method_id
480
                        ? new CodeLocation($source, $stmt->name)
481
                        : null
482
                ));
483
484
        if (!$corrected_method_exists
485
            || ($config->use_phpdoc_method_without_magic_or_parent
486
                && isset($class_storage->pseudo_methods[$method_name_lc]))
487
        ) {
488
            MissingMethodCallHandler::handleMissingOrMagicMethod(
489
                $statements_analyzer,
490
                $codebase,
491
                $stmt,
492
                $method_id,
493
                $is_interface,
494
                $context,
495
                $config,
496
                $all_intersection_return_type,
497
                $result
498
            );
499
500
            if ($all_intersection_return_type && $all_intersection_existent_method_ids) {
501
                $result->existent_method_ids = array_merge(
0 ignored issues
show
Documentation Bug introduced by
It seems like \array_merge($result->ex...on_existent_method_ids) of type array is incompatible with the declared type array<integer,string> of property $existent_method_ids.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
502
                    $result->existent_method_ids,
503
                    $all_intersection_existent_method_ids
504
                );
505
506
                if (!$result->return_type) {
507
                    $result->return_type = $all_intersection_return_type;
508
                } else {
509
                    $result->return_type = Type::combineUnionTypes($all_intersection_return_type, $result->return_type);
510
                }
511
512
                return;
513
            }
514
515
            if ((!$is_interface && !$config->use_phpdoc_method_without_magic_or_parent)
516
                || !isset($class_storage->pseudo_methods[$method_name_lc])
517
            ) {
518
                if ($is_interface) {
519
                    $result->non_existent_interface_method_ids[] = $intersection_method_id ?: $cased_method_id;
520
                } else {
521
                    $result->non_existent_class_method_ids[] = $intersection_method_id ?: $cased_method_id;
522
                }
523
            }
524
525
            return;
526
        }
527
528
        if ($codebase->store_node_types
529
            && !$context->collect_initializations
530
            && !$context->collect_mutations
531
        ) {
532
            $codebase->analyzer->addNodeReference(
533
                $statements_analyzer->getFilePath(),
534
                $stmt->name,
535
                $method_id . '()'
536
            );
537
        }
538
539
        if ($context->collect_initializations && $context->calling_method_id) {
540
            list($calling_method_class) = explode('::', $context->calling_method_id);
541
            $codebase->file_reference_provider->addMethodReferenceToClassMember(
542
                $calling_method_class . '::__construct',
543
                strtolower((string) $method_id)
544
            );
545
        }
546
547
        $result->existent_method_ids[] = $method_id;
548
549
        if ($stmt->var instanceof PhpParser\Node\Expr\Variable
550
            && ($context->collect_initializations || $context->collect_mutations)
551
            && $stmt->var->name === 'this'
552
            && $source instanceof FunctionLikeAnalyzer
553
        ) {
554
            self::collectSpecialInformation($source, $stmt->name->name, $context);
555
        }
556
557
        $fq_class_name = $codebase->classlikes->getUnAliasedName($fq_class_name);
558
559
        $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
560
561
        $parent_source = $statements_analyzer->getSource();
562
563
        $class_template_params = ClassTemplateParamCollector::collect(
564
            $codebase,
565
            $codebase->methods->getClassLikeStorageForMethod($method_id),
566
            $class_storage,
567
            $method_name_lc,
568
            $lhs_type_part,
569
            $lhs_var_id
570
        );
571
572
        if ($lhs_var_id === '$this' && $parent_source instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) {
573
            $grandparent_source = $parent_source->getSource();
574
575
            if ($grandparent_source instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) {
576
                $fq_trait_name = $grandparent_source->getFQCLN();
577
578
                $fq_trait_name_lc = strtolower($fq_trait_name);
579
580
                $trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name_lc);
581
582
                if (isset($trait_storage->methods[$method_name_lc])) {
583
                    $trait_method_id = new MethodIdentifier($trait_storage->name, $method_name_lc);
584
585
                    $class_template_params = ClassTemplateParamCollector::collect(
586
                        $codebase,
587
                        $codebase->methods->getClassLikeStorageForMethod($trait_method_id),
588
                        $class_storage,
589
                        $method_name_lc,
590
                        $lhs_type_part,
591
                        $lhs_var_id
592
                    );
593
                }
594
            }
595
        }
596
597
        $template_result = new \Psalm\Internal\Type\TemplateResult([], $class_template_params ?: []);
598
599
        if ($codebase->store_node_types
600
            && !$context->collect_initializations
601
            && !$context->collect_mutations
602
        ) {
603
            ArgumentMapPopulator::recordArgumentPositions(
604
                $statements_analyzer,
605
                $stmt,
606
                $codebase,
607
                (string) $method_id
608
            );
609
        }
610
611
        if (self::checkMethodArgs(
612
            $method_id,
613
            $args,
614
            $template_result,
615
            $context,
616
            new CodeLocation($source, $stmt->name),
617
            $statements_analyzer
618
        ) === false) {
619
            return;
620
        }
621
622
        $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
623
624
        $can_memoize = false;
625
626
        $return_type_candidate = MethodCallReturnTypeFetcher::fetch(
627
            $statements_analyzer,
628
            $codebase,
629
            $stmt,
630
            $context,
631
            $method_id,
632
            $declaring_method_id,
633
            $naive_method_id,
634
            $cased_method_id,
635
            $lhs_type_part,
636
            $static_type,
637
            $args,
638
            $result,
639
            $template_result
640
        );
641
642
        $in_call_map = InternalCallMapHandler::inCallMap((string) ($declaring_method_id ?: $method_id));
643
644
        if (!$in_call_map) {
645
            $name_code_location = new CodeLocation($statements_analyzer, $stmt->name);
646
647
            if ($result->check_visibility) {
648
                if (MethodVisibilityAnalyzer::analyze(
649
                    $method_id,
650
                    $context,
651
                    $statements_analyzer->getSource(),
652
                    $name_code_location,
653
                    $statements_analyzer->getSuppressedIssues()
654
                ) === false) {
655
                    self::updateResultReturnType(
656
                        $result,
657
                        $return_type_candidate,
658
                        $all_intersection_return_type,
659
                        $method_name_lc,
660
                        $codebase
661
                    );
662
663
                    return;
664
                }
665
            }
666
667
            MethodCallProhibitionAnalyzer::analyze(
668
                $codebase,
669
                $context,
670
                $method_id,
671
                $name_code_location,
672
                $statements_analyzer->getSuppressedIssues()
673
            );
674
675
            $getter_return_type = self::getMagicGetterOrSetterProperty(
676
                $statements_analyzer,
677
                $stmt,
678
                $context,
679
                $fq_class_name
680
            );
681
682
            if ($getter_return_type) {
683
                $return_type_candidate = $getter_return_type;
684
            }
685
        }
686
687
        try {
688
            $method_storage = $codebase->methods->getStorage($declaring_method_id ?: $method_id);
689
        } catch (\UnexpectedValueException $e) {
690
            $method_storage = null;
691
        }
692
693
        if ($method_storage) {
694
            if (!$context->collect_mutations && !$context->collect_initializations) {
695
                $can_memoize = MethodCallPurityAnalyzer::analyze(
696
                    $statements_analyzer,
697
                    $codebase,
698
                    $stmt,
699
                    $lhs_var_id,
700
                    $cased_method_id,
701
                    $method_id,
702
                    $method_storage,
703
                    $class_storage,
704
                    $context,
705
                    $config
706
                );
707
            }
708
709
            $has_packed_arg = false;
710
            foreach ($args as $arg) {
711
                $has_packed_arg = $has_packed_arg || $arg->unpack;
712
            }
713
714
            if (!$has_packed_arg) {
715
                $has_variadic_param = $method_storage->variadic;
716
717
                foreach ($method_storage->params as $param) {
718
                    $has_variadic_param = $has_variadic_param || $param->is_variadic;
719
                }
720
721
                for ($i = count($args), $j = count($method_storage->params); $i < $j; ++$i) {
722
                    $param = $method_storage->params[$i];
723
724
                    if (!$param->is_optional
725
                        && !$param->is_variadic
726
                        && !$in_call_map
727
                    ) {
728
                        $result->too_few_arguments = true;
729
                        $result->too_few_arguments_method_ids[] = $declaring_method_id ?: $method_id;
730
                    }
731
                }
732
733
                if ($has_variadic_param || count($method_storage->params) >= count($args) || $in_call_map) {
734
                    $result->too_many_arguments = false;
735
                } else {
736
                    $result->too_many_arguments_method_ids[] = $declaring_method_id ?: $method_id;
737
                }
738
            }
739
740
            $class_template_params = $template_result->upper_bounds;
741
742
            if ($method_storage->assertions) {
743
                self::applyAssertionsToContext(
744
                    $stmt->name,
745
                    ExpressionIdentifier::getArrayVarId($stmt->var, null, $statements_analyzer),
746
                    $method_storage->assertions,
747
                    $args,
748
                    $class_template_params,
749
                    $context,
750
                    $statements_analyzer
751
                );
752
            }
753
754
            if ($method_storage->if_true_assertions) {
755
                $statements_analyzer->node_data->setIfTrueAssertions(
756
                    $stmt,
757
                    array_map(
758
                        function (Assertion $assertion) use (
759
                            $class_template_params,
760
                            $lhs_var_id
761
                        ) : Assertion {
762
                            return $assertion->getUntemplatedCopy(
763
                                $class_template_params ?: [],
764
                                $lhs_var_id
765
                            );
766
                        },
767
                        $method_storage->if_true_assertions
768
                    )
769
                );
770
            }
771
772
            if ($method_storage->if_false_assertions) {
773
                $statements_analyzer->node_data->setIfFalseAssertions(
774
                    $stmt,
775
                    array_map(
776
                        function (Assertion $assertion) use (
777
                            $class_template_params,
778
                            $lhs_var_id
779
                        ) : Assertion {
780
                            return $assertion->getUntemplatedCopy(
781
                                $class_template_params ?: [],
782
                                $lhs_var_id
783
                            );
784
                        },
785
                        $method_storage->if_false_assertions
786
                    )
787
                );
788
            }
789
        }
790
791
        if ($old_node_data) {
792
            $statements_analyzer->node_data = $old_node_data;
793
        }
794
795
        if (!$args && $lhs_var_id) {
796
            if ($config->memoize_method_calls || $can_memoize) {
797
                $method_var_id = $lhs_var_id . '->' . $method_name_lc . '()';
798
799
                if (isset($context->vars_in_scope[$method_var_id])) {
800
                    $return_type_candidate = clone $context->vars_in_scope[$method_var_id];
801
802
                    if ($can_memoize) {
803
                        /** @psalm-suppress UndefinedPropertyAssignment */
804
                        $stmt->pure = true;
805
                    }
806
                } else {
807
                    $context->vars_in_scope[$method_var_id] = $return_type_candidate;
808
                }
809
            }
810
        }
811
812
        if ($codebase->methods_to_rename) {
813
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
814
815
            foreach ($codebase->methods_to_rename as $original_method_id => $new_method_name) {
816
                if ($declaring_method_id && (strtolower((string) $declaring_method_id)) === $original_method_id) {
817
                    $file_manipulations = [
818
                        new \Psalm\FileManipulation(
819
                            (int) $stmt->name->getAttribute('startFilePos'),
820
                            (int) $stmt->name->getAttribute('endFilePos') + 1,
821
                            $new_method_name
822
                        )
823
                    ];
824
825
                    \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
826
                        $statements_analyzer->getFilePath(),
827
                        $file_manipulations
828
                    );
829
                }
830
            }
831
        }
832
833
        if ($config->after_method_checks) {
834
            $file_manipulations = [];
835
836
            $appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
837
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
838
839
            if ($appearing_method_id !== null && $declaring_method_id !== null) {
840
                foreach ($config->after_method_checks as $plugin_fq_class_name) {
841
                    $plugin_fq_class_name::afterMethodCallAnalysis(
842
                        $stmt,
843
                        (string) $method_id,
844
                        (string) $appearing_method_id,
845
                        (string) $declaring_method_id,
846
                        $context,
847
                        $source,
848
                        $codebase,
849
                        $file_manipulations,
850
                        $return_type_candidate
851
                    );
852
                }
853
            }
854
855
            if ($file_manipulations) {
856
                FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
857
            }
858
        }
859
860
        self::updateResultReturnType(
861
            $result,
862
            $return_type_candidate,
863
            $all_intersection_return_type,
864
            $method_name_lc,
865
            $codebase
866
        );
867
    }
868
869
    private static function updateResultReturnType(
870
        AtomicMethodCallAnalysisResult $result,
871
        ?Type\Union $return_type_candidate,
872
        ?Type\Union $all_intersection_return_type,
873
        string $method_name,
874
        Codebase $codebase
875
    ) : void {
876
        if ($return_type_candidate) {
877
            if ($all_intersection_return_type) {
878
                $return_type_candidate = Type::intersectUnionTypes(
879
                    $all_intersection_return_type,
880
                    $return_type_candidate,
881
                    $codebase
882
                ) ?: Type::getMixed();
883
            }
884
885
            if (!$result->return_type) {
886
                $result->return_type = $return_type_candidate;
887
            } else {
888
                $result->return_type = Type::combineUnionTypes($return_type_candidate, $result->return_type);
889
            }
890
        } elseif ($all_intersection_return_type) {
891
            if (!$result->return_type) {
892
                $result->return_type = $all_intersection_return_type;
893
            } else {
894
                $result->return_type = Type::combineUnionTypes($all_intersection_return_type, $result->return_type);
895
            }
896
        } elseif ($method_name === '__tostring') {
897
            $result->return_type = Type::getString();
898
        } else {
899
            $result->return_type = Type::getMixed();
900
        }
901
    }
902
903
    private static function handleInvalidClass(
904
        StatementsAnalyzer $statements_analyzer,
905
        Codebase $codebase,
906
        PhpParser\Node\Expr\MethodCall $stmt,
907
        Type\Atomic $lhs_type_part,
908
        ?string $lhs_var_id,
909
        Context $context,
910
        bool $is_intersection,
911
        AtomicMethodCallAnalysisResult $result
912
    ) : void {
913
        switch (get_class($lhs_type_part)) {
914
            case Type\Atomic\TNull::class:
915
            case Type\Atomic\TFalse::class:
916
                // handled above
917
                return;
918
919
            case Type\Atomic\TTemplateParam::class:
920
            case Type\Atomic\TEmptyMixed::class:
921
            case Type\Atomic\TEmpty::class:
922
            case Type\Atomic\TMixed::class:
923
            case Type\Atomic\TNonEmptyMixed::class:
924
            case Type\Atomic\TObject::class:
925
            case Type\Atomic\TObjectWithProperties::class:
926
                if (!$context->collect_initializations
927
                    && !$context->collect_mutations
928
                    && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
929
                    && (!(($parent_source = $statements_analyzer->getSource())
930
                            instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
931
                        || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
932
                ) {
933
                    $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
934
                }
935
936
                $result->has_mixed_method_call = true;
937
938
                if ($lhs_type_part instanceof Type\Atomic\TObjectWithProperties
939
                    && $stmt->name instanceof PhpParser\Node\Identifier
940
                    && isset($lhs_type_part->methods[$stmt->name->name])
941
                ) {
942
                    $result->existent_method_ids[] = $lhs_type_part->methods[$stmt->name->name];
943
                } elseif (!$is_intersection) {
944
                    if ($stmt->name instanceof PhpParser\Node\Identifier) {
945
                        $codebase->analyzer->addMixedMemberName(
946
                            strtolower($stmt->name->name),
947
                            $context->calling_method_id ?: $statements_analyzer->getFileName()
948
                        );
949
                    }
950
951
                    if ($context->check_methods) {
952
                        $message = 'Cannot determine the type of the object'
953
                            . ' on the left hand side of this expression';
954
955
                        if ($lhs_var_id) {
956
                            $message = 'Cannot determine the type of ' . $lhs_var_id;
957
958
                            if ($stmt->name instanceof PhpParser\Node\Identifier) {
959
                                $message .= ' when calling method ' . $stmt->name->name;
960
                            }
961
                        }
962
963
                        if (IssueBuffer::accepts(
964
                            new MixedMethodCall(
965
                                $message,
966
                                new CodeLocation($statements_analyzer, $stmt->name)
967
                            ),
968
                            $statements_analyzer->getSuppressedIssues()
969
                        )) {
970
                            // fall through
971
                        }
972
                    }
973
                }
974
975
                if (ArgumentsAnalyzer::analyze(
976
                    $statements_analyzer,
977
                    $stmt->args,
978
                    null,
979
                    null,
980
                    $context
981
                ) === false) {
982
                    return;
983
                }
984
985
                $result->return_type = Type::getMixed();
986
                return;
987
988
            default:
989
                $result->invalid_method_call_types[] = (string)$lhs_type_part;
990
                return;
991
        }
992
    }
993
994
    /**
995
     * Check properties accessed with magic getters and setters.
996
     * If `@psalm-seal-properties` is set, they must be defined.
997
     * If an `@property` annotation is specified, the setter must set something with the correct
998
     * type.
999
     *
1000
     * @param StatementsAnalyzer $statements_analyzer
1001
     * @param PhpParser\Node\Expr\MethodCall $stmt
1002
     * @param string $fq_class_name
1003
     */
1004
    private static function getMagicGetterOrSetterProperty(
1005
        StatementsAnalyzer $statements_analyzer,
1006
        PhpParser\Node\Expr\MethodCall $stmt,
1007
        Context $context,
1008
        $fq_class_name
1009
    ) : ?Type\Union {
1010
        if (!$stmt->name instanceof PhpParser\Node\Identifier) {
1011
            return null;
1012
        }
1013
1014
        $method_name = strtolower($stmt->name->name);
1015
        if (!in_array($method_name, ['__get', '__set'], true)) {
1016
            return null;
1017
        }
1018
1019
        $codebase = $statements_analyzer->getCodebase();
1020
1021
        $first_arg_value = $stmt->args[0]->value;
1022
        if (!$first_arg_value instanceof PhpParser\Node\Scalar\String_) {
1023
            return null;
1024
        }
1025
1026
        $prop_name = $first_arg_value->value;
1027
        $property_id = $fq_class_name . '::$' . $prop_name;
1028
1029
        $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
1030
1031
        $codebase->properties->propertyExists(
1032
            $property_id,
1033
            $method_name === '__get',
1034
            $statements_analyzer,
1035
            $context,
1036
            new CodeLocation($statements_analyzer->getSource(), $stmt)
1037
        );
1038
1039
        switch ($method_name) {
1040
            case '__set':
1041
                // If `@psalm-seal-properties` is set, the property must be defined with
1042
                // a `@property` annotation
1043
                if ($class_storage->sealed_properties
1044
                    && !isset($class_storage->pseudo_property_set_types['$' . $prop_name])
1045
                    && IssueBuffer::accepts(
1046
                        new UndefinedThisPropertyAssignment(
1047
                            'Instance property ' . $property_id . ' is not defined',
1048
                            new CodeLocation($statements_analyzer->getSource(), $stmt),
1049
                            $property_id
1050
                        ),
1051
                        $statements_analyzer->getSuppressedIssues()
1052
                    )
1053
                ) {
1054
                    // fall through
1055
                }
1056
1057
                // If a `@property` annotation is set, the type of the value passed to the
1058
                // magic setter must match the annotation.
1059
                $second_arg_type = $statements_analyzer->node_data->getType($stmt->args[1]->value);
1060
1061
                if (isset($class_storage->pseudo_property_set_types['$' . $prop_name]) && $second_arg_type) {
1062
                    $pseudo_set_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
1063
                        $codebase,
1064
                        $class_storage->pseudo_property_set_types['$' . $prop_name],
1065
                        $fq_class_name,
1066
                        new Type\Atomic\TNamedObject($fq_class_name),
1067
                        $class_storage->parent_class
1068
                    );
1069
1070
                    $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
1071
1072
                    $type_match_found = TypeAnalyzer::isContainedBy(
1073
                        $codebase,
1074
                        $second_arg_type,
1075
                        $pseudo_set_type,
1076
                        $second_arg_type->ignore_nullable_issues,
1077
                        $second_arg_type->ignore_falsable_issues,
1078
                        $union_comparison_results
1079
                    );
1080
1081
                    if ($union_comparison_results->type_coerced) {
1082
                        if ($union_comparison_results->type_coerced_from_mixed) {
1083
                            if (IssueBuffer::accepts(
1084
                                new MixedPropertyTypeCoercion(
1085
                                    $prop_name . ' expects \'' . $pseudo_set_type->getId() . '\', '
1086
                                        . ' parent type `' . $second_arg_type . '` provided',
1087
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
1088
                                    $property_id
1089
                                ),
1090
                                $statements_analyzer->getSuppressedIssues()
1091
                            )) {
1092
                                // keep soldiering on
1093
                            }
1094
                        } else {
1095
                            if (IssueBuffer::accepts(
1096
                                new PropertyTypeCoercion(
1097
                                    $prop_name . ' expects \'' . $pseudo_set_type->getId() . '\', '
1098
                                        . ' parent type `' . $second_arg_type . '` provided',
1099
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
1100
                                    $property_id
1101
                                ),
1102
                                $statements_analyzer->getSuppressedIssues()
1103
                            )) {
1104
                                // keep soldiering on
1105
                            }
1106
                        }
1107
                    }
1108
1109
                    if (!$type_match_found && !$union_comparison_results->type_coerced_from_mixed) {
1110
                        if (TypeAnalyzer::canBeContainedBy(
1111
                            $codebase,
1112
                            $second_arg_type,
1113
                            $pseudo_set_type
1114
                        )) {
1115
                            if (IssueBuffer::accepts(
1116
                                new PossiblyInvalidPropertyAssignmentValue(
1117
                                    $prop_name . ' with declared type \''
1118
                                    . $pseudo_set_type
1119
                                    . '\' cannot be assigned possibly different type \'' . $second_arg_type . '\'',
1120
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
1121
                                    $property_id
1122
                                ),
1123
                                $statements_analyzer->getSuppressedIssues()
1124
                            )) {
1125
                                // fall through
1126
                            }
1127
                        } else {
1128
                            if (IssueBuffer::accepts(
1129
                                new InvalidPropertyAssignmentValue(
1130
                                    $prop_name . ' with declared type \''
1131
                                    . $pseudo_set_type
1132
                                    . '\' cannot be assigned type \'' . $second_arg_type . '\'',
1133
                                    new CodeLocation($statements_analyzer->getSource(), $stmt),
1134
                                    $property_id
1135
                                ),
1136
                                $statements_analyzer->getSuppressedIssues()
1137
                            )) {
1138
                                // fall through
1139
                            }
1140
                        }
1141
                    }
1142
                }
1143
                break;
1144
1145
            case '__get':
1146
                // If `@psalm-seal-properties` is set, the property must be defined with
1147
                // a `@property` annotation
1148
                if ($class_storage->sealed_properties
1149
                    && !isset($class_storage->pseudo_property_get_types['$' . $prop_name])
1150
                    && IssueBuffer::accepts(
1151
                        new UndefinedThisPropertyFetch(
1152
                            'Instance property ' . $property_id . ' is not defined',
1153
                            new CodeLocation($statements_analyzer->getSource(), $stmt),
1154
                            $property_id
1155
                        ),
1156
                        $statements_analyzer->getSuppressedIssues()
1157
                    )
1158
                ) {
1159
                    // fall through
1160
                }
1161
1162
                if (isset($class_storage->pseudo_property_get_types['$' . $prop_name])) {
1163
                    return clone $class_storage->pseudo_property_get_types['$' . $prop_name];
1164
                }
1165
1166
                break;
1167
        }
1168
1169
        return null;
1170
    }
1171
}
1172