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-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-13 15:10:23 +02:00
use Psalm\Internal\Provider\FakeFileProvider ;
use Psalm\Internal\Provider\Providers ;
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 ;
use ReflectionFunction ;
use ReflectionNamedType ;
use ReflectionParameter ;
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-14 15:16:13 +02:00
private array $ignoredFunctions = [
'sprintf' , 'printf' , 'ctype_print' , 'date_sunrise' /** deprecated in 8.1 */ ,
'ctype_digit' , 'ctype_lower' , 'ctype_alnum' , 'ctype_alpha' , 'ctype_cntrl' ,
'ctype_graph' , 'ctype_lower' , 'ctype_print' , 'ctype_punct' , 'ctype_space' , 'ctype_upper' ,
'ctype_xdigit' , 'file_put_contents' , 'sodium_crypto_generichash' , 'sodium_crypto_generichash_final' ,
2022-06-14 16:58:49 +02:00
'dom_import_simplexml' , 'imagegd' , 'imagegd2' , 'pg_exec' , 'mysqli_execute' , 'array_multisort' ,
2022-06-14 15:16:13 +02:00
];
2022-06-14 15:43:13 +02:00
2022-06-13 15:10:23 +02:00
/**
*
* @ var bool whether to skip params for which no definition can be found in the callMap
*/
private $skipUndefinedParams = true ;
2022-06-14 15:16:13 +02:00
/**
* These are items that very likely need updating to PHP8 . 1
* @ var bool whether to skip params that are specified in the callmap as `resource`
*/
private $skipResources = false ;
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
public function callMapEntryProvider () : iterable
{
$project_analyzer = new ProjectAnalyzer (
new TestConfig (),
new Providers (
new FakeFileProvider (),
new FakeParserCacheProvider ()
)
);
$callMap = InternalCallMapHandler :: getCallMap ();
unset ( $project_analyzer );
foreach ( $callMap as $function => $entry ) {
// Skip class methods
if ( strpos ( $function , '::' ) !== false ) {
continue ;
}
2022-06-14 15:16:13 +02:00
// if (!str_starts_with($function, 'array_')) {
// 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 ;
}
yield " $function : " . json_encode ( $entry ) => [ $function , $entry ];
}
}
/**
* This function will test functions that are in the callmap AND currently defined
* @ return void
* @ coversNothing
* @ depends testGetcallmapReturnsAValidCallmap
* @ dataProvider callMapEntryProvider
*/
public function testCallMapCompliesWithReflection ( string $functionName , array $callMapEntry ) : void
{
if ( ! function_exists ( $functionName )) {
$this -> markTestSkipped ( " Function $functionName does not exist " );
}
$this -> assertEntryIsCorrect ( $callMapEntry , $functionName );
}
private function assertEntryIsCorrect ( array $callMapEntry , string $functionName ) : void
{
$rF = new ReflectionFunction ( $functionName );
// For now, ignore return types.
unset ( $callMapEntry [ 0 ]);
/**
* Parse the parameter names from the map .
* @ var array < string , array { byRef : bool , refMode : 'rw' | 'w' , variadic : bool , optional : bool , type : string }
*/
$normalizedEntries = [];
foreach ( $callMapEntry as $key => $entry ) {
$normalizedKey = $key ;
$normalizedEntry = [
'variadic' => false ,
'byRef' => false ,
'optional' => false ,
'type' => $entry ,
'name' => $key
];
// Strip prefixes.
if ( strncmp ( $normalizedKey , '&rw_' , 4 ) === 0 ) {
$normalizedEntry [ 'byRef' ] = true ;
$normalizedEntry [ 'refMode' ] = 'rw' ;
$normalizedKey = substr ( $normalizedKey , 4 );
} elseif ( strncmp ( $normalizedKey , '&w_' , 3 ) === 0 ) {
$normalizedEntry [ 'byRef' ] = true ;
$normalizedEntry [ 'refMode' ] = 'w' ;
$normalizedKey = substr ( $normalizedKey , 3 );
}
if ( strncmp ( $normalizedKey , '...' , 3 ) === 0 ) {
$normalizedEntry [ 'variadic' ] = true ;
$normalizedKey = substr ( $normalizedKey , 3 );
}
if ( substr ( $normalizedKey , - 1 , 1 ) === " = " ) {
$normalizedEntry [ 'optional' ] = true ;
$normalizedKey = substr ( $normalizedKey , 0 , - 1 );
}
$normalizedEntries [ $normalizedKey ] = $normalizedEntry ;
}
foreach ( $rF -> getParameters () as $parameter ) {
if ( $this -> skipUndefinedParams && ! isset ( $normalizedEntries [ $parameter -> getName ()])) {
continue ;
}
$this -> assertParameter ( $normalizedEntries [ $parameter -> getName ()], $parameter , $functionName );
}
}
/**
*
* @ param array { byRef : bool , refMode : 'rw' | 'w' , variadic : bool , optional : bool , type : string } $normalizedEntry
*/
private function assertParameter ( array $normalizedEntry , ReflectionParameter $param , string $functionName ) : void
{
2022-06-14 15:16:13 +02:00
if ( in_array ( $functionName , $this -> ignoredFunctions )) {
$this -> markTestSkipped ( 'Function is ignored in config' );
}
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-13 15:10:23 +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-14 15:16:13 +02:00
} catch ( \Throwable $t ) {
$this -> markTestSkipped ( $t -> getMessage ());
}
2022-06-13 15:10:23 +02:00
$expectedType = $param -> getType ();
2022-06-14 15:16:13 +02:00
2022-06-13 15:10:23 +02:00
if ( $expectedType instanceof ReflectionNamedType ) {
$this -> assertTypeValidity ( $expectedType , $normalizedEntry [ 'type' ], " Param ' { $name } ' has incorrect type " );
} else {
// $this->markTestSkipped('Only simple named types are tested');
}
}
/**
* Since string equality is too strict , we do some extra checking here
*/
private function assertTypeValidity ( ReflectionNamedType $reflected , string $specified , string $message ) : void
{
2022-06-14 15:16:13 +02:00
// In case reflection returns mixed we assume any type specified in the callmap is more specific and correct
if ( $reflected -> getName () === 'mixed' ) {
return ;
}
if ( $reflected -> getName () === 'callable' && $reflected -> allowsNull ()
&& preg_match ( '/^(null\|callable\(.*\):.*|callable\(.*\):.*\|null)$/' , $specified )
) {
return ;
}
2022-06-13 15:10:23 +02:00
// Trim leading namespace separator
$specified = ltrim ( $specified , " \\ " );
2022-06-14 15:16:13 +02:00
if ( $reflected -> getName () === 'array' && ! $reflected -> allowsNull ()) {
if ( preg_match ( '/^(array|list|non-empty-array)(<.*>|{.*})?$/' , $specified )
|| in_array ( $specified , [ 'string[]|int[]' ])
) {
return ;
}
} elseif ( $reflected -> getName () === 'array' ) {
// Optional array
if ( preg_match ( '/^((array|list|non-empty-array)(<.*>|{.*})?\|null|null\|(array|list|non-empty-array)(<.*>|{.*})?)$/' , $specified )) {
return ;
}
2022-06-13 15:10:23 +02:00
}
2022-06-14 15:16:13 +02:00
if ( $reflected -> getName () === 'float' && in_array ( $specified , [ 'int|float' , 'float|int' ])) {
2022-06-13 15:10:23 +02:00
return ;
}
2022-06-13 15:29:21 +02:00
if ( $reflected -> getName () === 'bool' && in_array ( $specified , [ 'true' , 'false' ])) {
return ;
}
if ( $reflected -> getName () === 'callable' && preg_match ( '/^callable\(/' , $specified )) {
return ;
}
if ( $reflected -> getName () === 'string' && $specified === '?string' && $reflected -> allowsNull ()) {
return ;
}
if ( $reflected -> getName () === 'string' && in_array ( $specified , [ 'class-string' , 'numeric-string' , 'string' ])) {
return ;
}
2022-06-14 15:16:13 +02:00
if ( $reflected -> getName () === 'int' && preg_match ( '/^(\d+|positive-int|int(<\d+,\d+>))?(\|(\d+|positive-int|int))*$/' , $specified )) {
// in_array($specified , [
// 'positive-int', 'int', '0|positive-int', '256|512|1024|16384', '1|2|3|4|5|6|7'
// ])) {
2022-06-13 15:29:21 +02:00
return ;
}
2022-06-13 15:10:23 +02:00
2022-06-14 15:16:13 +02:00
2022-06-13 15:10:23 +02:00
if ( $reflected -> allowsNull ()) {
2022-06-14 15:16:13 +02:00
$escaped = preg_quote ( $reflected -> getName ());
$this -> assertMatchesRegularExpression ( " /^( \ ? { $escaped } | { $escaped } \ |null|null \ | { $escaped } ) $ / " , $specified , $message );
2022-06-13 15:10:23 +02:00
return ;
}
2022-06-14 15:16:13 +02:00
if ( $this -> skipResources && $specified === 'resource' ) {
return ;
}
2022-06-13 15:29:21 +02:00
$this -> assertEqualsIgnoringCase ( $reflected -> getName (), $specified , $message );
2022-06-13 15:10:23 +02:00
}
2021-08-08 10:39:54 +02:00
}