FunctionLikeAnalyzer::alterParams()   F
last analyzed

Complexity

Conditions 27
Paths 1014

Size

Total Lines 121

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
nc 1014
nop 4
dl 0
loc 121
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
namespace Psalm\Internal\Analyzer;
3
4
use PhpParser;
5
use PhpParser\Node\Expr\ArrowFunction;
6
use PhpParser\Node\Expr\Closure;
7
use PhpParser\Node\Stmt\ClassMethod;
8
use PhpParser\Node\Stmt\Function_;
9
use Psalm\Codebase;
10
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeAnalyzer;
11
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeCollector;
12
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
13
use Psalm\CodeLocation;
14
use Psalm\Context;
15
use Psalm\Internal\FileManipulation\FunctionDocblockManipulator;
16
use Psalm\Issue\InvalidDocblockParamName;
17
use Psalm\Issue\InvalidParamDefault;
18
use Psalm\Issue\MismatchingDocblockParamType;
19
use Psalm\Issue\MissingClosureParamType;
20
use Psalm\Issue\MissingParamType;
21
use Psalm\Issue\MissingThrowsDocblock;
22
use Psalm\Issue\ReferenceConstraintViolation;
23
use Psalm\Issue\ReservedWord;
24
use Psalm\Issue\UnusedClosureParam;
25
use Psalm\Issue\UnusedParam;
26
use Psalm\IssueBuffer;
27
use Psalm\StatementsSource;
28
use Psalm\Storage\FunctionLikeParameter;
29
use Psalm\Storage\FunctionLikeStorage;
30
use Psalm\Storage\MethodStorage;
31
use Psalm\Type;
32
use Psalm\Type\Atomic\TNamedObject;
33
use function md5;
34
use function strtolower;
35
use function array_merge;
36
use function array_filter;
37
use function array_key_exists;
38
use function substr;
39
use function strpos;
40
use function array_search;
41
use function array_keys;
42
use function end;
43
use Psalm\Internal\Taint\TaintNode;
44
use Psalm\Storage\FunctionStorage;
45
46
/**
47
 * @internal
48
 */
