Skip to content

Commit

Permalink
Add callmap generator
Browse files Browse the repository at this point in the history
  • Loading branch information
danog committed Nov 26, 2024
1 parent e2d1e3a commit ac94583
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 6 deletions.
186 changes: 186 additions & 0 deletions bin/gen_callmap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

// Written by SamMousa in https://github.com/vimeo/psalm/issues/8101, finalized by @danog

require 'vendor/autoload.php';

use DG\BypassFinals;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Codebase\Reflection;
use Psalm\Internal\Provider\FileProvider;
use Psalm\Internal\Provider\Providers;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Tests\TestConfig;
use Psalm\Type;

/**
* Returns the correct reflection type for function or method name.
*/
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;
}
}

/**
* @param array<string, string> $entryParameters
*/
function assertEntryParameters(ReflectionFunctionAbstract $function, array &$entryParameters): void
{
assertEntryReturnType($function, $entryParameters[0]);
/**
* Parse the parameter names from the map.
*
* @var array<string, array{byRef: bool, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string}>
*/
$normalizedEntries = [];

foreach ($entryParameters as $key => $entry) {
if ($key === 0) {
continue;
}
$normalizedKey = $key;
/**
* @var array{byRef: bool, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string} $normalizedEntry
*/
$normalizedEntry = [
'variadic' => false,
'byRef' => false,
'optional' => false,
'type' => $entry,
];
if (strncmp($normalizedKey, '&', 1) === 0) {
$normalizedEntry['byRef'] = true;
$normalizedKey = substr($normalizedKey, 1);
}

if (strncmp($normalizedKey, '...', 3) === 0) {
$normalizedEntry['variadic'] = true;
$normalizedKey = substr($normalizedKey, 3);
}

// Read the reference mode
if ($normalizedEntry['byRef']) {
$parts = explode('_', $normalizedKey, 2);
if (count($parts) === 2) {
if (!($parts[0] === 'rw' || $parts[0] === 'w' || $parts[0] === 'r')) {
throw new InvalidArgumentException('Invalid refMode: '.$parts[0]);
}
$normalizedEntry['refMode'] = $parts[0];
$normalizedKey = $parts[1];
} else {
$normalizedEntry['refMode'] = 'rw';
}
}

// Strip prefixes.
if (substr($normalizedKey, -1, 1) === "=") {
$normalizedEntry['optional'] = true;
$normalizedKey = substr($normalizedKey, 0, -1);
}

$normalizedEntry['name'] = $normalizedKey;
$normalizedEntries[$normalizedKey] = $normalizedEntry;
}
}

/**
* @param array{byRef: bool, name?: string, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string} $normalizedEntry
*/
function assertParameter(array &$normalizedEntry, ReflectionParameter $param): void
{
$name = $param->getName();

$expectedType = $param->getType();

if (isset($expectedType) && !empty($normalizedEntry['type'])) {
assertTypeValidity($expectedType, $normalizedEntry['type'], "Param '{$name}'");
}
}

function assertEntryReturnType(ReflectionFunctionAbstract $function, string &$entryReturnType): void
{
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
$expectedType = $function->hasTentativeReturnType() ? $function->getTentativeReturnType() : $function->getReturnType();
} else {
$expectedType = $function->getReturnType();
}

if ($expectedType !== null) {
assertTypeValidity($expectedType, $entryReturnType, 'Return');
}
}

/**
* Since string equality is too strict, we do some extra checking here
*/
function assertTypeValidity(ReflectionType $reflected, string &$specified, string $msgPrefix): void
{
$expectedType = Reflection::getPsalmTypeFromReflectionType($reflected);
$callMapType = Type::parseString($specified === '' ? 'mixed' : $specified);

$codebase = ProjectAnalyzer::getInstance()->getCodebase();
try {
if (!UnionTypeComparator::isContainedBy($codebase, $callMapType, $expectedType, false, false, null, false, false) && !str_contains($specified, 'static')) {
$specified = $expectedType->getId(true);
}
} catch (Throwable) {
}

// Reflection::getPsalmTypeFromReflectionType adds |null to mixed types so skip comparison
/*if (!$expectedType->hasMixed()) {
$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}'");
}*/
}

BypassFinals::enable();

function writeCallMap(string $file, array $callMap) {
file_put_contents($file, '<?php // phpcs:ignoreFile
namespace Phan\Language\Internal;
return '.var_export($callMap, true).';');
}

new ProjectAnalyzer(new TestConfig, new Providers(new FileProvider));
$callMap = require "dictionaries/CallMap.php";
$orig = $callMap;

$codebase = ProjectAnalyzer::getInstance()->getCodebase();

foreach ($callMap as $functionName => &$entry) {
$refl = getReflectionFunction($functionName);
if (!$refl) {
continue;
}
assertEntryParameters($refl, $entry);
} unset($entry);

writeCallMap("dictionaries/CallMap.php", $callMap);

$diffFile = "dictionaries/CallMap_84_delta.php";

$diff = require $diffFile;

foreach ($callMap as $functionName => $entry) {
if ($orig[$functionName] !== $entry) {
$diff['changed'][$functionName]['old'] = $orig[$functionName];
$diff['changed'][$functionName]['new'] = $entry;
}
}

writeCallMap($diffFile, $diff);
6 changes: 0 additions & 6 deletions tests/Internal/Codebase/InternalCallMapHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ class InternalCallMapHandlerTest extends TestCase
'datetime::settimezone' => ['8.1', '8.2', '8.3', '8.4'], // DateTime does not contain static
'datetime::sub' => ['8.1', '8.2', '8.3', '8.4'], // DateTime does not contain static
'datetimeimmutable::createfrominterface',
'fiber::getcurrent',
'filteriterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
'get_cfg_var', // Ignore array return type
'infiniteiterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
Expand All @@ -208,13 +207,8 @@ class InternalCallMapHandlerTest extends TestCase
'locale::getscript' => ['8.1', '8.2', '8.3', '8.4'],
'locale::parselocale' => ['8.1', '8.2', '8.3', '8.4'],
'messageformatter::create' => ['8.1', '8.2', '8.3', '8.4'],
'multipleiterator::current' => ['8.1', '8.2', '8.3', '8.4'],
'mysqli::get_charset' => ['8.1', '8.2', '8.3', '8.4'],
'mysqli_stmt::get_warnings' => ['8.1', '8.2', '8.3', '8.4'],
'mysqli_stmt_get_warnings',
'mysqli_stmt_insert_id',
'norewinditerator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
'passthru',
'recursivecachingiterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
'recursivecallbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
'recursivefilteriterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
Expand Down

0 comments on commit ac94583

Please sign in to comment.