Methods   F
last analyzed

Complexity

Total Complexity 180

Size/Duplication

Total Lines 1055
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 28

Importance

Changes 0
Metric Value
dl 0
loc 1055
rs 1.3799
c 0
b 0
f 0
wmc 180
lcom 1
cbo 28

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
F methodExists() 0 223 49
F getMethodParams() 0 144 25
F localizeType() 0 115 26
A getExtendedTemplatedTypes() 0 28 4
A isVariadic() 0 10 2
F getMethodReturnType() 0 179 36
A getMethodReturnsByRef() 0 18 4
A getMethodReturnTypeLocation() 0 27 5
A setDeclaringMethodId() 0 13 1
A setAppearingMethodId() 0 13 1
A getDeclaringMethodId() 0 19 4
A getAppearingMethodId() 0 15 2
A getOverriddenMethodIds() 0 11 2
A getCasedMethodId() 0 24 4
A getUserMethodStorage() 0 16 3
A getClassLikeStorageForMethod() 0 30 5
A getStorage() 0 18 3
A hasStorage() 0 16 3

How to fix   Complexity   

Complex Class

Complex classes like Methods often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Methods, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Psalm\Internal\Codebase;
3
4
use function array_pop;
5
use function assert;
6
use function count;
7
use function explode;
8
use PhpParser;
9
use function preg_replace;
10
use Psalm\Codebase;
11
use Psalm\CodeLocation;
12
use Psalm\Context;
13
use Psalm\Internal\Analyzer\TypeAnalyzer;
14
use Psalm\Internal\MethodIdentifier;
15
use Psalm\Internal\Provider\ClassLikeStorageProvider;
16
use Psalm\Internal\Provider\FileReferenceProvider;
17
use Psalm\Internal\Provider\MethodExistenceProvider;
18
use Psalm\Internal\Provider\MethodParamsProvider;
19
use Psalm\Internal\Provider\MethodReturnTypeProvider;
20
use Psalm\Internal\Provider\MethodVisibilityProvider;
21
use Psalm\StatementsSource;
22
use Psalm\Storage\ClassLikeStorage;
23
use Psalm\Storage\FunctionLikeParameter;
24
use Psalm\Storage\MethodStorage;
25
use Psalm\Type;
26
use function reset;
27
use function strtolower;
28
29
/**
30
 * @internal
31
 *
32
 * Handles information about class methods
33
 */