49
abstract class FunctionLikeAnalyzer extends SourceAnalyzer
50
{
51
    /**
52
     * @var Closure|Function_|ClassMethod|ArrowFunction
53
     */
54
    protected $function;
55
56
    /**
57
     * @var Codebase
58
     */
59
    protected $codebase;
60
61
    /**
62
     * @var array<string>
63
     */
64
    protected $suppressed_issues;
65
66
    /**
67
     * @var bool
68
     */
69
    protected $is_static = false;
70
71
    /**
72
     * @var StatementsSource
73
     */
74
    protected $source;
75
76
    /**
77
     * @var ?array<string, Type\Union>
78
     */
79
    protected $return_vars_in_scope = [];
80
81
    /**
82
     * @var ?array<string, bool>
83
     */
84
    protected $return_vars_possibly_in_scope = [];
85
86
    /**
87
     * @var Type\Union|null
88
     */
89
    private $local_return_type;
90
91
    /**
92
     * @var array<string, bool>
93
     */
94
    protected static $no_effects_hashes = [];
95
96
    /**
97
     * @var FunctionLikeStorage
98
     */
99
    protected $storage;
100
101
    /**
102
     * @param Closure|Function_|ClassMethod|ArrowFunction $function
103
     * @param SourceAnalyzer $source
104
     */
105
    protected function __construct($function, SourceAnalyzer $source, FunctionLikeStorage $storage)
106
    {
107
        $this->function = $function;
108
        $this->source = $source;
109
        $this->suppressed_issues = $source->getSuppressedIssues();
110
        $this->codebase = $source->getCodebase();
111
        $this->storage = $storage;
112
    }
113
114
    /**
115
     * @param Context       $context
116
     * @param Context|null  $global_context
117
     * @param bool          $add_mutations  whether or not to add mutations to this method
118
     * @param ?array<string, bool> $byref_uses
119
     *
120
     * @return false|null
121
     */
122
    public function analyze(
123
        Context $context,
124
        \Psalm\Internal\Provider\NodeDataProvider $type_provider,
125
        Context $global_context = null,
126
        $add_mutations = false,
127
        array $byref_uses = null
128
    ) {
129
        $storage = $this->storage;
130
131
        $function_stmts = $this->function->getStmts() ?: [];
132
133
        $hash = null;
134
        $real_method_id = null;
135
        $method_id = null;
136
137
        $cased_method_id = null;
138
139
        $appearing_class_storage = null;
140
141
        if ($global_context) {
142
            foreach ($global_context->constants as $const_name => $var_type) {
143
                if (!$context->hasVariable($const_name)) {
144
                    $context->vars_in_scope[$const_name] = clone $var_type;
145
                }
146
            }
147
        }
148
149
        $codebase = $this->codebase;
150
        $project_analyzer = $this->getProjectAnalyzer();
151
152
        $implemented_docblock_param_types = [];
153
154
        $classlike_storage_provider = $codebase->classlike_storage_provider;
155
156
        if ($codebase->track_unused_suppressions && !isset($storage->suppressed_issues[0])) {
157
            foreach ($storage->suppressed_issues as $offset => $issue_name) {
158
                IssueBuffer::addUnusedSuppression($this->getFilePath(), $offset, $issue_name);
159
            }
160
        }
161
162
        foreach ($storage->docblock_issues as $docblock_issue) {
163
            IssueBuffer::add($docblock_issue);
164
        }
165
166
        $overridden_method_ids = [];
167
168
        if ($this->function instanceof ClassMethod) {
169
            if (!$storage instanceof MethodStorage || !$this instanceof MethodAnalyzer) {
170
                throw new \UnexpectedValueException('$storage must be MethodStorage');
171
            }
172
173
            $real_method_id = $this->getMethodId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getMethodId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\MethodAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
174
175
            $method_id = $this->getMethodId($context->self);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getMethodId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\MethodAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
176
177
            $fq_class_name = (string)$context->self;
178
            $appearing_class_storage = $classlike_storage_provider->get($fq_class_name);
179
180
            if ($add_mutations) {
181
                if (!$context->collect_initializations) {
182
                    $hash = md5($real_method_id . '::' . $context->getScopeSummary());
183
184
                    // if we know that the function has no effects on vars, we don't bother rechecking
185
                    if (isset(self::$no_effects_hashes[$hash])) {
186
                        return null;
187
                    }
188
                }
189
            } elseif ($context->self) {
190
                if ($appearing_class_storage->template_types) {
191
                    $template_params = [];
192
193
                    foreach ($appearing_class_storage->template_types as $param_name => $template_map) {
194
                        $key = array_keys($template_map)[0];
195
196
                        $template_params[] = new Type\Union([
197
                            new Type\Atomic\TTemplateParam(
198
                                $param_name,
199
                                \reset($template_map)[0],
200
                                $key
201
                            )
202
                        ]);
203
                    }
204
205
                    $this_object_type = new Type\Atomic\TGenericObject(
206
                        $context->self,
207
                        $template_params
208
                    );
209
                    $this_object_type->was_static = true;
210
                } else {
211
                    $this_object_type = new TNamedObject($context->self);
212
                    $this_object_type->was_static = true;
213
                }
214
215
                $context->vars_in_scope['$this'] = new Type\Union([$this_object_type]);
216
217
                if ($storage->external_mutation_free
218
                    && !$storage->mutation_free_inferred
219
                ) {
220
                    $context->vars_in_scope['$this']->reference_free = true;
221
222
                    if ($this->function->name->name !== '__construct') {
223
                        $context->vars_in_scope['$this']->allow_mutations = false;
224
                    }
225
                }
226
227
                $context->vars_possibly_in_scope['$this'] = true;
228
            }
229
230
            if ($appearing_class_storage->has_visitor_issues) {
231
                return null;
232
            }
233
234
            $cased_method_id = $fq_class_name . '::' . $storage->cased_name;
235
236
            $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($method_id);
237
238
            if ($this->function->name->name === '__construct') {
239
                $context->inside_constructor = true;
240
            }
241
242
            $codeLocation = new CodeLocation(
243
                $this,
244
                $this->function,
245
                null,
246
                true
247
            );
248
249
            if ($overridden_method_ids
250
                && $this->function->name->name !== '__construct'
251
                && !$context->collect_initializations
252
                && !$context->collect_mutations
253
            ) {
254
                foreach ($overridden_method_ids as $overridden_method_id) {
255
                    $parent_method_storage = $codebase->methods->getStorage($overridden_method_id);
256
257
                    $overridden_fq_class_name = $overridden_method_id->fq_class_name;
258
259
                    $parent_storage = $classlike_storage_provider->get($overridden_fq_class_name);
260
261
                    $implementer_visibility = $storage->visibility;
262
263
                    $implementer_appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
264
                    $implementer_declaring_method_id = $real_method_id;
265
266
                    $declaring_class_storage = $appearing_class_storage;
267
268
                    if ($implementer_appearing_method_id
269
                        && $implementer_appearing_method_id !== $implementer_declaring_method_id
270
                    ) {
271
                        $appearing_fq_class_name = $implementer_appearing_method_id->fq_class_name;
272
                        $appearing_method_name = $implementer_appearing_method_id->method_name;
273
274
                        $declaring_fq_class_name = $implementer_declaring_method_id->fq_class_name;
275
276
                        $appearing_class_storage = $classlike_storage_provider->get(
277
                            $appearing_fq_class_name
278
                        );
279
280
                        $declaring_class_storage = $classlike_storage_provider->get(
281
                            $declaring_fq_class_name
282
                        );
283
284
                        if (isset($appearing_class_storage->trait_visibility_map[$appearing_method_name])) {
285
                            $implementer_visibility
286
                                = $appearing_class_storage->trait_visibility_map[$appearing_method_name];
287
                        }
288
                    }
289
290
                    // we've already checked this in the class checker
291
                    if (!isset($appearing_class_storage->class_implements[strtolower($overridden_fq_class_name)])) {
292
                        MethodComparator::compare(
293
                            $codebase,
294
                            $declaring_class_storage,
295
                            $parent_storage,
296
                            $storage,
297
                            $parent_method_storage,
298
                            $fq_class_name,
299
                            $implementer_visibility,
300
                            $codeLocation,
301
                            $storage->suppressed_issues
302
                        );
303
                    }
304
305
                    foreach ($parent_method_storage->params as $i => $guide_param) {
306
                        if ($guide_param->type
307
                            && (!$guide_param->signature_type
308
                                || ($guide_param->signature_type !== $guide_param->type
309
                                    && $storage->inheritdoc)
310
                                || !$parent_storage->user_defined
311
                            )
312
                        ) {
313
                            if (!isset($implemented_docblock_param_types[$i])) {
314
                                $implemented_docblock_param_types[$i] = $guide_param->type;
315
                            }
316
                        }
317
                    }
318
                }
319
            }
320
321
            MethodAnalyzer::checkMethodSignatureMustOmitReturnType($storage, $codeLocation);
322
323
            if (!$context->calling_method_id || !$context->collect_initializations) {
324
                $context->calling_method_id = strtolower((string) $method_id);
325
            }
326
        } elseif ($this->function instanceof Function_) {
327
            $function_name = $this->function->name->name;
328
            $namespace_prefix = $this->getNamespace();
329
            $cased_method_id = ($namespace_prefix !== null ? $namespace_prefix . '\\' : '') . $function_name;
330
            $context->calling_function_id = strtolower($cased_method_id);
331
        } else { // Closure
332
            if ($storage->return_type) {
333
                $closure_return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
334
                    $codebase,
335
                    $storage->return_type,
336
                    $context->self,
337
                    $context->self,
338
                    $this->getParentFQCLN()
339
                );
340
            } else {
341
                $closure_return_type = Type::getMixed();
342
            }
343
344
            $closure_type = new Type\Atomic\TFn(
345
                'Closure',
346
                $storage->params,
347
                $closure_return_type
348
            );
349
350
            if ($storage instanceof FunctionStorage) {
351
                $closure_type->byref_uses = $storage->byref_uses;
352
            }
353
354
            $type_provider->setType(
355
                $this->function,
356
                new Type\Union([
357
                    $closure_type,
358
                ])
359
            );
360
        }
361
362
        $this->suppressed_issues = $this->getSource()->getSuppressedIssues() + $storage->suppressed_issues;
363
364
        if ($storage instanceof MethodStorage && $storage->is_static) {
365
            $this->is_static = true;
366
        }
367
368
        $statements_analyzer = new StatementsAnalyzer($this, $type_provider);
369
370
        if ($byref_uses) {
371
            $statements_analyzer->setByRefUses($byref_uses);
372
        }
373
374
        if ($storage->template_types) {
375
            foreach ($storage->template_types as $param_name => $_) {
376
                $fq_classlike_name = Type::getFQCLNFromString(
377
                    $param_name,
378
                    $this->getAliases()
379
                );
380
381
                if ($codebase->classOrInterfaceExists($fq_classlike_name)) {
382
                    if (IssueBuffer::accepts(
383
                        new ReservedWord(
384
                            'Cannot use ' . $param_name . ' as template name since the class already exists',
385
                            new CodeLocation($this, $this->function),
386
                            'resource'
387
                        ),
388
                        $this->getSuppressedIssues()
389
                    )) {
390
                        // fall through
391
                    }
392
                }
393
            }
394
        }
395
396
        $template_types = $storage->template_types;
397
398
        if ($appearing_class_storage && $appearing_class_storage->template_types) {
399
            $template_types = array_merge($template_types ?: [], $appearing_class_storage->template_types);
400
        }
401
402
        $params = $storage->params;
403
404
        if ($storage instanceof MethodStorage) {
405
            $non_null_param_types = array_filter(
406
                $storage->params,
407
                /** @return bool */
408
                function (FunctionLikeParameter $p) {
409
                    return $p->type !== null && $p->has_docblock_type;
410
                }
411
            );
412
        } else {
413
            $non_null_param_types = array_filter(
414
                $storage->params,
415
                /** @return bool */
416
                function (FunctionLikeParameter $p) {
417
                    return $p->type !== null;
418
                }
419
            );
420
        }
421
422
        if ($storage instanceof MethodStorage
423
            && $method_id instanceof \Psalm\Internal\MethodIdentifier
424
            && $overridden_method_ids
425
        ) {
426
            $types_without_docblocks = array_filter(
427
                $storage->params,
428
                /** @return bool */
429
                function (FunctionLikeParameter $p) {
430
                    return !$p->type || !$p->has_docblock_type;
431
                }
432
            );
433
434
            if ($types_without_docblocks) {
435
                $params = $codebase->methods->getMethodParams(
436
                    $method_id,
437
                    $this
438
                );
439
            }
440
        }
441
442
        if ($codebase->alter_code) {
443
            $this->alterParams($codebase, $storage, $params, $context);
444
        }
445
446
        foreach ($codebase->methods_to_rename as $original_method_id => $new_method_name) {
447
            if ($this->function instanceof ClassMethod
448
                && $this instanceof MethodAnalyzer
449
                && strtolower((string) $this->getMethodId()) === $original_method_id
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getMethodId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\MethodAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
450
            ) {
451
                $file_manipulations = [
452
                    new \Psalm\FileManipulation(
453
                        (int) $this->function->name->getAttribute('startFilePos'),
454
                        (int) $this->function->name->getAttribute('endFilePos') + 1,
455
                        $new_method_name
456
                    )
457
                ];
458
459
                \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
460
                    $this->getFilePath(),
461
                    $file_manipulations
462
                );
463
            }
464
        }
