Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dictionaries for 8.4 #11169

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
- "8.4"
count: ${{ fromJson(needs.chunk-matrix.outputs.count) }}
chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }}

Expand All @@ -148,7 +149,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M
ini-values: zend.assertions=1, assert.exception=1
tools: composer:v2
coverage: none
extensions: none, curl, dom, filter, intl, json, libxml, mbstring, opcache, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter
Expand Down
192 changes: 192 additions & 0 deletions bin/gen_callmap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?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;
}

foreach ($function->getParameters() as $parameter) {
if (isset($normalizedEntries[$parameter->getName()])) {
assertParameter($normalizedEntries[$parameter->getName()], $parameter);
}
}
}

/**
* @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);
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"composer/xdebug-handler": "^2.0 || ^3.0",
"dnoegel/php-xdg-base-dir": "^0.1.1",
"felixfbecker/advanced-json-rpc": "^3.1",
"felixfbecker/language-server-protocol": "^1.5.2",
"felixfbecker/language-server-protocol": "^1.5.3",
"fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"nikic/php-parser": "^5.0.0",
Expand Down
Loading
Loading