NegatedAssertionReconciler   F
last analyzed

Complexity

Total Complexity 92

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 19

Importance

Changes 0
Metric Value
dl 0
loc 383
rs 2
c 0
b 0
f 0
wmc 92
lcom 0
cbo 19

2 Methods

Rating   Name   Duplication   Size   Complexity  
F handleLiteralNegatedEquality() 0 112 29
F reconcile() 0 250 63

How to fix   Complexity   

Complex Class

Complex classes like NegatedAssertionReconciler 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 NegatedAssertionReconciler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Psalm\Internal\Type;
4
5
use function count;
6
use Psalm\CodeLocation;
7
use Psalm\Internal\Analyzer\StatementsAnalyzer;
8
use Psalm\Internal\Analyzer\TraitAnalyzer;
9
use Psalm\Internal\Analyzer\TypeAnalyzer;
10
use Psalm\Issue\DocblockTypeContradiction;
11
use Psalm\Issue\TypeDoesNotContainType;
12
use Psalm\IssueBuffer;
13
use Psalm\Type;
14
use Psalm\Type\Atomic;
15
use Psalm\Type\Atomic\TArray;
16
use Psalm\Type\Atomic\TFalse;
17
use Psalm\Type\Atomic\TNamedObject;
18
use Psalm\Type\Atomic\TString;
19
use Psalm\Type\Atomic\TTrue;
20
use Psalm\Type\Reconciler;
21
use function strpos;
22
use function strtolower;
23
use function substr;
24
25
class NegatedAssertionReconciler extends Reconciler
26
{
27
    /**
28
     * @param  array<string, array<string, array{Type\Union}>> $template_type_map
29
     * @param  string[]   $suppressed_issues
30
     * @param  0|1|2      $failed_reconciliation
0 ignored issues
show
Documentation introduced by
The doc-type 0|1|2 could not be parsed: Unknown type name "0" 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...
31
     *
32
     * @return Type\Union
33
     */
34
    public static function reconcile(
35
        StatementsAnalyzer $statements_analyzer,
36
        string $assertion,
37
        bool $is_strict_equality,
38
        bool $is_loose_equality,
39
        Type\Union $existing_var_type,
40
        array $template_type_map,
41
        string $old_var_type_string,
42
        ?string $key,
43
        ?CodeLocation $code_location,
44
        array $suppressed_issues,
45
        int &$failed_reconciliation
46
    ) {
47
        $is_equality = $is_strict_equality || $is_loose_equality;
48
49
        // this is a specific value comparison type that cannot be negated
50
        if ($is_equality && $bracket_pos = strpos($assertion, '(')) {
51
            if ($existing_var_type->hasMixed()) {
52
                return $existing_var_type;
53
            }
54
55
            return self::handleLiteralNegatedEquality(
56
                $statements_analyzer,
57
                $assertion,
58
                $bracket_pos,
59
                $existing_var_type,
60
                $old_var_type_string,
61
                $key,
62
                $code_location,
63
                $suppressed_issues,
64
                $is_strict_equality
65
            );
66
        }
67
68
        if (!$is_equality) {
69
            if ($assertion === 'isset') {
70
                if ($existing_var_type->possibly_undefined) {
71
                    return Type::getEmpty();
72
                }
73
74
                if (!$existing_var_type->isNullable()
75
                    && $key
76
                    && strpos($key, '[') === false
77
                    && $key !== '$_SESSION'
78
                ) {
79
                    foreach ($existing_var_type->getAtomicTypes() as $atomic) {
80
                        if (!$existing_var_type->hasMixed()
81
                            || $atomic instanceof Type\Atomic\TNonEmptyMixed
82
                        ) {
83
                            $failed_reconciliation = 2;
84
85
                            if ($code_location) {
86
                                if ($existing_var_type->from_docblock) {
87
                                    if (IssueBuffer::accepts(
88
                                        new DocblockTypeContradiction(
89
                                            'Cannot resolve types for ' . $key . ' with docblock-defined type '
90
                                                . $existing_var_type . ' and !isset assertion',
91
                                            $code_location
92
                                        ),
93
                                        $suppressed_issues
94
                                    )) {
95
                                        // fall through
96
                                    }
97
                                } else {
98
                                    if (IssueBuffer::accepts(
99
                                        new TypeDoesNotContainType(
100
                                            'Cannot resolve types for ' . $key . ' with type '
101
                                                . $existing_var_type . ' and !isset assertion',
102
                                            $code_location
103
                                        ),
104
                                        $suppressed_issues
105
                                    )) {
106
                                        // fall through
107
                                    }
108
                                }
109
                            }
110
111
                            return $existing_var_type->from_docblock
112
                                ? Type::getNull()
113
                                : Type::getEmpty();
114
                        }
115
                    }
116
                }
117
118
                return Type::getNull();
119
            } elseif ($assertion === 'array-key-exists') {
120
                return Type::getEmpty();
121
            } elseif (substr($assertion, 0, 9) === 'in-array-') {
122
                return $existing_var_type;
123
            } elseif (substr($assertion, 0, 14) === 'has-array-key-') {
124
                return $existing_var_type;
125
            }
126
        }
127
128
        $existing_var_atomic_types = $existing_var_type->getAtomicTypes();
129
130
        if ($assertion === 'false' && isset($existing_var_atomic_types['bool'])) {
131
            $existing_var_type->removeType('bool');
132
            $existing_var_type->addType(new TTrue);
133
        } elseif ($assertion === 'true' && isset($existing_var_atomic_types['bool'])) {
134
            $existing_var_type->removeType('bool');
135
            $existing_var_type->addType(new TFalse);
136
        } else {
137
            $simple_negated_type = SimpleNegatedAssertionReconciler::reconcile(
138
                $assertion,
139
                $existing_var_type,
140
                $key,
141
                $code_location,
142
                $suppressed_issues,
143
                $failed_reconciliation,
144
                $is_equality,
145
                $is_strict_equality
146
            );
147
148
            if ($simple_negated_type) {
149
                return $simple_negated_type;
150
            }
151
        }
152
153
        if ($assertion === 'iterable' || $assertion === 'countable') {
154
            $existing_var_type->removeType('array');
155
        }
156
157
        if (!$is_equality
158
            && isset($existing_var_atomic_types['int'])
159
            && $existing_var_type->from_calculation
160
            && ($assertion === 'int' || $assertion === 'float')
161
        ) {
162
            $existing_var_type->removeType($assertion);
163
164
            if ($assertion === 'int') {
165
                $existing_var_type->addType(new Type\Atomic\TFloat);
166
            } else {
167
                $existing_var_type->addType(new Type\Atomic\TInt);
168
            }
169
170
            $existing_var_type->from_calculation = false;
171
172
            return $existing_var_type;
173
        }
174
175
        if (strtolower($assertion) === 'traversable'
176
            && isset($existing_var_atomic_types['iterable'])
177
        ) {
178
            /** @var Type\Atomic\TIterable */
179
            $iterable = $existing_var_atomic_types['iterable'];
180
            $existing_var_type->removeType('iterable');
181
            $existing_var_type->addType(new TArray(
182
                [
183
                    $iterable->type_params[0]->hasMixed()
184
                        ? Type::getArrayKey()
185
                        : clone $iterable->type_params[0],
186
                    clone $iterable->type_params[1],
187
                ]
188
            ));
189
        } elseif (strtolower($assertion) === 'int'
190
            && isset($existing_var_type->getAtomicTypes()['array-key'])
191
        ) {
192
            $existing_var_type->removeType('array-key');
193
            $existing_var_type->addType(new TString);
194
        } elseif (substr($assertion, 0, 9) === 'getclass-') {
195
            $assertion = substr($assertion, 9);
196
        } elseif (!$is_equality) {
197
            $codebase = $statements_analyzer->getCodebase();
198
199
            // if there wasn't a direct hit, go deeper, eliminating subtypes
200
            if (!$existing_var_type->removeType($assertion)) {
201
                foreach ($existing_var_type->getAtomicTypes() as $part_name => $existing_var_type_part) {
202
                    if (!$existing_var_type_part->isObjectType() || strpos($assertion, '-')) {
203
                        continue;
204
                    }
205
206
                    $new_type_part = Atomic::create($assertion);
207
208
                    if (!$new_type_part instanceof TNamedObject) {
209
                        continue;
210
                    }
211
212
                    if (TypeAnalyzer::isAtomicContainedBy(
213
                        $codebase,
214
                        $existing_var_type_part,
215
                        $new_type_part,
216
                        false,
217
                        false
218
                    )) {
219
                        $existing_var_type->removeType($part_name);
220
                    } elseif (TypeAnalyzer::isAtomicContainedBy(
221
                        $codebase,
222
                        $new_type_part,
223
                        $existing_var_type_part,
224
                        false,
225
                        false
226
                    )) {
227
                        $existing_var_type->different = true;
228
                    }
229
                }
230
            }
231
        }
232
233
        if ($is_strict_equality
234
            && $assertion !== 'isset'
235
            && ($key !== '$this'
236
                || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer))
237
        ) {
238
            $assertion = Type::parseString($assertion, null, $template_type_map);
239
240
            if ($key
241
                && $code_location
242
                && !TypeAnalyzer::canExpressionTypesBeIdentical(
243
                    $statements_analyzer->getCodebase(),
244
                    $existing_var_type,
245
                    $assertion
246
                )
247
            ) {
248
                self::triggerIssueForImpossible(
249
                    $existing_var_type,
250
                    $old_var_type_string,
251
                    $key,
252
                    '!=' . $assertion,
253
                    true,
254
                    $code_location,
255
                    $suppressed_issues
256
                );
257
            }
258
        }
259
260
        if (empty($existing_var_type->getAtomicTypes())) {
261
            if ($key !== '$this'
262
                || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)
263
            ) {
264
                if ($key && $code_location && !$is_equality) {
265
                    self::triggerIssueForImpossible(
266
                        $existing_var_type,
267
                        $old_var_type_string,
268
                        $key,
269
                        '!' . $assertion,
270
                        false,
271
                        $code_location,
272
                        $suppressed_issues
273
                    );
274
                }
275
            }
276
277
            $failed_reconciliation = 2;
278
279
            return new Type\Union([new Type\Atomic\TEmptyMixed]);
280
        }
