TypeChecker   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 27

Importance

Changes 0
Metric Value
dl 0
loc 383
rs 3.6
c 0
b 0
f 0
wmc 60
lcom 1
cbo 27

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
B enterNode() 0 32 9
A hasErrors() 0 4 1
C checkNamedObject() 0 61 13
C checkGenericParams() 0 84 13
B checkScalarClassConstant() 0 63 8
C checkTemplateParam() 0 41 12
A checkResource() 0 15 3

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
namespace Psalm\Internal\TypeVisitor;
3
4
use Psalm\CodeLocation;
5
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
6
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
7
use Psalm\Internal\Analyzer\TypeAnalyzer;
8
use Psalm\Internal\Type\TypeExpander;
9
use Psalm\Storage\MethodStorage;
10
use Psalm\Type\Atomic\TArray;
11
use Psalm\Type\Atomic\TScalarClassConstant;
12
use Psalm\Type\Atomic\TGenericObject;
13
use Psalm\Type\Atomic\TNamedObject;
14
use Psalm\Type\Atomic\TResource;
15
use Psalm\Type\Atomic\TTemplateParam;
16
use Psalm\Issue\DeprecatedClass;
17
use Psalm\Issue\InvalidTemplateParam;
18
use Psalm\Issue\MissingTemplateParam;
19
use Psalm\Issue\TooManyTemplateParams;
20
use Psalm\Issue\UndefinedConstant;
21
use Psalm\IssueBuffer;
22
use Psalm\StatementsSource;
23
use Psalm\Type\TypeNode;
24
use Psalm\Type\NodeVisitor;
25
use function strtolower;
26
27
class TypeChecker extends NodeVisitor
28
{
29
    /**
30
     * @var StatementsSource
31
     */
32
    private $source;
33
34
    /**
35
     * @var CodeLocation
36
     */
37
    private $code_location;
38
39
    /**
40
     * @var array<string>
41
     */
42
    private $suppressed_issues;
43
44
    /**
45
     * @var array<string, bool>
46
     */
47
    private $phantom_classes;
48
49
    /**
50
     * @var bool
51
     */
52
    private $inferred;
53
54
    /**
55
     * @var bool
56
     */
57
    private $inherited;
58
59
    /**
60
     * @var bool
61
     */
62
    private $prevent_template_covariance;
63
64
    /** @var bool */
65
    private $has_errors = false;
66
67
    private $calling_method_id;
68
69
    /**
70
     * @param  StatementsSource $source
71
     * @param  CodeLocation     $code_location
72
     * @param  array<string>    $suppressed_issues
73
     * @param  array<string, bool> $phantom_classes
74
     * @param  bool             $inferred
75
     *
76
     * @return null|false
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
77
     */
78
    public function __construct(
79
        StatementsSource $source,
80
        CodeLocation $code_location,
81
        array $suppressed_issues,
82
        array $phantom_classes = [],
83
        bool $inferred = true,
84
        bool $inherited = false,
85
        bool $prevent_template_covariance = false,
86
        ?string $calling_method_id = null
87
    ) {
88
        $this->source = $source;
89
        $this->code_location = $code_location;
90
        $this->suppressed_issues = $suppressed_issues;
91
        $this->phantom_classes = $phantom_classes;
92
        $this->inferred = $inferred;
93
        $this->inherited = $inherited;
94
        $this->prevent_template_covariance = $prevent_template_covariance;
95
        $this->calling_method_id = $calling_method_id;
96
    }
97
98
    /**
99
     * @psalm-suppress MoreSpecificImplementedParamType
100
     *
101
     * @param  \Psalm\Type\Atomic|\Psalm\Type\Union $type
102
     */
103
    protected function enterNode(TypeNode $type) : ?int
104
    {
105
        if ($type->checked) {
0 ignored issues
show
Bug introduced by
Accessing checked on the interface Psalm\Type\TypeNode suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
106
            return NodeVisitor::DONT_TRAVERSE_CHILDREN;
107
        }
108
109
        if ($type instanceof TNamedObject) {
110
            $this->checkNamedObject($type);
111
        } elseif ($type instanceof TScalarClassConstant) {
112
            $this->checkScalarClassConstant($type);
113
        } elseif ($type instanceof TTemplateParam) {
114
            $this->checkTemplateParam($type);
115
        } elseif ($type instanceof TResource) {
116
            $this->checkResource($type);
117
        } elseif ($type instanceof TArray) {
118
            if (\count($type->type_params) > 2) {
119
                if (IssueBuffer::accepts(
120
                    new TooManyTemplateParams(
121
                        $type->getId(). ' has too many template params, expecting 2',
122
                        $this->code_location
123
                    ),
124
                    $this->suppressed_issues
125
                )) {
126
                    // fall through
127
                }
128
            }
129
        }
130
131
        $type->checked = true;
0 ignored issues
show
Bug introduced by
Accessing checked on the interface Psalm\Type\TypeNode suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
132
133
        return null;
134
    }
135
136
    public function hasErrors() : bool
137
    {
138
        return $this->has_errors;
139
    }
140
141
    private function checkNamedObject(TNamedObject $atomic) : void
142
    {
143
        $codebase = $this->source->getCodebase();
144
145
        if ($this->code_location instanceof CodeLocation\DocblockTypeLocation
146
            && $codebase->store_node_types
147
            && $atomic->offset_start !== null
148
            && $atomic->offset_end !== null
149
        ) {
150
            $codebase->analyzer->addOffsetReference(
151
                $this->source->getFilePath(),
152
                $this->code_location->raw_file_start + $atomic->offset_start,
153
                $this->code_location->raw_file_start + $atomic->offset_end,
154
                $atomic->value
155
            );
156
        }
157
158
        if (!isset($this->phantom_classes[\strtolower($atomic->value)]) &&
159
            ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
160
                $this->source,
161
                $atomic->value,
162
                $this->code_location,
163
                $this->source->getFQCLN(),
164
                $this->calling_method_id,
165
                $this->suppressed_issues,
166
                $this->inferred,
167
                false,
168
                true,
169
                $atomic->from_docblock
170
            ) === false
171
        ) {
172
            $this->has_errors = true;
173
            return;
174
        }
175
176
        $fq_class_name_lc = strtolower($atomic->value);
177
178
        if (!$this->inherited
179
            && $codebase->classlike_storage_provider->has($fq_class_name_lc)
180
            && $this->source->getFQCLN() !== $atomic->value
181
        ) {
182
            $class_storage = $codebase->classlike_storage_provider->get($fq_class_name_lc);
183
184
            if ($class_storage->deprecated) {
185
                if (IssueBuffer::accepts(
186
                    new DeprecatedClass(
187
                        'Class ' . $atomic->value . ' is marked as deprecated',
188
                        $this->code_location,
189
                        $atomic->value
190
                    ),
191
                    $this->source->getSuppressedIssues()
192
                )) {
193
                    // fall through
194
                }
195
            }
196
        }
197
198
        if ($atomic instanceof TGenericObject) {
199
            $this->checkGenericParams($atomic);
200
        }
201
    }
