CallAnalyzer::collectSpecialInformation()   F
last analyzed

Complexity

Conditions 40
Paths 1338

Size

Total Lines 175

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 40
nc 1338
nop 3
dl 0
loc 175
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
6
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
7
use Psalm\Internal\Analyzer\StatementsAnalyzer;
8
use Psalm\Internal\Analyzer\TypeAnalyzer;
9
use Psalm\Internal\Type\TemplateResult;
10
use Psalm\CodeLocation;
11
use Psalm\Context;
12
use Psalm\Issue\InvalidArgument;
13
use Psalm\Issue\InvalidScalarArgument;
14
use Psalm\Issue\MixedArgumentTypeCoercion;
15
use Psalm\Issue\ArgumentTypeCoercion;
16
use Psalm\Issue\UndefinedFunction;
17
use Psalm\IssueBuffer;
18
use Psalm\Storage\ClassLikeStorage;
19
use Psalm\Type;
20
use Psalm\Type\Atomic\TNamedObject;
21
use function strtolower;
22
use function strpos;
23
use function count;
24
use function in_array;
25
use function is_null;
26
use function is_string;
27
use function preg_match;
28
use function preg_replace;
29
use function str_replace;
30
use function is_int;
31
use function substr;
32
use function array_merge;
33
34
/**
35
 * @internal
36
 */