465
466
        $check_stmts = $this->processParams(
467
            $statements_analyzer,
468
            $storage,
469
            $cased_method_id,
470
            $params,
471
            $context,
472
            $implemented_docblock_param_types,
473
            (bool) $non_null_param_types,
474
            (bool) $template_types
475
        );
476
477
        if ($storage->pure) {
478
            $context->pure = true;
479
        }
480
481
        if ($storage->mutation_free
482
            && $cased_method_id
483
            && !strpos($cased_method_id, '__construct')
484
            && !($storage instanceof MethodStorage && $storage->mutation_free_inferred)
485
        ) {
486
            $context->mutation_free = true;
487
        }
488
489
        if ($storage instanceof MethodStorage
490
            && $storage->external_mutation_free
491
            && !$storage->mutation_free_inferred
492
        ) {
493
            $context->external_mutation_free = true;
494
        }
495
496
        if ($storage->unused_docblock_params) {
497
            foreach ($storage->unused_docblock_params as $param_name => $param_location) {
498
                if (IssueBuffer::accepts(
499
                    new InvalidDocblockParamName(
500
                        'Incorrect param name $' . $param_name . ' in docblock for ' . $cased_method_id,
501
                        $param_location
502
                    )
503
                )) {
504
                }
505
            }
506
        }
507
508
        if ($storage->signature_return_type && $storage->signature_return_type_location) {
509
            list($start, $end) = $storage->signature_return_type_location->getSelectionBounds();
510
511
            $codebase->analyzer->addOffsetReference(
512
                $this->getFilePath(),
513
                $start,
514
                $end,
515
                (string) $storage->signature_return_type
516
            );
517
        }
518
519
        if (ReturnTypeAnalyzer::checkReturnType(
520
            $this->function,
521
            $project_analyzer,
522
            $this,
523
            $storage,
524
            $context
525
        ) === false) {
526
            $check_stmts = false;
527
        }
528
529
        if (!$check_stmts) {
530
            return false;
531
        }
532
533
        if ($context->collect_initializations || $context->collect_mutations) {
534
            $statements_analyzer->addSuppressedIssues([
535
                'DocblockTypeContradiction',
536
                'InvalidReturnStatement',
537
                'RedundantCondition',
538
                'RedundantConditionGivenDocblockType',
539
                'TypeDoesNotContainNull',
540
                'TypeDoesNotContainType',
541
                'LoopInvalidation',
542
            ]);
543
544
            if ($context->collect_initializations) {
545
                $statements_analyzer->addSuppressedIssues([
546
                    'UndefinedInterfaceMethod',
547
                    'UndefinedMethod',
548
                    'PossiblyUndefinedMethod',
549
                ]);
550
            }
551
        } elseif ($cased_method_id && strpos($cased_method_id, '__destruct')) {
552
            $statements_analyzer->addSuppressedIssues([
553
                'InvalidPropertyAssignmentValue',
554
                'PossiblyNullPropertyAssignmentValue',
555
            ]);
556
        }
557
558
        $statements_analyzer->analyze($function_stmts, $context, $global_context, true);
559
560
        $this->examineParamTypes($statements_analyzer, $context, $codebase);
561
562
        foreach ($storage->params as $offset => $function_param) {
563
            // only complain if there's no type defined by a parent type
564
            if (!$function_param->type
565
                && $function_param->location
566
                && !isset($implemented_docblock_param_types[$offset])
567
            ) {
568
                if ($this->function instanceof Closure
569
                    || $this->function instanceof ArrowFunction
570
                ) {
571
                    IssueBuffer::accepts(
572
                        new MissingClosureParamType(
573
                            'Parameter $' . $function_param->name . ' has no provided type',
574
                            $function_param->location
575
                        ),
576
                        $storage->suppressed_issues + $this->getSuppressedIssues()
577
                    );
578
                } else {
579
                    IssueBuffer::accepts(
580
                        new MissingParamType(
581
                            'Parameter $' . $function_param->name . ' has no provided type',
582
                            $function_param->location
583
                        ),
584
                        $storage->suppressed_issues + $this->getSuppressedIssues()
585
                    );
586
                }
587
            }
588
        }
589
590
        if ($this->function instanceof Closure
591
            || $this->function instanceof ArrowFunction
592
        ) {
593
            $this->verifyReturnType(
594
                $function_stmts,
595
                $statements_analyzer,
596
                $storage->return_type,
597
                $this->source->getFQCLN(),
598
                $storage->return_type_location,
599
                $context->has_returned,
600
                $global_context && $global_context->inside_call
601
            );
602
603
            $closure_yield_types = [];
604
605
            $closure_return_types = ReturnTypeCollector::getReturnTypes(
606
                $codebase,
607
                $type_provider,
608
                $function_stmts,
609
                $closure_yield_types,
610
                true
611
            );
612
613
            $closure_return_type = $closure_return_types
614
                ? \Psalm\Type::combineUnionTypeArray(
615
                    $closure_return_types,
616
                    $codebase
617
                )
618
                : null;
619
620
            $closure_yield_type = $closure_yield_types
621
                ? \Psalm\Type::combineUnionTypeArray(
622
                    $closure_yield_types,
623
                    $codebase
624
                )
625
                : null;
626
627
            if ($closure_return_type || $closure_yield_type) {
628
                if ($closure_yield_type) {
629
                    $closure_return_type = $closure_yield_type;
630
                }
631
632
                if (($storage->return_type === $storage->signature_return_type)
633
                    && (!$storage->return_type
634
                        || $storage->return_type->hasMixed()
635
                        || TypeAnalyzer::isContainedBy(
636
                            $codebase,
637
                            $closure_return_type,
0 ignored issues
show
Bug introduced by
It seems like $closure_return_type defined by $closure_return_types ? ...ypes, $codebase) : null on line 613 can be null; however, Psalm\Internal\Analyzer\...alyzer::isContainedBy() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
638
                            $storage->return_type
639
                        ))
640
                ) {
641
                    if ($function_type = $statements_analyzer->node_data->getType($this->function)) {
642
                        /**
643
                         * @var Type\Atomic\TFn
644
                         */
645
                        $closure_atomic = \array_values($function_type->getAtomicTypes())[0];
646
                        $closure_atomic->return_type = $closure_return_type;
647
                    }
648
                }
649
            }
650
        }
651
652
        if ($codebase->collect_references
653
            && !$context->collect_initializations
654
            && !$context->collect_mutations
655
            && $codebase->find_unused_variables
656
            && $context->check_variables
657
        ) {
658
            $this->checkParamReferences(
659
                $statements_analyzer,
660
                $storage,
661
                $appearing_class_storage,
662
                $context
663
            );
664
        }
