mirror of
https://github.com/danog/psalm.git
synced 2024-12-16 11:26:55 +01:00
840 lines
30 KiB
PHP
840 lines
30 KiB
PHP
|
<?php
|
||
|
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||
|
|
||
|
use PhpParser;
|
||
|
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\StatementsAnalyzer;
|
||
|
use Psalm\Internal\Analyzer\TypeAnalyzer;
|
||
|
use Psalm\Internal\Codebase\CallMap;
|
||
|
use Psalm\Internal\Type\TypeCombination;
|
||
|
use Psalm\Internal\Type\UnionTemplateHandler;
|
||
|
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\Type;
|
||
|
use Psalm\Type\Atomic\ObjectLike;
|
||
|
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;
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
class ArrayFunctionArgumentsAnalyzer
|
||
|
{
|
||
|
/**
|
||
|
* @param StatementsAnalyzer $statements_analyzer
|
||
|
* @param array<int, PhpParser\Node\Arg> $args
|
||
|
* @param string $method_id
|
||
|
*
|
||
|
* @return false|null
|
||
|
*/
|
||
|
public static function checkArgumentsMatch(
|
||
|
StatementsAnalyzer $statements_analyzer,
|
||
|
Context $context,
|
||
|
array $args,
|
||
|
$method_id,
|
||
|
bool $check_functions
|
||
|
) {
|
||
|
$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 ObjectLike|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 ObjectLike) {
|
||
|
$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 StatementsAnalyzer $statements_analyzer
|
||
|
* @param array<int, PhpParser\Node\Arg> $args
|
||
|
* @param Context $context
|
||
|
*
|
||
|
* @return false|null
|
||
|
*/
|
||
|
public static function handleAddition(
|
||
|
StatementsAnalyzer $statements_analyzer,
|
||
|
array $args,
|
||
|
Context $context,
|
||
|
bool $is_push
|
||
|
) {
|
||
|
$array_arg = $args[0]->value;
|
||
|
|
||
|
$context->inside_call = true;
|
||
|
|
||
|
if (ExpressionAnalyzer::analyze(
|
||
|
$statements_analyzer,
|
||
|
$array_arg,
|
||
|
$context
|
||
|
) === false) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
for ($i = 1; $i < count($args); $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|ObjectLike|TList
|
||
|
*/
|
||
|
$array_type = $array_arg_type->getAtomicTypes()['array'];
|
||
|
|
||
|
$objectlike_list = null;
|
||
|
|
||
|
if ($array_type instanceof ObjectLike) {
|
||
|
if ($array_type->is_list) {
|
||
|
$objectlike_list = clone $array_type;
|
||
|
}
|
||
|
|
||
|
$array_type = $array_type->getGenericArrayType();
|
||
|
}
|
||
|
|
||
|
$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) {
|
||
|
$by_ref_type = Type::combineUnionTypes(
|
||
|
$by_ref_type,
|
||
|
clone $arg_value_type
|
||
|
);
|
||
|
} else {
|
||
|
if ($objectlike_list) {
|
||
|
if ($is_push) {
|
||
|
\array_push($objectlike_list->properties, $arg_value_type);
|
||
|
} else {
|
||
|
\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;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param StatementsAnalyzer $statements_analyzer
|
||
|
* @param array<int, PhpParser\Node\Arg> $args
|
||
|
* @param Context $context
|
||
|
*
|
||
|
* @return false|null
|
||
|
*/
|
||
|
public static function handleSplice(
|
||
|
StatementsAnalyzer $statements_analyzer,
|
||
|
array $args,
|
||
|
Context $context
|
||
|
) {
|
||
|
$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;
|
||
|
}
|
||
|
|
||
|
$length_arg = $args[2]->value;
|
||
|
|
||
|
if (ExpressionAnalyzer::analyze(
|
||
|
$statements_analyzer,
|
||
|
$length_arg,
|
||
|
$context
|
||
|
) === false) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!isset($args[3])) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$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|ObjectLike|TList
|
||
|
*/
|
||
|
$array_type = $array_arg_type->getAtomicTypes()['array'];
|
||
|
|
||
|
if ($array_type instanceof ObjectLike) {
|
||
|
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|ObjectLike|TList
|
||
|
*/
|
||
|
$replacement_array_type = $replacement_arg_type->getAtomicTypes()['array'];
|
||
|
|
||
|
$by_ref_type = TypeCombination::combineTypes([$array_type, $replacement_array_type]);
|
||
|
|
||
|
AssignmentAnalyzer::assignByRefParam(
|
||
|
$statements_analyzer,
|
||
|
$array_arg,
|
||
|
$by_ref_type,
|
||
|
$by_ref_type,
|
||
|
$context,
|
||
|
false
|
||
|
);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$array_type = Type::getArray();
|
||
|
|
||
|
AssignmentAnalyzer::assignByRefParam(
|
||
|
$statements_analyzer,
|
||
|
$array_arg,
|
||
|
$array_type,
|
||
|
$array_type,
|
||
|
$context,
|
||
|
false
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return void
|
||
|
*/
|
||
|
public static function handleByRefArrayAdjustment(
|
||
|
StatementsAnalyzer $statements_analyzer,
|
||
|
PhpParser\Node\Arg $arg,
|
||
|
Context $context
|
||
|
) {
|
||
|
$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 ObjectLike) {
|
||
|
$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);
|
||
|
$context->removeDescendents($var_id, $array_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 string $method_id
|
||
|
* @param int $min_closure_param_count
|
||
|
* @param int $max_closure_param_count [description]
|
||
|
* @param (TArray|null)[] $array_arg_types
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
private static function checkClosureType(
|
||
|
StatementsAnalyzer $statements_analyzer,
|
||
|
Context $context,
|
||
|
$method_id,
|
||
|
Type\Atomic $closure_type,
|
||
|
PhpParser\Node\Arg $closure_arg,
|
||
|
$min_closure_param_count,
|
||
|
$max_closure_param_count,
|
||
|
array $array_arg_types,
|
||
|
bool $check_functions
|
||
|
) {
|
||
|
$codebase = $statements_analyzer->getCodebase();
|
||
|
|
||
|
if (!$closure_type instanceof Type\Atomic\TFn) {
|
||
|
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) {
|
||
|
$function_id_parts = explode('&', $function_id);
|
||
|
|
||
|
foreach ($function_id_parts as $function_id_part) {
|
||
|
list($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,
|
||
|
$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\TFn(
|
||
|
'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 (CallMap::inCallMap($function_id)) {
|
||
|
$callmap_callables = CallMap::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\TFn(
|
||
|
'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\TFn|Type\Atomic\TCallable $closure_type
|
||
|
* @param string $method_id
|
||
|
* @param int $min_closure_param_count
|
||
|
* @param int $max_closure_param_count
|
||
|
* @param (TArray|null)[] $array_arg_types
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
private static function checkClosureTypeArgs(
|
||
|
StatementsAnalyzer $statements_analyzer,
|
||
|
Context $context,
|
||
|
$method_id,
|
||
|
Type\Atomic $closure_type,
|
||
|
PhpParser\Node\Arg $closure_arg,
|
||
|
$min_closure_param_count,
|
||
|
$max_closure_param_count,
|
||
|
array $array_arg_types
|
||
|
) {
|
||
|
$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 = UnionTemplateHandler::replaceTemplateTypesWithStandins(
|
||
|
$closure_param_type,
|
||
|
$template_result,
|
||
|
$codebase,
|
||
|
$statements_analyzer,
|
||
|
$input_type,
|
||
|
$i,
|
||
|
$context->self,
|
||
|
$context->calling_method_id ?: $context->calling_function_id
|
||
|
);
|
||
|
|
||
|
$closure_type->return_type->replaceTemplateTypesWithArgTypes(
|
||
|
$template_result,
|
||
|
$codebase
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
|
||
|
|
||
|
$type_match_found = TypeAnalyzer::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(
|
||
|
'First parameter 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(
|
||
|
'First parameter 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 = TypeAnalyzer::canExpressionTypesBeIdentical(
|
||
|
$codebase,
|
||
|
$input_type,
|
||
|
$closure_param_type
|
||
|
);
|
||
|
|
||
|
if ($union_comparison_results->scalar_type_match_found) {
|
||
|
if (IssueBuffer::accepts(
|
||
|
new InvalidScalarArgument(
|
||
|
'First parameter 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(
|
||
|
'First parameter 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(
|
||
|
'First parameter 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
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|