ArgumentAnalyzer::processTaintedness()   D
last analyzed

Complexity

Conditions 16
Paths 97

Size

Total Lines 133

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 97
nop 10
dl 0
loc 133
rs 4.4532
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\Analyzer\Statements\Expression\Call;
3
4
use PhpParser;
5
use Psalm\Codebase;
6
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
7
use Psalm\Internal\Analyzer\MethodAnalyzer;
8
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
9
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
10
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
11
use Psalm\Internal\Analyzer\Statements\Expression\CastAnalyzer;
12
use Psalm\Internal\Analyzer\StatementsAnalyzer;
13
use Psalm\Internal\Analyzer\TypeAnalyzer;
14
use Psalm\Internal\Taint\Sink;
15
use Psalm\Internal\Taint\TaintNode;
16
use Psalm\Internal\Type\TemplateResult;
17
use Psalm\Internal\Type\UnionTemplateHandler;
18
use Psalm\CodeLocation;
19
use Psalm\Context;
20
use Psalm\Issue\ImplicitToStringCast;
21
use Psalm\Issue\InvalidArgument;
22
use Psalm\Issue\InvalidScalarArgument;
23
use Psalm\Issue\MixedArgument;
24
use Psalm\Issue\MixedArgumentTypeCoercion;
25
use Psalm\Issue\NoValue;
26
use Psalm\Issue\NullArgument;
27
use Psalm\Issue\PossiblyFalseArgument;
28
use Psalm\Issue\PossiblyInvalidArgument;
29
use Psalm\Issue\PossiblyNullArgument;
30
use Psalm\Issue\ArgumentTypeCoercion;
31
use Psalm\IssueBuffer;
32
use Psalm\Storage\FunctionLikeParameter;
33
use Psalm\Type;
34
use Psalm\Type\Atomic\TArray;
35
use Psalm\Type\Atomic\TClassString;
36
use Psalm\Type\Atomic\TCallable;
37
use Psalm\Type\Atomic\TList;
38
use function strtolower;
39
use function strpos;
40
use function explode;
41
42
/**
43
 * @internal
44
 */
