AtomicMethodCallAnalyzer::analyze()   F
last analyzed

Complexity

Conditions 157
Paths > 20000

Size

Total Lines 815

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 157
nc 429496.7295
nop 9
dl 0
loc 815
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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