Skip to content

Commit

Permalink
Allow testing expected CallMap return types and ignore functions that…
Browse files Browse the repository at this point in the history
… currently fail
  • Loading branch information
othercorey committed Jun 25, 2022
1 parent d626d24 commit f4df504
Showing 1 changed file with 239 additions and 31 deletions.
270 changes: 239 additions & 31 deletions tests/Internal/Codebase/InternalCallMapHandlerTest.php
Expand Up @@ -2,7 +2,9 @@

namespace Psalm\Tests\Internal\Codebase;

use Exception;
use InvalidArgumentException;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\SkippedTestError;
use Psalm\Codebase;
Expand All @@ -16,6 +18,7 @@
use Psalm\Tests\TestCase;
use Psalm\Tests\TestConfig;
use Psalm\Type;
use ReflectionException;
use ReflectionFunction;
use ReflectionParameter;
use ReflectionType;
Expand Down Expand Up @@ -336,6 +339,186 @@ class InternalCallMapHandlerTest extends TestCase
'zlib_encode',
];

/**
* List of function names to ignore only for return type checks.
*
* @var list<string>
*/
private static $ignoredReturnTypeOnlyFunctions = [
'array_combine',
'bzopen',
'cal_from_jd',
'collator_get_strength',
'curl_copy_handle',
'curl_init',
'curl_multi_errno',
'curl_multi_init',
'curl_share_errno',
'curl_share_init',
'date_add',
'date_date_set',
'date_diff',
'date_format',
'date_offset_get',
'date_parse',
'date_sub',
'date_sun_info',
'date_sunrise',
'date_sunset',
'date_time_set',
'date_timestamp_set',
'date_timezone_set',
'datefmt_set_lenient',
'deflate_init',
'dom_import_simplexml',
'explode',
'fgetcsv',
'filter_input_array',
'finfo_open',
'fopen',
'fpassthru',
'fsockopen',
'ftp_connect',
'ftp_get_option',
'ftp_ssl_connect',
'get_declared_traits',
'gzeof',
'gzopen',
'gzpassthru',
'hash',
'hash_hkdf',
'hash_hmac',
'hash_init',
'iconv_get_encoding',
'imageaffine',
'imagecolorclosest',
'imagecolorclosestalpha',
'imagecolorclosesthwb',
'imagecolorexact',
'imagecolorexactalpha',
'imagecolorresolve',
'imagecolorresolvealpha',
'imagecolorset',
'imagecolorsforindex',
'imagecolorstotal',
'imagecolortransparent',
'imagecreate',
'imagecreatefrombmp',
'imagecreatefromgd',
'imagecreatefromgd2',
'imagecreatefromgd2part',
'imagecreatefromgif',
'imagecreatefromjpeg',
'imagecreatefrompng',
'imagecreatefromstring',
'imagecreatefromwbmp',
'imagecreatefromwebp',
'imagecreatefromxbm',
'imagecreatefromxpm',
'imagecreatetruecolor',
'imagecrop',
'imagecropauto',
'imagegetclip',
'imageloadfont',
'imagescale',
'imagesx',
'imagesy',
'inflate_init',
'intlcal_get',
'intlcal_get_keyword_values_for_locale',
'intlgregcal_set_gregorian_change',
'intltz_get_offset',
'jddayofweek',
'jdtounix',
'mb_encoding_aliases',
'mb_ereg',
'mb_eregi',
'mb_str_split',
'metaphone',
'msg_get_queue',
'mysqli_stmt_get_warnings',
'mysqli_stmt_insert_id',
'numfmt_create',
'ob_list_handlers',
'opendir',
'openssl_csr_get_public_key',
'openssl_csr_new',
'openssl_csr_sign',
'openssl_get_privatekey',
'openssl_get_publickey',
'openssl_pkey_get_private',
'openssl_pkey_get_public',
'openssl_pkey_new',
'openssl_random_pseudo_bytes',
'openssl_spki_export',
'openssl_spki_export_challenge',
'openssl_spki_new',
'openssl_x509_read',
'pack',
'parse_url',
'passthru',
'password_hash',
'pcntl_exec',
'pcntl_signal_get_handler',
'pcntl_strerror',
'pfsockopen',
'pg_connect',
'pg_insert',
'pg_port',
'pg_socket',
'popen',
'proc_get_status',
'proc_open',
'register_shutdown_function',
'rewinddir',
'set_error_handler',
'set_exception_handler',
'shm_attach',
'shmop_open',
'simplexml_import_dom',
'sleep',
'socket_accept',
'socket_addrinfo_bind',
'socket_addrinfo_connect',
'socket_create',
'socket_create_listen',
'socket_export_stream',
'socket_import_stream',
'sodium_crypto_aead_chacha20poly1305_encrypt',
'sodium_crypto_aead_chacha20poly1305_ietf_encrypt',
'sodium_crypto_aead_xchacha20poly1305_ietf_encrypt',
'spl_autoload_functions',
'stream_bucket_new',
'stream_context_create',
'stream_context_get_default',
'stream_context_set_default',
'stream_filter_append',
'stream_filter_prepend',
'stream_set_chunk_size',
'stream_socket_accept',
'stream_socket_client',
'stream_socket_server',
'substr',
'substr_compare',
'timezone_abbreviations_list',
'timezone_identifiers_list',
'timezone_offset_get',
'tmpfile',
'user_error',
'xml_get_current_byte_index',
'xml_get_current_column_number',
'xml_get_current_line_number',
'xml_get_error_code',
'xml_parser_create',
'xml_parser_create_ns',
'xml_parser_get_option',
'xmlwriter_flush',
'xmlwriter_open_memory',
'xmlwriter_open_uri',
'zip_open',
'zip_read',
];