45
class ArgumentAnalyzer
46
{
47
    /**
48
     * @param  ?string $self_fq_class_name
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?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...
49
     * @param  ?string $static_fq_class_name
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?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...
50
     * @param  array<string, array<string, array{Type\Union, 1?:int}>> $class_generic_params
51
     * @return false|null
52
     */
53
    public static function checkArgumentMatches(
54
        StatementsAnalyzer $statements_analyzer,
55
        ?string $cased_method_id,
56
        ?string $self_fq_class_name,
57
        ?string $static_fq_class_name,
58
        CodeLocation $function_call_location,
59
        ?FunctionLikeParameter $function_param,
60
        int $argument_offset,
61
        PhpParser\Node\Arg $arg,
62
        Context $context,
63
        array $class_generic_params,
64
        ?TemplateResult $template_result,
65
        bool $specialize_taint,
66
        bool $in_call_map
67
    ) {
68
        $codebase = $statements_analyzer->getCodebase();
69
70
        $arg_value_type = $statements_analyzer->node_data->getType($arg->value);
71
72
        if (!$arg_value_type) {
73
            if ($function_param && !$function_param->by_ref) {
74
                if (!$context->collect_initializations
75
                    && !$context->collect_mutations
76
                    && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
77
                    && (!(($parent_source = $statements_analyzer->getSource())
78
                            instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
79
                        || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
80
                ) {
81
                    $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
82
                }
83
84
                $param_type = $function_param->type;
85
86
                if ($function_param->is_variadic
87
                    && $param_type
88
                    && $param_type->hasArray()
89
                ) {
90
                    /**
91
                     * @psalm-suppress PossiblyUndefinedStringArrayOffset
92
                     * @var TList|TArray
93
                     */
94
                    $array_type = $param_type->getAtomicTypes()['array'];
95
96
                    if ($array_type instanceof TList) {
97
                        $param_type = $array_type->type_param;
98
                    } else {
99
                        $param_type = $array_type->type_params[1];
100
                    }
101
                }
102
103
                if ($param_type && !$param_type->hasMixed()) {
104
                    if (IssueBuffer::accepts(
105
                        new MixedArgument(
106
                            'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
107
                                . ' cannot be mixed, expecting ' . $param_type,
108
                            new CodeLocation($statements_analyzer->getSource(), $arg->value),
109
                            $cased_method_id
110
                        ),
111
                        $statements_analyzer->getSuppressedIssues()
112
                    )) {
113
                        // fall through
114
                    }
115
                }
116
            }
117
118
            return;
119
        }
120
121
        if (!$function_param) {
122
            return;
123
        }
124
125
        if (self::checkFunctionLikeTypeMatches(
126
            $statements_analyzer,
127
            $codebase,
128
            $cased_method_id,
129
            $self_fq_class_name,
130
            $static_fq_class_name,
131
            $function_call_location,
132
            $function_param,
133
            $arg_value_type,
134
            $argument_offset,
135
            $arg,
136
            $context,
137
            $class_generic_params,
138
            $template_result,
139
            $specialize_taint,
140
            $in_call_map
141
        ) === false) {
142
            return false;
143
        }
144
    }
145
146
    /**
147
     * @param  ?string $self_fq_class_name
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?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...
148
     * @param  ?string $static_fq_class_name
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?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...
149
     * @param  array<string, array<string, array{Type\Union, 1?:int}>> $class_generic_params
150
     * @param  array<string, array<string, array{Type\Union, 1?:int}>> $generic_params
151
     * @param  array<string, array<string, array{Type\Union}>> $template_types
152
     * @return false|null
153
     */
154
    private static function checkFunctionLikeTypeMatches(
155
        StatementsAnalyzer $statements_analyzer,
156
        Codebase $codebase,
157
        ?string $cased_method_id,
158
        ?string $self_fq_class_name,
159
        ?string $static_fq_class_name,
160
        CodeLocation $function_call_location,
161
        FunctionLikeParameter $function_param,
162
        Type\Union $arg_type,
163
        int $argument_offset,
164
        PhpParser\Node\Arg $arg,
165
        Context $context,
166
        ?array $class_generic_params,
167
        ?TemplateResult $template_result,
168
        bool $specialize_taint,
169
        bool $in_call_map
170
    ) {
171
        if (!$function_param->type) {
172
            if (!$codebase->infer_types_from_usage && !$codebase->taint) {
173
                return;
174
            }
175
176
            $param_type = Type::getMixed();
177
        } else {
178
            $param_type = clone $function_param->type;
179
        }
180
181
        $bindable_template_params = [];
182
183
        if ($template_result) {
184
            $bindable_template_params = $param_type->getTemplateTypes();
185
        }
186
187
        if ($class_generic_params) {
188
            $empty_generic_params = [];
189
190
            $empty_template_result = new TemplateResult($class_generic_params, $empty_generic_params);
191
192
            $arg_value_type = $statements_analyzer->node_data->getType($arg->value);
193
194
            $param_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
195
                $param_type,
196
                $empty_template_result,
197
                $codebase,
198
                $statements_analyzer,
199
                $arg_value_type,
200
                $argument_offset,
201
                $context->self ?: 'fn-' . $context->calling_function_id
202
            );
203
204
            $arg_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
205
                $arg_type,
206
                $empty_template_result,
207
                $codebase,
208
                $statements_analyzer,
209
                $arg_value_type,
210
                $argument_offset,
211
                $context->self ?: 'fn-' . $context->calling_function_id
212
            );
213
        }
214
215
        if ($template_result && $template_result->template_types) {
216
            $arg_type_param = $arg_type;
217
218
            if ($arg->unpack) {
219
                $arg_type_param = null;
220
221
                foreach ($arg_type->getAtomicTypes() as $arg_atomic_type) {
222
                    if ($arg_atomic_type instanceof Type\Atomic\TArray
223
                        || $arg_atomic_type instanceof Type\Atomic\TList
224
                        || $arg_atomic_type instanceof Type\Atomic\ObjectLike
225
                    ) {
226
                        if ($arg_atomic_type instanceof Type\Atomic\ObjectLike) {
227
                            $arg_type_param = $arg_atomic_type->getGenericValueType();
228
                        } elseif ($arg_atomic_type instanceof Type\Atomic\TList) {
229
                            $arg_type_param = $arg_atomic_type->type_param;
230
                        } else {
231
                            $arg_type_param = $arg_atomic_type->type_params[1];
232
                        }
233
                    } elseif ($arg_atomic_type instanceof Type\Atomic\TIterable) {
234
                        $arg_type_param = $arg_atomic_type->type_params[1];
235
                    } elseif ($arg_atomic_type instanceof Type\Atomic\TNamedObject) {
236
                        ForeachAnalyzer::getKeyValueParamsForTraversableObject(
237
                            $arg_atomic_type,
238
                            $codebase,
239
                            $key_type,
240
                            $arg_type_param
241
                        );
242
                    }
243
                }
244
245
                if (!$arg_type_param) {
246
                    $arg_type_param = Type::getMixed();
247
                }
248
            }
249
250
            $param_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
251
                $param_type,
252
                $template_result,
253
                $codebase,
254
                $statements_analyzer,
255
                $arg_type_param,
256
                $argument_offset,
257
                $context->self,
258
                $context->calling_method_id ?: $context->calling_function_id
259
            );
260
261
            foreach ($bindable_template_params as $template_type) {
262
                if (!isset(
263
                    $template_result->upper_bounds
264
                        [$template_type->param_name]
265
                        [$template_type->defining_class]
266
                )
267
                    && !isset(
268
                        $template_result->lower_bounds
269
                        [$template_type->param_name]
270
                        [$template_type->defining_class]
271
                    )
272
                ) {
273
                    $template_result->upper_bounds[$template_type->param_name][$template_type->defining_class] = [
274
                        clone $template_type->as,
275
                        0
276
                    ];
277
                }
278
            }
279
        }
280
281
        if (!$context->check_variables) {
282
            return;
283
        }
284
285
        $parent_class = null;
286
287
        $classlike_storage = null;
288
        $static_classlike_storage = null;
289
290
        if ($self_fq_class_name) {
291
            $classlike_storage = $codebase->classlike_storage_provider->get($self_fq_class_name);
292
            $parent_class = $classlike_storage->parent_class;
293
            $static_classlike_storage = $classlike_storage;
294
295
            if ($static_fq_class_name && $static_fq_class_name !== $self_fq_class_name) {
296
                $static_classlike_storage = $codebase->classlike_storage_provider->get($static_fq_class_name);
297
            }
298
        }
299
300
        $fleshed_out_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
301
            $codebase,
302
            $param_type,
303
            $classlike_storage ? $classlike_storage->name : null,
304
            $static_classlike_storage ? $static_classlike_storage->name : null,
305
            $parent_class,
306
            true,
307
            false,
308
            $static_classlike_storage ? $static_classlike_storage->final : false
309
        );
310
311
        $fleshed_out_signature_type = $function_param->signature_type
312
            ? \Psalm\Internal\Type\TypeExpander::expandUnion(
313
                $codebase,
314
                $function_param->signature_type,
315
                $classlike_storage ? $classlike_storage->name : null,
316
                $static_classlike_storage ? $static_classlike_storage->name : null,
317
                $parent_class
318
            )
