Core composer packages update

This commit is contained in:
Clemens Schwaighofer
2023-08-22 13:04:19 +09:00
parent 53192da571
commit 5b8e4e4e3e
74 changed files with 2297 additions and 1737 deletions

View File

@@ -39,6 +39,9 @@
"symfony/console": "^4.1.6 || ^5.0 || ^6.0",
"symfony/filesystem": "^5.4 || ^6.0"
},
"conflict": {
"nikic/php-parser": "4.17.0"
},
"provide": {
"psalm/psalm": "self.version"
},

View File

@@ -1679,10 +1679,10 @@ return [
'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'qualifiedName'=>'string'],
'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespace'=>'?string', 'localName'=>'string'],
'DOMDocument::importNode' => ['DOMNode|false', 'node'=>'DOMNode', 'deep='=>'bool'],
'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'],
'DOMDocument::load' => ['bool', 'filename'=>'string', 'options='=>'int'],
'DOMDocument::loadHTML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'],
'DOMDocument::loadHTMLFile' => ['bool', 'filename'=>'string', 'options='=>'int'],
'DOMDocument::loadXML' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'],
'DOMDocument::loadXML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'],
'DOMDocument::normalizeDocument' => ['void'],
'DOMDocument::registerNodeClass' => ['bool', 'baseClass'=>'string', 'extendedClass'=>'?string'],
'DOMDocument::relaxNGValidate' => ['bool', 'filename'=>'string'],
@@ -7825,10 +7825,10 @@ return [
'mysqli::begin_transaction' => ['bool', 'flags='=>'int', 'name='=>'?string'],
'mysqli::change_user' => ['bool', 'username'=>'string', 'password'=>'string', 'database'=>'?string'],
'mysqli::character_set_name' => ['string'],
'mysqli::close' => ['bool'],
'mysqli::close' => ['true'],
'mysqli::commit' => ['bool', 'flags='=>'int', 'name='=>'?string'],
'mysqli::connect' => ['bool', 'hostname='=>'string|null', 'username='=>'string|null', 'password='=>'string|null', 'database='=>'string|null', 'port='=>'int|null', 'socket='=>'string|null'],
'mysqli::debug' => ['bool', 'options'=>'string'],
'mysqli::debug' => ['true', 'options'=>'string'],
'mysqli::dump_debug_info' => ['bool'],
'mysqli::escape_string' => ['string', 'string'=>'string'],
'mysqli::execute_query' => ['mysqli_result|bool', 'query'=>'non-empty-string', 'params='=>'list<mixed>|null'],
@@ -7857,7 +7857,7 @@ return [
'mysqli::select_db' => ['bool', 'database'=>'string'],
'mysqli::set_charset' => ['bool', 'charset'=>'string'],
'mysqli::set_opt' => ['bool', 'option'=>'int', 'value'=>'string|int'],
'mysqli::ssl_set' => ['bool', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'],
'mysqli::ssl_set' => ['true', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'],
'mysqli::stat' => ['string|false'],
'mysqli::stmt_init' => ['mysqli_stmt'],
'mysqli::store_result' => ['mysqli_result|false', 'mode='=>'int'],
@@ -7903,7 +7903,7 @@ return [
'mysqli_fetch_object' => ['object|false|null', 'result'=>'mysqli_result', 'class='=>'string', 'constructor_args='=>'array'],
'mysqli_fetch_row' => ['list<null|int|float|string>|false|null', 'result'=>'mysqli_result'],
'mysqli_field_count' => ['int', 'mysql'=>'mysqli'],
'mysqli_field_seek' => ['bool', 'result'=>'mysqli_result', 'index'=>'int'],
'mysqli_field_seek' => ['true', 'result'=>'mysqli_result', 'index'=>'int'],
'mysqli_field_tell' => ['int', 'result'=>'mysqli_result'],
'mysqli_free_result' => ['void', 'result'=>'mysqli_result'],
'mysqli_get_cache_stats' => ['array|false'],
@@ -7957,7 +7957,7 @@ return [
'mysqli_result::fetch_fields' => ['list<object{name:non-empty-string,orgname:string,table:string,orgtable:string,max_length:int,length:int,charsetnr:int,flags:int,type:int,decimals:int,db:string,def:string,catalog:string}>'],
'mysqli_result::fetch_object' => ['object|false|null', 'class='=>'string', 'constructor_args='=>'array'],
'mysqli_result::fetch_row' => ['list<null|int|float|string>|false|null'],
'mysqli_result::field_seek' => ['bool', 'index'=>'int'],
'mysqli_result::field_seek' => ['true', 'index'=>'int'],
'mysqli_result::free' => ['void'],
'mysqli_result::free_result' => ['void'],
'mysqli_rollback' => ['bool', 'mysql'=>'mysqli', 'flags='=>'int', 'name='=>'?string'],
@@ -7981,7 +7981,7 @@ return [
'mysqli_stmt::attr_set' => ['bool', 'attribute'=>'int', 'value'=>'int'],
'mysqli_stmt::bind_param' => ['bool', 'types'=>'string', '&var'=>'mixed', '&...vars='=>'mixed'],
'mysqli_stmt::bind_result' => ['bool', '&w_var1'=>'', '&...w_vars='=>''],
'mysqli_stmt::close' => ['bool'],
'mysqli_stmt::close' => ['true'],
'mysqli_stmt::data_seek' => ['void', 'offset'=>'int'],
'mysqli_stmt::execute' => ['bool', 'params='=>'list<mixed>|null'],
'mysqli_stmt::fetch' => ['bool|null'],

View File

@@ -100,6 +100,22 @@ return [
'old' => ['DOMNodeList', 'namespace'=>'string', 'localName'=>'string'],
'new' => ['DOMNodeList', 'namespace'=>'?string', 'localName'=>'string'],
],
'DOMDocument::load' => [
'old' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'],
'new' => ['bool', 'filename'=>'string', 'options='=>'int'],
],
'DOMDocument::loadXML' => [
'old' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'],
'new' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'],
],
'DOMDocument::loadHTML' => [
'old' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'],
'new' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'],
],
'DOMDocument::loadHTMLFile' => [
'old' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'],
'new' => ['bool', 'filename'=>'string', 'options='=>'int'],
],
'DOMImplementation::createDocument' => [
'old' => ['DOMDocument|false', 'namespace='=>'string', 'qualifiedName='=>'string', 'doctype='=>'DOMDocumentType'],
'new' => ['DOMDocument|false', 'namespace='=>'?string', 'qualifiedName='=>'string', 'doctype='=>'?DOMDocumentType'],
@@ -224,6 +240,14 @@ return [
'old' => ['string', 'locale'=>'string', 'displayLocale='=>'string'],
'new' => ['string', 'locale'=>'string', 'displayLocale='=>'?string'],
],
'mysqli_field_seek' => [
'old' => ['bool', 'result'=>'mysqli_result', 'index'=>'int'],
'new' => ['true', 'result'=>'mysqli_result', 'index'=>'int'],
],
'mysqli_result::field_seek' => [
'old' => ['bool', 'index'=>'int'],
'new' => ['true', 'index'=>'int'],
],
'mysqli_stmt::__construct' => [
'old' => ['void', 'mysql'=>'mysqli', 'query='=>'string'],
'new' => ['void', 'mysql'=>'mysqli', 'query='=>'?string'],

View File

@@ -94,6 +94,10 @@ return [
'old' => ['int|false', 'fields'=>'array<array-key, null|scalar|Stringable>', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'],
'new' => ['int|false', 'fields'=>'array<array-key, null|scalar|Stringable>', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string', 'eol='=>'string'],
],
'hash_pbkdf2' => [
'old' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'],
'new' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'],
],
'finfo_buffer' => [
'old' => ['string|false', 'finfo'=>'resource', 'string'=>'string', 'flags='=>'int', 'context='=>'resource'],
'new' => ['string|false', 'finfo'=>'finfo', 'string'=>'string', 'flags='=>'int', 'context='=>'resource'],

View File

@@ -49,10 +49,6 @@ return [
'old' => ['bool', '&rw_array'=>'array', 'flags='=>'int'],
'new' => ['true', '&rw_array'=>'array', 'flags='=>'int'],
],
'hash_pbkdf2' => [
'old' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'],
'new' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'],
],
'imap_setflag_full' => [
'old' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'],
'new' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'],

View File

@@ -733,8 +733,8 @@ return [
'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespace'=>'string', 'localName'=>'string'],
'DOMDocument::importNode' => ['DOMNode|false', 'node'=>'DOMNode', 'deep='=>'bool'],
'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'],
'DOMDocument::loadHTML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'],
'DOMDocument::loadHTMLFile' => ['bool', 'filename'=>'string', 'options='=>'int'],
'DOMDocument::loadHTML' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'],
'DOMDocument::loadHTMLFile' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'],
'DOMDocument::loadXML' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'],
'DOMDocument::normalizeDocument' => ['void'],
'DOMDocument::registerNodeClass' => ['bool', 'baseClass'=>'string', 'extendedClass'=>'?string'],
@@ -12737,10 +12737,10 @@ return [
'mysqli::begin_transaction' => ['bool', 'flags='=>'int', 'name='=>'string'],
'mysqli::change_user' => ['bool', 'username'=>'string', 'password'=>'string', 'database'=>'?string'],
'mysqli::character_set_name' => ['string'],
'mysqli::close' => ['bool'],
'mysqli::close' => ['true'],
'mysqli::commit' => ['bool', 'flags='=>'int', 'name='=>'string'],
'mysqli::connect' => ['null|false', 'hostname='=>'string', 'username='=>'string', 'password='=>'string', 'database='=>'string', 'port='=>'int', 'socket='=>'string'],
'mysqli::debug' => ['bool', 'options'=>'string'],
'mysqli::debug' => ['true', 'options'=>'string'],
'mysqli::dump_debug_info' => ['bool'],
'mysqli::escape_string' => ['string', 'string'=>'string'],
'mysqli::get_charset' => ['object'],
@@ -12768,7 +12768,7 @@ return [
'mysqli::select_db' => ['bool', 'database'=>'string'],
'mysqli::set_charset' => ['bool', 'charset'=>'string'],
'mysqli::set_opt' => ['bool', 'option'=>'int', 'value'=>'string|int'],
'mysqli::ssl_set' => ['bool', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'],
'mysqli::ssl_set' => ['true', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'],
'mysqli::stat' => ['string|false'],
'mysqli::stmt_init' => ['mysqli_stmt'],
'mysqli::store_result' => ['mysqli_result|false', 'mode='=>'int'],
@@ -12889,7 +12889,7 @@ return [
'mysqli_stmt::attr_set' => ['bool', 'attribute'=>'int', 'value'=>'int'],
'mysqli_stmt::bind_param' => ['bool', 'types'=>'string', '&var'=>'mixed', '&...vars='=>'mixed'],
'mysqli_stmt::bind_result' => ['bool', '&w_var1'=>'', '&...w_vars='=>''],
'mysqli_stmt::close' => ['bool'],
'mysqli_stmt::close' => ['true'],
'mysqli_stmt::data_seek' => ['void', 'offset'=>'int'],
'mysqli_stmt::execute' => ['bool'],
'mysqli_stmt::fetch' => ['bool|null'],

View File

@@ -699,6 +699,7 @@ final class Codebase
);
}
/** @psalm-mutation-free */
public function classExtendsOrImplements(string $fq_class_name, string $possible_parent): bool
{
return $this->classlikes->classExtends($fq_class_name, $possible_parent)

View File

@@ -42,7 +42,6 @@ use Psalm\Progress\Progress;
use Psalm\Progress\VoidProgress;
use RuntimeException;
use SimpleXMLElement;
use SimpleXMLIterator;
use Symfony\Component\Filesystem\Path;
use Throwable;
use UnexpectedValueException;
@@ -728,8 +727,6 @@ class Config
$this->eventDispatcher = new EventDispatcher();
$this->universal_object_crates = [
strtolower(stdClass::class),
strtolower(SimpleXMLElement::class),
strtolower(SimpleXMLIterator::class),
];
}
@@ -1030,7 +1027,6 @@ class Config
/**
* @param non-empty-string $file_contents
* @psalm-suppress MixedMethodCall
* @psalm-suppress MixedAssignment
* @psalm-suppress MixedArgument
* @psalm-suppress MixedPropertyFetch
@@ -1161,12 +1157,13 @@ class Config
}
if (isset($config_xml['autoloader'])) {
$autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $config_xml['autoloader'];
$autoloader = (string) $config_xml['autoloader'];
$autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $autoloader;
if (!file_exists($autoloader_path)) {
// in here for legacy reasons where people put absolute paths but psalm resolved it relative
if ($config_xml['autoloader']->__toString()[0] === '/') {
$autoloader_path = $config_xml['autoloader']->__toString();
if ($autoloader[0] === '/') {
$autoloader_path = $autoloader;
}
if (!file_exists($autoloader_path)) {
@@ -1312,7 +1309,7 @@ class Config
);
}
if (isset($config_xml->fileExtensions)) {
if (isset($config_xml->fileExtensions->extension)) {
$config->file_extensions = [];
$config->loadFileExtensions($config_xml->fileExtensions->extension);
@@ -1336,7 +1333,6 @@ class Config
if (isset($config_xml->ignoreExceptions)) {
if (isset($config_xml->ignoreExceptions->class)) {
/** @var SimpleXMLElement $exception_class */
foreach ($config_xml->ignoreExceptions->class as $exception_class) {
$exception_name = (string) $exception_class['name'];
$global_attribute_text = (string) $exception_class['onlyGlobalScope'];
@@ -1347,7 +1343,6 @@ class Config
}
}
if (isset($config_xml->ignoreExceptions->classAndDescendants)) {
/** @var SimpleXMLElement $exception_class */
foreach ($config_xml->ignoreExceptions->classAndDescendants as $exception_class) {
$exception_name = (string) $exception_class['name'];
$global_attribute_text = (string) $exception_class['onlyGlobalScope'];
@@ -1401,7 +1396,6 @@ class Config
// this plugin loading system borrows heavily from etsy/phan
if (isset($config_xml->plugins)) {
if (isset($config_xml->plugins->plugin)) {
/** @var SimpleXMLElement $plugin */
foreach ($config_xml->plugins->plugin as $plugin) {
$plugin_file_name = (string) $plugin['filename'];
@@ -1413,7 +1407,6 @@ class Config
}
}
if (isset($config_xml->plugins->pluginClass)) {
/** @var SimpleXMLElement $plugin */
foreach ($config_xml->plugins->pluginClass as $plugin) {
$plugin_class_name = $plugin['class'];
// any child elements are used as plugin configuration
@@ -1429,21 +1422,23 @@ class Config
if (isset($config_xml->issueHandlers)) {
foreach ($config_xml->issueHandlers as $issue_handlers) {
/** @var SimpleXMLElement $issue_handler */
foreach ($issue_handlers->children() as $key => $issue_handler) {
if ($key === 'PluginIssue') {
$custom_class_name = (string) $issue_handler['name'];
/** @var string $key */
$config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
$issue_handler,
$base_dir,
);
} else {
/** @var string $key */
$config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
$issue_handler,
$base_dir,
);
$issue_handler_children = $issue_handlers->children();
if ($issue_handler_children) {
foreach ($issue_handler_children as $key => $issue_handler) {
if ($key === 'PluginIssue') {
$custom_class_name = (string)$issue_handler['name'];
/** @var string $key */
$config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
$issue_handler,
$base_dir,
);
} else {
/** @var string $key */
$config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
$issue_handler,
$base_dir,
);
}
}
}
}
@@ -2263,6 +2258,10 @@ class Config
$stubsDir . 'SPL.phpstub',
];
if ($codebase->analysis_php_version_id >= 7_04_00) {
$this->internal_stubs[] = $stubsDir . 'Php74.phpstub';
}
if ($codebase->analysis_php_version_id >= 8_00_00) {
$this->internal_stubs[] = $stubsDir . 'CoreGenericAttributes.phpstub';
$this->internal_stubs[] = $stubsDir . 'Php80.phpstub';

View File

@@ -389,7 +389,6 @@ class FileFilter
if ($e->directory) {
$config['directory'] = [];
/** @var SimpleXMLElement $directory */
foreach ($e->directory as $directory) {
$config['directory'][] = [
'name' => (string) $directory['name'],
@@ -402,7 +401,6 @@ class FileFilter
if ($e->file) {
$config['file'] = [];
/** @var SimpleXMLElement $file */
foreach ($e->file as $file) {
$config['file'][]['name'] = (string) $file['name'];
}
@@ -410,7 +408,6 @@ class FileFilter
if ($e->referencedClass) {
$config['referencedClass'] = [];
/** @var SimpleXMLElement $referenced_class */
foreach ($e->referencedClass as $referenced_class) {
$config['referencedClass'][]['name'] = strtolower((string)$referenced_class['name']);
}
@@ -418,7 +415,6 @@ class FileFilter
if ($e->referencedMethod) {
$config['referencedMethod'] = [];
/** @var SimpleXMLElement $referenced_method */
foreach ($e->referencedMethod as $referenced_method) {
$config['referencedMethod'][]['name'] = (string)$referenced_method['name'];
}
@@ -426,7 +422,6 @@ class FileFilter
if ($e->referencedFunction) {
$config['referencedFunction'] = [];
/** @var SimpleXMLElement $referenced_function */
foreach ($e->referencedFunction as $referenced_function) {
$config['referencedFunction'][]['name'] = strtolower((string)$referenced_function['name']);
}
@@ -434,7 +429,6 @@ class FileFilter
if ($e->referencedProperty) {
$config['referencedProperty'] = [];
/** @var SimpleXMLElement $referenced_property */
foreach ($e->referencedProperty as $referenced_property) {
$config['referencedProperty'][]['name'] = strtolower((string)$referenced_property['name']);
}
@@ -442,7 +436,6 @@ class FileFilter
if ($e->referencedConstant) {
$config['referencedConstant'] = [];
/** @var SimpleXMLElement $referenced_constant */
foreach ($e->referencedConstant as $referenced_constant) {
$config['referencedConstant'][]['name'] = strtolower((string)$referenced_constant['name']);
}
@@ -450,8 +443,6 @@ class FileFilter
if ($e->referencedVariable) {
$config['referencedVariable'] = [];
/** @var SimpleXMLElement $referenced_variable */
foreach ($e->referencedVariable as $referenced_variable) {
$config['referencedVariable'][]['name'] = strtolower((string)$referenced_variable['name']);
}

View File

@@ -38,9 +38,10 @@ final class IssueHandler
}
}
/** @var SimpleXMLElement $error_level */
foreach ($e->errorLevel as $error_level) {
$handler->custom_levels[] = ErrorLevelFileFilter::loadFromXMLElement($error_level, $base_dir, true);
if (isset($e->errorLevel)) {
foreach ($e->errorLevel as $error_level) {
$handler->custom_levels[] = ErrorLevelFileFilter::loadFromXMLElement($error_level, $base_dir, true);
}
}
return $handler;

View File

@@ -28,7 +28,6 @@ final class ProjectFileFilter extends FileFilter
throw new ConfigException('Cannot nest ignoreFiles inside itself');
}
/** @var SimpleXMLElement $e->ignoreFiles */
$filter->file_filter = static::loadFromXMLElement($e->ignoreFiles, $base_dir, false);
}

View File

@@ -12,7 +12,8 @@ use Psalm\Internal\Scope\LoopScope;
use Psalm\Internal\Type\AssertionReconciler;
use Psalm\Storage\FunctionLikeStorage;
use Psalm\Type\Atomic\DependentType;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Union;
use RuntimeException;
@@ -868,10 +869,19 @@ final class Context
public function defineGlobals(): void
{
$globals = [
// not sure why this is declared here again, see VariableFetchAnalyzer
'$argv' => new Union([
new TArray([Type::getInt(), Type::getString()]),
Type::getNonEmptyListAtomic(Type::getString()),
new TNull(),
], [
'ignore_nullable_issues' => true,
]),
'$argc' => new Union([
new TIntRange(1, null),
new TNull(),
], [
'ignore_nullable_issues' => true,
]),
'$argc' => Type::getInt(),
];
$config = Config::getInstance();

View File

@@ -200,6 +200,10 @@ class ProjectAnalyzer
UnnecessaryVarAnnotation::class,
];
private const PHP_VERSION_REGEX = '^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\..*)?$';
private const PHP_SUPPORTED_VERSIONS_REGEX = '^(5\.[456]|7\.[01234]|8\.[0123])(\..*)?$';
/**
* @param array<ReportOptions> $generated_report_options
*/
@@ -1179,8 +1183,16 @@ class ProjectAnalyzer
*/
public function setPhpVersion(string $version, string $source): void
{
if (!preg_match('/^(5\.[456]|7\.[01234]|8\.[012])(\..*)?$/', $version)) {
throw new UnexpectedValueException('Expecting a version number in the format x.y');
if (!preg_match('/' . self::PHP_VERSION_REGEX . '/', $version)) {
throw new UnexpectedValueException('Expecting a version number in the format x.y or x.y.z');
}
if (!preg_match('/' . self::PHP_SUPPORTED_VERSIONS_REGEX . '/', $version)) {
throw new UnexpectedValueException(
'Psalm supports PHP version ">=5.4". The specified version '
. $version
. " is either not supported or doesn't exist.",
);
}
[$php_major_version, $php_minor_version] = explode('.', $version);

View File

@@ -755,20 +755,6 @@ class ForeachAnalyzer
$has_valid_iterator = true;
if ($iterator_atomic_type instanceof TNamedObject
&& strtolower($iterator_atomic_type->value) === 'simplexmlelement'
) {
$value_type = Type::combineUnionTypes(
$value_type,
new Union([$iterator_atomic_type]),
);
$key_type = Type::combineUnionTypes(
$key_type,
Type::getString(),
);
}
if ($iterator_atomic_type instanceof TIterable
|| (strtolower($iterator_atomic_type->value) === 'traversable'
|| $codebase->classImplements(

View File

@@ -29,6 +29,7 @@ use Psalm\Issue\UndefinedMethod;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
@@ -412,7 +413,7 @@ class MethodCallAnalyzer extends CallAnalyzer
$types = $class_type->getAtomicTypes();
foreach ($types as $key => &$type) {
if (!$type instanceof TNamedObject) {
if (!$type instanceof TNamedObject && !$type instanceof TObject) {
unset($types[$key]);
} else {
$type = $type->setFromDocblock(false);

View File

@@ -1737,8 +1737,12 @@ class ArrayFetchAnalyzer
?Union &$array_access_type,
bool &$has_array_access
): void {
if (strtolower($type->value) === 'simplexmlelement') {
$call_array_access_type = new Union([new TNamedObject('SimpleXMLElement')]);
$codebase = $statements_analyzer->getCodebase();
if (strtolower($type->value) === 'simplexmlelement'
|| ($codebase->classExists($type->value)
&& $codebase->classExtendsOrImplements($type->value, 'SimpleXMLElement'))
) {
$call_array_access_type = new Union([new TNull(), new TNamedObject('SimpleXMLElement')]);
} elseif (strtolower($type->value) === 'domnodelist' && $stmt->dim) {
$old_data_provider = $statements_analyzer->node_data;

View File

@@ -228,7 +228,7 @@ class AtomicPropertyFetchAnalyzer
self::handleEnumValue($statements_analyzer, $stmt, $stmt_var_type, $class_storage);
} elseif ($prop_name === 'name') {
$has_valid_fetch_type = true;
self::handleEnumName($statements_analyzer, $stmt, $lhs_type_part);
self::handleEnumName($statements_analyzer, $stmt, $stmt_var_type, $class_storage);
} else {
self::handleNonExistentProperty(
$statements_analyzer,
@@ -979,16 +979,31 @@ class AtomicPropertyFetchAnalyzer
private static function handleEnumName(
StatementsAnalyzer $statements_analyzer,
PropertyFetch $stmt,
Atomic $lhs_type_part
Union $stmt_var_type,
ClassLikeStorage $class_storage
): void {
if ($lhs_type_part instanceof TEnumCase) {
$statements_analyzer->node_data->setType(
$stmt,
new Union([Type::getAtomicStringFromLiteral($lhs_type_part->case_name)]),
);
} else {
$statements_analyzer->node_data->setType($stmt, Type::getNonEmptyString());
$relevant_enum_cases = array_filter(
$stmt_var_type->getAtomicTypes(),
static fn(Atomic $type): bool => $type instanceof TEnumCase,
);
$relevant_enum_case_names = array_map(
static fn(TEnumCase $enumCase): string => $enumCase->case_name,
$relevant_enum_cases,
);
if (empty($relevant_enum_case_names)) {
$relevant_enum_case_names = array_keys($class_storage->enum_cases);
}
$statements_analyzer->node_data->setType(
$stmt,
empty($relevant_enum_case_names)
? Type::getNonEmptyString()
: new Union(array_map(
fn(string $name): TString => Type::getAtomicStringFromLiteral($name),
$relevant_enum_case_names,
)),
);
}
private static function handleEnumValue(

View File

@@ -3,9 +3,12 @@
namespace Psalm\Internal\Analyzer\Statements\Expression;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Issue\InvalidArgument;
use Psalm\IssueBuffer;
use Psalm\Type;
/**
@@ -30,6 +33,15 @@ class IssetAnalyzer
$context->vars_in_scope[$var_id] = Type::getMixed();
$context->vars_possibly_in_scope[$var_id] = true;
}
} elseif (!self::isValidStatement($isset_var)) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Isset only works with variables and array elements',
new CodeLocation($statements_analyzer->getSource(), $isset_var),
'empty',
),
$statements_analyzer->getSuppressedIssues(),
);
}
self::analyzeIssetVar($statements_analyzer, $isset_var, $context);
@@ -49,4 +61,15 @@ class IssetAnalyzer
$context->inside_isset = false;
}
private static function isValidStatement(PhpParser\Node\Expr $stmt): bool
{
return $stmt instanceof PhpParser\Node\Expr\Variable
|| $stmt instanceof PhpParser\Node\Expr\ArrayDimFetch
|| $stmt instanceof PhpParser\Node\Expr\PropertyFetch
|| $stmt instanceof PhpParser\Node\Expr\StaticPropertyFetch
|| $stmt instanceof PhpParser\Node\Expr\NullsafePropertyFetch
|| $stmt instanceof PhpParser\Node\Expr\ClassConstFetch
|| $stmt instanceof PhpParser\Node\Expr\AssignRef;
}
}

View File

@@ -12,6 +12,7 @@ use Psalm\Exception\ConfigNotFoundException;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Report;
use RuntimeException;
use UnexpectedValueException;
use function array_filter;
use function array_key_exists;
@@ -485,7 +486,15 @@ final class CliUtils
}
if ($version !== null && $source !== null) {
$project_analyzer->setPhpVersion($version, $source);
try {
$project_analyzer->setPhpVersion($version, $source);
} catch (UnexpectedValueException $e) {
fwrite(
STDERR,
$e->getMessage() . PHP_EOL,
);
exit(1);
}
}
}

View File

@@ -597,6 +597,7 @@ class ClassLikes
/**
* Determine whether or not a class extends a parent
*
* @psalm-mutation-free
* @throws UnpopulatedClasslikeException when called on unpopulated class
* @throws InvalidArgumentException when class does not exist
*/
@@ -620,6 +621,8 @@ class ClassLikes
/**
* Check whether a class implements an interface
*
* @psalm-mutation-free
*/
public function classImplements(string $fq_class_name, string $interface): bool
{

View File

@@ -23,7 +23,6 @@ use LanguageServerProtocol\CodeDescription;
use LanguageServerProtocol\CompletionOptions;
use LanguageServerProtocol\Diagnostic;
use LanguageServerProtocol\DiagnosticSeverity;
use LanguageServerProtocol\ExecuteCommandOptions;
use LanguageServerProtocol\InitializeResult;
use LanguageServerProtocol\InitializeResultServerInfo;
use LanguageServerProtocol\LogMessage;
@@ -446,9 +445,6 @@ class LanguageServer extends Dispatcher
$serverCapabilities = new ServerCapabilities();
//The server provides execute command support.
$serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']);
$textDocumentSyncOptions = new TextDocumentSyncOptions();
//Open and close notifications are sent to the server.

View File

@@ -455,25 +455,6 @@ class TextDocument
],
]),
);
/*
$fixers["fixAll.{$diagnostic->data->type}"] = new CodeAction(
"FixAll {$diagnostic->data->type} for this file",
CodeActionKind::QUICK_FIX,
null,
null,
null,
null,
new Command(
"Fix All",
"psalm.fixall",
[
'uri' => $textDocument->uri,
'type' => $diagnostic->data->type
]
)
);
*/
}
if (empty($fixers)) {

View File

@@ -11,7 +11,6 @@ use Psalm\Internal\Provider\ReturnTypeProvider\DateTimeModifyReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DomNodeAppendChild;
use Psalm\Internal\Provider\ReturnTypeProvider\ImagickPixelColorReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\PdoStatementReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\SimpleXmlElementAsXml;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\StatementsSource;
@@ -39,7 +38,6 @@ class MethodReturnTypeProvider
$this->registerClass(DomNodeAppendChild::class);
$this->registerClass(ImagickPixelColorReturnTypeProvider::class);
$this->registerClass(SimpleXmlElementAsXml::class);
$this->registerClass(PdoStatementReturnTypeProvider::class);
$this->registerClass(ClosureFromCallableReturnTypeProvider::class);
$this->registerClass(DateTimeModifyReturnTypeProvider::class);

View File

@@ -1,34 +0,0 @@
<?php
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Union;
use function count;
/**
* @internal
*/
class SimpleXmlElementAsXml implements MethodReturnTypeProviderInterface
{
public static function getClassLikeNames(): array
{
return ['SimpleXMLElement'];
}
public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{
$call_args = $event->getCallArgs();
$method_name_lowercase = $event->getMethodNameLowercase();
if ($method_name_lowercase === 'asxml'
&& !count($call_args)
) {
return Type::parseString('string|false');
}
return null;
}
}

View File

@@ -4,6 +4,7 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider;
use ArgumentCountError;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\RedundantFunctionCall;
use Psalm\Issue\TooFewArguments;
use Psalm\Issue\TooManyArguments;
use Psalm\IssueBuffer;
@@ -25,6 +26,7 @@ use function count;
use function is_string;
use function preg_match;
use function sprintf;
use function strlen;
/**
* @internal
@@ -47,6 +49,11 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
$statements_source = $event->getStatementsSource();
$call_args = $event->getCallArgs();
// invalid - will already report an error for the params anyway
if (count($call_args) < 1) {
return null;
}
$has_splat_args = false;
$node_type_provider = $statements_source->getNodeTypeProvider();
foreach ($call_args as $call_arg) {
@@ -67,17 +74,29 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
// eventually this could be refined
// to check if it's an array with literal string as first element for further checking
if (count($call_args) === 1 && $has_splat_args === true) {
IssueBuffer::maybeAdd(
new RedundantFunctionCall(
'Using the splat operator is redundant, as v' . $event->getFunctionId()
. ' without splat operator can be used instead of ' . $event->getFunctionId(),
$event->getCodeLocation(),
),
$statements_source->getSuppressedIssues(),
);
return null;
}
// it makes no sense to use sprintf when there is only 1 arg (the format)
// as it wouldn't have any placeholders
if (count($call_args) === 1 && $event->getFunctionId() === 'sprintf') {
// if it's a literal string, we can check it further though!
$first_arg_type = $node_type_provider->getType($call_args[0]->value);
if (count($call_args) === 1
&& ($first_arg_type === null || !$first_arg_type->isSingleStringLiteral())) {
IssueBuffer::maybeAdd(
new TooFewArguments(
'Too few arguments for ' . $event->getFunctionId() . ', expecting at least 2 arguments',
new RedundantFunctionCall(
'Using ' . $event->getFunctionId()
. ' with a single argument is redundant, since there are no placeholder params to be substituted',
$event->getCodeLocation(),
$event->getFunctionId(),
),
$statements_source->getSuppressedIssues(),
);
@@ -89,7 +108,10 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
$is_falsable = true;
foreach ($call_args as $index => $call_arg) {
$type = $node_type_provider->getType($call_arg->value);
if ($type === null && $index === 0 && $event->getFunctionId() === 'printf') {
// printf only has the format validated above
// don't change the return type
break;
}
@@ -100,10 +122,9 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
if ($index === 0 && $type->isSingleStringLiteral()) {
if ($type->getSingleStringLiteral()->value === '') {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Argument 1 of ' . $event->getFunctionId() . ' must not be an empty string',
new RedundantFunctionCall(
'Calling ' . $event->getFunctionId() . ' with an empty first argument does nothing',
$event->getCodeLocation(),
$event->getFunctionId(),
),
$statements_source->getSuppressedIssues(),
);
@@ -158,17 +179,48 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
$initial_result = $result;
if ($result === $type->getSingleStringLiteral()->value) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Argument 1 of ' . $event->getFunctionId()
. ' does not contain any placeholders',
$event->getCodeLocation(),
$event->getFunctionId(),
),
$statements_source->getSuppressedIssues(),
);
if (count($call_args) > 1) {
// we need to report this here too, since we return early without further validation
// otherwise people who have suspended RedundantFunctionCall errors
// will not get an error for this
IssueBuffer::maybeAdd(
new TooManyArguments(
'Too many arguments for the number of placeholders in '
. $event->getFunctionId(),
$event->getCodeLocation(),
$event->getFunctionId(),
),
$statements_source->getSuppressedIssues(),
);
}
return null;
// the same error as above, but we have validated the pattern now
if (count($call_args) === 1) {
IssueBuffer::maybeAdd(
new RedundantFunctionCall(
'Using ' . $event->getFunctionId()
. ' with a single argument is redundant,'
. ' since there are no placeholder params to be substituted',
$event->getCodeLocation(),
),
$statements_source->getSuppressedIssues(),
);
} else {
IssueBuffer::maybeAdd(
new RedundantFunctionCall(
'Argument 1 of ' . $event->getFunctionId()
. ' does not contain any placeholders',
$event->getCodeLocation(),
),
$statements_source->getSuppressedIssues(),
);
}
if ($event->getFunctionId() === 'printf') {
return Type::getInt(false, strlen($type->getSingleStringLiteral()->value));
}
return $type;
}
}
} catch (ValueError $value_error) {

View File

@@ -323,7 +323,7 @@ abstract class Atomic implements TypeNode
return $analysis_php_version_id !== null ? new TNamedObject($value) : new TScalar();
case 'null':
if ($analysis_php_version_id === null || $analysis_php_version_id >= 8_00_00) {
if ($analysis_php_version_id === null || $analysis_php_version_id >= 7_00_00) {
return new TNull();
}

View File

@@ -424,6 +424,10 @@ class Reconciler
{
foreach ($new_types as $nk => $type) {
if (strpos($nk, '[') || strpos($nk, '->')) {
$type = array_values($type);
if (!isset($type[0][0])) {
continue;
}
if ($type[0][0] instanceof IsEqualIsset
|| $type[0][0] instanceof IsIsset
|| $type[0][0] instanceof NonEmpty

View File

@@ -1359,9 +1359,10 @@ function realpath(string $path) {}
*
* @param numeric-string $num1
* @param numeric-string $num2
* @param int|null $scale
* @return (PHP_MAJOR_VERSION is 8 ? numeric-string : ($num2 is "0" ? null : numeric-string))
*/
function bcdiv(string $num1, string $num2, int $scale = 0): ?string {}
function bcdiv(string $num1, string $num2, ?int $scale = null): ?string {}
/**
* @psalm-pure

11
vendor/vimeo/psalm/stubs/Php74.phpstub vendored Normal file
View File

@@ -0,0 +1,11 @@
<?php
/**
* @psalm-pure
*
* @psalm-taint-escape html
* @psalm-flow ($string) -> return
*
* @param null|string|array<array-key,string> $allowed_tags
*/
function strip_tags(string $string, null|string|array $allowed_tags = null) : string {}

View File

@@ -471,13 +471,13 @@ class DOMDocument extends DOMNode implements DOMParentNode
public function importNode(DOMNode $node, bool $deep = false) {}
/**
* @return DOMDocument|false
* @return bool
* @psalm-ignore-falsable-return
**/
public function load(string $filename, int $options = 0) {}
/**
* @return DOMDocument|false
* @return bool
* @psalm-ignore-falsable-return
*/
public function loadXML(string $source, int $options = 0) {}
@@ -492,10 +492,10 @@ class DOMDocument extends DOMNode implements DOMParentNode
*/
public function save(string $filename, int $options = 0) {}
/** @return DOMDocument|bool */
/** @return bool */
public function loadHTML(string $source, int $options = 0) {}
/** @return DOMDocument|bool */
/** @return bool */
public function loadHTMLFile(string $filename, int $options = 0) {}
/**

View File

@@ -25,6 +25,7 @@ function simplexml_import_dom(SimpleXMLElement|DOMNode $node, ?string $class_nam
/**
* @implements Traversable<string, SimpleXMLElement>
* @psalm-no-seal-properties
*/
class SimpleXMLElement implements Traversable, Countable
{
@@ -63,6 +64,8 @@ class SimpleXMLElement implements Traversable, Countable
public function __toString(): string {}
public function count(): int {}
public function __get(string $name): SimpleXMLElement|SimpleXMLIterator|null {}
}
/**