NegatedAssertionReconciler::reconcile()   F
last analyzed

Complexity

Conditions 63
Paths 1816

Size

Total Lines 250

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 63
nc 1816
nop 11
dl 0
loc 250
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
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