Skip to content

Commit ac94583

Browse files
committed
Add callmap generator
1 parent e2d1e3a commit ac94583

File tree

2 files changed

+186
-6
lines changed

2 files changed

+186
-6
lines changed

bin/gen_callmap.php

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
// Written by SamMousa in https://github.com/vimeo/psalm/issues/8101, finalized by @danog
6+
7+
require 'vendor/autoload.php';
8+
9+
use DG\BypassFinals;
10+
use Psalm\Internal\Analyzer\ProjectAnalyzer;
11+
use Psalm\Internal\Codebase\Reflection;
12+
use Psalm\Internal\Provider\FileProvider;
13+
use Psalm\Internal\Provider\Providers;
14+
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
15+
use Psalm\Tests\TestConfig;
16+
use Psalm\Type;
17+
18+
/**
19+
* Returns the correct reflection type for function or method name.
20+
*/
21+
function getReflectionFunction(string $functionName): ?ReflectionFunctionAbstract
22+
{
23+
try {
24+
if (strpos($functionName, '::') !== false) {
25+
return new ReflectionMethod($functionName);
26+
}
27+
28+
/** @var callable-string $functionName */
29+
return new ReflectionFunction($functionName);
30+
} catch (ReflectionException $e) {
31+
return null;
32+
}
33+
}
34+
35+
/**
36+
* @param array<string, string> $entryParameters
37+
*/
38+
function assertEntryParameters(ReflectionFunctionAbstract $function, array &$entryParameters): void
39+
{
40+
assertEntryReturnType($function, $entryParameters[0]);
41+
/**
42+
* Parse the parameter names from the map.
43+
*
44+
* @var array<string, array{byRef: bool, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string}>
45+
*/
46+
$normalizedEntries = [];
47+
48+
foreach ($entryParameters as $key => $entry) {
49+
if ($key === 0) {
50+
continue;
51+
}
52+
$normalizedKey = $key;
53+
/**
54+
* @var array{byRef: bool, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string} $normalizedEntry
55+
*/
56+
$normalizedEntry = [
57+
'variadic' => false,
58+
'byRef' => false,
59+
'optional' => false,
60+
'type' => $entry,
61+
];
62+
if (strncmp($normalizedKey, '&', 1) === 0) {
63+
$normalizedEntry['byRef'] = true;
64+
$normalizedKey = substr($normalizedKey, 1);
65+
}
66+
67+
if (strncmp($normalizedKey, '...', 3) === 0) {
68+
$normalizedEntry['variadic'] = true;
69+
$normalizedKey = substr($normalizedKey, 3);
70+
}
71+
72+
// Read the reference mode
73+
if ($normalizedEntry['byRef']) {
74+
$parts = explode('_', $normalizedKey, 2);
75+
if (count($parts) === 2) {
76+
if (!($parts[0] === 'rw' || $parts[0] === 'w' || $parts[0] === 'r')) {
77+
throw new InvalidArgumentException('Invalid refMode: '.$parts[0]);
78+
}
79+
$normalizedEntry['refMode'] = $parts[0];
80+
$normalizedKey = $parts[1];
81+
} else {
82+
$normalizedEntry['refMode'] = 'rw';
83+
}
84+
}
85+
86+
// Strip prefixes.
87+
if (substr($normalizedKey, -1, 1) === "=") {
88+
$normalizedEntry['optional'] = true;
89+
$normalizedKey = substr($normalizedKey, 0, -1);
90+
}
91+
92+
$normalizedEntry['name'] = $normalizedKey;
93+
$normalizedEntries[$normalizedKey] = $normalizedEntry;
94+
}
95+
}
96+
97+
/**
98+
* @param array{byRef: bool, name?: string, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string} $normalizedEntry
99+
*/
100+
function assertParameter(array &$normalizedEntry, ReflectionParameter $param): void
101+
{
102+
$name = $param->getName();
103+
104+
$expectedType = $param->getType();
105+
106+
if (isset($expectedType) && !empty($normalizedEntry['type'])) {
107+
assertTypeValidity($expectedType, $normalizedEntry['type'], "Param '{$name}'");
108+
}
109+
}
110+
111+
function assertEntryReturnType(ReflectionFunctionAbstract $function, string &$entryReturnType): void
112+
{
113+
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
114+
$expectedType = $function->hasTentativeReturnType() ? $function->getTentativeReturnType() : $function->getReturnType();
115+
} else {
116+
$expectedType = $function->getReturnType();
117+
}
118+
119+
if ($expectedType !== null) {
120+
assertTypeValidity($expectedType, $entryReturnType, 'Return');
121+
}
122+
}
123+
124+
/**
125+
* Since string equality is too strict, we do some extra checking here
126+
*/
127+
function assertTypeValidity(ReflectionType $reflected, string &$specified, string $msgPrefix): void
128+
{
129+
$expectedType = Reflection::getPsalmTypeFromReflectionType($reflected);
130+
$callMapType = Type::parseString($specified === '' ? 'mixed' : $specified);
131+
132+
$codebase = ProjectAnalyzer::getInstance()->getCodebase();
133+
try {
134+
if (!UnionTypeComparator::isContainedBy($codebase, $callMapType, $expectedType, false, false, null, false, false) && !str_contains($specified, 'static')) {
135+
$specified = $expectedType->getId(true);
136+
}
137+
} catch (Throwable) {
138+
}
139+
140+
// Reflection::getPsalmTypeFromReflectionType adds |null to mixed types so skip comparison
141+
/*if (!$expectedType->hasMixed()) {
142+
$this->assertSame($expectedType->isNullable(), $callMapType->isNullable(), "{$msgPrefix} type '{$specified}' missing null from reflected type '{$reflected}'");
143+
//$this->assertSame($expectedType->hasBool(), $callMapType->hasBool(), "{$msgPrefix} type '{$specified}' missing bool from reflected type '{$reflected}'");
144+
$this->assertSame($expectedType->hasArray(), $callMapType->hasArray(), "{$msgPrefix} type '{$specified}' missing array from reflected type '{$reflected}'");
145+
$this->assertSame($expectedType->hasInt(), $callMapType->hasInt(), "{$msgPrefix} type '{$specified}' missing int from reflected type '{$reflected}'");
146+
$this->assertSame($expectedType->hasFloat(), $callMapType->hasFloat(), "{$msgPrefix} type '{$specified}' missing float from reflected type '{$reflected}'");
147+
}*/
148+
}
149+
150+
BypassFinals::enable();
151+
152+
function writeCallMap(string $file, array $callMap) {
153+
file_put_contents($file, '<?php // phpcs:ignoreFile
154+
namespace Phan\Language\Internal;
155+
156+
return '.var_export($callMap, true).';');
157+
}
158+
159+
new ProjectAnalyzer(new TestConfig, new Providers(new FileProvider));
160+
$callMap = require "dictionaries/CallMap.php";
161+
$orig = $callMap;
162+
163+
$codebase = ProjectAnalyzer::getInstance()->getCodebase();
164+
165+
foreach ($callMap as $functionName => &$entry) {
166+
$refl = getReflectionFunction($functionName);
167+
if (!$refl) {
168+
continue;
169+
}
170+
assertEntryParameters($refl, $entry);
171+
} unset($entry);
172+
173+
writeCallMap("dictionaries/CallMap.php", $callMap);
174+
175+
$diffFile = "dictionaries/CallMap_84_delta.php";
176+
177+
$diff = require $diffFile;
178+
179+
foreach ($callMap as $functionName => $entry) {
180+
if ($orig[$functionName] !== $entry) {
181+
$diff['changed'][$functionName]['old'] = $orig[$functionName];
182+
$diff['changed'][$functionName]['new'] = $entry;
183+
}
184+
}
185+
186+
writeCallMap($diffFile, $diff);

tests/Internal/Codebase/InternalCallMapHandlerTest.php

-6
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ class InternalCallMapHandlerTest extends TestCase
195195
'datetime::settimezone' => ['8.1', '8.2', '8.3', '8.4'], // DateTime does not contain static
196196
'datetime::sub' => ['8.1', '8.2', '8.3', '8.4'], // DateTime does not contain static
197197
'datetimeimmutable::createfrominterface',
198-
'fiber::getcurrent',
199198
'filteriterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
200199
'get_cfg_var', // Ignore array return type
201200
'infiniteiterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
@@ -208,13 +207,8 @@ class InternalCallMapHandlerTest extends TestCase
208207
'locale::getscript' => ['8.1', '8.2', '8.3', '8.4'],
209208
'locale::parselocale' => ['8.1', '8.2', '8.3', '8.4'],
210209
'messageformatter::create' => ['8.1', '8.2', '8.3', '8.4'],
211-
'multipleiterator::current' => ['8.1', '8.2', '8.3', '8.4'],
212210
'mysqli::get_charset' => ['8.1', '8.2', '8.3', '8.4'],
213-
'mysqli_stmt::get_warnings' => ['8.1', '8.2', '8.3', '8.4'],
214-
'mysqli_stmt_get_warnings',
215-
'mysqli_stmt_insert_id',
216211
'norewinditerator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
217-
'passthru',
218212
'recursivecachingiterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
219213
'recursivecallbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],
220214
'recursivefilteriterator::getinneriterator' => ['8.1', '8.2', '8.3', '8.4'],

0 commit comments

Comments
 (0)