mirror of
https://github.com/danog/psalm.git
synced 2025-01-05 12:38:35 +01:00
1395 lines
50 KiB
PHP
1395 lines
50 KiB
PHP
<?php
|
|
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
|
|
|
use PhpParser;
|
|
use Psalm\Codebase;
|
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
|
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
|
|
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
|
|
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
|
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer;
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
|
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
|
use Psalm\Internal\Codebase\TaintFlowGraph;
|
|
use Psalm\Internal\DataFlow\TaintSink;
|
|
use Psalm\Internal\Stubs\Generator\StubsGenerator;
|
|
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
|
use Psalm\Internal\MethodIdentifier;
|
|
use Psalm\Internal\Type\TemplateResult;
|
|
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
|
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
|
use Psalm\CodeLocation;
|
|
use Psalm\Context;
|
|
use Psalm\Issue\InvalidPassByReference;
|
|
use Psalm\Issue\InvalidNamedArgument;
|
|
use Psalm\Issue\PossiblyUndefinedVariable;
|
|
use Psalm\Issue\TooFewArguments;
|
|
use Psalm\Issue\TooManyArguments;
|
|
use Psalm\IssueBuffer;
|
|
use Psalm\Storage\ClassLikeStorage;
|
|
use Psalm\Storage\FunctionLikeParameter;
|
|
use Psalm\Storage\FunctionLikeStorage;
|
|
use Psalm\Type;
|
|
use Psalm\Type\Atomic\TKeyedArray;
|
|
use Psalm\Type\Atomic\TArray;
|
|
use Psalm\Type\Atomic\TList;
|
|
use function strtolower;
|
|
use function strpos;
|
|
use function count;
|
|
use function in_array;
|
|
use function array_reverse;
|
|
use function is_string;
|
|
use function array_map;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class ArgumentsAnalyzer
|
|
{
|
|
/**
|
|
* @param list<PhpParser\Node\Arg> $args
|
|
* @param array<int, FunctionLikeParameter>|null $function_params
|
|
*
|
|
* @return false|null
|
|
*/
|
|
public static function analyze(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
array $args,
|
|
?array $function_params,
|
|
?string $method_id,
|
|
bool $allow_named_args,
|
|
Context $context,
|
|
?TemplateResult $template_result = null
|
|
): ?bool {
|
|
$last_param = $function_params
|
|
? $function_params[count($function_params) - 1]
|
|
: null;
|
|
|
|
// if this modifies the array type based on further args
|
|
if ($method_id
|
|
&& in_array($method_id, ['array_push', 'array_unshift'], true)
|
|
&& $function_params
|
|
&& isset($args[0])
|
|
&& isset($args[1])
|
|
) {
|
|
if (ArrayFunctionArgumentsAnalyzer::handleAddition(
|
|
$statements_analyzer,
|
|
$args,
|
|
$context,
|
|
$method_id === 'array_push'
|
|
) === false
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($method_id && $method_id === 'array_splice' && $function_params && count($args) > 1) {
|
|
if (ArrayFunctionArgumentsAnalyzer::handleSplice($statements_analyzer, $args, $context) === false) {
|
|
return false;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($method_id === 'array_map') {
|
|
$args = array_reverse($args, true);
|
|
}
|
|
|
|
foreach ($args as $argument_offset => $arg) {
|
|
if ($function_params === null) {
|
|
if (self::evaluateAribitraryParam(
|
|
$statements_analyzer,
|
|
$arg,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$param = null;
|
|
|
|
if ($arg->name && $allow_named_args) {
|
|
foreach ($function_params as $candidate_param) {
|
|
if ($candidate_param->name === $arg->name->name) {
|
|
$param = $candidate_param;
|
|
break;
|
|
}
|
|
}
|
|
} elseif ($argument_offset < count($function_params)) {
|
|
$param = $function_params[$argument_offset];
|
|
} elseif ($last_param && $last_param->is_variadic) {
|
|
$param = $last_param;
|
|
}
|
|
|
|
$by_ref = $param && $param->by_ref;
|
|
|
|
$by_ref_type = null;
|
|
|
|
if ($by_ref && $param) {
|
|
$by_ref_type = $param->type ? clone $param->type : Type::getMixed();
|
|
}
|
|
|
|
if ($by_ref
|
|
&& $by_ref_type
|
|
&& !($arg->value instanceof PhpParser\Node\Expr\Closure
|
|
|| $arg->value instanceof PhpParser\Node\Expr\ConstFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\FuncCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\MethodCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\StaticCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\New_
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Assign
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Array_
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Ternary
|
|
|| $arg->value instanceof PhpParser\Node\Expr\BinaryOp
|
|
)
|
|
) {
|
|
if (self::handleByRefFunctionArg(
|
|
$statements_analyzer,
|
|
$method_id,
|
|
$argument_offset,
|
|
$arg,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$toggled_class_exists = false;
|
|
|
|
if ($method_id === 'class_exists'
|
|
&& $argument_offset === 0
|
|
&& !$context->inside_class_exists
|
|
) {
|
|
$context->inside_class_exists = true;
|
|
$toggled_class_exists = true;
|
|
}
|
|
|
|
if (($arg->value instanceof PhpParser\Node\Expr\Closure
|
|
|| $arg->value instanceof PhpParser\Node\Expr\ArrowFunction)
|
|
&& $template_result
|
|
&& $template_result->upper_bounds
|
|
&& $param
|
|
&& !$arg->value->getDocComment()
|
|
) {
|
|
self::handleClosureArg(
|
|
$statements_analyzer,
|
|
$args,
|
|
$method_id,
|
|
$context,
|
|
$template_result,
|
|
$argument_offset,
|
|
$arg,
|
|
$param
|
|
);
|
|
}
|
|
|
|
$was_inside_call = $context->inside_call;
|
|
|
|
$context->inside_call = true;
|
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!$was_inside_call) {
|
|
$context->inside_call = false;
|
|
}
|
|
|
|
if (($argument_offset === 0 && $method_id === 'array_filter' && count($args) === 2)
|
|
|| ($argument_offset > 0 && $method_id === 'array_map' && count($args) >= 2)
|
|
) {
|
|
self::handleArrayMapFilterArrayArg(
|
|
$statements_analyzer,
|
|
$method_id,
|
|
$argument_offset,
|
|
$arg,
|
|
$context,
|
|
$template_result
|
|
);
|
|
}
|
|
|
|
if ($toggled_class_exists) {
|
|
$context->inside_class_exists = false;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static function handleArrayMapFilterArrayArg(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
string $method_id,
|
|
int $argument_offset,
|
|
PhpParser\Node\Arg $arg,
|
|
Context $context,
|
|
?TemplateResult &$template_result
|
|
) : void {
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
$generic_param_type = new Type\Union([
|
|
new Type\Atomic\TArray([
|
|
Type::getArrayKey(),
|
|
new Type\Union([
|
|
new Type\Atomic\TTemplateParam(
|
|
'ArrayValue' . $argument_offset,
|
|
Type::getMixed(),
|
|
$method_id
|
|
)
|
|
])
|
|
])
|
|
]);
|
|
|
|
$template_types = ['ArrayValue' . $argument_offset => [$method_id => Type::getMixed()]];
|
|
|
|
$replace_template_result = new \Psalm\Internal\Type\TemplateResult(
|
|
$template_types,
|
|
[]
|
|
);
|
|
|
|
$existing_type = $statements_analyzer->node_data->getType($arg->value);
|
|
|
|
\Psalm\Internal\Type\TemplateStandinTypeReplacer::replace(
|
|
$generic_param_type,
|
|
$replace_template_result,
|
|
$codebase,
|
|
$statements_analyzer,
|
|
$existing_type,
|
|
$argument_offset,
|
|
'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
|
|
);
|
|
|
|
if ($replace_template_result->upper_bounds) {
|
|
if (!$template_result) {
|
|
$template_result = new TemplateResult([], []);
|
|
}
|
|
|
|
$template_result->upper_bounds += $replace_template_result->upper_bounds;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, PhpParser\Node\Arg> $args
|
|
*/
|
|
private static function handleClosureArg(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
array $args,
|
|
?string $method_id,
|
|
Context $context,
|
|
TemplateResult $template_result,
|
|
int $argument_offset,
|
|
PhpParser\Node\Arg $arg,
|
|
FunctionLikeParameter $param
|
|
) : void {
|
|
if (!$param->type) {
|
|
return;
|
|
}
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
if (($argument_offset === 1 && $method_id === 'array_filter' && count($args) === 2)
|
|
|| ($argument_offset === 0 && $method_id === 'array_map' && count($args) >= 2)
|
|
) {
|
|
$function_like_params = [];
|
|
|
|
foreach ($template_result->upper_bounds as $template_name => $_) {
|
|
$function_like_params[] = new \Psalm\Storage\FunctionLikeParameter(
|
|
'function',
|
|
false,
|
|
new Type\Union([
|
|
new Type\Atomic\TTemplateParam(
|
|
$template_name,
|
|
Type::getMixed(),
|
|
$method_id
|
|
)
|
|
])
|
|
);
|
|
}
|
|
|
|
$replaced_type = new Type\Union([
|
|
new Type\Atomic\TCallable(
|
|
'callable',
|
|
array_reverse($function_like_params)
|
|
)
|
|
]);
|
|
} else {
|
|
$replaced_type = clone $param->type;
|
|
}
|
|
|
|
$replace_template_result = new \Psalm\Internal\Type\TemplateResult(
|
|
array_map(
|
|
function ($template_map) {
|
|
return array_map(
|
|
function ($bound) {
|
|
return $bound->type;
|
|
},
|
|
$template_map
|
|
);
|
|
},
|
|
$template_result->upper_bounds
|
|
),
|
|
[]
|
|
);
|
|
|
|
$replaced_type = \Psalm\Internal\Type\TemplateStandinTypeReplacer::replace(
|
|
$replaced_type,
|
|
$replace_template_result,
|
|
$codebase,
|
|
$statements_analyzer,
|
|
null,
|
|
null,
|
|
null,
|
|
'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
|
|
);
|
|
|
|
TemplateInferredTypeReplacer::replace(
|
|
$replaced_type,
|
|
$replace_template_result,
|
|
$codebase
|
|
);
|
|
|
|
$closure_id = strtolower($statements_analyzer->getFilePath())
|
|
. ':' . $arg->value->getLine()
|
|
. ':' . (int)$arg->value->getAttribute('startFilePos')
|
|
. ':-:closure';
|
|
|
|
try {
|
|
$closure_storage = $codebase->getClosureStorage(
|
|
$statements_analyzer->getFilePath(),
|
|
$closure_id
|
|
);
|
|
} catch (\UnexpectedValueException $e) {
|
|
return;
|
|
}
|
|
|
|
foreach ($closure_storage->params as $closure_param_offset => $param_storage) {
|
|
$param_type_inferred = $param_storage->type_inferred;
|
|
|
|
$newly_inferred_type = null;
|
|
$has_different_docblock_type = false;
|
|
|
|
if ($param_storage->type && !$param_type_inferred) {
|
|
if ($param_storage->type !== $param_storage->signature_type) {
|
|
$has_different_docblock_type = true;
|
|
}
|
|
}
|
|
|
|
if (!$has_different_docblock_type) {
|
|
foreach ($replaced_type->getAtomicTypes() as $replaced_type_part) {
|
|
if ($replaced_type_part instanceof Type\Atomic\TCallable
|
|
|| $replaced_type_part instanceof Type\Atomic\TClosure
|
|
) {
|
|
if (isset($replaced_type_part->params[$closure_param_offset]->type)
|
|
&& !$replaced_type_part->params[$closure_param_offset]->type->hasTemplate()
|
|
) {
|
|
if ($param_storage->type && !$param_type_inferred) {
|
|
$type_match_found = UnionTypeComparator::isContainedBy(
|
|
$codebase,
|
|
$replaced_type_part->params[$closure_param_offset]->type,
|
|
$param_storage->type
|
|
);
|
|
|
|
if (!$type_match_found) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!$newly_inferred_type) {
|
|
$newly_inferred_type = $replaced_type_part->params[$closure_param_offset]->type;
|
|
} else {
|
|
$newly_inferred_type = Type::combineUnionTypes(
|
|
$newly_inferred_type,
|
|
$replaced_type_part->params[$closure_param_offset]->type,
|
|
$codebase
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($newly_inferred_type) {
|
|
$param_storage->type = $newly_inferred_type;
|
|
$param_storage->type_inferred = true;
|
|
}
|
|
|
|
if ($param_storage->type && ($method_id === 'array_map' || $method_id === 'array_filter')) {
|
|
ArrayFetchAnalyzer::taintArrayFetch(
|
|
$statements_analyzer,
|
|
$args[1 - $argument_offset]->value,
|
|
null,
|
|
$param_storage->type,
|
|
Type::getMixed()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<PhpParser\Node\Arg> $args
|
|
* @param string|MethodIdentifier|null $method_id
|
|
* @param array<int,FunctionLikeParameter> $function_params
|
|
*
|
|
* @return false|null
|
|
*
|
|
* @psalm-suppress ComplexMethod there's just not much that can be done about this
|
|
*/
|
|
public static function checkArgumentsMatch(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
array $args,
|
|
$method_id,
|
|
array $function_params,
|
|
?FunctionLikeStorage $function_storage,
|
|
?ClassLikeStorage $class_storage,
|
|
?TemplateResult $class_template_result,
|
|
CodeLocation $code_location,
|
|
Context $context
|
|
): ?bool {
|
|
$in_call_map = $method_id ? InternalCallMapHandler::inCallMap((string) $method_id) : false;
|
|
|
|
$cased_method_id = (string) $method_id;
|
|
|
|
$is_variadic = false;
|
|
|
|
$fq_class_name = null;
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
if ($method_id) {
|
|
if (!$in_call_map && $method_id instanceof MethodIdentifier) {
|
|
$fq_class_name = $method_id->fq_class_name;
|
|
}
|
|
|
|
if ($function_storage) {
|
|
$is_variadic = $function_storage->variadic;
|
|
} elseif (is_string($method_id)) {
|
|
$is_variadic = $codebase->functions->isVariadic(
|
|
$codebase,
|
|
strtolower($method_id),
|
|
$statements_analyzer->getRootFilePath()
|
|
);
|
|
} else {
|
|
$is_variadic = $codebase->methods->isVariadic($method_id);
|
|
}
|
|
}
|
|
|
|
if ($method_id instanceof MethodIdentifier) {
|
|
$cased_method_id = $codebase->methods->getCasedMethodId($method_id);
|
|
} elseif ($function_storage) {
|
|
$cased_method_id = $function_storage->cased_name;
|
|
}
|
|
|
|
$calling_class_storage = $class_storage;
|
|
|
|
$static_fq_class_name = $fq_class_name;
|
|
$self_fq_class_name = $fq_class_name;
|
|
|
|
if ($method_id instanceof MethodIdentifier) {
|
|
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
|
|
|
if ($declaring_method_id && (string)$declaring_method_id !== (string)$method_id) {
|
|
$self_fq_class_name = $declaring_method_id->fq_class_name;
|
|
$class_storage = $codebase->classlike_storage_provider->get($self_fq_class_name);
|
|
}
|
|
|
|
$appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
|
|
|
|
if ($appearing_method_id && $declaring_method_id !== $appearing_method_id) {
|
|
$self_fq_class_name = $appearing_method_id->fq_class_name;
|
|
}
|
|
}
|
|
|
|
if ($function_params) {
|
|
foreach ($function_params as $function_param) {
|
|
$is_variadic = $is_variadic || $function_param->is_variadic;
|
|
}
|
|
}
|
|
|
|
$has_packed_var = false;
|
|
|
|
$packed_var_definite_args = 0;
|
|
|
|
foreach ($args as $arg) {
|
|
if ($arg->unpack) {
|
|
$arg_value_type = $statements_analyzer->node_data->getType($arg->value);
|
|
|
|
if (!$arg_value_type
|
|
|| !$arg_value_type->isSingle()
|
|
|| !$arg_value_type->hasArray()
|
|
) {
|
|
$has_packed_var = true;
|
|
break;
|
|
}
|
|
|
|
foreach ($arg_value_type->getAtomicTypes() as $atomic_arg_type) {
|
|
if (!$atomic_arg_type instanceof TKeyedArray) {
|
|
$has_packed_var = true;
|
|
break 2;
|
|
}
|
|
|
|
$packed_var_definite_args = 0;
|
|
|
|
foreach ($atomic_arg_type->properties as $property_type) {
|
|
if ($property_type->possibly_undefined) {
|
|
$has_packed_var = true;
|
|
} else {
|
|
$packed_var_definite_args++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$has_packed_var) {
|
|
$packed_var_definite_args = \max(0, $packed_var_definite_args - 1);
|
|
}
|
|
|
|
$last_param = $function_params
|
|
? $function_params[count($function_params) - 1]
|
|
: null;
|
|
|
|
$template_result = null;
|
|
|
|
$class_generic_params = [];
|
|
|
|
if ($class_template_result) {
|
|
foreach ($class_template_result->upper_bounds as $template_name => $type_map) {
|
|
foreach ($type_map as $class => $bound) {
|
|
$class_generic_params[$template_name][$class] = clone $bound->type;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($function_storage) {
|
|
$template_result = self::getProvisionalTemplateResultForFunctionLike(
|
|
$statements_analyzer,
|
|
$codebase,
|
|
$context,
|
|
$class_storage,
|
|
$self_fq_class_name,
|
|
$calling_class_storage,
|
|
$function_storage,
|
|
$class_generic_params,
|
|
$class_template_result,
|
|
$args,
|
|
$function_params,
|
|
$last_param
|
|
);
|
|
}
|
|
|
|
$function_param_count = count($function_params);
|
|
|
|
if (count($function_params) > count($args) && !$has_packed_var) {
|
|
for ($i = count($args), $iMax = count($function_params); $i < $iMax; $i++) {
|
|
if ($function_params[$i]->default_type
|
|
&& $function_params[$i]->type
|
|
&& $function_params[$i]->type->hasTemplate()
|
|
&& $function_params[$i]->default_type->hasLiteralValue()
|
|
) {
|
|
ArgumentAnalyzer::checkArgumentMatches(
|
|
$statements_analyzer,
|
|
$cased_method_id,
|
|
$method_id instanceof MethodIdentifier ? $method_id : null,
|
|
$self_fq_class_name,
|
|
$static_fq_class_name,
|
|
$code_location,
|
|
$function_params[$i],
|
|
$i,
|
|
$i,
|
|
$function_storage ? $function_storage->allow_named_arg_calls : true,
|
|
new PhpParser\Node\Arg(
|
|
StubsGenerator::getExpressionFromType($function_params[$i]->default_type)
|
|
),
|
|
$function_params[$i]->default_type,
|
|
$context,
|
|
$class_generic_params,
|
|
$template_result,
|
|
$function_storage ? $function_storage->specialize_call : true,
|
|
$in_call_map
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($method_id === 'preg_match_all' && count($args) > 3) {
|
|
$args = array_reverse($args, true);
|
|
}
|
|
|
|
$arg_function_params = [];
|
|
|
|
foreach ($args as $argument_offset => $arg) {
|
|
if ($arg->unpack && $function_param_count > $argument_offset) {
|
|
for ($i = $argument_offset; $i < $function_param_count; $i++) {
|
|
$arg_function_params[$argument_offset][] = $function_params[$i];
|
|
}
|
|
} elseif ($arg->name && $function_storage && $function_storage->allow_named_arg_calls) {
|
|
foreach ($function_params as $candidate_param) {
|
|
if ($candidate_param->name === $arg->name->name || $candidate_param->is_variadic) {
|
|
$arg_function_params[$argument_offset] = [$candidate_param];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isset($arg_function_params[$argument_offset])) {
|
|
if (IssueBuffer::accepts(
|
|
new InvalidNamedArgument(
|
|
'Parameter $' . $arg->name->name . ' does not exist on function '
|
|
. ($cased_method_id ?: $method_id),
|
|
new CodeLocation($statements_analyzer, $arg->name),
|
|
(string) $method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
}
|
|
} elseif ($function_param_count > $argument_offset) {
|
|
$arg_function_params[$argument_offset] = [$function_params[$argument_offset]];
|
|
} elseif ($last_param && $last_param->is_variadic) {
|
|
$arg_function_params[$argument_offset] = [$last_param];
|
|
}
|
|
}
|
|
|
|
foreach ($args as $argument_offset => $arg) {
|
|
if (!isset($arg_function_params[$argument_offset])) {
|
|
continue;
|
|
}
|
|
|
|
if ($arg_function_params[$argument_offset][0]->by_ref
|
|
&& $method_id !== 'extract'
|
|
) {
|
|
if (self::handlePossiblyMatchingByRefParam(
|
|
$statements_analyzer,
|
|
$codebase,
|
|
(string) $method_id,
|
|
$cased_method_id,
|
|
$last_param,
|
|
$function_params,
|
|
$argument_offset,
|
|
$arg,
|
|
$context,
|
|
$template_result
|
|
) === false) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
$arg_value_type = $statements_analyzer->node_data->getType($arg->value);
|
|
|
|
foreach ($arg_function_params[$argument_offset] as $i => $function_param) {
|
|
if (ArgumentAnalyzer::checkArgumentMatches(
|
|
$statements_analyzer,
|
|
$cased_method_id,
|
|
$method_id instanceof MethodIdentifier ? $method_id : null,
|
|
$self_fq_class_name,
|
|
$static_fq_class_name,
|
|
$code_location,
|
|
$function_param,
|
|
$argument_offset + $i,
|
|
$i,
|
|
$function_storage ? $function_storage->allow_named_arg_calls : true,
|
|
$arg,
|
|
$arg_value_type,
|
|
$context,
|
|
$class_generic_params,
|
|
$template_result,
|
|
$function_storage ? $function_storage->specialize_call : true,
|
|
$in_call_map
|
|
) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
|
|
&& $cased_method_id
|
|
) {
|
|
foreach ($args as $argument_offset => $_) {
|
|
if (!isset($arg_function_params[$argument_offset])) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($arg_function_params[$argument_offset] as $function_param) {
|
|
if ($function_param->sinks) {
|
|
if (!$function_storage || $function_storage->specialize_call) {
|
|
$sink = TaintSink::getForMethodArgument(
|
|
$cased_method_id,
|
|
$cased_method_id,
|
|
$argument_offset,
|
|
$function_param->location,
|
|
$code_location
|
|
);
|
|
} else {
|
|
$sink = TaintSink::getForMethodArgument(
|
|
$cased_method_id,
|
|
$cased_method_id,
|
|
$argument_offset,
|
|
$function_param->location
|
|
);
|
|
}
|
|
|
|
$sink->taints = $function_param->sinks;
|
|
|
|
$statements_analyzer->data_flow_graph->addSink($sink);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($method_id === 'array_map' || $method_id === 'array_filter') {
|
|
if ($method_id === 'array_map' && count($args) < 2) {
|
|
if (IssueBuffer::accepts(
|
|
new TooFewArguments(
|
|
'Too few arguments for ' . $method_id,
|
|
$code_location,
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
} elseif ($method_id === 'array_filter' && count($args) < 1) {
|
|
if (IssueBuffer::accepts(
|
|
new TooFewArguments(
|
|
'Too few arguments for ' . $method_id,
|
|
$code_location,
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
}
|
|
|
|
ArrayFunctionArgumentsAnalyzer::checkArgumentsMatch(
|
|
$statements_analyzer,
|
|
$context,
|
|
$args,
|
|
$method_id,
|
|
$context->check_functions
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
self::checkArgCount(
|
|
$statements_analyzer,
|
|
$codebase,
|
|
$function_storage,
|
|
$context,
|
|
$template_result,
|
|
$is_variadic,
|
|
$args,
|
|
$function_params,
|
|
$in_call_map,
|
|
$method_id,
|
|
$cased_method_id,
|
|
$code_location,
|
|
$has_packed_var,
|
|
$packed_var_definite_args
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, FunctionLikeParameter> $function_params
|
|
* @return false|null
|
|
*/
|
|
private static function handlePossiblyMatchingByRefParam(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
Codebase $codebase,
|
|
?string $method_id,
|
|
?string $cased_method_id,
|
|
?FunctionLikeParameter $last_param,
|
|
array $function_params,
|
|
int $argument_offset,
|
|
PhpParser\Node\Arg $arg,
|
|
Context $context,
|
|
?TemplateResult $template_result
|
|
): ?bool {
|
|
if ($arg->value instanceof PhpParser\Node\Scalar
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Cast
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Array_
|
|
|| $arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\BinaryOp
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Ternary
|
|
|| (
|
|
(
|
|
$arg->value instanceof PhpParser\Node\Expr\ConstFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\FuncCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\MethodCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\StaticCall
|
|
) && (
|
|
!($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
|
|
|| !$arg_value_type->by_ref
|
|
)
|
|
)
|
|
) {
|
|
if (IssueBuffer::accepts(
|
|
new InvalidPassByReference(
|
|
'Parameter ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects a variable',
|
|
new CodeLocation($statements_analyzer->getSource(), $arg->value)
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!in_array(
|
|
$method_id,
|
|
[
|
|
'ksort', 'asort', 'krsort', 'arsort', 'natcasesort', 'natsort',
|
|
'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift',
|
|
'array_push', 'array_unshift', 'socket_select', 'array_splice',
|
|
],
|
|
true
|
|
)) {
|
|
$by_ref_type = null;
|
|
$by_ref_out_type = null;
|
|
|
|
$check_null_ref = true;
|
|
|
|
if ($last_param) {
|
|
if ($argument_offset < count($function_params)) {
|
|
$function_param = $function_params[$argument_offset];
|
|
} else {
|
|
$function_param = $last_param;
|
|
}
|
|
|
|
$by_ref_type = $function_param->type;
|
|
$by_ref_out_type = $function_param->out_type;
|
|
|
|
if ($by_ref_type && $by_ref_type->isNullable()) {
|
|
$check_null_ref = false;
|
|
}
|
|
|
|
if ($template_result && $by_ref_type) {
|
|
$original_by_ref_type = clone $by_ref_type;
|
|
|
|
$by_ref_type = TemplateStandinTypeReplacer::replace(
|
|
clone $by_ref_type,
|
|
$template_result,
|
|
$codebase,
|
|
$statements_analyzer,
|
|
$statements_analyzer->node_data->getType($arg->value),
|
|
$argument_offset,
|
|
'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
|
|
);
|
|
|
|
if ($template_result->upper_bounds) {
|
|
TemplateInferredTypeReplacer::replace(
|
|
$original_by_ref_type,
|
|
$template_result,
|
|
$codebase
|
|
);
|
|
|
|
$by_ref_type = $original_by_ref_type;
|
|
}
|
|
}
|
|
|
|
if ($template_result && $by_ref_out_type) {
|
|
$original_by_ref_out_type = clone $by_ref_out_type;
|
|
|
|
$by_ref_out_type = TemplateStandinTypeReplacer::replace(
|
|
clone $by_ref_out_type,
|
|
$template_result,
|
|
$codebase,
|
|
$statements_analyzer,
|
|
$statements_analyzer->node_data->getType($arg->value),
|
|
$argument_offset,
|
|
'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
|
|
);
|
|
|
|
if ($template_result->upper_bounds) {
|
|
TemplateInferredTypeReplacer::replace(
|
|
$original_by_ref_out_type,
|
|
$template_result,
|
|
$codebase
|
|
);
|
|
|
|
$by_ref_out_type = $original_by_ref_out_type;
|
|
}
|
|
}
|
|
|
|
if ($by_ref_type && $function_param->is_variadic && $arg->unpack) {
|
|
$by_ref_type = new Type\Union([
|
|
new Type\Atomic\TArray([
|
|
Type::getInt(),
|
|
$by_ref_type,
|
|
]),
|
|
]);
|
|
}
|
|
}
|
|
|
|
$by_ref_type = $by_ref_type ?: Type::getMixed();
|
|
|
|
AssignmentAnalyzer::assignByRefParam(
|
|
$statements_analyzer,
|
|
$arg->value,
|
|
$by_ref_type,
|
|
$by_ref_out_type ?: $by_ref_type,
|
|
$context,
|
|
$method_id && (strpos($method_id, '::') !== false || !InternalCallMapHandler::inCallMap($method_id)),
|
|
$check_null_ref
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return false|null
|
|
*/
|
|
private static function evaluateAribitraryParam(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
PhpParser\Node\Arg $arg,
|
|
Context $context
|
|
): ?bool {
|
|
// there are a bunch of things we want to evaluate even when we don't
|
|
// know what function/method is being called
|
|
if ($arg->value instanceof PhpParser\Node\Expr\Closure
|
|
|| $arg->value instanceof PhpParser\Node\Expr\ConstFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\FuncCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\MethodCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\StaticCall
|
|
|| $arg->value instanceof PhpParser\Node\Expr\New_
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Cast
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Assign
|
|
|| $arg->value instanceof PhpParser\Node\Expr\ArrayDimFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\PropertyFetch
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Array_
|
|
|| $arg->value instanceof PhpParser\Node\Expr\BinaryOp
|
|
|| $arg->value instanceof PhpParser\Node\Expr\Ternary
|
|
|| $arg->value instanceof PhpParser\Node\Scalar\Encapsed
|
|
|| $arg->value instanceof PhpParser\Node\Expr\PostInc
|
|
|| $arg->value instanceof PhpParser\Node\Expr\PostDec
|
|
|| $arg->value instanceof PhpParser\Node\Expr\PreInc
|
|
|| $arg->value instanceof PhpParser\Node\Expr\PreDec
|
|
) {
|
|
$was_inside_call = $context->inside_call;
|
|
$context->inside_call = true;
|
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!$was_inside_call) {
|
|
$context->inside_call = false;
|
|
}
|
|
}
|
|
|
|
if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch
|
|
&& $arg->value->name instanceof PhpParser\Node\Identifier
|
|
) {
|
|
$var_id = '$' . $arg->value->name->name;
|
|
} else {
|
|
$var_id = ExpressionIdentifier::getVarId(
|
|
$arg->value,
|
|
$statements_analyzer->getFQCLN(),
|
|
$statements_analyzer
|
|
);
|
|
}
|
|
|
|
if ($var_id) {
|
|
if ($arg->value instanceof PhpParser\Node\Expr\Variable) {
|
|
$statements_analyzer->registerPossiblyUndefinedVariable($var_id, $arg->value);
|
|
}
|
|
|
|
if (!$context->hasVariable($var_id)
|
|
|| $context->vars_in_scope[$var_id]->isNull()
|
|
) {
|
|
if (!isset($context->vars_in_scope[$var_id])
|
|
&& $arg->value instanceof PhpParser\Node\Expr\Variable
|
|
) {
|
|
if (IssueBuffer::accepts(
|
|
new PossiblyUndefinedVariable(
|
|
'Variable ' . $var_id
|
|
. ' must be defined prior to use within an unknown function or method',
|
|
new CodeLocation($statements_analyzer->getSource(), $arg->value)
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
}
|
|
|
|
// we don't know if it exists, assume it's passed by reference
|
|
$context->vars_in_scope[$var_id] = Type::getMixed();
|
|
$context->vars_possibly_in_scope[$var_id] = true;
|
|
} else {
|
|
$was_inside_call = $context->inside_call;
|
|
$context->inside_call = true;
|
|
ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context);
|
|
$context->inside_call = $was_inside_call;
|
|
|
|
$context->removeVarFromConflictingClauses(
|
|
$var_id,
|
|
$context->vars_in_scope[$var_id],
|
|
$statements_analyzer
|
|
);
|
|
|
|
foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $type) {
|
|
if ($type instanceof TArray && $type->type_params[1]->isEmpty()) {
|
|
$context->vars_in_scope[$var_id]->removeType('array');
|
|
$context->vars_in_scope[$var_id]->addType(
|
|
new TArray(
|
|
[Type::getArrayKey(), Type::getMixed()]
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return false|null
|
|
*/
|
|
private static function handleByRefFunctionArg(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
?string $method_id,
|
|
int $argument_offset,
|
|
PhpParser\Node\Arg $arg,
|
|
Context $context
|
|
): ?bool {
|
|
$var_id = ExpressionIdentifier::getVarId(
|
|
$arg->value,
|
|
$statements_analyzer->getFQCLN(),
|
|
$statements_analyzer
|
|
);
|
|
|
|
$builtin_array_functions = [
|
|
'ksort', 'asort', 'krsort', 'arsort', 'natcasesort', 'natsort',
|
|
'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift',
|
|
];
|
|
|
|
if (($var_id && isset($context->vars_in_scope[$var_id]))
|
|
|| ($method_id
|
|
&& in_array(
|
|
$method_id,
|
|
$builtin_array_functions,
|
|
true
|
|
))
|
|
) {
|
|
$was_inside_assignment = $context->inside_assignment;
|
|
$context->inside_assignment = true;
|
|
|
|
// if the variable is in scope, get or we're in a special array function,
|
|
// figure out its type before proceeding
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$arg->value,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
$context->inside_assignment = $was_inside_assignment;
|
|
}
|
|
|
|
// special handling for array sort
|
|
if ($argument_offset === 0
|
|
&& $method_id
|
|
&& in_array(
|
|
$method_id,
|
|
$builtin_array_functions,
|
|
true
|
|
)
|
|
) {
|
|
if (in_array($method_id, ['array_pop', 'array_shift'], true)) {
|
|
ArrayFunctionArgumentsAnalyzer::handleByRefArrayAdjustment(
|
|
$statements_analyzer,
|
|
$arg,
|
|
$context,
|
|
$method_id === 'array_shift'
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
// noops
|
|
if (in_array($method_id, ['reset', 'end', 'next', 'prev', 'ksort'], true)) {
|
|
return null;
|
|
}
|
|
|
|
if (($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
|
|
&& $arg_value_type->hasArray()
|
|
) {
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
* @var TArray|TList|TKeyedArray
|
|
*/
|
|
$array_type = $arg_value_type->getAtomicTypes()['array'];
|
|
|
|
if ($array_type instanceof TKeyedArray) {
|
|
$array_type = $array_type->getGenericArrayType();
|
|
}
|
|
|
|
if ($array_type instanceof TList) {
|
|
$array_type = new TArray([Type::getInt(), $array_type->type_param]);
|
|
}
|
|
|
|
$by_ref_type = new Type\Union([clone $array_type]);
|
|
|
|
AssignmentAnalyzer::assignByRefParam(
|
|
$statements_analyzer,
|
|
$arg->value,
|
|
$by_ref_type,
|
|
$by_ref_type,
|
|
$context,
|
|
false
|
|
);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if ($method_id === 'socket_select') {
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$arg->value,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!$arg->value instanceof PhpParser\Node\Expr\Variable) {
|
|
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
|
|
|
if (!in_array('EmptyArrayAccess', $suppressed_issues, true)) {
|
|
$statements_analyzer->addSuppressedIssues(['EmptyArrayAccess']);
|
|
}
|
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!in_array('EmptyArrayAccess', $suppressed_issues, true)) {
|
|
$statements_analyzer->removeSuppressedIssues(['EmptyArrayAccess']);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param list<PhpParser\Node\Arg> $args
|
|
* @param array<int,FunctionLikeParameter> $function_params
|
|
* @param array<string, array<string, Type\Union>> $class_generic_params
|
|
*/
|
|
private static function getProvisionalTemplateResultForFunctionLike(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
Codebase $codebase,
|
|
Context $context,
|
|
?ClassLikeStorage $class_storage,
|
|
?string $self_fq_class_name,
|
|
?ClassLikeStorage $calling_class_storage,
|
|
FunctionLikeStorage $function_storage,
|
|
array $class_generic_params,
|
|
?TemplateResult $class_template_result,
|
|
array $args,
|
|
array $function_params,
|
|
?FunctionLikeParameter $last_param
|
|
): ?TemplateResult {
|
|
$template_types = CallAnalyzer::getTemplateTypesForCall(
|
|
$codebase,
|
|
$class_storage,
|
|
$self_fq_class_name,
|
|
$calling_class_storage,
|
|
$function_storage->template_types ?: [],
|
|
$class_generic_params
|
|
);
|
|
|
|
if (!$template_types) {
|
|
return null;
|
|
}
|
|
|
|
if (!$class_template_result) {
|
|
return new TemplateResult($template_types, []);
|
|
}
|
|
|
|
$template_result = $class_template_result;
|
|
|
|
if (!$template_result->template_types) {
|
|
$template_result->template_types = $template_types;
|
|
}
|
|
|
|
foreach ($args as $argument_offset => $arg) {
|
|
$function_param = null;
|
|
|
|
if ($arg->name && $function_storage->allow_named_arg_calls) {
|
|
foreach ($function_params as $candidate_param) {
|
|
if ($candidate_param->name === $arg->name->name) {
|
|
$function_param = $candidate_param;
|
|
break;
|
|
}
|
|
}
|
|
} elseif ($argument_offset < count($function_params)) {
|
|
$function_param = $function_params[$argument_offset];
|
|
} elseif ($last_param && $last_param->is_variadic) {
|
|
$function_param = $last_param;
|
|
}
|
|
|
|
if (!$function_param
|
|
|| !$function_param->type
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$arg_value_type = $statements_analyzer->node_data->getType($arg->value);
|
|
|
|
if (!$arg_value_type) {
|
|
continue;
|
|
}
|
|
|
|
$fleshed_out_param_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
|
$codebase,
|
|
$function_param->type,
|
|
$class_storage ? $class_storage->name : null,
|
|
$calling_class_storage ? $calling_class_storage->name : null,
|
|
null,
|
|
true,
|
|
false,
|
|
$calling_class_storage ? $calling_class_storage->final : false
|
|
);
|
|
|
|
TemplateStandinTypeReplacer::replace(
|
|
$fleshed_out_param_type,
|
|
$template_result,
|
|
$codebase,
|
|
$statements_analyzer,
|
|
$arg_value_type,
|
|
$argument_offset,
|
|
$context->self,
|
|
$context->calling_method_id ?: $context->calling_function_id,
|
|
false
|
|
);
|
|
}
|
|
|
|
return $template_result;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, PhpParser\Node\Arg> $args
|
|
* @param string|MethodIdentifier|null $method_id
|
|
* @param array<int,FunctionLikeParameter> $function_params
|
|
*/
|
|
private static function checkArgCount(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
Codebase $codebase,
|
|
?FunctionLikeStorage $function_storage,
|
|
Context $context,
|
|
?TemplateResult $template_result,
|
|
bool $is_variadic,
|
|
array $args,
|
|
array $function_params,
|
|
bool $in_call_map,
|
|
$method_id,
|
|
?string $cased_method_id,
|
|
CodeLocation $code_location,
|
|
bool $has_packed_var,
|
|
int $packed_var_definite_args
|
|
): void {
|
|
if (!$is_variadic
|
|
&& count($args) > count($function_params)
|
|
&& (!count($function_params) || $function_params[count($function_params) - 1]->name !== '...=')
|
|
&& ($in_call_map
|
|
|| !$function_storage instanceof \Psalm\Storage\MethodStorage
|
|
|| $function_storage->is_static
|
|
|| ($method_id instanceof MethodIdentifier
|
|
&& $method_id->method_name === '__construct'))
|
|
) {
|
|
if (IssueBuffer::accepts(
|
|
new TooManyArguments(
|
|
'Too many arguments for ' . ($cased_method_id ?: $method_id)
|
|
. ' - expecting ' . count($function_params) . ' but saw ' . count($args),
|
|
$code_location,
|
|
(string)$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
|
|
//return null;
|
|
}
|
|
|
|
if (!$has_packed_var && count($args) < count($function_params)) {
|
|
if ($function_storage) {
|
|
$expected_param_count = $function_storage->required_param_count;
|
|
} else {
|
|
for ($i = 0, $j = count($function_params); $i < $j; ++$i) {
|
|
$param = $function_params[$i];
|
|
|
|
if ($param->is_optional || $param->is_variadic) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$expected_param_count = $i;
|
|
}
|
|
|
|
for ($i = count($args) + $packed_var_definite_args, $j = count($function_params); $i < $j; ++$i) {
|
|
$param = $function_params[$i];
|
|
|
|
if (!$param->is_optional
|
|
&& !$param->is_variadic
|
|
&& ($in_call_map
|
|
|| !$function_storage instanceof \Psalm\Storage\MethodStorage
|
|
|| $function_storage->is_static
|
|
|| ($method_id instanceof MethodIdentifier
|
|
&& $method_id->method_name === '__construct'))
|
|
) {
|
|
if (IssueBuffer::accepts(
|
|
new TooFewArguments(
|
|
'Too few arguments for ' . $cased_method_id
|
|
. ' - expecting ' . $expected_param_count
|
|
. ' but saw ' . (count($args) + $packed_var_definite_args),
|
|
$code_location,
|
|
(string)$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if ($param->is_optional
|
|
&& $param->type
|
|
&& $param->default_type
|
|
&& !$param->is_variadic
|
|
&& $template_result
|
|
) {
|
|
TemplateStandinTypeReplacer::replace(
|
|
$param->type,
|
|
$template_result,
|
|
$codebase,
|
|
$statements_analyzer,
|
|
clone $param->default_type,
|
|
$i,
|
|
$context->self,
|
|
$context->calling_method_id ?: $context->calling_function_id,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|