mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Merge pull request #8166 from othercorey/func-return-signatures
Allow testing expected CallMap return types
This commit is contained in:
commit
38443effb5
@ -11453,7 +11453,9 @@ return [
|
||||
'ReflectionFunctionAbstract::getShortName' => ['string'],
|
||||
'ReflectionFunctionAbstract::getStartLine' => ['int|false'],
|
||||
'ReflectionFunctionAbstract::getStaticVariables' => ['array'],
|
||||
'ReflectionFunctionAbstract::getTentativeReturnType' => ['?ReflectionType'],
|
||||
'ReflectionFunctionAbstract::hasReturnType' => ['bool'],
|
||||
'ReflectionFunctionAbstract::hasTentativeReturnType' => ['bool'],
|
||||
'ReflectionFunctionAbstract::inNamespace' => ['bool'],
|
||||
'ReflectionFunctionAbstract::isClosure' => ['bool'],
|
||||
'ReflectionFunctionAbstract::isDeprecated' => ['bool'],
|
||||
|
@ -46,6 +46,8 @@ return [
|
||||
'ReflectionEnumUnitCase::getEnum' => ['ReflectionEnum'],
|
||||
'ReflectionEnumUnitCase::getValue' => ['UnitEnum'],
|
||||
'ReflectionEnumBackedCase::getBackingValue' => ['string|int'],
|
||||
'ReflectionFunctionAbstract::getTentativeReturnType' => ['?ReflectionType'],
|
||||
'ReflectionFunctionAbstract::hasTentativeReturnType' => ['bool'],
|
||||
'ReflectionFunctionAbstract::isStatic' => ['bool'],
|
||||
'ReflectionObject::isEnum' => ['bool'],
|
||||
],
|
||||
|
@ -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\Internal\Provider\FakeParserCacheProvider;
|
||||
use Psalm\Tests\TestCase;
|
||||
use Psalm\Tests\TestConfig;
|
||||
use Psalm\Type;
|
||||
use ReflectionException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionParameter;
|
||||
use ReflectionType;
|
||||
@ -336,6 +339,167 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
'zlib_encode',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of function names to ignore only for return type checks.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private static $ignoredReturnTypeOnlyFunctions = [
|
||||
'bcsqrt',
|
||||
'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',
|
||||
'dba_open',
|
||||
'dba_popen',
|
||||
'deflate_init',
|
||||
'enchant_broker_init',
|
||||
'fgetcsv',
|
||||
'filter_input_array',
|
||||
'fopen',
|
||||
'fpassthru',
|
||||
'fsockopen',
|
||||
'ftp_get_option',
|
||||
'get_declared_traits',
|
||||
'gmp_export',
|
||||
'gmp_hamdist',
|
||||
'gmp_import',
|
||||
'gzeof',
|
||||
'gzopen',
|
||||
'gzpassthru',
|
||||
'hash',
|
||||
'hash_hkdf',
|
||||
'hash_hmac',
|
||||
'iconv_get_encoding',
|
||||
'igbinary_serialize',
|
||||
'imagecolorclosest',
|
||||
'imagecolorclosestalpha',
|
||||
'imagecolorclosesthwb',
|
||||
'imagecolorexact',
|
||||
'imagecolorexactalpha',
|
||||
'imagecolorresolve',
|
||||
'imagecolorresolvealpha',
|
||||
'imagecolorset',
|
||||
'imagecolorsforindex',
|
||||
'imagecolorstotal',
|
||||
'imagecolortransparent',
|
||||
'imageloadfont',
|
||||
'imagesx',
|
||||
'imagesy',
|
||||
'imap_mailboxmsginfo',
|
||||
'imap_msgno',
|
||||
'imap_num_recent',
|
||||
'inflate_init',
|
||||
'intlcal_get',
|
||||
'intlcal_get_keyword_values_for_locale',
|
||||
'intlgregcal_set_gregorian_change',
|
||||
'intltz_get_offset',
|
||||
'jddayofweek',
|
||||
'jdtounix',
|
||||
'ldap_count_entries',
|
||||
'ldap_exop',
|
||||
'ldap_get_attributes',
|
||||
'mb_encoding_aliases',
|
||||
'metaphone',
|
||||
'mongodb\\bson\\fromjson',
|
||||
'mongodb\\bson\\fromphp',
|
||||
'mongodb\\bson\\tojson',
|
||||
'msg_get_queue',
|
||||
'mysqli_stmt_get_warnings',
|
||||
'mysqli_stmt_insert_id',
|
||||
'numfmt_create',
|
||||
'ob_list_handlers',
|
||||
'odbc_autocommit',
|
||||
'odbc_columnprivileges',
|
||||
'odbc_columns',
|
||||
'odbc_connect',
|
||||
'odbc_do',
|
||||
'odbc_exec',
|
||||
'odbc_fetch_object',
|
||||
'odbc_foreignkeys',
|
||||
'odbc_gettypeinfo',
|
||||
'odbc_pconnect',
|
||||
'odbc_prepare',
|
||||
'odbc_primarykeys',
|
||||
'odbc_specialcolumns',
|
||||
'odbc_statistics',
|
||||
'odbc_tableprivileges',
|
||||
'odbc_tables',
|
||||
'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',
|
||||
'pspell_config_create',
|
||||
'pspell_new',
|
||||
'pspell_new_config',
|
||||
'pspell_new_personal',
|
||||
'register_shutdown_function',
|
||||
'rewinddir',
|
||||
'set_error_handler',
|
||||
'set_exception_handler',
|
||||
'shm_attach',
|
||||
'shmop_open',
|
||||
'simplexml_import_dom',
|
||||
'sleep',
|
||||
'snmp_set_oid_numeric_print',
|
||||
'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',
|
||||
'yaml_parse',
|
||||
'yaml_parse_file',
|
||||
'yaml_parse_url',
|
||||
'zip_open',
|
||||
'zip_read',
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Codebase
|
||||
@ -391,7 +555,7 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
|
||||
/**
|
||||
*
|
||||
* @return iterable<string, array{0: callable-string, 1: array<int|string>}>
|
||||
* @return iterable<string, array{0: callable-string, 1: array<int|string, string>}>
|
||||
*/
|
||||
public function callMapEntryProvider(): iterable
|
||||
{
|
||||
@ -437,45 +601,59 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private function isReturnTypeOnlyIgnored(string $functionName): bool
|
||||
{
|
||||
return in_array($functionName, static::$ignoredReturnTypeOnlyFunctions, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testIgnoresAreSortedAndUnique
|
||||
* @depends testGetcallmapReturnsAValidCallmap
|
||||
* @dataProvider callMapEntryProvider
|
||||
* @coversNothing
|
||||
* @psalm-param callable-string $functionName
|
||||
* @param array<int|string, string> $callMapEntry
|
||||
*/
|
||||
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);
|
||||
$function = new ReflectionFunction($functionName);
|
||||
/** @var string $entryReturnType */
|
||||
$entryReturnType = array_shift($callMapEntry);
|
||||
|
||||
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]);
|
||||
if ($functionIgnored) {
|
||||
try {
|
||||
/** @var array<string, string> $callMapEntry */
|
||||
$this->assertEntryParameters($function, $callMapEntry);
|
||||
$this->assertEntryReturnType($function, $entryReturnType);
|
||||
} catch (AssertionFailedError $e) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
} catch (ExpectationFailedException $e) {
|
||||
$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 $e) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
} catch (ExpectationFailedException $e) {
|
||||
$this->assertTrue(true);
|
||||
return;
|
||||
}
|
||||
$this->fail("Remove '{$functionName}' from InternalCallMapHandlerTest::\$ignoredReturnTypeOnlyFunctions");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -485,7 +663,7 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
* @depends testIgnoresAreSortedAndUnique
|
||||
* @dataProvider callMapEntryProvider
|
||||
* @psalm-param callable-string $functionName
|
||||
* @param array $callMapEntry
|
||||
* @param array<int|string, string> $callMapEntry
|
||||
*/
|
||||
public function testCallMapCompliesWithReflection(string $functionName, array $callMapEntry): void
|
||||
{
|
||||
@ -493,27 +671,31 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
$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($function, $callMapEntry);
|
||||
|
||||
if (!$this->isReturnTypeOnlyIgnored($functionName)) {
|
||||
$this->assertEntryReturnType($function, $entryReturnType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array<string, string> $callMapEntryWithoutReturn
|
||||
* @psalm-param callable-string $functionName
|
||||
* @param array<string, string> $entryParameters
|
||||
*/
|
||||
private function assertEntryIsCorrect(array $callMapEntryWithoutReturn, string $functionName): void
|
||||
private function assertEntryParameters(ReflectionFunction $function, array $entryParameters): 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;
|
||||
/**
|
||||
*
|
||||
@ -555,8 +737,8 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
$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 +761,26 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @psalm-suppress UndefinedMethod
|
||||
*/
|
||||
public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
|
||||
/** @var \ReflectionType|null $expectedType */
|
||||
$expectedType = $function->hasTentativeReturnType() ? $function->getTentativeReturnType() : $function->getReturnType();
|
||||
} else {
|
||||
$expectedType = $function->getReturnType();
|
||||
}
|
||||
if ($expectedType === null) {
|
||||
$this->assertSame('', $entryReturnType, 'CallMap entry has incorrect return type');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->assertTypeValidity($expectedType, $entryReturnType, 'CallMap entry has incorrect return type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Since string equality is too strict, we do some extra checking here
|
||||
*/
|
||||
@ -594,7 +796,7 @@ class InternalCallMapHandlerTest extends TestCase
|
||||
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]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user