mirror of
https://github.com/danog/psalm.git
synced 2024-11-29 20:28:59 +01:00
Add support for checking closure return types
This commit is contained in:
parent
34c238c1a7
commit
846cc59d5f
@ -86,6 +86,7 @@
|
||||
<xs:element name="InvalidClass" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="InvalidParamDefault" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="InvalidDocblock" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="InvalidFunctionCall" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="InvalidGlobal" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="InvalidIterator" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="InvalidNamespace" type="IssueHandlerType" minOccurs="0" />
|
||||
|
@ -335,7 +335,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
$this->function->inferredType = new Type\Union([
|
||||
new Type\Fn(
|
||||
'Closure',
|
||||
array_values($function_param_names),
|
||||
$function_params,
|
||||
$closure_return_type ?: Type::getMixed()
|
||||
)
|
||||
]);
|
||||
@ -1070,10 +1070,9 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
/**
|
||||
* @param string $method_id
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @param string $file_path
|
||||
* @return array<int,FunctionLikeParameter>
|
||||
* @return array<int, FunctionLikeParameter>
|
||||
*/
|
||||
public static function getParamsById($method_id, array $args, $file_path)
|
||||
public static function getMethodParamsById($method_id, array $args)
|
||||
{
|
||||
$fq_class_name = strpos($method_id, '::') !== false ? explode('::', $method_id)[0] : null;
|
||||
|
||||
@ -1085,31 +1084,56 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
|
||||
return $method_params;
|
||||
} elseif (!$fq_class_name && FunctionChecker::inCallMap($method_id)) {
|
||||
}
|
||||
|
||||
$declaring_method_id = MethodChecker::getDeclaringMethodId($method_id);
|
||||
|
||||
if (FunctionChecker::inCallMap($declaring_method_id ?: $method_id)) {
|
||||
$function_param_options = FunctionChecker::getParamsFromCallMap($declaring_method_id ?: $method_id);
|
||||
|
||||
if ($function_param_options === null) {
|
||||
throw new \UnexpectedValueException('Not expecting $function_param_options to be null');
|
||||
}
|
||||
|
||||
return self::getMatchingParamsFromCallMapOptions($function_param_options, $args);
|
||||
}
|
||||
|
||||
if ($method_params = MethodChecker::getMethodParams($method_id)) {
|
||||
// fall back to using reflected params anyway
|
||||
return $method_params;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Cannot get params for ' . $method_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method_id
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @param string $file_path
|
||||
* @return array<int, FunctionLikeParameter>
|
||||
*/
|
||||
public static function getFunctionParamsById($method_id, array $args, $file_path)
|
||||
{
|
||||
if (FunctionChecker::inCallMap($method_id)) {
|
||||
$function_param_options = FunctionChecker::getParamsFromCallMap($method_id);
|
||||
|
||||
if ($function_param_options === null) {
|
||||
throw new \UnexpectedValueException('Not expecting $function_param_options to be null');
|
||||
}
|
||||
} elseif ($fq_class_name) {
|
||||
$declaring_method_id = MethodChecker::getDeclaringMethodId($method_id);
|
||||
|
||||
if (FunctionChecker::inCallMap($declaring_method_id ?: $method_id)) {
|
||||
$function_param_options = FunctionChecker::getParamsFromCallMap($declaring_method_id ?: $method_id);
|
||||
|
||||
if ($function_param_options === null) {
|
||||
throw new \UnexpectedValueException('Not expecting $function_param_options to be null');
|
||||
}
|
||||
} elseif ($method_params = MethodChecker::getMethodParams($method_id)) {
|
||||
// fall back to using reflected params anyway
|
||||
return $method_params;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Cannot get params for ' . $method_id);
|
||||
}
|
||||
} else {
|
||||
return FunctionChecker::getParams(strtolower($method_id), $file_path);
|
||||
return self::getMatchingParamsFromCallMapOptions($function_param_options, $args);
|
||||
}
|
||||
|
||||
return FunctionChecker::getParams(strtolower($method_id), $file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<int, FunctionLikeParameter>> $function_param_options
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @return array<int, FunctionLikeParameter>
|
||||
*/
|
||||
protected static function getMatchingParamsFromCallMapOptions(array $function_param_options, array $args)
|
||||
{
|
||||
$function_params = null;
|
||||
|
||||
if (count($function_param_options) === 1) {
|
||||
|
@ -13,9 +13,11 @@ use Psalm\Checker\TraitChecker;
|
||||
use Psalm\Checker\TypeChecker;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\FunctionLikeParameter;
|
||||
use Psalm\Issue\ForbiddenCode;
|
||||
use Psalm\Issue\ImplicitToStringCast;
|
||||
use Psalm\Issue\InvalidArgument;
|
||||
use Psalm\Issue\InvalidFunctionCall;
|
||||
use Psalm\Issue\InvalidScalarArgument;
|
||||
use Psalm\Issue\InvalidScope;
|
||||
use Psalm\Issue\MixedArgument;
|
||||
@ -94,67 +96,131 @@ class CallChecker
|
||||
$method_id = null;
|
||||
|
||||
if ($context->check_functions) {
|
||||
if (!($stmt->name instanceof PhpParser\Node\Name)) {
|
||||
return null;
|
||||
}
|
||||
$in_call_map = false;
|
||||
|
||||
$method_id = implode('\\', $stmt->name->parts);
|
||||
|
||||
$aliased_functions = $statements_checker->getAliasedFunctions();
|
||||
|
||||
if (isset($aliased_functions[strtolower($method_id)])) {
|
||||
$method_id = $aliased_functions[strtolower($method_id)];
|
||||
}
|
||||
|
||||
if ($context->self) {
|
||||
//$method_id = $statements_checker->getFQCLN() . '::' . $method_id;
|
||||
}
|
||||
|
||||
$in_call_map = FunctionChecker::inCallMap($method_id);
|
||||
$function_params = null;
|
||||
|
||||
$code_location = new CodeLocation($statements_checker->getSource(), $stmt);
|
||||
|
||||
if (!$in_call_map &&
|
||||
self::checkFunctionExists($statements_checker, $method_id, $context, $code_location) === false
|
||||
) {
|
||||
return false;
|
||||
if ($stmt->name instanceof PhpParser\Node\Expr) {
|
||||
if (ExpressionChecker::check($statements_checker, $stmt->name, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($stmt->name->inferredType)) {
|
||||
foreach ($stmt->name->inferredType->types as $var_type_part) {
|
||||
if ($var_type_part instanceof Type\Fn) {
|
||||
$function_params = $var_type_part->parameters;
|
||||
|
||||
if ($var_type_part->return_type) {
|
||||
if (isset($stmt->inferredType)) {
|
||||
$stmt->inferredType = Type::combineUnionTypes(
|
||||
$stmt->inferredType,
|
||||
$var_type_part->return_type
|
||||
);
|
||||
} else {
|
||||
$stmt->inferredType = $var_type_part->return_type;
|
||||
}
|
||||
}
|
||||
} elseif (!$var_type_part->isMixed() && $var_type_part->value !== 'Closure') {
|
||||
$var_id = ExpressionChecker::getVarId(
|
||||
$stmt->name,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker->getNamespace(),
|
||||
$statements_checker->getAliasedClasses()
|
||||
);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidFunctionCall(
|
||||
'Cannot treat ' . $var_id . ' of type ' . $var_type_part . ' as function',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($stmt->inferredType)) {
|
||||
$stmt->inferredType = Type::getMixed();
|
||||
}
|
||||
|
||||
} else {
|
||||
$method_id = implode('\\', $stmt->name->parts);
|
||||
|
||||
$aliased_functions = $statements_checker->getAliasedFunctions();
|
||||
|
||||
if (isset($aliased_functions[strtolower($method_id)])) {
|
||||
$method_id = $aliased_functions[strtolower($method_id)];
|
||||
}
|
||||
|
||||
if ($context->self) {
|
||||
//$method_id = $statements_checker->getFQCLN() . '::' . $method_id;
|
||||
}
|
||||
|
||||
$in_call_map = FunctionChecker::inCallMap($method_id);
|
||||
|
||||
if (!$in_call_map &&
|
||||
self::checkFunctionExists($statements_checker, $method_id, $context, $code_location) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$function_params = FunctionLikeChecker::getFunctionParamsById(
|
||||
$method_id,
|
||||
$stmt->args,
|
||||
$statements_checker->getFilePath()
|
||||
);
|
||||
}
|
||||
|
||||
if (self::checkFunctionArguments(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$function_params,
|
||||
$context
|
||||
) === false) {
|
||||
// fall through
|
||||
}
|
||||
|
||||
if ($stmt->name instanceof PhpParser\Node\Name && $method_id) {
|
||||
$function_params = FunctionLikeChecker::getFunctionParamsById(
|
||||
$method_id,
|
||||
$stmt->args,
|
||||
$statements_checker->getFilePath()
|
||||
);
|
||||
}
|
||||
|
||||
if (self::checkFunctionArgumentsMatch(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$function_params,
|
||||
$context,
|
||||
$code_location
|
||||
) === false) {
|
||||
// fall through
|
||||
}
|
||||
|
||||
if ($in_call_map) {
|
||||
$stmt->inferredType = FunctionChecker::getReturnTypeFromCallMapWithArgs(
|
||||
$method_id,
|
||||
$stmt->args,
|
||||
$code_location,
|
||||
$statements_checker->getSuppressedIssues()
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
$stmt->inferredType = FunctionChecker::getFunctionReturnType(
|
||||
if ($method_id) {
|
||||
if ($in_call_map) {
|
||||
$stmt->inferredType = FunctionChecker::getReturnTypeFromCallMapWithArgs(
|
||||
$method_id,
|
||||
$statements_checker->getCheckedFilePath()
|
||||
$stmt->args,
|
||||
$code_location,
|
||||
$statements_checker->getSuppressedIssues()
|
||||
);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// this can happen when the function was defined in the Config startup script
|
||||
$stmt->inferredType = Type::getMixed();
|
||||
} else {
|
||||
try {
|
||||
$stmt->inferredType = FunctionChecker::getFunctionReturnType(
|
||||
$method_id,
|
||||
$statements_checker->getCheckedFilePath()
|
||||
);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// this can happen when the function was defined in the Config startup script
|
||||
$stmt->inferredType = Type::getMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -236,10 +302,12 @@ class CallChecker
|
||||
if (MethodChecker::methodExists($fq_class_name . '::__construct')) {
|
||||
$method_id = $fq_class_name . '::__construct';
|
||||
|
||||
$method_params = FunctionLikeChecker::getMethodParamsById($method_id, $stmt->args);
|
||||
|
||||
if (self::checkFunctionArguments(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
@ -249,6 +317,7 @@ class CallChecker
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context,
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
) === false) {
|
||||
@ -527,10 +596,12 @@ class CallChecker
|
||||
$stmt->inferredType = $return_type;
|
||||
}
|
||||
|
||||
$method_params = $method_id ? FunctionLikeChecker::getMethodParamsById($method_id, $stmt->args) : [];
|
||||
|
||||
if (self::checkFunctionArguments(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
@ -540,6 +611,7 @@ class CallChecker
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context,
|
||||
new CodeLocation($statements_checker->getSource(), $stmt),
|
||||
$has_mock
|
||||
@ -660,6 +732,8 @@ class CallChecker
|
||||
|
||||
$has_mock = $has_mock || $is_mock;
|
||||
|
||||
$method_id = null;
|
||||
|
||||
if (is_string($stmt->name) &&
|
||||
!MethodChecker::methodExists($fq_class_name . '::__callStatic') &&
|
||||
!$is_mock
|
||||
@ -732,10 +806,12 @@ class CallChecker
|
||||
}
|
||||
}
|
||||
|
||||
$method_params = $method_id ? FunctionLikeChecker::getMethodParamsById($method_id, $stmt->args) : [];
|
||||
|
||||
if (self::checkFunctionArguments(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
@ -745,6 +821,7 @@ class CallChecker
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context,
|
||||
new CodeLocation($statements_checker->getSource(), $stmt),
|
||||
$has_mock
|
||||
@ -757,33 +834,21 @@ class CallChecker
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StatementsChecker $statements_checker
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @param string|null $method_id
|
||||
* @param Context $context
|
||||
* @param StatementsChecker $statements_checker
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @param array<int,FunctionLikeParameter>|null $function_params
|
||||
* @param Context $context
|
||||
* @return false|null
|
||||
*/
|
||||
protected static function checkFunctionArguments(
|
||||
StatementsChecker $statements_checker,
|
||||
array $args,
|
||||
$method_id,
|
||||
array $function_params = null,
|
||||
Context $context
|
||||
) {
|
||||
$function_params = null;
|
||||
|
||||
$in_call_map = $method_id ? FunctionChecker::inCallMap($method_id) : false;
|
||||
|
||||
if ($method_id) {
|
||||
$function_params = FunctionLikeChecker::getParamsById(
|
||||
$method_id,
|
||||
$args,
|
||||
$statements_checker->getFilePath()
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($args as $argument_offset => $arg) {
|
||||
if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch) {
|
||||
if ($method_id) {
|
||||
if ($function_params !== null) {
|
||||
$by_ref = false;
|
||||
$by_ref_type = null;
|
||||
|
||||
@ -821,7 +886,7 @@ class CallChecker
|
||||
}
|
||||
}
|
||||
} elseif ($arg->value instanceof PhpParser\Node\Expr\Variable) {
|
||||
if ($method_id) {
|
||||
if ($function_params !== null) {
|
||||
$by_ref = false;
|
||||
$by_ref_type = null;
|
||||
|
||||
@ -865,27 +930,24 @@ class CallChecker
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StatementsChecker $statements_checker
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @param string|null $method_id
|
||||
* @param Context $context
|
||||
* @param CodeLocation $code_location
|
||||
* @param boolean $is_mock
|
||||
* @param StatementsChecker $statements_checker
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @param string|null $method_id
|
||||
* @param array<int,FunctionLikeParameter>|null $function_params
|
||||
* @param Context $context
|
||||
* @param CodeLocation $code_location
|
||||
* @param boolean $is_mock
|
||||
* @return false|null
|
||||
*/
|
||||
protected static function checkFunctionArgumentsMatch(
|
||||
StatementsChecker $statements_checker,
|
||||
array $args,
|
||||
$method_id,
|
||||
array $function_params = null,
|
||||
Context $context,
|
||||
CodeLocation $code_location,
|
||||
$is_mock = false
|
||||
) {
|
||||
// we need to do this calculation after the above vars have already processed
|
||||
$function_params = $method_id
|
||||
? FunctionLikeChecker::getParamsById($method_id, $args, $statements_checker->getFilePath())
|
||||
: [];
|
||||
|
||||
$in_call_map = $method_id ? FunctionChecker::inCallMap($method_id) : false;
|
||||
|
||||
$cased_method_id = $method_id;
|
||||
@ -920,7 +982,7 @@ class CallChecker
|
||||
}
|
||||
|
||||
foreach ($args as $argument_offset => $arg) {
|
||||
if ($method_id && $cased_method_id && isset($arg->value->inferredType)) {
|
||||
if ($function_params !== null && $cased_method_id && isset($arg->value->inferredType)) {
|
||||
if (count($function_params) > $argument_offset) {
|
||||
$param_type = $function_params[$argument_offset]->type;
|
||||
|
||||
@ -948,136 +1010,21 @@ class CallChecker
|
||||
}
|
||||
}
|
||||
|
||||
if ($method_id === 'array_map' || $method_id === 'array_filter') {
|
||||
$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;
|
||||
}
|
||||
|
||||
$array_arg = isset($args[$i]->value) ? $args[$i]->value : null;
|
||||
|
||||
$array_arg_types[] = $array_arg
|
||||
&& isset($array_arg->inferredType)
|
||||
&& isset($array_arg->inferredType->types['array'])
|
||||
&& $array_arg->inferredType->types['array'] instanceof Type\Generic
|
||||
? $array_arg->inferredType->types['array']
|
||||
: null;
|
||||
}
|
||||
|
||||
/** @var PhpParser\Node\Arg */
|
||||
$closure_arg = isset($args[$closure_index]) ? $args[$closure_index] : null;
|
||||
|
||||
/** @var Type\Union|null */
|
||||
$closure_arg_type = $closure_arg && isset($closure_arg->value->inferredType)
|
||||
? $closure_arg->value->inferredType
|
||||
: null;
|
||||
|
||||
if ($closure_arg_type) {
|
||||
$expected_closure_param_count = $method_id === 'array_filter' ? 1 : count($array_arg_types);
|
||||
|
||||
foreach ($closure_arg_type->types as $closure_type) {
|
||||
if (!$closure_type instanceof Type\Fn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($closure_type->parameters) > $expected_closure_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooManyArguments(
|
||||
'Too many arguments in closure for ' . ($cased_method_id ?: $method_id),
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (count($closure_type->parameters) < $expected_closure_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooFewArguments(
|
||||
'You must supply a param in the closure for ' . ($cased_method_id ?: $method_id),
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$closure_params = $closure_type->parameters;
|
||||
$closure_return_type = $closure_type->return_type;
|
||||
|
||||
foreach ($closure_params as $i => $closure_param_type) {
|
||||
if (!$array_arg_types[$i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var Type\Generic */
|
||||
$array_arg_type = $array_arg_types[$i];
|
||||
|
||||
$input_type = $array_arg_type->type_params[1];
|
||||
|
||||
if ($input_type->isMixed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type_match_found = TypeChecker::isContainedBy(
|
||||
$input_type,
|
||||
$closure_param_type,
|
||||
false,
|
||||
$scalar_type_match_found,
|
||||
$coerced_type
|
||||
);
|
||||
|
||||
if ($coerced_type) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TypeCoercion(
|
||||
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
|
||||
$closure_param_type . ', parent type ' . $input_type . ' provided',
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$type_match_found) {
|
||||
if ($scalar_type_match_found) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidScalarArgument(
|
||||
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
|
||||
$closure_param_type . ', ' . $input_type . ' provided',
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (IssueBuffer::accepts(
|
||||
new InvalidArgument(
|
||||
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
|
||||
$closure_param_type . ', ' . $input_type . ' provided',
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($function_params !== null && ($method_id === 'array_map' || $method_id === 'array_filter')) {
|
||||
if (self::checkArrayFunctionArgumentsMatch(
|
||||
$statements_checker,
|
||||
$args,
|
||||
$method_id,
|
||||
$function_params,
|
||||
$context,
|
||||
$code_location
|
||||
) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($method_id) {
|
||||
if ($function_params !== null) {
|
||||
if (!$is_variadic
|
||||
&& count($args) > count($function_params)
|
||||
&& (!count($function_params) || $function_params[count($function_params) - 1]->name !== '...=')
|
||||
@ -1117,6 +1064,153 @@ class CallChecker
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StatementsChecker $statements_checker
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
* @param string|null $method_id
|
||||
* @param array<int,FunctionLikeParameter> $function_params
|
||||
* @param Context $context
|
||||
* @param CodeLocation $code_location
|
||||
* @return false|null
|
||||
*/
|
||||
protected static function checkArrayFunctionArgumentsMatch(
|
||||
StatementsChecker $statements_checker,
|
||||
array $args,
|
||||
$method_id,
|
||||
array $function_params,
|
||||
Context $context,
|
||||
CodeLocation $code_location
|
||||
) {
|
||||
$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;
|
||||
}
|
||||
|
||||
$array_arg = isset($args[$i]->value) ? $args[$i]->value : null;
|
||||
|
||||
$array_arg_types[] = $array_arg
|
||||
&& isset($array_arg->inferredType)
|
||||
&& isset($array_arg->inferredType->types['array'])
|
||||
&& $array_arg->inferredType->types['array'] instanceof Type\Generic
|
||||
? $array_arg->inferredType->types['array']
|
||||
: null;
|
||||
}
|
||||
|
||||
/** @var PhpParser\Node\Arg */
|
||||
$closure_arg = isset($args[$closure_index]) ? $args[$closure_index] : null;
|
||||
|
||||
/** @var Type\Union|null */
|
||||
$closure_arg_type = $closure_arg && isset($closure_arg->value->inferredType)
|
||||
? $closure_arg->value->inferredType
|
||||
: null;
|
||||
|
||||
if ($closure_arg_type) {
|
||||
$expected_closure_param_count = $method_id === 'array_filter' ? 1 : count($array_arg_types);
|
||||
|
||||
foreach ($closure_arg_type->types as $closure_type) {
|
||||
if (!$closure_type instanceof Type\Fn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($closure_type->parameters) > $expected_closure_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooManyArguments(
|
||||
'Too many arguments in closure for ' . $method_id,
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (count($closure_type->parameters) < $expected_closure_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooFewArguments(
|
||||
'You must supply a param in the closure for ' . $method_id,
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$closure_params = $closure_type->parameters;
|
||||
$closure_return_type = $closure_type->return_type;
|
||||
|
||||
foreach ($closure_params as $i => $closure_param) {
|
||||
if (!$array_arg_types[$i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var Type\Generic */
|
||||
$array_arg_type = $array_arg_types[$i];
|
||||
|
||||
$input_type = $array_arg_type->type_params[1];
|
||||
|
||||
if ($input_type->isMixed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$closure_param_type = $closure_param->type;
|
||||
|
||||
$type_match_found = TypeChecker::isContainedBy(
|
||||
$input_type,
|
||||
$closure_param_type,
|
||||
false,
|
||||
$scalar_type_match_found,
|
||||
$coerced_type
|
||||
);
|
||||
|
||||
if ($coerced_type) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TypeCoercion(
|
||||
'First parameter of closure passed to function ' . $method_id . ' expects ' .
|
||||
$closure_param_type . ', parent type ' . $input_type . ' provided',
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$type_match_found) {
|
||||
if ($scalar_type_match_found) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidScalarArgument(
|
||||
'First parameter of closure passed to function ' . $method_id . ' expects ' .
|
||||
$closure_param_type . ', ' . $input_type . ' provided',
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (IssueBuffer::accepts(
|
||||
new InvalidArgument(
|
||||
'First parameter of closure passed to function ' . $method_id . ' expects ' .
|
||||
$closure_param_type . ', ' . $input_type . ' provided',
|
||||
new CodeLocation($statements_checker->getSource(), $closure_arg)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StatementsChecker $statements_checker
|
||||
* @param Type\Union $input_type
|
||||
|
6
src/Psalm/Issue/InvalidFunctionCall.php
Normal file
6
src/Psalm/Issue/InvalidFunctionCall.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class InvalidFunctionCall extends CodeIssue
|
||||
{
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace Psalm\Type;
|
||||
|
||||
use Psalm\FunctionLikeParameter;
|
||||
|
||||
class Fn extends Atomic
|
||||
{
|
||||
/**
|
||||
@ -9,7 +11,7 @@ class Fn extends Atomic
|
||||
public $value = 'Closure';
|
||||
|
||||
/**
|
||||
* @var array<int, Union>
|
||||
* @var array<int, FunctionLikeParameter>
|
||||
*/
|
||||
public $parameters = [];
|
||||
|
||||
@ -21,9 +23,9 @@ class Fn extends Atomic
|
||||
/**
|
||||
* Constructs a new instance of a generic type
|
||||
*
|
||||
* @param string $value
|
||||
* @param array<int, Union> $parameters
|
||||
* @param Union $return_type
|
||||
* @param string $value
|
||||
* @param array<int, FunctionLikeParameter> $parameters
|
||||
* @param Union $return_type
|
||||
*/
|
||||
public function __construct($value, array $parameters, Union $return_type)
|
||||
{
|
||||
|
@ -121,4 +121,38 @@ class ClosureTest extends PHPUnit_Framework_TestCase
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testVarReturnType()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
|
||||
$add_one = function(int $a) : int {
|
||||
return $a + 1;
|
||||
};
|
||||
|
||||
$a = $add_one(1);
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
$this->assertEquals('int', (string) $context->vars_in_scope['$a']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage InvalidFunctionCall
|
||||
*/
|
||||
public function testStringFunctionCall()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
$bad_one = "hello";
|
||||
$a = $bad_one(1);
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
$this->assertEquals('int', (string) $context->vars_in_scope['$a']);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user