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-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 ;
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-14 15:16:13 +02:00
'sprintf' , 'printf' , 'ctype_print' , 'date_sunrise' /** deprecated in 8.1 */ ,
2022-06-15 10:56:15 +02:00
'file_put_contents' ,
'dom_import_simplexml' , 'imagegd' , 'imagegd2' , 'mysqli_execute' , 'array_multisort' ,
'intlcal_from_date_time' , 'simplexml_import_dom' , 'imagefilledpolygon' ,
/** deprecated in 8.0 */
2022-06-15 12:09:47 +02:00
'zip_entry_close' ,
'date_time_set' ,
'curl_unescape' ,
'extract' ,
'enum_exists' ,
'igbinary_unserialize' ,
'count' ,
'lzf_compress' ,
'long2ip' ,
'array_column' ,
'preg_replace_callback_array' ,
'preg_filter' ,
'zlib_encode' ,
'inotify_rm_watch' ,
'mail' ,
'easter_date' ,
'date_isodate_set' ,
'snmpset' ,
'get_class_methods' ,
'filter_var_array' ,
'deflate_add' ,
'bzdecompress' ,
'substr_replace' ,
'lzf_decompress' ,
'mongodb\bson\tophp' ,
'fputcsv' ,
'get_headers' ,
'get_parent_class' ,
'filter_var' ,
'array_key_exists' ,
2022-06-15 13:23:32 +02:00
'sapi_windows_cp_get' ,
'stream_select' ,
2022-06-15 13:29:22 +02:00
'hash_hmac_file'
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-15 15:05:06 +02:00
* @ return iterable < string , array { 0 : 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 ];
}
}
/**
* This function will test functions that are in the callmap AND currently defined
* @ coversNothing
* @ depends testGetcallmapReturnsAValidCallmap
* @ dataProvider callMapEntryProvider
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-15 15:05:06 +02:00
/** @psalm-assert callable-string $functionName */
2022-06-13 15:10:23 +02:00
if ( ! function_exists ( $functionName )) {
$this -> markTestSkipped ( " Function $functionName does not exist " );
}
2022-06-15 10:56:15 +02:00
if ( in_array ( $functionName , self :: $ignoredFunctions )) {
$this -> markTestSkipped ( " Function $functionName is ignored in config " );
}
if ( preg_match ( self :: $prefixRegex , $functionName )) {
$this -> markTestSkipped ( " Function $functionName has ignored prefix " );
}
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 ();
// $identifier = "Param $functionName - $name";
2022-06-14 15:16:13 +02:00
try {
2022-06-15 13:28:09 +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 " );
} catch ( Throwable $t ) {
2022-06-15 10:56:15 +02:00
$this -> markTestSkipped ( " Exception: " . $t -> getMessage ());
2022-06-14 15:16:13 +02:00
}
2022-06-13 15:10:23 +02:00
$expectedType = $param -> getType ();
2022-06-14 15:16:13 +02:00
2022-06-15 10:56:15 +02:00
if ( isset ( $expectedType )) {
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 );
$parsedType = Type :: parseString ( $specified );
2022-06-15 10:56:15 +02:00
$this -> assertTrue ( UnionTypeComparator :: isContainedBy ( self :: $codebase , $parsedType , $expectedType ), $message );
2022-06-13 15:10:23 +02:00
}
2021-08-08 10:39:54 +02:00
}