319
            : null;
320
321
        $unpacked_atomic_array = null;
322
323
        if ($arg->unpack) {
324
            if ($arg_type->hasMixed()) {
325
                if (!$context->collect_initializations
326
                    && !$context->collect_mutations
327
                    && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
328
                    && (!(($parent_source = $statements_analyzer->getSource())
329
                            instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
330
                        || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
331
                ) {
332
                    $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
333
                }
334
335
                if (IssueBuffer::accepts(
336
                    new MixedArgument(
337
                        'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
338
                            . ' cannot be ' . $arg_type->getId() . ', expecting array',
339
                        new CodeLocation($statements_analyzer->getSource(), $arg->value),
340
                        $cased_method_id
341
                    ),
342
                    $statements_analyzer->getSuppressedIssues()
343
                )) {
344
                    // fall through
345
                }
346
347
                if ($cased_method_id) {
348
                    $arg_location = new CodeLocation($statements_analyzer->getSource(), $arg->value);
349
350
                    self::processTaintedness(
351
                        $statements_analyzer,
352
                        $cased_method_id,
353
                        $argument_offset,
354
                        $arg_location,
355
                        $function_call_location,
356
                        $function_param,
357
                        $arg_type,
358
                        $arg->value,
359
                        $context,
360
                        $specialize_taint
361
                    );
362
                }
363
364
                return;
365
            }
366
367
            if ($arg_type->hasArray()) {
368
                /**
369
                 * @psalm-suppress PossiblyUndefinedStringArrayOffset
370
                 * @var Type\Atomic\TArray|Type\Atomic\TList|Type\Atomic\ObjectLike
371
                 */
372
                $unpacked_atomic_array = $arg_type->getAtomicTypes()['array'];
373
374
                if ($unpacked_atomic_array instanceof Type\Atomic\ObjectLike) {
375
                    if ($unpacked_atomic_array->is_list
376
                        && isset($unpacked_atomic_array->properties[$argument_offset])
377
                    ) {
378
                        $arg_type = clone $unpacked_atomic_array->properties[$argument_offset];
379
                    } else {
380
                        $arg_type = $unpacked_atomic_array->getGenericValueType();
381
                    }
382
                } elseif ($unpacked_atomic_array instanceof Type\Atomic\TList) {
383
                    $arg_type = $unpacked_atomic_array->type_param;
384
                } else {
385
                    $arg_type = $unpacked_atomic_array->type_params[1];
386
                }
387
            } else {
388
                foreach ($arg_type->getAtomicTypes() as $atomic_type) {
389
                    if (!$atomic_type->isIterable($codebase)) {
390
                        if (IssueBuffer::accepts(
391
                            new InvalidArgument(
392
                                'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
393
                                    . ' expects array, ' . $atomic_type->getId() . ' provided',
394
                                new CodeLocation($statements_analyzer->getSource(), $arg->value),
395
                                $cased_method_id
396
                            ),
397
                            $statements_analyzer->getSuppressedIssues()
398
                        )) {
399
                            // fall through
400
                        }
401
402
                        continue;
403
                    }
404
                }
405
406
                return;
407
            }
408
        }
409
410
        if (self::verifyType(
411
            $statements_analyzer,
412
            $arg_type,
413
            $fleshed_out_type,
414
            $fleshed_out_signature_type,
415
            $cased_method_id,
416
            $argument_offset,
417
            new CodeLocation($statements_analyzer->getSource(), $arg->value),
418
            $arg->value,
419
            $context,
420
            $function_param,
421
            $arg->unpack,
422
            $unpacked_atomic_array,
423
            $specialize_taint,
424
            $in_call_map,
425
            $function_call_location
426
        ) === false) {
427
            return false;
428
        }
429
    }
430
431
    /**
432
     * @param Type\Atomic\ObjectLike|Type\Atomic\TArray|Type\Atomic\TList $unpacked_atomic_array
433
     * @return  null|false
434
     */
435
    public static function verifyType(
436
        StatementsAnalyzer $statements_analyzer,
437
        Type\Union $input_type,
438
        Type\Union $param_type,
439
        ?Type\Union $signature_param_type,
440
        ?string $cased_method_id,
441
        int $argument_offset,
442
        CodeLocation $arg_location,
443
        PhpParser\Node\Expr $input_expr,
444
        Context $context,
445
        FunctionLikeParameter $function_param,
446
        bool $unpack,
447
        ?Type\Atomic $unpacked_atomic_array,
448
        bool $specialize_taint,
449
        bool $in_call_map,
450
        CodeLocation $function_call_location
451
    ) {
452
        $codebase = $statements_analyzer->getCodebase();
453
454
        if ($param_type->hasMixed()) {
455
            if ($codebase->infer_types_from_usage
456
                && !$input_type->hasMixed()
457
                && !$param_type->from_docblock
458
                && !$param_type->had_template
459
                && $cased_method_id
460
                && strpos($cased_method_id, '::')
461
                && !strpos($cased_method_id, '__')
462
            ) {
463
                $method_parts = explode('::', $cased_method_id);
464
465
                $method_id = new \Psalm\Internal\MethodIdentifier($method_parts[0], strtolower($method_parts[1]));
466
                $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
467
468
                if ($declaring_method_id) {
469
                    $id_lc = strtolower((string) $declaring_method_id);
470
                    if (!isset($codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset])) {
471
                        $codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset]
472
                            = clone $input_type;
473
                    } else {
474
                        $codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset]
475
                            = Type::combineUnionTypes(
476
                                $codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset],
477
                                clone $input_type,
478
                                $codebase
479
                            );
480
                    }
481
                }
482
            }
