1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-14 02:07:37 +01:00
psalm/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php

474 lines
18 KiB
PHP
Raw Normal View History

2016-01-08 00:28:27 +01:00
<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer;
2016-01-08 00:28:27 +01:00
2016-02-04 15:22:46 +01:00
use PhpParser;
use Psalm\Internal\Codebase\CallMap;
use Psalm\Context;
2016-08-14 05:26:45 +02:00
use Psalm\Type;
use function strtolower;
use function array_values;
use function count;
2016-01-08 00:28:27 +01:00
/**
* @internal
*/
2018-11-06 03:57:36 +01:00
class FunctionAnalyzer extends FunctionLikeAnalyzer
2016-01-08 00:28:27 +01:00
{
2018-11-06 03:57:36 +01:00
public function __construct(PhpParser\Node\Stmt\Function_ $function, SourceAnalyzer $source)
2016-05-16 22:12:02 +02:00
{
$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);
2016-01-08 00:28:27 +01:00
}
2016-11-01 05:39:41 +01:00
/**
* @param string $function_id
* @param array<PhpParser\Node\Arg> $call_args
2017-05-27 02:16:18 +02:00
*
2016-11-01 05:39:41 +01:00
* @return Type\Union
*/
public static function getReturnTypeFromCallMapWithArgs(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
2016-11-01 05:39:41 +01:00
$function_id,
array $call_args,
Context $context
2016-11-01 05:39:41 +01:00
) {
$call_map_key = strtolower($function_id);
2016-08-22 21:00:12 +02:00
2018-02-04 00:52:35 +01:00
$call_map = CallMap::getCallMap();
2016-08-30 06:05:13 +02:00
$codebase = $statements_analyzer->getCodebase();
2016-10-22 23:35:59 +02:00
if (!isset($call_map[$call_map_key])) {
throw new \InvalidArgumentException('Function ' . $function_id . ' was not found in callmap');
}
2018-07-08 02:35:24 +02:00
if (!$call_args) {
switch ($call_map_key) {
case 'getenv':
return new Type\Union([new Type\Atomic\TArray([Type::getArrayKey(), Type::getString()])]);
2018-07-08 02:35:24 +02:00
case 'gettimeofday':
return new Type\Union([
new Type\Atomic\TArray([
Type::getString(),
Type::getInt()
])
]);
2018-10-10 22:03:00 +02:00
case 'microtime':
return Type::getString();
2019-04-03 23:08:37 +02:00
case 'hrtime':
return new Type\Union([
new Type\Atomic\ObjectLike([
Type::getInt(),
Type::getInt()
])
]);
case 'get_called_class':
return new Type\Union([new Type\Atomic\TClassString($context->self ?: 'object')]);
case 'get_parent_class':
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]
)
]);
}
}
2018-07-08 02:35:24 +02:00
}
} else {
2018-05-24 03:17:14 +02:00
switch ($call_map_key) {
case 'pathinfo':
if (isset($call_args[1])) {
return Type::getString();
}
2016-11-01 05:39:41 +01:00
2018-05-24 03:17:14 +02:00
return Type::getArray();
2016-11-01 05:39:41 +01:00
2018-05-24 03:17:14 +02:00
case 'count':
if (isset($call_args[0]->value->inferredType)) {
$atomic_types = $call_args[0]->value->inferredType->getTypes();
if (count($atomic_types) === 1) {
if (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\TNonEmptyList) {
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))
]);
}
} elseif (isset($atomic_types['callable-array'])
|| isset($atomic_types['callable-list'])
) {
return Type::getInt(false, 2);
2018-05-24 03:17:14 +02:00
}
}
}
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([
2018-05-24 03:17:14 +02:00
new Type\Atomic\TString,
$call_map_key === 'var_export' ? new Type\Atomic\TNull : new Type\Atomic\TBool
]);
}
2018-05-24 03:17:14 +02:00
return $call_map_key === 'var_export' ? Type::getVoid() : Type::getBool();
2018-04-03 04:19:58 +02:00
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();
}
2018-10-10 22:03:00 +02:00
}
2018-10-10 22:03:00 +02:00
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();
}
}
2018-10-10 22:03:00 +02:00
return new Type\Union([
new Type\Atomic\TFloat,
new Type\Atomic\TString
]);
2019-04-03 23:08:37 +02:00
case 'hrtime':
if (isset($call_args[0]->value->inferredType)) {
$subject_type = $call_args[0]->value->inferredType;
if ((string) $subject_type === 'true') {
$int = Type::getInt();
$int->from_calculation = true;
return $int;
}
if ((string) $subject_type === 'false') {
return new Type\Union([
new Type\Atomic\ObjectLike([
Type::getInt(),
Type::getInt()
])
]);
}
return new Type\Union([
new Type\Atomic\ObjectLike([
Type::getInt(),
Type::getInt()
]),
new Type\Atomic\TInt()
]);
}
$int = Type::getInt();
$int->from_calculation = true;
return $int;
2018-07-08 02:35:24 +02:00
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;
2018-05-24 03:17:14 +02:00
case 'explode':
if (count($call_args) >= 2) {
$can_return_empty = isset($call_args[2])
&& (
!$call_args[2]->value instanceof PhpParser\Node\Scalar\LNumber
|| $call_args[2]->value->value < 0
);
if ($call_args[0]->value instanceof PhpParser\Node\Scalar\String_) {
if ($call_args[0]->value->value === '') {
return Type::getFalse();
}
2016-08-30 06:05:13 +02:00
return new Type\Union([
$can_return_empty
? new Type\Atomic\TList(Type::getString())
: new Type\Atomic\TNonEmptyList(Type::getString())
]);
} elseif (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->hasString()
) {
$falsable_array = new Type\Union([
$can_return_empty
? new Type\Atomic\TList(Type::getString())
: new Type\Atomic\TNonEmptyList(Type::getString()),
new Type\Atomic\TFalse
]);
if ($codebase->config->ignore_internal_falsable_issues) {
$falsable_array->ignore_falsable_issues = true;
}
return $falsable_array;
}
2018-05-24 03:17:14 +02:00
}
2018-04-16 22:03:04 +02:00
2018-05-24 03:17:14 +02:00
break;
2018-05-24 03:17:14 +02:00
case 'abs':
if (isset($call_args[0]->value)) {
$first_arg = $call_args[0]->value;
2018-05-24 03:17:14 +02:00
if (isset($first_arg->inferredType)) {
$numeric_types = [];
foreach ($first_arg->inferredType->getTypes() as $inner_type) {
if ($inner_type->isNumericType()) {
$numeric_types[] = $inner_type;
}
}
2018-05-24 03:17:14 +02:00
if ($numeric_types) {
return new Type\Union($numeric_types);
}
}
}
2018-05-24 03:17:14 +02:00
break;
2018-05-24 03:17:14 +02:00
case 'min':
case 'max':
if (isset($call_args[0])) {
$first_arg = $call_args[0]->value;
2018-05-24 03:17:14 +02:00
if (isset($first_arg->inferredType)) {
if ($first_arg->inferredType->hasArray()) {
2019-11-11 16:11:42 +01:00
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
2018-05-24 03:17:14 +02:00
$array_type = $first_arg->inferredType->getTypes()['array'];
if ($array_type instanceof Type\Atomic\ObjectLike) {
return $array_type->getGenericValueType();
}
2018-05-24 03:17:14 +02:00
if ($array_type instanceof Type\Atomic\TArray) {
return clone $array_type->type_params[1];
}
if ($array_type instanceof Type\Atomic\TList) {
return clone $array_type->type_param;
}
2018-05-24 03:17:14 +02:00
} elseif ($first_arg->inferredType->hasScalarType() &&
isset($call_args[1]) &&
2018-05-24 03:17:14 +02:00
($second_arg = $call_args[1]->value) &&
isset($second_arg->inferredType) &&
$second_arg->inferredType->hasScalarType()
) {
return Type::combineUnionTypes($first_arg->inferredType, $second_arg->inferredType);
}
}
}
2018-05-24 03:17:14 +02:00
break;
2018-10-23 20:38:36 +02:00
2019-01-28 17:57:49 +01:00
case 'round':
if (isset($call_args[1])) {
$second_arg = $call_args[1]->value;
if (isset($second_arg->inferredType)
&& $second_arg->inferredType->isSingleIntLiteral()
) {
switch ($second_arg->inferredType->getSingleIntLiteral()->value) {
case 0:
return Type::getInt(true);
default:
return Type::getFloat();
}
}
return new Type\Union([new Type\Atomic\TInt, new Type\Atomic\TFloat]);
}
return Type::getInt(true);
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;
}
2016-11-02 14:24:36 +01:00
}
2016-11-02 14:24:36 +01:00
if (!$call_map[$call_map_key][0]) {
return Type::getMixed();
}
$call_map_return_type = Type::parseString($call_map[$call_map_key][0]);
2018-05-24 03:17:14 +02:00
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':
2018-05-24 03:17:14 +02:00
break;
default:
if ($call_map_return_type->isFalsable()
&& $codebase->config->ignore_internal_falsable_issues
) {
2018-05-24 03:17:14 +02:00
$call_map_return_type->ignore_falsable_issues = true;
}
}
return $call_map_return_type;
2016-11-02 14:24:36 +01:00
}
/**
* @param array<PhpParser\Node\Arg> $call_args
*/
public static function taintBuiltinFunctionReturn(
StatementsAnalyzer $statements_analyzer,
string $function_id,
array $call_args,
Type\Union $return_type
) : void {
$codebase = $statements_analyzer->getCodebase();
if (!$codebase->taint) {
return;
}
switch ($function_id) {
case 'htmlspecialchars':
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->tainted
) {
// input is now safe from tainted sql and html
$return_type->tainted = $call_args[0]->value->inferredType->tainted
& ~(Type\Union::TAINTED_INPUT_SQL | Type\Union::TAINTED_INPUT_HTML);
$return_type->sources = $call_args[0]->value->inferredType->sources;
}
break;
case 'strtolower':
case 'strtoupper':
case 'sprintf':
case 'preg_quote':
2019-08-13 14:07:16 +02:00
case 'substr':
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->tainted
) {
$return_type->tainted = $call_args[0]->value->inferredType->tainted;
$return_type->sources = $call_args[0]->value->inferredType->sources;
}
break;
2019-08-13 14:07:16 +02:00
case 'str_replace':
case 'preg_replace':
$first_arg_taint = $call_args[0]->value->inferredType->tainted ?? 0;
$third_arg_taint = $call_args[2]->value->inferredType->tainted ?? 0;
if ($first_arg_taint || $third_arg_taint) {
$return_type->tainted = $first_arg_taint | $third_arg_taint;
$return_type->sources = $call_args[0]->value->inferredType->sources;
}
break;
case 'htmlentities':
case 'striptags':
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->tainted
) {
// input is now safe from tainted html
$return_type->tainted = $call_args[0]->value->inferredType->tainted
& ~Type\Union::TAINTED_INPUT_HTML;
$return_type->sources = $call_args[0]->value->inferredType->sources;
}
break;
}
}
2016-01-08 00:28:27 +01:00
}