1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-14 18:36:58 +01:00
psalm/src/Psalm/Internal/Codebase/Functions.php

599 lines
20 KiB
PHP
Raw Normal View History

2018-02-04 00:52:35 +01:00
<?php
namespace Psalm\Internal\Codebase;
2018-02-04 00:52:35 +01:00
2021-12-03 21:40:18 +01:00
use Exception;
2021-12-04 03:37:19 +01:00
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Closure as ClosureNode;
2018-11-06 03:57:36 +01:00
use Psalm\Codebase;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
2021-06-08 04:55:21 +02:00
use Psalm\Internal\MethodIdentifier;
2019-07-05 22:24:00 +02:00
use Psalm\Internal\Provider\FileStorageProvider;
use Psalm\Internal\Provider\FunctionExistenceProvider;
use Psalm\Internal\Provider\FunctionParamsProvider;
use Psalm\Internal\Provider\FunctionReturnTypeProvider;
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
2021-12-03 20:11:20 +01:00
use Psalm\NodeTypeProvider;
2018-02-04 00:52:35 +01:00
use Psalm\StatementsSource;
use Psalm\Storage\FunctionStorage;
2021-06-08 04:55:21 +02:00
use Psalm\Type\Atomic\TNamedObject;
2021-12-03 21:40:18 +01:00
use UnexpectedValueException;
2021-06-08 04:55:21 +02:00
use function array_shift;
2021-12-03 21:07:25 +01:00
use function count;
use function end;
2021-06-08 04:55:21 +02:00
use function explode;
use function implode;
2021-12-03 21:07:25 +01:00
use function in_array;
2021-06-08 04:55:21 +02:00
use function is_bool;
use function rtrim;
2019-07-05 22:24:00 +02:00
use function strpos;
use function strtolower;
use function substr;
2018-02-04 00:52:35 +01:00
/**
* @internal
*/
2018-02-04 00:52:35 +01:00
class Functions
{
/**
* @var FileStorageProvider
*/
private $file_storage_provider;
/**
* @var array<lowercase-string, FunctionStorage>
2018-02-04 00:52:35 +01:00
*/
private static $stubbed_functions;
/** @var FunctionReturnTypeProvider */
public $return_type_provider;
/** @var FunctionExistenceProvider */
public $existence_provider;
/** @var FunctionParamsProvider */
public $params_provider;
2018-02-04 00:52:35 +01:00
/**
* @var Reflection
*/
private $reflection;
public function __construct(FileStorageProvider $storage_provider, Reflection $reflection)
{
$this->file_storage_provider = $storage_provider;
$this->reflection = $reflection;
$this->return_type_provider = new FunctionReturnTypeProvider();
$this->existence_provider = new FunctionExistenceProvider();
$this->params_provider = new FunctionParamsProvider();
2018-02-04 00:52:35 +01:00
self::$stubbed_functions = [];
}
/**
2020-05-15 16:18:05 +02:00
* @param non-empty-lowercase-string $function_id
*/
public function getStorage(
?StatementsAnalyzer $statements_analyzer,
string $function_id,
?string $root_file_path = null,
?string $checked_file_path = null
): FunctionStorage {
if ($function_id[0] === '\\') {
$function_id = substr($function_id, 1);
}
if (isset(self::$stubbed_functions[$function_id])) {
return self::$stubbed_functions[$function_id];
2018-02-04 00:52:35 +01:00
}
$file_storage = null;
if ($statements_analyzer) {
$root_file_path = $statements_analyzer->getRootFilePath();
$checked_file_path = $statements_analyzer->getFilePath();
2018-02-04 00:52:35 +01:00
$file_storage = $this->file_storage_provider->get($root_file_path);
2018-02-04 00:52:35 +01:00
$function_analyzers = $statements_analyzer->getFunctionAnalyzers();
2018-02-04 00:52:35 +01:00
if (isset($function_analyzers[$function_id])) {
$function_id = $function_analyzers[$function_id]->getFunctionId();
2018-02-04 00:52:35 +01:00
if (isset($file_storage->functions[$function_id])) {
return $file_storage->functions[$function_id];
}
}
// closures can be returned here
if (isset($file_storage->functions[$function_id])) {
return $file_storage->functions[$function_id];
2018-02-04 00:52:35 +01:00
}
}
if (!$root_file_path || !$checked_file_path) {
if ($this->reflection->hasFunction($function_id)) {
return $this->reflection->getFunctionStorage($function_id);
}
2021-12-03 21:40:18 +01:00
throw new UnexpectedValueException(
'Expecting non-empty $root_file_path and $checked_file_path'
);
}
if ($this->reflection->hasFunction($function_id)) {
return $this->reflection->getFunctionStorage($function_id);
}
2018-02-04 00:52:35 +01:00
if (!isset($file_storage->declaring_function_ids[$function_id])) {
if ($checked_file_path !== $root_file_path) {
$file_storage = $this->file_storage_provider->get($checked_file_path);
if (isset($file_storage->functions[$function_id])) {
return $file_storage->functions[$function_id];
}
}
2021-12-03 21:40:18 +01:00
throw new UnexpectedValueException(
'Expecting ' . $function_id . ' to have storage in ' . $checked_file_path
2018-02-04 00:52:35 +01:00
);
}
$declaring_file_path = $file_storage->declaring_function_ids[$function_id];
$declaring_file_storage = $this->file_storage_provider->get($declaring_file_path);
if (!isset($declaring_file_storage->functions[$function_id])) {
2021-12-03 21:40:18 +01:00
throw new UnexpectedValueException(
2018-02-04 00:52:35 +01:00
'Not expecting ' . $function_id . ' to not have storage in ' . $declaring_file_path
);
}
return $declaring_file_storage->functions[$function_id];
}
public function addGlobalFunction(string $function_id, FunctionStorage $storage): void
2018-02-04 00:52:35 +01:00
{
self::$stubbed_functions[strtolower($function_id)] = $storage;
2018-02-04 00:52:35 +01:00
}
public function hasStubbedFunction(string $function_id): bool
2018-02-04 00:52:35 +01:00
{
return isset(self::$stubbed_functions[strtolower($function_id)]);
2018-02-04 00:52:35 +01:00
}
/**
* @return array<string, FunctionStorage>
*/
public function getAllStubbedFunctions(): array
{
return self::$stubbed_functions;
}
2018-02-04 00:52:35 +01:00
/**
2020-06-21 17:43:08 +02:00
* @param lowercase-string $function_id
2018-02-04 00:52:35 +01:00
*/
public function functionExists(
StatementsAnalyzer $statements_analyzer,
string $function_id
): bool {
if ($this->existence_provider->has($function_id)) {
$function_exists = $this->existence_provider->doesFunctionExist($statements_analyzer, $function_id);
if ($function_exists !== null) {
return $function_exists;
}
}
2018-11-11 18:01:14 +01:00
$file_storage = $this->file_storage_provider->get($statements_analyzer->getRootFilePath());
2018-02-04 00:52:35 +01:00
if (isset($file_storage->declaring_function_ids[$function_id])) {
return true;
}
if ($this->reflection->hasFunction($function_id)) {
return true;
}
2020-06-21 17:43:08 +02:00
if (isset(self::$stubbed_functions[$function_id])) {
2018-02-04 00:52:35 +01:00
return true;
}
2018-11-11 18:01:14 +01:00
if (isset($statements_analyzer->getFunctionAnalyzers()[$function_id])) {
2018-02-04 00:52:35 +01:00
return true;
}
$predefined_functions = $statements_analyzer->getCodebase()->config->getPredefinedFunctions();
if (isset($predefined_functions[$function_id])) {
/** @psalm-suppress ArgumentTypeCoercion */
if ($this->reflection->registerFunction($function_id) === false) {
return false;
}
return true;
2018-02-04 00:52:35 +01:00
}
return false;
2018-02-04 00:52:35 +01:00
}
/**
2020-05-15 16:18:05 +02:00
* @param non-empty-string $function_name
2018-02-04 00:52:35 +01:00
*
2020-05-15 16:18:05 +02:00
* @return non-empty-string
2018-02-04 00:52:35 +01:00
*/
public function getFullyQualifiedFunctionNameFromString(string $function_name, StatementsSource $source): string
2018-02-04 00:52:35 +01:00
{
if ($function_name[0] === '\\') {
2020-05-15 16:18:05 +02:00
$function_name = substr($function_name, 1);
if ($function_name === '') {
2021-12-03 21:40:18 +01:00
throw new UnexpectedValueException('Malformed function name');
2020-05-15 16:18:05 +02:00
}
return $function_name;
2018-02-04 00:52:35 +01:00
}
$function_name_lcase = strtolower($function_name);
$aliases = $source->getAliases();
$imported_function_namespaces = $aliases->functions;
$imported_namespaces = $aliases->uses;
if (strpos($function_name, '\\') !== false) {
$function_name_parts = explode('\\', $function_name);
$first_namespace = array_shift($function_name_parts);
$first_namespace_lcase = strtolower($first_namespace);
if (isset($imported_namespaces[$first_namespace_lcase])) {
return $imported_namespaces[$first_namespace_lcase] . '\\' . implode('\\', $function_name_parts);
}
if (isset($imported_function_namespaces[$first_namespace_lcase])) {
return $imported_function_namespaces[$first_namespace_lcase] . '\\' .
implode('\\', $function_name_parts);
}
} elseif (isset($imported_function_namespaces[$function_name_lcase])) {
return $imported_function_namespaces[$function_name_lcase];
}
$namespace = $source->getNamespace();
return ($namespace ? $namespace . '\\' : '') . $function_name;
}
/**
* @return array<lowercase-string,FunctionStorage>
*/
public function getMatchingFunctionNames(
string $stub,
int $offset,
string $file_path,
Codebase $codebase
): array {
if ($stub[0] === '*') {
$stub = substr($stub, 1);
}
$fully_qualified = false;
if ($stub[0] === '\\') {
$fully_qualified = true;
$stub = substr($stub, 1);
$stub_namespace = '';
} else {
// functions can reference either the current namespace or root-namespaced
// equivalents. We therefore want to make both candidates.
[$stub_namespace, $stub] = explode('-', $stub);
}
/** @var array<lowercase-string, FunctionStorage> */
$matching_functions = [];
$file_storage = $this->file_storage_provider->get($file_path);
$current_namespace_aliases = null;
foreach ($file_storage->namespace_aliases as $namespace_start => $namespace_aliases) {
if ($namespace_start < $offset) {
$current_namespace_aliases = $namespace_aliases;
break;
}
}
// We will search all functions for several patterns. This will
// be for all used namespaces, the global namespace and matched
// used functions.
$match_function_patterns = [
$stub . '*',
];
if ($stub_namespace) {
$match_function_patterns[] = $stub_namespace . '\\' . $stub . '*';
}
if ($current_namespace_aliases) {
foreach ($current_namespace_aliases->functions as $alias_name => $function_name) {
if (strpos($alias_name, $stub) === 0) {
try {
$match_function_patterns[] = $function_name;
2021-12-03 21:40:18 +01:00
} catch (Exception $e) {
}
}
}
if (!$fully_qualified) {
foreach ($current_namespace_aliases->uses as $namespace_name) {
$match_function_patterns[] = $namespace_name . '\\' . $stub . '*';
}
}
}
$function_map = $file_storage->functions
+ $this->getAllStubbedFunctions()
+ $this->reflection->getFunctions()
+ $codebase->config->getPredefinedFunctions();
foreach ($function_map as $function_name => $function) {
foreach ($match_function_patterns as $pattern) {
2021-12-03 21:07:25 +01:00
$pattern_lc = strtolower($pattern);
if (substr($pattern, -1, 1) === '*') {
if (strpos($function_name, rtrim($pattern_lc, '*')) !== 0) {
continue;
}
} elseif ($function_name !== $pattern) {
continue;
}
if (is_bool($function)) {
/** @var callable-string $function_name */
if ($this->reflection->registerFunction($function_name) === false) {
continue;
}
$function = $this->reflection->getFunctionStorage($function_name);
}
if ($function->cased_name) {
2021-12-03 21:07:25 +01:00
$cased_name_parts = explode('\\', $function->cased_name);
$pattern_parts = explode('\\', $pattern);
2021-12-03 21:07:25 +01:00
if (end($cased_name_parts)[0] !== end($pattern_parts)[0]) {
continue;
}
}
/** @var lowercase-string $function_name */
$matching_functions[$function_name] = $function;
}
}
return $matching_functions;
}
public static function isVariadic(Codebase $codebase, string $function_id, string $file_path): bool
2018-02-04 00:52:35 +01:00
{
2018-11-06 03:57:36 +01:00
$file_storage = $codebase->file_storage_provider->get($file_path);
2018-02-04 00:52:35 +01:00
2018-03-17 20:02:25 +01:00
if (!isset($file_storage->declaring_function_ids[$function_id])) {
2018-03-17 20:28:41 +01:00
return false;
2018-03-17 20:02:25 +01:00
}
$declaring_file_path = $file_storage->declaring_function_ids[$function_id];
$file_storage = $declaring_file_path === $file_path
? $file_storage
2018-11-06 03:57:36 +01:00
: $codebase->file_storage_provider->get($declaring_file_path);
2018-03-17 20:02:25 +01:00
2018-02-04 00:52:35 +01:00
return isset($file_storage->functions[$function_id]) && $file_storage->functions[$function_id]->variadic;
}
2019-05-17 00:36:36 +02:00
/**
2021-12-04 03:37:19 +01:00
* @param ?list<Arg> $args
*/
public function isCallMapFunctionPure(
Codebase $codebase,
2021-12-03 20:11:20 +01:00
?NodeTypeProvider $type_provider,
string $function_id,
?array $args,
bool &$must_use = true
): bool {
$impure_functions = [
// file io
'chdir', 'chgrp', 'chmod', 'chown', 'chroot', 'copy', 'file_get_contents', 'file_put_contents',
2020-07-14 23:14:09 +02:00
'opendir', 'readdir', 'closedir', 'rewinddir', 'scandir',
'fopen', 'fread', 'fwrite', 'fclose', 'touch', 'fpassthru', 'fputs', 'fscanf', 'fseek', 'flock',
'ftruncate', 'fprintf', 'symlink', 'mkdir', 'unlink', 'rename', 'rmdir', 'popen', 'pclose',
'fgetcsv', 'fputcsv', 'umask', 'finfo_open', 'finfo_close', 'finfo_file', 'readline_add_history',
'stream_set_timeout', 'fgets', 'fflush', 'move_uploaded_file', 'file_exists', 'realpath', 'glob',
'is_readable', 'is_dir', 'is_file',
// stream/socket io
'stream_context_set_option', 'socket_write', 'stream_set_blocking', 'socket_close',
'socket_set_option', 'stream_set_write_buffer', 'stream_socket_enable_crypto', 'stream_copy_to_stream',
'stream_wrapper_register',
// meta calls
'call_user_func', 'call_user_func_array', 'define', 'create_function',
// http
'header', 'header_remove', 'http_response_code', 'setcookie',
// output buffer
2021-06-24 15:34:38 +02:00
'ob_start', 'ob_end_clean', 'ob_get_clean', 'readfile', 'printf', 'var_dump', 'phpinfo',
'ob_implicit_flush', 'vprintf',
// mcrypt
'mcrypt_generic_init', 'mcrypt_generic_deinit', 'mcrypt_module_close',
// internal optimisation
'opcache_compile_file', 'clearstatcache',
// process-related
'pcntl_signal', 'pcntl_alarm', 'posix_kill', 'cli_set_process_title', 'pcntl_async_signals', 'proc_close',
2019-10-03 21:01:31 +02:00
'proc_nice', 'proc_open', 'proc_terminate',
// curl
'curl_setopt', 'curl_close', 'curl_multi_add_handle', 'curl_multi_remove_handle',
'curl_multi_select', 'curl_multi_close', 'curl_setopt_array',
// apc, apcu
'apc_store', 'apc_delete', 'apc_clear_cache', 'apc_add', 'apc_inc', 'apc_dec', 'apc_cas',
'apcu_store', 'apcu_delete', 'apcu_clear_cache', 'apcu_add', 'apcu_inc', 'apcu_dec', 'apcu_cas',
// gz
'gzwrite', 'gzrewind', 'gzseek', 'gzclose',
// newrelic
'newrelic_start_transaction', 'newrelic_name_transaction', 'newrelic_add_custom_parameter',
'newrelic_add_custom_tracer', 'newrelic_background_job', 'newrelic_end_transaction',
'newrelic_set_appname',
// execution
'shell_exec', 'exec', 'system', 'passthru', 'pcntl_exec',
// well-known functions
2019-11-13 19:22:04 +01:00
'libxml_use_internal_errors', 'libxml_disable_entity_loader', 'curl_exec',
'mt_srand', 'openssl_pkcs7_sign',
2020-02-05 14:51:03 +01:00
'mt_rand', 'rand', 'random_int', 'random_bytes',
'wincache_ucache_delete', 'wincache_ucache_set', 'wincache_ucache_inc',
2019-11-13 19:22:04 +01:00
'class_alias',
'class_exists', // impure by virtue of triggering autoloader
// php environment
'ini_set', 'sleep', 'usleep', 'register_shutdown_function',
'error_reporting', 'register_tick_function', 'unregister_tick_function',
'set_error_handler', 'user_error', 'trigger_error', 'restore_error_handler',
2019-09-09 16:43:10 +02:00
'date_default_timezone_set', 'assert_options', 'setlocale',
'set_exception_handler', 'set_time_limit', 'putenv', 'spl_autoload_register',
2021-06-22 21:00:05 +02:00
'spl_autoload_unregister', 'microtime', 'array_rand', 'set_include_path',
// logging
'openlog', 'syslog', 'error_log', 'define_syslog_variables',
// session
'session_id', 'session_decode', 'session_name', 'session_set_cookie_params',
'session_set_save_handler', 'session_regenerate_id', 'mb_internal_encoding',
'session_start', 'session_cache_limiter',
// ldap
'ldap_set_option',
// iterators
'rewind', 'iterator_apply', 'iterator_to_array',
2019-10-03 21:01:31 +02:00
// mysqli
'mysqli_select_db', 'mysqli_dump_debug_info', 'mysqli_kill', 'mysqli_multi_query',
'mysqli_next_result', 'mysqli_options', 'mysqli_ping', 'mysqli_query', 'mysqli_report',
'mysqli_rollback', 'mysqli_savepoint', 'mysqli_set_charset', 'mysqli_ssl_set', 'mysqli_close',
// script execution
'ignore_user_abort',
// ftp
2021-10-23 15:17:49 +02:00
'ftp_close', 'ftp_pasv',
// bcmath
'bcscale',
// json
'json_last_error',
// opcache
'opcache_compile_file', 'opcache_get_configuration', 'opcache_get_status',
'opcache_invalidate', 'opcache_is_script_cached', 'opcache_reset',
2021-07-18 14:24:43 +02:00
//gettext
'bindtextdomain',
];
2021-12-03 21:07:25 +01:00
if (in_array(strtolower($function_id), $impure_functions, true)) {
return false;
}
if (strpos($function_id, 'image') === 0) {
return false;
}
2019-09-07 19:01:36 +02:00
if (($function_id === 'var_export' || $function_id === 'print_r') && !isset($args[1])) {
return false;
}
2019-09-09 16:43:10 +02:00
if ($function_id === 'assert') {
$must_use = false;
return true;
}
2020-09-19 19:58:29 +02:00
if ($function_id === 'func_num_args' || $function_id === 'func_get_args') {
return true;
}
2020-06-14 17:06:53 +02:00
if ($function_id === 'count' && isset($args[0]) && $type_provider) {
$count_type = $type_provider->getType($args[0]->value);
if ($count_type) {
foreach ($count_type->getAtomicTypes() as $atomic_count_type) {
if ($atomic_count_type instanceof TNamedObject) {
$count_method_id = new MethodIdentifier(
$atomic_count_type->value,
'count'
);
try {
2021-09-25 02:34:21 +02:00
return $codebase->methods->getStorage($count_method_id)->mutation_free;
2021-12-03 21:40:18 +01:00
} catch (Exception $e) {
2020-06-14 17:06:53 +02:00
// do nothing
}
}
}
}
}
2020-11-04 17:02:34 +01:00
$function_callable = InternalCallMapHandler::getCallableFromCallMapById(
$codebase,
$function_id,
$args ?: [],
null
);
if (!$function_callable->params
2021-12-03 21:07:25 +01:00
|| ($args !== null && count($args) === 0)
|| ($function_callable->return_type && $function_callable->return_type->isVoid())
) {
return false;
}
$must_use = $function_id !== 'array_map'
|| (isset($args[0]) && !$args[0]->value instanceof ClosureNode);
2019-09-08 17:34:16 +02:00
foreach ($function_callable->params as $i => $param) {
if ($type_provider && $param->type && $param->type->hasCallableType() && isset($args[$i])) {
$arg_type = $type_provider->getType($args[$i]->value);
if ($arg_type) {
foreach ($arg_type->getAtomicTypes() as $possible_callable) {
$possible_callable = CallableTypeComparator::getCallableFromAtomic(
$codebase,
$possible_callable
);
if ($possible_callable && !$possible_callable->is_pure) {
return false;
}
}
}
2019-09-08 17:34:16 +02:00
}
if ($param->by_ref && isset($args[$i])) {
$must_use = false;
}
2019-09-08 17:34:16 +02:00
}
return true;
}
public static function clearCache(): void
2019-05-17 00:36:36 +02:00
{
self::$stubbed_functions = [];
}
2018-02-04 00:52:35 +01:00
}