483
484
            if ($cased_method_id) {
485
                self::processTaintedness(
486
                    $statements_analyzer,
487
                    $cased_method_id,
488
                    $argument_offset,
489
                    $arg_location,
490
                    $function_call_location,
491
                    $function_param,
492
                    $input_type,
493
                    $input_expr,
494
                    $context,
495
                    $specialize_taint
496
                );
497
            }
498
499
            return null;
500
        }
501
502
        $method_identifier = $cased_method_id ? ' of ' . $cased_method_id : '';
503
504
        if ($input_type->hasMixed()) {
505
            if (!$context->collect_initializations
506
                && !$context->collect_mutations
507
                && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
508
                && (!(($parent_source = $statements_analyzer->getSource())
509
                        instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
510
                    || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
511
            ) {
512
                $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
513
            }
514
515
            if (IssueBuffer::accepts(
516
                new MixedArgument(
517
                    'Argument ' . ($argument_offset + 1) . $method_identifier
518
                        . ' cannot be ' . $input_type->getId() . ', expecting ' .
519
                        $param_type,
520
                    $arg_location,
521
                    $cased_method_id
522
                ),
523
                $statements_analyzer->getSuppressedIssues()
524
            )) {
525
                // fall through
526
            }
527
528
            if ($input_type->isMixed()) {
529
                if (!$function_param->by_ref
530
                    && !($function_param->is_variadic xor $unpack)
531
                    && $cased_method_id !== 'echo'
532
                    && $cased_method_id !== 'print'
533
                    && (!$in_call_map || $context->strict_types)
534
                ) {
535
                    self::coerceValueAfterGatekeeperArgument(
536
                        $statements_analyzer,
537
                        $input_type,
538
                        false,
539
                        $input_expr,
540
                        $param_type,
541
                        $signature_param_type,
542
                        $context,
543
                        $unpack,
544
                        $unpacked_atomic_array
545
                    );
546
                }
547
            }
548
549
            if ($cased_method_id) {
550
                $input_type = self::processTaintedness(
551
                    $statements_analyzer,
552
                    $cased_method_id,
553
                    $argument_offset,
554
                    $arg_location,
555
                    $function_call_location,
556
                    $function_param,
557
                    $input_type,
558
                    $input_expr,
559
                    $context,
560
                    $specialize_taint
561
                );
562
            }
563
564
            if ($input_type->isMixed()) {
565
                return null;
566
            }
567
        }
568
569
        if ($input_type->isNever()) {
570
            if (IssueBuffer::accepts(
571
                new NoValue(
572
                    'This function or method call never returns output',
573
                    $arg_location
574
                ),
575
                $statements_analyzer->getSuppressedIssues()
576
            )) {
577
                // fall through
578
            }
579
580
            return null;
581
        }
582
583
        if (!$context->collect_initializations
584
            && !$context->collect_mutations
585
            && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
586
            && (!(($parent_source = $statements_analyzer->getSource())
587
                    instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
588
                || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
589
        ) {
590
            $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
591
        }
592
593
        if ($function_param->by_ref) {
594
            $param_type->possibly_undefined = true;
595
        }
596
597
        if ($param_type->hasCallableType()
598
            && $param_type->isSingle()
599
            && $input_type->isSingleStringLiteral()
600
            && !\Psalm\Internal\Codebase\InternalCallMapHandler::inCallMap($input_type->getSingleStringLiteral()->value)
601
        ) {
602
            foreach ($input_type->getAtomicTypes() as $key => $atomic_type) {
603
                $candidate_callable = TypeAnalyzer::getCallableFromAtomic(
604
                    $codebase,
605
                    $atomic_type,
606
                    null,
607
                    $statements_analyzer
608
                );
609
610
                if ($candidate_callable) {
611
                    $input_type->removeType($key);
612
                    $input_type->addType($candidate_callable);
613
                }
614
            }
615
        }
616
617
        $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
618
619
        $type_match_found = TypeAnalyzer::isContainedBy(
620
            $codebase,
621
            $input_type,
622
            $param_type,
623
            true,
624
            true,
625
            $union_comparison_results
626
        );
627
628
        $replace_input_type = false;
629
630
        if ($union_comparison_results->replacement_union_type) {
631
            $replace_input_type = true;
632
            $input_type = $union_comparison_results->replacement_union_type;
633
        }
634
635
        if ($cased_method_id) {
636
            $old_input_type = $input_type;
637
638
            $input_type = self::processTaintedness(
639
                $statements_analyzer,
640
                $cased_method_id,
641
                $argument_offset,
642
                $arg_location,
643
                $function_call_location,
644
                $function_param,
645
                $input_type,
646
                $input_expr,
647
                $context,
648
                $specialize_taint
649
            );
650
651
            if ($old_input_type !== $input_type) {
652
                $replace_input_type = true;
653
            }
654
        }
655
656
        if ($type_match_found
657
            && $param_type->hasCallableType()
658
        ) {
659
            $potential_method_ids = [];
660
661
            foreach ($input_type->getAtomicTypes() as $input_type_part) {
662
                if ($input_type_part instanceof Type\Atomic\ObjectLike) {
663
                    $potential_method_id = TypeAnalyzer::getCallableMethodIdFromObjectLike(
664
                        $input_type_part,
665
                        $codebase,
666
                        $context->calling_method_id,
667
                        $statements_analyzer->getFilePath()
668
                    );
669
670
                    if ($potential_method_id && $potential_method_id !== 'not-callable') {
671
                        $potential_method_ids[] = $potential_method_id;
672
                    }
673
                } elseif ($input_type_part instanceof Type\Atomic\TLiteralString
674
                    && strpos($input_type_part->value, '::')
675
                ) {
676
                    $parts = explode('::', $input_type_part->value);
677
                    $potential_method_ids[] = new \Psalm\Internal\MethodIdentifier(
678
                        $parts[0],
679
                        strtolower($parts[1])
680
                    );
681
                }
682
            }
683
684
            foreach ($potential_method_ids as $potential_method_id) {
685
                $codebase->methods->methodExists(
686
                    $potential_method_id,
687
                    $context->calling_method_id,
688
                    null,
689
                    $statements_analyzer,
690
                    $statements_analyzer->getFilePath()
691
                );
692
            }
693
        }
694
695
        if ($context->strict_types
696
            && !$input_type->hasArray()
697
            && !$param_type->from_docblock
698
            && $cased_method_id !== 'echo'
699
            && $cased_method_id !== 'print'
700
            && $cased_method_id !== 'sprintf'
701
        ) {
702
            $union_comparison_results->scalar_type_match_found = false;
703
704
            if ($union_comparison_results->to_string_cast) {
705
                $union_comparison_results->to_string_cast = false;
706
                $type_match_found = false;
707
            }
708
        }
709
710
        if ($union_comparison_results->type_coerced && !$input_type->hasMixed()) {
711
            if ($union_comparison_results->type_coerced_from_mixed) {
712
                if (IssueBuffer::accepts(
713
                    new MixedArgumentTypeCoercion(
714
                        'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
715
                            ', parent type ' . $input_type->getId() . ' provided',
716
                        $arg_location,
717
                        $cased_method_id
718
                    ),
719
                    $statements_analyzer->getSuppressedIssues()
720
                )) {
721
                    // keep soldiering on
722
                }
723
            } else {
724
                if (IssueBuffer::accepts(
725
                    new ArgumentTypeCoercion(
726
                        'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
727
                            ', parent type ' . $input_type->getId() . ' provided',
728
                        $arg_location,
729
                        $cased_method_id
730
                    ),
731
                    $statements_analyzer->getSuppressedIssues()
732
                )) {
733
                    // keep soldiering on
734
                }
735
            }
736
        }
737
738
        if ($union_comparison_results->to_string_cast && $cased_method_id !== 'echo' && $cased_method_id !== 'print') {
739
            if (IssueBuffer::accepts(
740
                new ImplicitToStringCast(
741
                    'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' .
742
                        $param_type->getId() . ', ' . $input_type->getId() . ' provided with a __toString method',
743
                    $arg_location
744
                ),
745
                $statements_analyzer->getSuppressedIssues()
746
            )) {
747
                // fall through
748
            }
749
        }
750
751
        if (!$type_match_found && !$union_comparison_results->type_coerced) {
752
            $types_can_be_identical = TypeAnalyzer::canBeContainedBy(
753
                $codebase,
754
                $input_type,
755
                $param_type,
756
                true,
757
                true
758
            );
759
760
            if ($union_comparison_results->scalar_type_match_found) {
761
                if ($cased_method_id !== 'echo' && $cased_method_id !== 'print') {
762
                    if (IssueBuffer::accepts(
763
                        new InvalidScalarArgument(
764
                            'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' .
765
                                $param_type->getId() . ', ' . $input_type->getId() . ' provided',
766
                            $arg_location,
767
                            $cased_method_id
768
                        ),
769
                        $statements_analyzer->getSuppressedIssues()
770
                    )) {
771
                        // fall through
772
                    }
773
                }
774
            } elseif ($types_can_be_identical) {
775
                if (IssueBuffer::accepts(
776
                    new PossiblyInvalidArgument(
777
                        'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
778
                            ', possibly different type ' . $input_type->getId() . ' provided',
779
                        $arg_location,
780
                        $cased_method_id
781
                    ),
782
                    $statements_analyzer->getSuppressedIssues()
783
                )) {
784
                    // fall through
785
                }
786
            } else {
787
                if (IssueBuffer::accepts(
788
                    new InvalidArgument(
789
                        'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
790
                            ', ' . $input_type->getId() . ' provided',
791
                        $arg_location,
792
                        $cased_method_id
793
                    ),
794
                    $statements_analyzer->getSuppressedIssues()
795
                )) {
796
                    // fall through
797
                }
798
            }
799
800
            return;
801
        }
802
803
        if ($input_expr instanceof PhpParser\Node\Scalar\String_
804
            || $input_expr instanceof PhpParser\Node\Expr\Array_
805
            || $input_expr instanceof PhpParser\Node\Expr\BinaryOp\Concat
806
        ) {
807
            foreach ($param_type->getAtomicTypes() as $param_type_part) {
808
                if ($param_type_part instanceof TClassString
809
                    && $input_expr instanceof PhpParser\Node\Scalar\String_
810
                    && !$param_type->getLiteralStrings()
811
                ) {
812
                    if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
813
                        $statements_analyzer,
814
                        $input_expr->value,
815
                        $arg_location,
816
                        $context->self,
817
                        $context->calling_method_id,
818
                        $statements_analyzer->getSuppressedIssues()
819
                    ) === false
820
                    ) {
821
                        return;
822
                    }
823
                } elseif ($param_type_part instanceof TArray
824
                    && $input_expr instanceof PhpParser\Node\Expr\Array_
825
                ) {
826
                    foreach ($param_type_part->type_params[1]->getAtomicTypes() as $param_array_type_part) {
827
                        if ($param_array_type_part instanceof TClassString) {
828
                            foreach ($input_expr->items as $item) {
829
                                if ($item && $item->value instanceof PhpParser\Node\Scalar\String_) {
830
                                    if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
831
                                        $statements_analyzer,
832
                                        $item->value->value,
833
                                        $arg_location,
834
                                        $context->self,
835
                                        $context->calling_method_id,
836
                                        $statements_analyzer->getSuppressedIssues()
837
                                    ) === false
838
                                    ) {
839
                                        return;
840
                                    }
841
                                }
842
                            }
843
                        }
844
                    }
845
                } elseif ($param_type_part instanceof TCallable) {
846
                    $can_be_callable_like_array = false;
847
                    if ($param_type->hasArray()) {
848
                        /**
849
                         * @psalm-suppress PossiblyUndefinedStringArrayOffset
850
                         */
851
                        $param_array_type = $param_type->getAtomicTypes()['array'];
852
853
                        $row_type = null;
854
                        if ($param_array_type instanceof TList) {
855
                            $row_type = $param_array_type->type_param;
856
                        } elseif ($param_array_type instanceof TArray) {
857
                            $row_type = $param_array_type->type_params[1];
858
                        } elseif ($param_array_type instanceof Type\Atomic\ObjectLike) {
859
                            $row_type = $param_array_type->getGenericArrayType()->type_params[1];
860
                        }
861
862
                        if ($row_type &&
863
                            ($row_type->hasMixed() || $row_type->hasString())
864
                        ) {
865
                            $can_be_callable_like_array = true;
866
                        }
867
                    }
868
869
                    if (!$can_be_callable_like_array) {
870
                        $function_ids = CallAnalyzer::getFunctionIdsFromCallableArg(
871
                            $statements_analyzer,
872
                            $input_expr
873
                        );
874
875
                        foreach ($function_ids as $function_id) {
876
                            if (strpos($function_id, '::') !== false) {
877
                                if ($function_id[0] === '$') {
878
                                    $function_id = \substr($function_id, 1);
879
                                }
880
881
                                $function_id_parts = explode('&', $function_id);
882
883
                                $non_existent_method_ids = [];
884
                                $has_valid_method = false;
885
886
                                foreach ($function_id_parts as $function_id_part) {
887
                                    list($callable_fq_class_name, $method_name) = explode('::', $function_id_part);
888
889
                                    switch ($callable_fq_class_name) {
890
                                        case 'self':
891
                                        case 'static':
892
                                        case 'parent':
893
                                            $container_class = $statements_analyzer->getFQCLN();
894
895
                                            if ($callable_fq_class_name === 'parent') {
896
                                                $container_class = $statements_analyzer->getParentFQCLN();
897
                                            }
898
899
                                            if (!$container_class) {
900
                                                continue 2;
901
                                            }
902
903
                                            $callable_fq_class_name = $container_class;
904
                                    }
905
906
                                    if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
907
                                        $statements_analyzer,
908
                                        $callable_fq_class_name,
909
                                        $arg_location,
910
                                        $context->self,
911
                                        $context->calling_method_id,
912
                                        $statements_analyzer->getSuppressedIssues()
913
                                    ) === false
914
                                    ) {
915
                                        return;
916
                                    }
917
918
                                    $function_id_part = new \Psalm\Internal\MethodIdentifier(
919
                                        $callable_fq_class_name,
920
                                        strtolower($method_name)
921
                                    );
922
923
                                    $call_method_id = new \Psalm\Internal\MethodIdentifier(
924
                                        $callable_fq_class_name,
925
                                        '__call'
926
                                    );
927
928
                                    if (!$codebase->classOrInterfaceExists($callable_fq_class_name)) {
929
                                        return;
930
                                    }
931
932
                                    if (!$codebase->methods->methodExists($function_id_part)
933
                                        && !$codebase->methods->methodExists($call_method_id)
934
                                    ) {
935
                                        $non_existent_method_ids[] = $function_id_part;
936
                                    } else {
937
                                        $has_valid_method = true;
938
                                    }
939
                                }
940
941
                                if (!$has_valid_method && !$param_type->hasString() && !$param_type->hasArray()) {
942
                                    if (MethodAnalyzer::checkMethodExists(
943
                                        $codebase,
944
                                        $non_existent_method_ids[0],
945
                                        $arg_location,
946
                                        $statements_analyzer->getSuppressedIssues()
947
                                    ) === false
948
                                    ) {
949
                                        return;
950
                                    }
951
                                }
952
                            } else {
953
                                if (!$param_type->hasString()
954
                                    && !$param_type->hasArray()
955
                                    && CallAnalyzer::checkFunctionExists(
956
                                        $statements_analyzer,
957
                                        $function_id,
958
                                        $arg_location,
959
                                        false
960
                                    ) === false
961
                                ) {
962
                                    return;
963
                                }
964
                            }
965
                        }
966
                    }
967
                }
