1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 11:26:55 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
2019-09-19 11:59:43 -04:00

3373 lines
124 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Psalm\Internal\Analyzer\Statements\Expression;
use PhpParser;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Analyzer\MethodAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TypeAnalyzer;
use Psalm\Internal\Codebase\CallMap;
use Psalm\Internal\Taint\Sink;
use Psalm\Internal\Taint\Source;
use Psalm\Internal\Type\TypeCombination;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\ImplicitToStringCast;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\InvalidPassByReference;
use Psalm\Issue\InvalidScalarArgument;
use Psalm\Issue\MixedArgument;
use Psalm\Issue\MixedArgumentTypeCoercion;
use Psalm\Issue\NoValue;
use Psalm\Issue\NullArgument;
use Psalm\Issue\PossiblyFalseArgument;
use Psalm\Issue\PossiblyInvalidArgument;
use Psalm\Issue\PossiblyNullArgument;
use Psalm\Issue\TooFewArguments;
use Psalm\Issue\TooManyArguments;
use Psalm\Issue\ArgumentTypeCoercion;
use Psalm\Issue\UndefinedFunction;
use Psalm\Issue\UndefinedVariable;
use Psalm\IssueBuffer;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Storage\FunctionLikeStorage;
use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyArray;
use function strtolower;
use function strpos;
use function explode;
use function count;
use function in_array;
use function array_reverse;
use function array_filter;
use function is_null;
use function is_string;
use function assert;
use function preg_match;
use function preg_replace;
use function str_replace;
use function is_int;
use function substr;
use function array_merge;
use Psalm\Issue\TaintedInput;
/**
* @internal
*/
class CallAnalyzer
{
/**
* @param FunctionLikeAnalyzer $source
* @param string $method_name
* @param Context $context
*
* @return void
*/
public static function collectSpecialInformation(
FunctionLikeAnalyzer $source,
$method_name,
Context $context
) {
$fq_class_name = (string)$source->getFQCLN();
$project_analyzer = $source->getFileAnalyzer()->project_analyzer;
$codebase = $source->getCodebase();
if ($context->collect_mutations &&
$context->self &&
(
$context->self === $fq_class_name ||
$codebase->classExtends(
$context->self,
$fq_class_name
)
)
) {
$method_id = $fq_class_name . '::' . strtolower($method_name);
if ($method_id !== $source->getMethodId()) {
if ($context->collect_initializations) {
if (isset($context->initialized_methods[$method_id])) {
return;
}
if ($context->initialized_methods === null) {
$context->initialized_methods = [];
}
$context->initialized_methods[$method_id] = true;
}
$project_analyzer->getMethodMutations(
$method_id,
$context,
$source->getRootFilePath(),
$source->getRootFileName()
);
}
} elseif ($context->collect_initializations &&
$context->self &&
(
$context->self === $fq_class_name
|| $codebase->classlikes->classExtends(
$context->self,
$fq_class_name
)
) &&
$source->getMethodName() !== $method_name
) {
$method_id = $fq_class_name . '::' . strtolower($method_name);
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if (isset($context->vars_in_scope['$this'])) {
foreach ($context->vars_in_scope['$this']->getTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
if ($fq_class_name === $atomic_type->value) {
$alt_declaring_method_id = $declaring_method_id;
} else {
$fq_class_name = $atomic_type->value;
$method_id = $fq_class_name . '::' . strtolower($method_name);
$alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
}
if ($alt_declaring_method_id) {
$declaring_method_id = $alt_declaring_method_id;
break;
}
if (!$atomic_type->extra_types) {
continue;
}
foreach ($atomic_type->extra_types as $intersection_type) {
if ($intersection_type instanceof TNamedObject) {
$fq_class_name = $intersection_type->value;
$method_id = $fq_class_name . '::' . strtolower($method_name);
$alt_declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if ($alt_declaring_method_id) {
$declaring_method_id = $alt_declaring_method_id;
break 2;
}
}
}
}
}
}
if (!$declaring_method_id) {
// can happen for __call
return;
}
if (isset($context->initialized_methods[$declaring_method_id])) {
return;
}
if ($context->initialized_methods === null) {
$context->initialized_methods = [];
}
$context->initialized_methods[$declaring_method_id] = true;
$method_storage = $codebase->methods->getStorage($declaring_method_id);
$class_analyzer = $source->getSource();
if ($class_analyzer instanceof ClassLikeAnalyzer && !$method_storage->is_static) {
$local_vars_in_scope = [];
$local_vars_possibly_in_scope = [];
foreach ($context->vars_in_scope as $var => $_) {
if (strpos($var, '$this->') !== 0 && $var !== '$this') {
$local_vars_in_scope[$var] = $context->vars_in_scope[$var];
}
}
foreach ($context->vars_possibly_in_scope as $var => $_) {
if (strpos($var, '$this->') !== 0 && $var !== '$this') {
$local_vars_possibly_in_scope[$var] = $context->vars_possibly_in_scope[$var];
}
}
if ($fq_class_name === $source->getFQCLN()) {
$class_analyzer->getMethodMutations(strtolower($method_name), $context);
} else {
list($declaring_fq_class_name) = explode('::', $declaring_method_id);
$old_self = $context->self;
$context->self = $declaring_fq_class_name;
$project_analyzer->getMethodMutations(
$declaring_method_id,
$context,
$source->getRootFilePath(),
$source->getRootFileName()
);
$context->self = $old_self;
}
foreach ($local_vars_in_scope as $var => $type) {
$context->vars_in_scope[$var] = $type;
}
foreach ($local_vars_possibly_in_scope as $var => $_) {
$context->vars_possibly_in_scope[$var] = true;
}
}
}
}
/**
* @param string|null $method_id
* @param array<int, PhpParser\Node\Arg> $args
* @param array<string, array<string, array{Type\Union, 1?:int}>>|null &$generic_params
* @param Context $context
* @param CodeLocation $code_location
* @param StatementsAnalyzer $statements_analyzer
*
* @return false|null
*/
protected static function checkMethodArgs(
$method_id,
array $args,
&$generic_params,
Context $context,
CodeLocation $code_location,
StatementsAnalyzer $statements_analyzer
) {
$codebase = $statements_analyzer->getCodebase();
$method_params = $method_id
? $codebase->methods->getMethodParams($method_id, $statements_analyzer, $args, $context)
: null;
if (self::checkFunctionArguments(
$statements_analyzer,
$args,
$method_params,
$method_id,
$context,
$generic_params
) === false) {
return false;
}
if (!$method_id || $method_params === null) {
return;
}
list($fq_class_name, $method_name) = explode('::', $method_id);
$fq_class_name = $codebase->classlikes->getUnAliasedName($fq_class_name);
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
$method_storage = null;
if (isset($class_storage->declaring_method_ids[strtolower($method_name)])) {
$declaring_method_id = $class_storage->declaring_method_ids[strtolower($method_name)];
list($declaring_fq_class_name, $declaring_method_name) = explode('::', $declaring_method_id);
if ($declaring_fq_class_name !== $fq_class_name) {
$declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_fq_class_name);
} else {
$declaring_class_storage = $class_storage;
}
if (!isset($declaring_class_storage->methods[strtolower($declaring_method_name)])) {
throw new \UnexpectedValueException('Storage should not be empty here');
}
$method_storage = $declaring_class_storage->methods[strtolower($declaring_method_name)];
if ($declaring_class_storage->user_defined
&& !$method_storage->has_docblock_param_types
&& isset($declaring_class_storage->documenting_method_ids[$method_name])
) {
$documenting_method_id = $declaring_class_storage->documenting_method_ids[$method_name];
$documenting_method_storage = $codebase->methods->getStorage($documenting_method_id);
if ($documenting_method_storage->template_types) {
$method_storage = $documenting_method_storage;
}
}
if (!$context->isSuppressingExceptions($statements_analyzer)) {
$context->mergeFunctionExceptions($method_storage, $code_location);
}
}
if (self::checkFunctionLikeArgumentsMatch(
$statements_analyzer,
$args,
$method_id,
$method_params,
$method_storage,
$class_storage,
$generic_params,
$code_location,
$context
) === false) {
return false;
}
return null;
}
/**
* @param StatementsAnalyzer $statements_analyzer
* @param array<int, PhpParser\Node\Arg> $args
* @param array<int, FunctionLikeParameter>|null $function_params
* @param array<string, array<string, array{Type\Union, 1?:int}>>|null $generic_params
* @param string|null $method_id
* @param Context $context
*
* @return false|null
*/
protected static function checkFunctionArguments(
StatementsAnalyzer $statements_analyzer,
array $args,
?array $function_params,
?string $method_id,
Context $context,
?array $generic_params = null
) {
$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) {
if (self::handleArrayAddition($statements_analyzer, $args, $context) === false) {
return false;
}
return;
}
if ($method_id && $method_id === 'array_splice' && $function_params && count($args) > 1) {
if (self::handleArraySplice($statements_analyzer, $args, $context) === false) {
return false;
}
return;
}
if ($method_id === 'array_map' && count($args) === 2) {
$args = array_reverse($args, true);
}
foreach ($args as $argument_offset => $arg) {
if ($function_params !== null) {
$param = $argument_offset < count($function_params)
? $function_params[$argument_offset]
: ($last_param && $last_param->is_variadic ? $last_param : null);
$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\Assign
|| $arg->value instanceof PhpParser\Node\Expr\Array_
)
) {
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;
}
$codebase = $statements_analyzer->getCodebase();
if ($arg->value instanceof PhpParser\Node\Expr\Closure
&& $generic_params
&& $param
&& $param->type
&& !$arg->value->getDocComment()
&& !array_filter(
$arg->value->params,
function (PhpParser\Node\Param $closure_param) : bool {
return !!$closure_param->type;
}
)
) {
if (count($args) === 2
&& (($argument_offset === 1 && $method_id === 'array_filter')
|| ($argument_offset === 0 && $method_id === 'array_map'))
) {
$replaced_type = new Type\Union([
new Type\Atomic\TCallable(
'callable',
[
new \Psalm\Storage\FunctionLikeParameter(
'function',
false,
new Type\Union([
new Type\Atomic\TTemplateParam(
'ArrayValue',
Type::getMixed()
)
])
)
]
)
]);
} else {
$replaced_type = clone $param->type;
}
$empty_generic_params = [];
$replaced_type->replaceTemplateTypesWithStandins(
$generic_params,
$empty_generic_params,
$codebase,
null
);
$replaced_type->replaceTemplateTypesWithArgTypes(
$generic_params
);
$closure_id = $statements_analyzer->getFilePath()
. ':' . $arg->value->getLine()
. ':' . (int)$arg->value->getAttribute('startFilePos')
. ':-:closure';
$closure_storage = $codebase->getClosureStorage($statements_analyzer->getFilePath(), $closure_id);
foreach ($replaced_type->getTypes() as $replaced_type_part) {
if ($replaced_type_part instanceof Type\Atomic\TCallable
|| $replaced_type_part instanceof Type\Atomic\TFn
) {
foreach ($closure_storage->params as $closure_param_offset => $param_storage) {
if (isset($replaced_type_part->params[$closure_param_offset]->type)
&& !$replaced_type_part->params[$closure_param_offset]->type->hasTemplate()
) {
$param_storage->type = $replaced_type_part->params[$closure_param_offset]->type;
}
}
}
}
}
$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 (count($args) === 2
&& (($argument_offset === 0 && $method_id === 'array_filter')
|| ($argument_offset === 1 || $method_id === 'array_map'))
) {
$generic_param_type = new Type\Union([
new Type\Atomic\TArray([
Type::getArrayKey(),
new Type\Union([
new Type\Atomic\TTemplateParam(
'ArrayValue',
Type::getMixed()
)
])
])
]);
$template_types = ['ArrayValue' => ['' => [Type::getMixed()]]];
if ($generic_params === null) {
$generic_params = [];
}
$generic_param_type->replaceTemplateTypesWithStandins(
$template_types,
$generic_params,
$codebase,
isset($arg->value->inferredType)
? $arg->value->inferredType
: null
);
}
if ($context->collect_references
&& ($arg->value instanceof PhpParser\Node\Expr\AssignOp
|| $arg->value instanceof PhpParser\Node\Expr\PreInc
|| $arg->value instanceof PhpParser\Node\Expr\PreDec)
) {
$var_id = ExpressionAnalyzer::getVarId(
$arg->value->var,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
if ($var_id) {
$context->hasVariable($var_id, $statements_analyzer);
}
}
if ($toggled_class_exists) {
$context->inside_class_exists = false;
}
continue;
}
if (self::evaluateAribitraryParam(
$statements_analyzer,
$arg,
$context
) === false) {
return false;
}
}
}
/**
* @return false|null
*/
private static function evaluateAribitraryParam(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Arg $arg,
Context $context
) {
// 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\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\Scalar\Encapsed
) {
$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 = ExpressionAnalyzer::getVarId(
$arg->value,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
}
if ($var_id) {
if (!$context->hasVariable($var_id, $statements_analyzer)
|| $context->vars_in_scope[$var_id]->isNull()
) {
// 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;
if (strpos($var_id, '-') === false
&& strpos($var_id, '[') === false
&& !$statements_analyzer->hasVariable($var_id)
) {
$location = new CodeLocation($statements_analyzer, $arg->value);
$statements_analyzer->registerVariable(
$var_id,
$location,
null
);
$statements_analyzer->registerVariableUses([$location->getHash() => $location]);
}
} else {
$context->removeVarFromConflictingClauses(
$var_id,
$context->vars_in_scope[$var_id],
$statements_analyzer
);
foreach ($context->vars_in_scope[$var_id]->getTypes() 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()]
)
);
}
}
}
}
}
/**
* @param string|null $method_id
* @return false|null
*/
private static function handleByRefFunctionArg(
StatementsAnalyzer $statements_analyzer,
$method_id,
int $argument_offset,
PhpParser\Node\Arg $arg,
Context $context
) {
$var_id = ExpressionAnalyzer::getVarId(
$arg->value,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
$builtin_array_functions = [
'shuffle', 'sort', 'rsort', 'usort', '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
))
) {
// 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;
}
}
// 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)) {
self::handleByRefArrayAdjustment($statements_analyzer, $arg, $context);
return;
}
// noops
if (in_array($method_id, ['reset', 'end', 'next', 'prev', 'ksort'], true)) {
return;
}
if (isset($arg->value->inferredType)
&& $arg->value->inferredType->hasArray()
) {
/** @var TArray|ObjectLike */
$array_type = $arg->value->inferredType->getTypes()['array'];
if ($array_type instanceof ObjectLike) {
$array_type = $array_type->getGenericArrayType();
}
if (in_array($method_id, ['shuffle', 'sort', 'rsort', 'usort'], true)) {
$tvalue = $array_type->type_params[1];
$by_ref_type = new Type\Union([new TArray([Type::getInt(), clone $tvalue])]);
} else {
$by_ref_type = new Type\Union([clone $array_type]);
}
ExpressionAnalyzer::assignByRefParam(
$statements_analyzer,
$arg->value,
$by_ref_type,
$by_ref_type,
$context,
false
);
return;
}
}
if ($method_id === 'socket_select') {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$arg->value,
$context
) === false) {
return false;
}
}
}
/**
* @param StatementsAnalyzer $statements_analyzer
* @param array<int, PhpParser\Node\Arg> $args
* @param Context $context
*
* @return false|null
*/
private static function handleArrayAddition(
StatementsAnalyzer $statements_analyzer,
array $args,
Context $context
) {
$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 (isset($array_arg->inferredType) && $array_arg->inferredType->hasArray()) {
/** @var TArray|ObjectLike */
$array_type = $array_arg->inferredType->getTypes()['array'];
if ($array_type instanceof ObjectLike) {
$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 (!isset($arg->value->inferredType) || $arg->value->inferredType->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->inferredType
);
} else {
$by_ref_type = Type::combineUnionTypes(
$by_ref_type,
new Type\Union(
[
new TArray(
[
Type::getInt(),
clone $arg->value->inferredType
]
),
]
)
);
}
}
ExpressionAnalyzer::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
*/
private static function handleArraySplice(
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;
if (isset($replacement_arg->inferredType)
&& !$replacement_arg->inferredType->hasArray()
&& $replacement_arg->inferredType->hasString()
&& $replacement_arg->inferredType->isSingle()
) {
$replacement_arg->inferredType = new Type\Union([
new Type\Atomic\TArray([Type::getInt(), $replacement_arg->inferredType])
]);
}
if (isset($array_arg->inferredType)
&& $array_arg->inferredType->hasArray()
&& isset($replacement_arg->inferredType)
&& $replacement_arg->inferredType->hasArray()
) {
/** @var TArray|ObjectLike */
$array_type = $array_arg->inferredType->getTypes()['array'];
if ($array_type instanceof ObjectLike) {
$array_type = $array_type->getGenericArrayType();
}
/** @var TArray|ObjectLike */
$replacement_array_type = $replacement_arg->inferredType->getTypes()['array'];
if ($replacement_array_type instanceof ObjectLike) {
$replacement_array_type = $replacement_array_type->getGenericArrayType();
}
$by_ref_type = TypeCombination::combineTypes([$array_type, $replacement_array_type]);
ExpressionAnalyzer::assignByRefParam(
$statements_analyzer,
$array_arg,
$by_ref_type,
$by_ref_type,
$context,
false
);
return;
}
$array_type = Type::getArray();
ExpressionAnalyzer::assignByRefParam(
$statements_analyzer,
$array_arg,
$array_type,
$array_type,
$context,
false
);
}
/**
* @return void
*/
private static function handleByRefArrayAdjustment(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Arg $arg,
Context $context
) {
$var_id = ExpressionAnalyzer::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->getTypes();
foreach ($array_atomic_types as $array_atomic_type) {
if ($array_atomic_type instanceof ObjectLike) {
$generic_array_type = $array_atomic_type->getGenericArrayType();
if ($generic_array_type instanceof TNonEmptyArray) {
if (!$context->inside_loop && $generic_array_type->count !== null) {
if ($generic_array_type->count === 0) {
$generic_array_type = new TArray(
[
new Type\Union([new TEmpty]),
new Type\Union([new TEmpty]),
]
);
} else {
$generic_array_type->count--;
}
} else {
$generic_array_type = new TArray($generic_array_type->type_params);
}
}
$array_type->addType($generic_array_type);
} elseif ($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->vars_in_scope[$var_id] = $array_type;
}
}
}
/**
* @param StatementsAnalyzer $statements_analyzer
* @param array<int, PhpParser\Node\Arg> $args
* @param string|null $method_id
* @param array<int,FunctionLikeParameter> $function_params
* @param FunctionLikeStorage|null $function_storage
* @param ClassLikeStorage|null $class_storage
* @param array<string, array<string, array{Type\Union, 1?:int}>>|null $generic_params
* @param CodeLocation $code_location
* @param Context $context
*
* @return false|null
*/
protected static function checkFunctionLikeArgumentsMatch(
StatementsAnalyzer $statements_analyzer,
array $args,
$method_id,
array $function_params,
$function_storage,
$class_storage,
&$generic_params,
CodeLocation $code_location,
Context $context
) {
$in_call_map = $method_id ? CallMap::inCallMap($method_id) : false;
$cased_method_id = $method_id;
$is_variadic = false;
$fq_class_name = null;
$codebase = $statements_analyzer->getCodebase();
if ($method_id) {
if ($in_call_map || !strpos($method_id, '::')) {
$is_variadic = $codebase->functions->isVariadic(
$codebase,
strtolower($method_id),
$statements_analyzer->getRootFilePath()
);
} else {
$fq_class_name = explode('::', $method_id)[0];
$is_variadic = $codebase->methods->isVariadic($method_id);
}
}
if ($method_id && strpos($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 && strpos($method_id, '::')) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if ($declaring_method_id && $declaring_method_id !== $method_id) {
list($self_fq_class_name) = explode('::', $declaring_method_id);
$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) {
list($self_fq_class_name) = explode('::', $appearing_method_id);
}
}
if ($function_params) {
foreach ($function_params as $function_param) {
$is_variadic = $is_variadic || $function_param->is_variadic;
}
}
$has_packed_var = false;
foreach ($args as $arg) {
$has_packed_var = $has_packed_var || $arg->unpack;
}
$last_param = $function_params
? $function_params[count($function_params) - 1]
: null;
$template_types = null;
if ($function_storage) {
$template_types = self::getTemplateTypesForFunction(
$function_storage,
$class_storage,
$calling_class_storage
);
if ($template_types) {
foreach ($args as $argument_offset => $arg) {
$function_param = count($function_params) > $argument_offset
? $function_params[$argument_offset]
: ($last_param && $last_param->is_variadic ? $last_param : null);
if (!$function_param
|| !$function_param->type
|| !isset($arg->value->inferredType)
) {
continue;
}
$empty_generic_params = [];
$function_param->type->replaceTemplateTypesWithStandins(
$template_types,
$empty_generic_params,
$codebase,
$arg->value->inferredType,
false
);
}
}
}
$existing_generic_params = $generic_params ?: [];
foreach ($existing_generic_params as $template_name => $type_map) {
foreach ($type_map as $class => $type) {
$existing_generic_params[$template_name][$class][0] = clone $type[0];
}
}
$function_param_count = count($function_params);
foreach ($args as $argument_offset => $arg) {
$function_param = $function_param_count > $argument_offset
? $function_params[$argument_offset]
: ($last_param && $last_param->is_variadic ? $last_param : null);
if ($function_param
&& $function_param->by_ref
&& $method_id !== 'extract'
) {
if (self::handlePossiblyMatchingByRefParam(
$statements_analyzer,
$codebase,
$method_id,
$cased_method_id,
$last_param,
$function_params,
$function_storage,
$argument_offset,
$arg,
$context,
$generic_params,
$template_types
) === false) {
return;
}
}
if ($method_id === 'compact'
&& isset($arg->value->inferredType)
&& $arg->value->inferredType->isSingleStringLiteral()
) {
$literal = $arg->value->inferredType->getSingleStringLiteral();
if (!$context->hasVariable('$' . $literal->value, $statements_analyzer)) {
if (IssueBuffer::accepts(
new UndefinedVariable(
'Cannot find referenced variable $' . $literal->value,
new CodeLocation($statements_analyzer->getSource(), $arg->value)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
if (self::checkFunctionLikeArgumentMatches(
$statements_analyzer,
$cased_method_id,
$self_fq_class_name,
$static_fq_class_name,
$code_location,
$function_param,
$argument_offset,
$arg,
$context,
$existing_generic_params,
$generic_params,
$template_types,
$function_storage ? $function_storage->pure : false,
$in_call_map
) === false) {
return false;
}
}
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
}
}
if (self::checkArrayFunctionArgumentsMatch(
$statements_analyzer,
$args,
$method_id,
$context->check_functions
) === false
) {
return false;
}
return null;
}
if (!$is_variadic
&& count($args) > count($function_params)
&& (!count($function_params) || $function_params[count($function_params) - 1]->name !== '...=')
) {
if (IssueBuffer::accepts(
new TooManyArguments(
'Too many arguments for method ' . ($cased_method_id ?: $method_id)
. ' - expecting ' . count($function_params) . ' but saw ' . count($args),
$code_location,
$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), $j = count($function_params); $i < $j; ++$i) {
$param = $function_params[$i];
if (!$param->is_optional && !$param->is_variadic) {
if (IssueBuffer::accepts(
new TooFewArguments(
'Too few arguments for method ' . $cased_method_id
. ' - expecting ' . $expected_param_count . ' but saw ' . count($args),
$code_location,
$method_id ?: ''
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
break;
}
if ($param->is_optional
&& $param->type
&& $param->default_type
&& !$param->is_variadic
&& $template_types
) {
if ($generic_params === null) {
$generic_params = [];
}
$param_type = clone $param->type;
$param_type->replaceTemplateTypesWithStandins(
$template_types,
$generic_params,
$codebase,
clone $param->default_type,
true
);
}
}
}
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $existing_generic_params
* @param array<string, array<string, array{Type\Union, 1?:int}>> $generic_params
* @param array<string, array<string, array{Type\Union}>> $template_types
* @return false|null
*/
private static function checkFunctionLikeArgumentMatches(
StatementsAnalyzer $statements_analyzer,
?string $cased_method_id,
?string $self_fq_class_name,
?string $static_fq_class_name,
CodeLocation $function_location,
?FunctionLikeParameter $function_param,
int $argument_offset,
PhpParser\Node\Arg $arg,
Context $context,
array $existing_generic_params,
?array &$generic_params,
?array $template_types,
bool $function_is_pure,
bool $in_call_map
) {
$codebase = $statements_analyzer->getCodebase();
if (!isset($arg->value->inferredType)) {
if ($function_param && !$function_param->by_ref) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
$param_type = $function_param->type;
if ($function_param->is_variadic
&& $param_type
&& $param_type->hasArray()
) {
/** @var TArray */
$array_type = $param_type->getTypes()['array'];
$param_type = $array_type->type_params[1];
}
if ($param_type && !$param_type->hasMixed()) {
if (IssueBuffer::accepts(
new MixedArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
. ' cannot be mixed, expecting ' . $param_type,
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
return;
}
if (!$function_param) {
return;
}
if (self::checkFunctionLikeTypeMatches(
$statements_analyzer,
$codebase,
$cased_method_id,
$self_fq_class_name,
$static_fq_class_name,
$function_location,
$function_param,
$arg->value->inferredType,
$argument_offset,
$arg,
$context,
$existing_generic_params,
$generic_params,
$template_types,
$function_is_pure,
$in_call_map
) === false) {
return false;
}
}
/**
* @param string|null $method_id
* @param string|null $cased_method_id
* @param FunctionLikeParameter|null $last_param
* @param array<int, FunctionLikeParameter> $function_params
* @param array<string, array<string, array{Type\Union, 1?:int}>> $generic_params
* @param array<string, array<string, array{Type\Union}>> $template_types
* @param FunctionLikeStorage|null $function_storage
* @return false|null
*/
private static function handlePossiblyMatchingByRefParam(
StatementsAnalyzer $statements_analyzer,
Codebase $codebase,
$method_id,
$cased_method_id,
$last_param,
$function_params,
$function_storage,
int $argument_offset,
PhpParser\Node\Arg $arg,
Context $context,
array &$generic_params = null,
array $template_types = null
) {
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\ConstFetch
|| $arg->value instanceof PhpParser\Node\Expr\FuncCall
|| $arg->value instanceof PhpParser\Node\Expr\MethodCall
|| $arg->value instanceof PhpParser\Node\Expr\StaticCall
) && (
!isset($arg->value->inferredType)
|| !$arg->value->inferredType->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,
[
'shuffle', 'sort', 'rsort', 'usort', '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;
if (isset($function_storage->param_out_types[$argument_offset])) {
$by_ref_out_type = $function_storage->param_out_types[$argument_offset];
}
if ($by_ref_type && $by_ref_type->isNullable()) {
$check_null_ref = false;
}
if ($template_types && $by_ref_type) {
if ($generic_params === null) {
$generic_params = [];
}
$original_by_ref_type = clone $by_ref_type;
$by_ref_type = clone $by_ref_type;
$by_ref_type->replaceTemplateTypesWithStandins(
$template_types,
$generic_params,
$codebase,
isset($arg->value->inferredType)
? $arg->value->inferredType
: null
);
if ($generic_params) {
$original_by_ref_type->replaceTemplateTypesWithArgTypes(
$generic_params
);
$by_ref_type = $original_by_ref_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();
ExpressionAnalyzer::assignByRefParam(
$statements_analyzer,
$arg->value,
$by_ref_type,
$by_ref_out_type ?: $by_ref_type,
$context,
$method_id && (strpos($method_id, '::') !== false || !CallMap::inCallMap($method_id)),
$check_null_ref
);
}
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $existing_generic_params
* @param array<string, array<string, array{Type\Union, 1?:int}>> $generic_params
* @param array<string, array<string, array{Type\Union}>> $template_types
* @return false|null
*/
private static function checkFunctionLikeTypeMatches(
StatementsAnalyzer $statements_analyzer,
Codebase $codebase,
?string $cased_method_id,
?string $self_fq_class_name,
?string $static_fq_class_name,
CodeLocation $function_location,
FunctionLikeParameter $function_param,
Type\Union $arg_type,
int $argument_offset,
PhpParser\Node\Arg $arg,
Context $context,
?array $existing_generic_params,
?array &$generic_params,
?array $template_types,
bool $function_is_pure,
bool $in_call_map
) {
if (!$function_param->type) {
if (!$codebase->infer_types_from_usage) {
return;
}
$param_type = Type::getMixed();
} else {
$param_type = clone $function_param->type;
}
if ($existing_generic_params) {
$empty_generic_params = [];
$param_type->replaceTemplateTypesWithStandins(
$existing_generic_params,
$empty_generic_params,
$codebase,
$arg->value->inferredType
);
$arg_type->replaceTemplateTypesWithStandins(
$existing_generic_params,
$empty_generic_params,
$codebase,
$arg->value->inferredType
);
}
if ($template_types) {
if ($generic_params === null) {
$generic_params = [];
}
$arg_type_param = $arg_type;
if ($arg->unpack) {
if ($arg_type->hasArray()) {
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike */
$array_atomic_type = $arg_type->getTypes()['array'];
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
$array_atomic_type = $array_atomic_type->getGenericArrayType();
}
$arg_type_param = $array_atomic_type->type_params[1];
} else {
$arg_type_param = Type::getMixed();
}
}
$param_type->replaceTemplateTypesWithStandins(
$template_types,
$generic_params,
$codebase,
$arg_type_param
);
}
if (!$context->check_variables) {
return;
}
$parent_class = null;
if ($self_fq_class_name) {
$classlike_storage = $codebase->classlike_storage_provider->get($self_fq_class_name);
$parent_class = $classlike_storage->parent_class;
}
$fleshed_out_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$param_type,
$self_fq_class_name,
$static_fq_class_name,
$parent_class
);
$fleshed_out_signature_type = $function_param->signature_type
? ExpressionAnalyzer::fleshOutType(
$codebase,
$function_param->signature_type,
$self_fq_class_name,
$static_fq_class_name,
$parent_class
)
: null;
if ($arg->unpack) {
if ($arg_type->hasMixed()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
if (IssueBuffer::accepts(
new MixedArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
. ' cannot be mixed, expecting array',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($arg_type->hasArray()) {
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike */
$array_atomic_type = $arg_type->getTypes()['array'];
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
$array_atomic_type = $array_atomic_type->getGenericArrayType();
}
$arg_type = $array_atomic_type->type_params[1];
} else {
foreach ($arg_type->getTypes() as $atomic_type) {
if (!$atomic_type->isIterable($codebase)) {
if (IssueBuffer::accepts(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
. ' expects array, ' . $atomic_type->getId() . ' provided',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
continue;
}
}
return;
}
}
if (self::checkFunctionArgumentType(
$statements_analyzer,
$arg_type,
$fleshed_out_type,
$fleshed_out_signature_type,
$cased_method_id,
$argument_offset,
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$arg->value,
$context,
$function_param,
$arg->unpack,
$function_is_pure,
$in_call_map,
$function_location
) === false) {
return false;
}
}
/**
* @return array<string, array<string, array{Type\Union}>>
*/
private static function getTemplateTypesForFunction(
FunctionLikeStorage $function_storage,
ClassLikeStorage $class_storage = null,
ClassLikeStorage $calling_class_storage = null
) : array {
$template_types = [];
if ($function_storage->template_types) {
$template_types = $function_storage->template_types;
}
if ($class_storage) {
if ($calling_class_storage
&& $class_storage !== $calling_class_storage
&& $calling_class_storage->template_type_extends
) {
foreach ($calling_class_storage->template_type_extends as $class_name => $type_map) {
foreach ($type_map as $template_name => $type) {
if (is_string($template_name) && $class_name === $class_storage->name) {
$output_type = null;
foreach ($type->getTypes() as $atomic_type) {
if ($atomic_type instanceof Type\Atomic\TTemplateParam
&& $atomic_type->defining_class
&& isset(
$calling_class_storage
->template_type_extends
[$atomic_type->defining_class]
[$atomic_type->param_name]
)
) {
$output_type_candidate = $calling_class_storage
->template_type_extends[$atomic_type->defining_class][$atomic_type->param_name];
} else {
$output_type_candidate = new Type\Union([$atomic_type]);
}
if (!$output_type) {
$output_type = $output_type_candidate;
} else {
$output_type = Type::combineUnionTypes(
$output_type_candidate,
$output_type
);
}
}
$template_types[$template_name][$class_storage->name] = [$output_type ?: Type::getMixed()];
}
}
}
} elseif ($class_storage->template_types) {
foreach ($class_storage->template_types as $template_name => $type) {
$template_types[$template_name] = $type;
}
}
}
foreach ($template_types as $key => $type_map) {
foreach ($type_map as $class => $type) {
$template_types[$key][$class][0] = clone $type[0];
}
}
return $template_types;
}
/**
* @param StatementsAnalyzer $statements_analyzer
* @param array<int, PhpParser\Node\Arg> $args
* @param string $method_id
*
* @return false|null
*/
protected static function checkArrayFunctionArgumentsMatch(
StatementsAnalyzer $statements_analyzer,
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;
}
$array_arg = isset($arg->value) ? $arg->value : null;
/** @var ObjectLike|TArray|null */
$array_arg_type = $array_arg
&& isset($array_arg->inferredType)
&& isset($array_arg->inferredType->getTypes()['array'])
? $array_arg->inferredType->getTypes()['array']
: null;
if ($array_arg_type instanceof ObjectLike) {
$array_arg_type = $array_arg_type->getGenericArrayType();
}
$array_arg_types[] = $array_arg_type;
}
$closure_arg = isset($args[$closure_index]) ? $args[$closure_index] : null;
$closure_arg_type = $closure_arg && isset($closure_arg->value->inferredType)
? $closure_arg->value->inferredType
: null;
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->getTypes() as $closure_type) {
self::checkArrayFunctionClosureType(
$statements_analyzer,
$method_id,
$closure_type,
$closure_arg,
$min_closure_param_count,
$max_closure_param_count,
$array_arg_types,
$check_functions
);
}
}
}
/**
* @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 checkArrayFunctionClosureType(
StatementsAnalyzer $statements_analyzer,
$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 = self::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 = $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::checkArrayFunctionClosureTypeArgs(
$statements_analyzer,
$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 checkArrayFunctionClosureTypeArgs(
StatementsAnalyzer $statements_analyzer,
$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;
}
$i = 0;
foreach ($closure_params as $closure_param) {
if (!isset($array_arg_types[$i])) {
++$i;
continue;
}
$array_arg_type = $array_arg_types[$i];
$input_type = $array_arg_type->type_params[1];
if ($input_type->hasMixed()) {
++$i;
continue;
}
$closure_param_type = $closure_param->type;
if (!$closure_param_type) {
++$i;
continue;
}
$union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
$type_match_found = TypeAnalyzer::isContainedBy(
$codebase,
$input_type,
$closure_param_type,
false,
false,
$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
}
}
++$i;
}
}
/**
* @return null|false
*/
public static function checkFunctionArgumentType(
StatementsAnalyzer $statements_analyzer,
Type\Union $input_type,
Type\Union $param_type,
?Type\Union $signature_param_type,
?string $cased_method_id,
int $argument_offset,
CodeLocation $code_location,
PhpParser\Node\Expr $input_expr,
Context $context,
FunctionLikeParameter $function_param,
bool $unpack,
bool $function_is_pure,
bool $in_call_map,
CodeLocation $function_location
) {
$codebase = $statements_analyzer->getCodebase();
if ($param_type->hasMixed()) {
if ($codebase->infer_types_from_usage
&& !$input_type->hasMixed()
&& !$param_type->from_docblock
&& $cased_method_id
&& strpos($cased_method_id, '::')
&& !strpos($cased_method_id, '__')
) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($cased_method_id);
if ($declaring_method_id) {
$id_lc = strtolower($declaring_method_id);
if (!isset($codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset])) {
$codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset]
= clone $input_type;
} else {
$codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset]
= Type::combineUnionTypes(
$codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset],
clone $input_type,
$codebase
);
}
}
}
if ($cased_method_id) {
self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$argument_offset,
$code_location,
$function_location,
$function_param,
$input_type,
$function_is_pure
);
}
return null;
}
$method_identifier = $cased_method_id ? ' of ' . $cased_method_id : '';
if ($input_type->hasMixed()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
if (IssueBuffer::accepts(
new MixedArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be mixed, expecting ' .
$param_type,
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
if ($input_type->isMixed()) {
if (!$function_param->by_ref
&& !($function_param->is_variadic xor $unpack)
&& $cased_method_id !== 'echo'
&& (!$in_call_map || $context->strict_types)
) {
self::coerceValueAfterGatekeeperArgument(
$statements_analyzer,
$input_type,
false,
$input_expr,
$param_type,
$signature_param_type,
$context,
$unpack
);
}
}
if ($cased_method_id) {
self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$argument_offset,
$code_location,
$function_location,
$function_param,
$input_type,
$function_is_pure
);
}
if ($input_type->isMixed()) {
return null;
}
}
if ($input_type->isNever()) {
if (IssueBuffer::accepts(
new NoValue(
'This function or method call never returns output',
$code_location
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return null;
}
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
}
$param_type = TypeAnalyzer::simplifyUnionType(
$codebase,
$param_type
);
$union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
$type_match_found = TypeAnalyzer::isContainedBy(
$codebase,
$input_type,
$param_type,
true,
true,
$union_comparison_results
);
$replace_input_type = false;
if ($union_comparison_results->replacement_union_type) {
$replace_input_type = true;
$input_type = $union_comparison_results->replacement_union_type;
}
if ($cased_method_id) {
$old_input_type = $input_type;
self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$argument_offset,
$code_location,
$function_location,
$function_param,
$input_type,
$function_is_pure
);
if ($old_input_type !== $input_type) {
$replace_input_type = true;
}
}
if ($type_match_found
&& $param_type->hasCallableType()
) {
$potential_method_ids = [];
foreach ($input_type->getTypes() as $input_type_part) {
if ($input_type_part instanceof Type\Atomic\ObjectLike) {
$potential_method_id = TypeAnalyzer::getCallableMethodIdFromObjectLike(
$input_type_part,
$codebase,
$context->calling_method_id,
$statements_analyzer->getFilePath()
);
if ($potential_method_id) {
$potential_method_ids[] = $potential_method_id;
}
} elseif ($input_type_part instanceof Type\Atomic\TLiteralString) {
$potential_method_ids[] = $input_type_part->value;
}
}
foreach ($potential_method_ids as $potential_method_id) {
if (strpos($potential_method_id, '::')) {
$codebase->methods->methodExists(
$potential_method_id,
$context->calling_method_id,
null,
$statements_analyzer,
$statements_analyzer->getFilePath()
);
}
}
}
if ($context->strict_types
&& !$input_type->hasArray()
&& !$param_type->from_docblock
&& $cased_method_id !== 'echo'
&& $cased_method_id !== 'sprintf'
) {
$union_comparison_results->scalar_type_match_found = false;
if ($union_comparison_results->to_string_cast) {
$union_comparison_results->to_string_cast = false;
$type_match_found = false;
}
}
if ($union_comparison_results->type_coerced && !$input_type->hasMixed()) {
if ($union_comparison_results->type_coerced_from_mixed) {
if (IssueBuffer::accepts(
new MixedArgumentTypeCoercion(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', parent type ' . $input_type->getId() . ' provided',
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
} else {
if (IssueBuffer::accepts(
new ArgumentTypeCoercion(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', parent type ' . $input_type->getId() . ' provided',
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
}
}
if ($union_comparison_results->to_string_cast && $cased_method_id !== 'echo') {
if (IssueBuffer::accepts(
new ImplicitToStringCast(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' .
$param_type->getId() . ', ' . $input_type->getId() . ' provided with a __toString method',
$code_location
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if (!$type_match_found && !$union_comparison_results->type_coerced) {
$types_can_be_identical = TypeAnalyzer::canBeContainedBy(
$codebase,
$input_type,
$param_type,
true,
true
);
if ($union_comparison_results->scalar_type_match_found) {
if ($cased_method_id !== 'echo') {
if (IssueBuffer::accepts(
new InvalidScalarArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' .
$param_type->getId() . ', ' . $input_type->getId() . ' provided',
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
} elseif ($types_can_be_identical) {
if (IssueBuffer::accepts(
new PossiblyInvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', possibly different type ' . $input_type->getId() . ' provided',
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', ' . $input_type->getId() . ' provided',
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
return;
}
if ($input_expr instanceof PhpParser\Node\Scalar\String_
|| $input_expr instanceof PhpParser\Node\Expr\Array_
|| $input_expr instanceof PhpParser\Node\Expr\BinaryOp\Concat
) {
foreach ($param_type->getTypes() as $param_type_part) {
if ($param_type_part instanceof TClassString
&& $input_expr instanceof PhpParser\Node\Scalar\String_
) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$input_expr->value,
$code_location,
$statements_analyzer->getSuppressedIssues()
) === false
) {
return;
}
} elseif ($param_type_part instanceof TArray
&& $input_expr instanceof PhpParser\Node\Expr\Array_
) {
foreach ($param_type_part->type_params[1]->getTypes() as $param_array_type_part) {
if ($param_array_type_part instanceof TClassString) {
foreach ($input_expr->items as $item) {
if ($item && $item->value instanceof PhpParser\Node\Scalar\String_) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$item->value->value,
$code_location,
$statements_analyzer->getSuppressedIssues()
) === false
) {
return;
}
}
}
}
}
} elseif ($param_type_part instanceof TCallable) {
$function_ids = self::getFunctionIdsFromCallableArg(
$statements_analyzer,
$input_expr
);
foreach ($function_ids as $function_id) {
if (strpos($function_id, '::') !== false) {
$function_id_parts = explode('&', $function_id);
$non_existent_method_ids = [];
$has_valid_method = false;
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;
}
$function_id_part = $callable_fq_class_name . '::' . $method_name;
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$callable_fq_class_name,
$code_location,
$statements_analyzer->getSuppressedIssues()
) === false
) {
return;
}
if (!$codebase->classOrInterfaceExists($callable_fq_class_name)) {
return;
}
if (!$codebase->methodExists($function_id_part)
&& !$codebase->methodExists($callable_fq_class_name . '::__call')
) {
$non_existent_method_ids[] = $function_id_part;
} else {
$has_valid_method = true;
}
}
if (!$has_valid_method && !$param_type->hasString() && !$param_type->hasArray()) {
if (MethodAnalyzer::checkMethodExists(
$codebase,
$non_existent_method_ids[0],
$code_location,
$statements_analyzer->getSuppressedIssues()
) === false
) {
return;
}
}
} else {
if (!$param_type->hasString() && !$param_type->hasArray() && self::checkFunctionExists(
$statements_analyzer,
$function_id,
$code_location,
false
) === false
) {
return;
}
}
}
}
}
}
if (!$param_type->isNullable() && $cased_method_id !== 'echo') {
if ($input_type->isNull()) {
if (IssueBuffer::accepts(
new NullArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, ' .
'null value provided to parameter with type ' . $param_type->getId(),
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return null;
}
if ($input_type->isNullable() && !$input_type->ignore_nullable_issues) {
if (IssueBuffer::accepts(
new PossiblyNullArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, possibly ' .
'null value provided',
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
if ($input_type->isFalsable()
&& !$param_type->hasBool()
&& !$param_type->hasScalar()
&& !$input_type->ignore_falsable_issues
) {
if (IssueBuffer::accepts(
new PossiblyFalseArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be false, possibly ' .
'false value provided',
$code_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if (($type_match_found || $input_type->hasMixed())
&& !$function_param->by_ref
&& !($function_param->is_variadic xor $unpack)
&& $cased_method_id !== 'echo'
&& (!$in_call_map || $context->strict_types)
) {
self::coerceValueAfterGatekeeperArgument(
$statements_analyzer,
$input_type,
$replace_input_type,
$input_expr,
$param_type,
$signature_param_type,
$context,
$unpack
);
}
return null;
}
private static function processTaintedness(
StatementsAnalyzer $statements_analyzer,
string $cased_method_id,
int $argument_offset,
CodeLocation $code_location,
CodeLocation $function_location,
FunctionLikeParameter $function_param,
Type\Union &$input_type,
bool $function_is_pure
) : void {
$codebase = $statements_analyzer->getCodebase();
if (!$codebase->taint) {
return;
}
$method_sink = Sink::getForMethodArgument(
$cased_method_id,
$argument_offset,
$code_location
);
$child_sink = null;
if (($function_param->sink || ($child_sink = $codebase->taint->hasPreviousSink($method_sink)))
&& $input_type->sources
) {
$all_possible_sinks = [];
foreach ($input_type->sources as $source) {
if ($codebase->taint->hasExistingSink($source)) {
continue;
}
$base_sink = new Sink(
$source->id,
$source->code_location
);
$base_sink->children = [$child_sink ?: $method_sink];
$all_possible_sinks[] = $base_sink;
if (strpos($source->id, '::') && strpos($source->id, '#')) {
list($fq_classlike_name, $method_name) = explode('::', $source->id);
$method_name_parts = explode('#', $method_name);
$method_name = strtolower($method_name_parts[0]);
$class_storage = $codebase->classlike_storage_provider->get($fq_classlike_name);
foreach ($class_storage->dependent_classlikes as $dependent_classlike => $_) {
$new_sink = Sink::getForMethodArgument(
$dependent_classlike . '::' . $method_name,
(int) $method_name_parts[1] - 1,
$code_location,
null
);
$new_sink->children = [$child_sink ?: $method_sink];
$new_sink->taint = $child_sink ? $child_sink->taint : $function_param->sink;
$all_possible_sinks[] = $new_sink;
}
if (isset($class_storage->overridden_method_ids[$method_name])) {
foreach ($class_storage->overridden_method_ids[$method_name] as $parent_method_id) {
$new_sink = Sink::getForMethodArgument(
$parent_method_id,
(int) $method_name_parts[1] - 1,
$code_location,
null
);
$new_sink->taint = $child_sink ? $child_sink->taint : $function_param->sink;
$new_sink->children = [$child_sink ?: $method_sink];
$all_possible_sinks[] = $new_sink;
}
}
}
}
$codebase->taint->addSinks(
$statements_analyzer,
$all_possible_sinks
);
}
if ($function_param->sink
&& $input_type->tainted
&& ($function_param->sink & $input_type->tainted)
&& $input_type->sources
) {
$method_sink = Sink::getForMethodArgument(
$cased_method_id,
$argument_offset,
$code_location
);
$existing_sink = $codebase->taint->hasExistingSink($method_sink);
foreach ($input_type->sources as $input_source) {
$existing_source = $codebase->taint->hasExistingSource($input_source);
if (IssueBuffer::accepts(
new TaintedInput(
'in path ' . $codebase->taint->getPredecessorPath($existing_source ?: $input_source)
. ' out path ' . $codebase->taint->getSuccessorPath($existing_sink ?: $method_sink),
$code_location
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
} elseif ($input_type->sources) {
if ($function_is_pure) {
$codebase->taint->addSpecialization(
strtolower($cased_method_id . '#' . ($argument_offset + 1)),
$function_location->file_name . ':' . $function_location->raw_file_start
);
}
foreach ($input_type->sources as $type_source) {
if (($previous_source = $codebase->taint->hasPreviousSource($type_source)) || $input_type->tainted) {
if ($function_is_pure) {
$method_source = Source::getForMethodArgument(
$cased_method_id,
$argument_offset,
$code_location,
$function_location
);
} else {
$method_source = Source::getForMethodArgument(
$cased_method_id,
$argument_offset,
$code_location
);
}
$method_source->parents = [$previous_source ?: $type_source];
$codebase->taint->addSources(
$statements_analyzer,
[$method_source]
);
}
}
} elseif ($input_type->tainted) {
throw new \UnexpectedValueException(
'sources should exist for tainted var in '
. $code_location->getShortSummary()
);
}
if ($function_param->assert_untainted) {
$input_type = clone $input_type;
$input_type->tainted = null;
$input_type->sources = [];
}
}
private static function coerceValueAfterGatekeeperArgument(
StatementsAnalyzer $statements_analyzer,
Type\Union $input_type,
bool $input_type_changed,
PhpParser\Node\Expr $input_expr,
Type\Union $param_type,
?Type\Union $signature_param_type,
Context $context,
bool $unpack
) : void {
if ($param_type->hasMixed()) {
return;
}
if (!$input_type_changed && $param_type->from_docblock && !$input_type->hasMixed()) {
$input_type = clone $input_type;
foreach ($param_type->getTypes() as $param_atomic_type) {
if ($param_atomic_type instanceof Type\Atomic\TGenericObject) {
foreach ($input_type->getTypes() as $input_atomic_type) {
if ($input_atomic_type instanceof Type\Atomic\TGenericObject
&& $input_atomic_type->value === $param_atomic_type->value
) {
foreach ($input_atomic_type->type_params as $i => $type_param) {
if ($type_param->isEmpty() && isset($param_atomic_type->type_params[$i])) {
$input_type_changed = true;
$input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i];
}
}
}
}
}
}
if (!$input_type_changed) {
return;
}
}
$var_id = ExpressionAnalyzer::getVarId(
$input_expr,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
if ($var_id) {
$was_cloned = false;
if ($input_type->isNullable() && !$param_type->isNullable()) {
$input_type = clone $input_type;
$was_cloned = true;
$input_type->removeType('null');
}
if ($input_type->getId() === $param_type->getId()) {
if (!$was_cloned) {
$was_cloned = true;
$input_type = clone $input_type;
}
$input_type->from_docblock = false;
foreach ($input_type->getTypes() as $atomic_type) {
$atomic_type->from_docblock = false;
}
} elseif ($input_type->hasMixed() && $signature_param_type) {
$was_cloned = true;
$input_type = clone $signature_param_type;
if ($input_type->isNullable()) {
$input_type->ignore_nullable_issues = true;
}
}
if ($was_cloned) {
$context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
}
if ($unpack) {
$input_type = new Type\Union([
new TArray([
Type::getInt(),
$input_type
]),
]);
}
$context->vars_in_scope[$var_id] = $input_type;
}
}
/**
* @param PhpParser\Node\Scalar\String_|PhpParser\Node\Expr\Array_|PhpParser\Node\Expr\BinaryOp\Concat
* $callable_arg
*
* @return string[]
*/
public static function getFunctionIdsFromCallableArg(
\Psalm\FileSource $file_source,
$callable_arg
) {
if ($callable_arg instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
if ($callable_arg->left instanceof PhpParser\Node\Expr\ClassConstFetch
&& $callable_arg->left->class instanceof PhpParser\Node\Name
&& $callable_arg->left->name instanceof PhpParser\Node\Identifier
&& strtolower($callable_arg->left->name->name) === 'class'
&& !in_array(strtolower($callable_arg->left->class->parts[0]), ['self', 'static', 'parent'])
&& $callable_arg->right instanceof PhpParser\Node\Scalar\String_
&& preg_match('/^::[A-Za-z0-9]+$/', $callable_arg->right->value)
) {
return [
(string) $callable_arg->left->class->getAttribute('resolvedName') . $callable_arg->right->value
];
}
return [];
}
if ($callable_arg instanceof PhpParser\Node\Scalar\String_) {
$potential_id = preg_replace('/^\\\/', '', $callable_arg->value);
if (preg_match('/^[A-Za-z0-9_]+(\\\[A-Za-z0-9_]+)*(::[A-Za-z0-9_]+)?$/', $potential_id)) {
return [$potential_id];
}
return [];
}
if (count($callable_arg->items) !== 2) {
return [];
}
if (!isset($callable_arg->items[0]) || !isset($callable_arg->items[1])) {
throw new \UnexpectedValueException('These should never be unset');
}
$class_arg = $callable_arg->items[0]->value;
$method_name_arg = $callable_arg->items[1]->value;
if (!$method_name_arg instanceof PhpParser\Node\Scalar\String_) {
return [];
}
if ($class_arg instanceof PhpParser\Node\Scalar\String_) {
return [preg_replace('/^\\\/', '', $class_arg->value) . '::' . $method_name_arg->value];
}
if ($class_arg instanceof PhpParser\Node\Expr\ClassConstFetch
&& $class_arg->name instanceof PhpParser\Node\Identifier
&& strtolower($class_arg->name->name) === 'class'
&& $class_arg->class instanceof PhpParser\Node\Name
) {
$fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
$class_arg->class,
$file_source->getAliases()
);
return [$fq_class_name . '::' . $method_name_arg->value];
}
if (!isset($class_arg->inferredType) || !$class_arg->inferredType->hasObjectType()) {
return [];
}
$method_ids = [];
foreach ($class_arg->inferredType->getTypes() as $type_part) {
if ($type_part instanceof TNamedObject) {
$method_id = $type_part->value . '::' . $method_name_arg->value;
if ($type_part->extra_types) {
foreach ($type_part->extra_types as $extra_type) {
if ($extra_type instanceof Type\Atomic\TTemplateParam
|| $extra_type instanceof Type\Atomic\TObjectWithProperties
) {
throw new \UnexpectedValueException('Shouldnt get a generic param here');
}
$method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value;
}
}
$method_ids[] = $method_id;
}
}
return $method_ids;
}
/**
* @param StatementsAnalyzer $statements_analyzer
* @param string $function_id
* @param CodeLocation $code_location
* @param bool $can_be_in_root_scope if true, the function can be shortened to the root version
*
* @return bool
*/
protected static function checkFunctionExists(
StatementsAnalyzer $statements_analyzer,
&$function_id,
CodeLocation $code_location,
$can_be_in_root_scope
) {
$cased_function_id = $function_id;
$function_id = strtolower($function_id);
$codebase = $statements_analyzer->getCodebase();
if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) {
$root_function_id = preg_replace('/.*\\\/', '', $function_id);
if ($can_be_in_root_scope
&& $function_id !== $root_function_id
&& $codebase->functions->functionExists($statements_analyzer, $root_function_id)
) {
$function_id = $root_function_id;
} else {
if (IssueBuffer::accepts(
new UndefinedFunction(
'Function ' . $cased_function_id . ' does not exist',
$code_location,
$function_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return false;
}
}
return true;
}
/**
* @param PhpParser\Node\Identifier|PhpParser\Node\Name $expr
* @param \Psalm\Storage\Assertion[] $assertions
* @param string $thisName
* @param array<int, PhpParser\Node\Arg> $args
* @param Context $context
* @param array<string, array<string, array{Type\Union}>> $template_type_map,
* @param StatementsAnalyzer $statements_analyzer
*
* @return void
*/
protected static function applyAssertionsToContext(
$expr,
?string $thisName,
array $assertions,
array $args,
array $template_type_map,
Context $context,
StatementsAnalyzer $statements_analyzer
) {
$type_assertions = [];
$asserted_keys = [];
foreach ($assertions as $assertion) {
$assertion_var_id = null;
$arg_value = null;
if (is_int($assertion->var_id)) {
if (!isset($args[$assertion->var_id])) {
continue;
}
$arg_value = $args[$assertion->var_id]->value;
$arg_var_id = ExpressionAnalyzer::getArrayVarId($arg_value, null, $statements_analyzer);
if ($arg_var_id) {
$assertion_var_id = $arg_var_id;
}
} elseif (isset($context->vars_in_scope[$assertion->var_id])) {
$assertion_var_id = $assertion->var_id;
} elseif (strpos($assertion->var_id, '$this->') === 0 && !is_null($thisName)) {
$assertion_var_id = $thisName . str_replace('$this->', '->', $assertion->var_id);
}
if ($assertion_var_id) {
$rule = $assertion->rule[0][0];
$prefix = '';
if ($rule[0] === '!') {
$prefix .= '!';
$rule = substr($rule, 1);
}
if ($rule[0] === '=') {
$prefix .= '=';
$rule = substr($rule, 1);
}
if ($rule[0] === '~') {
$prefix .= '~';
$rule = substr($rule, 1);
}
if (isset($template_type_map[$rule])) {
foreach ($template_type_map[$rule] as $template_map) {
if ($template_map[0]->hasMixed()) {
continue 2;
}
$replacement_atomic_types = $template_map[0]->getTypes();
if (count($replacement_atomic_types) > 1) {
continue 2;
}
$ored_type_assertions = [];
foreach ($replacement_atomic_types as $replacement_atomic_type) {
if ($replacement_atomic_type instanceof Type\Atomic\TMixed) {
continue 3;
}
if ($replacement_atomic_type instanceof Type\Atomic\TArray
|| $replacement_atomic_type instanceof Type\Atomic\ObjectLike
) {
$ored_type_assertions[] = $prefix . 'array';
} elseif ($replacement_atomic_type instanceof Type\Atomic\TNamedObject) {
$ored_type_assertions[] = $prefix . $replacement_atomic_type->value;
} elseif ($replacement_atomic_type instanceof Type\Atomic\Scalar) {
$ored_type_assertions[] = $prefix . $replacement_atomic_type->getId();
} elseif ($replacement_atomic_type instanceof Type\Atomic\TNull) {
$ored_type_assertions[] = $prefix . 'null';
} elseif ($replacement_atomic_type instanceof Type\Atomic\TTemplateParam) {
$ored_type_assertions[] = $prefix . $replacement_atomic_type->param_name;
}
}
if ($ored_type_assertions) {
$type_assertions[$assertion_var_id] = [$ored_type_assertions];
}
}
} else {
$type_assertions[$assertion_var_id] = $assertion->rule;
}
} elseif ($arg_value && ($assertion->rule === [['!falsy']] || $assertion->rule === [['true']])) {
if ($assertion->rule === [['true']]) {
$assert_clauses = \Psalm\Type\Algebra::getFormula(
new PhpParser\Node\Expr\BinaryOp\Identical(
$arg_value,
new PhpParser\Node\Expr\ConstFetch(new PhpParser\Node\Name('true'))
),
$statements_analyzer->getFQCLN(),
$statements_analyzer,
$statements_analyzer->getCodebase()
);
} else {
$assert_clauses = \Psalm\Type\Algebra::getFormula(
$arg_value,
$statements_analyzer->getFQCLN(),
$statements_analyzer,
$statements_analyzer->getCodebase()
);
}
$simplified_clauses = \Psalm\Type\Algebra::simplifyCNF(
array_merge($context->clauses, $assert_clauses)
);
$assert_type_assertions = \Psalm\Type\Algebra::getTruthsFromFormula(
$simplified_clauses
);
$type_assertions = array_merge($type_assertions, $assert_type_assertions);
}
}
$changed_vars = [];
foreach ($type_assertions as $var_id => $_) {
$asserted_keys[$var_id] = true;
}
if ($type_assertions) {
// while in an and, we allow scope to boil over to support
// statements of the form if ($x && $x->foo())
$op_vars_in_scope = \Psalm\Type\Reconciler::reconcileKeyedTypes(
$type_assertions,
$context->vars_in_scope,
$changed_vars,
$asserted_keys,
$statements_analyzer,
$template_type_map,
$context->inside_loop,
new CodeLocation($statements_analyzer->getSource(), $expr)
);
foreach ($changed_vars as $changed_var) {
if (isset($op_vars_in_scope[$changed_var])) {
$op_vars_in_scope[$changed_var]->from_docblock = true;
foreach ($op_vars_in_scope[$changed_var]->getTypes() as $changed_atomic_type) {
$changed_atomic_type->from_docblock = true;
if ($changed_atomic_type instanceof Type\Atomic\TNamedObject
&& $changed_atomic_type->extra_types
) {
foreach ($changed_atomic_type->extra_types as $extra_type) {
$extra_type->from_docblock = true;
}
}
}
}
}
$context->vars_in_scope = $op_vars_in_scope;
}
}
}