2021-08-08 10:39:54 +02:00
< ? php
2021-12-15 04:58:32 +01:00
2021-09-23 19:29:15 +02:00
namespace Psalm\Tests\Internal\Codebase ;
2021-08-08 10:39:54 +02:00
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 ;
2022-06-15 10:56:15 +02:00
use Psalm\Codebase ;
2022-06-13 15:10:23 +02:00
use Psalm\Internal\Analyzer\ProjectAnalyzer ;
2021-08-08 10:39:54 +02:00
use Psalm\Internal\Codebase\InternalCallMapHandler ;
2022-06-15 09:27:40 +02:00
use Psalm\Internal\Codebase\Reflection ;
2022-06-13 15:10:23 +02:00
use Psalm\Internal\Provider\FakeFileProvider ;
use Psalm\Internal\Provider\Providers ;
2022-06-15 09:27:40 +02:00
use Psalm\Internal\Type\Comparator\UnionTypeComparator ;
2022-06-13 15:10:23 +02:00
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider ;
2021-12-03 20:11:20 +01:00
use Psalm\Tests\TestCase ;
2022-06-13 15:10:23 +02:00
use Psalm\Tests\TestConfig ;
2022-06-15 09:27:40 +02:00
use Psalm\Type ;
2022-06-13 15:10:23 +02:00
use ReflectionFunction ;
use ReflectionParameter ;
2022-06-15 10:56:15 +02:00
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-08-08 10:39:54 +02:00
2021-12-03 20:11:20 +01:00
class InternalCallMapHandlerTest extends TestCase
2021-08-08 10:39:54 +02:00
{
2022-06-15 13:16:24 +02:00
/**
* @ var string []
*/
2022-06-15 10:56:15 +02:00
private static $ignoredFunctions = [
2022-06-22 13:28:17 +02:00
'array_column' ,
2022-06-22 12:59:47 +02:00
'array_diff' ,
2022-06-22 13:28:17 +02:00
'array_diff_assoc' ,
'array_diff_key' ,
2022-06-22 12:59:47 +02:00
'array_intersect' ,
2022-06-22 13:28:17 +02:00
'array_intersect_assoc' ,
2022-06-22 12:59:47 +02:00
'array_intersect_key' ,
2022-06-22 13:28:17 +02:00
'array_key_exists' ,
2022-06-22 12:59:47 +02:00
'array_merge' ,
'array_merge_recursive' ,
2022-06-22 13:28:17 +02:00
'array_multisort' ,
2022-06-22 12:59:47 +02:00
'array_push' ,
2022-06-22 13:28:17 +02:00
'array_replace' ,
'array_replace_recursive' ,
'array_unshift' ,
'bcdiv' ,
2022-06-22 12:59:47 +02:00
'bcmod' ,
2022-06-22 13:28:17 +02:00
'bcpowmod' ,
'bzdecompress' ,
'count' ,
'crypt' ,
'date_isodate_set' ,
2022-06-22 12:59:47 +02:00
'datefmt_create' ,
2022-06-22 13:28:17 +02:00
'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' ,
2022-06-22 13:28:17 +02:00
'imap_delete' ,
'imap_open' ,
'imap_rfc822_write_address' ,
'imap_sort' ,
2022-06-22 12:59:47 +02:00
'imap_undelete' ,
2022-06-22 13:28:17 +02:00
'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' ,
2022-06-22 13:28:17 +02:00
'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' ,
2022-06-22 13:28:17 +02:00
'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' ,
2022-06-22 13:28:17 +02:00
'socket_recvfrom' ,
'socket_recvmsg' ,
'socket_select' ,
2022-06-22 12:59:47 +02:00
'socket_send' ,
2022-06-22 13:28:17 +02:00
'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-15 12:09: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 []
*/
2022-06-15 10:56:15 +02:00
private static $ignoredPrefixes = [
2022-06-15 12:09:47 +02:00
'apcu_' ,
'bc' ,
2022-06-15 10:56:15 +02:00
'collator_' ,
'ctype_' ,
2022-06-15 12:09:47 +02:00
'datefmt_' ,
'enchant_' ,
2022-06-15 10:56:15 +02:00
'gmp_' ,
2022-06-15 12:09:47 +02:00
'gnupg_' ,
2022-06-15 10:56:15 +02:00
'image' ,
2022-06-15 12:09:47 +02:00
'imap_' ,
'inflate_' ,
'intl' ,
'ldap_' ,
'mailparse_' ,
'memcache_' ,
'msg_' ,
2022-06-15 10:56:15 +02:00
'mysqli_' ,
2022-06-15 12:09:47 +02:00
'normalizer_' ,
'oauth_' ,
'oci' ,
'odbc_' ,
'openssl_' ,
'pg_' ,
'sem_' ,
'shm_' ,
'shmop_' ,
'snmp_' ,
'socket_' ,
2022-06-15 10:56:15 +02:00
'sodium_' ,
2022-06-15 12:09:47 +02:00
'sqlsrv_' ,
'tidy_' ,
'transliterator_' ,
'uopz_' ,
'xdiff_' ,
'xmlrpc_server' ,
'yaml_' ,
2022-06-15 10:56:15 +02:00
];
/**
* Initialized in setup
* @ var string Regex
*/
private static $prefixRegex = '//' ;
2022-06-13 15:10:23 +02:00
/**
*
* @ 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-13 15:10:23 +02:00
2022-06-15 13:16:24 +02:00
/**
*
* @ var Codebase
*/
2022-06-15 10:56:15 +02:00
private static $codebase ;
2022-06-15 13:13:24 +02:00
2022-06-15 10:56:15 +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
2021-08-08 10:39:54 +02:00
/**
2022-01-11 16:45:29 +01:00
* @ covers \Psalm\Internal\Codebase\InternalCallMapHandler :: getCallMap
2021-08-08 10:39:54 +02:00
*/
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 );
}
}
}
2022-06-13 15:10:23 +02:00
2022-06-15 13:23:32 +02:00
/**
*
2022-06-22 13:28:17 +02:00
* @ return iterable < string , array { 0 : callable - string , 1 : array < int | string > } >
2022-06-15 13:23:32 +02:00
*/
2022-06-13 15:10:23 +02:00
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 (
2022-06-13 15:10:23 +02:00
new TestConfig (),
new Providers (
new FakeFileProvider (),
new FakeParserCacheProvider ()
)
);
$callMap = InternalCallMapHandler :: getCallMap ();
2022-06-15 13:28:09 +02:00
foreach ( $callMap as $function => $entry ) {
2022-06-13 15:10:23 +02:00
// Skip class methods
2022-06-15 09:11:14 +02:00
if ( strpos ( $function , '::' ) !== false || ! function_exists ( $function )) {
continue ;
}
2022-06-13 15:29:21 +02:00
// Skip functions with alternate signatures
2022-06-14 15:16:13 +02:00
if ( isset ( $callMap [ " $function '1 " ]) || preg_match ( " / \ ' \ d $ / " , $function )) {
2022-06-13 15:10:23 +02:00
continue ;
}
2022-06-15 10:56:15 +02:00
// if ($function != 'fprintf') continue;
2022-06-13 15:10:23 +02:00
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 ;
}
/**
2022-06-22 13:28:17 +02:00
* @ depends testGetcallmapReturnsAValidCallmap
2022-06-22 12:59:47 +02:00
* @ dataProvider callMapEntryProvider
2022-06-22 13:28:17 +02:00
* @ 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") ;
}
2022-06-13 15:10:23 +02:00
/**
* This function will test functions that are in the callmap AND currently defined
* @ coversNothing
* @ depends testGetcallmapReturnsAValidCallmap
* @ dataProvider callMapEntryProvider
2022-06-22 13:28:17 +02:00
* @ psalm - param callable - string $functionName
2022-06-15 15:05:06 +02:00
* @ param array $callMapEntry
2022-06-13 15:10:23 +02:00
*/
public function testCallMapCompliesWithReflection ( string $functionName , array $callMapEntry ) : void
{
2022-06-22 12:59:47 +02:00
if ( $this -> isIgnored ( $functionName )) {
2022-06-15 10:56:15 +02:00
$this -> markTestSkipped ( " Function $functionName is ignored in config " );
}
2022-06-15 15:05:06 +02:00
unset ( $callMapEntry [ 0 ]);
/** @var array<string, string> $callMapEntry */
2022-06-13 15:10:23 +02:00
$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
2022-06-13 15:10:23 +02:00
{
$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 } >
2022-06-13 15:10:23 +02:00
*/
$normalizedEntries = [];
2022-06-15 15:05:06 +02:00
foreach ( $callMapEntryWithoutReturn as $key => $entry ) {
2022-06-13 15:10:23 +02:00
$normalizedKey = $key ;
2022-06-15 15:05:06 +02:00
/**
*
* @ var array { byRef : bool , refMode : 'rw' | 'w' , variadic : bool , optional : bool , type : string } $normalizedEntry
*/
2022-06-13 15:10:23 +02:00
$normalizedEntry = [
'variadic' => false ,
'byRef' => false ,
'optional' => false ,
'type' => $entry ,
];
2022-06-15 10:56:15 +02:00
if ( strncmp ( $normalizedKey , '&' , 1 ) === 0 ) {
2022-06-13 15:10:23 +02:00
$normalizedEntry [ 'byRef' ] = true ;
2022-06-15 10:56:15 +02:00
$normalizedKey = substr ( $normalizedKey , 1 );
2022-06-13 15:10:23 +02:00
}
2022-06-15 10:56:15 +02:00
2022-06-13 15:10:23 +02:00
if ( strncmp ( $normalizedKey , '...' , 3 ) === 0 ) {
$normalizedEntry [ 'variadic' ] = true ;
$normalizedKey = substr ( $normalizedKey , 3 );
}
2022-06-15 10:56:15 +02:00
// 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.
2022-06-13 15:10:23 +02:00
if ( substr ( $normalizedKey , - 1 , 1 ) === " = " ) {
$normalizedEntry [ 'optional' ] = true ;
$normalizedKey = substr ( $normalizedKey , 0 , - 1 );
}
2022-06-15 10:56:15 +02:00
$normalizedEntry [ 'name' ] = $normalizedKey ;
2022-06-13 15:10:23 +02:00
$normalizedEntries [ $normalizedKey ] = $normalizedEntry ;
}
2022-06-15 13:28:09 +02:00
foreach ( $rF -> getParameters () as $parameter ) {
2022-06-13 15:10:23 +02:00
if ( $this -> skipUndefinedParams && ! isset ( $normalizedEntries [ $parameter -> getName ()])) {
continue ;
2022-06-15 09:11:14 +02:00
} else {
2022-06-15 10:56:15 +02:00
$this -> assertArrayHasKey ( $parameter -> getName (), $normalizedEntries , " Callmap is missing entry for param { $parameter -> getName () } in $functionName : " . print_r ( $normalizedEntries , true ));
2022-06-13 15:10:23 +02:00
}
2022-06-15 15:05:06 +02:00
$this -> assertParameter ( $normalizedEntries [ $parameter -> getName ()], $parameter );
2022-06-13 15:10:23 +02:00
}
}
/**
*
* @ 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
2022-06-13 15:10:23 +02:00
{
$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 " );
2022-06-13 15:10:23 +02:00
$expectedType = $param -> getType ();
2022-06-14 15:16:13 +02:00
2022-06-22 13:28:17 +02:00
if ( isset ( $expectedType ) && ! empty ( $normalizedEntry [ 'type' ])) {
2022-06-13 15:10:23 +02:00
$this -> assertTypeValidity ( $expectedType , $normalizedEntry [ 'type' ], " Param ' { $name } ' has incorrect type " );
}
}
/**
* Since string equality is too strict , we do some extra checking here
*/
2022-06-15 10:56:15 +02:00
private function assertTypeValidity ( ReflectionType $reflected , string $specified , string $message ) : void
2022-06-13 15:10:23 +02:00
{
2022-06-15 09:27:40 +02:00
$expectedType = Reflection :: getPsalmTypeFromReflectionType ( $reflected );
2022-06-22 12:59:47 +02:00
try {
2022-06-15 09:27:40 +02:00
$parsedType = Type :: parseString ( $specified );
2022-06-22 12:59:47 +02:00
} catch ( \Throwable $t ) {
die ( " Failed to parse type: $specified -- $message " );
}
2022-06-15 09:27:40 +02:00
2022-06-22 12:59:47 +02:00
try {
2022-06-15 10:56:15 +02:00
$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 ]);
}
}
2022-06-13 15:10:23 +02:00
}
2021-08-08 10:39:54 +02:00
}