37
class CallAnalyzer
38
{
39
    /**
40
     * @param   FunctionLikeAnalyzer $source
41
     * @param   string              $method_name
42
     * @param   Context             $context
43
     *
44
     * @return  void
45
     */
46
    public static function collectSpecialInformation(
47
        FunctionLikeAnalyzer $source,
48
        $method_name,
49
        Context $context
50
    ) {
51
        $fq_class_name = (string)$source->getFQCLN();
52
53
        $project_analyzer = $source->getFileAnalyzer()->project_analyzer;
54
        $codebase = $source->getCodebase();
55
56
        if ($context->collect_mutations &&
57
            $context->self &&
58
            (
59
                $context->self === $fq_class_name ||
60
                $codebase->classExtends(
61
                    $context->self,
62
                    $fq_class_name
63
                )
64
            )
65
        ) {
66
            $method_id = new \Psalm\Internal\MethodIdentifier(
67
                $fq_class_name,
68
                strtolower($method_name)
69
            );
70
71
            if ((string) $method_id !== $source->getId()) {
72
                if ($context->collect_initializations) {
73
                    if (isset($context->initialized_methods[(string) $method_id])) {
74
                        return;
75
                    }
76
77
                    if ($context->initialized_methods === null) {
78
                        $context->initialized_methods = [];
79
                    }
80
81
                    $context->initialized_methods[(string) $method_id] = true;
82
                }
83
84
                $project_analyzer->getMethodMutations(
85
                    $method_id,
86
                    $context,
87
                    $source->getRootFilePath(),
88
                    $source->getRootFileName()
89
                );
90
            }
91
        } elseif ($context->collect_initializations &&
92
            $context->self &&
93
            (
94
                $context->self === $fq_class_name
95
                || $codebase->classlikes->classExtends(
96
                    $context->self,
97
                    $fq_class_name
98
                )
99
            ) &&
100
            $source->getMethodName() !== $method_name
101
        ) {
102
            $method_id = new \Psalm\Internal\MethodIdentifier($fq_class_name, strtolower($method_name));
103
104
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
105
106
            if (isset($context->vars_in_scope['$this'])) {
107
                foreach ($context->vars_in_scope['$this']->getAtomicTypes() as $atomic_type) {
108
                    if ($atomic_type instanceof TNamedObject) {
109
                        if ($fq_class_name === $atomic_type->value) {
110
                            $alt_declaring_method_id = $declaring_method_id;
111
                        } else {
112
                            $fq_class_name = $atomic_type->value;
113
114
                            $method_id = new \Psalm\Internal\MethodIdentifier(
115
                                $fq_class_name,
116
                                strtolower($method_name)
117
                            );
118
119
                            $alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
120
                        }
121
122
                        if ($alt_declaring_method_id) {
123
                            $declaring_method_id = $alt_declaring_method_id;
124
                            break;
125
                        }
126
127
                        if (!$atomic_type->extra_types) {
128
                            continue;
129
                        }
130
131
                        foreach ($atomic_type->extra_types as $intersection_type) {
132
                            if ($intersection_type instanceof TNamedObject) {
133
                                $fq_class_name = $intersection_type->value;
134
                                $method_id = new \Psalm\Internal\MethodIdentifier(
135
                                    $fq_class_name,
136
                                    strtolower($method_name)
137
                                );
138
139
                                $alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
140
141
                                if ($alt_declaring_method_id) {
142
                                    $declaring_method_id = $alt_declaring_method_id;
143
                                    break 2;
144
                                }
145
                            }
146
                        }
147
                    }
148
                }
149
            }
150
151
            if (!$declaring_method_id) {
152
                // can happen for __call
153
                return;
154
            }
155
156
            if (isset($context->initialized_methods[(string) $declaring_method_id])) {
157
                return;
158
            }
159
160
            if ($context->initialized_methods === null) {
161
                $context->initialized_methods = [];
162
            }
163
164
            $context->initialized_methods[(string) $declaring_method_id] = true;
165
166
            $method_storage = $codebase->methods->getStorage($declaring_method_id);
167
168
            $class_analyzer = $source->getSource();
169
170
            if ($class_analyzer instanceof ClassLikeAnalyzer
171
                && !$method_storage->is_static
172
                && ($context->collect_nonprivate_initializations
173
                    || $method_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
174
                    || $method_storage->final)
175
            ) {
176
                $local_vars_in_scope = [];
177
                $local_vars_possibly_in_scope = [];
178
179
                foreach ($context->vars_in_scope as $var => $_) {
180
                    if (strpos($var, '$this->') !== 0 && $var !== '$this') {
181
                        $local_vars_in_scope[$var] = $context->vars_in_scope[$var];
182
                    }
183
                }
184
185
                foreach ($context->vars_possibly_in_scope as $var => $_) {
186
                    if (strpos($var, '$this->') !== 0 && $var !== '$this') {
187
                        $local_vars_possibly_in_scope[$var] = $context->vars_possibly_in_scope[$var];
188
                    }
189
                }
190
191
                $old_calling_method_id = $context->calling_method_id;
192
193
                if ($fq_class_name === $source->getFQCLN()) {
194
                    $class_analyzer->getMethodMutations(strtolower($method_name), $context);
195
                } else {
196
                    $declaring_fq_class_name = $declaring_method_id->fq_class_name;
197
198
                    $old_self = $context->self;
199
                    $context->self = $declaring_fq_class_name;
200
                    $project_analyzer->getMethodMutations(
201
                        $declaring_method_id,
202
                        $context,
203
                        $source->getRootFilePath(),
204
                        $source->getRootFileName()
205
                    );
206
                    $context->self = $old_self;
207
                }
208
209
                $context->calling_method_id = $old_calling_method_id;
210
211
                foreach ($local_vars_in_scope as $var => $type) {
212
                    $context->vars_in_scope[$var] = $type;
213
                }
214
215
                foreach ($local_vars_possibly_in_scope as $var => $_) {
216
                    $context->vars_possibly_in_scope[$var] = true;
217
                }
218
            }
219
        }
220
    }
221
222
    /**
223
     * @param  array<int, PhpParser\Node\Arg>   $args
224
     * @param  Context                          $context
225
     * @param  CodeLocation                     $code_location
226
     * @param  StatementsAnalyzer               $statements_analyzer
227
     */
228
    protected static function checkMethodArgs(
229
        ?\Psalm\Internal\MethodIdentifier $method_id,
230
        array $args,
231
        ?TemplateResult $class_template_result,
232
        Context $context,
233
        CodeLocation $code_location,
234
        StatementsAnalyzer $statements_analyzer
235
    ) : bool {
236
        $codebase = $statements_analyzer->getCodebase();
237
238
        $method_params = $method_id
239
            ? $codebase->methods->getMethodParams($method_id, $statements_analyzer, $args, $context)
240
            : null;
241
242
        if (Call\ArgumentsAnalyzer::analyze(
243
            $statements_analyzer,
244
            $args,
245
            $method_params,
246
            (string) $method_id,
247
            $context,
248
            $class_template_result
249
        ) === false) {
250
            return false;
251
        }
252
253
        if (!$method_id || $method_params === null) {
254
            return true;
255
        }
256
257
        $fq_class_name = $method_id->fq_class_name;
258
        $method_name = $method_id->method_name;
259
260
        $fq_class_name = strtolower($codebase->classlikes->getUnAliasedName($fq_class_name));
261
262
        $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
263
264
        $method_storage = null;
265
266
        if (isset($class_storage->declaring_method_ids[$method_name])) {
267
            $declaring_method_id = $class_storage->declaring_method_ids[$method_name];
268
269
            $declaring_fq_class_name = $declaring_method_id->fq_class_name;
270
            $declaring_method_name = $declaring_method_id->method_name;
271
272
            if ($declaring_fq_class_name !== $fq_class_name) {
273
                $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_fq_class_name);
274
            } else {
275
                $declaring_class_storage = $class_storage;
276
            }
277
278
            if (!isset($declaring_class_storage->methods[$declaring_method_name])) {
279
                throw new \UnexpectedValueException('Storage should not be empty here');
280
            }
281
282
            $method_storage = $declaring_class_storage->methods[$declaring_method_name];
283
284
            if ($declaring_class_storage->user_defined
285
                && !$method_storage->has_docblock_param_types
286
                && isset($declaring_class_storage->documenting_method_ids[$method_name])
287
            ) {
288
                $documenting_method_id = $declaring_class_storage->documenting_method_ids[$method_name];
289
290
                $documenting_method_storage = $codebase->methods->getStorage($documenting_method_id);
291
292
                if ($documenting_method_storage->template_types) {
293
                    $method_storage = $documenting_method_storage;
294
                }
295
            }
296
297
            if (!$context->isSuppressingExceptions($statements_analyzer)) {
298
                $context->mergeFunctionExceptions($method_storage, $code_location);
299
            }
300
        }
301
302
        if (Call\ArgumentsAnalyzer::checkArgumentsMatch(
303
            $statements_analyzer,
304
            $args,
305
            $method_id,
306
            $method_params,
307
            $method_storage,
308
            $class_storage,
309
            $class_template_result,
310
            $code_location,
311
            $context
312
        ) === false) {
313
            return false;
314
        }
315
316
        if ($class_template_result) {
317
            self::checkTemplateResult(
318
                $statements_analyzer,
319
                $class_template_result,
320
                $code_location,
321
                strtolower((string) $method_id)
322
            );
323
        }
324
325
        return true;
326
    }
