1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-03 18:17:55 +01:00
psalm/tests/Internal/Codebase/InternalCallMapHandlerTest.php

511 lines
16 KiB
PHP
Raw Normal View History

<?php
2021-09-23 19:29:15 +02:00
namespace Psalm\Tests\Internal\Codebase;
2022-06-22 12:59:47 +02:00
use InvalidArgumentException;
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
use PHPUnit\Framework\Error\Warning;
use PHPUnit\Framework\ExpectationFailedException;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Codebase\InternalCallMapHandler;
use Psalm\Internal\Codebase\Reflection;
use Psalm\Internal\Provider\FakeFileProvider;
use Psalm\Internal\Provider\Providers;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
2021-12-03 20:11:20 +01:00
use Psalm\Tests\TestCase;
use Psalm\Tests\TestConfig;
use Psalm\Type;
use ReflectionFunction;
use ReflectionParameter;
use ReflectionType;
2022-06-15 13:28:09 +02:00
use Throwable;
2022-06-22 12:59:47 +02:00
use function Amp\call;
2022-06-15 13:28:09 +02:00
use function count;
use function explode;
use function function_exists;
use function implode;
use function in_array;
use function json_encode;
use function preg_match;
use function print_r;
use function strncmp;
use function strpos;
use function substr;
2021-12-03 20:11:20 +01:00
class InternalCallMapHandlerTest extends TestCase
{
2022-06-15 13:16:24 +02:00
/**
* @var string[]
*/
private static $ignoredFunctions = [
'array_column',
2022-06-22 12:59:47 +02:00
'array_diff',
'array_diff_assoc',
'array_diff_key',
2022-06-22 12:59:47 +02:00
'array_intersect',
'array_intersect_assoc',
2022-06-22 12:59:47 +02:00
'array_intersect_key',
'array_key_exists',
2022-06-22 12:59:47 +02:00
'array_merge',
'array_merge_recursive',
'array_multisort',
2022-06-22 12:59:47 +02:00
'array_push',
'array_replace',
'array_replace_recursive',
'array_unshift',
'bcdiv',
2022-06-22 12:59:47 +02:00
'bcmod',
'bcpowmod',
'bzdecompress',
'count',
'crypt',
'date_isodate_set',
2022-06-22 12:59:47 +02:00
'datefmt_create',
'datefmt_get_timezone',
'datefmt_localtime',
'datefmt_parse',
'datefmt_set_timezone',
'debug_zval_dump',
'deflate_add',
'dns_get_mx',
'easter_date',
'enum_exists',
'extract',
'filter_var',
'filter_var_array',
'fputcsv',
'get_class_methods',
'get_headers',
'get_parent_class',
'hash_hmac_file',
'igbinary_unserialize',
'imagefilter',
'imagegd',
'imagegd2',
'imageinterlace',
'imageopenpolygon',
'imagepolygon',
'imagerotate',
'imagesetinterpolation',
'imagettfbbox',
2022-06-22 12:59:47 +02:00
'imagettftext',
'imagexbm',
'imap_delete',
'imap_open',
'imap_rfc822_write_address',
'imap_sort',
2022-06-22 12:59:47 +02:00
'imap_undelete',
'inflate_add',
'inflate_get_read_len',
'inflate_get_status',
'inotify_rm_watch',
'intlcal_from_date_time', 'imagefilledpolygon',
'intlcal_get_weekend_transition',
2022-06-22 12:59:47 +02:00
'intlgregcal_create_instance',
'intlgregcal_is_leap_year',
'intltz_create_enumeration',
'intltz_get_canonical_id',
'intltz_get_display_name',
'long2ip',
'lzf_compress',
'lzf_decompress',
'mail',
'mongodb\bson\tophp',
'msg_receive',
'msg_remove_queue',
'msg_send',
'msg_set_queue',
'msg_stat_queue',
'msg_stat_queue',
'mysqli_poll',
'mysqli_real_connect',
'mysqli_stmt_bind_param',
'normalizer_get_raw_decomposition',
'openssl_pkcs7_read',
'pg_exec',
'pg_exec',
'pg_fetch_all',
'pg_get_notify',
'pg_get_result',
'pg_pconnect',
'pg_select',
'pg_send_execute',
'preg_filter',
'preg_replace_callback_array',
'sapi_windows_cp_get',
'sem_acquire',
'sem_get',
'sem_release',
'sem_remove',
'shm_detach',
'shm_get_var',
'shm_has_var',
'shm_put_var',
'shm_put_var',
'shm_remove',
'shm_remove_var',
'shmop_close',
'shmop_delete',
'shmop_read',
'shmop_size',
'shmop_write',
'snmpset',
2022-06-22 12:59:47 +02:00
'socket_addrinfo_lookup',
'socket_bind',
'socket_cmsg_space',
'socket_connect',
'socket_create_pair',
'socket_get_option',
'socket_getopt',
'socket_getpeername',
'socket_getsockname',
'socket_read',
2022-06-22 12:59:47 +02:00
'socket_recv',
'socket_recvfrom',
'socket_recvmsg',
'socket_select',
2022-06-22 12:59:47 +02:00
'socket_send',
'socket_sendmsg',
'socket_sendto',
'socket_set_blocking',
'socket_set_option',
'socket_setopt',
'socket_shutdown',
'socket_strerror',
'sodium_crypto_generichash',
'sodium_crypto_generichash_final',
'sodium_crypto_generichash_init',
'sodium_crypto_generichash_update',
'sodium_crypto_kx_client_session_keys',
'sodium_crypto_secretstream_xchacha20poly1305_rekey',
'substr_replace',
'zip_entry_close',
'zlib_encode',
2022-06-22 12:59:47 +02:00
2022-06-14 16:58:49 +02:00
2022-06-14 15:16:13 +02:00
];
2022-06-14 15:43:13 +02:00
2022-06-15 13:16:24 +02:00
/**
* @var string[]
*/
private static $ignoredPrefixes = [
'apcu_',
'bc',
'collator_',
'ctype_',
'datefmt_',
'enchant_',
'gmp_',
'gnupg_',
'image',
'imap_',
'inflate_',
'intl',
'ldap_',
'mailparse_',
'memcache_',
'msg_',
'mysqli_',
'normalizer_',
'oauth_',
'oci',
'odbc_',
'openssl_',
'pg_',
'sem_',
'shm_',
'shmop_',
'snmp_',
'socket_',
'sodium_',
'sqlsrv_',
'tidy_',
'transliterator_',
'uopz_',
'xdiff_',
'xmlrpc_server',
'yaml_',
];
/**
* Initialized in setup
* @var string Regex
*/
private static $prefixRegex = '//';
/**
*
* @var bool whether to skip params for which no definition can be found in the callMap
*/
2022-06-15 09:11:14 +02:00
private $skipUndefinedParams = false;
2022-06-15 13:16:24 +02:00
/**
*
* @var Codebase
*/
private static $codebase;
2022-06-15 13:13:24 +02:00
public static function setUpBeforeClass(): void
{
self::$prefixRegex = '/^(' . implode('|', self::$ignoredPrefixes) . ')/';
$project_analyzer = new ProjectAnalyzer(
new TestConfig(),
new Providers(
new FakeFileProvider(),
new FakeParserCacheProvider()
)
);
self::$codebase = $project_analyzer->getCodebase();
}
public static function tearDownAfterClass(): void
{
self::$codebase = null;
}
2022-06-14 15:16:13 +02:00
/**
2022-01-11 16:45:29 +01:00
* @covers \Psalm\Internal\Codebase\InternalCallMapHandler::getCallMap
*/
public function testGetcallmapReturnsAValidCallmap(): void
{
$callMap = InternalCallMapHandler::getCallMap();
self::assertArrayKeysAreStrings($callMap, "Returned CallMap has non-string keys");
self::assertArrayValuesAreArrays($callMap, "Returned CallMap has non-array values");
foreach ($callMap as $function => $signature) {
self::assertArrayKeysAreZeroOrString($signature, "Function " . $function . " in returned CallMap has invalid keys");
self::assertArrayValuesAreStrings($signature, "Function " . $function . " in returned CallMap has non-string values");
foreach ($signature as $type) {
self::assertStringIsParsableType($type, "Function " . $function . " in returned CallMap contains invalid type declaration " . $type);
}
}
}
/**
*
* @return iterable<string, array{0: callable-string, 1: array<int|string>}>
*/
public function callMapEntryProvider(): iterable
{
2022-06-15 14:46:50 +02:00
/**
* This call is needed since InternalCallMapHandler uses the singleton that is initialized by it.
**/
new ProjectAnalyzer(
new TestConfig(),
new Providers(
new FakeFileProvider(),
new FakeParserCacheProvider()
)
);
$callMap = InternalCallMapHandler::getCallMap();
2022-06-15 13:28:09 +02:00
foreach ($callMap as $function => $entry) {
// Skip class methods
2022-06-15 09:11:14 +02:00
if (strpos($function, '::') !== false || !function_exists($function)) {
continue;
}
// Skip functions with alternate signatures
2022-06-14 15:16:13 +02:00
if (isset($callMap["$function'1"]) || preg_match("/\'\d$/", $function)) {
continue;
}
// if ($function != 'fprintf') continue;
yield "$function: " . json_encode($entry) => [$function, $entry];
}
}
2022-06-22 12:59:47 +02:00
/**
* @return bool
*/
private function isIgnored(string $functionName)
{
/** @psalm-assert callable-string $functionName */
if (in_array($functionName, self::$ignoredFunctions)) {
return true;
}
// if (preg_match(self::$prefixRegex, $functionName)) {
// return true;
// $this->markTestSkipped("Function $functionName has ignored prefix");
// }
return false;
}
/**
* @depends testGetcallmapReturnsAValidCallmap
2022-06-22 12:59:47 +02:00
* @dataProvider callMapEntryProvider
* @coversNothing
* @psalm-param callable-string $functionName
2022-06-22 12:59:47 +02:00
*/
public function testIgnoredFunctionsStillFail(string $functionName, array $callMapEntry): void
{
if (!$this->isIgnored($functionName)) {
// Dummy assertion to mark it as passed
$this->assertTrue(true);
return;
}
$this->expectException(ExpectationFailedException::class);
try {
unset($callMapEntry[0]);
/** @var array<string, string> $callMapEntry */
$this->assertEntryIsCorrect($callMapEntry, $functionName);
} catch(\InvalidArgumentException $t) {
// Silence this one for now.
$this->markTestSkipped('IA');
} catch(\PHPUnit\Framework\SkippedTestError $t) {
die('this should not happen');
} catch(ExpectationFailedException $e) {
// This is good!
throw $e;
} catch(InvalidArgumentException $e) {
// This can happen if a class does not exist, we handle the message to check for this case.
if (preg_match('/^Could not get class storage for (.*)$/', $e->getMessage(), $matches)
&& !class_exists($matches[1])
) {
die("Class mentioned in callmap does not exist: " . $matches[1]);
}
}
$this->markTestIncomplete("Remove function '{$functionName}' from your ignores");
// die("Function $functionName did not show error incallmap") ;
}
/**
* This function will test functions that are in the callmap AND currently defined
* @coversNothing
* @depends testGetcallmapReturnsAValidCallmap
* @dataProvider callMapEntryProvider
* @psalm-param callable-string $functionName
2022-06-15 15:05:06 +02:00
* @param array $callMapEntry
*/
public function testCallMapCompliesWithReflection(string $functionName, array $callMapEntry): void
{
2022-06-22 12:59:47 +02:00
if ($this->isIgnored($functionName)) {
$this->markTestSkipped("Function $functionName is ignored in config");
}
2022-06-15 15:05:06 +02:00
unset($callMapEntry[0]);
/** @var array<string, string> $callMapEntry */
$this->assertEntryIsCorrect($callMapEntry, $functionName);
}
2022-06-15 14:46:50 +02:00
/**
*
2022-06-15 15:05:06 +02:00
* @param array<string, string> $callMapEntryWithoutReturn
* @psalm-param callable-string $functionName
2022-06-15 14:46:50 +02:00
*/
2022-06-15 15:05:06 +02:00
private function assertEntryIsCorrect(array $callMapEntryWithoutReturn, string $functionName): void
{
$rF = new ReflectionFunction($functionName);
/**
* Parse the parameter names from the map.
2022-06-15 15:05:06 +02:00
* @var array<string, array{byRef: bool, refMode: 'rw'|'w', variadic: bool, optional: bool, type: string}>
*/
$normalizedEntries = [];
2022-06-15 15:05:06 +02:00
foreach ($callMapEntryWithoutReturn as $key => $entry) {
$normalizedKey = $key;
2022-06-15 15:05:06 +02:00
/**
*
* @var array{byRef: bool, refMode: 'rw'|'w', variadic: bool, optional: bool, type: string} $normalizedEntry
*/
$normalizedEntry = [
'variadic' => false,
'byRef' => false,
'optional' => false,
'type' => $entry,
];
if (strncmp($normalizedKey, '&', 1) === 0) {
$normalizedEntry['byRef'] = true;
$normalizedKey = substr($normalizedKey, 1);
}
if (strncmp($normalizedKey, '...', 3) === 0) {
$normalizedEntry['variadic'] = true;
$normalizedKey = substr($normalizedKey, 3);
}
// Read the reference mode
if ($normalizedEntry['byRef']) {
$parts = explode('_', $normalizedKey, 2);
if (count($parts) === 2) {
$normalizedEntry['refMode'] = $parts[0];
$normalizedKey = $parts[1];
} else {
$normalizedEntry['refMode'] = 'rw';
}
}
// Strip prefixes.
if (substr($normalizedKey, -1, 1) === "=") {
$normalizedEntry['optional'] = true;
$normalizedKey = substr($normalizedKey, 0, -1);
}
$normalizedEntry['name'] = $normalizedKey;
$normalizedEntries[$normalizedKey] = $normalizedEntry;
}
2022-06-15 13:28:09 +02:00
foreach ($rF->getParameters() as $parameter) {
if ($this->skipUndefinedParams && !isset($normalizedEntries[$parameter->getName()])) {
continue;
2022-06-15 09:11:14 +02:00
} else {
$this->assertArrayHasKey($parameter->getName(), $normalizedEntries, "Callmap is missing entry for param {$parameter->getName()} in $functionName: " . print_r($normalizedEntries, true));
}
2022-06-15 15:05:06 +02:00
$this->assertParameter($normalizedEntries[$parameter->getName()], $parameter);
}
}
/**
*
* @param array{byRef: bool, refMode: 'rw'|'w', variadic: bool, optional: bool, type: string} $normalizedEntry
*/
2022-06-15 15:05:06 +02:00
private function assertParameter(array $normalizedEntry, ReflectionParameter $param): void
{
$name = $param->getName();
2022-06-22 12:59:47 +02:00
$this->assertSame($param->isOptional(), $normalizedEntry['optional'], "Expected param '{$name}' to " . ($param->isOptional() ? "be" : "not be") . " optional");
$this->assertSame($param->isVariadic(), $normalizedEntry['variadic'], "Expected param '{$name}' to " . ($param->isVariadic() ? "be" : "not be") . " variadic");
$this->assertSame($param->isPassedByReference(), $normalizedEntry['byRef'], "Expected param '{$name}' to " . ($param->isPassedByReference() ? "be" : "not be") . " by reference");
$expectedType = $param->getType();
2022-06-14 15:16:13 +02:00
if (isset($expectedType) && !empty($normalizedEntry['type'])) {
$this->assertTypeValidity($expectedType, $normalizedEntry['type'], "Param '{$name}' has incorrect type");
}
}
/**
* Since string equality is too strict, we do some extra checking here
*/
private function assertTypeValidity(ReflectionType $reflected, string $specified, string $message): void
{
$expectedType = Reflection::getPsalmTypeFromReflectionType($reflected);
2022-06-22 12:59:47 +02:00
try {
$parsedType = Type::parseString($specified);
2022-06-22 12:59:47 +02:00
} catch(\Throwable $t) {
die("Failed to parse type: $specified -- $message");
}
2022-06-22 12:59:47 +02:00
try {
$this->assertTrue(UnionTypeComparator::isContainedBy(self::$codebase, $parsedType, $expectedType), $message);
2022-06-22 12:59:47 +02:00
} catch(InvalidArgumentException $e) {
if (preg_match('/^Could not get class storage for (.*)$/', $e->getMessage(), $matches)
&& !class_exists($matches[1])
) {
die("Class mentioned in callmap does not exist: " . $matches[1]);
}
}
}
}