968
            }
969
        }
970
971
        if (!$param_type->isNullable() && $cased_method_id !== 'echo' && $cased_method_id !== 'print') {
972
            if ($input_type->isNull()) {
973
                if (IssueBuffer::accepts(
974
                    new NullArgument(
975
                        'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, ' .
976
                            'null value provided to parameter with type ' . $param_type->getId(),
977
                        $arg_location,
978
                        $cased_method_id
979
                    ),
980
                    $statements_analyzer->getSuppressedIssues()
981
                )) {
982
                    // fall through
983
                }
984
985
                return null;
986
            }
987
988
            if ($input_type->isNullable() && !$input_type->ignore_nullable_issues) {
989
                if (IssueBuffer::accepts(
990
                    new PossiblyNullArgument(
991
                        'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, possibly ' .
992
                            'null value provided',
993
                        $arg_location,
994
                        $cased_method_id
995
                    ),
996
                    $statements_analyzer->getSuppressedIssues()
997
                )) {
998
                    // fall through
999
                }
1000
            }
1001
        }
1002
1003
        if ($input_type->isFalsable()
1004
            && !$param_type->hasBool()
1005
            && !$param_type->hasScalar()
1006
            && !$input_type->ignore_falsable_issues
1007
            && $cased_method_id !== 'echo'
1008
        ) {
1009
            if (IssueBuffer::accepts(
1010
                new PossiblyFalseArgument(
1011
                    'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be false, possibly ' .
1012
                        'false value provided',
1013
                    $arg_location,
1014
                    $cased_method_id
1015
                ),
1016
                $statements_analyzer->getSuppressedIssues()
1017
            )) {
1018
                // fall through
1019
            }
1020
        }