327
328
    /**
329
     * @return array<string, array<string, array{Type\Union}>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (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...
330
     * @param array<string, non-empty-array<string, array{Type\Union}>> $existing_template_types
331
     */
332
    public static function getTemplateTypesForCall(
333
        \Psalm\Codebase $codebase,
334
        ?ClassLikeStorage $declaring_class_storage,
335
        ?string $appearing_class_name,
336
        ?ClassLikeStorage $calling_class_storage,
337
        array $existing_template_types = []
338
    ) : array {
339
        $template_types = $existing_template_types;
340
341
        if ($declaring_class_storage) {
342
            if ($calling_class_storage
343
                && $declaring_class_storage !== $calling_class_storage
344
                && $calling_class_storage->template_type_extends
345
            ) {
346
                foreach ($calling_class_storage->template_type_extends as $class_name => $type_map) {
347
                    foreach ($type_map as $template_name => $type) {
348
                        if (is_string($template_name) && $class_name === $declaring_class_storage->name) {
349
                            $output_type = null;
350
351
                            foreach ($type->getAtomicTypes() as $atomic_type) {
352
                                if ($atomic_type instanceof Type\Atomic\TTemplateParam
353
                                    && isset(
354
                                        $calling_class_storage
355
                                            ->template_type_extends
356
                                                [$atomic_type->defining_class]
357
                                                [$atomic_type->param_name]
358
                                    )
359
                                ) {
360
                                    $output_type_candidate = $calling_class_storage
361
                                        ->template_type_extends
362
                                            [$atomic_type->defining_class]
363
                                            [$atomic_type->param_name];
364
                                } elseif ($atomic_type instanceof Type\Atomic\TTemplateParam) {
365
                                    $output_type_candidate = $atomic_type->as;
366
                                } else {
367
                                    $output_type_candidate = new Type\Union([$atomic_type]);
368
                                }
369
370
                                if (!$output_type) {
371
                                    $output_type = $output_type_candidate;
372
                                } else {
373
                                    $output_type = Type::combineUnionTypes(
374
                                        $output_type_candidate,
375
                                        $output_type
376
                                    );
377
                                }
378
                            }
379
380
                            $template_types[$template_name][$declaring_class_storage->name] = [$output_type];
381
                        }
382
                    }
383
                }
384
            } elseif ($declaring_class_storage->template_types) {
385
                foreach ($declaring_class_storage->template_types as $template_name => $type_map) {
386
                    foreach ($type_map as $key => list($type)) {
387
                        $template_types[$template_name][$key] = [$type];
388
                    }
389
                }
390
            }
391
        }
392
393
        foreach ($template_types as $key => $type_map) {
394
            foreach ($type_map as $class => $type) {
395
                $template_types[$key][$class][0] = \Psalm\Internal\Type\TypeExpander::expandUnion(
396
                    $codebase,
397
                    $type[0],
398
                    $appearing_class_name,
399
                    $calling_class_storage ? $calling_class_storage->name : null,
400
                    null,
401
                    true,
402
                    false,
403
                    $calling_class_storage ? $calling_class_storage->final : false
404
                );
405
            }
406
        }
407
408
        return $template_types;
409
    }