665
666
        foreach ($storage->throws as $expected_exception => $_) {
667
            if (($expected_exception === 'self'
668
                    || $expected_exception === 'static')
669
                && $context->self
670
            ) {
671
                $expected_exception = $context->self;
672
            }
673
674
            if (isset($storage->throw_locations[$expected_exception])) {
675
                if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
676
                    $statements_analyzer,
677
                    $expected_exception,
678
                    $storage->throw_locations[$expected_exception],
679
                    $context->self,
680
                    $context->calling_method_id,
681
                    $statements_analyzer->getSuppressedIssues(),
682
                    false,
683
                    false,
684
                    true,
685
                    true
686
                )) {
687
                    $input_type = new Type\Union([new TNamedObject($expected_exception)]);
688
                    $container_type = new Type\Union([new TNamedObject('Exception'), new TNamedObject('Throwable')]);
689
690
                    if (!TypeAnalyzer::isContainedBy($codebase, $input_type, $container_type)) {
691
                        if (IssueBuffer::accepts(
692
                            new \Psalm\Issue\InvalidThrow(
693
                                'Class supplied for @throws ' . $expected_exception
694
                                    . ' does not implement Throwable',
695
                                $storage->throw_locations[$expected_exception],
696
                                $expected_exception
697
                            ),
698
                            $statements_analyzer->getSuppressedIssues()
699
                        )) {
700
                            // fall through
701
                        }
702
                    }
703
704
                    if ($codebase->alter_code) {
705
                        $codebase->classlikes->handleDocblockTypeInMigration(
706
                            $codebase,
707
                            $this,
708
                            $input_type,
709
                            $storage->throw_locations[$expected_exception],
710
                            $context->calling_method_id
711
                        );
712
                    }
713
                }
714
            }
715
        }
716
717
        foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $codelocations) {
718
            $is_expected = false;
719
720
            foreach ($storage->throws as $expected_exception => $_) {
721
                if ($expected_exception === $possibly_thrown_exception
722
                    || $codebase->classExtendsOrImplements($possibly_thrown_exception, $expected_exception)
723
                ) {
724
                    $is_expected = true;
725
                    break;
726
                }
727
            }
728
729
            if (!$is_expected) {
730
                foreach ($codelocations as $codelocation) {
731
                    // issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc.
732
                    if (IssueBuffer::accepts(
733
                        new MissingThrowsDocblock(
734
                            $possibly_thrown_exception . ' is thrown but not caught - please either catch'
735
                                . ' or add a @throws annotation',
736
                            $codelocation
737
                        )
738
                    )) {
739
                        // fall through
740
                    }
741
                }
742
            }
743
        }
744
745
        if ($codebase->taint
746
            && $this->function instanceof ClassMethod
747
            && $cased_method_id
748
            && $storage->specialize_call
749
            && isset($context->vars_in_scope['$this'])
750
            && $context->vars_in_scope['$this']->parent_nodes
751
        ) {
752
            $method_source = TaintNode::getForMethodReturn(
753
                (string) $method_id,
754
                $cased_method_id,
755
                $storage->location
756
            );
757
758
            $codebase->taint->addTaintNode($method_source);
759
760
            foreach ($context->vars_in_scope['$this']->parent_nodes as $parent_node) {
761
                $codebase->taint->addPath(
762
                    $parent_node,
763
                    $method_source,
764
                    '$this'
765
                );
766
            }
767
        }
768
769
        if ($add_mutations) {
770
            if ($this->return_vars_in_scope !== null) {
771
                $context->vars_in_scope = TypeAnalyzer::combineKeyedTypes(
772
                    $context->vars_in_scope,
773
                    $this->return_vars_in_scope
774
                );
775
            }
776
777
            if ($this->return_vars_possibly_in_scope !== null) {
778
                $context->vars_possibly_in_scope = array_merge(
779
                    $context->vars_possibly_in_scope,
780
                    $this->return_vars_possibly_in_scope
781
                );
782
            }
783
784
            foreach ($context->vars_in_scope as $var => $_) {
785
                if (strpos($var, '$this->') !== 0 && $var !== '$this') {
786
                    unset($context->vars_in_scope[$var]);
787
                }
788
            }
789
790
            foreach ($context->vars_possibly_in_scope as $var => $_) {
791
                if (strpos($var, '$this->') !== 0 && $var !== '$this') {
792
                    unset($context->vars_possibly_in_scope[$var]);
793
                }
794
            }
795
796
            if ($hash
797
                && $real_method_id
798
                && $this instanceof MethodAnalyzer
799
                && !$context->collect_initializations
800
            ) {
801
                $new_hash = md5($real_method_id . '::' . $context->getScopeSummary());
802
803
                if ($new_hash === $hash) {
804
                    self::$no_effects_hashes[$hash] = true;
805
                }
806
            }
807
        }
808
809
        $plugin_classes = $codebase->config->after_functionlike_checks;
810
811
        if ($plugin_classes) {
812
            $file_manipulations = [];
813
814
            foreach ($plugin_classes as $plugin_fq_class_name) {
815
                if ($plugin_fq_class_name::afterStatementAnalysis(
816
                    $this->function,
817
                    $storage,
818
                    $this,
819
                    $codebase,
820
                    $file_manipulations
821
                ) === false) {
822
                    return false;
823
                }
824
            }
825
826
            if ($file_manipulations) {
827
                \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
828
                    $this->getFilePath(),
829
                    $file_manipulations
830
                );
831
            }
832
        }
833
834
        return null;
835
    }
836
837
    private function checkParamReferences(
838
        StatementsAnalyzer $statements_analyzer,
839
        FunctionLikeStorage $storage,
840
        ?\Psalm\Storage\ClassLikeStorage $class_storage,
841
        Context $context
842
    ) : void {
843
        $codebase = $statements_analyzer->getCodebase();
844
845
        $unused_params = [];
846
847
        foreach ($statements_analyzer->getUnusedVarLocations() as list($var_name, $original_location)) {
848
            if (!array_key_exists(substr($var_name, 1), $storage->param_lookup)) {
849
                continue;
850
            }
851
852
            if (strpos($var_name, '$_') === 0 || (strpos($var_name, '$unused') === 0 && $var_name !== '$unused')) {
853
                continue;
854
            }
855
856
            $position = array_search(substr($var_name, 1), array_keys($storage->param_lookup), true);
857
858
            if ($position === false) {
859
                throw new \UnexpectedValueException('$position should not be false here');
860
            }
861
862
            if ($storage->params[$position]->by_ref) {
863
                continue;
864
            }
865
866
            if (!($storage instanceof MethodStorage)
867
                || !$storage->cased_name
868
                || $storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
869
            ) {
870
                if ($this->function instanceof Closure
871
                    || $this->function instanceof ArrowFunction
872
                ) {
873
                    if (IssueBuffer::accepts(
874
                        new UnusedClosureParam(
875
                            'Param ' . $var_name . ' is never referenced in this method',
876
                            $original_location
877
                        ),
878
                        $this->getSuppressedIssues()
879
                    )) {
880
                        // fall through
881
                    }
882
                } else {
883
                    if (IssueBuffer::accepts(
884
                        new UnusedParam(
885
                            'Param ' . $var_name . ' is never referenced in this method',
886
                            $original_location
887
                        ),
888
                        $this->getSuppressedIssues()
889
                    )) {
890
                        // fall through
891
                    }
892
                }
893
            } else {
894
                $fq_class_name = (string)$context->self;
895
896
                $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
897
898
                $method_name_lc = strtolower($storage->cased_name);
899
900
                if ($storage->abstract) {
901
                    continue;
902
                }
903
904
                if (isset($class_storage->overridden_method_ids[$method_name_lc])) {
905
                    $parent_method_id = end($class_storage->overridden_method_ids[$method_name_lc]);
906
907
                    if ($parent_method_id) {
908
                        $parent_method_storage = $codebase->methods->getStorage($parent_method_id);
909
910
                        // if the parent method has a param at that position and isn't abstract
911
                        if (!$parent_method_storage->abstract
912
                            && isset($parent_method_storage->params[$position])
913
                        ) {
914
                            continue;
915
                        }
916
                    }
917
                }
918
919
                $unused_params[$position] = $original_location;
920
            }
921
        }
922
923
        if ($storage instanceof MethodStorage
924
            && $this instanceof MethodAnalyzer
925
            && $class_storage
926
            && $storage->cased_name
927
            && $storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PRIVATE
928
        ) {
929
            $method_id_lc = strtolower((string) $this->getMethodId());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getMethodId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\MethodAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
930
931
            foreach ($storage->params as $i => $_) {
932
                if (!isset($unused_params[$i])) {
933
                    $codebase->file_reference_provider->addMethodParamUse(
934
                        $method_id_lc,
935
                        $i,
936
                        $method_id_lc
937
                    );
938
939
                    $method_name_lc = strtolower($storage->cased_name);
940
941
                    if (!isset($class_storage->overridden_method_ids[$method_name_lc])) {
942
                        continue;
943
                    }
944
945
                    foreach ($class_storage->overridden_method_ids[$method_name_lc] as $parent_method_id) {
946
                        $codebase->file_reference_provider->addMethodParamUse(
947
                            strtolower((string) $parent_method_id),
948
                            $i,
949
                            $method_id_lc
950
                        );
951
                    }
952
                }
953
            }
954
        }
955
    }