1021
1022
        if (($type_match_found || $input_type->hasMixed())
1023
            && !$function_param->by_ref
1024
            && !($function_param->is_variadic xor $unpack)
1025
            && $cased_method_id !== 'echo'
1026
            && $cased_method_id !== 'print'
1027
            && (!$in_call_map || $context->strict_types)
1028
        ) {
1029
            self::coerceValueAfterGatekeeperArgument(
1030
                $statements_analyzer,
1031
                $input_type,
1032
                $replace_input_type,
1033
                $input_expr,
1034
                $param_type,
1035
                $signature_param_type,
1036
                $context,
1037
                $unpack,
1038
                $unpacked_atomic_array
1039
            );
1040
        }
1041
1042
        return null;
1043
    }
1044
1045
    /**
1046
     * @param Type\Atomic\ObjectLike|Type\Atomic\TArray|Type\Atomic\TList $unpacked_atomic_array
1047
     */
1048
    private static function coerceValueAfterGatekeeperArgument(
1049
        StatementsAnalyzer $statements_analyzer,
1050
        Type\Union $input_type,
1051
        bool $input_type_changed,
1052
        PhpParser\Node\Expr $input_expr,
1053
        Type\Union $param_type,
1054
        ?Type\Union $signature_param_type,
1055
        Context $context,
1056
        bool $unpack,
1057
        ?Type\Atomic $unpacked_atomic_array
1058
    ) : void {
1059
        if ($param_type->hasMixed()) {
1060
            return;
1061
        }
1062
1063
        if (!$input_type_changed && $param_type->from_docblock && !$input_type->hasMixed()) {
1064
            $input_type = clone $input_type;
1065
1066
            foreach ($param_type->getAtomicTypes() as $param_atomic_type) {
1067
                if ($param_atomic_type instanceof Type\Atomic\TGenericObject) {
1068
                    foreach ($input_type->getAtomicTypes() as $input_atomic_type) {
1069
                        if ($input_atomic_type instanceof Type\Atomic\TGenericObject
1070
                            && $input_atomic_type->value === $param_atomic_type->value
1071
                        ) {
1072
                            foreach ($input_atomic_type->type_params as $i => $type_param) {
1073
                                if ($type_param->isEmpty() && isset($param_atomic_type->type_params[$i])) {
1074
                                    $input_type_changed = true;
1075
1076
                                    /** @psalm-suppress PropertyTypeCoercion */
1077
                                    $input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i];
1078
                                }
1079
                            }
1080
                        }
1081
                    }
1082
                }
1083
            }
1084
1085
            if (!$input_type_changed) {
1086
                return;
1087
            }
1088
        }
