diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 6db374c4e34..9f218c3c9c1 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -2,7 +2,7 @@ namespace Phan\Language\Internal; /** - * CURRENT PHP TARGET VERSION: 8.1 + * CURRENT PHP TARGET VERSION: 8.2 * The version above has to match Psalm\Internal\Codebase\InternalCallMapHandler::PHP_(MAJOR|MINOR)_VERSION * * Format @@ -1695,6 +1695,7 @@ 'curl_share_setopt' => ['bool', 'share_handle'=>'CurlShareHandle', 'option'=>'int', 'value'=>'mixed'], 'curl_share_strerror' => ['?string', 'error_code'=>'int'], 'curl_strerror' => ['?string', 'error_code'=>'int'], +'curl_upkeep' => ['bool', 'handle'=>'CurlHandle'], 'curl_unescape' => ['string|false', 'handle'=>'CurlShareHandle', 'string'=>'string'], 'curl_version' => ['array', 'version='=>'int'], 'CURLFile::__construct' => ['void', 'filename'=>'string', 'mimetype='=>'string', 'postfilename='=>'string'], @@ -6136,6 +6137,7 @@ 'ini_get' => ['string|false', 'option'=>'string'], 'ini_get_all' => ['array|false', 'extension='=>'?string', 'details='=>'bool'], 'ini_restore' => ['void', 'option'=>'string'], +'ini_parse_quantity' => ['int', 'shorthand'=>'non-empty-string'], 'ini_set' => ['string|false', 'option'=>'string', 'value'=>'string|int|float|bool|null'], 'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], @@ -7474,6 +7476,7 @@ 'MemcachePool::setServerParams' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'int', 'retry_interval='=>'int', 'status='=>'bool', 'failure_callback='=>'?callable'], 'memory_get_peak_usage' => ['int', 'real_usage='=>'bool'], 'memory_get_usage' => ['int', 'real_usage='=>'bool'], +'memory_reset_peak_usage' => ['void'], 'MessageFormatter::__construct' => ['void', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::format' => ['false|string', 'args'=>'array'], @@ -8442,6 +8445,7 @@ 'mysqli::disable_reads_from_master' => ['bool'], 'mysqli::dump_debug_info' => ['bool'], 'mysqli::escape_string' => ['string', 'string'=>'string'], +'mysqli::execute_query' => ['mysqli_result|bool', 'query'=>'non-empty-string', 'params='=>'list|null'], 'mysqli::get_charset' => ['object'], 'mysqli::get_client_info' => ['string'], 'mysqli::get_connection_stats' => ['array'], @@ -8503,6 +8507,7 @@ 'mysqli_error_list' => ['array', 'mysql'=>'mysqli'], 'mysqli_escape_string' => ['string', 'mysql'=>'mysqli', 'string'=>'string'], 'mysqli_execute' => ['bool', 'statement'=>'mysqli_stmt', 'params='=>'list|null'], +'mysqli_execute_query' => ['mysqli_result|bool', 'mysqli'=>'mysqli', 'query'=>'non-empty-string', 'params='=>'list|null'], 'mysqli_fetch_all' => ['list>', 'result'=>'mysqli_result', 'mode='=>'3'], 'mysqli_fetch_all\'1' => ['list>', 'result'=>'mysqli_result', 'mode='=>'1'], 'mysqli_fetch_all\'2' => ['list>', 'result'=>'mysqli_result', 'mode='=>'2'], @@ -9276,7 +9281,7 @@ 'ocigetbufferinglob' => ['bool'], 'ocisetbufferinglob' => ['bool', 'lob'=>'bool'], 'octdec' => ['int|float', 'octal_string'=>'string'], -'odbc_autocommit' => ['mixed', 'odbc'=>'resource', 'enable='=>'bool'], +'odbc_autocommit' => ['int|bool', 'odbc'=>'resource', 'enable='=>'bool'], 'odbc_binmode' => ['bool', 'statement'=>'resource', 'mode'=>'int'], 'odbc_close' => ['void', 'odbc'=>'resource'], 'odbc_close_all' => ['void'], @@ -9293,7 +9298,7 @@ 'odbc_execute' => ['bool', 'statement'=>'resource', 'params='=>'array'], 'odbc_fetch_array' => ['array|false', 'statement'=>'resource', 'row='=>'int'], 'odbc_fetch_into' => ['int', 'statement'=>'resource', '&w_array'=>'array', 'row='=>'int'], -'odbc_fetch_object' => ['object|false', 'statement'=>'resource', 'row='=>'int'], +'odbc_fetch_object' => ['stdClass|false', 'statement'=>'resource', 'row='=>'int'], 'odbc_fetch_row' => ['bool', 'statement'=>'resource', 'row='=>'int'], 'odbc_field_len' => ['int|false', 'statement'=>'resource', 'field'=>'int'], 'odbc_field_name' => ['string|false', 'statement'=>'resource', 'field'=>'int'], @@ -9353,6 +9358,7 @@ 'opendir' => ['resource|false', 'directory'=>'string', 'context='=>'resource'], 'openlog' => ['bool', 'prefix'=>'string', 'flags'=>'int', 'facility'=>'int'], 'openssl_cipher_iv_length' => ['int|false', 'cipher_algo'=>'string'], +'openssl_cipher_key_length' => ['positive-int|false', 'cipher_algo'=>'non-empty-string'], 'openssl_csr_export' => ['bool', 'csr'=>'OpenSSLCertificateSigningRequest|string', '&w_output'=>'string', 'no_text='=>'bool'], 'openssl_csr_export_to_file' => ['bool', 'csr'=>'OpenSSLCertificateSigningRequest|string', 'output_filename'=>'string', 'no_text='=>'bool'], 'openssl_csr_get_public_key' => ['OpenSSLAsymmetricKey|false', 'csr'=>'OpenSSLCertificateSigningRequest|string', 'short_names='=>'bool'], @@ -9409,6 +9415,7 @@ 'openssl_x509_free' => ['void', 'certificate'=>'OpenSSLCertificate'], 'openssl_x509_parse' => ['array|false', 'certificate'=>'OpenSSLCertificate|string', 'short_names='=>'bool'], 'openssl_x509_read' => ['OpenSSLCertificate|false', 'certificate'=>'OpenSSLCertificate|string'], +'openssl_x509_verify' => ['int', 'certificate'=>'string|OpenSSLCertificate', 'public_key'=>'string|OpenSSLCertificate|OpenSSLAsymmetricKey|array'], 'ord' => ['int', 'character'=>'string'], 'OuterIterator::current' => ['mixed'], 'OuterIterator::getInnerIterator' => ['Iterator'], @@ -11438,7 +11445,7 @@ 'ReflectionExtension::getFunctions' => ['array'], 'ReflectionExtension::getINIEntries' => ['array'], 'ReflectionExtension::getName' => ['string'], -'ReflectionExtension::getVersion' => ['string'], +'ReflectionExtension::getVersion' => ['?string'], 'ReflectionExtension::info' => ['void'], 'ReflectionExtension::isPersistent' => ['bool'], 'ReflectionExtension::isTemporary' => ['bool'], @@ -13218,24 +13225,24 @@ 'SplFileInfo::__construct' => ['void', 'file_name'=>'string'], 'SplFileInfo::__toString' => ['string'], 'SplFileInfo::__wakeup' => ['void'], -'SplFileInfo::getATime' => ['int'], +'SplFileInfo::getATime' => ['int|false'], 'SplFileInfo::getBasename' => ['string', 'suffix='=>'string'], -'SplFileInfo::getCTime' => ['int'], +'SplFileInfo::getCTime' => ['int|false'], 'SplFileInfo::getExtension' => ['string'], 'SplFileInfo::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplFileInfo::getFilename' => ['string'], -'SplFileInfo::getGroup' => ['int'], -'SplFileInfo::getInode' => ['int'], -'SplFileInfo::getLinkTarget' => ['string'], -'SplFileInfo::getMTime' => ['int'], -'SplFileInfo::getOwner' => ['int'], +'SplFileInfo::getGroup' => ['int|false'], +'SplFileInfo::getInode' => ['int|false'], +'SplFileInfo::getLinkTarget' => ['string|false'], +'SplFileInfo::getMTime' => ['int|false'], +'SplFileInfo::getOwner' => ['int|false'], 'SplFileInfo::getPath' => ['string'], -'SplFileInfo::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], +'SplFileInfo::getPathInfo' => ['SplFileInfo|null', 'class_name='=>'string'], 'SplFileInfo::getPathname' => ['string'], -'SplFileInfo::getPerms' => ['int'], +'SplFileInfo::getPerms' => ['int|false'], 'SplFileInfo::getRealPath' => ['string|false'], -'SplFileInfo::getSize' => ['int'], -'SplFileInfo::getType' => ['string'], +'SplFileInfo::getSize' => ['int|false'], +'SplFileInfo::getType' => ['string|false'], 'SplFileInfo::isDir' => ['bool'], 'SplFileInfo::isExecutable' => ['bool'], 'SplFileInfo::isFile' => ['bool'], @@ -13263,29 +13270,29 @@ 'SplFileObject::ftell' => ['int|false'], 'SplFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], -'SplFileObject::getATime' => ['int'], +'SplFileObject::getATime' => ['int|false'], 'SplFileObject::getBasename' => ['string', 'suffix='=>'string'], 'SplFileObject::getChildren' => ['null'], 'SplFileObject::getCsvControl' => ['array'], -'SplFileObject::getCTime' => ['int'], +'SplFileObject::getCTime' => ['int|false'], 'SplFileObject::getCurrentLine' => ['string|false'], 'SplFileObject::getExtension' => ['string'], 'SplFileObject::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplFileObject::getFilename' => ['string'], 'SplFileObject::getFlags' => ['int'], -'SplFileObject::getGroup' => ['int'], -'SplFileObject::getInode' => ['int'], -'SplFileObject::getLinkTarget' => ['string'], +'SplFileObject::getGroup' => ['int|false'], +'SplFileObject::getInode' => ['int|false'], +'SplFileObject::getLinkTarget' => ['string|false'], 'SplFileObject::getMaxLineLen' => ['int'], -'SplFileObject::getMTime' => ['int'], -'SplFileObject::getOwner' => ['int'], +'SplFileObject::getMTime' => ['int|false'], +'SplFileObject::getOwner' => ['int|false'], 'SplFileObject::getPath' => ['string'], -'SplFileObject::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], +'SplFileObject::getPathInfo' => ['SplFileInfo|null', 'class_name='=>'string'], 'SplFileObject::getPathname' => ['string'], -'SplFileObject::getPerms' => ['int'], +'SplFileObject::getPerms' => ['int|false'], 'SplFileObject::getRealPath' => ['false|string'], -'SplFileObject::getSize' => ['int'], -'SplFileObject::getType' => ['string'], +'SplFileObject::getSize' => ['int|false'], +'SplFileObject::getType' => ['string|false'], 'SplFileObject::hasChildren' => ['false'], 'SplFileObject::isDir' => ['bool'], 'SplFileObject::isExecutable' => ['bool'], @@ -13456,29 +13463,29 @@ 'SplTempFileObject::ftell' => ['int'], 'SplTempFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplTempFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], -'SplTempFileObject::getATime' => ['int'], +'SplTempFileObject::getATime' => ['int|false'], 'SplTempFileObject::getBasename' => ['string', 'suffix='=>'string'], 'SplTempFileObject::getChildren' => ['null'], 'SplTempFileObject::getCsvControl' => ['array'], -'SplTempFileObject::getCTime' => ['int'], +'SplTempFileObject::getCTime' => ['int|false'], 'SplTempFileObject::getCurrentLine' => ['string'], 'SplTempFileObject::getExtension' => ['string'], 'SplTempFileObject::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplTempFileObject::getFilename' => ['string'], 'SplTempFileObject::getFlags' => ['int'], -'SplTempFileObject::getGroup' => ['int'], -'SplTempFileObject::getInode' => ['int'], -'SplTempFileObject::getLinkTarget' => ['string'], +'SplTempFileObject::getGroup' => ['int|false'], +'SplTempFileObject::getInode' => ['int|false'], +'SplTempFileObject::getLinkTarget' => ['string|false'], 'SplTempFileObject::getMaxLineLen' => ['int'], -'SplTempFileObject::getMTime' => ['int'], -'SplTempFileObject::getOwner' => ['int'], +'SplTempFileObject::getMTime' => ['int|false'], +'SplTempFileObject::getOwner' => ['int|false'], 'SplTempFileObject::getPath' => ['string'], 'SplTempFileObject::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplTempFileObject::getPathname' => ['string'], -'SplTempFileObject::getPerms' => ['int'], -'SplTempFileObject::getRealPath' => ['string'], -'SplTempFileObject::getSize' => ['int'], -'SplTempFileObject::getType' => ['string'], +'SplTempFileObject::getPerms' => ['int|false'], +'SplTempFileObject::getRealPath' => ['string|false'], +'SplTempFileObject::getSize' => ['int|false'], +'SplTempFileObject::getType' => ['string|false'], 'SplTempFileObject::hasChildren' => ['bool'], 'SplTempFileObject::isDir' => ['bool'], 'SplTempFileObject::isExecutable' => ['bool'], @@ -13677,7 +13684,7 @@ 'ssh2_exec' => ['resource|false', 'session'=>'resource', 'command'=>'string', 'pty='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], 'ssh2_fetch_stream' => ['resource|false', 'channel'=>'resource', 'streamid'=>'int'], 'ssh2_fingerprint' => ['string|false', 'session'=>'resource', 'flags='=>'int'], -'ssh2_forward_accept' => ['resource|false', 'session'=>'resource'], +'ssh2_forward_accept' => ['resource|false', 'listener'=>'resource'], 'ssh2_forward_listen' => ['resource|false', 'session'=>'resource', 'port'=>'int', 'host='=>'string', 'max_connections='=>'string'], 'ssh2_methods_negotiated' => ['array|false', 'session'=>'resource'], 'ssh2_poll' => ['int', '&polldes'=>'array', 'timeout='=>'int'], @@ -13698,7 +13705,7 @@ 'ssh2_sftp_stat' => ['strict-array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_symlink' => ['bool', 'sftp'=>'resource', 'target'=>'string', 'link'=>'string'], 'ssh2_sftp_unlink' => ['bool', 'sftp'=>'resource', 'filename'=>'string'], -'ssh2_shell' => ['resource|false', 'session'=>'resource', 'term_type='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], +'ssh2_shell' => ['resource|false', 'session'=>'resource', 'termtype='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], 'ssh2_tunnel' => ['resource|false', 'session'=>'resource', 'host'=>'string', 'port'=>'int'], 'stat' => ['strict-array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'filename'=>'string'], 'stats_absolute_deviation' => ['float', 'a'=>'array'], @@ -13818,7 +13825,7 @@ 'str_replace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], 'str_rot13' => ['string', 'string'=>'string'], 'str_shuffle' => ['string', 'string'=>'string'], -'str_split' => ['non-empty-list', 'string'=>'string', 'length='=>'positive-int'], +'str_split' => ['list', 'string'=>'string', 'length='=>'positive-int'], 'str_starts_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_word_count' => ['array|int', 'string'=>'string', 'format='=>'int', 'characters='=>'string'], 'strcasecmp' => ['int', 'string1'=>'string', 'string2'=>'string'], @@ -15670,7 +15677,7 @@ 'XMLDiff\Memory::diff' => ['string', 'from'=>'string', 'to'=>'string'], 'XMLDiff\Memory::merge' => ['string', 'src'=>'string', 'diff'=>'string'], 'XMLReader::close' => ['bool'], -'XMLReader::expand' => ['DOMNode|false', 'basenode='=>'DOMNode'], +'XMLReader::expand' => ['DOMNode|false', 'baseNode='=>'?DOMNode'], 'XMLReader::getAttribute' => ['?string', 'name'=>'string'], 'XMLReader::getAttributeNo' => ['?string', 'index'=>'int'], 'XMLReader::getAttributeNs' => ['?string', 'name'=>'string', 'namespaceuri'=>'string'], diff --git a/dictionaries/CallMap_74_delta.php b/dictionaries/CallMap_74_delta.php index af5e0d976bf..6226077179f 100644 --- a/dictionaries/CallMap_74_delta.php +++ b/dictionaries/CallMap_74_delta.php @@ -18,6 +18,7 @@ 'added' => [ 'ReflectionProperty::getType' => ['?ReflectionType'], 'mb_str_split' => ['list|false', 'string'=>'string', 'length='=>'positive-int', 'encoding='=>'string'], + 'openssl_x509_verify' => ['int', 'certificate'=>'string|resource', 'public_key'=>'string|array|resource'], ], 'changed' => [ 'array_merge' => [ diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index b60e895b99b..163be304fca 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -1201,6 +1201,10 @@ 'old' => ['resource|false', 'certificate'=>'string|resource'], 'new' => ['OpenSSLCertificate|false', 'certificate'=>'OpenSSLCertificate|string'], ], + 'openssl_x509_verify' => [ + 'old' => ['int', 'certificate'=>'string|resource', 'public_key'=>'string|array|resource'], + 'new' => ['int', 'certificate'=>'string|OpenSSLCertificate', 'public_key'=>'string|OpenSSLCertificate|OpenSSLAsymmetricKey|array'], + ], 'parse_str' => [ 'old' => ['void', 'string'=>'string', '&w_result='=>'array'], 'new' => ['void', 'string'=>'string', '&w_result'=>'array'], diff --git a/dictionaries/CallMap_82_delta.php b/dictionaries/CallMap_82_delta.php new file mode 100644 index 00000000000..ecbff9d3972 --- /dev/null +++ b/dictionaries/CallMap_82_delta.php @@ -0,0 +1,36 @@ + [ + 'mysqli_execute_query' => ['mysqli_result|bool', 'mysqli'=>'mysqli', 'query'=>'non-empty-string', 'params='=>'list|null'], + 'mysqli::execute_query' => ['mysqli_result|bool', 'query'=>'non-empty-string', 'params='=>'list|null'], + 'openssl_cipher_key_length' => ['positive-int|false', 'cipher_algo'=>'non-empty-string'], + 'curl_upkeep' => ['bool', 'handle'=>'CurlHandle'], + 'ini_parse_quantity' => ['int', 'shorthand'=>'non-empty-string'], + 'memory_reset_peak_usage' => ['void'], + ], + + 'changed' => [ + 'str_split' => [ + 'old' => ['non-empty-list', 'string'=>'string', 'length='=>'positive-int'], + 'new' => ['list', 'string'=>'string', 'length='=>'positive-int'], + ], + ], + + 'removed' => [ + ], +]; diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index aeb0d8ca095..e9d0bf23515 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -6071,7 +6071,7 @@ 'ReflectionExtension::getFunctions' => ['array'], 'ReflectionExtension::getINIEntries' => ['array'], 'ReflectionExtension::getName' => ['string'], - 'ReflectionExtension::getVersion' => ['string'], + 'ReflectionExtension::getVersion' => ['?string'], 'ReflectionExtension::info' => ['void'], 'ReflectionExtension::isPersistent' => ['bool'], 'ReflectionExtension::isTemporary' => ['bool'], @@ -7757,24 +7757,24 @@ 'SplFileInfo::__construct' => ['void', 'file_name'=>'string'], 'SplFileInfo::__toString' => ['string'], 'SplFileInfo::__wakeup' => ['void'], - 'SplFileInfo::getATime' => ['int'], + 'SplFileInfo::getATime' => ['int|false'], 'SplFileInfo::getBasename' => ['string', 'suffix='=>'string'], - 'SplFileInfo::getCTime' => ['int'], + 'SplFileInfo::getCTime' => ['int|false'], 'SplFileInfo::getExtension' => ['string'], 'SplFileInfo::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplFileInfo::getFilename' => ['string'], - 'SplFileInfo::getGroup' => ['int'], - 'SplFileInfo::getInode' => ['int'], - 'SplFileInfo::getLinkTarget' => ['string'], - 'SplFileInfo::getMTime' => ['int'], - 'SplFileInfo::getOwner' => ['int'], + 'SplFileInfo::getGroup' => ['int|false'], + 'SplFileInfo::getInode' => ['int|false'], + 'SplFileInfo::getLinkTarget' => ['string|false'], + 'SplFileInfo::getMTime' => ['int|false'], + 'SplFileInfo::getOwner' => ['int|false'], 'SplFileInfo::getPath' => ['string'], - 'SplFileInfo::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], + 'SplFileInfo::getPathInfo' => ['SplFileInfo|null', 'class_name='=>'string'], 'SplFileInfo::getPathname' => ['string'], - 'SplFileInfo::getPerms' => ['int'], + 'SplFileInfo::getPerms' => ['int|false'], 'SplFileInfo::getRealPath' => ['string|false'], - 'SplFileInfo::getSize' => ['int'], - 'SplFileInfo::getType' => ['string'], + 'SplFileInfo::getSize' => ['int|false'], + 'SplFileInfo::getType' => ['string|false'], 'SplFileInfo::isDir' => ['bool'], 'SplFileInfo::isExecutable' => ['bool'], 'SplFileInfo::isFile' => ['bool'], @@ -7803,9 +7803,9 @@ 'SplFileObject::ftell' => ['int|false'], 'SplFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], - 'SplFileObject::getATime' => ['int'], + 'SplFileObject::getATime' => ['int|false'], 'SplFileObject::getBasename' => ['string', 'suffix='=>'string'], - 'SplFileObject::getCTime' => ['int'], + 'SplFileObject::getCTime' => ['int|false'], 'SplFileObject::getChildren' => ['null'], 'SplFileObject::getCsvControl' => ['array'], 'SplFileObject::getCurrentLine' => ['string|false'], @@ -7813,19 +7813,19 @@ 'SplFileObject::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplFileObject::getFilename' => ['string'], 'SplFileObject::getFlags' => ['int'], - 'SplFileObject::getGroup' => ['int'], - 'SplFileObject::getInode' => ['int'], - 'SplFileObject::getLinkTarget' => ['string'], - 'SplFileObject::getMTime' => ['int'], + 'SplFileObject::getGroup' => ['int|false'], + 'SplFileObject::getInode' => ['int|false'], + 'SplFileObject::getLinkTarget' => ['string|false'], 'SplFileObject::getMaxLineLen' => ['int'], - 'SplFileObject::getOwner' => ['int'], + 'SplFileObject::getMTime' => ['int|false'], + 'SplFileObject::getOwner' => ['int|false'], 'SplFileObject::getPath' => ['string'], - 'SplFileObject::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], + 'SplFileObject::getPathInfo' => ['SplFileInfo|null', 'class_name='=>'string'], 'SplFileObject::getPathname' => ['string'], - 'SplFileObject::getPerms' => ['int'], + 'SplFileObject::getPerms' => ['int|false'], 'SplFileObject::getRealPath' => ['false|string'], - 'SplFileObject::getSize' => ['int'], - 'SplFileObject::getType' => ['string'], + 'SplFileObject::getSize' => ['int|false'], + 'SplFileObject::getType' => ['string|false'], 'SplFileObject::hasChildren' => ['false'], 'SplFileObject::isDir' => ['bool'], 'SplFileObject::isExecutable' => ['bool'], @@ -7995,9 +7995,9 @@ 'SplTempFileObject::ftell' => ['int'], 'SplTempFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplTempFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], - 'SplTempFileObject::getATime' => ['int'], + 'SplTempFileObject::getATime' => ['int|false'], 'SplTempFileObject::getBasename' => ['string', 'suffix='=>'string'], - 'SplTempFileObject::getCTime' => ['int'], + 'SplTempFileObject::getCTime' => ['int|false'], 'SplTempFileObject::getChildren' => ['null'], 'SplTempFileObject::getCsvControl' => ['array'], 'SplTempFileObject::getCurrentLine' => ['string'], @@ -8005,19 +8005,19 @@ 'SplTempFileObject::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplTempFileObject::getFilename' => ['string'], 'SplTempFileObject::getFlags' => ['int'], - 'SplTempFileObject::getGroup' => ['int'], - 'SplTempFileObject::getInode' => ['int'], - 'SplTempFileObject::getLinkTarget' => ['string'], - 'SplTempFileObject::getMTime' => ['int'], + 'SplTempFileObject::getGroup' => ['int|false'], + 'SplTempFileObject::getInode' => ['int|false'], + 'SplTempFileObject::getLinkTarget' => ['string|false'], 'SplTempFileObject::getMaxLineLen' => ['int'], - 'SplTempFileObject::getOwner' => ['int'], + 'SplTempFileObject::getMTime' => ['int|false'], + 'SplTempFileObject::getOwner' => ['int|false'], 'SplTempFileObject::getPath' => ['string'], 'SplTempFileObject::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplTempFileObject::getPathname' => ['string'], - 'SplTempFileObject::getPerms' => ['int'], - 'SplTempFileObject::getRealPath' => ['string'], - 'SplTempFileObject::getSize' => ['int'], - 'SplTempFileObject::getType' => ['string'], + 'SplTempFileObject::getPerms' => ['int|false'], + 'SplTempFileObject::getRealPath' => ['string|false'], + 'SplTempFileObject::getSize' => ['int|false'], + 'SplTempFileObject::getType' => ['string|false'], 'SplTempFileObject::hasChildren' => ['bool'], 'SplTempFileObject::isDir' => ['bool'], 'SplTempFileObject::isExecutable' => ['bool'], @@ -8497,7 +8497,7 @@ 'XMLDiff\Memory::merge' => ['string', 'src'=>'string', 'diff'=>'string'], 'XMLReader::XML' => ['bool', 'source'=>'string', 'encoding='=>'?string', 'options='=>'int'], 'XMLReader::close' => ['bool'], - 'XMLReader::expand' => ['DOMNode|false', 'basenode='=>'DOMNode'], + 'XMLReader::expand' => ['DOMNode|false', 'baseNode='=>'?DOMNode'], 'XMLReader::getAttribute' => ['?string', 'name'=>'string'], 'XMLReader::getAttributeNo' => ['?string', 'index'=>'int'], 'XMLReader::getAttributeNs' => ['?string', 'name'=>'string', 'namespaceuri'=>'string'], @@ -14056,7 +14056,7 @@ 'ocigetbufferinglob' => ['bool'], 'ocisetbufferinglob' => ['bool', 'lob'=>'bool'], 'octdec' => ['int|float', 'octal_string'=>'string'], - 'odbc_autocommit' => ['mixed', 'odbc'=>'resource', 'enable='=>'bool'], + 'odbc_autocommit' => ['int|bool', 'odbc'=>'resource', 'enable='=>'bool'], 'odbc_binmode' => ['bool', 'statement'=>'resource', 'mode'=>'int'], 'odbc_close' => ['void', 'odbc'=>'resource'], 'odbc_close_all' => ['void'], @@ -14073,7 +14073,7 @@ 'odbc_execute' => ['bool', 'statement'=>'resource', 'params='=>'array'], 'odbc_fetch_array' => ['array|false', 'statement'=>'resource', 'row='=>'int'], 'odbc_fetch_into' => ['int', 'statement'=>'resource', '&w_array'=>'array', 'row='=>'int'], - 'odbc_fetch_object' => ['object|false', 'statement'=>'resource', 'row='=>'int'], + 'odbc_fetch_object' => ['stdClass|false', 'statement'=>'resource', 'row='=>'int'], 'odbc_fetch_row' => ['bool', 'statement'=>'resource', 'row='=>'int'], 'odbc_field_len' => ['int|false', 'statement'=>'resource', 'field'=>'int'], 'odbc_field_name' => ['string|false', 'statement'=>'resource', 'field'=>'int'], @@ -15107,7 +15107,7 @@ 'ssh2_exec' => ['resource|false', 'session'=>'resource', 'command'=>'string', 'pty='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], 'ssh2_fetch_stream' => ['resource|false', 'channel'=>'resource', 'streamid'=>'int'], 'ssh2_fingerprint' => ['string|false', 'session'=>'resource', 'flags='=>'int'], - 'ssh2_forward_accept' => ['resource|false', 'session'=>'resource'], + 'ssh2_forward_accept' => ['resource|false', 'listener'=>'resource'], 'ssh2_forward_listen' => ['resource|false', 'session'=>'resource', 'port'=>'int', 'host='=>'string', 'max_connections='=>'string'], 'ssh2_methods_negotiated' => ['array|false', 'session'=>'resource'], 'ssh2_poll' => ['int', '&polldes'=>'array', 'timeout='=>'int'], @@ -15128,7 +15128,7 @@ 'ssh2_sftp_stat' => ['strict-array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_symlink' => ['bool', 'sftp'=>'resource', 'target'=>'string', 'link'=>'string'], 'ssh2_sftp_unlink' => ['bool', 'sftp'=>'resource', 'filename'=>'string'], - 'ssh2_shell' => ['resource|false', 'session'=>'resource', 'term_type='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], + 'ssh2_shell' => ['resource|false', 'session'=>'resource', 'termtype='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], 'ssh2_tunnel' => ['resource|false', 'session'=>'resource', 'host'=>'string', 'port'=>'int'], 'stat' => ['strict-array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'filename'=>'string'], 'stats_absolute_deviation' => ['float', 'a'=>'array'], diff --git a/docs/annotating_code/type_syntax/array_types.md b/docs/annotating_code/type_syntax/array_types.md index 1a94f0bdbb4..5e3b1b519e3 100644 --- a/docs/annotating_code/type_syntax/array_types.md +++ b/docs/annotating_code/type_syntax/array_types.md @@ -25,15 +25,17 @@ PHP treats all these arrays the same, essentially (though there are some optimis Psalm has a few different ways to represent arrays in its type system: -- [array<int, string>](#generic-arrays) -- [non-empty-array](#non-empty-array) -- [string\[\]](#phpdoc-syntax) -- [list & non-empty-list](#lists) +_Click on the » next to each type to view detailed documentation and examples._ + +- [array<int, string> »](#generic-arrays) +- [non-empty-array »](#non-empty-array) +- [string\[\] »](#phpdoc-syntax) +- [list & non-empty-list »](#lists) - [list<string>](#lists) - [array{foo: int, bar: string} and list{int, string} »](#object-like-arrays) - [Sealed arrays »](#sealed-object-like-arrays) - [Unsealed arrays »](#unsealed-object-like-arrays) -- [callable-array](#callable-arrays) +- [callable-array »](#callable-arrays) ## Generic arrays @@ -191,7 +193,7 @@ avgShape([123.1, 321.0, 1.0, new class {}, 'test']); The above examples contain bugs which can be detected by Psalm *only when using sealed arrays*. -The counterpart to sealed arrays are [unsealed arrays &rauo;](#unsealed-object-like-arrays), generated as intermediate types when asserting raw arrays. +The counterpart to sealed arrays are [unsealed arrays »](#unsealed-object-like-arrays), generated as intermediate types when asserting raw arrays. Unsealed arrays are by definition uncertain, so Psalm can't reason well about them: always convert them to sealed arrays as specified [here »](#unsealed-object-like-arrays). Tip: if you find yourself copying the same complex sealed array shape over and over again to avoid `InvalidArgument` issues, try using [type aliases](utility_types.md#type-aliases), instead. @@ -274,7 +276,6 @@ function avgCoefficient(array $params): float { You can also manually provide a `['a' => $arr['a'], 'b' => $arr['b']]`, but there's an even better way to seamlessly validate user-provided input: - + Tip: if you find yourself copying the same complex sealed array shape over and over again to avoid `InvalidArgument` issues, try using [type aliases](utility_types.md#type-aliases), instead. diff --git a/docs/annotating_code/type_syntax/object_types.md b/docs/annotating_code/type_syntax/object_types.md index eca8397ae54..33e6a63fb73 100644 --- a/docs/annotating_code/type_syntax/object_types.md +++ b/docs/annotating_code/type_syntax/object_types.md @@ -1,8 +1,10 @@ # Object types -- [object](#unnamed-objects) -- [object{foo: string}](#object-properties) -- [Exception, Foo\MyClass and Foo\MyClass](#named-objectsmd) +_Click on the » next to each type to view detailed documentation and examples._ + +- [object »](#unnamed-objects) +- [object{foo: string} »](#object-properties) +- [Exception, Foo\MyClass and Foo\MyClass<Bar> »](#named-objects) - [Generator](#generators) ### Unnamed objects diff --git a/docs/annotating_code/type_syntax/other_types.md b/docs/annotating_code/type_syntax/other_types.md new file mode 100644 index 00000000000..d8744e7cb0d --- /dev/null +++ b/docs/annotating_code/type_syntax/other_types.md @@ -0,0 +1,6 @@ +# Other types + +- `iterable` - represents the [iterable pseudo-type](https://php.net/manual/en/language.types.iterable.php). Like arrays, iterables can have type parameters e.g. `iterable`. +- `void` - can be used in a return type when a function does not return a value. +- `resource` represents a [PHP resource](https://www.php.net/manual/en/language.types.resource.php). +- `closed-resource` represents a [PHP resource](https://www.php.net/manual/en/language.types.resource.php) that was closed (using `fclose` or another closing function). diff --git a/docs/annotating_code/type_syntax/scalar_types.md b/docs/annotating_code/type_syntax/scalar_types.md index 92e8d69d317..b202294d570 100644 --- a/docs/annotating_code/type_syntax/scalar_types.md +++ b/docs/annotating_code/type_syntax/scalar_types.md @@ -1,5 +1,7 @@ # Scalar types +_Click on the » next to each type to view detailed documentation and examples._ + * [bool »](scalar_types.md#scalar) * [int »](scalar_types.md#scalar) * [float »](scalar_types.md#scalar) diff --git a/docs/annotating_code/type_syntax/top_bottom_types.md b/docs/annotating_code/type_syntax/top_bottom_types.md new file mode 100644 index 00000000000..324c021a72a --- /dev/null +++ b/docs/annotating_code/type_syntax/top_bottom_types.md @@ -0,0 +1,15 @@ + +# Top types, bottom types + +## `mixed` + +This is the _top type_ in PHP's type system, and represents a lack of type information. Psalm warns about `mixed` types when the `reportMixedIssues` flag is turned on, or when you're on level 1. + +## `never` +It can be aliased to `no-return` or `never-return` in docblocks. Note: it replaced the old `empty` type that used to exist in Psalm + +This is the _bottom type_ in PHP's type system. It's used to describe a type that has no possible value. It can happen in multiple cases: +- the actual `never` type from PHP 8.1 (can be used in docblocks for older versions). This type can be used as a return type for functions that will never return, either because they always throw exceptions or always exit() +- an union type that have been stripped for all its possible types. (For example, if a variable is `string|int` and we perform a is_bool() check in a condition, the type of the variable in the condition will be `never` as the condition will never be entered) +- it can represent a placeholder for types yet to come — a good example is the type of the empty array `[]`, which Psalm types as `array`, the content of the array is void so it can accept any content +- it can also happen in the same context as the line above for templates that have yet to be defined diff --git a/docs/annotating_code/type_syntax/utility_types.md b/docs/annotating_code/type_syntax/utility_types.md index 8b3fef940cc..6f6670d5802 100644 --- a/docs/annotating_code/type_syntax/utility_types.md +++ b/docs/annotating_code/type_syntax/utility_types.md @@ -2,14 +2,16 @@ Psalm supports some _magical_ utility types that brings superpower to the PHP type system. -- [(T is true ? string : bool)](conditional_types.md) -- [`key-of`](#key-oft) -- [`value-of`](#value-oft) -- [`properties-of`](#properties-oft) -- [`class-string-map`](#class-string-mapt-as-foo-t) -- [`T[K]`](#tk) -- [Type aliases](#type-aliases) -- [Variable templates](#variable-templates) +_Click on the » next to each type to view detailed documentation and examples._ + +- [(T is true ? string : bool) »](conditional_types.md) +- [`key-of` »](#key-oft) +- [`value-of` »](#value-oft) +- [`properties-of` »](#properties-oft) +- [`class-string-map` »](#class-string-mapt-as-foo-t) +- [`T[K]` »](#tk) +- [Type aliases »](#type-aliases) +- [Variable templates »](#variable-templates) ## `key-of` diff --git a/docs/annotating_code/type_syntax/value_types.md b/docs/annotating_code/type_syntax/value_types.md index a9bb86caa14..610274202a2 100644 --- a/docs/annotating_code/type_syntax/value_types.md +++ b/docs/annotating_code/type_syntax/value_types.md @@ -2,10 +2,12 @@ Psalm also allows you to specify values in types. -- [null](#null) -- [true, false](#true-false) -- [6, 7.0, "forty-two" and 'forty two'](#some_string-4-314) -- [Foo\Bar::MY_SCALAR_CONST](#regular-class-constants) +_Click on the » next to each type to view detailed documentation and examples._ + +- [null »](#null) +- [true, false »](#true-false) +- [6, 7.0, "forty-two" and 'forty two' »](#some_string-4-314) +- [Foo\Bar::MY_SCALAR_CONST »](#regular-class-constants) ### null diff --git a/src/Psalm/Internal/Clause.php b/src/Psalm/Internal/Clause.php index 4f4f66e449c..b13fa913467 100644 --- a/src/Psalm/Internal/Clause.php +++ b/src/Psalm/Internal/Clause.php @@ -111,6 +111,7 @@ public function __construct( $possibility_strings[$i] = array_keys($v); } + /** @psalm-suppress ImpureFunctionCall */ $data = serialize($possibility_strings); $this->hash = PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data); } diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 60a9475c417..8b5c2a6851e 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -524,15 +524,26 @@ public function isCallMapFunctionPure( //gettext 'bindtextdomain', - + // hash 'hash_update', 'hash_update_file', 'hash_update_stream', + + // unserialize + 'unserialize' ]; if (in_array(strtolower($function_id), $impure_functions, true)) { return false; } + if ($function_id === 'serialize' && isset($args[0]) && $type_provider) { + $serialize_type = $type_provider->getType($args[0]->value); + + if ($serialize_type && $serialize_type->canContainObjectType($codebase)) { + return false; + } + } + if (strpos($function_id, 'image') === 0) { return false; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 9bc6d5709b0..b6432adf48b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -181,7 +181,7 @@ public static function parse( } } - foreach (['psalm-self-out', 'psalm-this-out'] as $alias) { + foreach (['psalm-self-out', 'psalm-this-out', 'phpstan-self-out', 'phpstan-this-out'] as $alias) { if (isset($parsed_docblock->tags[$alias])) { foreach ($parsed_docblock->tags[$alias] as $offset => $param) { $line_parts = CommentAnalyzer::splitDocLine($param); @@ -485,36 +485,48 @@ public static function parse( } } - if (isset($parsed_docblock->tags['psalm-assert'])) { - foreach ($parsed_docblock->tags['psalm-assert'] as $assertion) { - $line_parts = self::sanitizeAssertionLineParts(CommentAnalyzer::splitDocLine($assertion)); + foreach (['psalm-assert', 'phpstan-assert'] as $assert) { + if (isset($parsed_docblock->tags[$assert])) { + foreach ($parsed_docblock->tags[$assert] as $assertion) { + $line_parts = self::sanitizeAssertionLineParts(CommentAnalyzer::splitDocLine($assertion)); - $info->assertions[] = [ - 'type' => $line_parts[0], - 'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1], - ]; + $info->assertions[] = [ + 'type' => $line_parts[0], + 'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1], + ]; + } + + break; } } - if (isset($parsed_docblock->tags['psalm-assert-if-true'])) { - foreach ($parsed_docblock->tags['psalm-assert-if-true'] as $assertion) { - $line_parts = self::sanitizeAssertionLineParts(CommentAnalyzer::splitDocLine($assertion)); + foreach (['psalm-assert-if-true', 'phpstan-assert-if-true'] as $assert) { + if (isset($parsed_docblock->tags[$assert])) { + foreach ($parsed_docblock->tags[$assert] as $assertion) { + $line_parts = self::sanitizeAssertionLineParts(CommentAnalyzer::splitDocLine($assertion)); - $info->if_true_assertions[] = [ - 'type' => $line_parts[0], - 'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1], - ]; + $info->if_true_assertions[] = [ + 'type' => $line_parts[0], + 'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1], + ]; + } + + break; } } - if (isset($parsed_docblock->tags['psalm-assert-if-false'])) { - foreach ($parsed_docblock->tags['psalm-assert-if-false'] as $assertion) { - $line_parts = self::sanitizeAssertionLineParts(CommentAnalyzer::splitDocLine($assertion)); + foreach (['psalm-assert-if-false', 'phpstan-assert-if-false'] as $assert) { + if (isset($parsed_docblock->tags[$assert])) { + foreach ($parsed_docblock->tags[$assert] as $assertion) { + $line_parts = self::sanitizeAssertionLineParts(CommentAnalyzer::splitDocLine($assertion)); - $info->if_false_assertions[] = [ - 'type' => $line_parts[0], - 'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1], - ]; + $info->if_false_assertions[] = [ + 'type' => $line_parts[0], + 'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1], + ]; + } + + break; } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index 80bd3a7c638..2f0d8c4e220 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -19,12 +19,22 @@ use function array_merge; use function array_shift; +use function in_array; /** * @internal */ class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTypeProviderInterface { + /** + * These functions are already handled by the CoreGenericFunctions stub + */ + const IGNORE_FUNCTION_IDS_FOR_FALSE_RETURN_TYPE = [ + 'reset', + 'end', + 'current', + ]; + /** * @return array */ @@ -85,7 +95,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($value_type->isNever()) { $value_type = Type::getFalse(); - } elseif (($function_id !== 'reset' && $function_id !== 'end') || !$definitely_has_items) { + } elseif (!$definitely_has_items || self::isFunctionAlreadyHandledByStub($function_id)) { $value_type = $value_type->getBuilder()->addType(new TFalse); $codebase = $statements_source->getCodebase(); @@ -108,4 +118,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return $value_type; } + + private static function isFunctionAlreadyHandledByStub(string $function_id): bool + { + return !in_array($function_id, self::IGNORE_FUNCTION_IDS_FOR_FALSE_RETURN_TYPE, true); + } } diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index 7a586e89cae..5a72fbe34d1 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -260,9 +260,11 @@ private static function resolveTags(ParsedDocblock $docblock): void if (isset($docblock->tags['param-out']) || isset($docblock->tags['psalm-param-out']) + || isset($docblock->tags['phpstan-param-out']) ) { $docblock->combined_tags['param-out'] = ($docblock->tags['param-out'] ?? []) + + ($docblock->tags['phpstan-param-out'] ?? []) + ($docblock->tags['psalm-param-out'] ?? []); } } diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index 9ceabd5bd07..b7fd6e43535 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -6,6 +6,7 @@ use Psalm\Internal\Type\TypeExpander; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArrayKey; +use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TIntRange; @@ -20,6 +21,10 @@ use function array_pop; use function array_push; use function array_reverse; +use function count; +use function is_array; + +use const PHP_INT_MAX; /** * @internal @@ -133,6 +138,48 @@ public static function isContainedBy( continue; } + // if params are specified + if ($container_type_part instanceof TCallable + && is_array($container_type_part->params) + && $input_type_part instanceof TCallable + ) { + $container_all_param_count = count($container_type_part->params); + $container_required_param_count = 0; + foreach ($container_type_part->params as $index => $container_param) { + if ($container_param->is_optional === false) { + $container_required_param_count = $index + 1; + } + + if ($container_param->is_variadic === true) { + $container_all_param_count = PHP_INT_MAX; + } + } + + $input_required_param_count = 0; + if (!is_array($input_type_part->params)) { + // it's not declared, there can be an arbitrary number of params + $input_all_param_count = PHP_INT_MAX; + } else { + $input_all_param_count = count($input_type_part->params); + foreach ($input_type_part->params as $index => $input_param) { + if ($input_param->is_optional === false) { + $input_required_param_count = $index + 1; + } + + if ($input_param->is_variadic === true) { + $input_all_param_count = PHP_INT_MAX; + } + } + } + + // too few or too many non-optional params provided in callback + if ($container_required_param_count > $input_all_param_count + || $container_all_param_count < $input_required_param_count + ) { + return false; + } + } + if ($union_comparison_result) { $atomic_comparison_result = new TypeComparisonResult(); } else { diff --git a/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php b/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php new file mode 100644 index 00000000000..7787ca0a660 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php @@ -0,0 +1,48 @@ +codebase = $codebase; + } + + protected function enterNode(TypeNode $type): ?int + { + if (($type instanceof Union + && ($type->hasObjectType() || $type->hasIterable() || $type->hasMixed()) + ) || ($type instanceof Atomic + && ($type->isObjectType() || $type->isIterable($this->codebase) || $type instanceof TMixed) + )) { + $this->contains_object_type = true; + return self::STOP_TRAVERSAL; + } + + return null; + } + + public function matches(): bool + { + return $this->contains_object_type; + } +} diff --git a/src/Psalm/Type/TypeVisitor.php b/src/Psalm/Type/TypeVisitor.php index 8e9122ff455..477f505e122 100644 --- a/src/Psalm/Type/TypeVisitor.php +++ b/src/Psalm/Type/TypeVisitor.php @@ -8,8 +8,6 @@ abstract class TypeVisitor public const DONT_TRAVERSE_CHILDREN = 2; /** - * @internal Can only be called by a TypeNode - * * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ abstract protected function enterNode(TypeNode $type): ?int; diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index 2df7db1515e..fe84bd74409 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -5,6 +5,7 @@ use InvalidArgumentException; use Psalm\CodeLocation; use Psalm\Codebase; +use Psalm\Internal\TypeVisitor\CanContainObjectTypeVisitor; use Psalm\Internal\TypeVisitor\ClasslikeReplacer; use Psalm\Internal\TypeVisitor\ContainsClassLikeVisitor; use Psalm\Internal\TypeVisitor\ContainsLiteralVisitor; @@ -498,6 +499,18 @@ public function hasObjectType(): bool return false; } + /** + * @psalm-mutation-free + */ + public function canContainObjectType(Codebase $codebase): bool + { + $object_type_visitor = new CanContainObjectTypeVisitor($codebase); + + $object_type_visitor->traverseArray($this->types); + + return $object_type_visitor->matches(); + } + /** * @psalm-mutation-free */ @@ -1258,6 +1271,7 @@ public function queueClassLikesForScanning( $phantom_classes ); + /** @psalm-suppress ImpureMethodCall */ $scanner_visitor->traverseArray($this->types); } diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index a00fd2fcb75..e83dff12add 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -135,7 +135,7 @@ function array_flip(array $array) * * @param TArray $array * - * @return (TArray is array ? null : TKey|null) + * @return (TArray is array ? null : (TArray is non-empty-list ? int<0,max> : (TArray is non-empty-array ? TKey : TKey|null))) * @psalm-pure * @psalm-ignore-nullable-return */ @@ -144,7 +144,48 @@ function key($array) } /** - * @psalm-template TArray as array + * @psalm-template TKey as array-key + * @psalm-template TValue + * @psalm-template TArray as array + * + * @param TArray $array + * + * @return (TArray is array ? false : (TArray is non-empty-array ? TValue : TValue|false)) + * @psalm-pure + */ +function current($array) +{ +} + +/** + * @psalm-template TKey as array-key + * @psalm-template TValue + * @psalm-template TArray as array + * + * @param TArray $array + * + * @return (TArray is array ? false : (TArray is non-empty-array ? TValue : TValue|false)) + */ +function reset(&$array) +{ +} + +/** + * @psalm-template TKey as array-key + * @psalm-template TValue + * @psalm-template TArray as array + * + * @param TArray $array + * + * @return (TArray is array ? false : (TArray is non-empty-array ? TValue : TValue|false)) + */ +function end(&$array) +{ +} + +/** + * @psalm-template TKey as array-key + * @psalm-template TArray as array * * @param TArray $array * diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index f4432d23aa6..c69583d9dfb 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -571,7 +571,7 @@ class LimitIterator extends IteratorIterator implements OuterIterator { /** * @param TIterator $iterator */ - public function __construct(Iterator $iterator, int $offset = 0, int $count = -1) {} + public function __construct(Iterator $iterator, int $offset = 0, int $limit = -1) {} /** * @return TValue|null current value or null when iterator is drained diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 001ebf17ae6..50d9d78e4be 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -57,6 +57,8 @@ class ReflectionClassConstant class Attribute { + public int $flags; + public const TARGET_CLASS = 1; public const TARGET_FUNCTION = 2; public const TARGET_METHOD = 4; diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 0809e0996b9..6449fa5d396 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -100,6 +100,21 @@ class ReflectionProperty implements Reflector * @psalm-mutation-free */ public function getType() : ?ReflectionType {} + + /** + * @since 8.0 + */ + public function hasDefaultValue(): bool {} + + /** + * @since 8.0 + */ + public function isPromoted(): bool {} + + /** + * @since 8.1 + */ + public function isReadOnly(): bool {} } class ReflectionMethod implements Reflector @@ -181,6 +196,11 @@ class ReflectionParameter implements Reflector { * @return ($name is null ? array> : array>) */ public function getAttributes(?string $name = null, int $flags = 0): array {} + + /** + * @since 8.0 + */ + public function isPromoted(): bool {} } /** diff --git a/tests/ArgTest.php b/tests/ArgTest.php index 90bb1ebb3fd..cb1cfb9b249 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -323,6 +323,40 @@ function a(array $a): string { a($sealed); ', ], + 'variadicCallbackArgsCountMatch' => [ + 'code' => ' [ + 'code' => ' 'InvalidArgument', ], + 'callbackArgsCountMismatch' => [ + 'code' => ' 'InvalidScalarArgument', + ], + 'callableArgsCountMismatch' => [ + 'code' => ' 'InvalidScalarArgument', + ], ]; } } diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 2ff1985ba1e..48eafc50029 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -1086,7 +1086,7 @@ class Foo { $a = ["one" => 1, "two" => 3]; $b = key($a);', 'assertions' => [ - '$b' => 'null|string', + '$b' => 'string', ], ], 'keyEmptyArray' => [ @@ -1101,12 +1101,90 @@ class Foo { 'code' => ' [ + 'code' => ' 1, "two" => 3]; + $b = current($a);', + 'assertions' => [ + '$b' => 'int', + ], + ], + 'currentEmptyArray' => [ + 'code' => ' [ + '$b' => 'false', + ], + ], + 'currentNonEmptyArray' => [ + 'code' => ' $arr + * @return int + */ + function foo(array $arr) { + return current($arr); + }', + ], + 'reset' => [ + 'code' => ' 1, "two" => 3]; + $b = reset($a);', + 'assertions' => [ + '$b' => 'int', + ], + ], + 'resetEmptyArray' => [ + 'code' => ' [ + '$b' => 'false', + ], + ], + 'resetNonEmptyArray' => [ + 'code' => ' $arr + * @return int + */ + function foo(array $arr) { + return reset($arr); + }', + ], + 'end' => [ + 'code' => ' 1, "two" => 3]; + $b = end($a);', + 'assertions' => [ + '$b' => 'int', + ], + ], + 'endEmptyArray' => [ + 'code' => ' [ + '$b' => 'false', + ], + ], + 'endNonEmptyArray' => [ + 'code' => ' $arr + * @return int + */ + function foo(array $arr) { + return end($arr); + }', + ], 'arrayKeyFirst' => [ 'code' => ' */ diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 0992c3d5d1c..064b06682fe 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -282,6 +282,7 @@ class InternalCallMapHandlerTest extends TestCase 'sqlsrv_prepare', 'sqlsrv_query', 'sqlsrv_server_info', + 'ssh2_forward_accept', 'stomp_abort', 'stomp_ack', 'stomp_begin', @@ -336,7 +337,6 @@ class InternalCallMapHandlerTest extends TestCase */ private static $ignoredReturnTypeOnlyFunctions = [ 'bcsqrt', - 'bzopen', 'cal_from_jd', 'collator_get_strength', 'curl_multi_init', @@ -354,25 +354,19 @@ class InternalCallMapHandlerTest extends TestCase 'date_timestamp_set', 'date_timezone_set', 'datefmt_set_lenient', - 'dba_open', - 'dba_popen', 'deflate_init', 'enchant_broker_init', 'fgetcsv', 'filter_input_array', - 'fopen', 'fpassthru', - 'fsockopen', 'ftp_get_option', 'get_declared_traits', 'gmp_export', 'gmp_hamdist', 'gmp_import', 'gzeof', - 'gzopen', 'gzpassthru', 'iconv_get_encoding', - 'igbinary_serialize', 'imagecolorclosest', 'imagecolorclosestalpha', 'imagecolorclosesthwb', @@ -402,31 +396,11 @@ class InternalCallMapHandlerTest extends TestCase 'ldap_get_attributes', 'mb_encoding_aliases', 'metaphone', - 'mongodb\\bson\\fromjson', - 'mongodb\\bson\\fromphp', - 'mongodb\\bson\\tojson', 'msg_get_queue', 'mysqli_stmt_get_warnings', 'mysqli_stmt_insert_id', 'numfmt_create', 'ob_list_handlers', - 'odbc_autocommit', - 'odbc_columnprivileges', - 'odbc_columns', - 'odbc_connect', - 'odbc_do', - 'odbc_exec', - 'odbc_fetch_object', - 'odbc_foreignkeys', - 'odbc_gettypeinfo', - 'odbc_pconnect', - 'odbc_prepare', - 'odbc_primarykeys', - 'odbc_specialcolumns', - 'odbc_statistics', - 'odbc_tableprivileges', - 'odbc_tables', - 'opendir', 'openssl_random_pseudo_bytes', 'openssl_spki_export', 'openssl_spki_export_challenge', @@ -434,58 +408,36 @@ class InternalCallMapHandlerTest extends TestCase 'parse_url', 'passthru', 'pcntl_exec', - 'pcntl_signal_get_handler', 'pcntl_strerror', - 'pfsockopen', 'pg_port', - 'pg_socket', - 'popen', - 'proc_open', 'pspell_config_create', 'pspell_new', 'pspell_new_config', 'pspell_new_personal', 'register_shutdown_function', 'rewinddir', - 'set_error_handler', - 'set_exception_handler', 'shm_attach', 'shmop_open', 'simplexml_import_dom', 'sleep', 'snmp_set_oid_numeric_print', - 'socket_export_stream', 'socket_import_stream', 'sodium_crypto_aead_chacha20poly1305_encrypt', 'sodium_crypto_aead_chacha20poly1305_ietf_encrypt', 'sodium_crypto_aead_xchacha20poly1305_ietf_encrypt', 'spl_autoload_functions', 'stream_bucket_new', - 'stream_context_create', - 'stream_context_get_default', - 'stream_context_set_default', - 'stream_filter_append', - 'stream_filter_prepend', 'stream_set_chunk_size', - 'stream_socket_accept', - 'stream_socket_client', - 'stream_socket_server', 'substr', 'substr_compare', 'timezone_abbreviations_list', 'timezone_offset_get', - 'tmpfile', 'user_error', 'xml_get_current_byte_index', 'xml_get_current_column_number', 'xml_get_current_line_number', 'xml_get_error_code', 'xml_parser_get_option', - 'yaml_parse', - 'yaml_parse_file', - 'yaml_parse_url', - 'zip_open', - 'zip_read', ]; /** @@ -766,12 +718,12 @@ public function assertEntryReturnType(ReflectionFunction $function, string $entr } else { $expectedType = $function->getReturnType(); } - if ($expectedType === null) { - $this->assertSame('', $entryReturnType, 'CallMap entry has incorrect return type'); - return; - } - $this->assertTypeValidity($expectedType, $entryReturnType, true, 'CallMap entry has incorrect return type'); + if ($expectedType !== null) { + $this->assertTypeValidity($expectedType, $entryReturnType, true, 'CallMap entry has incorrect return type'); + } else { + $this->assertNotEmpty($entryReturnType); + } } /** diff --git a/tests/SuperGlobalsTest.php b/tests/SuperGlobalsTest.php index a1780fd9897..f3d70f36caf 100644 --- a/tests/SuperGlobalsTest.php +++ b/tests/SuperGlobalsTest.php @@ -22,5 +22,14 @@ function returnsList(): array { ', 'assertions' => [] ]; + + yield 'ENV has scalar entries only' => [ + 'code' => ' */ + function f(): array { + return $_ENV; + } + ' + ]; } } diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index fcad3997ba1..f770149ec7f 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -1563,13 +1563,13 @@ function deserialize_object(string $data, string $type) {}' * @template TNewKey of array-key * @template TNewValue * @psalm-param iterable $iterable - * @psalm-param callable(TKey, TValue): iterable $mapper + * @psalm-param callable(TKey): iterable $mapper * @psalm-return \Generator */ function map(iterable $iterable, callable $mapper): Generator { - foreach ($iterable as $key => $value) { - yield from $mapper($key, $value); + foreach ($iterable as $key => $_) { + yield from $mapper($key); } } diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index 4dae0e22b39..3f77e1793d0 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -259,7 +259,6 @@ function contains(array $data, array $needle): bool { while (!empty($needle)) { $key = key($needle); - if ($key === null) continue; $val = $needle[$key]; unset($needle[$key]); diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index 8e5e2892e60..43e078100e7 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -737,6 +737,36 @@ public function unserialize($_serialized) : void {} new Foo();' ], + 'ignoreSerializeAndUnserialize' => [ + 'code' => ' [ 'code' => '