AssertionReconciler   F
last analyzed

Complexity

Total Complexity 287

Size/Duplication

Total Lines 1213
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 29

Importance

Changes 0
Metric Value
dl 0
loc 1213
rs 0.8
c 0
b 0
f 0
wmc 287
lcom 1
cbo 29

4 Methods

Rating   Name   Duplication   Size   Complexity  
F reconcile() 0 290 56
F refine() 0 293 73
F filterTypeWithAnother() 0 257 55
F handleLiteralEquality() 0 340 103

How to fix   Complexity   

Complex Class

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

1
<?php
2
namespace Psalm\Internal\Type;
3
4
use function array_filter;
5
use function count;
6
use function get_class;
7
use function is_string;
8
use Psalm\Codebase;
9
use Psalm\CodeLocation;
10
use Psalm\Internal\Analyzer\StatementsAnalyzer;
11
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer;
12
use Psalm\Internal\Analyzer\TraitAnalyzer;
13
use Psalm\Internal\Analyzer\TypeAnalyzer;
14
use Psalm\Issue\DocblockTypeContradiction;
15
use Psalm\Issue\TypeDoesNotContainNull;
16
use Psalm\Issue\TypeDoesNotContainType;
17
use Psalm\IssueBuffer;
18
use Psalm\Type;
19
use Psalm\Type\Atomic;
20
use Psalm\Type\Union;
21
use Psalm\Type\Atomic\TClassString;
22
use Psalm\Type\Atomic\TInt;
23
use Psalm\Type\Atomic\TMixed;
24
use Psalm\Type\Atomic\TNamedObject;
25
use Psalm\Type\Atomic\TString;
26
use Psalm\Type\Atomic\TTemplateParam;
27
use function strpos;
28
use function substr;
29
use Psalm\Issue\InvalidDocblock;
30
use function array_intersect_key;
31
use function array_merge;
32
33
class AssertionReconciler extends \Psalm\Type\Reconciler
34
{
35
    /**
36
     * Reconciles types
37
     *
38
     * think of this as a set of functions e.g. empty(T), notEmpty(T), null(T), notNull(T) etc. where
39
     *  - empty(Object) => null,
40
     *  - empty(bool) => false,
41
     *  - notEmpty(Object|null) => Object,
42
     *  - notEmpty(Object|false) => Object
43
     *
44
     * @param   string[]            $suppressed_issues
45
     * @param   array<string, array<string, array{Type\Union}>> $template_type_map
46
     * @param-out   0|1|2   $failed_reconciliation
47
     */
48
    public static function reconcile(
49
        string $assertion,
50
        ?Union $existing_var_type,
51
        ?string $key,
52
        StatementsAnalyzer $statements_analyzer,
53
        bool $inside_loop,
54
        array $template_type_map,
55
        CodeLocation $code_location = null,
56
        array $suppressed_issues = [],
57
        ?int &$failed_reconciliation = 0
58
    ) : Union {
59
        $codebase = $statements_analyzer->getCodebase();
60
61
        $is_strict_equality = false;
62
        $is_loose_equality = false;
63
        $is_equality = false;
64
        $is_negation = false;
65
        $failed_reconciliation = 0;
66
67
        if ($assertion[0] === '!') {
68
            $assertion = substr($assertion, 1);
69
            $is_negation = true;
70
        }
71
72
        if ($assertion[0] === '=') {
73
            $assertion = substr($assertion, 1);
74
            $is_strict_equality = true;
75
            $is_equality = true;
76
        }
77
78
        if ($assertion[0] === '~') {
79
            $assertion = substr($assertion, 1);
80
            $is_loose_equality = true;
81
            $is_equality = true;
82
        }
83
84
        if ($assertion[0] === '>') {
85
            $assertion = 'falsy';
86
            $is_negation = true;
87
        }
88
89
        if ($existing_var_type === null
90
            && is_string($key)
91
            && VariableFetchAnalyzer::isSuperGlobal($key)
92
        ) {
93
            $existing_var_type = VariableFetchAnalyzer::getGlobalType($key);
94
        }
95
96
        if ($existing_var_type === null) {
97
            if (($assertion === 'isset' && !$is_negation)
98
                || ($assertion === 'empty' && $is_negation)
99
            ) {
100
                return Type::getMixed($inside_loop);
101
            }
102
103
            if ($assertion === 'array-key-exists'
104
                || $assertion === 'non-empty-countable'
105
                || strpos($assertion, 'has-at-least-') === 0
106
            ) {
107
                return Type::getMixed();
108
            }
109
110
            if (!$is_negation && $assertion !== 'falsy' && $assertion !== 'empty') {
111
                if ($is_equality) {
112
                    $bracket_pos = strpos($assertion, '(');
113
114
                    if ($bracket_pos) {
115
                        $assertion = substr($assertion, 0, $bracket_pos);
116
                    }
117
                }
118
119
                try {
120
                    return Type::parseString($assertion, null, $template_type_map);
121
                } catch (\Exception $e) {
122
                    return Type::getMixed();
123
                }
124
            }
125
126
            return Type::getMixed();
127
        }
128
129
        $old_var_type_string = $existing_var_type->getId();
130
131
        if ($is_negation) {
132
            return NegatedAssertionReconciler::reconcile(
133
                $statements_analyzer,
134
                $assertion,
135
                $is_strict_equality,
136
                $is_loose_equality,
137
                $existing_var_type,
138
                $template_type_map,
139
                $old_var_type_string,
140
                $key,
141
                $code_location,
142
                $suppressed_issues,
143
                $failed_reconciliation
144
            );
145
        }
146
147
        $simply_reconciled_type = SimpleAssertionReconciler::reconcile(
148
            $assertion,
149
            $codebase,
150
            $existing_var_type,
151
            $key,
152
            $code_location,
153
            $suppressed_issues,
154
            $failed_reconciliation,
155
            $is_equality,
156
            $is_strict_equality,
157
            $inside_loop
158
        );
159
160
        if ($simply_reconciled_type) {
161
            return $simply_reconciled_type;
162
        }
163
164
        if (substr($assertion, 0, 4) === 'isa-') {
165
            $assertion = substr($assertion, 4);
166
167
            $allow_string_comparison = false;
168
169
            if (substr($assertion, 0, 7) === 'string-') {
170
                $assertion = substr($assertion, 7);
171
                $allow_string_comparison = true;
172
            }
173
174
            if ($existing_var_type->hasMixed()) {
175
                $type = new Type\Union([
176
                    new Type\Atomic\TNamedObject($assertion),
177
                ]);
178
179
                if ($allow_string_comparison) {
180
                    $type->addType(
181
                        new Type\Atomic\TClassString(
182
                            $assertion,
183
                            new Type\Atomic\TNamedObject($assertion)
184
                        )
185
                    );
186
                }
187
188
                return $type;
189
            }
190
191
            $existing_has_object = $existing_var_type->hasObjectType();
192
            $existing_has_string = $existing_var_type->hasString();
193
194
            if ($existing_has_object && !$existing_has_string) {
195
                $new_type = Type::parseString($assertion, null, $template_type_map);
196
            } elseif ($existing_has_string && !$existing_has_object) {
197
                if (!$allow_string_comparison && $code_location) {
198
                    if (IssueBuffer::accepts(
199
                        new TypeDoesNotContainType(
200
                            'Cannot allow string comparison to object for ' . $key,
201
                            $code_location
202
                        ),
203
                        $suppressed_issues
204
                    )) {
205
                        // fall through
206
                    }
207
208
                    $new_type = Type::getMixed();
209
                } else {
210
                    $new_type_has_interface_string = $codebase->interfaceExists($assertion);
211
212
                    $old_type_has_interface_string = false;
213
214
                    foreach ($existing_var_type->getAtomicTypes() as $existing_type_part) {
215
                        if ($existing_type_part instanceof TClassString
216
                            && $existing_type_part->as_type
217
                            && $codebase->interfaceExists($existing_type_part->as_type->value)
218
                        ) {
219
                            $old_type_has_interface_string = true;
220
                            break;
221
                        }
222
                    }
223
224
                    $new_type = Type::getClassString($assertion);
225
226
                    if ((
227
                        $new_type_has_interface_string
228
                            && !TypeAnalyzer::isContainedBy(
229
                                $codebase,
230
                                $existing_var_type,
231
                                $new_type
232
                            )
233
                    )
234
                        || (
235
                            $old_type_has_interface_string
236
                            && !TypeAnalyzer::isContainedBy(
237
                                $codebase,
238
                                $new_type,
239
                                $existing_var_type
240
                            )
241
                        )
242
                    ) {
243
                        $new_type_part = Atomic::create($assertion, null, $template_type_map);
244
245
                        $acceptable_atomic_types = [];
246
247
                        foreach ($existing_var_type->getAtomicTypes() as $existing_var_type_part) {
248
                            if (!$new_type_part instanceof TNamedObject
249
                                || !$existing_var_type_part instanceof TClassString
250
                            ) {
251
                                $acceptable_atomic_types = [];
252
253
                                break;
254
                            }
255
256
                            if (!$existing_var_type_part->as_type instanceof TNamedObject) {
257
                                $acceptable_atomic_types = [];
258
259
                                break;
260
                            }
261
262
                            $existing_var_type_part = $existing_var_type_part->as_type;
263
264
                            if (TypeAnalyzer::isAtomicContainedBy(
265
                                $codebase,
266
                                $existing_var_type_part,
267
                                $new_type_part
268
                            )) {
269
                                $acceptable_atomic_types[] = clone $existing_var_type_part;
270
                                continue;
271
                            }
272
273
                            if ($codebase->classExists($existing_var_type_part->value)
274
                                || $codebase->interfaceExists($existing_var_type_part->value)
275
                            ) {
276
                                $existing_var_type_part = clone $existing_var_type_part;
277
                                $existing_var_type_part->addIntersectionType($new_type_part);
278
                                $acceptable_atomic_types[] = $existing_var_type_part;
279
                            }
280
                        }
281
282
                        if (count($acceptable_atomic_types) === 1) {
283
                            return new Type\Union([
284
                                new TClassString('object', $acceptable_atomic_types[0]),
285
                            ]);
286
                        }
287
                    }
288
                }
289
            } else {
290
                $new_type = Type::getMixed();
291
            }
292
        } elseif (substr($assertion, 0, 9) === 'getclass-') {
293
            $assertion = substr($assertion, 9);
294
            $new_type = Type::parseString($assertion, null, $template_type_map);
295
        } else {
296
            $bracket_pos = strpos($assertion, '(');
297
298
            if ($bracket_pos) {
299
                return self::handleLiteralEquality(
300
                    $assertion,
301
                    $bracket_pos,
302
                    $is_loose_equality,
303
                    $existing_var_type,
304
                    $old_var_type_string,
305
                    $key,
306
                    $code_location,
307
                    $suppressed_issues
308
                );
309
            }
310
311
            $new_type = Type::parseString($assertion, null, $template_type_map);
312
        }
313
314
        if ($existing_var_type->hasMixed()) {
315
            if ($is_loose_equality
316
                && $new_type->hasScalarType()
317
            ) {
318
                return $existing_var_type;
319
            }
320
321
            return $new_type;
322
        }
323
324
        return self::refine(
325
            $statements_analyzer,
326
            $assertion,
327
            $new_type,
328
            $existing_var_type,
329
            $template_type_map,
330
            $key,
331
            $code_location,
332
            $is_equality,
333
            $is_loose_equality,
334
            $suppressed_issues,
335
            $failed_reconciliation
336
        );
337
    }
338
339
    /**
340
     * @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...
341
     * @param   string[]    $suppressed_issues
342
     * @param   array<string, array<string, array{Type\Union}>> $template_type_map
343
     * @param-out   0|1|2   $failed_reconciliation
344
     */
345
    private static function refine(
346
        StatementsAnalyzer $statements_analyzer,
347
        string $assertion,
348
        Union $new_type,
349
        Union $existing_var_type,
350
        array $template_type_map,
351
        ?string $key,
352
        ?CodeLocation $code_location,
353
        bool $is_equality,
354
        bool $is_loose_equality,
355
        array $suppressed_issues,
356
        int &$failed_reconciliation
357
    ) : Union {
358
        $codebase = $statements_analyzer->getCodebase();
359
360
        $old_var_type_string = $existing_var_type->getId();
361
362
        $new_type_has_interface = false;
363
364
        if ($new_type->hasObjectType()) {
365
            foreach ($new_type->getAtomicTypes() as $new_type_part) {
366
                if ($new_type_part instanceof TNamedObject &&
367
                    $codebase->interfaceExists($new_type_part->value)
368
                ) {
369
                    $new_type_has_interface = true;
370
                    break;
371
                }
372
            }
373
        }
374
375
        $old_type_has_interface = false;
376
377
        if ($existing_var_type->hasObjectType()) {
378
            foreach ($existing_var_type->getAtomicTypes() as $existing_type_part) {
379
                if ($existing_type_part instanceof TNamedObject &&
380
                    $codebase->interfaceExists($existing_type_part->value)
381
                ) {
382
                    $old_type_has_interface = true;
383
                    break;
384
                }
385
            }
386
        }
387
388
        try {
389
            if (strpos($assertion, '<') || strpos($assertion, '[') || strpos($assertion, '{')) {
390
                $new_type_union = Type::parseString($assertion);
391
392
                $new_type_part = \array_values($new_type_union->getAtomicTypes())[0];
393
            } else {
394
                $new_type_part = Atomic::create($assertion, null, $template_type_map);
395
            }
396
        } catch (\Psalm\Exception\TypeParseTreeException $e) {
397
            $new_type_part = new TMixed();
398
399
            if ($code_location) {
400
                if (IssueBuffer::accepts(
401
                    new InvalidDocblock(
402
                        $assertion . ' cannot be used in an assertion',
403
                        $code_location
404
                    ),
405
                    $suppressed_issues
406
                )) {
407
                    // fall through
408
                }
409
            }
410
        }
411
412
        if ($new_type_part instanceof Type\Atomic\TTemplateParam
413
            && $new_type_part->as->isSingle()
414
        ) {
415
            $new_as_atomic = \array_values($new_type_part->as->getAtomicTypes())[0];
416
417
            $acceptable_atomic_types = [];
418
419
            foreach ($existing_var_type->getAtomicTypes() as $existing_var_type_part) {
420
                if ($existing_var_type_part instanceof TNamedObject
421
                    || $existing_var_type_part instanceof TTemplateParam
422
                ) {
423
                    $new_type_part->addIntersectionType($existing_var_type_part);
424
                    $acceptable_atomic_types[] = clone $existing_var_type_part;
425
                } else {
426
                    if (TypeAnalyzer::isAtomicContainedBy(
427
                        $codebase,
428
                        $existing_var_type_part,
429
                        $new_as_atomic
430
                    )) {
431
                        $acceptable_atomic_types[] = clone $existing_var_type_part;
432
                    }
433
                }
434
            }
435
436
            if ($acceptable_atomic_types) {
437
                $new_type_part->as = new Type\Union($acceptable_atomic_types);
438
439
                return new Type\Union([$new_type_part]);
440
            }
441
        }
442
443
        if ($new_type_part instanceof Type\Atomic\ObjectLike) {
444
            $acceptable_atomic_types = [];
445
446
            foreach ($existing_var_type->getAtomicTypes() as $existing_var_type_part) {
447
                if ($existing_var_type_part instanceof Type\Atomic\ObjectLike) {
448
                    if (!array_intersect_key(
449
                        $existing_var_type_part->properties,
450
                        $new_type_part->properties
451
                    )) {
452
                        $existing_var_type_part = clone $existing_var_type_part;
453
                        $existing_var_type_part->properties = array_merge(
454
                            $existing_var_type_part->properties,
455
                            $new_type_part->properties
456
                        );
457
458
                        $acceptable_atomic_types[] = $existing_var_type_part;
459
                    }
460
                }
461
            }
462
463
            if ($acceptable_atomic_types) {
464
                return new Type\Union($acceptable_atomic_types);
465
            }
466
        }
467
468
        if ($new_type_part instanceof TNamedObject
469
            && ((
470
                $new_type_has_interface
471
                    && !TypeAnalyzer::isContainedBy(
472
                        $codebase,
473
                        $existing_var_type,
474
                        $new_type
475
                    )
476
            )
477
                || (
478
                    $old_type_has_interface
479
                    && !TypeAnalyzer::isContainedBy(
480
                        $codebase,
481
                        $new_type,
482
                        $existing_var_type
483
                    )
484
                ))
485
        ) {
486
            $acceptable_atomic_types = [];
487
488
            foreach ($existing_var_type->getAtomicTypes() as $existing_var_type_part) {
489
                if (TypeAnalyzer::isAtomicContainedBy(
490
                    $codebase,
491
                    $existing_var_type_part,
492
                    $new_type_part
493
                )) {
494
                    $acceptable_atomic_types[] = clone $existing_var_type_part;
495
                    continue;
496
                }
497
498
                if ($existing_var_type_part instanceof TNamedObject
499
                    && ($codebase->classExists($existing_var_type_part->value)
500
                        || $codebase->interfaceExists($existing_var_type_part->value))
501
                ) {
502
                    $existing_var_type_part = clone $existing_var_type_part;
503
                    $existing_var_type_part->addIntersectionType($new_type_part);
504
                    $acceptable_atomic_types[] = $existing_var_type_part;
505
                }
506
507
                if ($existing_var_type_part instanceof TTemplateParam) {
508
                    $existing_var_type_part = clone $existing_var_type_part;
509
                    $existing_var_type_part->addIntersectionType($new_type_part);
510
                    $acceptable_atomic_types[] = $existing_var_type_part;
511
                }
512
            }
513
514
            if ($acceptable_atomic_types) {
515
                return new Type\Union($acceptable_atomic_types);
516
            }
517
        } elseif (!$new_type->hasMixed()) {
518
            $has_match = true;
519
520
            if ($key
521
                && $code_location
522
                && $new_type->getId() === $existing_var_type->getId()
523
                && !$is_equality
524
                && (!($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)
525
                    || ($key !== '$this'
526
                        && !($existing_var_type->hasLiteralClassString() && $new_type->hasLiteralClassString())))
527
            ) {
528
                self::triggerIssueForImpossible(
529
                    $existing_var_type,
530
                    $old_var_type_string,
531
                    $key,
532
                    $assertion,
533
                    true,
534
                    $code_location,
535
                    $suppressed_issues
536
                );
537
            }
538
539
            $any_scalar_type_match_found = false;
540
541
            if ($code_location
542
                && $key
543
                && !$is_equality
544
                && $new_type_part instanceof TNamedObject
545
                && !$new_type_has_interface
546
                && (!($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)
547
                    || ($key !== '$this'
548
                        && !($existing_var_type->hasLiteralClassString() && $new_type->hasLiteralClassString())))
549
                && TypeAnalyzer::isContainedBy(
550
                    $codebase,
551
                    $existing_var_type,
552
                    $new_type
553
                )
554
            ) {
555
                self::triggerIssueForImpossible(
556
                    $existing_var_type,
557
                    $old_var_type_string,
558
                    $key,
559
                    $assertion,
560
                    true,
561
                    $code_location,
562
                    $suppressed_issues
563
                );
564
            }
565
566
            $new_type = self::filterTypeWithAnother(
567
                $codebase,
568
                $existing_var_type,
569
                $new_type,
570
                $template_type_map,
571
                $has_match,
572
                $any_scalar_type_match_found
573
            );
574
575
            if ($code_location
576
                && !$has_match
577
                && (!$is_loose_equality || !$any_scalar_type_match_found)
578
            ) {
579
                if ($assertion === 'null') {
580
                    if ($existing_var_type->from_docblock) {
581
                        if (IssueBuffer::accepts(
582
                            new DocblockTypeContradiction(
583
                                'Cannot resolve types for ' . $key . ' - docblock-defined type '
584
                                    . $existing_var_type . ' does not contain null',
585
                                $code_location
586
                            ),
587
                            $suppressed_issues
588
                        )) {
589
                            // fall through
590
                        }
591
                    } else {
592
                        if (IssueBuffer::accepts(
593
                            new TypeDoesNotContainNull(
594
                                'Cannot resolve types for ' . $key . ' - ' . $existing_var_type
595
                                    . ' does not contain null',
596
                                $code_location
597
                            ),
598
                            $suppressed_issues
599
                        )) {
600
                            // fall through
601
                        }
602
                    }
603
                } elseif (!($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)
604
                    || ($key !== '$this'
605
                        && !($existing_var_type->hasLiteralClassString() && $new_type->hasLiteralClassString()))
606
                ) {
607
                    if ($existing_var_type->from_docblock) {
608
                        if (IssueBuffer::accepts(
609
                            new DocblockTypeContradiction(
610
                                'Cannot resolve types for ' . $key . ' - docblock-defined type '
611
                                    . $existing_var_type->getId() . ' does not contain ' . $new_type->getId(),
612
                                $code_location
613
                            ),
614
                            $suppressed_issues
615
                        )) {
616
                            // fall through
617
                        }
618
                    } else {
619
                        if (IssueBuffer::accepts(
620
                            new TypeDoesNotContainType(
621
                                'Cannot resolve types for ' . $key . ' - ' . $existing_var_type->getId() .
622
                                ' does not contain ' . $new_type->getId(),
623
                                $code_location
624
                            ),
625
                            $suppressed_issues
626
                        )) {
627
                            // fall through
628
                        }
629
                    }
630
                }
631
632
                $failed_reconciliation = 2;
633
            }
634
        }
635
636
        return $new_type;
637
    }
638
639
640
641
    /**
642
     * @param array<string, array<string, array{0:Type\Union, 1?: int}>> $template_type_map
643
     */
644
    private static function filterTypeWithAnother(
645
        Codebase $codebase,
646
        Type\Union $existing_type,
647
        Type\Union $new_type,
648
        array $template_type_map,
649
        bool &$has_match,
650
        bool &$any_scalar_type_match_found
651
    ) : Type\Union {
652
        $matching_atomic_types = [];
653
654
        $has_cloned_type = false;
655
656
        foreach ($new_type->getAtomicTypes() as $new_type_part) {
657
            $has_local_match = false;
658
659
            foreach ($existing_type->getAtomicTypes() as $key => $existing_type_part) {
660
                // special workaround because PHP allows floats to contain ints, but we don’t want this
661
                // behaviour here
662
                if ($existing_type_part instanceof Type\Atomic\TFloat
663
                    && $new_type_part instanceof Type\Atomic\TInt
664
                ) {
665
                    $any_scalar_type_match_found = true;
666
                    continue;
667
                }
668
669
                $atomic_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
670
671
                if ($existing_type_part instanceof TNamedObject) {
672
                    $existing_type_part->was_static = false;
673
                }
674
675
                $atomic_contained_by = TypeAnalyzer::isAtomicContainedBy(
676
                    $codebase,
677
                    $new_type_part,
678
                    $existing_type_part,
679
                    true,
680
                    false,
681
                    $atomic_comparison_results
682
                );
683
684
                if ($atomic_contained_by) {
685
                    $has_local_match = true;
686
687
                    if ($atomic_comparison_results->type_coerced
688
                        && get_class($new_type_part) === Type\Atomic\TNamedObject::class
689
                        && $existing_type_part instanceof Type\Atomic\TGenericObject
690
                    ) {
691
                        // this is a hack - it's not actually rigorous, as the params may be different
692
                        $matching_atomic_types[] = new Type\Atomic\TGenericObject(
693
                            $new_type_part->value,
694
                            $existing_type_part->type_params
695
                        );
696
                    }
697
                } elseif (TypeAnalyzer::isAtomicContainedBy(
698
                    $codebase,
699
                    $existing_type_part,
700
                    $new_type_part,
701
                    true,
702
                    false,
703
                    null
704
                )) {
705
                    $has_local_match = true;
706
                    $matching_atomic_types[] = $existing_type_part;
707
                }
708
709
                if ($new_type_part instanceof Type\Atomic\ObjectLike
710
                    && $existing_type_part instanceof Type\Atomic\TList
711
                ) {
712
                    $new_type_key = $new_type_part->getGenericKeyType();
713
                    $new_type_value = $new_type_part->getGenericValueType();
714
715
                    if (!$new_type_key->hasString()) {
716
                        $has_param_match = false;
717
718
                        $new_type_value = self::filterTypeWithAnother(
719
                            $codebase,
720
                            $existing_type_part->type_param,
721
                            $new_type_value,
722
                            $template_type_map,
723
                            $has_param_match,
724
                            $any_scalar_type_match_found
725
                        );
726
727
                        $hybrid_type_part = new Type\Atomic\ObjectLike($new_type_part->properties);
728
                        $hybrid_type_part->previous_key_type = Type::getInt();
729
                        $hybrid_type_part->previous_value_type = $new_type_value;
730
                        $hybrid_type_part->is_list = true;
731
732
                        if (!$has_cloned_type) {
733
                            $new_type = clone $new_type;
734
                            $has_cloned_type = true;
735
                        }
736
737
                        $has_local_match = true;
738
739
                        $new_type->removeType($key);
740
                        $new_type->addType($hybrid_type_part);
741
742
                        continue;
743
                    }
744
                }
745
746
                if ($new_type_part instanceof Type\Atomic\TTemplateParam
747
                    && $existing_type_part instanceof Type\Atomic\TTemplateParam
748
                    && $new_type_part->param_name !== $existing_type_part->param_name
749
                    && $new_type_part->as->hasObject()
750
                    && $existing_type_part->as->hasObject()
751
                ) {
752
                    $new_type_part->extra_types[$existing_type_part->getKey()] = $existing_type_part;
753
                    $matching_atomic_types[] = $new_type_part;
754
                    $has_local_match = true;
755
756
                    continue;
757
                }
758
759
                if (($new_type_part instanceof Type\Atomic\TGenericObject
760
                        || $new_type_part instanceof Type\Atomic\TArray
761
                        || $new_type_part instanceof Type\Atomic\TIterable)
762
                    && ($existing_type_part instanceof Type\Atomic\TGenericObject
763
                        || $existing_type_part instanceof Type\Atomic\TArray
764
                        || $existing_type_part instanceof Type\Atomic\TIterable)
765
                    && count($new_type_part->type_params) === count($existing_type_part->type_params)
766
                ) {
767
                    $has_any_param_match = false;
768
769
                    foreach ($new_type_part->type_params as $i => $new_param) {
770
                        $existing_param = $existing_type_part->type_params[$i];
771
772
                        $has_param_match = true;
773
774
                        $new_param = self::filterTypeWithAnother(
775
                            $codebase,
776
                            $existing_param,
777
                            $new_param,
778
                            $template_type_map,
779
                            $has_param_match,
780
                            $any_scalar_type_match_found
781
                        );
782
783
                        if ($template_type_map) {
784
                            $new_param->replaceTemplateTypesWithArgTypes(
785
                                new TemplateResult([], $template_type_map),
786
                                $codebase
787
                            );
788
                        }
789
790
                        $existing_type->bustCache();
791
792
                        if ($has_param_match
793
                            && $existing_type_part->type_params[$i]->getId() !== $new_param->getId()
794
                        ) {
795
                            $existing_type_part->type_params[$i] = $new_param;
796
797
                            if (!$has_local_match) {
798
                                $has_any_param_match = true;
799
                            }
800
                        }
801
                    }
802
803
                    if ($has_any_param_match) {
804
                        $has_local_match = true;
805
                        $matching_atomic_types[] = $existing_type_part;
806
                        $atomic_comparison_results->type_coerced = true;
807
                    }
808
                }
809
810
                if (($new_type_part instanceof Type\Atomic\TArray
811
                        || $new_type_part instanceof Type\Atomic\TIterable)
812
                    && $existing_type_part instanceof Type\Atomic\TList
813
                ) {
814
                    $has_any_param_match = false;
815
816
                    $new_param = $new_type_part->type_params[1];
817
                    $existing_param = $existing_type_part->type_param;
818
819
                    $has_param_match = true;
820
821
                    $new_param = self::filterTypeWithAnother(
822
                        $codebase,
823
                        $existing_param,
824
                        $new_param,
825
                        $template_type_map,
826
                        $has_param_match,
827
                        $any_scalar_type_match_found
828
                    );
829
830
                    if ($template_type_map) {
831
                        $new_param->replaceTemplateTypesWithArgTypes(
832
                            new TemplateResult([], $template_type_map),
833
                            $codebase
834
                        );
835
                    }
836
837
                    $existing_type->bustCache();
838
839
                    if ($has_param_match
840
                        && $existing_type_part->type_param->getId() !== $new_param->getId()
841
                    ) {
842
                        $existing_type_part->type_param = $new_param;
843
844
                        if (!$has_local_match) {
845
                            $has_any_param_match = true;
846
                        }
847
                    }
848
849
                    if ($has_any_param_match) {
850
                        $has_local_match = true;
851
                        $matching_atomic_types[] = $existing_type_part;
852
                        $atomic_comparison_results->type_coerced = true;
853
                    }
854
                }
855
856
                if ($atomic_contained_by || $atomic_comparison_results->type_coerced) {
857
                    if ($atomic_contained_by
858
                        && $existing_type_part instanceof TNamedObject
859
                        && $new_type_part instanceof TNamedObject
860
                        && $existing_type_part->extra_types
861
                        && !$codebase->classExists($existing_type_part->value)
862
                        && !$codebase->classExists($new_type_part->value)
863
                        && !array_filter(
864
                            $existing_type_part->extra_types,
865
                            function ($extra_type) use ($codebase) {
866
                                return $extra_type instanceof TNamedObject
867
                                    && $codebase->classExists($extra_type->value);
868
                            }
869
                        )
870
                    ) {
871
                        if (!$has_cloned_type) {
872
                            $new_type = clone $new_type;
873
                            $has_cloned_type = true;
874
                        }
875
876
                        $new_type->removeType($key);
877
                        $new_type->addType($existing_type_part);
878
                        $new_type->from_docblock = $existing_type_part->from_docblock;
879
                    }
880
881
                    continue;
882
                }
883
884
                if ($atomic_comparison_results->scalar_type_match_found) {
885
                    $any_scalar_type_match_found = true;
886
                }
887
            }
888
889
            if (!$has_local_match) {
890
                $has_match = false;
891
                break;
892
            }
893
        }
894
895
        if ($matching_atomic_types) {
896
            return new Type\Union($matching_atomic_types);
897
        }
898
899
        return $new_type;
900
    }
901
902
    /**
903
     * @param  string[]   $suppressed_issues
904
     */
905
    private static function handleLiteralEquality(
906
        string $assertion,
907
        int $bracket_pos,
908
        bool $is_loose_equality,
909
        Type\Union $existing_var_type,
910
        string $old_var_type_string,
911
        ?string $var_id,
912
        ?CodeLocation $code_location,
913
        array $suppressed_issues
914
    ) : Type\Union {
915
        $value = substr($assertion, $bracket_pos + 1, -1);
916
917
        $scalar_type = substr($assertion, 0, $bracket_pos);
918
919
        $existing_var_atomic_types = $existing_var_type->getAtomicTypes();
920
921
        if ($scalar_type === 'int') {
922
            $value = (int) $value;
923
924
            if ($existing_var_type->hasMixed()
925
                || $existing_var_type->hasScalar()
926
                || $existing_var_type->hasNumeric()
927
                || $existing_var_type->hasArrayKey()
928
            ) {
929
                if ($is_loose_equality) {
930
                    return $existing_var_type;
931
                }
932
933
                return new Type\Union([new Type\Atomic\TLiteralInt($value)]);
934
            }
935
936
            $has_int = false;
937
938
            foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
939
                if ($existing_var_atomic_type instanceof TInt) {
940
                    $has_int = true;
941
                } elseif ($existing_var_atomic_type instanceof TTemplateParam) {
942
                    if ($existing_var_atomic_type->as->hasMixed()
943
                        || $existing_var_atomic_type->as->hasScalar()
944
                        || $existing_var_atomic_type->as->hasNumeric()
945
                        || $existing_var_atomic_type->as->hasArrayKey()
946
                    ) {
947
                        if ($is_loose_equality) {
948
                            return $existing_var_type;
949
                        }
950
951
                        return new Type\Union([new Type\Atomic\TLiteralInt($value)]);
952
                    }
953
954
                    if ($existing_var_atomic_type->as->hasInt()) {
955
                        $has_int = true;
956
                    }
957
                }
958
            }
959
960
            if ($has_int) {
961
                $existing_int_types = $existing_var_type->getLiteralInts();
962
963
                if ($existing_int_types) {
964
                    $can_be_equal = false;
965
                    $did_remove_type = false;
966
967
                    foreach ($existing_var_atomic_types as $atomic_key => $_) {
968
                        if ($atomic_key !== $assertion) {
969
                            $existing_var_type->removeType($atomic_key);
970
                            $did_remove_type = true;
971
                        } else {
972
                            $can_be_equal = true;
973
                        }
974
                    }
975
976
                    if ($var_id
977
                        && $code_location
978
                        && (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
979
                    ) {
980
                        self::triggerIssueForImpossible(
981
                            $existing_var_type,
982
                            $old_var_type_string,
983
                            $var_id,
984
                            $assertion,
985
                            $can_be_equal,
986
                            $code_location,
987
                            $suppressed_issues
988
                        );
989
                    }
990
                } else {
991
                    $existing_var_type = new Type\Union([new Type\Atomic\TLiteralInt($value)]);
992
                }
993
            } elseif ($var_id && $code_location && !$is_loose_equality) {
994
                self::triggerIssueForImpossible(
995
                    $existing_var_type,
996
                    $old_var_type_string,
997
                    $var_id,
998
                    $assertion,
999
                    false,
1000
                    $code_location,
1001
                    $suppressed_issues
1002
                );
1003
            } elseif ($is_loose_equality && $existing_var_type->hasFloat()) {
1004
                // convert floats to ints
1005
                $existing_float_types = $existing_var_type->getLiteralFloats();
1006
1007
                if ($existing_float_types) {
1008
                    $can_be_equal = false;
1009
                    $did_remove_type = false;
1010
1011
                    foreach ($existing_var_atomic_types as $atomic_key => $_) {
1012
                        if (substr($atomic_key, 0, 6) === 'float(') {
1013
                            $atomic_key = 'int(' . substr($atomic_key, 6);
1014
                        }
1015
                        if ($atomic_key !== $assertion) {
1016
                            $existing_var_type->removeType($atomic_key);
1017
                            $did_remove_type = true;
1018
                        } else {
1019
                            $can_be_equal = true;
1020
                        }
1021
                    }
1022
1023
                    if ($var_id
1024
                        && $code_location
1025
                        && (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
1026
                    ) {
1027
                        self::triggerIssueForImpossible(
1028
                            $existing_var_type,
1029
                            $old_var_type_string,
1030
                            $var_id,
1031
                            $assertion,
1032
                            $can_be_equal,
1033
                            $code_location,
1034
                            $suppressed_issues
1035
                        );
1036
                    }
1037
                }
1038
            }
1039
        } elseif ($scalar_type === 'string'
1040
            || $scalar_type === 'class-string'
1041
            || $scalar_type === 'interface-string'
1042
            || $scalar_type === 'callable-string'
1043
            || $scalar_type === 'trait-string'
1044
        ) {
1045
            if ($existing_var_type->hasMixed()
1046
                || $existing_var_type->hasScalar()
1047
                || $existing_var_type->hasArrayKey()
1048
            ) {
1049
                if ($is_loose_equality) {
1050
                    return $existing_var_type;
1051
                }
1052
1053
                if ($scalar_type === 'class-string'
1054
                    || $scalar_type === 'interface-string'
1055
                    || $scalar_type === 'trait-string'
1056
                ) {
1057
                    return new Type\Union([new Type\Atomic\TLiteralClassString($value)]);
1058
                }
1059
1060
                return new Type\Union([new Type\Atomic\TLiteralString($value)]);
1061
            }
1062
1063
            $has_string = false;
1064
1065
            foreach ($existing_var_atomic_types as $existing_var_atomic_type) {
1066
                if ($existing_var_atomic_type instanceof TString) {
1067
                    $has_string = true;
1068
                } elseif ($existing_var_atomic_type instanceof TTemplateParam) {
1069
                    if ($existing_var_atomic_type->as->hasMixed()
1070
                        || $existing_var_atomic_type->as->hasString()
1071
                        || $existing_var_atomic_type->as->hasScalar()
1072
                        || $existing_var_atomic_type->as->hasArrayKey()
1073
                    ) {
1074
                        if ($is_loose_equality) {
1075
                            return $existing_var_type;
1076
                        }
1077
1078
                        $existing_var_atomic_type = clone $existing_var_atomic_type;
1079
1080
                        $existing_var_atomic_type->as = self::handleLiteralEquality(
1081
                            $assertion,
1082
                            $bracket_pos,
1083
                            $is_loose_equality,
1084
                            $existing_var_atomic_type->as,
1085
                            $old_var_type_string,
1086
                            $var_id,
1087
                            $code_location,
1088
                            $suppressed_issues
1089
                        );
1090
1091
                        return new Type\Union([$existing_var_atomic_type]);
1092
                    }
1093
1094
                    if ($existing_var_atomic_type->as->hasString()) {
1095
                        $has_string = true;
1096
                    }
1097
                }
1098
            }
1099
1100
            if ($has_string) {
1101
                $existing_string_types = $existing_var_type->getLiteralStrings();
1102
1103
                if ($existing_string_types) {
1104
                    $can_be_equal = false;
1105
                    $did_remove_type = false;
1106
1107
                    foreach ($existing_var_atomic_types as $atomic_key => $_) {
1108
                        if ($atomic_key !== $assertion) {
1109
                            $existing_var_type->removeType($atomic_key);
1110
                            $did_remove_type = true;
1111
                        } else {
1112
                            $can_be_equal = true;
1113
                        }
1114
                    }
1115
1116
                    if ($var_id
1117
                        && $code_location
1118
                        && (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
1119
                    ) {
1120
                        self::triggerIssueForImpossible(
1121
                            $existing_var_type,
1122
                            $old_var_type_string,
1123
                            $var_id,
1124
                            $assertion,
1125
                            $can_be_equal,
1126
                            $code_location,
1127
                            $suppressed_issues
1128
                        );
1129
                    }
1130
                } else {
1131
                    if ($scalar_type === 'class-string'
1132
                        || $scalar_type === 'interface-string'
1133
                        || $scalar_type === 'trait-string'
1134
                    ) {
1135
                        $existing_var_type = new Type\Union([new Type\Atomic\TLiteralClassString($value)]);
1136
                    } else {
1137
                        $existing_var_type = new Type\Union([new Type\Atomic\TLiteralString($value)]);
1138
                    }
1139
                }
1140
            } elseif ($var_id && $code_location && !$is_loose_equality) {
1141
                self::triggerIssueForImpossible(
1142
                    $existing_var_type,
1143
                    $old_var_type_string,
1144
                    $var_id,
1145
                    $assertion,
1146
                    false,
1147
                    $code_location,
1148
                    $suppressed_issues
1149
                );
1150
            }
1151
        } elseif ($scalar_type === 'float') {
1152
            $value = (float) $value;
1153
1154
            if ($existing_var_type->hasMixed() || $existing_var_type->hasScalar() || $existing_var_type->hasNumeric()) {
1155
                if ($is_loose_equality) {
1156
                    return $existing_var_type;
1157
                }
1158
1159
                return new Type\Union([new Type\Atomic\TLiteralFloat($value)]);
1160
            }
1161
1162
            if ($existing_var_type->hasFloat()) {
1163
                $existing_float_types = $existing_var_type->getLiteralFloats();
1164
1165
                if ($existing_float_types) {
1166
                    $can_be_equal = false;
1167
                    $did_remove_type = false;
1168
1169
                    foreach ($existing_var_atomic_types as $atomic_key => $_) {
1170
                        if ($atomic_key !== $assertion) {
1171
                            $existing_var_type->removeType($atomic_key);
1172
                            $did_remove_type = true;
1173
                        } else {
1174
                            $can_be_equal = true;
1175
                        }
1176
                    }
1177
1178
                    if ($var_id
1179
                        && $code_location
1180
                        && (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
1181
                    ) {
1182
                        self::triggerIssueForImpossible(
1183
                            $existing_var_type,
1184
                            $old_var_type_string,
1185
                            $var_id,
1186
                            $assertion,
1187
                            $can_be_equal,
1188
                            $code_location,
1189
                            $suppressed_issues
1190
                        );
1191
                    }
1192
                } else {
1193
                    $existing_var_type = new Type\Union([new Type\Atomic\TLiteralFloat($value)]);
1194
                }
1195
            } elseif ($var_id && $code_location && !$is_loose_equality) {
1196
                self::triggerIssueForImpossible(
1197
                    $existing_var_type,
1198
                    $old_var_type_string,
1199
                    $var_id,
1200
                    $assertion,
1201
                    false,
1202
                    $code_location,
1203
                    $suppressed_issues
1204
                );
1205
            } elseif ($is_loose_equality && $existing_var_type->hasInt()) {
1206
                // convert ints to floats
1207
                $existing_float_types = $existing_var_type->getLiteralInts();
1208
1209
                if ($existing_float_types) {
1210
                    $can_be_equal = false;
1211
                    $did_remove_type = false;
1212
1213
                    foreach ($existing_var_atomic_types as $atomic_key => $_) {
1214
                        if (substr($atomic_key, 0, 4) === 'int(') {
1215
                            $atomic_key = 'float(' . substr($atomic_key, 4);
1216
                        }
1217
                        if ($atomic_key !== $assertion) {
1218
                            $existing_var_type->removeType($atomic_key);
1219
                            $did_remove_type = true;
1220
                        } else {
1221
                            $can_be_equal = true;
1222
                        }
1223
                    }
1224
1225
                    if ($var_id
1226
                        && $code_location
1227
                        && (!$can_be_equal || (!$did_remove_type && count($existing_var_atomic_types) === 1))
1228
                    ) {
1229
                        self::triggerIssueForImpossible(
1230
                            $existing_var_type,
1231
                            $old_var_type_string,
1232
                            $var_id,
1233
                            $assertion,
1234
                            $can_be_equal,
1235
                            $code_location,
1236
                            $suppressed_issues
1237
                        );
1238
                    }
1239
                }
1240
            }
1241
        }
1242
1243
        return $existing_var_type;
1244
    }
1245
}
1246