956
957
    /**
958
     * @param array<int, \Psalm\Storage\FunctionLikeParameter> $params
959
     * @param array<int, Type\Union> $implemented_docblock_param_types
960
     */
961
    private function processParams(
962
        StatementsAnalyzer $statements_analyzer,
963
        FunctionLikeStorage $storage,
964
        ?string $cased_method_id,
965
        array $params,
966
        Context $context,
967
        array $implemented_docblock_param_types,
968
        bool $non_null_param_types,
969
        bool $has_template_types
970
    ) : bool {
971
        $check_stmts = true;
972
        $codebase = $statements_analyzer->getCodebase();
973
        $project_analyzer = $statements_analyzer->getProjectAnalyzer();
974
975
        foreach ($params as $offset => $function_param) {
976
            $signature_type = $function_param->signature_type;
977
            $signature_type_location = $function_param->signature_type_location;
978
979
            if ($signature_type && $signature_type_location && $signature_type->hasObjectType()) {
980
                $referenced_type = $signature_type;
981
                if ($referenced_type->isNullable()) {
982
                    $referenced_type = clone $referenced_type;
983
                    $referenced_type->removeType('null');
984
                }
985
                list($start, $end) = $signature_type_location->getSelectionBounds();
986
                $codebase->analyzer->addOffsetReference(
987
                    $this->getFilePath(),
988
                    $start,
989
                    $end,
990
                    (string) $referenced_type
991
                );
992
            }
993
994
            if ($signature_type) {
995
                $signature_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
996
                    $codebase,
997
                    $signature_type,
998
                    $context->self,
999
                    $context->self,
1000
                    $this->getParentFQCLN()
1001
                );
1002
            }
1003
1004
            if ($function_param->type) {
1005
                $is_signature_type = $function_param->type === $function_param->signature_type;
1006
1007
                if ($is_signature_type
1008
                    && $storage instanceof MethodStorage
1009
                    && $storage->inheritdoc
1010
                    && isset($implemented_docblock_param_types[$offset])
1011
                ) {
1012
                    $param_type = clone $implemented_docblock_param_types[$offset];
1013
                } else {
1014
                    $param_type = clone $function_param->type;
1015
                }
1016
1017
                $param_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
1018
                    $codebase,
1019
                    $param_type,
1020
                    $context->self,
1021
                    $context->self,
1022
                    $this->getParentFQCLN()
1023
                );
1024
1025
                if ($function_param->type_location) {
1026
                    if ($param_type->check(
1027
                        $this,
1028
                        $function_param->type_location,
1029
                        $storage->suppressed_issues,
1030
                        [],
1031
                        false,
1032
                        false,
1033
                        $this->function instanceof ClassMethod
1034
                            && strtolower($this->function->name->name) !== '__construct'
1035
                    ) === false) {
1036
                        $check_stmts = false;
1037
                    }
1038
                }
1039
            } else {
1040
                if (!$non_null_param_types && isset($implemented_docblock_param_types[$offset])) {
1041
                    $param_type = clone $implemented_docblock_param_types[$offset];
1042
1043
                    $param_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
1044
                        $codebase,
1045
                        $param_type,
1046
                        $context->self,
1047
                        $context->self,
1048
                        $this->getParentFQCLN()
1049
                    );
1050
                } else {
1051
                    $param_type = Type::getMixed();
1052
                }
1053
            }
1054
1055
            if ($param_type->hasTemplate() && $param_type->isSingle()) {
1056
                /** @var Type\Atomic\TTemplateParam */
1057
                $template_type = \array_values($param_type->getAtomicTypes())[0];
1058
1059
                if ($template_type->as->getTemplateTypes()) {
1060
                    $param_type = $template_type->as;
1061
                }
1062
            }
1063
1064
            $var_type = $param_type;
1065
1066
            if ($function_param->is_variadic) {
1067
                $var_type = new Type\Union([
1068
                    new Type\Atomic\TList($param_type),
1069
                ]);
1070
            }
1071
1072
            if ($cased_method_id && $codebase->taint) {
1073
                $type_source = TaintNode::getForMethodArgument(
1074
                    $cased_method_id,
1075
                    $cased_method_id,
1076
                    $offset,
1077
                    $function_param->location,
1078
                    null
1079
                );
1080
                $var_type->parent_nodes = [$type_source];
1081
            }
1082
1083
            $context->vars_in_scope['$' . $function_param->name] = $var_type;
1084
            $context->vars_possibly_in_scope['$' . $function_param->name] = true;
1085
1086
            if ($codebase->find_unused_variables && $function_param->location) {
1087
                $context->unreferenced_vars['$' . $function_param->name] = [
1088
                    $function_param->location->getHash() => $function_param->location
1089
                ];
1090
            }
1091
1092
            if ($function_param->by_ref) {
1093
                $context->vars_in_scope['$' . $function_param->name]->by_ref = true;
1094
            }
1095
1096
            $parser_param = $this->function->getParams()[$offset];
1097
1098
            if (!$function_param->type_location || !$function_param->location) {
1099
                if ($parser_param->default) {
1100
                    ExpressionAnalyzer::analyze($statements_analyzer, $parser_param->default, $context);
1101
                }
1102
1103
                continue;
1104
            }
1105
1106
            if ($signature_type) {
1107
                $union_comparison_result = new TypeComparisonResult();
1108
1109
                if (!TypeAnalyzer::isContainedBy(
1110
                    $codebase,
1111
                    $param_type,
1112
                    $signature_type,
1113
                    false,
1114
                    false,
1115
                    $union_comparison_result
1116
                ) && !$union_comparison_result->type_coerced_from_mixed
1117
                ) {
1118
                    if ($codebase->alter_code
1119
                        && isset($project_analyzer->getIssuesToFix()['MismatchingDocblockParamType'])
1120
                    ) {
1121
                        $this->addOrUpdateParamType($project_analyzer, $function_param->name, $signature_type, true);
1122
1123
                        continue;
1124
                    }
1125
1126
                    if (IssueBuffer::accepts(
1127
                        new MismatchingDocblockParamType(
1128
                            'Parameter $' . $function_param->name . ' has wrong type \'' . $param_type .
1129
                                '\', should be \'' . $signature_type . '\'',
1130
                            $function_param->type_location
1131
                        ),
1132
                        $storage->suppressed_issues,
1133
                        true
1134
                    )) {
1135
                        // do nothing
1136
                    }
1137
1138
                    if ($signature_type->check(
1139
                        $this,
1140
                        $function_param->type_location,
1141
                        $storage->suppressed_issues,
1142
                        [],
1143
                        false
1144
                    ) === false) {
1145
                        $check_stmts = false;
1146
                    }
1147
1148
                    continue;
1149
                }
1150
            }