410
411
    /**
412
     * @param  PhpParser\Node\Scalar\String_|PhpParser\Node\Expr\Array_|PhpParser\Node\Expr\BinaryOp\Concat
413
     *         $callable_arg
414
     *
415
     * @return non-empty-string[]
0 ignored issues
show
Documentation introduced by
The doc-type non-empty-string[] could not be parsed: Unknown type name "non-empty-string" 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...
416
     *
417
     * @psalm-suppress LessSpecificReturnStatement
418
     * @psalm-suppress MoreSpecificReturnType
419
     */
420
    public static function getFunctionIdsFromCallableArg(
421
        \Psalm\FileSource $file_source,
422
        $callable_arg
423
    ) {
424
        if ($callable_arg instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
425
            if ($callable_arg->left instanceof PhpParser\Node\Expr\ClassConstFetch
426
                && $callable_arg->left->class instanceof PhpParser\Node\Name
427
                && $callable_arg->left->name instanceof PhpParser\Node\Identifier
428
                && strtolower($callable_arg->left->name->name) === 'class'
429
                && !in_array(strtolower($callable_arg->left->class->parts[0]), ['self', 'static', 'parent'])
430
                && $callable_arg->right instanceof PhpParser\Node\Scalar\String_
431
                && preg_match('/^::[A-Za-z0-9]+$/', $callable_arg->right->value)
432
            ) {
433
                return [
434
                    (string) $callable_arg->left->class->getAttribute('resolvedName') . $callable_arg->right->value
435
                ];
436
            }
437
438
            return [];
439
        }
440
441
        if ($callable_arg instanceof PhpParser\Node\Scalar\String_) {
442
            $potential_id = preg_replace('/^\\\/', '', $callable_arg->value);
443
444
            if (preg_match('/^[A-Za-z0-9_]+(\\\[A-Za-z0-9_]+)*(::[A-Za-z0-9_]+)?$/', $potential_id)) {
445
                return [$potential_id];
446
            }
447
448
            return [];
449
        }
450
451
        if (count($callable_arg->items) !== 2) {
452
            return [];
453
        }
454
455
        /** @psalm-suppress PossiblyNullPropertyFetch */
456
        if ($callable_arg->items[0]->key || $callable_arg->items[1]->key) {
457
            return [];
458
        }
459
460
        if (!isset($callable_arg->items[0]) || !isset($callable_arg->items[1])) {
461
            throw new \UnexpectedValueException('These should never be unset');
462
        }
463
464
        $class_arg = $callable_arg->items[0]->value;
465
        $method_name_arg = $callable_arg->items[1]->value;
466
467
        if (!$method_name_arg instanceof PhpParser\Node\Scalar\String_) {
468
            return [];
469
        }
470
471
        if ($class_arg instanceof PhpParser\Node\Scalar\String_) {
472
            return [preg_replace('/^\\\/', '', $class_arg->value) . '::' . $method_name_arg->value];
473
        }
474
475
        if ($class_arg instanceof PhpParser\Node\Expr\ClassConstFetch
476
            && $class_arg->name instanceof PhpParser\Node\Identifier
477
            && strtolower($class_arg->name->name) === 'class'
478
            && $class_arg->class instanceof PhpParser\Node\Name
479
        ) {
480
            $fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
481
                $class_arg->class,
482
                $file_source->getAliases()
483
            );
484
485
            return [$fq_class_name . '::' . $method_name_arg->value];
486
        }
