From e7cddec59adbe6f4682428da6a4a8158ad26c90a Mon Sep 17 00:00:00 2001 From: Corey Taylor Date: Sat, 25 Jun 2022 17:01:12 -0500 Subject: [PATCH] Allow testing expected CallMap return types and ignore functions that currently fail --- .../Codebase/InternalCallMapHandlerTest.php | 222 +++++++++++++++--- 1 file changed, 189 insertions(+), 33 deletions(-) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 198509bceab..b7a01e6c396 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -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; @@ -16,6 +18,7 @@ use Psalm\Tests\TestCase; use Psalm\Tests\TestConfig; use Psalm\Type; +use ReflectionException; use ReflectionFunction; use ReflectionParameter; use ReflectionType; @@ -336,6 +339,126 @@ class InternalCallMapHandlerTest extends TestCase 'zlib_encode', ]; + /** + * List of function names to ignore only for return type checks. + * + * @var list + */ + private static $ignoredReturnTypeOnlyFunctions = [ + 'bzopen', + 'cal_from_jd', + 'collator_get_strength', + 'curl_multi_init', + 'date_add', + 'date_date_set', + 'date_diff', + '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', + 'fgetcsv', + 'filter_input_array', + 'fopen', + 'fpassthru', + 'fsockopen', + 'ftp_get_option', + 'hash', + 'hash_hkdf', + 'hash_hmac', + 'get_declared_traits', + 'gzeof', + 'gzopen', + 'gzpassthru', + 'iconv_get_encoding', + 'imagecolorclosest', + 'imagecolorclosestalpha', + 'imagecolorclosesthwb', + 'imagecolorexact', + 'imagecolorexactalpha', + 'imagecolorresolve', + 'imagecolorresolvealpha', + 'imagecolorset', + 'imagecolorsforindex', + 'imagecolorstotal', + 'imagecolortransparent', + 'imageloadfont', + 'imagesx', + 'imagesy', + 'inflate_init', + 'intlcal_get', + 'intlcal_get_keyword_values_for_locale', + 'intlgregcal_set_gregorian_change', + 'intltz_get_offset', + 'jddayofweek', + 'jdtounix', + 'mb_encoding_aliases', + 'metaphone', + 'msg_get_queue', + 'mysqli_stmt_get_warnings', + 'mysqli_stmt_insert_id', + 'numfmt_create', + 'ob_list_handlers', + 'opendir', + 'openssl_random_pseudo_bytes', + 'openssl_spki_export', + 'openssl_spki_export_challenge', + 'pack', + 'parse_url', + 'passthru', + 'pcntl_exec', + 'pcntl_signal_get_handler', + 'pcntl_strerror', + 'pfsockopen', + 'pg_port', + 'pg_socket', + 'popen', + 'proc_open', + 'register_shutdown_function', + 'rewinddir', + 'set_error_handler', + 'set_exception_handler', + 'shm_attach', + 'shmop_open', + 'simplexml_import_dom', + 'sleep', + '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_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_get_option', + 'zip_open', + 'zip_read', + ]; + /** * * @var Codebase @@ -437,6 +560,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 @@ -446,36 +576,39 @@ 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 $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 $callMapEntry */ + $this->assertEntryParameters($callMapEntry, $function); + $this->assertEntryReturnType($function, $entryReturnType); + } catch (AssertionFailedError) { + $this->assertTrue(true); + return; + } catch (ExpectationFailedException) { + $this->assertTrue(true); + return; } + $this->fail("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->fail("Remove '{$functionName}' from InternalCallMapHandlerTest::\$ignoredReturnTypeOnlyFunctions"); } /** @@ -493,27 +626,31 @@ 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 $callMapEntry */ - $this->assertEntryIsCorrect($callMapEntry, $functionName); + $this->assertEntryParameters($callMapEntry, $function); + + if (!$this->isReturnTypeOnlyIgnored($functionName)) { + $this->assertEntryReturnType($function, $entryReturnType); + } } /** * - * @param array $callMapEntryWithoutReturn - * @psalm-param callable-string $functionName + * @param array $entryParameters */ - 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 */ $normalizedEntries = []; - foreach ($callMapEntryWithoutReturn as $key => $entry) { + foreach ($entryParameters as $key => $entry) { $normalizedKey = $key; /** * @@ -555,8 +692,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); } } @@ -579,6 +716,25 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa } } + /** + */ + public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void + { + if (version_compare(PHP_VERSION, '8.1.0', '>=')) { + $expectedReturn = $function->hasTentativeReturnType() ? $function->getTentativeReturnType() : $function->getReturnType(); + } else { + $expectedReturn = $function->getReturnType(); + } + + /** @var \ReflectionType|null $expectedReturn */ + 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 */ @@ -594,7 +750,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]}"); } } }