mirror of
https://github.com/danog/psalm.git
synced 2024-12-16 11:26:55 +01:00
944 lines
34 KiB
PHP
944 lines
34 KiB
PHP
<?php
|
|
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
|
|
|
use PhpParser;
|
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
|
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\ArrayAssignmentAnalyzer;
|
|
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\StatementsAnalyzer;
|
|
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
|
use Psalm\Internal\Type\TypeCombiner;
|
|
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
|
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
|
use Psalm\CodeLocation;
|
|
use Psalm\Context;
|
|
use Psalm\Issue\InvalidArgument;
|
|
use Psalm\Issue\InvalidScalarArgument;
|
|
use Psalm\Issue\MixedArgumentTypeCoercion;
|
|
use Psalm\Issue\PossiblyInvalidArgument;
|
|
use Psalm\Issue\TooFewArguments;
|
|
use Psalm\Issue\TooManyArguments;
|
|
use Psalm\Issue\ArgumentTypeCoercion;
|
|
use Psalm\IssueBuffer;
|
|
use Psalm\Node\Expr\VirtualArrayDimFetch;
|
|
use Psalm\Type;
|
|
use Psalm\Type\Atomic\TKeyedArray;
|
|
use Psalm\Type\Atomic\TArray;
|
|
use Psalm\Type\Atomic\TEmpty;
|
|
use Psalm\Type\Atomic\TList;
|
|
use Psalm\Type\Atomic\TNonEmptyArray;
|
|
use Psalm\Type\Atomic\TNonEmptyList;
|
|
use function strtolower;
|
|
use function strpos;
|
|
use function explode;
|
|
use function count;
|
|
use function array_filter;
|
|
use function assert;
|
|
use Psalm\Internal\Type\TypeExpander;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class ArrayFunctionArgumentsAnalyzer
|
|
{
|
|
/**
|
|
* @param array<int, PhpParser\Node\Arg> $args
|
|
*/
|
|
public static function checkArgumentsMatch(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
Context $context,
|
|
array $args,
|
|
string $method_id,
|
|
bool $check_functions
|
|
): void {
|
|
$closure_index = $method_id === 'array_map' ? 0 : 1;
|
|
|
|
$array_arg_types = [];
|
|
|
|
foreach ($args as $i => $arg) {
|
|
if ($i === 0 && $method_id === 'array_map') {
|
|
continue;
|
|
}
|
|
|
|
if ($i === 1 && $method_id === 'array_filter') {
|
|
break;
|
|
}
|
|
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
* @var TKeyedArray|TArray|TList|null
|
|
*/
|
|
$array_arg_type = ($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
|
|
&& ($types = $arg_value_type->getAtomicTypes())
|
|
&& isset($types['array'])
|
|
? $types['array']
|
|
: null;
|
|
|
|
if ($array_arg_type instanceof TKeyedArray) {
|
|
$array_arg_type = $array_arg_type->getGenericArrayType();
|
|
}
|
|
|
|
if ($array_arg_type instanceof TList) {
|
|
$array_arg_type = new TArray([Type::getInt(), $array_arg_type->type_param]);
|
|
}
|
|
|
|
$array_arg_types[] = $array_arg_type;
|
|
}
|
|
|
|
$closure_arg = isset($args[$closure_index]) ? $args[$closure_index] : null;
|
|
|
|
$closure_arg_type = null;
|
|
|
|
if ($closure_arg) {
|
|
$closure_arg_type = $statements_analyzer->node_data->getType($closure_arg->value);
|
|
}
|
|
|
|
if ($closure_arg && $closure_arg_type) {
|
|
$min_closure_param_count = $max_closure_param_count = count($array_arg_types);
|
|
|
|
if ($method_id === 'array_filter') {
|
|
$max_closure_param_count = count($args) > 2 ? 2 : 1;
|
|
}
|
|
|
|
foreach ($closure_arg_type->getAtomicTypes() as $closure_type) {
|
|
self::checkClosureType(
|
|
$statements_analyzer,
|
|
$context,
|
|
$method_id,
|
|
$closure_type,
|
|
$closure_arg,
|
|
$min_closure_param_count,
|
|
$max_closure_param_count,
|
|
$array_arg_types,
|
|
$check_functions
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param list<PhpParser\Node\Arg> $args
|
|
*
|
|
* @return false|null
|
|
*/
|
|
public static function handleAddition(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
array $args,
|
|
Context $context,
|
|
bool $is_push
|
|
): ?bool {
|
|
$array_arg = $args[0]->value;
|
|
|
|
$unpacked_args = array_filter(
|
|
$args,
|
|
function ($arg) {
|
|
return $arg->unpack;
|
|
}
|
|
);
|
|
|
|
if ($is_push && !$unpacked_args) {
|
|
for ($i = 1, $iMax = count($args); $i < $iMax; $i++) {
|
|
$was_inside_assignment = $context->inside_assignment;
|
|
|
|
$context->inside_assignment = true;
|
|
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$args[$i]->value,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
$context->inside_assignment = $was_inside_assignment;
|
|
|
|
$old_node_data = $statements_analyzer->node_data;
|
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
|
|
|
ArrayAssignmentAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
new VirtualArrayDimFetch(
|
|
$args[0]->value,
|
|
null,
|
|
$args[$i]->value->getAttributes()
|
|
),
|
|
$context,
|
|
$args[$i]->value,
|
|
$statements_analyzer->node_data->getType($args[$i]->value) ?: Type::getMixed()
|
|
);
|
|
|
|
$statements_analyzer->node_data = $old_node_data;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
$context->inside_call = true;
|
|
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$array_arg,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
for ($i = 1, $iMax = count($args); $i < $iMax; $i++) {
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$args[$i]->value,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
|
|
&& $array_arg_type->hasArray()
|
|
) {
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
* @var TArray|TKeyedArray|TList
|
|
*/
|
|
$array_type = $array_arg_type->getAtomicTypes()['array'];
|
|
|
|
$objectlike_list = null;
|
|
|
|
if ($array_type instanceof TKeyedArray) {
|
|
if ($array_type->is_list) {
|
|
$objectlike_list = clone $array_type;
|
|
}
|
|
|
|
$array_type = $array_type->getGenericArrayType();
|
|
|
|
if ($objectlike_list) {
|
|
if ($array_type instanceof TNonEmptyArray) {
|
|
$array_type = new TNonEmptyList($array_type->type_params[1]);
|
|
} else {
|
|
$array_type = new TList($array_type->type_params[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
$by_ref_type = new Type\Union([clone $array_type]);
|
|
|
|
foreach ($args as $argument_offset => $arg) {
|
|
if ($argument_offset === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$arg->value,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
|
|
|| $arg_value_type->hasMixed()
|
|
) {
|
|
$by_ref_type = Type::combineUnionTypes(
|
|
$by_ref_type,
|
|
new Type\Union([new TArray([Type::getInt(), Type::getMixed()])])
|
|
);
|
|
} elseif ($arg->unpack) {
|
|
$arg_value_type = clone $arg_value_type;
|
|
|
|
foreach ($arg_value_type->getAtomicTypes() as $arg_value_atomic_type) {
|
|
if ($arg_value_atomic_type instanceof TKeyedArray) {
|
|
$was_list = $arg_value_atomic_type->is_list;
|
|
|
|
$arg_value_atomic_type = $arg_value_atomic_type->getGenericArrayType();
|
|
|
|
if ($was_list) {
|
|
if ($arg_value_atomic_type instanceof TNonEmptyArray) {
|
|
$arg_value_atomic_type = new TNonEmptyList($arg_value_atomic_type->type_params[1]);
|
|
} else {
|
|
$arg_value_atomic_type = new TList($arg_value_atomic_type->type_params[1]);
|
|
}
|
|
}
|
|
|
|
$arg_value_type->addType($arg_value_atomic_type);
|
|
}
|
|
}
|
|
|
|
$by_ref_type = Type::combineUnionTypes(
|
|
$by_ref_type,
|
|
$arg_value_type
|
|
);
|
|
} else {
|
|
if ($objectlike_list) {
|
|
\array_unshift($objectlike_list->properties, $arg_value_type);
|
|
|
|
$by_ref_type = new Type\Union([$objectlike_list]);
|
|
} elseif ($array_type instanceof TList) {
|
|
$by_ref_type = Type::combineUnionTypes(
|
|
$by_ref_type,
|
|
new Type\Union(
|
|
[
|
|
new TNonEmptyList(clone $arg_value_type),
|
|
]
|
|
)
|
|
);
|
|
} else {
|
|
$by_ref_type = Type::combineUnionTypes(
|
|
$by_ref_type,
|
|
new Type\Union(
|
|
[
|
|
new TNonEmptyArray(
|
|
[
|
|
Type::getInt(),
|
|
clone $arg_value_type
|
|
]
|
|
),
|
|
]
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
AssignmentAnalyzer::assignByRefParam(
|
|
$statements_analyzer,
|
|
$array_arg,
|
|
$by_ref_type,
|
|
$by_ref_type,
|
|
$context,
|
|
false
|
|
);
|
|
}
|
|
|
|
$context->inside_call = false;
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param list<PhpParser\Node\Arg> $args
|
|
*
|
|
* @return false|null
|
|
*/
|
|
public static function handleSplice(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
array $args,
|
|
Context $context
|
|
): ?bool {
|
|
$context->inside_call = true;
|
|
$array_arg = $args[0]->value;
|
|
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$array_arg,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
$offset_arg = $args[1]->value;
|
|
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$offset_arg,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!isset($args[2])) {
|
|
return null;
|
|
}
|
|
|
|
$length_arg = $args[2]->value;
|
|
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$length_arg,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!isset($args[3])) {
|
|
return null;
|
|
}
|
|
|
|
$replacement_arg = $args[3]->value;
|
|
|
|
if (ExpressionAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$replacement_arg,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
$context->inside_call = false;
|
|
|
|
$replacement_arg_type = $statements_analyzer->node_data->getType($replacement_arg);
|
|
|
|
if ($replacement_arg_type
|
|
&& !$replacement_arg_type->hasArray()
|
|
&& $replacement_arg_type->hasString()
|
|
&& $replacement_arg_type->isSingle()
|
|
) {
|
|
$replacement_arg_type = new Type\Union([
|
|
new Type\Atomic\TArray([Type::getInt(), $replacement_arg_type])
|
|
]);
|
|
|
|
$statements_analyzer->node_data->setType($replacement_arg, $replacement_arg_type);
|
|
}
|
|
|
|
if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
|
|
&& $array_arg_type->hasArray()
|
|
&& $replacement_arg_type
|
|
&& $replacement_arg_type->hasArray()
|
|
) {
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
* @var TArray|TKeyedArray|TList
|
|
*/
|
|
$array_type = $array_arg_type->getAtomicTypes()['array'];
|
|
|
|
if ($array_type instanceof TKeyedArray) {
|
|
if ($array_type->is_list) {
|
|
$array_type = new TNonEmptyList($array_type->getGenericValueType());
|
|
} else {
|
|
$array_type = $array_type->getGenericArrayType();
|
|
}
|
|
}
|
|
|
|
if ($array_type instanceof TArray
|
|
&& $array_type->type_params[0]->hasInt()
|
|
&& !$array_type->type_params[0]->hasString()
|
|
) {
|
|
if ($array_type instanceof TNonEmptyArray) {
|
|
$array_type = new TNonEmptyList($array_type->type_params[1]);
|
|
} else {
|
|
$array_type = new TList($array_type->type_params[1]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
* @var TArray|TKeyedArray|TList
|
|
*/
|
|
$replacement_array_type = $replacement_arg_type->getAtomicTypes()['array'];
|
|
|
|
if ($replacement_array_type instanceof TKeyedArray) {
|
|
$was_list = $replacement_array_type->is_list;
|
|
|
|
$replacement_array_type = $replacement_array_type->getGenericArrayType();
|
|
|
|
if ($was_list) {
|
|
if ($replacement_array_type instanceof TNonEmptyArray) {
|
|
$replacement_array_type = new TNonEmptyList($replacement_array_type->type_params[1]);
|
|
} else {
|
|
$replacement_array_type = new TList($replacement_array_type->type_params[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
$by_ref_type = TypeCombiner::combine([$array_type, $replacement_array_type]);
|
|
|
|
AssignmentAnalyzer::assignByRefParam(
|
|
$statements_analyzer,
|
|
$array_arg,
|
|
$by_ref_type,
|
|
$by_ref_type,
|
|
$context,
|
|
false
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
$array_type = Type::getArray();
|
|
|
|
AssignmentAnalyzer::assignByRefParam(
|
|
$statements_analyzer,
|
|
$array_arg,
|
|
$array_type,
|
|
$array_type,
|
|
$context,
|
|
false
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
public static function handleByRefArrayAdjustment(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
PhpParser\Node\Arg $arg,
|
|
Context $context,
|
|
bool $is_array_shift
|
|
): void {
|
|
$var_id = ExpressionIdentifier::getVarId(
|
|
$arg->value,
|
|
$statements_analyzer->getFQCLN(),
|
|
$statements_analyzer
|
|
);
|
|
|
|
if ($var_id) {
|
|
$context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
|
|
|
|
if (isset($context->vars_in_scope[$var_id])) {
|
|
$array_type = clone $context->vars_in_scope[$var_id];
|
|
|
|
$array_atomic_types = $array_type->getAtomicTypes();
|
|
|
|
foreach ($array_atomic_types as $array_atomic_type) {
|
|
if ($array_atomic_type instanceof TKeyedArray) {
|
|
if ($is_array_shift && $array_atomic_type->is_list) {
|
|
$array_atomic_type = clone $array_atomic_type;
|
|
|
|
$array_properties = $array_atomic_type->properties;
|
|
|
|
\array_shift($array_properties);
|
|
|
|
if (!$array_properties) {
|
|
$array_atomic_type = new Type\Atomic\TList(
|
|
$array_atomic_type->previous_value_type ?: Type::getMixed()
|
|
);
|
|
|
|
$array_type->addType($array_atomic_type);
|
|
} else {
|
|
$array_atomic_type->properties = $array_properties;
|
|
}
|
|
}
|
|
|
|
if ($array_atomic_type instanceof TKeyedArray) {
|
|
$array_atomic_type = $array_atomic_type->getGenericArrayType();
|
|
}
|
|
}
|
|
|
|
if ($array_atomic_type instanceof TNonEmptyArray) {
|
|
if (!$context->inside_loop && $array_atomic_type->count !== null) {
|
|
if ($array_atomic_type->count === 0) {
|
|
$array_atomic_type = new TArray(
|
|
[
|
|
new Type\Union([new TEmpty]),
|
|
new Type\Union([new TEmpty]),
|
|
]
|
|
);
|
|
} else {
|
|
$array_atomic_type->count--;
|
|
}
|
|
} else {
|
|
$array_atomic_type = new TArray($array_atomic_type->type_params);
|
|
}
|
|
|
|
$array_type->addType($array_atomic_type);
|
|
} elseif ($array_atomic_type instanceof TNonEmptyList) {
|
|
if (!$context->inside_loop && $array_atomic_type->count !== null) {
|
|
if ($array_atomic_type->count === 0) {
|
|
$array_atomic_type = new TArray(
|
|
[
|
|
new Type\Union([new TEmpty]),
|
|
new Type\Union([new TEmpty]),
|
|
]
|
|
);
|
|
} else {
|
|
$array_atomic_type->count--;
|
|
}
|
|
} else {
|
|
$array_atomic_type = new TList($array_atomic_type->type_param);
|
|
}
|
|
|
|
$array_type->addType($array_atomic_type);
|
|
}
|
|
}
|
|
|
|
$context->removeDescendents($var_id, $array_type);
|
|
$context->vars_in_scope[$var_id] = $array_type;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param (TArray|null)[] $array_arg_types
|
|
*
|
|
*/
|
|
private static function checkClosureType(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
Context $context,
|
|
string $method_id,
|
|
Type\Atomic $closure_type,
|
|
PhpParser\Node\Arg $closure_arg,
|
|
int $min_closure_param_count,
|
|
int $max_closure_param_count,
|
|
array $array_arg_types,
|
|
bool $check_functions
|
|
): void {
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
if (!$closure_type instanceof Type\Atomic\TClosure) {
|
|
if ($method_id === 'array_map') {
|
|
return;
|
|
}
|
|
|
|
if (!$closure_arg->value instanceof PhpParser\Node\Scalar\String_
|
|
&& !$closure_arg->value instanceof PhpParser\Node\Expr\Array_
|
|
&& !$closure_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat
|
|
) {
|
|
return;
|
|
}
|
|
|
|
$function_ids = CallAnalyzer::getFunctionIdsFromCallableArg(
|
|
$statements_analyzer,
|
|
$closure_arg->value
|
|
);
|
|
|
|
$closure_types = [];
|
|
|
|
foreach ($function_ids as $function_id) {
|
|
$function_id = strtolower($function_id);
|
|
|
|
if (strpos($function_id, '::') !== false) {
|
|
if ($function_id[0] === '$') {
|
|
$function_id = \substr($function_id, 1);
|
|
}
|
|
|
|
$function_id_parts = explode('&', $function_id);
|
|
|
|
foreach ($function_id_parts as $function_id_part) {
|
|
[$callable_fq_class_name, $method_name] = explode('::', $function_id_part);
|
|
|
|
switch ($callable_fq_class_name) {
|
|
case 'self':
|
|
case 'static':
|
|
case 'parent':
|
|
$container_class = $statements_analyzer->getFQCLN();
|
|
|
|
if ($callable_fq_class_name === 'parent') {
|
|
$container_class = $statements_analyzer->getParentFQCLN();
|
|
}
|
|
|
|
if (!$container_class) {
|
|
continue 2;
|
|
}
|
|
|
|
$callable_fq_class_name = $container_class;
|
|
}
|
|
|
|
if (!$codebase->classOrInterfaceExists($callable_fq_class_name)) {
|
|
return;
|
|
}
|
|
|
|
$function_id_part = new \Psalm\Internal\MethodIdentifier(
|
|
$callable_fq_class_name,
|
|
strtolower($method_name)
|
|
);
|
|
|
|
try {
|
|
$method_storage = $codebase->methods->getStorage($function_id_part);
|
|
} catch (\UnexpectedValueException $e) {
|
|
// the method may not exist, but we're suppressing that issue
|
|
continue;
|
|
}
|
|
|
|
$closure_types[] = new Type\Atomic\TClosure(
|
|
'Closure',
|
|
$method_storage->params,
|
|
$method_storage->return_type ?: Type::getMixed()
|
|
);
|
|
}
|
|
} else {
|
|
if (!$check_functions) {
|
|
continue;
|
|
}
|
|
|
|
if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) {
|
|
continue;
|
|
}
|
|
|
|
$function_storage = $codebase->functions->getStorage(
|
|
$statements_analyzer,
|
|
$function_id
|
|
);
|
|
|
|
if (InternalCallMapHandler::inCallMap($function_id)) {
|
|
$callmap_callables = InternalCallMapHandler::getCallablesFromCallMap($function_id);
|
|
|
|
if ($callmap_callables === null) {
|
|
throw new \UnexpectedValueException('This should not happen');
|
|
}
|
|
|
|
$passing_callmap_callables = [];
|
|
|
|
foreach ($callmap_callables as $callmap_callable) {
|
|
$required_param_count = 0;
|
|
|
|
assert($callmap_callable->params !== null);
|
|
|
|
foreach ($callmap_callable->params as $i => $param) {
|
|
if (!$param->is_optional && !$param->is_variadic) {
|
|
$required_param_count = $i + 1;
|
|
}
|
|
}
|
|
|
|
if ($required_param_count <= $max_closure_param_count) {
|
|
$passing_callmap_callables[] = $callmap_callable;
|
|
}
|
|
}
|
|
|
|
if ($passing_callmap_callables) {
|
|
foreach ($passing_callmap_callables as $passing_callmap_callable) {
|
|
$closure_types[] = $passing_callmap_callable;
|
|
}
|
|
} else {
|
|
$closure_types[] = $callmap_callables[0];
|
|
}
|
|
} else {
|
|
$closure_types[] = new Type\Atomic\TClosure(
|
|
'Closure',
|
|
$function_storage->params,
|
|
$function_storage->return_type ?: Type::getMixed()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$closure_types = [$closure_type];
|
|
}
|
|
|
|
foreach ($closure_types as $closure_type) {
|
|
if ($closure_type->params === null) {
|
|
continue;
|
|
}
|
|
|
|
self::checkClosureTypeArgs(
|
|
$statements_analyzer,
|
|
$context,
|
|
$method_id,
|
|
$closure_type,
|
|
$closure_arg,
|
|
$min_closure_param_count,
|
|
$max_closure_param_count,
|
|
$array_arg_types
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Type\Atomic\TClosure|Type\Atomic\TCallable $closure_type
|
|
* @param (TArray|null)[] $array_arg_types
|
|
*/
|
|
private static function checkClosureTypeArgs(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
Context $context,
|
|
string $method_id,
|
|
Type\Atomic $closure_type,
|
|
PhpParser\Node\Arg $closure_arg,
|
|
int $min_closure_param_count,
|
|
int $max_closure_param_count,
|
|
array $array_arg_types
|
|
): void {
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
$closure_params = $closure_type->params;
|
|
|
|
if ($closure_params === null) {
|
|
throw new \UnexpectedValueException('Closure params should not be null here');
|
|
}
|
|
|
|
$required_param_count = 0;
|
|
|
|
foreach ($closure_params as $i => $param) {
|
|
if (!$param->is_optional && !$param->is_variadic) {
|
|
$required_param_count = $i + 1;
|
|
}
|
|
}
|
|
|
|
if (count($closure_params) < $min_closure_param_count) {
|
|
$argument_text = $min_closure_param_count === 1 ? 'one argument' : $min_closure_param_count . ' arguments';
|
|
|
|
if (IssueBuffer::accepts(
|
|
new TooManyArguments(
|
|
'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
|
|
. $required_param_count,
|
|
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
|
|
return;
|
|
} elseif ($required_param_count > $max_closure_param_count) {
|
|
$argument_text = $max_closure_param_count === 1 ? 'one argument' : $max_closure_param_count . ' arguments';
|
|
|
|
if (IssueBuffer::accepts(
|
|
new TooFewArguments(
|
|
'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
|
|
. $required_param_count,
|
|
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// abandon attempt to validate closure params if we have an extra arg for ARRAY_FILTER
|
|
if ($method_id === 'array_filter' && $max_closure_param_count > 1) {
|
|
return;
|
|
}
|
|
|
|
foreach ($closure_params as $i => $closure_param) {
|
|
if (!isset($array_arg_types[$i])) {
|
|
continue;
|
|
}
|
|
|
|
$array_arg_type = $array_arg_types[$i];
|
|
|
|
$input_type = $array_arg_type->type_params[1];
|
|
|
|
if ($input_type->hasMixed()) {
|
|
continue;
|
|
}
|
|
|
|
$closure_param_type = $closure_param->type;
|
|
|
|
if (!$closure_param_type) {
|
|
continue;
|
|
}
|
|
|
|
if ($method_id === 'array_map'
|
|
&& $i === 0
|
|
&& $closure_type->return_type
|
|
&& $closure_param_type->hasTemplate()
|
|
) {
|
|
$closure_param_type = clone $closure_param_type;
|
|
$closure_type->return_type = clone $closure_type->return_type;
|
|
|
|
$template_result = new \Psalm\Internal\Type\TemplateResult(
|
|
[],
|
|
[]
|
|
);
|
|
|
|
foreach ($closure_param_type->getTemplateTypes() as $template_type) {
|
|
$template_result->template_types[$template_type->param_name] = [
|
|
($template_type->defining_class) => $template_type->as
|
|
];
|
|
}
|
|
|
|
$closure_param_type = TemplateStandinTypeReplacer::replace(
|
|
$closure_param_type,
|
|
$template_result,
|
|
$codebase,
|
|
$statements_analyzer,
|
|
$input_type,
|
|
$i,
|
|
$context->self,
|
|
$context->calling_method_id ?: $context->calling_function_id
|
|
);
|
|
|
|
$closure_type->replaceTemplateTypesWithArgTypes(
|
|
$template_result,
|
|
$codebase
|
|
);
|
|
}
|
|
|
|
$closure_param_type = TypeExpander::expandUnion(
|
|
$codebase,
|
|
$closure_param_type,
|
|
$context->self,
|
|
null,
|
|
$statements_analyzer->getParentFQCLN()
|
|
);
|
|
|
|
$union_comparison_results = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();
|
|
|
|
$type_match_found = UnionTypeComparator::isContainedBy(
|
|
$codebase,
|
|
$input_type,
|
|
$closure_param_type,
|
|
$input_type->ignore_nullable_issues,
|
|
$input_type->ignore_falsable_issues,
|
|
$union_comparison_results
|
|
);
|
|
|
|
if ($union_comparison_results->type_coerced) {
|
|
if ($union_comparison_results->type_coerced_from_mixed) {
|
|
if (IssueBuffer::accepts(
|
|
new MixedArgumentTypeCoercion(
|
|
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
|
$closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided',
|
|
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// keep soldiering on
|
|
}
|
|
} else {
|
|
if (IssueBuffer::accepts(
|
|
new ArgumentTypeCoercion(
|
|
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
|
$closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided',
|
|
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// keep soldiering on
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$union_comparison_results->type_coerced && !$type_match_found) {
|
|
$types_can_be_identical = UnionTypeComparator::canExpressionTypesBeIdentical(
|
|
$codebase,
|
|
$input_type,
|
|
$closure_param_type
|
|
);
|
|
|
|
if ($union_comparison_results->scalar_type_match_found) {
|
|
if (IssueBuffer::accepts(
|
|
new InvalidScalarArgument(
|
|
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
|
$closure_param_type->getId() . ', ' . $input_type->getId() . ' provided',
|
|
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
} elseif ($types_can_be_identical) {
|
|
if (IssueBuffer::accepts(
|
|
new PossiblyInvalidArgument(
|
|
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects '
|
|
. $closure_param_type->getId() . ', possibly different type '
|
|
. $input_type->getId() . ' provided',
|
|
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
} elseif (IssueBuffer::accepts(
|
|
new InvalidArgument(
|
|
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
|
$closure_param_type->getId() . ', ' . $input_type->getId() . ' provided',
|
|
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
|
$method_id
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// fall through
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|