1151
1152
            if ($parser_param->default) {
1153
                ExpressionAnalyzer::analyze($statements_analyzer, $parser_param->default, $context);
1154
1155
                $default_type = $statements_analyzer->node_data->getType($parser_param->default);
1156
1157
                if ($default_type
1158
                    && !$default_type->hasMixed()
1159
                    && !TypeAnalyzer::isContainedBy(
1160
                        $codebase,
1161
                        $default_type,
1162
                        $param_type,
1163
                        false,
1164
                        false,
1165
                        null,
1166
                        true
1167
                    )
1168
                ) {
1169
                    if (IssueBuffer::accepts(
1170
                        new InvalidParamDefault(
1171
                            'Default value type ' . $default_type->getId() . ' for argument ' . ($offset + 1)
1172
                                . ' of method ' . $cased_method_id
1173
                                . ' does not match the given type ' . $param_type->getId(),
1174
                            $function_param->type_location
1175
                        )
1176
                    )) {
1177
                        // fall through
1178
                    }
1179
                }
1180
            }
1181
1182
            if ($has_template_types) {
1183
                $substituted_type = clone $param_type;
1184
                if ($substituted_type->check(
1185
                    $this->source,
1186
                    $function_param->type_location,
1187
                    $this->suppressed_issues,
1188
                    [],
1189
                    false
1190
                ) === false) {
1191
                    $check_stmts = false;
1192
                }
1193
            } else {
1194
                if ($param_type->isVoid()) {
1195
                    if (IssueBuffer::accepts(
1196
                        new ReservedWord(
1197
                            'Parameter cannot be void',
1198
                            $function_param->type_location,
1199
                            'void'
1200
                        ),
1201
                        $this->suppressed_issues
1202
                    )) {
1203
                        // fall through
1204
                    }
1205
                }
1206
1207
                if ($param_type->check(
1208
                    $this->source,
1209
                    $function_param->type_location,
1210
                    $this->suppressed_issues,
1211
                    [],
1212
                    false
1213
                ) === false) {
1214
                    $check_stmts = false;
1215
                }
1216
            }
1217
1218
            if ($codebase->collect_locations) {
1219
                if ($function_param->type_location !== $function_param->signature_type_location &&
1220
                    $function_param->signature_type_location &&
1221
                    $function_param->signature_type
1222
                ) {
1223
                    if ($function_param->signature_type->check(
1224
                        $this->source,
1225
                        $function_param->signature_type_location,
1226
                        $this->suppressed_issues,
1227
                        [],
1228
                        false
1229
                    ) === false) {
1230
                        $check_stmts = false;
1231
                    }
1232
                }
1233
            }
1234
1235
            if ($function_param->by_ref) {
1236
                // register by ref params as having been used, to avoid false positives
1237
                // @todo change the assignment analysis *just* for byref params
1238
                // so that we don't have to do this
1239
                $context->hasVariable('$' . $function_param->name);
1240
            }
1241
1242
            $statements_analyzer->registerVariable(
1243
                '$' . $function_param->name,
1244
                $function_param->location,
1245
                null
1246
            );
1247
        }
1248
1249
        return $check_stmts;
1250
    }
1251
1252
    /**
1253
     * @param \Psalm\Storage\FunctionLikeParameter[] $params
1254
     */
1255
    private function alterParams(
1256
        Codebase $codebase,
1257
        FunctionLikeStorage $storage,
1258
        array $params,
1259
        Context $context
1260
    ) : void {
1261
        foreach ($this->function->params as $param) {
1262
            $param_name_node = null;
1263
1264
            if ($param->type instanceof PhpParser\Node\Name) {
1265
                $param_name_node = $param->type;
1266
            } elseif ($param->type instanceof PhpParser\Node\NullableType
1267
                && $param->type->type instanceof PhpParser\Node\Name
1268
            ) {
1269
                $param_name_node = $param->type->type;
1270
            }
1271
1272
            if ($param_name_node) {
1273
                $resolved_name = ClassLikeAnalyzer::getFQCLNFromNameObject($param_name_node, $this->getAliases());
1274
1275
                $parent_fqcln = $this->getParentFQCLN();
1276
1277
                if ($resolved_name === 'self' && $context->self) {
1278
                    $resolved_name = (string) $context->self;
1279
                } elseif ($resolved_name === 'parent' && $parent_fqcln) {
1280
                    $resolved_name = $parent_fqcln;
1281
                }
1282
1283
                $codebase->classlikes->handleClassLikeReferenceInMigration(
1284
                    $codebase,
1285
                    $this,
1286
                    $param_name_node,
1287
                    $resolved_name,
1288
                    $context->calling_method_id,
1289
                    false,
1290
                    true
1291
                );
1292
            }
1293
        }
1294
1295
        if ($this->function->returnType) {
1296
            $return_name_node = null;
1297
1298
            if ($this->function->returnType instanceof PhpParser\Node\Name) {
1299
                $return_name_node = $this->function->returnType;
1300
            } elseif ($this->function->returnType instanceof PhpParser\Node\NullableType
1301
                && $this->function->returnType->type instanceof PhpParser\Node\Name
1302
            ) {
1303
                $return_name_node = $this->function->returnType->type;
1304
            }
1305
1306
            if ($return_name_node) {
1307
                $resolved_name = ClassLikeAnalyzer::getFQCLNFromNameObject($return_name_node, $this->getAliases());
1308
1309
                $parent_fqcln = $this->getParentFQCLN();
1310
1311
                if ($resolved_name === 'self' && $context->self) {
1312
                    $resolved_name = (string) $context->self;
1313
                } elseif ($resolved_name === 'parent' && $parent_fqcln) {
1314
                    $resolved_name = $parent_fqcln;
1315
                }
1316
1317
                $codebase->classlikes->handleClassLikeReferenceInMigration(
1318
                    $codebase,
1319
                    $this,
1320
                    $return_name_node,
1321
                    $resolved_name,
1322
                    $context->calling_method_id,
1323
                    false,
1324
                    true
1325
                );
1326
            }
1327
        }
1328
1329
        if ($storage->return_type
1330
            && $storage->return_type_location
1331
            && $storage->return_type_location !== $storage->signature_return_type_location
1332
        ) {
1333
            $replace_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
1334
                $codebase,
1335
                $storage->return_type,
1336
                $context->self,
1337
                'static',
1338
                $this->getParentFQCLN(),
1339
                false
1340
            );
1341
1342
            $codebase->classlikes->handleDocblockTypeInMigration(
1343
                $codebase,
1344
                $this,
1345
                $replace_type,
1346
                $storage->return_type_location,
1347
                $context->calling_method_id
1348
            );
1349
        }
1350
1351
        foreach ($params as $function_param) {
1352
            if ($function_param->type
1353
                && $function_param->type_location
1354
                && $function_param->type_location !== $function_param->signature_type_location
1355
                && $function_param->type_location->file_path === $this->getFilePath()
1356
            ) {
1357
                $replace_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
1358
                    $codebase,
1359
                    $function_param->type,
1360
                    $context->self,
1361
                    'static',
1362
                    $this->getParentFQCLN(),
1363
                    false
1364
                );
1365
1366
                $codebase->classlikes->handleDocblockTypeInMigration(
1367
                    $codebase,
1368
                    $this,
1369
                    $replace_type,
1370
                    $function_param->type_location,
1371
                    $context->calling_method_id
1372
                );
1373
            }
