1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-13 17:57:37 +01:00
psalm/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php
2019-01-06 13:02:46 -05:00

1635 lines
65 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;
use PhpParser;
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
use Psalm\Internal\Codebase\CallMap;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\CodeLocation;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\InvalidReturnType;
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Reconciler;
use Psalm\Internal\Type\TypeCombination;
/**
* @internal
*/
class FunctionAnalyzer extends FunctionLikeAnalyzer
{
public function __construct(PhpParser\Node\Stmt\Function_ $function, SourceAnalyzer $source)
{
$codebase = $source->getCodebase();
$file_storage_provider = $codebase->file_storage_provider;
$file_storage = $file_storage_provider->get($source->getFilePath());
$namespace = $source->getNamespace();
$function_id = ($namespace ? strtolower($namespace) . '\\' : '') . strtolower($function->name->name);
if (!isset($file_storage->functions[$function_id])) {
throw new \UnexpectedValueException(
'Function ' . $function_id . ' should be defined in ' . $source->getFilePath()
);
}
$storage = $file_storage->functions[$function_id];
parent::__construct($function, $source, $storage);
}
/**
* @param string $function_id
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
* @param array $suppressed_issues
*
* @return Type\Union
*/
public static function getReturnTypeFromCallMapWithArgs(
StatementsAnalyzer $statements_analyzer,
$function_id,
array $call_args,
Context $context,
CodeLocation $code_location,
array $suppressed_issues
) {
$call_map_key = strtolower($function_id);
$call_map = CallMap::getCallMap();
if (!isset($call_map[$call_map_key])) {
throw new \InvalidArgumentException('Function ' . $function_id . ' was not found in callmap');
}
if (!$call_args) {
switch ($call_map_key) {
case 'getenv':
return new Type\Union([new Type\Atomic\TArray([Type::getArrayKey(), Type::getString()])]);
case 'gettimeofday':
return new Type\Union([
new Type\Atomic\TArray([
Type::getString(),
Type::getInt()
])
]);
case 'microtime':
return Type::getString();
case 'get_called_class':
return new Type\Union([new Type\Atomic\TClassString($context->self ?: 'object')]);
case 'get_parent_class':
$codebase = $statements_analyzer->getCodebase();
if ($context->self && $codebase->classExists($context->self)) {
$classlike_storage = $codebase->classlike_storage_provider->get($context->self);
if ($classlike_storage->parent_classes) {
return new Type\Union([
new Type\Atomic\TClassString(
array_values($classlike_storage->parent_classes)[0]
)
]);
}
}
}
} else {
switch ($call_map_key) {
case 'str_replace':
case 'str_ireplace':
case 'substr_replace':
case 'preg_replace':
case 'preg_replace_callback':
if (isset($call_args[2]->value->inferredType)) {
$subject_type = $call_args[2]->value->inferredType;
if (!$subject_type->hasString() && $subject_type->hasArray()) {
return Type::getArray();
}
$return_type = Type::getString();
if (in_array($call_map_key, ['preg_replace', 'preg_replace_callback'], true)) {
$return_type->addType(new Type\Atomic\TNull());
$codebase = $statements_analyzer->getCodebase();
if ($codebase->config->ignore_internal_nullable_issues) {
$return_type->ignore_nullable_issues = true;
}
}
return $return_type;
}
return Type::getMixed();
case 'pathinfo':
if (isset($call_args[1])) {
return Type::getString();
}
return Type::getArray();
case 'current':
case 'next':
case 'prev':
case 'reset':
case 'end':
return self::getArrayPointerAdjustReturn($call_args, $statements_analyzer->getCodebase());
case 'count':
if (isset($call_args[0]->value->inferredType)) {
$atomic_types = $call_args[0]->value->inferredType->getTypes();
if (count($atomic_types) === 1 && isset($atomic_types['array'])) {
if ($atomic_types['array'] instanceof Type\Atomic\TNonEmptyArray) {
return new Type\Union([
$atomic_types['array']->count !== null
? new Type\Atomic\TLiteralInt($atomic_types['array']->count)
: new Type\Atomic\TInt
]);
} elseif ($atomic_types['array'] instanceof Type\Atomic\ObjectLike
&& $atomic_types['array']->sealed
) {
return new Type\Union([
new Type\Atomic\TLiteralInt(count($atomic_types['array']->properties))
]);
}
}
}
break;
case 'var_export':
case 'highlight_string':
case 'highlight_file':
if (isset($call_args[1]->value->inferredType)) {
$subject_type = $call_args[1]->value->inferredType;
if ((string) $subject_type === 'true') {
return Type::getString();
}
return new Type\Union([
new Type\Atomic\TString,
$call_map_key === 'var_export' ? new Type\Atomic\TNull : new Type\Atomic\TBool
]);
}
return $call_map_key === 'var_export' ? Type::getVoid() : Type::getBool();
case 'print_r':
if (isset($call_args[1]->value->inferredType)) {
$subject_type = $call_args[1]->value->inferredType;
if ((string) $subject_type === 'true') {
return Type::getString();
}
}
return new Type\Union([
new Type\Atomic\TString,
new Type\Atomic\TTrue
]);
case 'microtime':
if (isset($call_args[0]->value->inferredType)) {
$subject_type = $call_args[0]->value->inferredType;
if ((string) $subject_type === 'true') {
return Type::getFloat();
}
if ((string) $subject_type === 'false') {
return Type::getString();
}
}
return new Type\Union([
new Type\Atomic\TFloat,
new Type\Atomic\TString
]);
case 'getenv':
return new Type\Union([new Type\Atomic\TString, new Type\Atomic\TFalse]);
case 'gettimeofday':
if (isset($call_args[0]->value->inferredType)) {
$subject_type = $call_args[0]->value->inferredType;
if ((string) $subject_type === 'true') {
return Type::getFloat();
}
if ((string) $subject_type === 'false') {
return new Type\Union([
new Type\Atomic\TArray([
Type::getString(),
Type::getInt()
])
]);
}
}
break;
case 'array_map':
return self::getArrayMapReturnType(
$statements_analyzer,
$context,
$call_args
);
case 'array_filter':
return self::getArrayFilterReturnType(
$statements_analyzer,
$call_args,
$code_location,
$suppressed_issues
);
case 'array_reduce':
return self::getArrayReduceReturnType(
$statements_analyzer,
$context,
$call_args
);
case 'array_merge':
return self::getArrayMergeReturnType($call_args);
case 'array_rand':
return self::getArrayRandReturnType($call_args);
case 'array_slice':
return self::getArraySliceReturnType($call_args);
case 'array_pop':
case 'array_shift':
return self::getArrayPopReturnType($call_args, $statements_analyzer->getCodebase());
case 'explode':
if ($call_args[0]->value instanceof PhpParser\Node\Scalar\String_) {
if ($call_args[0]->value->value === '') {
return Type::getFalse();
}
return new Type\Union([
new Type\Atomic\TNonEmptyArray([
Type::getInt(),
Type::getString()
])
]);
} elseif (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->hasString()
) {
$falsable_array = new Type\Union([
new Type\Atomic\TNonEmptyArray([
Type::getInt(),
Type::getString()
]),
new Type\Atomic\TFalse
]);
$codebase = $statements_analyzer->getCodebase();
if ($codebase->config->ignore_internal_falsable_issues) {
$falsable_array->ignore_falsable_issues = true;
}
return $falsable_array;
}
break;
case 'iterator_to_array':
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->hasObjectType()
) {
$value_type = null;
foreach ($call_args[0]->value->inferredType->getTypes() as $call_arg_atomic_type) {
if ($call_arg_atomic_type instanceof Type\Atomic\TGenericObject) {
$type_params = $call_arg_atomic_type->type_params;
$last_param_type = $type_params[count($type_params) - 1];
$value_type = $value_type
? Type::combineUnionTypes($value_type, $last_param_type)
: $last_param_type;
}
}
if ($value_type) {
return new Type\Union([
new Type\Atomic\TArray([
Type::getArrayKey(),
$value_type
])
]);
}
}
break;
case 'array_column':
$row_shape = null;
// calculate row shape
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->isSingle()
&& $call_args[0]->value->inferredType->hasArray()
) {
$input_array = $call_args[0]->value->inferredType->getTypes()['array'];
if ($input_array instanceof Type\Atomic\ObjectLike) {
$row_type = $input_array->getGenericArrayType()->type_params[1];
if ($row_type->isSingle() && $row_type->hasArray()) {
$row_shape = $row_type->getTypes()['array'];
}
} elseif ($input_array instanceof Type\Atomic\TArray) {
$row_type = $input_array->type_params[1];
if ($row_type->isSingle() && $row_type->hasArray()) {
$row_shape = $row_type->getTypes()['array'];
}
}
}
$value_column_name = null;
// calculate value column name
if (isset($call_args[1]->value->inferredType)) {
$value_column_name_arg= $call_args[1]->value->inferredType;
if ($value_column_name_arg->isSingleIntLiteral()) {
$value_column_name = $value_column_name_arg->getSingleIntLiteral()->value;
} elseif ($value_column_name_arg->isSingleStringLiteral()) {
$value_column_name = $value_column_name_arg->getSingleStringLiteral()->value;
}
}
$key_column_name = null;
// calculate key column name
if (isset($call_args[2]->value->inferredType)) {
$key_column_name_arg = $call_args[2]->value->inferredType;
if ($key_column_name_arg->isSingleIntLiteral()) {
$key_column_name = $key_column_name_arg->getSingleIntLiteral()->value;
} elseif ($key_column_name_arg->isSingleStringLiteral()) {
$key_column_name = $key_column_name_arg->getSingleStringLiteral()->value;
}
}
$result_key_type = Type::getArrayKey();
$result_element_type = null;
// calculate results
if ($row_shape instanceof Type\Atomic\ObjectLike) {
if ((null !== $value_column_name) && isset($row_shape->properties[$value_column_name])) {
$result_element_type = $row_shape->properties[$value_column_name];
} else {
$result_element_type = Type::getMixed();
}
if ((null !== $key_column_name) && isset($row_shape->properties[$key_column_name])) {
$result_key_type = $row_shape->properties[$key_column_name];
}
}
if ($result_element_type) {
return new Type\Union([
new Type\Atomic\TArray([
$result_key_type,
$result_element_type
])
]);
}
break;
case 'abs':
if (isset($call_args[0]->value)) {
$first_arg = $call_args[0]->value;
if (isset($first_arg->inferredType)) {
$numeric_types = [];
foreach ($first_arg->inferredType->getTypes() as $inner_type) {
if ($inner_type->isNumericType()) {
$numeric_types[] = $inner_type;
}
}
if ($numeric_types) {
return new Type\Union($numeric_types);
}
}
}
break;
case 'version_compare':
if (count($call_args) > 2) {
if (isset($call_args[2]->value->inferredType)) {
$operator_type = $call_args[2]->value->inferredType;
if (!$operator_type->hasMixed()) {
$acceptable_operator_type = new Type\Union([
new Type\Atomic\TLiteralString('<'),
new Type\Atomic\TLiteralString('lt'),
new Type\Atomic\TLiteralString('<='),
new Type\Atomic\TLiteralString('le'),
new Type\Atomic\TLiteralString('>'),
new Type\Atomic\TLiteralString('gt'),
new Type\Atomic\TLiteralString('>='),
new Type\Atomic\TLiteralString('ge'),
new Type\Atomic\TLiteralString('=='),
new Type\Atomic\TLiteralString('='),
new Type\Atomic\TLiteralString('eq'),
new Type\Atomic\TLiteralString('!='),
new Type\Atomic\TLiteralString('<>'),
new Type\Atomic\TLiteralString('ne'),
]);
$codebase = $statements_analyzer->getCodebase();
if (TypeAnalyzer::isContainedBy(
$codebase,
$operator_type,
$acceptable_operator_type
)) {
return Type::getBool();
}
}
}
return new Type\Union([
new Type\Atomic\TBool,
new Type\Atomic\TNull
]);
}
return new Type\Union([
new Type\Atomic\TLiteralInt(-1),
new Type\Atomic\TLiteralInt(0),
new Type\Atomic\TLiteralInt(1)
]);
case 'parse_url':
if (count($call_args) > 1) {
if (isset($call_args[1]->value->inferredType)) {
$component_type = $call_args[1]->value->inferredType;
if (!$component_type->hasMixed()) {
$codebase = $statements_analyzer->getCodebase();
$acceptable_string_component_type = new Type\Union([
new Type\Atomic\TLiteralInt(PHP_URL_SCHEME),
new Type\Atomic\TLiteralInt(PHP_URL_USER),
new Type\Atomic\TLiteralInt(PHP_URL_PASS),
new Type\Atomic\TLiteralInt(PHP_URL_HOST),
new Type\Atomic\TLiteralInt(PHP_URL_PATH),
new Type\Atomic\TLiteralInt(PHP_URL_QUERY),
new Type\Atomic\TLiteralInt(PHP_URL_FRAGMENT),
]);
$acceptable_int_component_type = new Type\Union([
new Type\Atomic\TLiteralInt(PHP_URL_PORT)
]);
if (TypeAnalyzer::isContainedBy(
$codebase,
$component_type,
$acceptable_string_component_type
)) {
$nullable_string = new Type\Union([
new Type\Atomic\TString,
new Type\Atomic\TNull
]);
$codebase = $statements_analyzer->getCodebase();
if ($codebase->config->ignore_internal_nullable_issues) {
$nullable_string->ignore_nullable_issues = true;
}
return $nullable_string;
}
if (TypeAnalyzer::isContainedBy(
$codebase,
$component_type,
$acceptable_int_component_type
)) {
$nullable_int = new Type\Union([
new Type\Atomic\TInt,
new Type\Atomic\TNull
]);
$codebase = $statements_analyzer->getCodebase();
if ($codebase->config->ignore_internal_nullable_issues) {
$nullable_int->ignore_nullable_issues = true;
}
return $nullable_int;
}
}
}
$nullable_string_or_int = new Type\Union([
new Type\Atomic\TString,
new Type\Atomic\TInt,
new Type\Atomic\TNull
]);
$codebase = $statements_analyzer->getCodebase();
if ($codebase->config->ignore_internal_nullable_issues) {
$nullable_string_or_int->ignore_nullable_issues = true;
}
return $nullable_string_or_int;
}
$component_key_type = new Type\Union([
new Type\Atomic\TLiteralString('scheme'),
new Type\Atomic\TLiteralString('user'),
new Type\Atomic\TLiteralString('pass'),
new Type\Atomic\TLiteralString('host'),
new Type\Atomic\TLiteralString('port'),
new Type\Atomic\TLiteralString('path'),
new Type\Atomic\TLiteralString('query'),
new Type\Atomic\TLiteralString('fragment'),
]);
$nullable_string_or_int = new Type\Union([
new Type\Atomic\TArray([$component_key_type, Type::getMixed()]),
new Type\Atomic\TFalse
]);
$codebase = $statements_analyzer->getCodebase();
if ($codebase->config->ignore_internal_falsable_issues) {
$nullable_string_or_int->ignore_falsable_issues = true;
}
return $nullable_string_or_int;
case 'min':
case 'max':
if (isset($call_args[0])) {
$first_arg = $call_args[0]->value;
if (isset($first_arg->inferredType)) {
if ($first_arg->inferredType->hasArray()) {
$array_type = $first_arg->inferredType->getTypes()['array'];
if ($array_type instanceof Type\Atomic\ObjectLike) {
return $array_type->getGenericValueType();
}
if ($array_type instanceof Type\Atomic\TArray) {
return clone $array_type->type_params[1];
}
} elseif ($first_arg->inferredType->hasScalarType() &&
($second_arg = $call_args[1]->value) &&
isset($second_arg->inferredType) &&
$second_arg->inferredType->hasScalarType()
) {
return Type::combineUnionTypes($first_arg->inferredType, $second_arg->inferredType);
}
}
}
break;
case 'filter_var':
return self::getFilterVar($call_args);
case 'range':
$all_ints = true;
foreach ($call_args as $call_arg) {
$all_ints = $all_ints
&& isset($call_arg->value->inferredType)
&& $call_arg->value->inferredType->isInt();
}
if ($all_ints) {
return new Type\Union([new Type\Atomic\TArray([Type::getInt(), Type::getInt()])]);
}
return new Type\Union([new Type\Atomic\TArray([
Type::getInt(),
new Type\Union([new Type\Atomic\TInt, new Type\Atomic\TFloat])
])]);
case 'get_parent_class':
// this is unreliable, as it's hard to know exactly what's wanted - attempted this in
// https://github.com/vimeo/psalm/commit/355ed831e1c69c96bbf9bf2654ef64786cbe9fd7
// but caused problems where it didnt know exactly what level of child we
// were receiving.
//
// Really this should only work on instances we've created with new Foo(),
// but that requires more work
break;
}
}
if (!$call_map[$call_map_key][0]) {
return Type::getMixed();
}
$call_map_return_type = Type::parseString($call_map[$call_map_key][0]);
switch ($call_map_key) {
case 'mb_strpos':
case 'mb_strrpos':
case 'mb_stripos':
case 'mb_strripos':
case 'strpos':
case 'strrpos':
case 'stripos':
case 'strripos':
case 'strstr':
case 'stristr':
case 'strrchr':
case 'strpbrk':
case 'array_search':
break;
default:
$codebase = $statements_analyzer->getCodebase();
if ($call_map_return_type->isFalsable()
&& $codebase->config->ignore_internal_falsable_issues
) {
$call_map_return_type->ignore_falsable_issues = true;
}
}
return $call_map_return_type;
}
/**
* @param array<PhpParser\Node\Arg> $call_args
*
* @return Type\Union
*/
private static function getFilterVar(array $call_args)
{
if (isset($call_args[1]->value->inferredType)
&& $call_args[1]->value->inferredType->isSingleIntLiteral()
) {
$filter_type_type = $call_args[1]->value->inferredType->getSingleIntLiteral();
$filter_type = null;
switch ($filter_type_type->value) {
case \FILTER_VALIDATE_INT:
$filter_type = Type::getInt();
break;
case \FILTER_VALIDATE_FLOAT:
$filter_type = Type::getFloat();
break;
case \FILTER_VALIDATE_BOOLEAN:
$filter_type = Type::getBool();
break;
case \FILTER_VALIDATE_IP:
case \FILTER_VALIDATE_MAC:
case \FILTER_VALIDATE_REGEXP:
case \FILTER_VALIDATE_URL:
case \FILTER_VALIDATE_EMAIL:
case \FILTER_VALIDATE_DOMAIN:
$filter_type = Type::getString();
break;
}
$has_object_like = false;
if (isset($call_args[2]->value->inferredType) && $filter_type) {
foreach ($call_args[2]->value->inferredType->getTypes() as $atomic_type) {
if ($atomic_type instanceof Type\Atomic\ObjectLike) {
$has_object_like = true;
if (isset($atomic_type->properties['options'])
&& $atomic_type->properties['options']->hasArray()
&& ($options_array = $atomic_type->properties['options']->getTypes()['array'])
&& $options_array instanceof Type\Atomic\ObjectLike
&& isset($options_array->properties['default'])
) {
$filter_type = Type::combineUnionTypes(
$filter_type,
$options_array->properties['default']
);
} else {
$filter_type->addType(new Type\Atomic\TFalse);
}
if (isset($atomic_type->properties['flags'])
&& $atomic_type->properties['flags']->isSingleIntLiteral()
) {
$filter_flag_type =
$atomic_type->properties['flags']->getSingleIntLiteral();
if ($filter_type->hasBool()
&& $filter_flag_type->value === \FILTER_NULL_ON_FAILURE
) {
$filter_type->addType(new Type\Atomic\TNull);
}
}
} elseif ($atomic_type instanceof Type\Atomic\TLiteralInt) {
if ($filter_type->hasBool() && $atomic_type->value === \FILTER_NULL_ON_FAILURE) {
$filter_type->addType(new Type\Atomic\TNull);
}
}
}
}
if (!$has_object_like && $filter_type) {
$filter_type->addType(new Type\Atomic\TFalse);
}
return $filter_type ?: Type::getMixed();
}
return Type::getMixed();
}
/**
* @param array<PhpParser\Node\Arg> $call_args
*
* @return Type\Union
*/
private static function getArrayPointerAdjustReturn(array $call_args, Codebase $codebase)
{
$first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
$first_arg_array = $first_arg
&& isset($first_arg->inferredType)
&& $first_arg->inferredType->hasType('array')
&& ($array_atomic_type = $first_arg->inferredType->getTypes()['array'])
&& ($array_atomic_type instanceof Type\Atomic\TArray ||
$array_atomic_type instanceof Type\Atomic\ObjectLike)
? $array_atomic_type
: null;
if (!$first_arg_array) {
return Type::getMixed();
}
if ($first_arg_array instanceof Type\Atomic\TArray) {
$value_type = clone $first_arg_array->type_params[1];
} else {
$value_type = $first_arg_array->getGenericValueType();
}
$value_type->addType(new Type\Atomic\TFalse);
if ($codebase->config->ignore_internal_falsable_issues) {
$value_type->ignore_falsable_issues = true;
}
return $value_type;
}
/**
* @param array<PhpParser\Node\Arg> $call_args
*
* @return Type\Union
*/
private static function getArrayMergeReturnType(array $call_args)
{
$inner_value_types = [];
$inner_key_types = [];
$generic_properties = [];
foreach ($call_args as $call_arg) {
if (!isset($call_arg->value->inferredType)) {
return Type::getArray();
}
foreach ($call_arg->value->inferredType->getTypes() as $type_part) {
if ($call_arg->unpack) {
if (!$type_part instanceof Type\Atomic\TArray) {
if ($type_part instanceof Type\Atomic\ObjectLike) {
$type_part_value_type = $type_part->getGenericValueType();
} else {
return Type::getArray();
}
} else {
$type_part_value_type = $type_part->type_params[1];
}
$unpacked_type_parts = [];
foreach ($type_part_value_type->getTypes() as $value_type_part) {
$unpacked_type_parts[] = $value_type_part;
}
} else {
$unpacked_type_parts = [$type_part];
}
foreach ($unpacked_type_parts as $unpacked_type_part) {
if (!$unpacked_type_part instanceof Type\Atomic\TArray) {
if ($unpacked_type_part instanceof Type\Atomic\ObjectLike) {
if ($generic_properties !== null) {
$generic_properties = array_merge(
$generic_properties,
$unpacked_type_part->properties
);
}
$unpacked_type_part = $unpacked_type_part->getGenericArrayType();
} else {
if ($unpacked_type_part instanceof Type\Atomic\TMixed
&& $unpacked_type_part->from_loop_isset
) {
$unpacked_type_part = new Type\Atomic\TArray([
Type::getArrayKey(),
Type::getMixed(true)
]);
} else {
return Type::getArray();
}
}
} elseif (!$unpacked_type_part->type_params[0]->isEmpty()) {
$generic_properties = null;
}
if ($unpacked_type_part->type_params[1]->isEmpty()) {
continue;
}
$inner_key_types = array_merge(
$inner_key_types,
array_values($unpacked_type_part->type_params[0]->getTypes())
);
$inner_value_types = array_merge(
$inner_value_types,
array_values($unpacked_type_part->type_params[1]->getTypes())
);
}
}
}
if ($generic_properties) {
return new Type\Union([
new Type\Atomic\ObjectLike($generic_properties),
]);
}
if ($inner_value_types) {
return new Type\Union([
new Type\Atomic\TArray([
TypeCombination::combineTypes($inner_key_types, null, true),
TypeCombination::combineTypes($inner_value_types, null, true),
]),
]);
}
return Type::getArray();
}
/**
* @param array<PhpParser\Node\Arg> $call_args
*
* @return Type\Union
*/
private static function getArrayRandReturnType(array $call_args)
{
$first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
$second_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
$first_arg_array = $first_arg
&& isset($first_arg->inferredType)
&& $first_arg->inferredType->hasType('array')
&& ($array_atomic_type = $first_arg->inferredType->getTypes()['array'])
&& ($array_atomic_type instanceof Type\Atomic\TArray ||
$array_atomic_type instanceof Type\Atomic\ObjectLike)
? $array_atomic_type
: null;
if (!$first_arg_array) {
return Type::getMixed();
}
if ($first_arg_array instanceof Type\Atomic\TArray) {
$key_type = clone $first_arg_array->type_params[0];
} else {
$key_type = $first_arg_array->getGenericKeyType();
}
if (!$second_arg
|| ($second_arg instanceof PhpParser\Node\Scalar\LNumber && $second_arg->value === 1)
) {
return $key_type;
}
$arr_type = new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$key_type,
]),
]);
if ($second_arg instanceof PhpParser\Node\Scalar\LNumber) {
return $arr_type;
}
return Type::combineUnionTypes($key_type, $arr_type);
}
/**
* @param array<PhpParser\Node\Arg> $call_args
*
* @return Type\Union
*/
private static function getArraySliceReturnType(array $call_args)
{
$first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
$preserve_keys_arg = isset($call_args[3]->value) ? $call_args[3]->value : null;
$first_arg_array = $first_arg
&& isset($first_arg->inferredType)
&& $first_arg->inferredType->hasType('array')
&& ($array_atomic_type = $first_arg->inferredType->getTypes()['array'])
&& ($array_atomic_type instanceof Type\Atomic\TArray ||
$array_atomic_type instanceof Type\Atomic\ObjectLike)
? $array_atomic_type
: null;
if (!$first_arg_array) {
return Type::getArray();
}
if (!$preserve_keys_arg
|| ($preserve_keys_arg instanceof PhpParser\Node\Expr\ConstFetch
&& strtolower($preserve_keys_arg->name->parts[0]) === 'false')
) {
if ($first_arg_array instanceof Type\Atomic\TArray) {
$value_type = clone $first_arg_array->type_params[1];
} else {
$value_type = $first_arg_array->getGenericValueType();
}
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$value_type,
]),
]);
}
if ($first_arg_array instanceof Type\Atomic\TArray) {
return new Type\Union([clone $first_arg_array]);
}
return new Type\Union([$first_arg_array->getGenericArrayType()]);
}
/**
* @param array<PhpParser\Node\Arg> $call_args
*
* @return Type\Union
*/
private static function getArrayPopReturnType(array $call_args, Codebase $codebase)
{
$first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
$first_arg_array = $first_arg
&& isset($first_arg->inferredType)
&& $first_arg->inferredType->hasType('array')
&& ($array_atomic_type = $first_arg->inferredType->getTypes()['array'])
&& ($array_atomic_type instanceof Type\Atomic\TArray ||
$array_atomic_type instanceof Type\Atomic\ObjectLike)
? $array_atomic_type
: null;
if (!$first_arg_array) {
return Type::getMixed();
}
$nullable = false;
if ($first_arg_array instanceof Type\Atomic\TArray) {
$value_type = clone $first_arg_array->type_params[1];
if ($value_type->isEmpty()) {
return Type::getNull();
}
if (!$first_arg_array instanceof Type\Atomic\TNonEmptyArray) {
$nullable = true;
}
} else {
$value_type = $first_arg_array->getGenericValueType();
if (!$first_arg_array->sealed) {
$nullable = true;
}
}
if ($nullable) {
$value_type->addType(new Type\Atomic\TNull);
if ($codebase->config->ignore_internal_nullable_issues) {
$value_type->ignore_nullable_issues = true;
}
}
return $value_type;
}
/**
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
* @param array $suppressed_issues
*
* @return Type\Union
*/
private static function getArrayMapReturnType(
StatementsAnalyzer $statements_analyzer,
Context $context,
$call_args
) {
$array_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
$array_arg_type = null;
if ($array_arg && isset($array_arg->inferredType)) {
$arg_types = $array_arg->inferredType->getTypes();
if (isset($arg_types['array'])
&& ($arg_types['array'] instanceof Type\Atomic\TArray
|| $arg_types['array'] instanceof Type\Atomic\ObjectLike)
) {
$array_arg_type = $arg_types['array'];
}
}
if (isset($call_args[0])) {
$function_call_arg = $call_args[0];
if (count($call_args) === 2) {
if ($array_arg_type instanceof Type\Atomic\ObjectLike) {
$generic_key_type = $array_arg_type->getGenericKeyType();
} else {
$generic_key_type = $array_arg_type ? clone $array_arg_type->type_params[0] : Type::getArrayKey();
}
} else {
$generic_key_type = Type::getInt();
}
if (isset($function_call_arg->value->inferredType)
&& ($first_arg_atomic_types = $function_call_arg->value->inferredType->getTypes())
&& ($closure_atomic_type = isset($first_arg_atomic_types['Closure'])
? $first_arg_atomic_types['Closure']
: null)
&& $closure_atomic_type instanceof Type\Atomic\Fn
) {
$closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed();
if ($closure_return_type->isVoid()) {
$closure_return_type = Type::getNull();
}
$inner_type = clone $closure_return_type;
if ($array_arg_type instanceof Type\Atomic\ObjectLike && count($call_args) === 2) {
return new Type\Union([
new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($inner_type) {
return clone $inner_type;
},
$array_arg_type->properties
)
),
]);
}
if ($array_arg_type instanceof Type\Atomic\TNonEmptyArray) {
return new Type\Union([
new Type\Atomic\TNonEmptyArray([
$generic_key_type,
$inner_type,
]),
]);
}
return new Type\Union([
new Type\Atomic\TArray([
$generic_key_type,
$inner_type,
]),
]);
} elseif ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_
|| $function_call_arg->value instanceof PhpParser\Node\Expr\Array_
|| $function_call_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat
) {
$mapping_function_ids = Statements\Expression\CallAnalyzer::getFunctionIdsFromCallableArg(
$statements_analyzer,
$function_call_arg->value
);
$call_map = CallMap::getCallMap();
$mapping_return_type = null;
$codebase = $statements_analyzer->getCodebase();
foreach ($mapping_function_ids as $mapping_function_id) {
$mapping_function_id = strtolower($mapping_function_id);
$mapping_function_id_parts = explode('&', $mapping_function_id);
$part_match_found = false;
foreach ($mapping_function_id_parts as $mapping_function_id_part) {
if (isset($call_map[$mapping_function_id_part][0])) {
if ($call_map[$mapping_function_id_part][0]) {
$mapped_function_return =
Type::parseString($call_map[$mapping_function_id_part][0]);
if ($mapping_return_type) {
$mapping_return_type = Type::combineUnionTypes(
$mapping_return_type,
$mapped_function_return
);
} else {
$mapping_return_type = $mapped_function_return;
}
$part_match_found = true;
}
} else {
if (strpos($mapping_function_id_part, '::') !== false) {
list($callable_fq_class_name) = explode('::', $mapping_function_id_part);
if (in_array($callable_fq_class_name, ['self', 'static', 'parent'], true)) {
continue;
}
if (!$codebase->methods->methodExists(
$mapping_function_id_part,
$context->calling_method_id,
new CodeLocation(
$statements_analyzer->getSource(),
$function_call_arg->value
)
)) {
continue;
}
$part_match_found = true;
$self_class = 'self';
$return_type = $codebase->methods->getMethodReturnType(
$mapping_function_id_part,
$self_class
) ?: Type::getMixed();
if ($mapping_return_type) {
$mapping_return_type = Type::combineUnionTypes(
$mapping_return_type,
$return_type
);
} else {
$mapping_return_type = $return_type;
}
} else {
if (!$codebase->functions->functionExists(
$statements_analyzer,
$mapping_function_id_part
)) {
$mapping_return_type = Type::getMixed();
continue;
}
$part_match_found = true;
$function_storage = $codebase->functions->getStorage(
$statements_analyzer,
$mapping_function_id_part
);
$return_type = $function_storage->return_type ?: Type::getMixed();
if ($mapping_return_type) {
$mapping_return_type = Type::combineUnionTypes(
$mapping_return_type,
$return_type
);
} else {
$mapping_return_type = $return_type;
}
}
}
}
if ($part_match_found === false) {
$mapping_return_type = Type::getMixed();
}
}
if ($mapping_return_type) {
if ($array_arg_type instanceof Type\Atomic\ObjectLike && count($call_args) === 2) {
return new Type\Union([
new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($mapping_return_type) {
return clone $mapping_return_type;
},
$array_arg_type->properties
)
),
]);
}
return new Type\Union([
new Type\Atomic\TArray([
$generic_key_type,
$mapping_return_type,
]),
]);
}
}
}
return Type::getArray();
}
/**
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
* @param array $suppressed_issues
*
* @return Type\Union
*/
private static function getArrayReduceReturnType(
StatementsAnalyzer $statements_analyzer,
Context $context,
array $call_args
) {
if (!isset($call_args[0]) || !isset($call_args[1])) {
return Type::getMixed();
}
$codebase = $statements_analyzer->getCodebase();
$array_arg = $call_args[0]->value;
$function_call_arg = $call_args[1]->value;
if (!isset($array_arg->inferredType) || !isset($function_call_arg->inferredType)) {
return Type::getMixed();
}
$array_arg_type = null;
$array_arg_types = $array_arg->inferredType->getTypes();
if (isset($array_arg_types['array'])
&& ($array_arg_types['array'] instanceof Type\Atomic\TArray
|| $array_arg_types['array'] instanceof Type\Atomic\ObjectLike)
) {
$array_arg_type = $array_arg_types['array'];
if ($array_arg_type instanceof Type\Atomic\ObjectLike) {
$array_arg_type = $array_arg_type->getGenericArrayType();
}
}
if (!isset($call_args[2])) {
$reduce_return_type = Type::getNull();
$reduce_return_type->ignore_nullable_issues = true;
} else {
if (!isset($call_args[2]->value->inferredType)) {
return Type::getMixed();
}
$reduce_return_type = $call_args[2]->value->inferredType;
if ($reduce_return_type->hasMixed()) {
return Type::getMixed();
}
}
$initial_type = $reduce_return_type;
if (($first_arg_atomic_types = $function_call_arg->inferredType->getTypes())
&& ($closure_atomic_type = isset($first_arg_atomic_types['Closure'])
? $first_arg_atomic_types['Closure']
: null)
&& $closure_atomic_type instanceof Type\Atomic\Fn
) {
$closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed();
if ($closure_return_type->isVoid()) {
$closure_return_type = Type::getNull();
}
$reduce_return_type = Type::combineUnionTypes($closure_return_type, $reduce_return_type);
if ($closure_atomic_type->params !== null) {
if (count($closure_atomic_type->params) < 2) {
if (IssueBuffer::accepts(
new InvalidArgument(
'The closure passed to array_reduce needs two params',
new CodeLocation($statements_analyzer->getSource(), $function_call_arg)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return Type::getMixed();
}
$carry_param = $closure_atomic_type->params[0];
$item_param = $closure_atomic_type->params[1];
if ($carry_param->type
&& (!TypeAnalyzer::isContainedBy(
$codebase,
$initial_type,
$carry_param->type
) || (!$reduce_return_type->hasMixed()
&& !TypeAnalyzer::isContainedBy(
$codebase,
$reduce_return_type,
$carry_param->type
)
)
)
) {
if (IssueBuffer::accepts(
new InvalidArgument(
'The first param of the closure passed to array_reduce must take '
. $reduce_return_type . ' but only accepts ' . $carry_param->type,
$carry_param->type_location
?: new CodeLocation($statements_analyzer->getSource(), $function_call_arg)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return Type::getMixed();
}
if ($item_param->type
&& $array_arg_type
&& !$array_arg_type->type_params[1]->hasMixed()
&& !TypeAnalyzer::isContainedBy(
$codebase,
$array_arg_type->type_params[1],
$item_param->type
)
) {
if (IssueBuffer::accepts(
new InvalidArgument(
'The second param of the closure passed to array_reduce must take '
. $array_arg_type->type_params[1] . ' but only accepts ' . $item_param->type,
$item_param->type_location
?: new CodeLocation($statements_analyzer->getSource(), $function_call_arg)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return Type::getMixed();
}
}
return $reduce_return_type;
}
if ($function_call_arg instanceof PhpParser\Node\Scalar\String_
|| $function_call_arg instanceof PhpParser\Node\Expr\Array_
|| $function_call_arg instanceof PhpParser\Node\Expr\BinaryOp\Concat
) {
$mapping_function_ids = Statements\Expression\CallAnalyzer::getFunctionIdsFromCallableArg(
$statements_analyzer,
$function_call_arg
);
$call_map = CallMap::getCallMap();
foreach ($mapping_function_ids as $mapping_function_id) {
$mapping_function_id = strtolower($mapping_function_id);
$mapping_function_id_parts = explode('&', $mapping_function_id);
$part_match_found = false;
foreach ($mapping_function_id_parts as $mapping_function_id_part) {
if (isset($call_map[$mapping_function_id_part][0])) {
if ($call_map[$mapping_function_id_part][0]) {
$mapped_function_return =
Type::parseString($call_map[$mapping_function_id_part][0]);
$reduce_return_type = Type::combineUnionTypes(
$reduce_return_type,
$mapped_function_return
);
$part_match_found = true;
}
} else {
if (strpos($mapping_function_id_part, '::') !== false) {
list($callable_fq_class_name) = explode('::', $mapping_function_id_part);
if (in_array($callable_fq_class_name, ['self', 'static', 'parent'], true)) {
continue;
}
if (!$codebase->methods->methodExists(
$mapping_function_id_part,
$context->calling_method_id,
new CodeLocation(
$statements_analyzer->getSource(),
$function_call_arg
)
)) {
continue;
}
$part_match_found = true;
$self_class = 'self';
$return_type = $codebase->methods->getMethodReturnType(
$mapping_function_id_part,
$self_class
) ?: Type::getMixed();
$reduce_return_type = Type::combineUnionTypes(
$reduce_return_type,
$return_type
);
} else {
if (!$codebase->functions->functionExists(
$statements_analyzer,
$mapping_function_id_part
)) {
return Type::getMixed();
}
$part_match_found = true;
$function_storage = $codebase->functions->getStorage(
$statements_analyzer,
$mapping_function_id_part
);
$return_type = $function_storage->return_type ?: Type::getMixed();
$reduce_return_type = Type::combineUnionTypes(
$reduce_return_type,
$return_type
);
}
}
}
if ($part_match_found === false) {
return Type::getMixed();
}
}
return $reduce_return_type;
}
return Type::getMixed();
}
/**
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
* @param array $suppressed_issues
*
* @return Type\Union
*/
private static function getArrayFilterReturnType(
StatementsAnalyzer $statements_analyzer,
$call_args,
CodeLocation $code_location,
array $suppressed_issues
) {
$array_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
$first_arg_array = $array_arg
&& isset($array_arg->inferredType)
&& $array_arg->inferredType->hasType('array')
&& ($array_atomic_type = $array_arg->inferredType->getTypes()['array'])
&& ($array_atomic_type instanceof Type\Atomic\TArray ||
$array_atomic_type instanceof Type\Atomic\ObjectLike)
? $array_atomic_type
: null;
if (!$first_arg_array) {
return Type::getArray();
}
if ($first_arg_array instanceof Type\Atomic\TArray) {
$inner_type = $first_arg_array->type_params[1];
$key_type = clone $first_arg_array->type_params[0];
} else {
$inner_type = $first_arg_array->getGenericValueType();
$key_type = $first_arg_array->getGenericKeyType();
}
if (!isset($call_args[1])) {
$inner_type->removeType('null');
$inner_type->removeType('false');
} elseif (!isset($call_args[2])) {
$function_call_arg = $call_args[1];
if ($function_call_arg->value instanceof PhpParser\Node\Expr\Closure
&& isset($function_call_arg->value->inferredType)
&& ($closure_atomic_type = $function_call_arg->value->inferredType->getTypes()['Closure'])
&& $closure_atomic_type instanceof Type\Atomic\Fn
) {
$closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed();
if ($closure_return_type->isVoid()) {
IssueBuffer::accepts(
new InvalidReturnType(
'No return type could be found in the closure passed to array_filter',
$code_location
),
$suppressed_issues
);
return Type::getArray();
}
if (count($function_call_arg->value->stmts) === 1 && count($function_call_arg->value->params)) {
$first_param = $function_call_arg->value->params[0];
$stmt = $function_call_arg->value->stmts[0];
if ($first_param->variadic === false
&& $first_param->var instanceof PhpParser\Node\Expr\Variable
&& is_string($first_param->var->name)
&& $stmt instanceof PhpParser\Node\Stmt\Return_
&& $stmt->expr
) {
$codebase = $statements_analyzer->getCodebase();
AssertionFinder::scrapeAssertions($stmt->expr, null, $statements_analyzer, $codebase);
$assertions = isset($stmt->expr->assertions) ? $stmt->expr->assertions : null;
if (isset($assertions['$' . $first_param->var->name])) {
$changed_var_ids = [];
$reconciled_types = Reconciler::reconcileKeyedTypes(
['$inner_type' => $assertions['$' . $first_param->var->name]],
['$inner_type' => $inner_type],
$changed_var_ids,
['$inner_type' => true],
$statements_analyzer,
false,
new CodeLocation($statements_analyzer->getSource(), $stmt)
);
if (isset($reconciled_types['$inner_type'])) {
$inner_type = $reconciled_types['$inner_type'];
}
}
}
}
}
return new Type\Union([
new Type\Atomic\TArray([
$key_type,
$inner_type,
]),
]);
}
return new Type\Union([
new Type\Atomic\TArray([
$key_type,
$inner_type,
]),
]);
}
}