487
488
        $class_arg_type = null;
489
490
        if (!$file_source instanceof StatementsAnalyzer
491
            || !($class_arg_type = $file_source->node_data->getType($class_arg))
492
        ) {
493
            return [];
494
        }
495
496
        $method_ids = [];
497
498
        foreach ($class_arg_type->getAtomicTypes() as $type_part) {
499
            if ($type_part instanceof TNamedObject) {
500
                $method_id = $type_part->value . '::' . $method_name_arg->value;
501
502
                if ($type_part->extra_types) {
503
                    foreach ($type_part->extra_types as $extra_type) {
504
                        if ($extra_type instanceof Type\Atomic\TTemplateParam
505
                            || $extra_type instanceof Type\Atomic\TObjectWithProperties
506
                        ) {
507
                            throw new \UnexpectedValueException('Shouldn’t get a generic param here');
508
                        }
509
510
                        $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value;
511
                    }
512
                }
513
514
                $method_ids[] = '$' . $method_id;
515
            }
516
        }
517
518
        return $method_ids;
519
    }
520
521
    /**
522
     * @param  StatementsAnalyzer   $statements_analyzer
523
     * @param  non-empty-string     $function_id
0 ignored issues
show
Documentation introduced by
The doc-type non-empty-string could not be parsed: Unknown type name "non-empty-string" 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...
524
     * @param  CodeLocation         $code_location
525
     * @param  bool                 $can_be_in_root_scope if true, the function can be shortened to the root version
526
     *
527
     * @return bool
528
     */
529
    public static function checkFunctionExists(
530
        StatementsAnalyzer $statements_analyzer,
531
        &$function_id,
532
        CodeLocation $code_location,
533
        $can_be_in_root_scope
534
    ) {
535
        $cased_function_id = $function_id;
536
        $function_id = strtolower($function_id);
537
538
        $codebase = $statements_analyzer->getCodebase();
539
540
        if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) {
541
            /** @var non-empty-lowercase-string */
542
            $root_function_id = preg_replace('/.*\\\/', '', $function_id);
543
544
            if ($can_be_in_root_scope
545
                && $function_id !== $root_function_id
546
                && $codebase->functions->functionExists($statements_analyzer, $root_function_id)
547
            ) {
548
                $function_id = $root_function_id;
549
            } else {
550
                if (IssueBuffer::accepts(
551
                    new UndefinedFunction(
552
                        'Function ' . $cased_function_id . ' does not exist',
553
                        $code_location,
554
                        $function_id
555
                    ),
556
                    $statements_analyzer->getSuppressedIssues()
557
                )) {
558
                    // fall through
559
                }
560
561
                return false;
562
            }
563
        }
564
565
        return true;
566
    }