1374
        }
1375
    }
1376
1377
    /**
1378
     * @param array<PhpParser\Node\Stmt> $function_stmts
1379
     * @param Type\Union|null     $return_type
1380
     * @param string              $fq_class_name
1381
     * @param CodeLocation|null   $return_type_location
1382
     *
1383
     * @return  false|null
1384
     */
1385
    public function verifyReturnType(
1386
        array $function_stmts,
1387
        StatementsAnalyzer $statements_analyzer,
1388
        Type\Union $return_type = null,
1389
        $fq_class_name = null,
1390
        CodeLocation $return_type_location = null,
1391
        bool $did_explicitly_return = false,
1392
        bool $closure_inside_call = false
1393
    ) {
1394
        ReturnTypeAnalyzer::verifyReturnType(
1395
            $this->function,
1396
            $function_stmts,
1397
            $statements_analyzer,
1398
            $statements_analyzer->node_data,
1399
            $this,
1400
            $return_type,
1401
            $fq_class_name,
1402
            $return_type_location,
1403
            [],
1404
            $did_explicitly_return,
1405
            $closure_inside_call
1406
        );
1407
    }
1408
1409
    /**
1410
     * @param string $param_name
1411
     * @param bool $docblock_only
1412
     *
1413
     * @return void
1414
     */
1415
    public function addOrUpdateParamType(
1416
        ProjectAnalyzer $project_analyzer,
1417
        $param_name,
1418
        Type\Union $inferred_return_type,
1419
        $docblock_only = false
1420
    ) {
1421
        $manipulator = FunctionDocblockManipulator::getForFunction(
1422
            $project_analyzer,
1423
            $this->source->getFilePath(),
1424
            $this->getId(),
1425
            $this->function
1426
        );
1427
1428
        $codebase = $project_analyzer->getCodebase();
1429
        $is_final = true;
1430
        $fqcln = $this->source->getFQCLN();
1431
1432
        if ($fqcln !== null && $this->function instanceof ClassMethod) {
1433
            $class_storage = $codebase->classlike_storage_provider->get($fqcln);
1434
            $is_final = $this->function->isFinal() || $class_storage->final;
0 ignored issues
show
Bug introduced by
The method isFinal does only exist in PhpParser\Node\Stmt\ClassMethod, but not in PhpParser\Node\Expr\Arro...ser\Node\Stmt\Function_.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1435
        }
1436
1437
        $allow_native_type = !$docblock_only
1438
            && $codebase->php_major_version >= 7
1439
            && (
1440
                $codebase->allow_backwards_incompatible_changes
1441
                || $is_final
1442
                || !$this->function instanceof PhpParser\Node\Stmt\ClassMethod
1443
            );
1444
1445
        $manipulator->setParamType(
1446
            $param_name,
1447
            $allow_native_type
1448
                ? $inferred_return_type->toPhpString(
1449
                    $this->source->getNamespace(),
1450
                    $this->source->getAliasedClassesFlipped(),
1451
                    $this->source->getFQCLN(),
1452
                    $project_analyzer->getCodebase()->php_major_version,
1453
                    $project_analyzer->getCodebase()->php_minor_version
1454
                ) : null,
1455
            $inferred_return_type->toNamespacedString(
1456
                $this->source->getNamespace(),
1457
                $this->source->getAliasedClassesFlipped(),
1458
                $this->source->getFQCLN(),
1459
                false
1460
            ),
1461
            $inferred_return_type->toNamespacedString(
1462
                $this->source->getNamespace(),
1463
                $this->source->getAliasedClassesFlipped(),
1464
                $this->source->getFQCLN(),
1465
                true
1466
            )
1467
        );
1468
    }
1469
1470
    /**
1471
     * Adds return types for the given function
1472
     *
1473
     * @param   string  $return_type
0 ignored issues
show
Bug introduced by
There is no parameter named $return_type. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1474
     * @param   Context $context
1475
     *
1476
     * @return  void
1477
     */
1478
    public function addReturnTypes(Context $context)
1479
    {
1480
        if ($this->return_vars_in_scope !== null) {
1481
            $this->return_vars_in_scope = TypeAnalyzer::combineKeyedTypes(
1482
                $context->vars_in_scope,
1483
                $this->return_vars_in_scope
1484
            );
1485
        } else {
1486
            $this->return_vars_in_scope = $context->vars_in_scope;
1487
        }
1488
1489
        if ($this->return_vars_possibly_in_scope !== null) {
1490
            $this->return_vars_possibly_in_scope = array_merge(
1491
                $context->vars_possibly_in_scope,
1492
                $this->return_vars_possibly_in_scope
1493
            );
1494
        } else {
1495
            $this->return_vars_possibly_in_scope = $context->vars_possibly_in_scope;
1496
        }
1497
    }
1498
1499
    /**
1500
     * @return void
1501
     */
1502
    public function examineParamTypes(
1503
        StatementsAnalyzer $statements_analyzer,
1504
        Context $context,
1505
        Codebase $codebase,
1506
        PhpParser\Node $stmt = null
1507
    ) {
1508
        $storage = $this->getFunctionLikeStorage($statements_analyzer);
1509
1510
        foreach ($storage->params as $param) {
1511
            if ($param->by_ref && isset($context->vars_in_scope['$' . $param->name]) && !$param->is_variadic) {
1512
                $actual_type = $context->vars_in_scope['$' . $param->name];
1513
                $param_out_type = $param->out_type ?: $param->type;
1514
1515
                if ($param_out_type && !$actual_type->hasMixed() && $param->location) {
1516
                    if (!TypeAnalyzer::isContainedBy(
1517
                        $codebase,
1518
                        $actual_type,
1519
                        $param_out_type,
1520
                        $actual_type->ignore_nullable_issues,
1521
                        $actual_type->ignore_falsable_issues
1522
                    )
1523
                    ) {
1524
                        if (IssueBuffer::accepts(
1525
                            new ReferenceConstraintViolation(
1526
                                'Variable ' . '$' . $param->name . ' is limited to values of type '
1527
                                    . $param_out_type->getId()
1528
                                    . ' because it is passed by reference, '
1529
                                    . $actual_type->getId() . ' type found. Use @param-out to specify '
1530
                                    . 'a different output type',
1531
                                $stmt
1532
                                    ? new CodeLocation($this, $stmt)
1533
                                    : $param->location
1534
                            ),
1535
                            $statements_analyzer->getSuppressedIssues()
1536
                        )) {
1537
                            // fall through
1538
                        }
1539
                    }
1540
                }
1541
            }
1542
        }
1543
    }
1544
1545
    /**
1546
     * @return null|string
1547
     */
1548
    public function getMethodName()
1549
    {
1550
        if ($this->function instanceof ClassMethod) {
1551
            return (string)$this->function->name;
1552
        }
1553
    }
1554
1555
    /**
1556
     * @param string|null $context_self
1557
     *
1558
     * @return string
1559
     */
1560
    public function getCorrectlyCasedMethodId($context_self = null)
1561
    {
1562
        if ($this->function instanceof ClassMethod) {
1563
            $function_name = (string)$this->function->name;
1564
1565
            return ($context_self ?: $this->source->getFQCLN()) . '::' . $function_name;
1566
        }
1567
1568
        if ($this->function instanceof Function_) {
1569
            $namespace = $this->source->getNamespace();
1570
1571
            return ($namespace ? $namespace . '\\' : '') . $this->function->name;
1572
        }
1573
1574
        if (!$this instanceof ClosureAnalyzer) {
1575
            throw new \UnexpectedValueException('This is weird');
1576
        }
1577
1578
        return $this->getClosureId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getClosureId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\ClosureAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1579
    }