281
282
        return $existing_var_type;
283
    }
284
285
    /**
286
     * @param  string     $assertion
287
     * @param  int        $bracket_pos
288
     * @param  string     $old_var_type_string
289
     * @param  string|null $key
290
     * @param  CodeLocation|null $code_location
291
     * @param  string[]   $suppressed_issues
292
     *
293
     * @return Type\Union
294
     */
295
    private static function handleLiteralNegatedEquality(
296
        StatementsAnalyzer $statements_analyzer,
297
        string $assertion,
298
        int $bracket_pos,
299
        Type\Union $existing_var_type,
300
        string $old_var_type_string,
301
        ?string $key,
302
        ?CodeLocation $code_location,
303
        array $suppressed_issues,
304
        bool $is_strict_equality
305
    ) {
306
        $scalar_type = substr($assertion, 0, $bracket_pos);
307
308
        $existing_var_atomic_types = $existing_var_type->getAtomicTypes();
309
310
        $did_remove_type = false;
311
        $did_match_literal_type = false;
312
313
        $scalar_var_type = null;
314
315
        if ($scalar_type === 'int') {
316
            if ($existing_var_type->hasInt()) {
317
                if ($existing_int_types = $existing_var_type->getLiteralInts()) {
318
                    $did_match_literal_type = true;
319
320
                    if (isset($existing_int_types[$assertion])) {
321
                        $existing_var_type->removeType($assertion);
322
323
                        $did_remove_type = true;
324
                    }
325
                }
326
            } else {
327
                $scalar_value = substr($assertion, $bracket_pos + 1, -1);
328
                $scalar_var_type = Type::getInt(false, (int) $scalar_value);
329
            }
330
        } elseif ($scalar_type === 'string'
331
            || $scalar_type === 'class-string'
332
            || $scalar_type === 'interface-string'
333
            || $scalar_type === 'trait-string'
334
            || $scalar_type === 'callable-string'
335
        ) {
336
            if ($existing_var_type->hasString()) {
337
                if ($existing_string_types = $existing_var_type->getLiteralStrings()) {
338
                    $did_match_literal_type = true;
339
340
                    if (isset($existing_string_types[$assertion])) {
341
                        $existing_var_type->removeType($assertion);
342
343
                        $did_remove_type = true;
344
                    }
345
                } elseif ($assertion === 'string()') {
346
                    $existing_var_type->addType(new Type\Atomic\TNonEmptyString());
347
                }
348
            } elseif ($scalar_type === 'string') {
349
                $scalar_value = substr($assertion, $bracket_pos + 1, -1);
350
                $scalar_var_type = Type::getString($scalar_value);
351
            }
352
        } elseif ($scalar_type === 'float') {
353
            if ($existing_var_type->hasFloat()) {
354
                if ($existing_float_types = $existing_var_type->getLiteralFloats()) {
355
                    $did_match_literal_type = true;
356
357
                    if (isset($existing_float_types[$assertion])) {
358
                        $existing_var_type->removeType($assertion);
359
360
                        $did_remove_type = true;
361
                    }
362
                }
363
            } else {
364
                $scalar_value = substr($assertion, $bracket_pos + 1, -1);
365
                $scalar_var_type = Type::getFloat((float) $scalar_value);
366
            }
367
        }
368
369
        if ($key && $code_location) {
370
            if ($did_match_literal_type
371
                && (!$did_remove_type || count($existing_var_atomic_types) === 1)
372
            ) {
373
                self::triggerIssueForImpossible(
374
                    $existing_var_type,
375
                    $old_var_type_string,
376
                    $key,
377
                    '!' . $assertion,
378
                    !$did_remove_type,
379
                    $code_location,
380
                    $suppressed_issues
381
                );
382
            } elseif ($scalar_var_type
383
                && $is_strict_equality
384
                && ($key !== '$this'
385
                    || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer))
386
            ) {
387
                if (!TypeAnalyzer::canExpressionTypesBeIdentical(
388
                    $statements_analyzer->getCodebase(),
389
                    $existing_var_type,
390
                    $scalar_var_type
391
                )) {
392
                    self::triggerIssueForImpossible(
393
                        $existing_var_type,
394
                        $old_var_type_string,
395
                        $key,
396
                        '!=' . $assertion,
397
                        true,
398
                        $code_location,
399
                        $suppressed_issues
400
                    );
401
                }
402
            }
403
        }
404
405
        return $existing_var_type;
406
    }
407
}
408