202
203
    private function checkGenericParams(TGenericObject $atomic) : void
204
    {
205
        $codebase = $this->source->getCodebase();
206
207
        try {
208
            $class_storage = $codebase->classlike_storage_provider->get(strtolower($atomic->value));
209
        } catch (\InvalidArgumentException $e) {
210
            return;
211
        }
212
213
        $expected_type_params = $class_storage->template_types ?: [];
214
        $expected_param_covariants = $class_storage->template_covariants;
215
216
        $template_type_count = \count($expected_type_params);
217
        $template_param_count = \count($atomic->type_params);
218
219
        if ($template_type_count > $template_param_count) {
220
            if (IssueBuffer::accepts(
221
                new MissingTemplateParam(
222
                    $atomic->value . ' has missing template params, expecting '
223
                        . $template_type_count,
224
                    $this->code_location
225
                ),
226
                $this->suppressed_issues
227
            )) {
228
                // fall through
229
            }
230
        } elseif ($template_type_count < $template_param_count) {
231
            if (IssueBuffer::accepts(
232
                new TooManyTemplateParams(
233
                    $atomic->getId(). ' has too many template params, expecting '
234
                        . $template_type_count,
235
                    $this->code_location
236
                ),
237
                $this->suppressed_issues
238
            )) {
239
                // fall through
240
            }
241
        }
242
243
        foreach ($atomic->type_params as $i => $type_param) {
244
            $this->prevent_template_covariance = $this->source instanceof \Psalm\Internal\Analyzer\MethodAnalyzer
245
                && $this->source->getMethodName() !== '__construct'
246
                && empty($expected_param_covariants[$i]);
247
248
            if (isset(\array_values($expected_type_params)[$i])) {
249
                $expected_type_param = \reset(\array_values($expected_type_params)[$i])[0];
250
251
                $expected_type_param = \Psalm\Internal\Type\TypeExpander::expandUnion(
252
                    $codebase,
253
                    $expected_type_param,
254
                    $this->source->getFQCLN(),
255
                    $this->source->getFQCLN(),
256
                    $this->source->getParentFQCLN()
257
                );
258
259
                $template_name = \array_keys($expected_type_params)[$i];
260
261
                $type_param = \Psalm\Internal\Type\TypeExpander::expandUnion(
262
                    $codebase,
263
                    $type_param,
264
                    $this->source->getFQCLN(),
265
                    $this->source->getFQCLN(),
266
                    $this->source->getParentFQCLN()
267
                );
268
269
                if (!TypeAnalyzer::isContainedBy($codebase, $type_param, $expected_type_param)) {
270
                    if (IssueBuffer::accepts(
271
                        new InvalidTemplateParam(
272
                            'Extended template param ' . $template_name
273
                                . ' of ' . $atomic->getId()
274
                                . ' expects type '
275
                                . $expected_type_param->getId()
276
                                . ', type ' . $type_param->getId() . ' given',
277
                            $this->code_location
278
                        ),
279
                        $this->suppressed_issues
280
                    )) {
281
                        // fall through
282
                    }
283
                }
284
            }
285
        }
286
    }