1580
1581
    /**
1582
     * @return FunctionLikeStorage
1583
     */
1584
    public function getFunctionLikeStorage(StatementsAnalyzer $statements_analyzer = null)
1585
    {
1586
        $codebase = $this->codebase;
1587
1588
        if ($this->function instanceof ClassMethod && $this instanceof MethodAnalyzer) {
1589
            $method_id = $this->getMethodId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getMethodId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\MethodAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1590
            $codebase_methods = $codebase->methods;
1591
1592
            try {
1593
                return $codebase_methods->getStorage($method_id);
1594
            } catch (\UnexpectedValueException $e) {
1595
                $declaring_method_id = $codebase_methods->getDeclaringMethodId($method_id);
1596
1597
                if ($declaring_method_id === null) {
1598
                    throw new \UnexpectedValueException('Cannot get storage for function that doesn‘t exist');
1599
                }
1600
1601
                // happens for fake constructors
1602
                return $codebase_methods->getStorage($declaring_method_id);
1603
            }
1604
        }
1605
1606
        if ($this instanceof FunctionAnalyzer) {
1607
            $function_id = $this->getFunctionId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getFunctionId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\FunctionAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1608
        } elseif ($this instanceof ClosureAnalyzer) {
1609
            $function_id = $this->getClosureId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getClosureId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\ClosureAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1610
        } else {
1611
            throw new \UnexpectedValueException('This is weird');
1612
        }
1613
1614
        return $codebase->functions->getStorage($statements_analyzer, $function_id);
1615
    }
1616
1617
    /** @return non-empty-string */
0 ignored issues
show
Documentation introduced by
The doc-type non-empty-string could not be parsed: Unknown type name "non-empty-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...
1618
    public function getId() : string
1619
    {
1620
        if ($this instanceof MethodAnalyzer) {
1621
            return (string) $this->getMethodId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getMethodId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\MethodAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1622
        }
1623
1624
        if ($this instanceof FunctionAnalyzer) {
1625
            return $this->getFunctionId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getFunctionId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\FunctionAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1626
        }
1627
1628
        if ($this instanceof ClosureAnalyzer) {
1629
            return $this->getClosureId();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Psalm\Internal\Analyzer\FunctionLikeAnalyzer as the method getClosureId() does only exist in the following sub-classes of Psalm\Internal\Analyzer\FunctionLikeAnalyzer: Psalm\Internal\Analyzer\ClosureAnalyzer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1630
        }
1631
1632
        throw new \UnexpectedValueException('This is weird');
1633
    }
1634
1635
    /**
1636
     * @return array<string, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, 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...
1637
     */
1638
    public function getAliasedClassesFlipped()
1639
    {
1640
        if ($this->source instanceof NamespaceAnalyzer ||
1641
            $this->source instanceof FileAnalyzer ||
1642
            $this->source instanceof ClassLikeAnalyzer
1643
        ) {
1644
            return $this->source->getAliasedClassesFlipped();
1645
        }
1646
1647
        return [];
1648
    }
1649
1650
    /**
1651
     * @return array<string, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, 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...
1652
     */
1653
    public function getAliasedClassesFlippedReplaceable()
1654
    {
1655
        if ($this->source instanceof NamespaceAnalyzer ||
1656
            $this->source instanceof FileAnalyzer ||
1657
            $this->source instanceof ClassLikeAnalyzer
1658
        ) {
1659
            return $this->source->getAliasedClassesFlippedReplaceable();
1660
        }
1661
1662
        return [];
1663
    }
1664
1665
    /**
1666
     * @return string|null
1667
     */
1668
    public function getFQCLN()
1669
    {
1670
        return $this->source->getFQCLN();
1671
    }
1672
1673
    /**
1674
     * @return null|string
1675
     */
1676
    public function getClassName()
1677
    {
1678
        return $this->source->getClassName();
1679
    }
1680
1681
    /**
1682
     * @return array<string, array<string, array{Type\Union}>>|null
0 ignored issues
show
Documentation introduced by
The doc-type array<string, 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...
1683
     */
1684
    public function getTemplateTypeMap()
1685
    {
1686
        if ($this->source instanceof ClassLikeAnalyzer) {
1687
            return ($this->source->getTemplateTypeMap() ?: [])
1688
                + ($this->storage->template_types ?: []);
1689
        }
1690
1691
        return $this->storage->template_types;
1692
    }
1693
1694
    /**
1695
     * @return string|null
1696
     */
1697
    public function getParentFQCLN()
1698
    {
1699
        return $this->source->getParentFQCLN();
1700
    }
1701
1702
    public function getNodeTypeProvider() : \Psalm\NodeTypeProvider
1703
    {
1704
        return $this->source->getNodeTypeProvider();
1705
    }
1706
1707
    /**
1708
     * @return bool
1709
     */
1710
    public function isStatic()
1711
    {
1712
        return $this->is_static;
1713
    }
1714
1715
    /**
1716
     * @return StatementsSource
1717
     */
1718
    public function getSource()
1719
    {
1720
        return $this->source;
1721
    }
1722
1723
    public function getCodebase() : Codebase
1724
    {
1725
        return $this->codebase;
1726
    }
1727
1728
    /**
1729
     * Get a list of suppressed issues
1730
     *
1731
     * @return array<string>
1732
     */
1733
    public function getSuppressedIssues()
1734
    {
1735
        return $this->suppressed_issues;
1736
    }
1737
1738
    /**
1739
     * @param array<int, string> $new_issues
1740
     *
1741
     * @return void
1742
     */
1743
    public function addSuppressedIssues(array $new_issues)
1744
    {
1745
        if (isset($new_issues[0])) {
1746
            $new_issues = \array_combine($new_issues, $new_issues);
1747
        }
1748
1749
        $this->suppressed_issues = $new_issues + $this->suppressed_issues;
1750
    }
1751
1752
    /**
1753
     * @param array<int, string> $new_issues
1754
     *
1755
     * @return void
1756
     */
1757
    public function removeSuppressedIssues(array $new_issues)
1758
    {
1759
        if (isset($new_issues[0])) {
1760
            $new_issues = \array_combine($new_issues, $new_issues);
1761
        }
1762
1763
        $this->suppressed_issues = \array_diff_key($this->suppressed_issues, $new_issues);
1764
    }
1765
1766
    /**
1767
     * Adds a suppressed issue, useful when creating a method checker from scratch
1768
     *
1769
     * @param string $issue_name
1770
     *
1771
     * @return void
1772
     */
1773
    public function addSuppressedIssue($issue_name)
1774
    {
1775
        $this->suppressed_issues[] = $issue_name;
1776
    }
1777
1778
    /**
1779
     * @return void
1780
     */
1781
    public static function clearCache()
1782
    {
1783
        self::$no_effects_hashes = [];
1784
    }
1785
1786
    /**
1787
     * @return Type\Union
1788
     */
1789
    public function getLocalReturnType(Type\Union $storage_return_type, bool $final = false)
1790
    {
1791
        if ($this->local_return_type) {
1792
            return $this->local_return_type;
1793
        }
1794
1795
        $this->local_return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
1796
            $this->codebase,
1797
            $storage_return_type,
1798
            $this->getFQCLN(),
1799
            $this->getFQCLN(),
1800
            $this->getParentFQCLN(),
1801
            true,
1802
            true,
1803
            $final
1804
        );
1805
1806
        return $this->local_return_type;
1807
    }
1808
}
1809