composer base packages updates

This commit is contained in:
Clemens Schwaighofer
2023-03-28 11:51:54 +09:00
parent c3b29ad0d7
commit 28909fdc03
174 changed files with 7527 additions and 5286 deletions

View File

@@ -25,7 +25,7 @@
"ext-tokenizer": "*",
"amphp/amp": "^2.4.2",
"amphp/byte-stream": "^1.5",
"composer/package-versions-deprecated": "^1.10.0",
"composer-runtime-api": "^2",
"composer/semver": "^1.4 || ^2.0 || ^3.0",
"composer/xdebug-handler": "^2.0 || ^3.0",
"dnoegel/php-xdg-base-dir": "^0.1.1",
@@ -33,7 +33,7 @@
"felixfbecker/language-server-protocol": "^1.5.2",
"fidry/cpu-core-counter": "^0.4.1 || ^0.5.1",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"nikic/php-parser": "^4.13",
"nikic/php-parser": "^4.14",
"sebastian/diff": "^4.0 || ^5.0",
"spatie/array-to-xml": "^2.17.0 || ^3.0",
"symfony/console": "^4.1.6 || ^5.0 || ^6.0",
@@ -117,7 +117,7 @@
"Composer\\Config::disableProcessTimeout",
"phpunit"
],
"verify-callmap": "phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php",
"verify-callmap": "@php phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php",
"psalm": "@php ./psalm",
"psalm-set-baseline": "@php ./psalm --set-baseline=psalm-baseline.xml",
"tests": [

View File

@@ -407,6 +407,7 @@
<xs:element name="PossiblyUnusedParam" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyUnusedProperty" type="PropertyIssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyUnusedReturnValue" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PrivateFinalMethod" type="MethodIssueHandlerType" minOccurs="0" />
<xs:element name="PropertyNotSetInConstructor" type="PropertyIssueHandlerType" minOccurs="0" />
<xs:element name="PropertyTypeCoercion" type="PropertyIssueHandlerType" minOccurs="0" />
<xs:element name="RawObjectIteration" type="IssueHandlerType" minOccurs="0" />
@@ -725,6 +726,7 @@
<xs:enumeration value="ffi"/>
<xs:enumeration value="geos"/>
<xs:enumeration value="gmp"/>
<xs:enumeration value="ibm_db2"/>
<xs:enumeration value="mongodb"/>
<xs:enumeration value="mysqli"/>
<xs:enumeration value="pdo"/>

File diff suppressed because it is too large Load Diff

View File

@@ -41,9 +41,9 @@ return [
'old' => ['int|false'],
'new' => ['int'],
],
'DateTime::diff' => [
'old' => ['DateInterval|false', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'],
'new' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'],
'CURLFile::__construct' => [
'old' => ['void', 'filename'=>'string', 'mime_type='=>'string', 'posted_filename='=>'string'],
'new' => ['void', 'filename'=>'string', 'mime_type='=>'?string', 'posted_filename='=>'?string'],
],
'DateTime::format' => [
'old' => ['string|false', 'format'=>'string'],
@@ -53,10 +53,6 @@ return [
'old' => ['int|false'],
'new' => ['int'],
],
'DateTime::setTime' => [
'old' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'],
'new' => ['static', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'],
],
'DateTimeInterface::getTimestamp' => [
'old' => ['int|false'],
'new' => ['int'],
@@ -81,14 +77,58 @@ return [
'old' => ['void', 'dir_handle='=>'resource'],
'new' => ['void'],
],
'DirectoryIterator::getFileInfo' => [
'old' => ['SplFileInfo', 'class='=>'class-string'],
'new' => ['SplFileInfo', 'class='=>'?class-string'],
],
'DirectoryIterator::getPathInfo' => [
'old' => ['?SplFileInfo', 'class='=>'class-string'],
'new' => ['?SplFileInfo', 'class='=>'?class-string'],
],
'DirectoryIterator::openFile' => [
'old' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'resource'],
'new' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'?resource'],
],
'DOMDocument::getElementsByTagNameNS' => [
'old' => ['DOMNodeList', 'namespace'=>'string', 'localName'=>'string'],
'new' => ['DOMNodeList', 'namespace'=>'?string', 'localName'=>'string'],
],
'DOMImplementation::createDocument' => [
'old' => ['DOMDocument|false', 'namespace='=>'string', 'qualifiedName='=>'string', 'doctype='=>'DOMDocumentType'],
'new' => ['DOMDocument|false', 'namespace='=>'?string', 'qualifiedName='=>'string', 'doctype='=>'?DOMDocumentType'],
],
'ErrorException::__construct' => [
'old' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'line='=>'int', 'previous='=>'?Throwable'],
'new' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'?string', 'line='=>'?int', 'previous='=>'?Throwable'],
],
'FilesystemIterator::getFileInfo' => [
'old' => ['SplFileInfo', 'class='=>'class-string'],
'new' => ['SplFileInfo', 'class='=>'?class-string'],
],
'FilesystemIterator::getPathInfo' => [
'old' => ['?SplFileInfo', 'class='=>'class-string'],
'new' => ['?SplFileInfo', 'class='=>'?class-string'],
],
'FilesystemIterator::openFile' => [
'old' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'resource'],
'new' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'?resource'],
],
'finfo::__construct' => [
'old' => ['void', 'flags='=>'int', 'magic_database='=>'string'],
'new' => ['void', 'flags='=>'int', 'magic_database='=>'?string'],
],
'GlobIterator::getFileInfo' => [
'old' => ['SplFileInfo', 'class='=>'class-string'],
'new' => ['SplFileInfo', 'class='=>'?class-string'],
],
'GlobIterator::getPathInfo' => [
'old' => ['?SplFileInfo', 'class='=>'class-string'],
'new' => ['?SplFileInfo', 'class='=>'?class-string'],
],
'GlobIterator::openFile' => [
'old' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'resource'],
'new' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'?resource'],
],
'IntlDateFormatter::__construct' => [
'old' => ['void', 'locale'=>'?string', 'datetype'=>'null|int', 'timetype'=>'null|int', 'timezone='=>'IntlTimeZone|DateTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'],
'new' => ['void', 'locale'=>'?string', 'dateType'=>'int', 'timeType'=>'int', 'timezone='=>'IntlTimeZone|DateTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'],
@@ -153,6 +193,10 @@ return [
'old' => ['null|false', 'zone'=>'IntlTimeZone|DateTimeZone|string|null'],
'new' => ['null|false', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'],
],
'IntlTimeZone::getIDForWindowsID' => [
'old' => ['string|false', 'timezoneId'=>'string', 'region='=>'string'],
'new' => ['string|false', 'timezoneId'=>'string', 'region='=>'?string'],
],
'Locale::getDisplayLanguage' => [
'old' => ['string', 'locale'=>'string', 'displayLocale='=>'string'],
'new' => ['string', 'locale'=>'string', 'displayLocale='=>'?string'],
@@ -209,21 +253,17 @@ return [
'old' => ['bool', 'mode'=>'int'],
'new' => ['bool', 'mode'=>'int', '...args='=>'mixed'],
],
'PharData::compress' => [
'old' => ['?PharData', 'compression'=>'int', 'extension='=>'string'],
'new' => ['?PharData', 'compression'=>'int', 'extension='=>'?string'],
'Phar::addFile' => [
'old' => ['void', 'filename'=>'string', 'localName='=>'string'],
'new' => ['void', 'filename'=>'string', 'localName='=>'?string'],
],
'PharData::convertToData' => [
'old' => ['?PharData', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'],
'new' => ['?PharData', 'format='=>'?int', 'compression='=>'?int', 'extension='=>'?string'],
'Phar::buildFromIterator' => [
'old' => ['array|false', 'iterator'=>'Traversable', 'baseDirectory='=>'string'],
'new' => ['array|false', 'iterator'=>'Traversable', 'baseDirectory='=>'?string'],
],
'PharData::convertToExecutable' => [
'old' => ['?Phar', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'],
'new' => ['?Phar', 'format='=>'?int', 'compression='=>'?int', 'extension='=>'?string'],
],
'PharData::decompress' => [
'old' => ['?PharData', 'extension='=>'string'],
'new' => ['?PharData', 'extension='=>'?string'],
'Phar::createDefaultStub' => [
'old' => ['string', 'index='=>'string', 'webIndex='=>'string'],
'new' => ['string', 'index='=>'?string', 'webIndex='=>'?string'],
],
'Phar::compress' => [
'old' => ['?Phar', 'compression'=>'int', 'extension='=>'string'],
@@ -245,10 +285,70 @@ return [
'old' => ['mixed'],
'new' => ['mixed', 'unserializeOptions='=>'array'],
],
'Phar::setDefaultStub' => [
'old' => ['bool', 'index='=>'?string', 'webIndex='=>'string'],
'new' => ['bool', 'index='=>'?string', 'webIndex='=>'?string'],
],
'Phar::setSignatureAlgorithm' => [
'old' => ['void', 'algo'=>'int', 'privateKey='=>'string'],
'new' => ['void', 'algo'=>'int', 'privateKey='=>'?string'],
],
'Phar::webPhar' => [
'old' => ['void', 'alias='=>'?string', 'index='=>'?string', 'fileNotFoundScript='=>'string', 'mimeTypes='=>'array', 'rewrite='=>'callable'],
'new' => ['void', 'alias='=>'?string', 'index='=>'?string', 'fileNotFoundScript='=>'?string', 'mimeTypes='=>'array', 'rewrite='=>'?callable'],
],
'PharData::addFile' => [
'old' => ['void', 'filename'=>'string', 'localName='=>'string'],
'new' => ['void', 'filename'=>'string', 'localName='=>'?string'],
],
'PharData::buildFromIterator' => [
'old' => ['array|false', 'iterator'=>'Traversable', 'baseDirectory='=>'string'],
'new' => ['array|false', 'iterator'=>'Traversable', 'baseDirectory='=>'?string'],
],
'PharData::compress' => [
'old' => ['?PharData', 'compression'=>'int', 'extension='=>'string'],
'new' => ['?PharData', 'compression'=>'int', 'extension='=>'?string'],
],
'PharData::convertToData' => [
'old' => ['?PharData', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'],
'new' => ['?PharData', 'format='=>'?int', 'compression='=>'?int', 'extension='=>'?string'],
],
'PharData::convertToExecutable' => [
'old' => ['?Phar', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'],
'new' => ['?Phar', 'format='=>'?int', 'compression='=>'?int', 'extension='=>'?string'],
],
'PharData::decompress' => [
'old' => ['?PharData', 'extension='=>'string'],
'new' => ['?PharData', 'extension='=>'?string'],
],
'PharData::setDefaultStub' => [
'old' => ['bool', 'index='=>'?string', 'webIndex='=>'string'],
'new' => ['bool', 'index='=>'?string', 'webIndex='=>'?string'],
],
'PharData::setSignatureAlgorithm' => [
'old' => ['void', 'algo'=>'int', 'privateKey='=>'string'],
'new' => ['void', 'algo'=>'int', 'privateKey='=>'?string'],
],
'PharFileInfo::getMetadata' => [
'old' => ['mixed'],
'new' => ['mixed', 'unserializeOptions='=>'array'],
],
'PharFileInfo::isCompressed' => [
'old' => ['bool', 'compression='=>'int'],
'new' => ['bool', 'compression='=>'?int'],
],
'RecursiveDirectoryIterator::getFileInfo' => [
'old' => ['SplFileInfo', 'class='=>'class-string'],
'new' => ['SplFileInfo', 'class='=>'?class-string'],
],
'RecursiveDirectoryIterator::getPathInfo' => [
'old' => ['?SplFileInfo', 'class='=>'class-string'],
'new' => ['?SplFileInfo', 'class='=>'?class-string'],
],
'RecursiveDirectoryIterator::openFile' => [
'old' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'resource'],
'new' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'?resource'],
],
'RecursiveIteratorIterator::getSubIterator' => [
'old' => ['?RecursiveIterator', 'level='=>'int'],
'new' => ['?RecursiveIterator', 'level='=>'?int'],
@@ -282,36 +382,36 @@ return [
'new' => ['mixed', 'object='=>'null|object'],
],
'SplFileInfo::getFileInfo' => [
'old' => ['SplFileInfo', 'class='=>'string'],
'new' => ['SplFileInfo', 'class='=>'?string'],
'old' => ['SplFileInfo', 'class='=>'class-string'],
'new' => ['SplFileInfo', 'class='=>'?class-string'],
],
'SplFileInfo::getPathInfo' => [
'old' => ['SplFileInfo|null', 'class='=>'string'],
'new' => ['SplFileInfo|null', 'class='=>'?string'],
'old' => ['SplFileInfo|null', 'class='=>'class-string'],
'new' => ['SplFileInfo|null', 'class='=>'?class-string'],
],
'SplFileInfo::openFile' => [
'old' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'resource'],
'new' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'?resource'],
],
'SplFileObject::getFileInfo' => [
'old' => ['SplFileInfo', 'class='=>'string'],
'new' => ['SplFileInfo', 'class='=>'?string'],
'old' => ['SplFileInfo', 'class='=>'class-string'],
'new' => ['SplFileInfo', 'class='=>'?class-string'],
],
'SplFileObject::getPathInfo' => [
'old' => ['SplFileInfo|null', 'class='=>'string'],
'new' => ['SplFileInfo|null', 'class='=>'?string'],
'old' => ['SplFileInfo|null', 'class='=>'class-string'],
'new' => ['SplFileInfo|null', 'class='=>'?class-string'],
],
'SplFileObject::openFile' => [
'old' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'resource'],
'new' => ['SplFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'?resource'],
],
'SplTempFileObject::getFileInfo' => [
'old' => ['SplFileInfo', 'class='=>'string'],
'new' => ['SplFileInfo', 'class='=>'?string'],
'old' => ['SplFileInfo', 'class='=>'class-string'],
'new' => ['SplFileInfo', 'class='=>'?class-string'],
],
'SplTempFileObject::getPathInfo' => [
'old' => ['SplFileInfo|null', 'class='=>'string'],
'new' => ['SplFileInfo|null', 'class='=>'?string'],
'old' => ['SplFileInfo|null', 'class='=>'class-string'],
'new' => ['SplFileInfo|null', 'class='=>'?class-string'],
],
'SplTempFileObject::openFile' => [
'old' => ['SplTempFileObject', 'mode='=>'string', 'useIncludePath='=>'bool', 'context='=>'resource'],
@@ -390,40 +490,40 @@ return [
'new' => ['array', 'array'=>'array', 'column_key'=>'int|string|null', 'index_key='=>'int|string|null'],
],
'array_combine' => [
'old' => ['associative-array|false', 'keys'=>'string[]|int[]', 'values'=>'array'],
'new' => ['associative-array', 'keys'=>'string[]|int[]', 'values'=>'array'],
'old' => ['array|false', 'keys'=>'string[]|int[]', 'values'=>'array'],
'new' => ['array', 'keys'=>'string[]|int[]', 'values'=>'array'],
],
'array_diff' => [
'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'],
'old' => ['array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['array', 'array'=>'array', '...arrays='=>'array'],
],
'array_diff_assoc' => [
'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'],
'old' => ['array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['array', 'array'=>'array', '...arrays='=>'array'],
],
'array_diff_key' => [
'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'],
'old' => ['array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['array', 'array'=>'array', '...arrays='=>'array'],
],
'array_filter' => [
'old' => ['associative-array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'],
'new' => ['associative-array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar|null', 'mode='=>'int'],
'old' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'],
'new' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar|null', 'mode='=>'int'],
],
'array_key_exists' => [
'old' => ['bool', 'key'=>'string|int', 'array'=>'array|object'],
'new' => ['bool', 'key'=>'string|int', 'array'=>'array'],
],
'array_intersect' => [
'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'],
'old' => ['array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['array', 'array'=>'array', '...arrays='=>'array'],
],
'array_intersect_assoc' => [
'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'],
'old' => ['array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['array', 'array'=>'array', '...arrays='=>'array'],
],
'array_intersect_key' => [
'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'],
'old' => ['array', 'array'=>'array', '...arrays'=>'array'],
'new' => ['array', 'array'=>'array', '...arrays='=>'array'],
],
'array_splice' => [
'old' => ['array', '&rw_array'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'array|string'],
@@ -497,6 +597,10 @@ return [
'old' => ['int<0, max>', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'],
'new' => ['int<0, max>', 'value'=>'Countable|array', 'mode='=>'int'],
],
'sizeof' => [
'old' => ['int<0, max>', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'],
'new' => ['int<0, max>', 'value'=>'Countable|array', 'mode='=>'int'],
],
'count_chars' => [
'old' => ['array<int,int>|false', 'input'=>'string', 'mode='=>'0|1|2'],
'new' => ['array<int,int>', 'input'=>'string', 'mode='=>'0|1|2'],

View File

@@ -66,6 +66,26 @@ return [
'old' => ['DOMDocumentFragment|false'],
'new' => ['DOMDocumentFragment'],
],
'DOMDocument::createTextNode' => [
'old' => ['DOMText|false', 'data'=>'string'],
'new' => ['DOMText', 'data'=>'string'],
],
'Phar::buildFromDirectory' => [
'old' => ['array|false', 'directory'=>'string', 'pattern='=>'string'],
'new' => ['array', 'directory'=>'string', 'pattern='=>'string'],
],
'Phar::buildFromIterator' => [
'old' => ['array|false', 'iterator'=>'Traversable', 'baseDirectory='=>'?string'],
'new' => ['array', 'iterator'=>'Traversable', 'baseDirectory='=>'?string'],
],
'PharData::buildFromDirectory' => [
'old' => ['array|false', 'directory'=>'string', 'pattern='=>'string'],
'new' => ['array', 'directory'=>'string', 'pattern='=>'string'],
],
'PharData::buildFromIterator' => [
'old' => ['array|false', 'iterator'=>'Traversable', 'baseDirectory='=>'?string'],
'new' => ['array', 'iterator'=>'Traversable', 'baseDirectory='=>'?string'],
],
'SplFileObject::fputcsv' => [
'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'],

View File

@@ -0,0 +1,32 @@
<?php // phpcs:ignoreFile
/**
* This contains the information needed to convert the function signatures for php 8.3 to php 8.2 (and vice versa)
*
* This file has three sections.
* The 'added' section contains function/method names from FunctionSignatureMap (And alternates, if applicable) that do not exist in php 8.2
* The 'removed' section contains the signatures that were removed in php 8.3
* The 'changed' section contains functions for which the signature has changed for php 8.3.
* Each function in the 'changed' section has an 'old' and a 'new' section,
* representing the function as it was in PHP 8.2 and in PHP 8.3, respectively
*
* @see CallMap.php
* @see https://php.watch/versions/8.3
*
* @phan-file-suppress PhanPluginMixedKeyNoKey (read by Phan when analyzing this file)
*/
return [
'added' => [
'json_validate' => ['bool', 'json'=>'string', 'depth='=>'positive-int', 'flags='=>'int'],
],
'changed' => [
'gc_status' => [
'old' => ['array{runs:int,collected:int,threshold:int,roots:int}'],
'new' => ['array{runs:int,collected:int,threshold:int,roots:int,running:bool,protected:bool,full:bool,buffer_size:int}'],
],
],
'removed' => [
],
];

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ use Psalm\Type\TaintKind;
// This maps internal function names to sink types that we dont want to end up there
/**
* @var array<string, list<list<TaintKind::*>>>
* @var non-empty-array<string, non-empty-list<list<TaintKind::*>>>
*/
return [
'exec' => [['shell']],

View File

@@ -238,6 +238,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even
- [MissingPropertyType](issues/MissingPropertyType.md)
- [MissingReturnType](issues/MissingReturnType.md)
- [NullOperand](issues/NullOperand.md)
- [PrivateFinalMethod](issues/PrivateFinalMethod.md)
- [PropertyNotSetInConstructor](issues/PropertyNotSetInConstructor.md)
- [RawObjectIteration](issues/RawObjectIteration.md)
- [RedundantConditionGivenDocblockType](issues/RedundantConditionGivenDocblockType.md)

View File

@@ -211,6 +211,7 @@
- [PossiblyUnusedParam](issues/PossiblyUnusedParam.md)
- [PossiblyUnusedProperty](issues/PossiblyUnusedProperty.md)
- [PossiblyUnusedReturnValue](issues/PossiblyUnusedReturnValue.md)
- [PrivateFinalMethod](issues/PrivateFinalMethod.md)
- [PropertyNotSetInConstructor](issues/PropertyNotSetInConstructor.md)
- [PropertyTypeCoercion](issues/PropertyTypeCoercion.md)
- [RawObjectIteration](issues/RawObjectIteration.md)

View File

@@ -0,0 +1,14 @@
# PrivateFinalMethod
Emitted when a class defines private final method. PHP 8.0+ emits a warning when it sees private final method (except `__construct` where it's allowed), and allows redefinition in child classes (effectively ignoring `final` modifier). Before PHP 8.0, `final` was respected in this case.
```php
<?php
class Foo {
final private function baz(): void {}
}
```
## Why this is bad
It causes a warning, and behavior differs between versions.

View File

@@ -600,26 +600,31 @@ class Config
/**
* A list of php extensions supported by Psalm.
* Where key - extension name (without ext- prefix), value - whether to load extensions stub.
* Values:
* - true: ext enabled explicitly or bundled with PHP (should load stubs)
* - false: ext disabled explicitly (should not load stubs)
* - null: state is unknown (e.g. config not processed yet) or ext neither explicitly enabled or disabled.
*
* @psalm-readonly-allow-private-mutation
* @var array<string, bool>
* @var array<string, bool|null>
*/
public $php_extensions = [
"apcu" => false,
"decimal" => false,
"dom" => false,
"ds" => false,
"ffi" => false,
"geos" => false,
"gmp" => false,
"mongodb" => false,
"mysqli" => false,
"pdo" => false,
"random" => false,
"redis" => false,
"simplexml" => false,
"soap" => false,
"xdebug" => false,
"apcu" => null,
"decimal" => null,
"dom" => null,
"ds" => null,
"ffi" => null,
"geos" => null,
"gmp" => null,
"ibm_db2" => null,
"mongodb" => null,
"mysqli" => null,
"pdo" => null,
"random" => null,
"redis" => null,
"simplexml" => null,
"soap" => null,
"xdebug" => null,
];
/**
@@ -645,6 +650,7 @@ class Config
'gettext',
'gmp',
'hash',
'ibm_db2',
'iconv',
'imap',
'intl',
@@ -1113,7 +1119,7 @@ class Config
}
}
foreach ($required_extensions as $required_ext => $_) {
if (isset($config->php_extensions[$required_ext])) {
if (array_key_exists($required_ext, $config->php_extensions)) {
$config->php_extensions[$required_ext] = true;
} else {
$config->php_extensions_not_supported[$required_ext] = true;
@@ -2240,7 +2246,8 @@ class Config
foreach ($extensions_to_load_stubs_using_deprecated_way as $ext_name) {
$ext_stub_path = $ext_stubs_dir . DIRECTORY_SEPARATOR . "$ext_name.phpstub";
$is_stub_already_loaded = in_array($ext_stub_path, $this->internal_stubs, true);
if (! $is_stub_already_loaded && extension_loaded($ext_name)) {
$is_ext_explicitly_disabled = ($this->php_extensions[$ext_name] ?? null) === false;
if (! $is_stub_already_loaded && ! $is_ext_explicitly_disabled && extension_loaded($ext_name)) {
$this->internal_stubs[] = $ext_stub_path;
$this->config_warnings[] = "Psalm 6 will not automatically load stubs for ext-$ext_name."
. " You should explicitly enable or disable this ext in composer.json or Psalm config.";

View File

@@ -2360,6 +2360,18 @@ class ClassAnalyzer extends ClassLikeAnalyzer
);
}
if ($parent_class_storage->readonly && !$storage->readonly) {
IssueBuffer::maybeAdd(
new InvalidExtendClass(
'Non-readonly class ' . $fq_class_name . ' may not inherit from '
. 'readonly class ' . $parent_fq_class_name,
$code_location,
$fq_class_name,
),
$storage->suppressed_issues + $this->getSuppressedIssues(),
);
}
if ($parent_class_storage->deprecated) {
IssueBuffer::maybeAdd(
new DeprecatedClass(

View File

@@ -127,6 +127,10 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer
$class_storage->suppressed_issues + $this->getSuppressedIssues(),
);
foreach ($class_storage->docblock_issues as $docblock_issue) {
IssueBuffer::maybeAdd($docblock_issue);
}
$member_stmts = [];
foreach ($this->class->stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {

View File

@@ -21,6 +21,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\Scope\LoopScope;
use Psalm\Internal\Type\AssertionReconciler;
use Psalm\Internal\Type\Comparator\AtomicTypeComparator;
use Psalm\Internal\Type\TypeExpander;
use Psalm\Issue\ImpureMethodCall;
@@ -36,6 +37,7 @@ use Psalm\Issue\UnnecessaryVarAnnotation;
use Psalm\IssueBuffer;
use Psalm\Node\Expr\VirtualMethodCall;
use Psalm\Node\VirtualIdentifier;
use Psalm\Storage\Assertion;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\Scalar;
@@ -268,6 +270,19 @@ class ForeachAnalyzer
$foreach_context->vars_in_scope[$context_var_id] = $context_type;
}
if ($var_id && $foreach_context->hasVariable($var_id)) {
// refine the type of the array variable we iterate over
// if we entered loop body, the array cannot be empty
$foreach_context->vars_in_scope[$var_id] = AssertionReconciler::reconcile(
new Assertion\NonEmpty(),
$foreach_context->vars_in_scope[$var_id],
null,
$statements_analyzer,
true, // inside loop ?
$statements_analyzer->getTemplateTypeMap() ?? [],
);
}
$foreach_context->inside_loop = true;
$foreach_context->break_types[] = 'loop';

View File

@@ -33,7 +33,6 @@ use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralClassString;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TObjectWithProperties;
@@ -536,7 +535,7 @@ class ArrayAnalyzer
continue 2;
}
$new_offset = $key;
$array_creation_info->item_key_atomic_types[] = new TLiteralString($new_offset);
$array_creation_info->item_key_atomic_types[] = Type::getAtomicStringFromLiteral($new_offset);
$array_creation_info->all_list = false;
} else {
$new_offset = $array_creation_info->int_offset++;

View File

@@ -1542,7 +1542,7 @@ class AssertionFinder
) {
if ($conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof'])
&& $conditional->left->getArgs()
&& ($conditional instanceof BinaryOp\Greater || $conditional instanceof BinaryOp\GreaterOrEqual)
) {
@@ -1551,7 +1551,7 @@ class AssertionFinder
$comparison_adjustment = $conditional instanceof BinaryOp\Greater ? 1 : 0;
} elseif ($conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->right->name->parts[0]), ['count', 'sizeof'])
&& $conditional->right->getArgs()
&& ($conditional instanceof BinaryOp\Smaller || $conditional instanceof BinaryOp\SmallerOrEqual)
) {
@@ -1584,7 +1584,7 @@ class AssertionFinder
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof'])
&& $conditional->left->getArgs();
$operator_less_than_or_equal =
@@ -1603,7 +1603,7 @@ class AssertionFinder
$right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->right->name->parts[0]), ['count', 'sizeof'])
&& $conditional->right->getArgs();
$operator_greater_than_or_equal =
@@ -1633,7 +1633,7 @@ class AssertionFinder
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof'])
&& $conditional->left->getArgs();
if ($left_count && $conditional->right instanceof PhpParser\Node\Scalar\LNumber) {
@@ -1644,7 +1644,7 @@ class AssertionFinder
$right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->right->name->parts[0]), ['count', 'sizeof'])
&& $conditional->right->getArgs();
if ($right_count && $conditional->left instanceof PhpParser\Node\Scalar\LNumber) {
@@ -1785,7 +1785,7 @@ class AssertionFinder
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count';
&& in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof']);
$right_number = $conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $conditional->right->value === (
@@ -1871,9 +1871,9 @@ class AssertionFinder
return new IsType(new Atomic\TString());
case 'is_int':
case 'is_integer':
case 'is_long':
return new IsType(new Atomic\TInt());
case 'is_float':
case 'is_long':
case 'is_double':
case 'is_real':
return new IsType(new Atomic\TFloat());
@@ -2044,7 +2044,8 @@ class AssertionFinder
protected static function hasNonEmptyCountCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'count';
return $stmt->name instanceof PhpParser\Node\Name &&
in_array(strtolower($stmt->name->parts[0]), ['count', 'sizeof']);
}
protected static function hasArrayKeyExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool

View File

@@ -170,7 +170,10 @@ class ArrayAssignmentAnalyzer
$key_values = [];
if ($current_dim instanceof PhpParser\Node\Scalar\String_) {
$key_values[] = new TLiteralString($current_dim->value);
$value_type = Type::getAtomicStringFromLiteral($current_dim->value);
if ($value_type instanceof TLiteralString) {
$key_values[] = $value_type;
}
} elseif ($current_dim instanceof PhpParser\Node\Scalar\LNumber && !$root_is_string) {
$key_values[] = new TLiteralInt($current_dim->value);
} elseif ($current_dim
@@ -330,7 +333,7 @@ class ArrayAssignmentAnalyzer
$v = $type->value;
$v[0] = $new_char;
$changed = true;
$type = new TLiteralString($v);
$type = Type::getAtomicStringFromLiteral($v);
break;
}
}
@@ -511,11 +514,8 @@ class ArrayAssignmentAnalyzer
$array_atomic_key_type = Type::getArrayKey();
}
if ($offset_already_existed
&& $parent_var_id
&& ($parent_type = $context->vars_in_scope[$parent_var_id] ?? null)
) {
if ($parent_type->hasList() && strpos($parent_var_id, '[') === false) {
if ($parent_var_id && ($parent_type = $context->vars_in_scope[$parent_var_id] ?? null)) {
if ($offset_already_existed && $parent_type->hasList() && strpos($parent_var_id, '[') === false) {
$array_atomic_type_list = $value_type;
} elseif ($parent_type->hasClassStringMap()
&& $key_type
@@ -1036,7 +1036,10 @@ class ArrayAssignmentAnalyzer
$key_values = [];
if ($dim instanceof PhpParser\Node\Scalar\String_) {
$key_values[] = new TLiteralString($dim->value);
$value_type = Type::getAtomicStringFromLiteral($dim->value);
if ($value_type instanceof TLiteralString) {
$key_values[] = $value_type;
}
} elseif ($dim instanceof PhpParser\Node\Scalar\LNumber) {
$key_values[] = new TLiteralInt($dim->value);
} else {
@@ -1077,7 +1080,10 @@ class ArrayAssignmentAnalyzer
&& $child_stmt_dim_type->isSingleStringLiteral())
) {
if ($child_stmt->dim instanceof PhpParser\Node\Scalar\String_) {
$offset_type = new TLiteralString($child_stmt->dim->value);
$offset_type = Type::getAtomicStringFromLiteral($child_stmt->dim->value);
if (!$offset_type instanceof TLiteralString) {
return [null, '[string]', false];
}
} else {
$offset_type = $child_stmt_dim_type->getSingleStringLiteral();
}

View File

@@ -34,6 +34,7 @@ use Psalm\Type\Atomic\TLowercaseString;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Atomic\TNonFalsyString;
use Psalm\Type\Atomic\TNonspecificLiteralString;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TNumericString;
@@ -173,7 +174,7 @@ class ConcatAnalyzer
break 2;
}
$result_type_parts[] = new TLiteralString($literal);
$result_type_parts[] = Type::getAtomicStringFromLiteral($literal);
}
}
@@ -278,6 +279,31 @@ class ConcatAnalyzer
}
}
}
} elseif ($left_type || $right_type) {
/**
* @var Union $known_operand
*/
$known_operand = $right_type ?? $left_type;
if ($known_operand->isSingle()) {
$known_operands_atomic = $known_operand->getSingleAtomic();
if ($known_operands_atomic instanceof TNonEmptyString) {
$result_type = Type::getNonEmptyString();
}
if ($known_operands_atomic instanceof TNonFalsyString) {
$result_type = Type::getNonFalsyString();
}
if ($known_operands_atomic instanceof TLiteralString) {
if ($known_operands_atomic->value) {
$result_type = Type::getNonFalsyString();
} elseif ($known_operands_atomic->value !== '') {
$result_type = Type::getNonEmptyString();
}
}
}
}
}

View File

@@ -50,7 +50,7 @@ class BitwiseNotAnalyzer
if ($type_part instanceof TLiteralInt) {
$type_part = new TLiteralInt(~$type_part->value);
} elseif ($type_part instanceof TLiteralString) {
$type_part = new TLiteralString(~$type_part->value);
$type_part = Type::getAtomicStringFromLiteral(~$type_part->value);
}
$acceptable_types[] = $type_part;

View File

@@ -35,7 +35,6 @@ use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TNull;
@@ -80,7 +79,7 @@ class FunctionCallReturnTypeFetcher
if ($stmt->isFirstClassCallable()) {
$candidate_callable = CallableTypeComparator::getCallableFromAtomic(
$codebase,
new TLiteralString($function_id),
Type::getAtomicStringFromLiteral($function_id),
null,
$statements_analyzer,
true,
@@ -354,6 +353,7 @@ class FunctionCallReturnTypeFetcher
} else {
switch ($call_map_key) {
case 'count':
case 'sizeof':
if (($first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value))) {
$atomic_types = $first_arg_type->getAtomicTypes();

View File

@@ -35,7 +35,6 @@ use Psalm\Type\Atomic\TDependentGetDebugType;
use Psalm\Type\Atomic\TDependentGetType;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TLowercaseString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
@@ -625,17 +624,17 @@ class NamedFunctionCallHandler
$class_string_types[] = new TClassString();
} else {
if ($class_type instanceof TInt) {
$class_string_types[] = new TLiteralString('int');
$class_string_types[] = Type::getAtomicStringFromLiteral('int');
} elseif ($class_type instanceof TString) {
$class_string_types[] = new TLiteralString('string');
$class_string_types[] = Type::getAtomicStringFromLiteral('string');
} elseif ($class_type instanceof TFloat) {
$class_string_types[] = new TLiteralString('float');
$class_string_types[] = Type::getAtomicStringFromLiteral('float');
} elseif ($class_type instanceof TBool) {
$class_string_types[] = new TLiteralString('bool');
$class_string_types[] = Type::getAtomicStringFromLiteral('bool');
} elseif ($class_type instanceof TClosedResource) {
$class_string_types[] = new TLiteralString('resource (closed)');
$class_string_types[] = Type::getAtomicStringFromLiteral('resource (closed)');
} elseif ($class_type instanceof TNull) {
$class_string_types[] = new TLiteralString('null');
$class_string_types[] = Type::getAtomicStringFromLiteral('null');
} else {
$class_string_types[] = new TString();
}

View File

@@ -721,7 +721,7 @@ class CastAnalyzer
|| $atomic_type instanceof TNumeric
) {
if ($atomic_type instanceof TLiteralInt || $atomic_type instanceof TLiteralFloat) {
$castable_types[] = new TLiteralString((string) $atomic_type->value);
$castable_types[] = Type::getAtomicStringFromLiteral((string) $atomic_type->value);
} elseif ($atomic_type instanceof TNonspecificLiteralInt) {
$castable_types[] = new TNonspecificLiteralString();
} else {
@@ -740,20 +740,20 @@ class CastAnalyzer
if ($atomic_type instanceof TNull
|| $atomic_type instanceof TFalse
) {
$valid_strings[] = new TLiteralString('');
$valid_strings[] = Type::getAtomicStringFromLiteral('');
continue;
}
if ($atomic_type instanceof TTrue
) {
$valid_strings[] = new TLiteralString('1');
$valid_strings[] = Type::getAtomicStringFromLiteral('1');
continue;
}
if ($atomic_type instanceof TBool
) {
$valid_strings[] = new TLiteralString('1');
$valid_strings[] = new TLiteralString('');
$valid_strings[] = Type::getAtomicStringFromLiteral('1');
$valid_strings[] = Type::getAtomicStringFromLiteral('');
continue;
}

View File

@@ -10,6 +10,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
@@ -123,7 +124,7 @@ class EncapsulatedStringAnalyzer
if ($non_empty) {
if ($literal_string !== null) {
$stmt_type = new Union(
[new TLiteralString($literal_string)],
[Type::getAtomicStringFromLiteral($literal_string)],
['parent_nodes' => $parent_nodes],
);
} elseif ($all_literals) {

View File

@@ -481,7 +481,10 @@ class ArrayFetchAnalyzer
$key_values = [];
if ($stmt->dim instanceof PhpParser\Node\Scalar\String_) {
$key_values[] = new TLiteralString($stmt->dim->value);
$value_type = Type::getAtomicStringFromLiteral($stmt->dim->value);
if ($value_type instanceof TLiteralString) {
$key_values[] = $value_type;
}
} elseif ($stmt->dim instanceof PhpParser\Node\Scalar\LNumber) {
$key_values[] = new TLiteralInt($stmt->dim->value);
} elseif ($stmt->dim && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))) {
@@ -514,7 +517,7 @@ class ArrayFetchAnalyzer
if ($in_assignment) {
$offset_type->removeType('null');
$offset_type->addType(new TLiteralString(''));
$offset_type->addType(Type::getAtomicStringFromLiteral(''));
}
}
@@ -534,7 +537,7 @@ class ArrayFetchAnalyzer
$offset_type->removeType('null');
if (!$offset_type->ignore_nullable_issues) {
$offset_type->addType(new TLiteralString(''));
$offset_type->addType(Type::getAtomicStringFromLiteral(''));
}
}
}

View File

@@ -52,7 +52,6 @@ use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
@@ -935,7 +934,7 @@ class AtomicPropertyFetchAnalyzer
if ($lhs_type_part instanceof TEnumCase) {
$statements_analyzer->node_data->setType(
$stmt,
new Union([new TLiteralString($lhs_type_part->case_name)]),
new Union([Type::getAtomicStringFromLiteral($lhs_type_part->case_name)]),
);
} else {
$statements_analyzer->node_data->setType($stmt, Type::getNonEmptyString());
@@ -971,7 +970,7 @@ class AtomicPropertyFetchAnalyzer
foreach ($enum_cases as $enum_case) {
if (is_string($enum_case->value)) {
$case_values[] = new TLiteralString($enum_case->value);
$case_values[] = Type::getAtomicStringFromLiteral($enum_case->value);
} elseif (is_int($enum_case->value)) {
$case_values[] = new TLiteralInt($enum_case->value);
} else {

View File

@@ -321,11 +321,9 @@ class VariableFetchAnalyzer
(bool) $statements_analyzer->getBranchPoint($var_name),
);
} else {
if ($codebase->alter_code) {
if (!isset($project_analyzer->getIssuesToFix()['PossiblyUndefinedVariable'])) {
return true;
}
if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['PossiblyUndefinedVariable'])
) {
$branch_point = $statements_analyzer->getBranchPoint($var_name);
if ($branch_point) {

View File

@@ -81,7 +81,8 @@ class MatchAnalyzer
&& ($stmt->cond->name->parts === ['get_class']
|| $stmt->cond->name->parts === ['gettype']
|| $stmt->cond->name->parts === ['get_debug_type']
|| $stmt->cond->name->parts === ['count'])
|| $stmt->cond->name->parts === ['count']
|| $stmt->cond->name->parts === ['sizeof'])
&& $stmt->cond->getArgs()
) {
$first_arg = $stmt->cond->getArgs()[0];

View File

@@ -758,7 +758,7 @@ class SimpleTypeInferer
foreach ($unpacked_atomic_type->properties as $key => $property_value) {
if (is_string($key)) {
$new_offset = $key;
$array_creation_info->item_key_atomic_types[] = new TLiteralString($new_offset);
$array_creation_info->item_key_atomic_types[] = Type::getAtomicStringFromLiteral($new_offset);
} else {
$new_offset = $array_creation_info->int_offset++;
$array_creation_info->item_key_atomic_types[] = new TLiteralInt($new_offset);

View File

@@ -20,6 +20,8 @@ use Psalm\Type\Atomic\TString;
use Psalm\Type\Union;
use RuntimeException;
use function is_int;
/**
* @internal
*/
@@ -51,7 +53,9 @@ class UnaryPlusMinusAnalyzer
continue;
}
if ($type_part instanceof TLiteralInt) {
$type_part = new TLiteralInt(-$type_part->value);
/** @var int|float $value */
$value = -$type_part->value;
$type_part = is_int($value) ? new TLiteralInt($value) : new TLiteralFloat($value);
} elseif ($type_part instanceof TLiteralFloat) {
$type_part = new TLiteralFloat(-$type_part->value);
} elseif ($type_part instanceof TIntRange) {

View File

@@ -76,7 +76,6 @@ use function fwrite;
use function get_class;
use function in_array;
use function is_string;
use function ltrim;
use function preg_split;
use function reset;
use function round;
@@ -794,21 +793,21 @@ class StatementsAnalyzer extends SourceAnalyzer
if (isset($comments->tags['psalm-scope-this'])) {
$trimmed = trim(reset($comments->tags['psalm-scope-this']));
$trimmed = ltrim($trimmed, '\\');
$scope_fqcn = Type::getFQCLNFromString($trimmed, $this->getAliases());
if (!$codebase->classExists($trimmed)) {
if (!$codebase->classExists($scope_fqcn)) {
IssueBuffer::maybeAdd(
new UndefinedDocblockClass(
'Scope class ' . $trimmed . ' does not exist',
'Scope class ' . $scope_fqcn . ' does not exist',
new CodeLocation($this->getSource(), $stmt, null, true),
$trimmed,
$scope_fqcn,
),
);
} else {
$this_type = Type::parseString($trimmed);
$context->self = $trimmed;
$this_type = Type::parseString($scope_fqcn);
$context->self = $scope_fqcn;
$context->vars_in_scope['$this'] = $this_type;
$this->setFQCLN($trimmed);
$this->setFQCLN($scope_fqcn);
}
}
}

View File

@@ -5,6 +5,7 @@ namespace Psalm\Internal\Analyzer;
use PhpParser\Node\Stmt\Trait_;
use Psalm\Aliases;
use Psalm\Context;
use Psalm\IssueBuffer;
use function assert;
@@ -80,5 +81,9 @@ class TraitAnalyzer extends ClassLikeAnalyzer
AttributesAnalyzer::TARGET_CLASS,
$storage->suppressed_issues + $statements_analyzer->getSuppressedIssues(),
);
foreach ($storage->docblock_issues as $docblock_issue) {
IssueBuffer::maybeAdd($docblock_issue);
}
}
}

View File

@@ -44,6 +44,7 @@ use function array_sum;
use function array_values;
use function chdir;
use function count;
use function extension_loaded;
use function file_exists;
use function file_put_contents;
use function function_exists;
@@ -899,8 +900,17 @@ final class Psalm
}
}
if ($threads > 1) {
if ($threads > 1
&& extension_loaded('grpc')
&& (ini_get('grpc.enable_fork_support') === '1' && ini_get('grpc.poll_strategy') === 'epoll1') === false
) {
$ini_handler->disableExtension('grpc');
$progress->warning(PHP_EOL
. 'grpc extension has been disabled. '
. 'Set grpc.enable_fork_support = 1 and grpc.poll_strategy = epoll1 in php.ini to enable it. '
. 'See https://github.com/grpc/grpc/issues/20250#issuecomment-531321945 for more information.'
. PHP_EOL . PHP_EOL);
}
$ini_handler->disableExtensions([

View File

@@ -1703,6 +1703,10 @@ class ClassLikes
continue;
}
if ($method_storage->public_api) {
continue;
}
if ($method_storage->location
&& !$project_analyzer->canReportIssues($method_storage->location->file_path)
&& !$codebase->analyzer->canReportIssues($method_storage->location->file_path)

View File

@@ -94,7 +94,7 @@ class ConstantTypeResolver
|| $right instanceof TLiteralFloat
|| $right instanceof TLiteralInt)
) {
return new TLiteralString($left->value . $right->value);
return Type::getAtomicStringFromLiteral($left->value . $right->value);
}
return new TString();
@@ -355,7 +355,7 @@ class ConstantTypeResolver
}
if (is_string($value)) {
return new TLiteralString($value);
return Type::getAtomicStringFromLiteral($value);
}
if (is_int($value)) {

View File

@@ -555,7 +555,7 @@ class Functions
return true;
}
if ($function_id === 'count' && isset($args[0]) && $type_provider) {
if (in_array($function_id, ['count', 'sizeof']) && isset($args[0]) && $type_provider) {
$count_type = $type_provider->getType($args[0]->value);
if ($count_type) {

View File

@@ -36,26 +36,26 @@ use function version_compare;
class InternalCallMapHandler
{
private const PHP_MAJOR_VERSION = 8;
private const PHP_MINOR_VERSION = 2;
private const PHP_MINOR_VERSION = 3;
private const LOWEST_AVAILABLE_DELTA = 71;
private static ?int $loaded_php_major_version = null;
private static ?int $loaded_php_minor_version = null;
/**
* @var array<lowercase-string, array<int|string,string>>|null
* @var non-empty-array<lowercase-string, array<int|string,string>>|null
*/
private static ?array $call_map = null;
/**
* @var array<list<TCallable>>|null
* @var array<string, non-empty-list<TCallable>>|null
*/
private static ?array $call_map_callables = [];
/**
* @var array<string, list<list<TaintKind::*>>>
* @var non-empty-array<string, non-empty-list<list<TaintKind::*>>>|null
*/
private static array $taint_sink_map = [];
private static ?array $taint_sink_map = null;
/**
* @param list<PhpParser\Node\Arg> $args
@@ -84,7 +84,7 @@ class InternalCallMapHandler
}
/**
* @param array<int, TCallable> $callables
* @param non-empty-list<TCallable> $callables
* @param list<PhpParser\Node\Arg> $args
*/
public static function getMatchingCallableFromCallMapOptions(
@@ -216,7 +216,7 @@ class InternalCallMapHandler
}
/**
* @return list<TCallable>|null
* @return non-empty-list<TCallable>|null
*/
public static function getCallablesFromCallMap(string $function_id): ?array
{
@@ -332,7 +332,9 @@ class InternalCallMapHandler
/**
* Gets the method/function call map
*
* @return array<string, array<int|string, string>>
* @return non-empty-array<string, array<int|string, string>>
* @psalm-assert !null self::$taint_sink_map
* @psalm-assert !null self::$call_map
*/
public static function getCallMap(): array
{
@@ -353,26 +355,31 @@ class InternalCallMapHandler
return self::$call_map;
}
/** @var array<string, array<int|string, string>> */
$call_map = require(dirname(__DIR__, 4) . '/dictionaries/CallMap.php');
/** @var non-empty-array<string, array<int|string, string>> */
$call_map_data = require(dirname(__DIR__, 4) . '/dictionaries/CallMap.php');
self::$call_map = [];
$call_map = [];
foreach ($call_map as $key => $value) {
foreach ($call_map_data as $key => $value) {
$cased_key = strtolower($key);
self::$call_map[$cased_key] = $value;
$call_map[$cased_key] = $value;
}
self::$call_map = $call_map;
/**
* @var array<string, list<list<TaintKind::*>>>
* @var non-empty-array<string, non-empty-list<list<TaintKind::*>>>
*/
$taint_map = require(dirname(__DIR__, 4) . '/dictionaries/InternalTaintSinkMap.php');
$taint_map_data = require(dirname(__DIR__, 4) . '/dictionaries/InternalTaintSinkMap.php');
foreach ($taint_map as $key => $value) {
$taint_map = [];
foreach ($taint_map_data as $key => $value) {
$cased_key = strtolower($key);
self::$taint_sink_map[$cased_key] = $value;
$taint_map[$cased_key] = $value;
}
self::$taint_sink_map = $taint_map;
if (version_compare($analyzer_version, $current_version, '<')) {
// the following assumes both minor and major versions a single digits
for ($i = $current_version_int; $i > $analyzer_version_int && $i >= self::LOWEST_AVAILABLE_DELTA; --$i) {
@@ -408,6 +415,7 @@ class InternalCallMapHandler
}
}
}
assert(!empty(self::$call_map));
self::$loaded_php_major_version = $analyzer_major_version;
self::$loaded_php_minor_version = $analyzer_minor_version;

View File

@@ -17,7 +17,9 @@ use Psalm\Internal\Scanner\ParsedDocblock;
use function array_key_exists;
use function array_merge;
use function array_reduce;
use function array_slice;
use function count;
use function implode;
use function is_string;
use function ltrim;
use function preg_match;
@@ -332,7 +334,29 @@ class FunctionDocblockManipulator
foreach ($parsed_docblock->tags['param'] as &$param_block) {
$doc_parts = CommentAnalyzer::splitDocLine($param_block);
// If there's no type
if (($doc_parts[0] ?? null) === '$' . $param_name) {
// If the parameter has a description add that back
if (count($doc_parts) > 1) {
$new_param_block .= " ". implode(" ", array_slice($doc_parts, 1));
}
if ($param_block !== $new_param_block) {
$modified_docblock = true;
}
$param_block = $new_param_block;
$found_in_params = true;
break;
}
// If there is a type
if (($doc_parts[1] ?? null) === '$' . $param_name) {
// If the parameter has a description add that back
if (count($doc_parts) > 2) {
$new_param_block .= " ". implode(" ", array_slice($doc_parts, 2));
}
if ($param_block !== $new_param_block) {
$modified_docblock = true;
}

View File

@@ -356,7 +356,12 @@ class ClassLikeDocblockParser
$method_tree = $parse_tree_creator->create();
} catch (TypeParseTreeException $e) {
throw new DocblockParseException($method_entry . ' is not a valid method');
throw new DocblockParseException(
$method_entry . ' is not a valid method: '
. $e->getMessage(),
0,
$e,
);
}
if (!$method_tree instanceof MethodWithReturnTypeTree

View File

@@ -42,11 +42,13 @@ use Psalm\Issue\ConstantDeclarationInTrait;
use Psalm\Issue\DuplicateClass;
use Psalm\Issue\DuplicateConstant;
use Psalm\Issue\DuplicateEnumCase;
use Psalm\Issue\InvalidAttribute;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\InvalidEnumBackingType;
use Psalm\Issue\InvalidEnumCaseValue;
use Psalm\Issue\InvalidTypeImport;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\MissingPropertyType;
use Psalm\Issue\ParseError;
use Psalm\IssueBuffer;
use Psalm\Storage\AttributeStorage;
@@ -256,6 +258,7 @@ class ClassLikeNodeScanner
if ($node instanceof PhpParser\Node\Stmt\Class_) {
$storage->abstract = $node->isAbstract();
$storage->final = $node->isFinal();
$storage->readonly = $node->isReadonly();
$this->codebase->classlikes->addFullyQualifiedClassName($fq_classlike_name, $this->file_path);
@@ -704,10 +707,10 @@ class ClassLikeNodeScanner
$name_types = [];
$values_types = [];
foreach ($storage->enum_cases as $name => $enumCaseStorage) {
$name_types[] = new Type\Atomic\TLiteralString($name);
$name_types[] = Type::getAtomicStringFromLiteral($name);
if ($storage->enum_type !== null) {
if (is_string($enumCaseStorage->value)) {
$values_types[] = new Type\Atomic\TLiteralString($enumCaseStorage->value);
$values_types[] = Type::getAtomicStringFromLiteral($enumCaseStorage->value);
} elseif (is_int($enumCaseStorage->value)) {
$values_types[] = new Type\Atomic\TLiteralInt($enumCaseStorage->value);
}
@@ -765,6 +768,14 @@ class ClassLikeNodeScanner
$storage->external_mutation_free = true;
}
if ($attribute->fq_class_name === 'AllowDynamicProperties' && $storage->readonly) {
IssueBuffer::maybeAdd(new InvalidAttribute(
'Readonly classes cannot have dynamic properties',
new CodeLocation($this->file_scanner, $attr, null, true),
));
continue;
}
$storage->attributes[] = $attribute;
}
}
@@ -1586,10 +1597,22 @@ class ClassLikeNodeScanner
if (count($property_storage->internal) === 0 && $var_comment && $var_comment->internal) {
$property_storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)];
}
$property_storage->readonly = $stmt->isReadonly() || ($var_comment && $var_comment->readonly);
$property_storage->readonly = $storage->readonly
|| $stmt->isReadonly()
|| ($var_comment && $var_comment->readonly);
$property_storage->allow_private_mutation = $var_comment ? $var_comment->allow_private_mutation : false;
$property_storage->description = $var_comment ? $var_comment->description : null;
if (!$signature_type && $storage->readonly) {
IssueBuffer::maybeAdd(
new MissingPropertyType(
'Properties of readonly classes must have a type',
new CodeLocation($this->file_scanner, $stmt, null, true),
$fq_classlike_name . '::$' . $property->name->name,
),
);
}
if (!$signature_type && !$doc_var_group_type) {
if ($property->default) {
$property_storage->suggested_type = SimpleTypeInferer::infer(
@@ -1883,8 +1906,8 @@ class ClassLikeNodeScanner
$type_string = str_replace("\n", '', implode('', $var_line_parts));
$type_string = preg_replace('/>[^>^\}]*$/', '>', $type_string, 1);
$type_string = preg_replace('/\}[^>^\}]*$/', '}', $type_string, 1);
// Strip any remaining characters after the last grouping character >, } or )
$type_string = preg_replace('/(?<=[>})])[^>})]*$/', '', $type_string, 1);
try {
$type_tokens = TypeTokenizer::getFullyQualifiedTokens(

View File

@@ -555,6 +555,8 @@ class FunctionLikeDocblockParser
$info->description = $parsed_docblock->description;
}
$info->public_api = isset($parsed_docblock->tags['psalm-api']) || isset($parsed_docblock->tags['api']);
return $info;
}

View File

@@ -417,6 +417,8 @@ class FunctionLikeDocblockScanner
if ($docblock_info->description) {
$storage->description = $docblock_info->description;
}
$storage->public_api = $docblock_info->public_api;
}
/**
@@ -489,7 +491,8 @@ class FunctionLikeDocblockScanner
// spaces are allowed before $foo in get(string $foo) magic method
// definitions, but we want to remove them in this instance
if (isset($fixed_type_tokens[$i - 1])
if ($i > 0
&& isset($fixed_type_tokens[$i - 1])
&& $fixed_type_tokens[$i - 1][0][0] === ' '
) {
unset($fixed_type_tokens[$i - 1]);

View File

@@ -34,6 +34,7 @@ use Psalm\Issue\DuplicateParam;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\ParseError;
use Psalm\Issue\PrivateFinalMethod;
use Psalm\IssueBuffer;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FileStorage;
@@ -1101,7 +1102,24 @@ class FunctionLikeNodeScanner
$storage->is_static = $stmt->isStatic();
$storage->abstract = $stmt->isAbstract();
$storage->final = $classlike_storage->final || $stmt->isFinal();
if ($stmt->isPrivate() && $stmt->isFinal() && $method_name_lc !== '__construct') {
IssueBuffer::maybeAdd(
new PrivateFinalMethod(
'Private methods cannot be final',
new CodeLocation($this->file_scanner, $stmt, null, true),
(string) $method_id,
),
);
if ($this->codebase->analysis_php_version_id >= 8_00_00) {
// ignore `final` on the method as that's what PHP does
$storage->final = $classlike_storage->final;
} else {
$storage->final = true;
}
} else {
$storage->final = $classlike_storage->final || $stmt->isFinal();
}
$storage->final_from_docblock = $classlike_storage->final_from_docblock;
if ($stmt->isPrivate()) {

View File

@@ -46,6 +46,9 @@ class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInterface
$second_arg = $item->getArgs()[1]->value ?? null;
if ($second_arg === null) {
if ($statements_analyzer->getCodebase()->analysis_php_version_id >= 8_01_00) {
return ['html', 'has_quotes'];
}
return ['html'];
}
@@ -95,6 +98,9 @@ class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInterface
$second_arg = $item->getArgs()[1]->value ?? null;
if ($second_arg === null) {
if ($statements_analyzer->getCodebase()->analysis_php_version_id >= 8_01_00) {
return ['html', 'has_quotes'];
}
return ['html'];
}

View File

@@ -56,7 +56,7 @@ class GetObjectVarsReturnTypeProvider implements FunctionReturnTypeProviderInter
$object_type = reset($atomics);
if ($object_type instanceof Atomic\TEnumCase) {
$properties = ['name' => new Union([new Atomic\TLiteralString($object_type->case_name)])];
$properties = ['name' => new Union([Type::getAtomicStringFromLiteral($object_type->case_name)])];
$codebase = $statements_source->getCodebase();
$enum_classlike_storage = $codebase->classlike_storage_provider->get($object_type->value);
if ($enum_classlike_storage->enum_type === null) {
@@ -66,7 +66,7 @@ class GetObjectVarsReturnTypeProvider implements FunctionReturnTypeProviderInter
if (is_int($enum_case_storage->value)) {
$properties['value'] = new Union([new Atomic\TLiteralInt($enum_case_storage->value)]);
} elseif (is_string($enum_case_storage->value)) {
$properties['value'] = new Union([new Atomic\TLiteralString($enum_case_storage->value)]);
$properties['value'] = new Union([Type::getAtomicStringFromLiteral($enum_case_storage->value)]);
}
return new TKeyedArray($properties);
}

View File

@@ -9,7 +9,6 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Union;
@@ -42,20 +41,20 @@ class VersionCompareReturnTypeProvider implements FunctionReturnTypeProviderInte
if ($operator_type) {
if (!$operator_type->hasMixed()) {
$acceptable_operator_type = new Union([
new TLiteralString('<'),
new TLiteralString('lt'),
new TLiteralString('<='),
new TLiteralString('le'),
new TLiteralString('>'),
new TLiteralString('gt'),
new TLiteralString('>='),
new TLiteralString('ge'),
new TLiteralString('=='),
new TLiteralString('='),
new TLiteralString('eq'),
new TLiteralString('!='),
new TLiteralString('<>'),
new TLiteralString('ne'),
Type::getAtomicStringFromLiteral('<'),
Type::getAtomicStringFromLiteral('lt'),
Type::getAtomicStringFromLiteral('<='),
Type::getAtomicStringFromLiteral('le'),
Type::getAtomicStringFromLiteral('>'),
Type::getAtomicStringFromLiteral('gt'),
Type::getAtomicStringFromLiteral('>='),
Type::getAtomicStringFromLiteral('ge'),
Type::getAtomicStringFromLiteral('=='),
Type::getAtomicStringFromLiteral('='),
Type::getAtomicStringFromLiteral('eq'),
Type::getAtomicStringFromLiteral('!='),
Type::getAtomicStringFromLiteral('<>'),
Type::getAtomicStringFromLiteral('ne'),
]);
$codebase = $statements_source->getCodebase();

View File

@@ -174,4 +174,6 @@ class FunctionDocblockComment
/** @var array<string, array{lines:list<int>, suggested_replacement?:string}> */
public array $unexpected_tags = [];
public bool $public_api = false;
}

View File

@@ -945,6 +945,15 @@ class AssertionReconciler extends Reconciler
$redundant = false;
$existing_var_type->removeType($atomic_key);
$existing_var_type->addType(new TEnumCase($fq_enum_name, $case_name));
} elseif (AtomicTypeComparator::canBeIdentical(
$statements_analyzer->getCodebase(),
$atomic_type,
$assertion_type,
)) {
$can_be_equal = true;
$redundant = $atomic_key === $assertion_type->getKey();
$existing_var_type->removeType($atomic_key);
$existing_var_type->addType(new TEnumCase($fq_enum_name, $case_name));
} elseif ($atomic_key !== $assertion_type->getKey()) {
$existing_var_type->removeType($atomic_key);
$redundant = false;

View File

@@ -780,8 +780,7 @@ class ParseTreeCreator
$type_token[0],
$new_parent,
);
} elseif ($type_token[0] !== 'array'
&& $type_token[0][0] !== '\\'
} elseif ($type_token[0][0] !== '\\'
&& $this->current_leaf instanceof Root
) {
$new_leaf = new MethodTree(

View File

@@ -987,17 +987,17 @@ class SimpleNegatedAssertionReconciler extends Reconciler
if (get_class($string_atomic_type) === TString::class) {
$existing_var_type->removeType('string');
$existing_var_type->addType(new TLiteralString(''));
$existing_var_type->addType(new TLiteralString('0'));
$existing_var_type->addType(Type::getAtomicStringFromLiteral(''));
$existing_var_type->addType(Type::getAtomicStringFromLiteral('0'));
} elseif (get_class($string_atomic_type) === TNonEmptyString::class) {
$existing_var_type->removeType('string');
$existing_var_type->addType(new TLiteralString('0'));
$existing_var_type->addType(Type::getAtomicStringFromLiteral('0'));
} elseif (get_class($string_atomic_type) === TNonEmptyLowercaseString::class) {
$existing_var_type->removeType('string');
$existing_var_type->addType(new TLiteralString('0'));
$existing_var_type->addType(Type::getAtomicStringFromLiteral('0'));
} elseif (get_class($string_atomic_type) === TNonEmptyNonspecificLiteralString::class) {
$existing_var_type->removeType('string');
$existing_var_type->addType(new TLiteralString('0'));
$existing_var_type->addType(Type::getAtomicStringFromLiteral('0'));
}
}

View File

@@ -1473,7 +1473,7 @@ class TypeCombiner
} elseif ($type instanceof TKeyedArray && isset($type->class_strings[$property_name])) {
$objectlike_keys[$property_name] = new TLiteralClassString($property_name, $from_docblock);
} else {
$objectlike_keys[$property_name] = new TLiteralString($property_name, $from_docblock);
$objectlike_keys[$property_name] = Type::getAtomicStringFromLiteral($property_name, $from_docblock);
}
}

View File

@@ -47,7 +47,6 @@ use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TLiteralClassString;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyArray;
@@ -393,7 +392,7 @@ class TypeParser
}
if ($parse_tree->value[0] === '"' || $parse_tree->value[0] === '\'') {
return new TLiteralString(substr($parse_tree->value, 1, -1), $from_docblock);
return Type::getAtomicStringFromLiteral(substr($parse_tree->value, 1, -1), $from_docblock);
}
if (strpos($parse_tree->value, '::')) {

View File

@@ -348,6 +348,7 @@ class TypeChecker extends TypeVisitor
{
if ($this->prevent_template_covariance
&& strpos($atomic->defining_class, 'fn-') !== 0
&& $atomic->defining_class !== 'class-string-map'
) {
$codebase = $this->source->getCodebase();

View File

@@ -2,8 +2,8 @@
namespace Psalm\Internal;
use Composer\InstalledVersions;
use OutOfBoundsException;
use PackageVersions\Versions;
use Phar;
use function class_exists;
@@ -89,11 +89,17 @@ final class VersionUtils
{
try {
return [
self::PSALM_PACKAGE => Versions::getVersion(self::PSALM_PACKAGE),
self::PHP_PARSER_PACKAGE => Versions::getVersion(self::PHP_PARSER_PACKAGE),
self::PSALM_PACKAGE => self::getVersion(self::PSALM_PACKAGE),
self::PHP_PARSER_PACKAGE => self::getVersion(self::PHP_PARSER_PACKAGE),
];
} catch (OutOfBoundsException $ex) {
}
return null;
}
private static function getVersion(string $packageName): string
{
return InstalledVersions::getPrettyVersion($packageName)
. '@' . InstalledVersions::getReference($packageName);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Psalm\Issue;
class PrivateFinalMethod extends MethodIssue
{
public const ERROR_LEVEL = 2;
public const SHORTCODE = 320;
}

View File

@@ -9,6 +9,7 @@ use Psalm\Plugin\EventHandler\AfterAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent;
use function array_filter;
use function array_key_exists;
use function array_merge;
use function array_values;
use function curl_close;
@@ -23,8 +24,11 @@ use function is_int;
use function is_string;
use function json_encode;
use function parse_url;
use function sprintf;
use function strip_tags;
use function strlen;
use function substr_compare;
use function var_export;
use const CURLINFO_HEADER_OUT;
use const CURLOPT_FOLLOWLOCATION;
@@ -46,93 +50,147 @@ final class Shepherd implements AfterAnalysisInterface
public static function afterAnalysis(
AfterAnalysisEvent $event
): void {
$codebase = $event->getCodebase();
$issues = $event->getIssues();
$build_info = $event->getBuildInfo();
$source_control_info = $event->getSourceControlInfo();
if (!function_exists('curl_init')) {
fwrite(STDERR, 'No curl found, cannot send data to ' . $codebase->config->shepherd_host . PHP_EOL);
fwrite(STDERR, "No curl found, cannot send data to shepherd server.\n");
return;
}
$rawPayload = self::collectPayloadToSend($event);
if ($rawPayload === null) {
return;
}
$config = $event->getCodebase()->config;
/**
* Deprecated logic, in Psalm 6 just use $config->shepherd_endpoint
* '#' here is just a hack/marker to use a custom endpoint instead just a custom domain
* case 1: empty option (use https://shepherd.dev/hooks/psalm/)
* case 2: custom domain (/hooks/psalm should be appended) (use https://custom.domain/hooks/psalm)
* case 3: custom endpoint (/hooks/psalm should be appended) (use custom endpoint)
*/
if (substr_compare($config->shepherd_endpoint, '#', -1) === 0) {
$shepherd_endpoint = $config->shepherd_endpoint;
} else {
/** @psalm-suppress DeprecatedProperty, DeprecatedMethod */
$shepherd_endpoint = self::buildShepherdUrlFromHost($config->shepherd_host);
}
self::sendPayload($shepherd_endpoint, $rawPayload);
}
/**
* @psalm-pure
* @deprecated Will be removed in Psalm 6
*/
private static function buildShepherdUrlFromHost(string $host): string
{
if (parse_url($host, PHP_URL_SCHEME) === null) {
$host = 'https://' . $host;
}
return $host . '/hooks/psalm';
}
/**
* @return array{build: array, git: array, issues: array, coverage: list<int>, level: int<1,8>}|null
*/
private static function collectPayloadToSend(AfterAnalysisEvent $event): ?array
{
/** @see \Psalm\Internal\ExecutionEnvironment\BuildInfoCollector::collect */
$build_info = $event->getBuildInfo();
$is_ci_env = array_key_exists('CI_NAME', $build_info); // 'git' key always presents
if (! $is_ci_env) {
return null;
}
$source_control_info = $event->getSourceControlInfo();
$source_control_data = $source_control_info ? $source_control_info->toArray() : [];
if (!$source_control_data && isset($build_info['git']) && is_array($build_info['git'])) {
if ($source_control_data === [] && isset($build_info['git']) && is_array($build_info['git'])) {
$source_control_data = $build_info['git'];
}
unset($build_info['git']);
if ($build_info) {
$normalized_data = $issues === [] ? [] : array_filter(
array_merge(...array_values($issues)),
static fn(IssueData $i): bool => $i->severity === 'error',
);
$data = [
'build' => $build_info,
'git' => $source_control_data,
'issues' => $normalized_data,
'coverage' => $codebase->analyzer->getTotalTypeCoverage($codebase),
'level' => Config::getInstance()->level,
];
$payload = json_encode($data, JSON_THROW_ON_ERROR);
/** @psalm-suppress DeprecatedProperty */
$base_address = $codebase->config->shepherd_host;
if (parse_url($base_address, PHP_URL_SCHEME) === null) {
$base_address = 'https://' . $base_address;
}
// Prepare new cURL resource
$ch = curl_init($base_address . '/hooks/psalm');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Set HTTP Header for POST request
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: ' . strlen($payload),
],
);
// Submit the POST request
$curl_result = curl_exec($ch);
/** @var array{http_code: int, ssl_verify_result: int} $curl_info */
$curl_info = curl_getinfo($ch);
// Close cURL session handle
curl_close($ch);
$response_status_code = $curl_info['http_code'];
if ($response_status_code >= 200 && $response_status_code < 300) {
$shepherd_host = parse_url($codebase->config->shepherd_endpoint, PHP_URL_HOST);
fwrite(STDERR, "🐑 results sent to $shepherd_host 🐑" . PHP_EOL);
return;
}
$is_ssl_error = $curl_info['ssl_verify_result'] > 1;
if ($is_ssl_error) {
fwrite(STDERR, self::getCurlSslErrorMessage($curl_info['ssl_verify_result']) . PHP_EOL);
return;
}
fwrite(STDERR, "Shepherd error: server responded with $response_status_code HTTP status code.\n");
$response_content = is_string($curl_result) ? strip_tags($curl_result) : 'n/a';
fwrite(STDERR, "Shepherd response: $response_content\n");
if ($build_info === []) {
return null;
}
$issues = $event->getIssues();
$normalized_data = $issues === [] ? [] : array_filter(
array_merge(...array_values($issues)),
static fn(IssueData $i): bool => $i->severity === 'error',
);
$codebase = $event->getCodebase();
return [
'build' => $build_info,
'git' => $source_control_data,
'issues' => $normalized_data,
'coverage' => $codebase->analyzer->getTotalTypeCoverage($codebase),
'level' => Config::getInstance()->level,
];
}
private static function sendPayload(string $endpoint, array $rawPayload): void
{
$payload = json_encode($rawPayload, JSON_THROW_ON_ERROR);
// Prepare new cURL resource
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Set HTTP Header for POST request
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: ' . strlen($payload),
],
);
// Submit the POST request
$curl_result = curl_exec($ch);
/** @var array{http_code: int, ssl_verify_result: int} $curl_info */
$curl_info = curl_getinfo($ch);
// Close cURL session handle
curl_close($ch);
$response_status_code = $curl_info['http_code'];
if ($response_status_code >= 200 && $response_status_code < 300) {
$shepherd_host = parse_url($endpoint, PHP_URL_HOST);
fwrite(STDERR, "🐑 results sent to $shepherd_host 🐑" . PHP_EOL);
return;
}
$is_ssl_error = $curl_info['ssl_verify_result'] > 1;
if ($is_ssl_error) {
fwrite(STDERR, self::getCurlSslErrorMessage($curl_info['ssl_verify_result']) . PHP_EOL);
return;
}
$output = "Shepherd error: $endpoint endpoint responded with $response_status_code HTTP status code.\n";
$response_content = is_string($curl_result) ? strip_tags($curl_result) : 'n/a';
$output .= "Shepherd response: $response_content\n";
if ($response_status_code === 0) {
$output .= "Please check shepherd endpoint — it should be a valid URL.\n";
}
$output .= sprintf("cURL Debug info:\n%s\n", var_export($curl_info, true));
fwrite(STDERR, $output);
}
/**

View File

@@ -470,6 +470,8 @@ final class ClassLikeStorage implements HasAttributesInterface
public bool $public_api = false;
public bool $readonly = false;
public function __construct(string $name)
{
$this->name = $name;

View File

@@ -243,6 +243,8 @@ abstract class FunctionLikeStorage implements HasAttributesInterface
*/
public $description;
public bool $public_api = false;
public function __toString(): string
{
return $this->getSignature(false);

View File

@@ -251,29 +251,27 @@ abstract class Type
public static function getString(?string $value = null): Union
{
$type = null;
return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]);
}
if ($value !== null) {
$config = Config::getInstance();
/** @return TLiteralString|TNonEmptyString */
public static function getAtomicStringFromLiteral(string $value, bool $from_docblock = false): TString
{
$config = Config::getInstance();
$event = new StringInterpreterEvent($value, ProjectAnalyzer::getInstance()->getCodebase());
$event = new StringInterpreterEvent($value, ProjectAnalyzer::getInstance()->getCodebase());
$type = $config->eventDispatcher->dispatchStringInterpreter($event);
$type = $config->eventDispatcher->dispatchStringInterpreter($event);
if (!$type) {
if (strlen($value) < $config->max_string_length) {
$type = new TLiteralString($value);
} else {
$type = new TNonEmptyString();
}
if (!$type) {
if (strlen($value) < $config->max_string_length) {
$type = new TLiteralString($value, $from_docblock);
} else {
$type = new TNonEmptyString($from_docblock);
}
}
if (!$type) {
$type = new TString();
}
return new Union([$type]);
return $type;
}
/**

View File

@@ -13,7 +13,6 @@ use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TLiteralClassString;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Union;
use UnexpectedValueException;
@@ -309,7 +308,8 @@ class TKeyedArray extends Atomic
} elseif (isset($this->class_strings[$key])) {
$key_types[] = new TLiteralClassString($key);
} else {
$key_types[] = new TLiteralString($key);
/** @psalm-suppress ImpureMethodCall let's assume string interpreters are pure */
$key_types[] = Type::getAtomicStringFromLiteral($key);
}
}
@@ -362,7 +362,8 @@ class TKeyedArray extends Atomic
} elseif (isset($this->class_strings[$key])) {
$key_types[] = new TLiteralClassString($key);
} else {
$key_types[] = new TLiteralString($key);
/** @psalm-suppress ImpureMethodCall let's assume string interpreters are pure */
$key_types[] = Type::getAtomicStringFromLiteral($key);
}
$value_type = Type::combineUnionTypes($property, $value_type);

View File

@@ -2,9 +2,13 @@
namespace Psalm\Type\Atomic;
use InvalidArgumentException;
use Psalm\Config;
use function addcslashes;
use function mb_strlen;
use function mb_substr;
use function strlen;
/**
* Denotes a string whose value is known.
@@ -16,12 +20,43 @@ class TLiteralString extends TString
/** @var string */
public $value;
/**
* Creates a literal string with a known value.
*
* Internal.
* String interpreters should use {@see TLiteralString::make} instead.
* All other clients should use {@see Type::getAtomicStringFromLiteral}.
*
* @psalm-internal Psalm\Type::getAtomicStringFromLiteral
* @psalm-internal Psalm\Type\Atomic\TLiteralClassString::__construct
* @psalm-internal Psalm\Type\Atomic\TLiteralString::make
*/
public function __construct(string $value, bool $from_docblock = false)
{
$config = Config::getInstance();
if (strlen($value) >= $config->max_string_length) {
throw new InvalidArgumentException(
'Literal string length should be below the configured limit ('
. $config->max_string_length
. ')',
);
}
$this->value = $value;
$this->from_docblock = $from_docblock;
}
/**
* Should only be used by string interpreters to avoid recursive calls.
*
* For all other purposes use {@see Type::getAtomicStringFromLiteral}
*
* @psalm-api
*/
public static function make(string $value, bool $from_docblock = false): self
{
return new self($value, $from_docblock);
}
/**
* @psalm-suppress PossiblyUnusedMethod
* @return static

View File

@@ -531,14 +531,3 @@ final class ReturnTypeWillChange
public function __construct() {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final class SensitiveParameter
{
public function __construct() {}
}
#[Attribute(Attribute::TARGET_CLASS)]
final class AllowDynamicProperties
{
public function __construct() {}
}

View File

@@ -240,6 +240,13 @@ class DatePeriod implements IteratorAggregate
*/
function get_headers(string $url, bool $associative = false, $context = null) : array|false {}
/**
* @psalm-pure
*
* @psalm-flow ($values) -> return
*/
function pack(string $format, mixed ...$values): string {}
final class CurlHandle
{
private function __construct()

View File

@@ -33,4 +33,16 @@ namespace {
/** @psalm-return (Start is string ? Iterator<int, DateTime> : Iterator<int, Start>) */
public function getIterator(): Iterator {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final class SensitiveParameter
{
public function __construct() {}
}
#[Attribute(Attribute::TARGET_CLASS)]
final class AllowDynamicProperties
{
public function __construct() {}
}
}

View File

@@ -19,9 +19,10 @@ namespace {
}
/**
* Note: Official docs say this could return `null`, but it throws exceptions instead.
* @param \FFI\CType|string $type
*/
public static function new($type, bool $owned = true, bool $persistent = false): ?\FFI\CData
public static function new($type, bool $owned = true, bool $persistent = false): \FFI\CData
{
}
@@ -106,6 +107,25 @@ namespace {
public static function isNull(\FFI\CData $ptr): bool
{
}
public function __call(...$values)
{
}
/**
* @param mixed $value
* @return mixed $value
*/
public function __set(string $key, $value)
{
}
/**
* @return string|int|float|\FFI\CData|null
*/
public function __get(string $key)
{
}
}
}
@@ -115,9 +135,36 @@ namespace FFI {
/**
* @since 7.4.0
*/
final class CData
final class CData implements \Countable, \ArrayAccess, \Countable, \Traversable
{
/**
* @return bool
*/
public function offsetExists($offset)
{
}
/**
* @return mixed|null
*/
public function offsetGet($offset)
{
}
/**
* @param mixed|null $value
* @return void
*/
public function offsetSet($offset, $value)
{
}
/**
* @return void
*/
public function offsetUnset($offset)
{
}
}
/**

View File

@@ -0,0 +1,9 @@
<?php
/**
* @param resource $connection
* @param 0|1 $value
*
* @return (func_num_args() > 1 ? bool : (0|1))
*/
function db2_autocommit($connection, int $value = null): int|bool {}