/**
*
* @var Codebase
Expand Down Expand Up @@ -437,6 +620,13 @@ private function isIgnored(string $functionName): bool
return false;
}

/**
*/
private function isReturnTypeOnlyIgnored(string $functionName): bool
{
return in_array($functionName, static::$ignoredReturnTypeOnlyFunctions, true);
}

/**
* @depends testIgnoresAreSortedAndUnique
* @depends testGetcallmapReturnsAValidCallmap
Expand All @@ -446,36 +636,36 @@ private function isIgnored(string $functionName): bool
*/
public function testIgnoredFunctionsStillFail(string $functionName, array $callMapEntry): void
{
if (!$this->isIgnored($functionName)) {
$functionIgnored = $this->isIgnored($functionName);
if (!$functionIgnored && !$this->isReturnTypeOnlyIgnored($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 (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]);
$function = new ReflectionFunction($functionName);
/** @var string $entryReturnType */
$entryReturnType = array_shift($callMapEntry);

if ($functionIgnored) {
try {
/** @var array<string, string> $callMapEntry */
$this->assertEntryParameters($callMapEntry, $function);
$this->assertEntryReturnType($function, $entryReturnType);
} catch (AssertionFailedError|ExpectationFailedException) {
$this->assertTrue(true);
return;
}
$this->markTestIncomplete("Remove '{$functionName}' from InternalCallMapHandlerTest::\$ignoredFunctions");
}

$this->markTestIncomplete("Remove function '{$functionName}' from your ignores");
try {
$this->assertEntryReturnType($function, $entryReturnType);
} catch (AssertionFailedError|ExpectationFailedException) {
$this->assertTrue(true);
return;
}
$this->markTestIncomplete("Remove '{$functionName}' from InternalCallMapHandlerTest::\$ignoredReturnTypeOnlyFunctions");
}

/**
Expand All @@ -493,27 +683,32 @@ public function testCallMapCompliesWithReflection(string $functionName, array $c
$this->markTestSkipped("Function $functionName is ignored in config");
}

unset($callMapEntry[0]);
$function = new ReflectionFunction($functionName);
/** @var string $entryReturnType */
$entryReturnType = array_shift($callMapEntry);

/** @var array<string, string> $callMapEntry */
$this->assertEntryIsCorrect($callMapEntry, $functionName);
$this->assertEntryParameters($callMapEntry, $function);

if (!$this->isReturnTypeOnlyIgnored($functionName)) {
$this->assertEntryReturnType($function, $entryReturnType);
}
}

/**
*
* @param array<string, string> $callMapEntryWithoutReturn
* @psalm-param callable-string $functionName
*/
private function assertEntryIsCorrect(array $callMapEntryWithoutReturn, string $functionName): void
private function assertEntryParameters(array $entryParameters, ReflectionFunction $function): void
{
$rF = new ReflectionFunction($functionName);

/**
* Parse the parameter names from the map.
* @var array<string, array{byRef: bool, refMode: 'rw'|'w', variadic: bool, optional: bool, type: string}>
*/
$normalizedEntries = [];

foreach ($callMapEntryWithoutReturn as $key => $entry) {
foreach ($entryParameters as $key => $entry) {
$normalizedKey = $key;
/**
*
Expand Down Expand Up @@ -555,8 +750,8 @@ private function assertEntryIsCorrect(array $callMapEntryWithoutReturn, string $
$normalizedEntry['name'] = $normalizedKey;
$normalizedEntries[$normalizedKey] = $normalizedEntry;
}
foreach ($rF->getParameters() as $parameter) {
$this->assertArrayHasKey($parameter->getName(), $normalizedEntries, "Callmap is missing entry for param {$parameter->getName()} in $functionName: " . print_r($normalizedEntries, true));
foreach ($function->getParameters() as $parameter) {
$this->assertArrayHasKey($parameter->getName(), $normalizedEntries, "Callmap is missing entry for param {$parameter->getName()} in {$function->getName()}: " . print_r($normalizedEntries, true));
$this->assertParameter($normalizedEntries[$parameter->getName()], $parameter);
}
}
Expand All @@ -579,6 +774,19 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa
}
}

/**
*/
public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void
{
$expectedReturn = $function->hasTentativeReturnType() ? $function->getTentativeReturnType() : $function->getReturnType();
if ($expectedReturn === null) {
$this->assertSame('', $entryReturnType, 'CallMap entry has incorrect return type');
return;
}

$this->assertTypeValidity($expectedReturn, $entryReturnType, 'CallMap entry has incorrect return type');
}

/**
* Since string equality is too strict, we do some extra checking here
*/
Expand All @@ -594,7 +802,7 @@ private function assertTypeValidity(ReflectionType $reflected, string $specified
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->fail("Class used in CallMap does not exist: {$matches[1]}");
}
}
}
Expand Down

0 comments on commit f4df504

Please sign in to comment.