1089
1090
        $var_id = ExpressionIdentifier::getVarId(
1091
            $input_expr,
1092
            $statements_analyzer->getFQCLN(),
1093
            $statements_analyzer
1094
        );
1095
1096
        if ($var_id) {
1097
            $was_cloned = false;
1098
1099
            if ($input_type->isNullable() && !$param_type->isNullable()) {
1100
                $input_type = clone $input_type;
1101
                $was_cloned = true;
1102
                $input_type->removeType('null');
1103
            }
1104
1105
            if ($input_type->getId() === $param_type->getId()) {
1106
                if (!$was_cloned) {
1107
                    $was_cloned = true;
1108
                    $input_type = clone $input_type;
1109
                }
1110
1111
                $input_type->from_docblock = false;
1112
1113
                foreach ($input_type->getAtomicTypes() as $atomic_type) {
1114
                    $atomic_type->from_docblock = false;
1115
                }
1116
            } elseif ($input_type->hasMixed() && $signature_param_type) {
1117
                $was_cloned = true;
1118
                $input_type = clone $signature_param_type;
1119
1120
                if ($input_type->isNullable()) {
1121
                    $input_type->ignore_nullable_issues = true;
1122
                }
1123
            }
1124
1125
            if ($context->inside_conditional) {
1126
                $context->assigned_var_ids[$var_id] = true;
1127
            }
1128
1129
            if ($was_cloned) {
1130
                $context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
1131
            }
1132
1133
            if ($unpack) {
1134
                if ($unpacked_atomic_array instanceof Type\Atomic\TList) {
1135
                    $unpacked_atomic_array = clone $unpacked_atomic_array;
1136
                    $unpacked_atomic_array->type_param = $input_type;
1137
1138
                    $context->vars_in_scope[$var_id] = new Type\Union([$unpacked_atomic_array]);
1139
                } elseif ($unpacked_atomic_array instanceof Type\Atomic\TArray) {
1140
                    $unpacked_atomic_array = clone $unpacked_atomic_array;
1141
                    /** @psalm-suppress PropertyTypeCoercion */
1142
                    $unpacked_atomic_array->type_params[1] = $input_type;
1143
1144
                    $context->vars_in_scope[$var_id] = new Type\Union([$unpacked_atomic_array]);
1145
                } elseif ($unpacked_atomic_array instanceof Type\Atomic\ObjectLike
1146
                    && $unpacked_atomic_array->is_list
1147
                ) {
1148
                    $unpacked_atomic_array = $unpacked_atomic_array->getList();
1149
                    $unpacked_atomic_array->type_param = $input_type;
1150
1151
                    $context->vars_in_scope[$var_id] = new Type\Union([$unpacked_atomic_array]);
1152
                } else {
1153
                    $context->vars_in_scope[$var_id] = new Type\Union([
1154
                        new TArray([
1155
                            Type::getInt(),
1156
                            $input_type
1157
                        ]),
1158
                    ]);
1159
                }
1160
            } else {
1161
                $context->vars_in_scope[$var_id] = $input_type;
1162
            }
1163
        }