567
568
    /**
569
     * @param PhpParser\Node\Identifier|PhpParser\Node\Name $expr
570
     * @param  \Psalm\Storage\Assertion[] $assertions
571
     * @param  string $thisName
572
     * @param  array<int, PhpParser\Node\Arg> $args
573
     * @param  Context           $context
574
     * @param  array<string, array<string, array{Type\Union}>> $template_type_map,
575
     * @param  StatementsAnalyzer $statements_analyzer
576
     *
577
     * @return void
578
     */
579
    protected static function applyAssertionsToContext(
580
        $expr,
581
        ?string $thisName,
582
        array $assertions,
583
        array $args,
584
        array $template_type_map,
585
        Context $context,
586
        StatementsAnalyzer $statements_analyzer
587
    ) {
588
        $type_assertions = [];
589
590
        $asserted_keys = [];
591
592
        foreach ($assertions as $assertion) {
593
            $assertion_var_id = null;
594
595
            $arg_value = null;
596
597
            if (is_int($assertion->var_id)) {
598
                if (!isset($args[$assertion->var_id])) {
599
                    continue;
600
                }
601
602
                $arg_value = $args[$assertion->var_id]->value;
603
604
                $arg_var_id = ExpressionIdentifier::getArrayVarId($arg_value, null, $statements_analyzer);
605
606
                if ($arg_var_id) {
607
                    $assertion_var_id = $arg_var_id;
608
                }
609
            } elseif ($assertion->var_id === '$this' && !is_null($thisName)) {
610
                $assertion_var_id = $thisName;
611
            } elseif (strpos($assertion->var_id, '$this->') === 0 && !is_null($thisName)) {
612
                $assertion_var_id = $thisName . str_replace('$this->', '->', $assertion->var_id);
613
            } elseif (isset($context->vars_in_scope[$assertion->var_id])) {
614
                $assertion_var_id = $assertion->var_id;
615
            }
616
617
            if ($assertion_var_id) {
618
                $rule = $assertion->rule[0][0];
619
620
                $prefix = '';
621
                if ($rule[0] === '!') {
622
                    $prefix .= '!';
623
                    $rule = substr($rule, 1);
624
                }
625
                if ($rule[0] === '=') {
626
                    $prefix .= '=';
627
                    $rule = substr($rule, 1);
628
                }
629
                if ($rule[0] === '~') {
630
                    $prefix .= '~';
631
                    $rule = substr($rule, 1);
632
                }
633
634
                if (isset($template_type_map[$rule])) {
635
                    foreach ($template_type_map[$rule] as $template_map) {
636
                        if ($template_map[0]->hasMixed()) {
637
                            continue 2;
638
                        }
639
640
                        $replacement_atomic_types = $template_map[0]->getAtomicTypes();
641
642
                        if (count($replacement_atomic_types) > 1) {
643
                            continue 2;
644
                        }
645
646
                        $ored_type_assertions = [];
647
648
                        foreach ($replacement_atomic_types as $replacement_atomic_type) {
649
                            if ($replacement_atomic_type instanceof Type\Atomic\TMixed) {
650
                                continue 3;
651
                            }
652
653
                            if ($replacement_atomic_type instanceof Type\Atomic\TArray
654
                                || $replacement_atomic_type instanceof Type\Atomic\ObjectLike
655
                            ) {
656
                                $ored_type_assertions[] = $prefix . 'array';
657
                            } elseif ($replacement_atomic_type instanceof Type\Atomic\TNamedObject) {
658
                                $ored_type_assertions[] = $prefix . $replacement_atomic_type->value;
659
                            } elseif ($replacement_atomic_type instanceof Type\Atomic\Scalar) {
660
                                $ored_type_assertions[] = $prefix . $replacement_atomic_type->getId();
661
                            } elseif ($replacement_atomic_type instanceof Type\Atomic\TNull) {
662
                                $ored_type_assertions[] = $prefix . 'null';
663
                            } elseif ($replacement_atomic_type instanceof Type\Atomic\TTemplateParam) {
664
                                $ored_type_assertions[] = $prefix . $replacement_atomic_type->param_name;
665
                            }
666
                        }
667
668
                        if ($ored_type_assertions) {
669
                            $type_assertions[$assertion_var_id] = [$ored_type_assertions];
670
                        }
671
                    }
672
                } else {
673
                    if (isset($type_assertions[$assertion_var_id])) {
674
                        $type_assertions[$assertion_var_id] = array_merge(
675
                            $type_assertions[$assertion_var_id],
676
                            $assertion->rule
677
                        );
678
                    } else {
679
                        $type_assertions[$assertion_var_id] = $assertion->rule;
680
                    }
681
                }
682
            } elseif ($arg_value && ($assertion->rule === [['!falsy']] || $assertion->rule === [['true']])) {
683
                if ($assertion->rule === [['true']]) {
684
                    $conditional = new PhpParser\Node\Expr\BinaryOp\Identical(
685
                        $arg_value,
686
                        new PhpParser\Node\Expr\ConstFetch(new PhpParser\Node\Name('true'))
687
                    );
688
689
                    $assert_clauses = \Psalm\Type\Algebra::getFormula(
690
                        \spl_object_id($conditional),
691
                        $conditional,
692
                        $context->self,
693
                        $statements_analyzer,
694
                        $statements_analyzer->getCodebase()
695
                    );
696
                } else {
697
                    $assert_clauses = \Psalm\Type\Algebra::getFormula(
698
                        \spl_object_id($arg_value),
699
                        $arg_value,
700
                        $context->self,
701
                        $statements_analyzer,
702
                        $statements_analyzer->getCodebase()
703
                    );
704
                }
705
706
                $simplified_clauses = \Psalm\Type\Algebra::simplifyCNF(
707
                    array_merge($context->clauses, $assert_clauses)
708
                );
709
710
                $assert_type_assertions = \Psalm\Type\Algebra::getTruthsFromFormula(
711
                    $simplified_clauses
712
                );
713
714
                $type_assertions = array_merge($type_assertions, $assert_type_assertions);
715
            }
716
        }
717
718
        $changed_var_ids = [];
719
720
        foreach ($type_assertions as $var_id => $_) {
721
            $asserted_keys[$var_id] = true;
722
        }
723
724
        if ($type_assertions) {
725
            foreach (($statements_analyzer->getTemplateTypeMap() ?: []) as $template_name => $map) {
726
                foreach ($map as $ref => list($type)) {
727
                    $template_type_map[$template_name][$ref] = [
728
                        new Type\Union([
729
                            new Type\Atomic\TTemplateParam(
730
                                $template_name,
731
                                $type,
732
                                $ref
733
                            )
734
                        ])
735
                    ];
736
                }
737
            }
738
739
            // while in an and, we allow scope to boil over to support
740
            // statements of the form if ($x && $x->foo())
741
            $op_vars_in_scope = \Psalm\Type\Reconciler::reconcileKeyedTypes(
742
                $type_assertions,
743
                $type_assertions,
744
                $context->vars_in_scope,
745
                $changed_var_ids,
746
                $asserted_keys,
747
                $statements_analyzer,
748
                $template_type_map,
749
                $context->inside_loop,
750
                new CodeLocation($statements_analyzer->getSource(), $expr)
751
            );
752
753
            foreach ($changed_var_ids as $var_id => $_) {
754
                if (isset($op_vars_in_scope[$var_id])) {
755
                    $first_appearance = $statements_analyzer->getFirstAppearance($var_id);
756
757
                    $codebase = $statements_analyzer->getCodebase();
758
759
                    if ($first_appearance
760
                        && isset($context->vars_in_scope[$var_id])
761
                        && $context->vars_in_scope[$var_id]->hasMixed()
762
                    ) {
763
                        if (!$context->collect_initializations
764
                            && !$context->collect_mutations
765
                            && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
766
                            && (!(($parent_source = $statements_analyzer->getSource())
767
                                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
768
                                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
769
                        ) {
770
                            $codebase->analyzer->decrementMixedCount($statements_analyzer->getFilePath());
771
                        }
772
773
                        IssueBuffer::remove(
774
                            $statements_analyzer->getFilePath(),
775
                            'MixedAssignment',
776
                            $first_appearance->raw_file_start
777
                        );
778
                    }
779
780
                    $op_vars_in_scope[$var_id]->from_docblock = true;
781
782
                    foreach ($op_vars_in_scope[$var_id]->getAtomicTypes() as $changed_atomic_type) {
783
                        $changed_atomic_type->from_docblock = true;
784
785
                        if ($changed_atomic_type instanceof Type\Atomic\TNamedObject
786
                            && $changed_atomic_type->extra_types
787
                        ) {
788
                            foreach ($changed_atomic_type->extra_types as $extra_type) {
789
                                $extra_type->from_docblock = true;
790
                            }
791
                        }
792
                    }
793
                }
794
            }
795
796
            $context->vars_in_scope = $op_vars_in_scope;
797
        }
798
    }
799
800
    public static function checkTemplateResult(
801
        StatementsAnalyzer $statements_analyzer,
802
        TemplateResult $template_result,
803
        CodeLocation $code_location,
804
        ?string $function_id
805
    ) : void {
806
        if ($template_result->upper_bounds && $template_result->lower_bounds) {
807
            foreach ($template_result->lower_bounds as $template_name => $defining_map) {
808
                foreach ($defining_map as $defining_id => list($lower_bound_type)) {
809
                    if (isset($template_result->upper_bounds[$template_name][$defining_id])) {
810
                        $upper_bound_type = $template_result->upper_bounds[$template_name][$defining_id][0];
811
812
                        $union_comparison_result = new \Psalm\Internal\Analyzer\TypeComparisonResult();
813
814
                        if (count($template_result->lower_bounds_unintersectable_types) > 1) {
815
                            $upper_bound_type = $template_result->lower_bounds_unintersectable_types[0];
816
                            $lower_bound_type = $template_result->lower_bounds_unintersectable_types[1];
817
                        }
818
819
                        if (!TypeAnalyzer::isContainedBy(
820
                            $statements_analyzer->getCodebase(),
821
                            $upper_bound_type,
822
                            $lower_bound_type,
823
                            false,
824
                            false,
825
                            $union_comparison_result
826
                        )) {
827
                            if ($union_comparison_result->type_coerced) {
828
                                if ($union_comparison_result->type_coerced_from_mixed) {
829
                                    if (IssueBuffer::accepts(
830
                                        new MixedArgumentTypeCoercion(
831
                                            'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
832
                                                . $lower_bound_type->getId(),
833
                                            $code_location,
834
                                            $function_id
835
                                        ),
836
                                        $statements_analyzer->getSuppressedIssues()
837
                                    )) {
838
                                        // continue
839
                                    }
840
                                } else {
841
                                    if (IssueBuffer::accepts(
842
                                        new ArgumentTypeCoercion(
843
                                            'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
844
                                                . $lower_bound_type->getId(),
845
                                            $code_location,
846
                                            $function_id
847
                                        ),
848
                                        $statements_analyzer->getSuppressedIssues()
849
                                    )) {
850
                                        // continue
851
                                    }
852
                                }
853
                            } elseif ($union_comparison_result->scalar_type_match_found) {
854
                                if (IssueBuffer::accepts(
855
                                    new InvalidScalarArgument(
856
                                        'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
857
                                                . $lower_bound_type->getId(),
858
                                        $code_location,
859
                                        $function_id
860
                                    ),
861
                                    $statements_analyzer->getSuppressedIssues()
862
                                )) {
863
                                    // continue
864
                                }
865
                            } else {
866
                                if (IssueBuffer::accepts(
867
                                    new InvalidArgument(
868
                                        'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
869
                                                . $lower_bound_type->getId(),
870
                                        $code_location,
871
                                        $function_id
872
                                    ),
873
                                    $statements_analyzer->getSuppressedIssues()
874
                                )) {
875
                                    // continue
876
                                }
877
                            }
878
                        }
879
                    } else {
880
                        $template_result->upper_bounds[$template_name][$defining_id][0] = clone $lower_bound_type;
881
                    }
882
                }
883
            }
884
        }
885
    }
886
}
887