AssertionReconciler::reconcile()   F
last analyzed

Complexity

Conditions 56
Paths 4608

Size

Total Lines 290

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 56
nc 4608
nop 9
dl 0
loc 290
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
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