34
class Methods
35
{
36
    /**
37
     * @var ClassLikeStorageProvider
38
     */
39
    private $classlike_storage_provider;
40
41
    /**
42
     * @var bool
43
     */
44
    public $collect_locations = false;
45
46
    /**
47
     * @var FileReferenceProvider
48
     */
49
    public $file_reference_provider;
50
51
    /**
52
     * @var ClassLikes
53
     */
54
    private $classlikes;
55
56
    /** @var MethodReturnTypeProvider */
57
    public $return_type_provider;
58
59
    /** @var MethodParamsProvider */
60
    public $params_provider;
61
62
    /** @var MethodExistenceProvider */
63
    public $existence_provider;
64
65
    /** @var MethodVisibilityProvider */
66
    public $visibility_provider;
67
68
    /**
69
     * @param ClassLikeStorageProvider $storage_provider
70
     */
71
    public function __construct(
72
        ClassLikeStorageProvider $storage_provider,
73
        FileReferenceProvider $file_reference_provider,
74
        ClassLikes $classlikes
75
    ) {
76
        $this->classlike_storage_provider = $storage_provider;
77
        $this->file_reference_provider = $file_reference_provider;
78
        $this->classlikes = $classlikes;
79
        $this->return_type_provider = new MethodReturnTypeProvider();
80
        $this->existence_provider = new MethodExistenceProvider();
81
        $this->visibility_provider = new MethodVisibilityProvider();
82
        $this->params_provider = new MethodParamsProvider();
83
    }
84
85
    /**
86
     * Whether or not a given method exists
87
     * @param lowercase-string|null $calling_method_id
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string|null could not be parsed: Unknown type name "lowercase-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...
88
     */
89
    public function methodExists(
90
        MethodIdentifier $method_id,
91
        ?string $calling_method_id = null,
92
        CodeLocation $code_location = null,
93
        StatementsSource $source = null,
94
        string $source_file_path = null
95
    ) : bool {
96
        $fq_class_name = $method_id->fq_class_name;
97
        $method_name = $method_id->method_name;
98
99
        if ($this->existence_provider->has($fq_class_name)) {
100
            $method_exists = $this->existence_provider->doesMethodExist(
101
                $fq_class_name,
102
                $method_name,
103
                $source,
104
                $code_location
105
            );
106
107
            if ($method_exists !== null) {
108
                return $method_exists;
109
            }
110
        }
111
112
        $old_method_id = null;
113
114
        $fq_class_name = strtolower($this->classlikes->getUnAliasedName($fq_class_name));
115
116
        try {
117
            $class_storage = $this->classlike_storage_provider->get($fq_class_name);
118
        } catch (\InvalidArgumentException $e) {
119
            return false;
120
        }
121
122
        $source_file_path = $source ? $source->getFilePath() : $source_file_path;
123
124
        $calling_class_name = $source ? $source->getFQCLN() : null;
125
126
        if (!$calling_class_name && $calling_method_id) {
127
            $calling_class_name = explode('::', $calling_method_id)[0];
128
        }
129
130
        if (isset($class_storage->declaring_method_ids[$method_name])) {
131
            $declaring_method_id = $class_storage->declaring_method_ids[$method_name];
132
133
            if ($calling_method_id === strtolower((string) $declaring_method_id)) {
134
                return true;
135
            }
136
137
            $declaring_fq_class_name = strtolower($declaring_method_id->fq_class_name);
138
139
            if ($declaring_fq_class_name !== strtolower((string) $calling_class_name)) {
140
                if ($calling_method_id) {
141
                    $this->file_reference_provider->addMethodReferenceToClass(
142
                        $calling_method_id,
143
                        $declaring_fq_class_name
144
                    );
145
                } elseif ($source_file_path) {
146
                    $this->file_reference_provider->addNonMethodReferenceToClass(
147
                        $source_file_path,
148
                        $declaring_fq_class_name
149
                    );
150
                }
151
            }
152
153
            if ((string) $method_id !== (string) $declaring_method_id
154
                && $class_storage->user_defined
155
                && isset($class_storage->potential_declaring_method_ids[$method_name])
156
            ) {
157
                foreach ($class_storage->potential_declaring_method_ids[$method_name] as $potential_id => $_) {
158
                    if ($calling_method_id) {
159
                        $this->file_reference_provider->addMethodReferenceToClassMember(
160
                            $calling_method_id,
161
                            $potential_id
162
                        );
163
                    } elseif ($source_file_path) {
164
                        $this->file_reference_provider->addFileReferenceToClassMember(
165
                            $source_file_path,
166
                            $potential_id
167
                        );
168
                    }
169
                }
170
            } else {
171
                if ($calling_method_id) {
172
                    $this->file_reference_provider->addMethodReferenceToClassMember(
173
                        $calling_method_id,
174
                        strtolower((string) $declaring_method_id)
175
                    );
176
                } elseif ($source_file_path) {
177
                    $this->file_reference_provider->addFileReferenceToClassMember(
178
                        $source_file_path,
179
                        strtolower((string) $declaring_method_id)
180
                    );
181
                }
182
            }
183
184
            if ($this->collect_locations && $code_location) {
185
                $this->file_reference_provider->addCallingLocationForClassMethod(
186
                    $code_location,
187
                    strtolower((string) $declaring_method_id)
188
                );
189
            }
190
191
            foreach ($class_storage->class_implements as $fq_interface_name) {
192
                $interface_method_id_lc = strtolower($fq_interface_name . '::' . $method_name);
193
194
                if ($this->collect_locations && $code_location) {
195
                    $this->file_reference_provider->addCallingLocationForClassMethod(
196
                        $code_location,
197
                        $interface_method_id_lc
198
                    );
199
                }
200
201
                if ($calling_method_id) {
202
                    $this->file_reference_provider->addMethodReferenceToClassMember(
203
                        $calling_method_id,
204
                        $interface_method_id_lc
205
                    );
206
                } elseif ($source_file_path) {
207
                    $this->file_reference_provider->addFileReferenceToClassMember(
208
                        $source_file_path,
209
                        $interface_method_id_lc
210
                    );
211
                }
212
            }
213
214
            $declaring_method_class = $declaring_method_id->fq_class_name;
215
            $declaring_method_name = $declaring_method_id->method_name;
216
217
            $declaring_class_storage = $this->classlike_storage_provider->get($declaring_method_class);
218
219
            if (isset($declaring_class_storage->overridden_method_ids[$declaring_method_name])) {
220
                $overridden_method_ids = $declaring_class_storage->overridden_method_ids[$declaring_method_name];
221
222
                foreach ($overridden_method_ids as $overridden_method_id) {
223
                    if ($this->collect_locations && $code_location) {
224
                        $this->file_reference_provider->addCallingLocationForClassMethod(
225
                            $code_location,
226
                            strtolower((string) $overridden_method_id)
227
                        );
228
                    }
229
230
                    if ($calling_method_id) {
231
                        // also store failures in case the method is added later
232
                        $this->file_reference_provider->addMethodReferenceToClassMember(
233
                            $calling_method_id,
234
                            strtolower((string) $overridden_method_id)
235
                        );
236
                    } elseif ($source_file_path) {
237
                        $this->file_reference_provider->addFileReferenceToClassMember(
238
                            $source_file_path,
239
                            strtolower((string) $overridden_method_id)
240
                        );
241
                    }
242
                }
243
            }
244
245
            return true;
246
        }
247
248
        if ($source_file_path && $fq_class_name !== strtolower((string) $calling_class_name)) {
249
            if ($calling_method_id) {
250
                $this->file_reference_provider->addMethodReferenceToClass(
251
                    $calling_method_id,
252
                    $fq_class_name
253
                );
254
            } else {
255
                $this->file_reference_provider->addNonMethodReferenceToClass(
256
                    $source_file_path,
257
                    $fq_class_name
258
                );
259
            }
260
        }
261
262
        if ($class_storage->abstract && isset($class_storage->overridden_method_ids[$method_name])) {
263
            return true;
264
        }
265
266
        // support checking oldstyle constructors
267
        if ($method_name === '__construct') {
268
            $method_name_parts = explode('\\', $fq_class_name);
269
            $old_constructor_name = array_pop($method_name_parts);
270
            $old_method_id = $fq_class_name . '::' . $old_constructor_name;
271
        }
272
273
        if (!$class_storage->user_defined
274
            && (InternalCallMapHandler::inCallMap((string) $method_id)
275
                || ($old_method_id && InternalCallMapHandler::inCallMap($old_method_id)))
276
        ) {
277
            return true;
278
        }
279
280
        foreach ($class_storage->parent_classes + $class_storage->used_traits as $potential_future_declaring_fqcln) {
281
            $potential_id = strtolower($potential_future_declaring_fqcln) . '::' . $method_name;
282
283
            if ($calling_method_id) {
284
                // also store failures in case the method is added later
285
                $this->file_reference_provider->addMethodReferenceToMissingClassMember(
286
                    $calling_method_id,
287
                    $potential_id
288
                );
289
            } elseif ($source_file_path) {
290
                $this->file_reference_provider->addFileReferenceToMissingClassMember(
291
                    $source_file_path,
292
                    $potential_id
293
                );
294
            }
295
        }
296
297
        if ($calling_method_id) {
298
            // also store failures in case the method is added later
299
            $this->file_reference_provider->addMethodReferenceToMissingClassMember(
300
                $calling_method_id,
301
                strtolower((string) $method_id)
302
            );
303
        } elseif ($source_file_path) {
304
            $this->file_reference_provider->addFileReferenceToMissingClassMember(
305
                $source_file_path,
306
                strtolower((string) $method_id)
307
            );
308
        }
309
310
        return false;
311
    }
312
313
    /**
314
     * @param  array<int, PhpParser\Node\Arg> $args
315
     *
316
     * @return array<int, FunctionLikeParameter>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (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...
317
     */
318
    public function getMethodParams(
319
        MethodIdentifier $method_id,
320
        StatementsSource $source = null,
321
        array $args = null,
322
        Context $context = null
323
    ) : array {
324
        $fq_class_name = $method_id->fq_class_name;
325
        $method_name = $method_id->method_name;
326
327
        if ($this->params_provider->has($fq_class_name)) {
328
            $method_params = $this->params_provider->getMethodParams(
329
                $fq_class_name,
330
                $method_name,
331
                $args,
332
                $source,
333
                $context
334
            );
335
336
            if ($method_params !== null) {
337
                return $method_params;
338
            }
339
        }
340
341
        $declaring_method_id = $this->getDeclaringMethodId($method_id);
342
343
        $callmap_id = $declaring_method_id ?: $method_id;
344
345
        // functions
346
        if (InternalCallMapHandler::inCallMap((string) $callmap_id)) {
347
            $class_storage = $this->classlike_storage_provider->get($callmap_id->fq_class_name);
348
349
            if (!$class_storage->stubbed) {
350
                $function_callables = InternalCallMapHandler::getCallablesFromCallMap((string) $callmap_id);
351
352
                if ($function_callables === null) {
353
                    throw new \UnexpectedValueException(
354
                        'Not expecting $function_callables to be null for ' . $callmap_id
355
                    );
356
                }
357
358
                if (!$source || $args === null || count($function_callables) === 1) {
359
                    assert($function_callables[0]->params !== null);
360
361
                    return $function_callables[0]->params;
362
                }
363
364
                if ($context && $source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
365
                    $was_inside_call = $context->inside_call;
366
367
                    $context->inside_call = true;
368
369
                    foreach ($args as $arg) {
370
                        \Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(
371
                            $source,
372
                            $arg->value,
373
                            $context
374
                        );
375
                    }
376
377
                    if (!$was_inside_call) {
378
                        $context->inside_call = false;
379
                    }
380
                }
381
382
                $matching_callable = InternalCallMapHandler::getMatchingCallableFromCallMapOptions(
383
                    $source->getCodebase(),
384
                    $function_callables,
385
                    $args,
386
                    $source->getNodeTypeProvider()
387
                );
388
389
                assert($matching_callable->params !== null);
390
391
                return $matching_callable->params;
392
            }
393
        }
394
395
        if ($declaring_method_id) {
396
            $storage = $this->getStorage($declaring_method_id);
397
398
            $params = $storage->params;
399
400
            if ($storage->has_docblock_param_types) {
401
                return $params;
402
            }
403
404
            $appearing_method_id = $this->getAppearingMethodId($declaring_method_id);
405
406
            if (!$appearing_method_id) {
407
                return $params;
408
            }
409
410
            $appearing_fq_class_name = $appearing_method_id->fq_class_name;
411
            $appearing_method_name = $appearing_method_id->method_name;
412
413
            $class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
414
415
            if (!isset($class_storage->overridden_method_ids[$appearing_method_name])) {
416
                return $params;
417
            }
418
419
            if (!isset($class_storage->documenting_method_ids[$appearing_method_name])) {
420
                return $params;
421
            }
422
423
            $overridden_method_id = $class_storage->documenting_method_ids[$appearing_method_name];
424
425
            $overridden_storage = $this->getStorage($overridden_method_id);
426
427
            $overriding_fq_class_name = $overridden_method_id->fq_class_name;
428
429
            foreach ($params as $i => $param) {
430
                if (isset($overridden_storage->params[$i]->type)
431
                    && $overridden_storage->params[$i]->has_docblock_type
432
                ) {
433
                    $params[$i] = clone $param;
434
                    /** @var Type\Union $params[$i]->type */
435
                    $params[$i]->type = clone $overridden_storage->params[$i]->type;
436
437
                    if ($source) {
438
                        $overridden_class_storage = $this->classlike_storage_provider->get($overriding_fq_class_name);
439
                        $params[$i]->type = self::localizeType(
440
                            $source->getCodebase(),
441
                            $params[$i]->type,
442
                            $appearing_fq_class_name,
443
                            $overridden_class_storage->name
444
                        );
445
                    }
446
447
                    if ($params[$i]->signature_type
448
                        && $params[$i]->signature_type->isNullable()
449
                    ) {
450
                        $params[$i]->type->addType(new Type\Atomic\TNull);
451
                    }
452
453
                    $params[$i]->type_location = $overridden_storage->params[$i]->type_location;
454
                }
455
            }
456
457
            return $params;
458
        }
459
460
        throw new \UnexpectedValueException('Cannot get method params for ' . $method_id);
461
    }
462
463
    public static function localizeType(
464
        Codebase $codebase,
465
        Type\Union $type,
466
        string $appearing_fq_class_name,
467
        string $base_fq_class_name
468
    ) : Type\Union {
469
        $class_storage = $codebase->classlike_storage_provider->get($appearing_fq_class_name);
470
        $extends = $class_storage->template_type_extends;
471
472
        if (!$extends) {
473
            return $type;
474
        }
475
476
        $type = clone $type;
477
478
        foreach ($type->getAtomicTypes() as $key => $atomic_type) {
479
            if ($atomic_type instanceof Type\Atomic\TTemplateParam
480
                && ($atomic_type->defining_class === $base_fq_class_name
481
                    || isset($extends[$atomic_type->defining_class]))
482
            ) {
483
                $types_to_add = self::getExtendedTemplatedTypes(
484
                    $atomic_type,
485
                    $extends
486
                );
487
488
                if ($types_to_add) {
489
                    $type->removeType($key);
490
491
                    foreach ($types_to_add as $extra_added_type) {
492
                        $type->addType($extra_added_type);
493
                    }
494
                }
495
            }
496
497
            if ($atomic_type instanceof Type\Atomic\TTemplateParamClass) {
498
                if ($atomic_type->defining_class === $base_fq_class_name) {
499
                    if (isset($extends[$base_fq_class_name][$atomic_type->param_name])) {
500
                        $extended_param = $extends[$base_fq_class_name][$atomic_type->param_name];
501
502
                        $types = \array_values($extended_param->getAtomicTypes());
503
504
                        if (count($types) === 1 && $types[0] instanceof Type\Atomic\TNamedObject) {
505
                            $atomic_type->as_type = $types[0];
506
                        } else {
507
                            $atomic_type->as_type = null;
508
                        }
509
                    }
510
                }
511
            }
512
513
            if ($atomic_type instanceof Type\Atomic\TArray
514
                || $atomic_type instanceof Type\Atomic\TIterable
515
                || $atomic_type instanceof Type\Atomic\TGenericObject
516
            ) {
517
                foreach ($atomic_type->type_params as &$type_param) {
518
                    $type_param = self::localizeType(
519
                        $codebase,
520
                        $type_param,
521
                        $appearing_fq_class_name,
522
                        $base_fq_class_name
523
                    );
524
                }
525
            }
526
527
            if ($atomic_type instanceof Type\Atomic\TList) {
528
                $atomic_type->type_param = self::localizeType(
529
                    $codebase,
530
                    $atomic_type->type_param,
531
                    $appearing_fq_class_name,
532
                    $base_fq_class_name
533
                );
534
            }
535
536
            if ($atomic_type instanceof Type\Atomic\ObjectLike) {
537
                foreach ($atomic_type->properties as &$property_type) {
538
                    $property_type = self::localizeType(
539
                        $codebase,
540
                        $property_type,
541
                        $appearing_fq_class_name,
542
                        $base_fq_class_name
543
                    );
544
                }
545
            }
546
547
            if ($atomic_type instanceof Type\Atomic\TCallable
548
                || $atomic_type instanceof Type\Atomic\TFn
549
            ) {
550
                if ($atomic_type->params) {
551
                    foreach ($atomic_type->params as $param) {
552
                        if ($param->type) {
553
                            $param->type = self::localizeType(
554
                                $codebase,
555
                                $param->type,
556
                                $appearing_fq_class_name,
557
                                $base_fq_class_name
558
                            );
559
                        }
560
                    }
561
                }
562
563
                if ($atomic_type->return_type) {
564
                    $atomic_type->return_type = self::localizeType(
565
                        $codebase,
566
                        $atomic_type->return_type,
567
                        $appearing_fq_class_name,
568
                        $base_fq_class_name
569
                    );
570
                }
571
            }
572
        }
573
574
        $type->bustCache();
575
576
        return $type;
577
    }
578
579
    /**
580
     * @param array<string, array<int|string, Type\Union>> $extends
581
     * @return list<Type\Atomic>
0 ignored issues
show
Documentation introduced by
The doc-type list<Type\Atomic> could not be parsed: Expected "|" or "end of type", but got "<" at position 4. (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...
582
     */
583
    public static function getExtendedTemplatedTypes(
584
        Type\Atomic\TTemplateParam $atomic_type,
585
        array $extends
586
    ) : array {
587
        $extra_added_types = [];
588
589
        if (isset($extends[$atomic_type->defining_class][$atomic_type->param_name])) {
590
            $extended_param = clone $extends[$atomic_type->defining_class][$atomic_type->param_name];
591
592
            foreach ($extended_param->getAtomicTypes() as $extended_atomic_type) {
593
                if ($extended_atomic_type instanceof Type\Atomic\TTemplateParam) {
594
                    $extra_added_types = \array_merge(
595
                        $extra_added_types,
596
                        self::getExtendedTemplatedTypes(
597
                            $extended_atomic_type,
598
                            $extends
599
                        )
600
                    );
601
                } else {
602
                    $extra_added_types[] = $extended_atomic_type;
603
                }
604
            }
605
        } else {
606
            $extra_added_types[] = $atomic_type;
607
        }
608
609
        return $extra_added_types;
610
    }
611
612
    /**
613
     * @return bool
614
     */
615
    public function isVariadic(MethodIdentifier $method_id)
616
    {
617
        $declaring_method_id = $this->getDeclaringMethodId($method_id);
618
619
        if (!$declaring_method_id) {
620
            return false;
621
        }
622
623
        return $this->getStorage($declaring_method_id)->variadic;
624
    }
625
626
    /**
627
     * @param  string $self_class
628
     * @param  array<int, PhpParser\Node\Arg>|null $args
629
     *
630
     * @return Type\Union|null
631
     */
632
    public function getMethodReturnType(
633
        MethodIdentifier $method_id,
634
        &$self_class,
635
        \Psalm\Internal\Analyzer\SourceAnalyzer $source_analyzer = null,
636
        array $args = null
637
    ) {
638
        $original_fq_class_name = $method_id->fq_class_name;
639
        $original_method_name = $method_id->method_name;
640
641
        $adjusted_fq_class_name = $this->classlikes->getUnAliasedName($original_fq_class_name);
642
643
        if ($adjusted_fq_class_name !== $original_fq_class_name) {
644
            $original_fq_class_name = strtolower($adjusted_fq_class_name);
645
        }
646
647
        $original_class_storage = $this->classlike_storage_provider->get($original_fq_class_name);
648
649
        if (isset($original_class_storage->pseudo_methods[$original_method_name])) {
650
            return $original_class_storage->pseudo_methods[$original_method_name]->return_type;
651
        }
652
653
        $declaring_method_id = $this->getDeclaringMethodId($method_id);
654
655
        if (!$declaring_method_id) {
656
            return null;
657
        }
658
659
        $appearing_method_id = $this->getAppearingMethodId($method_id);
660
661
        if (!$appearing_method_id) {
662
            $class_storage = $this->classlike_storage_provider->get($original_fq_class_name);
663
664
            if ($class_storage->abstract && isset($class_storage->overridden_method_ids[$original_method_name])) {
665
                $appearing_method_id = reset($class_storage->overridden_method_ids[$original_method_name]);
666
            } else {
667
                return null;
668
            }
669
        }
670
671
        $appearing_fq_class_name = $appearing_method_id->fq_class_name;
672
        $appearing_method_name = $appearing_method_id->method_name;
673
674
        $appearing_fq_class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
675
676
        if (!$appearing_fq_class_storage->user_defined
677
            && !$appearing_fq_class_storage->stubbed
678
            && InternalCallMapHandler::inCallMap((string) $appearing_method_id)
679
        ) {
680
            if ((string) $appearing_method_id === 'Closure::fromcallable'
681
                && isset($args[0])
682
                && $source_analyzer
683
                && ($first_arg_type = $source_analyzer->getNodeTypeProvider()->getType($args[0]->value))
684
                && $first_arg_type->isSingle()
685
            ) {
686
                foreach ($first_arg_type->getAtomicTypes() as $atomic_type) {
687
                    if ($atomic_type instanceof Type\Atomic\TCallable
688
                        || $atomic_type instanceof Type\Atomic\TFn
689
                    ) {
690
                        $callable_type = clone $atomic_type;
691
692
                        return new Type\Union([new Type\Atomic\TFn(
693
                            'Closure',
694
                            $callable_type->params,
695
                            $callable_type->return_type
696
                        )]);
697
                    }
698
699
                    if ($atomic_type instanceof Type\Atomic\TNamedObject
700
                        && $this->methodExists(
701
                            new MethodIdentifier($atomic_type->value, '__invoke')
702
                        )
703
                    ) {
704
                        $invokable_storage = $this->getStorage(
705
                            new MethodIdentifier($atomic_type->value, '__invoke')
706
                        );
707
708
                        return new Type\Union([new Type\Atomic\TFn(
709
                            'Closure',
710
                            $invokable_storage->params,
711
                            $invokable_storage->return_type
712
                        )]);
713
                    }
714
                }
715
            }
716
717
            $callmap_callables = InternalCallMapHandler::getCallablesFromCallMap((string) $appearing_method_id);
718
719
            if (!$callmap_callables || $callmap_callables[0]->return_type === null) {
720
                throw new \UnexpectedValueException('Shouldn’t get here');
721
            }
722
723
            $return_type_candidate = $callmap_callables[0]->return_type;
724
725
            if ($return_type_candidate->isFalsable()) {
726
                $return_type_candidate->ignore_falsable_issues = true;
727
            }
728
729
            return $return_type_candidate;
730
        }
731
732
        $storage = $this->getStorage($declaring_method_id);
733
734
        if ($storage->return_type) {
735
            $self_class = $appearing_fq_class_storage->name;
736
737
            return clone $storage->return_type;
738
        }
739
740
        $class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
741
742
        if (!isset($class_storage->overridden_method_ids[$appearing_method_name])) {
743
            return null;
744
        }
745
746
        $candidate_type = null;
747
748
        foreach ($class_storage->overridden_method_ids[$appearing_method_name] as $overridden_method_id) {
749
            $overridden_storage = $this->getStorage($overridden_method_id);
750
751
            if ($overridden_storage->return_type) {
752
                if ($overridden_storage->return_type->isNull()) {
753
                    if ($candidate_type && !$candidate_type->isVoid()) {
754
                        return null;
755
                    }
756
757
                    $candidate_type = Type::getVoid();
758
                    continue;
759
                }
760
761
                $fq_overridden_class = $overridden_method_id->fq_class_name;
762
763
                $overridden_class_storage =
764
                    $this->classlike_storage_provider->get($fq_overridden_class);
765
766
                $overridden_return_type = clone $overridden_storage->return_type;
767
768
                $self_class = $overridden_class_storage->name;
769
770
                if ($candidate_type
771
                    && $source_analyzer
772
                ) {
773
                    $old_contained_by_new = TypeAnalyzer::isContainedBy(
774
                        $source_analyzer->getCodebase(),
775
                        $candidate_type,
776
                        $overridden_return_type
777
                    );
778
779
                    $new_contained_by_old = TypeAnalyzer::isContainedBy(
780
                        $source_analyzer->getCodebase(),
781
                        $overridden_return_type,
782
                        $candidate_type
783
                    );
784
785
                    if (!$old_contained_by_new && !$new_contained_by_old) {
786
                        $attempted_intersection = Type::intersectUnionTypes(
787
                            $candidate_type,
788
                            $overridden_return_type,
789
                            $source_analyzer->getCodebase()
790
                        );
791
792
                        if ($attempted_intersection) {
793
                            $candidate_type = $attempted_intersection;
794
                            continue;
795
                        }
796
797
                        return null;
798
                    }
799
800
                    if ($old_contained_by_new) {
801
                        continue;
802
                    }
803
                }
804
805
                $candidate_type = $overridden_return_type;
806
            }
807
        }
808
809
        return $candidate_type;
810
    }
811
812
    /**
813
     * @return bool
814
     */
815
    public function getMethodReturnsByRef(MethodIdentifier $method_id)
816
    {
817
        $method_id = $this->getDeclaringMethodId($method_id);
818
819
        if (!$method_id) {
820
            return false;
821
        }
822
823
        $fq_class_storage = $this->classlike_storage_provider->get($method_id->fq_class_name);
824
825
        if (!$fq_class_storage->user_defined && InternalCallMapHandler::inCallMap((string) $method_id)) {
826
            return false;
827
        }
828
829
        $storage = $this->getStorage($method_id);
830
831
        return $storage->returns_by_ref;
832
    }
833
834
    /**
835
     * @param  CodeLocation|null    $defined_location
836
     *
837
     * @return CodeLocation|null
838
     */
839
    public function getMethodReturnTypeLocation(
840
        MethodIdentifier $method_id,
841
        CodeLocation &$defined_location = null
842
    ) {
843
        $method_id = $this->getDeclaringMethodId($method_id);
844
845
        if ($method_id === null) {
846
            return null;
847
        }
848
849
        $storage = $this->getStorage($method_id);
850
851
        if (!$storage->return_type_location) {
852
            $overridden_method_ids = $this->getOverriddenMethodIds($method_id);
853
854
            foreach ($overridden_method_ids as $overridden_method_id) {
855
                $overridden_storage = $this->getStorage($overridden_method_id);
856
857
                if ($overridden_storage->return_type_location) {
858
                    $defined_location = $overridden_storage->return_type_location;
859
                    break;
860
                }
861
            }
862
        }
863
864
        return $storage->return_type_location;
865
    }
866
867
    /**
868
     * @param string $fq_class_name
869
     * @param lowercase-string $method_name_lc
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-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...
870
     * @param string $declaring_fq_class_name
871
     * @param lowercase-string $declaring_method_name_lc
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-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...
872
     *
873
     * @return void
874
     */
875
    public function setDeclaringMethodId(
876
        $fq_class_name,
877
        $method_name_lc,
878
        $declaring_fq_class_name,
879
        $declaring_method_name_lc
880
    ) {
881
        $class_storage = $this->classlike_storage_provider->get($fq_class_name);
882
883
        $class_storage->declaring_method_ids[$method_name_lc] = new MethodIdentifier(
884
            $declaring_fq_class_name,
885
            $declaring_method_name_lc
886
        );
887
    }
888
889
    /**
890
     * @param string $fq_class_name
891
     * @param lowercase-string $method_name_lc
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-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...
892
     * @param string $appearing_fq_class_name
893
     * @param lowercase-string $appearing_method_name_lc
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-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...
894
     *
895
     * @return void
896
     */
897
    public function setAppearingMethodId(
898
        $fq_class_name,
899
        $method_name_lc,
900
        $appearing_fq_class_name,
901
        $appearing_method_name_lc
902
    ) {
903
        $class_storage = $this->classlike_storage_provider->get($fq_class_name);
904
905
        $class_storage->appearing_method_ids[$method_name_lc] = new MethodIdentifier(
906
            $appearing_fq_class_name,
907
            $appearing_method_name_lc
908
        );
909
    }
910
911
    public function getDeclaringMethodId(
912
        MethodIdentifier $method_id
913
    ) : ?MethodIdentifier {
914
        $fq_class_name = $this->classlikes->getUnAliasedName($method_id->fq_class_name);
915
916
        $class_storage = $this->classlike_storage_provider->get($fq_class_name);
917
918
        $method_name = $method_id->method_name;
919
920
        if (isset($class_storage->declaring_method_ids[$method_name])) {
921
            return $class_storage->declaring_method_ids[$method_name];
922
        }
923
924
        if ($class_storage->abstract && isset($class_storage->overridden_method_ids[$method_name])) {
925
            return reset($class_storage->overridden_method_ids[$method_name]);
926
        }
927
928
        return null;
929
    }
930
931
    /**
932
     * Get the class this method appears in (vs is declared in, which could give a trait
933
     */
934
    public function getAppearingMethodId(
935
        MethodIdentifier $method_id
936
    ) : ?MethodIdentifier {
937
        $fq_class_name = $this->classlikes->getUnAliasedName($method_id->fq_class_name);
938
939
        $class_storage = $this->classlike_storage_provider->get($fq_class_name);
940
941
        $method_name = $method_id->method_name;
942
943
        if (isset($class_storage->appearing_method_ids[$method_name])) {
944
            return $class_storage->appearing_method_ids[$method_name];
945
        }
946
947
        return null;
948
    }
949
950
    /**
951
     * @return array<MethodIdentifier>
952
     */
953
    public function getOverriddenMethodIds(MethodIdentifier $method_id)
954
    {
955
        $class_storage = $this->classlike_storage_provider->get($method_id->fq_class_name);
956
        $method_name = $method_id->method_name;
957
958
        if (isset($class_storage->overridden_method_ids[$method_name])) {
959
            return $class_storage->overridden_method_ids[$method_name];
960
        }
961
962
        return [];
963
    }
964
965
    /**
966
     * @return string
967
     */
968
    public function getCasedMethodId(MethodIdentifier $original_method_id)
969
    {
970
        $method_id = $this->getDeclaringMethodId($original_method_id);
971
972
        if ($method_id === null) {
973
            return $original_method_id;
974
        }
975
976
        $fq_class_name = $method_id->fq_class_name;
977
        $new_method_name = $method_id->method_name;
978
979
        $old_fq_class_name = $original_method_id->fq_class_name;
980
        $old_method_name = $original_method_id->method_name;
981
982
        $storage = $this->getStorage($method_id);
983
984
        if ($old_method_name === $new_method_name
985
            && strtolower($old_fq_class_name) !== $old_fq_class_name
986
        ) {
987
            return $old_fq_class_name . '::' . $storage->cased_name;
988
        }
989
990
        return $fq_class_name . '::' . $storage->cased_name;
991
    }
992
993
    /**
994
     * @return ?MethodStorage
0 ignored issues
show
Documentation introduced by
The doc-type ?MethodStorage could not be parsed: Unknown type name "?MethodStorage" 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...
995
     */
996
    public function getUserMethodStorage(MethodIdentifier $method_id)
997
    {
998
        $declaring_method_id = $this->getDeclaringMethodId($method_id);
999
1000
        if (!$declaring_method_id) {
1001
            throw new \UnexpectedValueException('$storage should not be null for ' . $method_id);
1002
        }
1003
1004
        $storage = $this->getStorage($declaring_method_id);
1005
1006
        if (!$storage->location) {
1007
            return null;
1008
        }
1009
1010
        return $storage;
1011
    }
1012
1013
    /**
1014
     * @return ClassLikeStorage
1015
     */
1016
    public function getClassLikeStorageForMethod(MethodIdentifier $method_id)
1017
    {
1018
        $fq_class_name = $method_id->fq_class_name;
1019
        $method_name = $method_id->method_name;
1020
1021
        if ($this->existence_provider->has($fq_class_name)) {
1022
            if ($this->existence_provider->doesMethodExist(
1023
                $fq_class_name,
1024
                $method_name,
1025
                null,
1026
                null
1027
            )) {
1028
                return $this->classlike_storage_provider->get($fq_class_name);
1029
            }
1030
        }
1031
1032
        $declaring_method_id = $this->getDeclaringMethodId($method_id);
1033
1034
        if ($declaring_method_id === null) {
1035
            if (InternalCallMapHandler::inCallMap((string) $method_id)) {
1036
                $declaring_method_id = $method_id;
1037
            } else {
1038
                throw new \UnexpectedValueException('$storage should not be null for ' . $method_id);
1039
            }
1040
        }
1041
1042
        $declaring_fq_class_name = $declaring_method_id->fq_class_name;
1043
1044
        return $this->classlike_storage_provider->get($declaring_fq_class_name);
1045
    }
1046
1047
    /**
1048
     * @return MethodStorage
1049
     */
1050
    public function getStorage(MethodIdentifier $method_id)
1051
    {
1052
        try {
1053
            $class_storage = $this->classlike_storage_provider->get($method_id->fq_class_name);
1054
        } catch (\InvalidArgumentException $e) {
1055
            throw new \UnexpectedValueException($e->getMessage());
1056
        }
1057
1058
        $method_name = $method_id->method_name;
1059
1060
        if (!isset($class_storage->methods[$method_name])) {
1061
            throw new \UnexpectedValueException(
1062
                '$storage should not be null for ' . $method_id
1063
            );
1064
        }
1065
1066
        return $class_storage->methods[$method_name];
1067
    }
1068
1069
    /**
1070
     * @return bool
1071
     */
1072
    public function hasStorage(MethodIdentifier $method_id)
1073
    {
1074
        try {
1075
            $class_storage = $this->classlike_storage_provider->get($method_id->fq_class_name);
1076
        } catch (\InvalidArgumentException $e) {
1077
            return false;
1078
        }
1079
1080
        $method_name = $method_id->method_name;
1081
1082
        if (!isset($class_storage->methods[$method_name])) {
1083
            return false;
1084
        }
1085
1086
        return true;
1087
    }
1088
}
1089