287
288
    public function checkScalarClassConstant(TScalarClassConstant $atomic) : void
289
    {
290
        $fq_classlike_name = $atomic->fq_classlike_name === 'self'
291
            ? $this->source->getClassName()
292
            : $atomic->fq_classlike_name;
293
294
        if (!$fq_classlike_name) {
295
            return;
296
        }
297
298
        if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
299
            $this->source,
300
            $fq_classlike_name,
301
            $this->code_location,
302
            null,
303
            null,
304
            $this->suppressed_issues,
305
            $this->inferred,
306
            false,
307
            true,
308
            $atomic->from_docblock
309
        ) === false
310
        ) {
311
            $this->has_errors = true;
312
            return;
313
        }
314
315
        $const_name = $atomic->const_name;
316
        if (\strpos($const_name, '*') !== false) {
317
            $expanded = TypeExpander::expandAtomic(
318
                $this->source->getCodebase(),
319
                $atomic,
320
                $fq_classlike_name,
321
                $fq_classlike_name,
322
                null,
323
                true,
324
                true
325
            );
326
327
            $is_defined = \is_array($expanded) && \count($expanded) > 0;
328
        } else {
329
            $class_constant_type = $this->source->getCodebase()->classlikes->getConstantForClass(
330
                $fq_classlike_name,
331
                $atomic->const_name,
332
                \ReflectionProperty::IS_PRIVATE,
333
                null
334
            );
335
336
            $is_defined = null !== $class_constant_type;
337
        }
338
339
        if (!$is_defined) {
340
            if (\Psalm\IssueBuffer::accepts(
341
                new UndefinedConstant(
342
                    'Constant ' . $fq_classlike_name . '::' . $const_name . ' is not defined',
343
                    $this->code_location
344
                ),
345
                $this->source->getSuppressedIssues()
346
            )) {
347
                // fall through
348
            }
349
        }
350
    }
351
352
    public function checkTemplateParam(\Psalm\Type\Atomic\TTemplateParam $atomic) : void
353
    {
354
        if ($this->prevent_template_covariance
355
            && \substr($atomic->defining_class, 0, 3) !== 'fn-'
356
        ) {
357
            $codebase = $this->source->getCodebase();
358
359
            $class_storage = $codebase->classlike_storage_provider->get($atomic->defining_class);
360
361
            $template_offset = $class_storage->template_types
362
                ? \array_search($atomic->param_name, \array_keys($class_storage->template_types), true)
363
                : false;
364
365
            if ($template_offset !== false
366
                && isset($class_storage->template_covariants[$template_offset])
367
                && $class_storage->template_covariants[$template_offset]
368
            ) {
369
                $method_storage = $this->source instanceof \Psalm\Internal\Analyzer\MethodAnalyzer
370
                    ? $this->source->getFunctionLikeStorage()
371
                    : null;
372
373
                if ($method_storage instanceof MethodStorage
374
                    && $method_storage->mutation_free
375
                    && !$method_storage->mutation_free_inferred
376
                ) {
377
                    // do nothing
378
                } else {
379
                    if (\Psalm\IssueBuffer::accepts(
380
                        new \Psalm\Issue\InvalidTemplateParam(
381
                            'Template param ' . $atomic->param_name . ' of '
382
                                . $atomic->defining_class . ' is marked covariant and cannot be used here',
383
                            $this->code_location
384
                        ),
385
                        $this->source->getSuppressedIssues()
386
                    )) {
387
                        // fall through
388
                    }
389
                }
390
            }
391
        }
392
    }
393
394
    public function checkResource(TResource $atomic) : void
395
    {
396
        if (!$atomic->from_docblock) {
397
            if (\Psalm\IssueBuffer::accepts(
398
                new \Psalm\Issue\ReservedWord(
399
                    '\'resource\' is a reserved word',
400
                    $this->code_location,
401
                    'resource'
402
                ),
403
                $this->source->getSuppressedIssues()
404
            )) {
405
                // fall through
406
            }
407
        }
408
    }
409
}
410