1164
    }
1165
1166
    private static function processTaintedness(
1167
        StatementsAnalyzer $statements_analyzer,
1168
        string $cased_method_id,
1169
        int $argument_offset,
1170
        CodeLocation $arg_location,
1171
        CodeLocation $function_call_location,
1172
        FunctionLikeParameter $function_param,
1173
        Type\Union $input_type,
1174
        PhpParser\Node\Expr $expr,
1175
        Context $context,
1176
        bool $specialize_taint
1177
    ) : Type\Union {
1178
        $codebase = $statements_analyzer->getCodebase();
1179
1180
        if (!$codebase->taint
1181
            || !$codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
1182
            || \in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
1183
        ) {
1184
            return $input_type;
1185
        }
1186
1187
        if ($function_param->type && $function_param->type->isString()) {
1188
            $input_type = CastAnalyzer::castStringAttempt(
1189
                $statements_analyzer,
1190
                $context,
1191
                $input_type,
1192
                $expr,
1193
                false
1194
            );
1195
        }
1196
1197
        if ($specialize_taint) {
1198
            $method_node = TaintNode::getForMethodArgument(
1199
                $cased_method_id,
1200
                $cased_method_id,
1201
                $argument_offset,
1202
                $function_param->location,
1203
                $function_call_location
1204
            );
1205
        } else {
1206
            $method_node = TaintNode::getForMethodArgument(
1207
                $cased_method_id,
1208
                $cased_method_id,
1209
                $argument_offset,
1210
                $function_param->location
1211
            );
1212
1213
            if (strpos($cased_method_id, '::')) {
1214
                list($fq_classlike_name, $cased_method_name) = explode('::', $cased_method_id);
1215
                $method_name = strtolower($cased_method_name);
1216
                $class_storage = $codebase->classlike_storage_provider->get($fq_classlike_name);
1217
1218
                foreach ($class_storage->dependent_classlikes as $dependent_classlike_lc => $_) {
1219
                    $dependent_classlike_storage = $codebase->classlike_storage_provider->get(
1220
                        $dependent_classlike_lc
1221
                    );
1222
                    $new_sink = TaintNode::getForMethodArgument(
1223
                        $dependent_classlike_lc . '::' . $method_name,
1224
                        $dependent_classlike_storage->name . '::' . $cased_method_name,
1225
                        $argument_offset,
1226
                        $arg_location,
1227
                        null
1228
                    );
1229
1230
                    $codebase->taint->addTaintNode($new_sink);
1231
                    $codebase->taint->addPath($method_node, $new_sink, 'arg');
1232
                }
1233
1234
                if (isset($class_storage->overridden_method_ids[$method_name])) {
1235
                    foreach ($class_storage->overridden_method_ids[$method_name] as $parent_method_id) {
1236
                        $new_sink = TaintNode::getForMethodArgument(
1237
                            (string) $parent_method_id,
1238
                            $codebase->methods->getCasedMethodId($parent_method_id),
0 ignored issues
show
Bug introduced by
It seems like $codebase->methods->getC...odId($parent_method_id) targeting Psalm\Internal\Codebase\...ods::getCasedMethodId() can also be of type object<Psalm\Internal\MethodIdentifier>; however, Psalm\Internal\Taint\Tai...:getForMethodArgument() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1239
                            $argument_offset,
1240
                            $arg_location,
1241
                            null
1242
                        );
1243
1244
                        $codebase->taint->addTaintNode($new_sink);
1245
                        $codebase->taint->addPath($method_node, $new_sink, 'arg');
1246
                    }
1247
                }
1248
            }
1249
        }
1250
1251
        $codebase->taint->addTaintNode($method_node);
1252
1253
        $argument_value_node = TaintNode::getForAssignment(
1254
            'call to ' . $cased_method_id,
1255
            $arg_location
1256
        );
1257
1258
        $codebase->taint->addTaintNode($argument_value_node);
1259
1260
        $codebase->taint->addPath($argument_value_node, $method_node, 'arg');
1261
1262
        if ($function_param->sinks) {
1263
            if ($specialize_taint) {
1264
                $sink = Sink::getForMethodArgument(
1265
                    $cased_method_id,
1266
                    $cased_method_id,
1267
                    $argument_offset,
1268
                    $function_param->location,
1269
                    $function_call_location
1270
                );
1271
            } else {
1272
                $sink = Sink::getForMethodArgument(
1273
                    $cased_method_id,
1274
                    $cased_method_id,
1275
                    $argument_offset,
1276
                    $function_param->location
1277
                );
1278
            }
1279
1280
            $sink->taints = $function_param->sinks;
1281
1282
            $codebase->taint->addSink($sink);
1283
        }
1284
1285
        if ($input_type->parent_nodes) {
1286
            foreach ($input_type->parent_nodes as $parent_node) {
1287
                $codebase->taint->addTaintNode($method_node);
1288
                $codebase->taint->addPath($parent_node, $argument_value_node, 'arg');
1289
            }
1290
        }
1291
1292
        if ($function_param->assert_untainted) {
1293
            $input_type = clone $input_type;
1294
            $input_type->parent_nodes = [];
1295
        }
1296
1297
        return $input_type;
1298
    }
1299
}
1300