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 ;
2022-06-26 00:01:12 +02:00
use PHPUnit\Framework\AssertionFailedError ;
2022-06-22 12:59:47 +02:00
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-12-04 09:19:57 +01:00
use ReflectionException ;
2022-06-13 15:10:23 +02:00
use ReflectionFunction ;
2022-12-04 09:19:57 +01:00
use ReflectionFunctionAbstract ;
use ReflectionMethod ;
2022-06-13 15:10:23 +02:00
use ReflectionParameter ;
2022-06-15 10:56:15 +02:00
use ReflectionType ;
2022-06-15 13:28:09 +02:00
2022-07-06 10:33:34 +02:00
use function array_shift ;
2022-06-22 15:05:24 +02:00
use function class_exists ;
2022-06-15 13:28:09 +02:00
use function count ;
use function explode ;
use function function_exists ;
use function in_array ;
2022-06-22 15:37:51 +02:00
use function is_array ;
2022-06-22 15:32:35 +02:00
use function is_int ;
2022-06-15 13:28:09 +02:00
use function json_encode ;
use function preg_match ;
use function print_r ;
2022-06-22 15:32:35 +02:00
use function strcmp ;
2022-06-15 13:28:09 +02:00
use function strncmp ;
use function strpos ;
use function substr ;
2022-07-06 10:33:34 +02:00
use function version_compare ;
2021-08-08 10:39:54 +02:00
2022-06-22 15:32:35 +02:00
use const PHP_MAJOR_VERSION ;
use const PHP_MINOR_VERSION ;
2022-07-06 10:33:34 +02:00
use const PHP_VERSION ;
2022-06-22 15:32:35 +02:00
2023-02-15 05:30:45 +01:00
/** @group callmap */
2021-12-03 20:11:20 +01:00
class InternalCallMapHandlerTest extends TestCase
2021-08-08 10:39:54 +02:00
{
2023-02-16 02:55:49 +01:00
/**
* Regex patterns for callmap entries that should be skipped .
*
* These will not be checked against reflection . This prevents a
* large ignore list for extension functions have invalid reflection
* or are not maintained .
*
2023-07-02 08:33:43 +02:00
* @ var list < non - empty - string >
2023-02-16 02:55:49 +01:00
*/
private static array $skippedPatterns = [
'/\'\d$/' , // skip alternate signatures
'/^redis/' , // redis extension
'/^imagick/' , // imagick extension
'/^uopz/' , // uopz extension
2023-02-24 03:49:30 +01:00
'/^memcache[_:]/' , // memcache extension
2023-02-17 08:27:37 +01:00
'/^memcachepool/' , // memcache extension
2023-03-28 11:20:39 +02:00
'/^gnupg/' , // gnupg extension
2023-02-16 02:55:49 +01:00
];
2022-06-15 13:16:24 +02:00
/**
2022-06-22 15:32:35 +02:00
* Specify a function name as value , or a function name as key and
* an array containing the PHP versions in which to ignore this function as values .
2022-12-14 20:34:41 +01:00
*
2022-06-22 15:32:35 +02:00
* @ var array < int | string , string | list < string >>
2022-06-15 13:16:24 +02:00
*/
2022-12-16 19:58:47 +01:00
private static array $ignoredFunctions = [
2022-06-22 13:28:17 +02:00
'array_multisort' ,
2022-12-04 09:19:57 +01:00
'datefmt_create' => [ '8.0' ],
'fiber::start' ,
2022-06-22 14:57:40 +02:00
'imagefilledpolygon' ,
2022-06-22 13:28:17 +02:00
'imagegd' ,
'imagegd2' ,
'imageopenpolygon' ,
'imagepolygon' ,
2022-12-04 09:19:57 +01:00
'intlgregoriancalendar::__construct' ,
2022-06-22 13:28:17 +02:00
'lzf_compress' ,
'lzf_decompress' ,
2022-06-22 14:57:40 +02:00
'mailparse_msg_extract_part' ,
'mailparse_msg_extract_part_file' ,
'mailparse_msg_extract_whole_part_file' ,
'mailparse_msg_free' ,
'mailparse_msg_get_part' ,
'mailparse_msg_get_part_data' ,
'mailparse_msg_get_structure' ,
'mailparse_msg_parse' ,
'mailparse_stream_encode' ,
2023-02-15 20:33:34 +01:00
'memcached::cas' , // memcached 3.2.0 has incorrect reflection
'memcached::casbykey' , // memcached 3.2.0 has incorrect reflection
2022-12-04 09:19:57 +01:00
'oauth::fetch' ,
'oauth::getaccesstoken' ,
'oauth::setcapath' ,
'oauth::settimeout' ,
'oauth::settimestamp' ,
'oauthprovider::consumerhandler' ,
'oauthprovider::isrequesttokenendpoint' ,
'oauthprovider::timestampnoncehandler' ,
'oauthprovider::tokenhandler' ,
2022-06-22 14:57:40 +02:00
'oci_collection_append' ,
'oci_collection_assign' ,
'oci_collection_element_assign' ,
'oci_collection_element_get' ,
'oci_collection_max' ,
'oci_collection_size' ,
'oci_collection_trim' ,
'oci_fetch_object' ,
'oci_field_is_null' ,
'oci_field_name' ,
'oci_field_precision' ,
'oci_field_scale' ,
'oci_field_size' ,
'oci_field_type' ,
'oci_field_type_raw' ,
'oci_free_collection' ,
'oci_free_descriptor' ,
'oci_lob_append' ,
'oci_lob_eof' ,
'oci_lob_erase' ,
'oci_lob_export' ,
'oci_lob_flush' ,
'oci_lob_import' ,
'oci_lob_load' ,
'oci_lob_read' ,
'oci_lob_rewind' ,
'oci_lob_save' ,
'oci_lob_seek' ,
'oci_lob_size' ,
'oci_lob_tell' ,
'oci_lob_truncate' ,
'oci_lob_write' ,
'oci_register_taf_callback' ,
'oci_result' ,
'ocigetbufferinglob' ,
'ocisetbufferinglob' ,
2023-02-27 13:24:12 +01:00
'recursiveiteratoriterator::__construct' , // Class used in CallMap does not exist: recursiveiterator
2022-06-22 14:57:40 +02:00
'sqlsrv_fetch_array' ,
'sqlsrv_fetch_object' ,
'sqlsrv_get_field' ,
'sqlsrv_prepare' ,
'sqlsrv_query' ,
'sqlsrv_server_info' ,
2022-11-06 06:58:50 +01:00
'ssh2_forward_accept' ,
2022-06-22 14:57:40 +02:00
'xdiff_file_bdiff' ,
'xdiff_file_bdiff_size' ,
'xdiff_file_diff' ,
'xdiff_file_diff_binary' ,
'xdiff_file_merge3' ,
'xdiff_file_rabdiff' ,
'xdiff_string_bdiff' ,
'xdiff_string_bdiff_size' ,
'xdiff_string_bpatch' ,
'xdiff_string_diff' ,
'xdiff_string_diff_binary' ,
'xdiff_string_merge3' ,
'xdiff_string_patch' ,
'xdiff_string_patch_binary' ,
'xdiff_string_rabdiff' ,
2022-06-14 15:16:13 +02:00
];
2022-06-14 15:43:13 +02:00
2022-06-26 00:01:12 +02:00
/**
* List of function names to ignore only for return type checks .
*
2022-12-04 09:19:57 +01:00
* @ var array < int | string , string | list < string >>
2022-06-26 00:01:12 +02:00
*/
2022-12-16 19:58:47 +01:00
private static array $ignoredReturnTypeOnlyFunctions = [
2023-07-20 01:01:10 +02:00
'appenditerator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'appenditerator::getiteratorindex' => [ '8.1' , '8.2' , '8.3' ],
'arrayobject::getiterator' => [ '8.1' , '8.2' , '8.3' ],
'cachingiterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'callbackfilteriterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
2022-12-04 09:19:57 +01:00
'curl_multi_getcontent' ,
2023-07-20 01:01:10 +02:00
'datetime::add' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
'datetime::modify' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
'datetime::createfromformat' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
2023-02-12 08:24:43 +01:00
'datetime::createfromimmutable' => [ '8.1' ],
2022-12-04 09:19:57 +01:00
'datetime::createfrominterface' ,
2023-07-20 01:01:10 +02:00
'datetime::setdate' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
'datetime::setisodate' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
'datetime::settime' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
'datetime::settimestamp' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
'datetime::settimezone' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
'datetime::sub' => [ '8.1' , '8.2' , '8.3' ], // DateTime does not contain static
2022-12-04 09:19:57 +01:00
'datetimeimmutable::createfrominterface' ,
'fiber::getcurrent' ,
2023-07-20 01:01:10 +02:00
'filteriterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
2023-03-09 10:42:10 +01:00
'get_cfg_var' , // Ignore array return type
2023-07-20 01:01:10 +02:00
'infiniteiterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'iteratoriterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'limititerator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'locale::canonicalize' => [ '8.1' , '8.2' , '8.3' ],
'locale::getallvariants' => [ '8.1' , '8.2' , '8.3' ],
'locale::getkeywords' => [ '8.1' , '8.2' , '8.3' ],
'locale::getprimarylanguage' => [ '8.1' , '8.2' , '8.3' ],
'locale::getregion' => [ '8.1' , '8.2' , '8.3' ],
'locale::getscript' => [ '8.1' , '8.2' , '8.3' ],
'locale::parselocale' => [ '8.1' , '8.2' , '8.3' ],
'messageformatter::create' => [ '8.1' , '8.2' , '8.3' ],
'multipleiterator::current' => [ '8.1' , '8.2' , '8.3' ],
'mysqli::get_charset' => [ '8.1' , '8.2' , '8.3' ],
'mysqli_stmt::get_warnings' => [ '8.1' , '8.2' , '8.3' ],
2022-06-26 00:01:12 +02:00
'mysqli_stmt_get_warnings' ,
'mysqli_stmt_insert_id' ,
2023-07-20 01:01:10 +02:00
'norewinditerator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
2022-06-26 00:01:12 +02:00
'passthru' ,
2023-07-20 01:01:10 +02:00
'recursivecachingiterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'recursivecallbackfilteriterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'recursivefilteriterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
'recursiveregexiterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
2022-12-04 09:19:57 +01:00
'reflectionclass::getstaticproperties' => [ '8.1' , '8.2' ],
2023-07-20 01:01:10 +02:00
'reflectionclass::newinstanceargs' => [ '8.1' , '8.2' , '8.3' ],
'reflectionfunction::getclosurescopeclass' => [ '8.1' , '8.2' , '8.3' ],
'reflectionfunction::getclosurethis' => [ '8.1' , '8.2' , '8.3' ],
'reflectionmethod::getclosurescopeclass' => [ '8.1' , '8.2' , '8.3' ],
'reflectionmethod::getclosurethis' => [ '8.1' , '8.2' , '8.3' ],
2022-12-04 09:19:57 +01:00
'reflectionobject::getstaticproperties' => [ '8.1' , '8.2' ],
2023-07-20 01:01:10 +02:00
'reflectionobject::newinstanceargs' => [ '8.1' , '8.2' , '8.3' ],
'regexiterator::getinneriterator' => [ '8.1' , '8.2' , '8.3' ],
2023-01-15 10:57:22 +01:00
'register_shutdown_function' => [ '8.0' , '8.1' ],
2023-07-20 01:01:10 +02:00
'splfileobject::fscanf' => [ '8.1' , '8.2' , '8.3' ],
'spltempfileobject::fscanf' => [ '8.1' , '8.2' , '8.3' ],
'xsltprocessor::transformtoxml' => [ '8.1' , '8.2' , '8.3' ],
2022-12-04 09:19:57 +01:00
];
/**
* List of function names to ignore because they cannot be reflected .
*
* These could be truly inaccessible , or they could be functions removed in newer PHP versions .
* Removed functions should be removed from CallMap and added to the appropriate delta .
*
* @ var array < int | string , string | list < string >>
*/
2022-12-16 19:58:47 +01:00
private static array $ignoredUnreflectableFunctions = [
2022-12-04 09:19:57 +01:00
'closure::__invoke' ,
'domimplementation::__construct' ,
'intliterator::__construct' ,
'pdo::cubrid_schema' ,
'pdo::pgsqlcopyfromarray' ,
'pdo::pgsqlcopyfromfile' ,
'pdo::pgsqlcopytoarray' ,
'pdo::pgsqlcopytofile' ,
'pdo::pgsqlgetnotify' ,
'pdo::pgsqlgetpid' ,
'pdo::pgsqllobcreate' ,
'pdo::pgsqllobopen' ,
'pdo::pgsqllobunlink' ,
'pdo::sqlitecreateaggregate' ,
'pdo::sqlitecreatecollation' ,
'pdo::sqlitecreatefunction' ,
'simplexmlelement::__get' ,
'simplexmlelement::offsetexists' ,
'simplexmlelement::offsetget' ,
'simplexmlelement::offsetset' ,
'simplexmlelement::offsetunset' ,
'spldoublylinkedlist::__construct' ,
'splheap::__construct' ,
'splmaxheap::__construct' ,
'splobjectstorage::__construct' ,
'splpriorityqueue::__construct' ,
'splstack::__construct' ,
2022-06-26 00:01:12 +02:00
];
2022-12-16 19:58:47 +01:00
private static Codebase $codebase ;
2022-06-15 13:13:24 +02:00
2022-06-15 10:56:15 +02:00
public static function setUpBeforeClass () : void
{
$project_analyzer = new ProjectAnalyzer (
new TestConfig (),
new Providers (
new FakeFileProvider (),
2022-12-18 17:15:15 +01:00
new FakeParserCacheProvider (),
),
2022-06-15 10:56:15 +02:00
);
self :: $codebase = $project_analyzer -> getCodebase ();
}
2022-06-22 15:32:35 +02:00
public function testIgnoresAreSortedAndUnique () : void
2022-06-22 14:57:40 +02:00
{
2022-06-22 15:32:35 +02:00
$previousFunction = " " ;
foreach ( self :: $ignoredFunctions as $key => $value ) {
2022-06-22 15:37:51 +02:00
/** @var string */
2022-06-22 15:32:35 +02:00
$function = is_int ( $key ) ? $value : $key ;
2022-09-22 00:41:06 +02:00
$diff = strcmp ( $function , $previousFunction );
2022-12-04 09:19:57 +01:00
$this -> assertGreaterThan ( 0 , $diff , " ' { $function } ' should come before ' { $previousFunction } ' in InternalCallMapHandlerTest:: \$ ignoredFunctions " );
2022-09-22 00:41:06 +02:00
2022-06-22 15:32:35 +02:00
$previousFunction = $function ;
}
2022-06-22 14:57:40 +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
/**
2023-06-17 15:58:58 +02:00
* @ return iterable < string , array { string , array < int | string , 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 (),
2022-12-18 17:15:15 +01:00
new FakeParserCacheProvider (),
),
2022-06-13 15:10:23 +02:00
);
$callMap = InternalCallMapHandler :: getCallMap ();
2022-06-15 13:28:09 +02:00
foreach ( $callMap as $function => $entry ) {
2023-02-16 02:55:49 +01:00
foreach ( static :: $skippedPatterns as $skipPattern ) {
if ( preg_match ( $skipPattern , $function )) {
continue 2 ;
}
}
// Skip functions with alternate signatures
if ( isset ( $callMap [ " $function '1 " ])) {
continue ;
}
2022-12-04 09:19:57 +01:00
$classNameEnd = strpos ( $function , '::' );
if ( $classNameEnd !== false ) {
$className = substr ( $function , 0 , $classNameEnd );
if ( ! class_exists ( $className , false )) {
continue ;
}
} elseif ( ! function_exists ( $function )) {
2022-06-15 09:11:14 +02:00
continue ;
}
2022-12-04 09:19:57 +01:00
2022-06-13 15:10:23 +02:00
yield " $function : " . json_encode ( $entry ) => [ $function , $entry ];
}
}
2022-06-22 15:05:24 +02:00
private function isIgnored ( string $functionName ) : bool
2022-06-22 12:59:47 +02:00
{
if ( in_array ( $functionName , self :: $ignoredFunctions )) {
return true ;
}
2022-06-22 15:32:35 +02:00
2022-06-22 15:37:51 +02:00
if ( isset ( self :: $ignoredFunctions [ $functionName ])
&& is_array ( self :: $ignoredFunctions [ $functionName ])
&& in_array ( PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION , self :: $ignoredFunctions [ $functionName ])) {
2022-06-22 15:32:35 +02:00
return true ;
}
2022-06-22 12:59:47 +02:00
return false ;
}
2022-06-26 00:01:12 +02:00
private function isReturnTypeOnlyIgnored ( string $functionName ) : bool
{
2022-12-04 09:19:57 +01:00
if ( in_array ( $functionName , static :: $ignoredReturnTypeOnlyFunctions , true )) {
return true ;
}
if ( isset ( self :: $ignoredReturnTypeOnlyFunctions [ $functionName ])
&& is_array ( self :: $ignoredReturnTypeOnlyFunctions [ $functionName ])
&& in_array ( PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION , self :: $ignoredReturnTypeOnlyFunctions [ $functionName ])) {
return true ;
}
return false ;
}
private function isUnreflectableIgnored ( string $functionName ) : bool
{
if ( in_array ( $functionName , static :: $ignoredUnreflectableFunctions , true )) {
return true ;
}
if ( isset ( self :: $ignoredUnreflectableFunctions [ $functionName ])
&& is_array ( self :: $ignoredUnreflectableFunctions [ $functionName ])
&& in_array ( PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION , self :: $ignoredUnreflectableFunctions [ $functionName ])) {
return true ;
}
return false ;
2022-06-26 00:01:12 +02:00
}
2022-06-22 12:59:47 +02:00
/**
2022-06-22 15:32:35 +02:00
* @ depends testIgnoresAreSortedAndUnique
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
2023-06-17 15:58:58 +02:00
* @ psalm - param string $functionName
2022-06-26 00:01:12 +02:00
* @ param array < int | string , string > $callMapEntry
2022-06-22 12:59:47 +02:00
*/
public function testIgnoredFunctionsStillFail ( string $functionName , array $callMapEntry ) : void
{
2022-06-26 00:01:12 +02:00
$functionIgnored = $this -> isIgnored ( $functionName );
2022-12-04 09:19:57 +01:00
$unreflectableIgnored = $this -> isUnreflectableIgnored ( $functionName );
if ( ! $functionIgnored && ! $this -> isReturnTypeOnlyIgnored ( $functionName ) && ! $unreflectableIgnored ) {
2022-06-22 12:59:47 +02:00
// Dummy assertion to mark it as passed
$this -> assertTrue ( true );
return ;
}
2022-12-04 09:19:57 +01:00
$function = $this -> getReflectionFunction ( $functionName );
if ( $unreflectableIgnored && $function !== null ) {
$this -> fail ( " Remove ' { $functionName } ' from InternalCallMapHandlerTest:: \$ ignoredUnreflectableFunctions " );
} elseif ( $function === null ) {
$this -> assertTrue ( true );
return ;
}
2022-06-26 00:01:12 +02:00
/** @var string $entryReturnType */
$entryReturnType = array_shift ( $callMapEntry );
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 " );
}
2022-06-22 12:59:47 +02:00
try {
2022-06-26 00:01:12 +02:00
$this -> assertEntryReturnType ( $function , $entryReturnType );
} catch ( AssertionFailedError $e ) {
$this -> assertTrue ( true );
return ;
2022-06-22 15:05:24 +02:00
} catch ( ExpectationFailedException $e ) {
2022-06-26 00:01:12 +02:00
$this -> assertTrue ( true );
return ;
2022-06-22 12:59:47 +02:00
}
2022-06-26 00:01:12 +02:00
$this -> fail ( " Remove ' { $functionName } ' from InternalCallMapHandlerTest:: \$ ignoredReturnTypeOnlyFunctions " );
2022-06-22 12:59:47 +02:00
}
2022-06-13 15:10:23 +02:00
/**
* This function will test functions that are in the callmap AND currently defined
2022-12-14 20:34:41 +01:00
*
2022-06-13 15:10:23 +02:00
* @ coversNothing
* @ depends testGetcallmapReturnsAValidCallmap
2022-06-22 15:32:35 +02:00
* @ depends testIgnoresAreSortedAndUnique
2022-06-13 15:10:23 +02:00
* @ dataProvider callMapEntryProvider
2023-06-17 15:58:58 +02:00
* @ psalm - param string $functionName
2022-06-26 00:01:12 +02:00
* @ param array < int | string , string > $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
2022-12-04 09:19:57 +01:00
$function = $this -> getReflectionFunction ( $functionName );
if ( $function === null ) {
if ( ! $this -> isUnreflectableIgnored ( $functionName )) {
$this -> fail ( 'Unable to reflect method. Add name to $ignoredUnreflectableFunctions if exists in latest PHP version.' );
}
return ;
}
2022-06-26 00:01:12 +02:00
/** @var string $entryReturnType */
$entryReturnType = array_shift ( $callMapEntry );
2022-06-15 15:05:06 +02:00
/** @var array<string, string> $callMapEntry */
2022-06-26 00:01:12 +02:00
$this -> assertEntryParameters ( $function , $callMapEntry );
if ( ! $this -> isReturnTypeOnlyIgnored ( $functionName )) {
$this -> assertEntryReturnType ( $function , $entryReturnType );
}
2022-06-13 15:10:23 +02:00
}
2022-12-04 09:19:57 +01:00
/**
* Returns the correct reflection type for function or method name .
*/
private function getReflectionFunction ( string $functionName ) : ? ReflectionFunctionAbstract
{
try {
if ( strpos ( $functionName , '::' ) !== false ) {
return new ReflectionMethod ( $functionName );
}
/** @var callable-string $functionName */
return new ReflectionFunction ( $functionName );
} catch ( ReflectionException $e ) {
return null ;
}
}
2022-06-15 14:46:50 +02:00
/**
2022-06-26 00:01:12 +02:00
* @ param array < string , string > $entryParameters
2022-06-15 14:46:50 +02:00
*/
2022-12-04 09:19:57 +01:00
private function assertEntryParameters ( ReflectionFunctionAbstract $function , array $entryParameters ) : void
2022-06-13 15:10:23 +02:00
{
/**
* Parse the parameter names from the map .
2022-12-14 20:34:41 +01:00
*
2022-11-12 02:14:21 +01:00
* @ var array < string , array { byRef : bool , refMode : 'rw' | 'w' | 'r' , variadic : bool , optional : bool , type : string } >
2022-06-13 15:10:23 +02:00
*/
$normalizedEntries = [];
2022-06-26 00:01:12 +02:00
foreach ( $entryParameters as $key => $entry ) {
2022-06-13 15:10:23 +02:00
$normalizedKey = $key ;
2022-06-15 15:05:06 +02:00
/**
2022-11-12 02:14:21 +01:00
* @ var array { byRef : bool , refMode : 'rw' | 'w' | 'r' , variadic : bool , optional : bool , type : string } $normalizedEntry
2022-06-15 15:05:06 +02:00
*/
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 ) {
2022-11-05 22:34:42 +01:00
if ( ! ( $parts [ 0 ] === 'rw' || $parts [ 0 ] === 'w' || $parts [ 0 ] === 'r' )) {
throw new InvalidArgumentException ( 'Invalid refMode: ' . $parts [ 0 ]);
}
2022-06-15 10:56:15 +02:00
$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-12-22 19:16:38 +01:00
//$this->assertTrue($this->hasParameter($function, $normalizedKey), "Calmap has extra param entry {$normalizedKey}");
2022-06-15 10:56:15 +02:00
$normalizedEntry [ 'name' ] = $normalizedKey ;
2022-06-13 15:10:23 +02:00
$normalizedEntries [ $normalizedKey ] = $normalizedEntry ;
}
2022-12-22 19:16:38 +01:00
2022-06-26 00:01:12 +02:00
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 ));
2022-06-15 15:05:06 +02:00
$this -> assertParameter ( $normalizedEntries [ $parameter -> getName ()], $parameter );
2022-06-13 15:10:23 +02:00
}
}
2022-12-22 19:16:38 +01:00
/* Used by above assert
private function hasParameter ( ReflectionFunctionAbstract $function , string $name ) : bool
{
foreach ( $function -> getParameters () as $parameter )
{
if ( $parameter -> getName () === $name ) {
return true ;
}
}
return false ;
}
*/
2022-06-13 15:10:23 +02:00
/**
2022-11-12 02:14:21 +01:00
* @ param array { byRef : bool , name ? : string , refMode : 'rw' | 'w' | 'r' , variadic : bool , optional : bool , type : string } $normalizedEntry
2022-06-13 15:10:23 +02:00
*/
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-12-22 19:16:38 +01:00
$this -> assertTypeValidity ( $expectedType , $normalizedEntry [ 'type' ], " Param ' { $name } ' " );
2022-06-13 15:10:23 +02:00
}
}
2022-12-04 09:19:57 +01:00
public function assertEntryReturnType ( ReflectionFunctionAbstract $function , string $entryReturnType ) : void
2022-06-26 00:01:12 +02:00
{
if ( version_compare ( PHP_VERSION , '8.1.0' , '>=' )) {
2022-07-06 10:33:34 +02:00
/** @var ReflectionType|null $expectedType */
2022-06-26 00:01:12 +02:00
$expectedType = $function -> hasTentativeReturnType () ? $function -> getTentativeReturnType () : $function -> getReturnType ();
} else {
$expectedType = $function -> getReturnType ();
}
2022-12-04 09:19:57 +01:00
$this -> assertNotEmpty ( $entryReturnType , 'CallMap entry has empty return type' );
2022-11-06 06:58:50 +01:00
if ( $expectedType !== null ) {
2022-12-22 19:16:38 +01:00
$this -> assertTypeValidity ( $expectedType , $entryReturnType , 'Return' );
2022-11-06 06:58:50 +01:00
}
2022-06-26 00:01:12 +02:00
}
2022-06-13 15:10:23 +02:00
/**
* Since string equality is too strict , we do some extra checking here
*/
2022-12-22 19:16:38 +01:00
private function assertTypeValidity ( ReflectionType $reflected , string $specified , string $msgPrefix ) : void
2022-06-13 15:10:23 +02:00
{
2022-06-15 09:27:40 +02:00
$expectedType = Reflection :: getPsalmTypeFromReflectionType ( $reflected );
2022-07-08 08:08:00 +02:00
$callMapType = Type :: parseString ( $specified );
2022-06-15 09:27:40 +02:00
2022-06-22 12:59:47 +02:00
try {
2023-03-09 10:42:10 +01:00
$this -> assertTrue ( UnionTypeComparator :: isContainedBy ( self :: $codebase , $callMapType , $expectedType , false , false , null , false , false ), " { $msgPrefix } type ' { $specified } ' is not contained by reflected type ' { $reflected } ' " );
2022-06-22 15:05:24 +02:00
} catch ( InvalidArgumentException $e ) {
2022-06-22 12:59:47 +02:00
if ( preg_match ( '/^Could not get class storage for (.*)$/' , $e -> getMessage (), $matches )
&& ! class_exists ( $matches [ 1 ])
) {
2022-06-26 00:01:12 +02:00
$this -> fail ( " Class used in CallMap does not exist: { $matches [ 1 ] } " );
2022-06-22 12:59:47 +02:00
}
}
2022-07-08 08:08:00 +02:00
// Reflection::getPsalmTypeFromReflectionType adds |null to mixed types so skip comparison
2022-12-22 19:16:38 +01:00
if ( ! $expectedType -> hasMixed ()) {
2023-03-09 10:42:10 +01:00
$this -> assertSame ( $expectedType -> isNullable (), $callMapType -> isNullable (), " { $msgPrefix } type ' { $specified } ' missing null from reflected type ' { $reflected } ' " );
//$this->assertSame($expectedType->hasBool(), $callMapType->hasBool(), "{$msgPrefix} type '{$specified}' missing bool from reflected type '{$reflected}'");
$this -> assertSame ( $expectedType -> hasArray (), $callMapType -> hasArray (), " { $msgPrefix } type ' { $specified } ' missing array from reflected type ' { $reflected } ' " );
$this -> assertSame ( $expectedType -> hasInt (), $callMapType -> hasInt (), " { $msgPrefix } type ' { $specified } ' missing int from reflected type ' { $reflected } ' " );
$this -> assertSame ( $expectedType -> hasFloat (), $callMapType -> hasFloat (), " { $msgPrefix } type ' { $specified } ' missing float from reflected type ' { $reflected } ' " );
2022-07-08 08:08:00 +02:00
}
2022-06-13 15:10:23 +02:00
}
2021-08-08 10:39:54 +02:00
}