diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index 357a36011bf..ffbf8aacfe4 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -8,8 +8,13 @@ on: types: - published +permissions: + contents: read + jobs: pre_job: + permissions: + actions: write runs-on: ubuntu-latest outputs: should_skip: ${{ steps.skip_check.outputs.should_skip }} @@ -24,6 +29,8 @@ jobs: paths: '["bin/**", "assets/**", "build/**", "dictionaries/**", "src/**", "stubs/**", "psalm", "psalm-language-server", "psalm-plugin", "psalm-refactor", "psalter", "box.json.dist", "composer.json", "config.xsd", "keys.asc.gpg", "scoper.inc.php"]' build-phar: + permissions: + contents: write # for release needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index a09b2ce81c3..b53d3f184a8 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,8 @@ "slevomat/coding-standard": "^7.0", "phpstan/phpdoc-parser": "1.6.4", "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^4.3 || ^5.0 || ^6.0" + "symfony/process": "^4.3 || ^5.0 || ^6.0", + "weirdan/prophecy-shim": "^1.0 || ^2.0" }, "suggest": { "ext-igbinary": "^2.0.5 is required, used to serialize caching data", diff --git a/config.xsd b/config.xsd index 29535209d3d..00caa5508af 100644 --- a/config.xsd +++ b/config.xsd @@ -75,6 +75,7 @@ + @@ -83,24 +84,25 @@ - + - + - + + @@ -110,7 +112,7 @@ - + @@ -118,7 +120,7 @@ - + @@ -131,21 +133,21 @@ - + - + - + @@ -153,21 +155,21 @@ - + - + - + @@ -175,6 +177,7 @@ + @@ -183,10 +186,11 @@ + - + @@ -411,6 +415,7 @@ + @@ -482,7 +487,7 @@ - + @@ -495,11 +500,13 @@ + + @@ -512,12 +519,14 @@ + + @@ -531,11 +540,13 @@ + + @@ -549,11 +560,13 @@ + + @@ -567,11 +580,13 @@ + + @@ -585,11 +600,13 @@ + + @@ -603,11 +620,13 @@ + + @@ -639,28 +658,32 @@ + + - + + + diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index c54df91f60c..2c7587ff2b4 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1066,7 +1066,7 @@ 'classObj::settext' => ['int', 'text'=>'string'], 'classObj::updateFromString' => ['int', 'snippet'=>'string'], 'clearstatcache' => ['void', 'clear_realpath_cache='=>'bool', 'filename='=>'string'], -'cli_get_process_title' => ['string'], +'cli_get_process_title' => ['?string'], 'cli_set_process_title' => ['bool', 'title'=>'string'], 'ClosedGeneratorException::__clone' => ['void'], 'ClosedGeneratorException::__toString' => ['string'], @@ -1106,7 +1106,7 @@ 'Collator::sortWithSortKeys' => ['bool', '&rw_arr'=>'array'], 'collator_asort' => ['bool', 'object'=>'collator', '&rw_array'=>'array', 'flags='=>'int'], 'collator_compare' => ['int', 'object'=>'collator', 'string1'=>'string', 'string2'=>'string'], -'collator_create' => ['Collator', 'locale'=>'string'], +'collator_create' => ['?Collator', 'locale'=>'string'], 'collator_get_attribute' => ['int|false', 'object'=>'collator', 'attribute'=>'int'], 'collator_get_error_code' => ['int', 'object'=>'collator'], 'collator_get_error_message' => ['string', 'object'=>'collator'], @@ -1723,7 +1723,7 @@ 'date_default_timezone_set' => ['bool', 'timezoneId'=>'string'], 'date_diff' => ['DateInterval|false', 'baseObject'=>'DateTimeInterface', 'targetObject'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string', 'object'=>'DateTimeInterface', 'format'=>'string'], -'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], +'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'date_interval_create_from_date_string' => ['DateInterval', 'datetime'=>'string'], 'date_interval_format' => ['string', 'object'=>'DateInterval', 'format'=>'string'], 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'dayOfWeek='=>'int|mixed'], @@ -1744,7 +1744,7 @@ 'datefmt_format' => ['string|false', 'formatter'=>'IntlDateFormatter', 'datetime'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'datetime'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int', 'formatter'=>'IntlDateFormatter'], -'datefmt_get_calendar_object' => ['IntlCalendar', 'formatter'=>'IntlDateFormatter'], +'datefmt_get_calendar_object' => ['IntlCalendar|false|null', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_datetype' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'formatter'=>'IntlDateFormatter'], @@ -1782,7 +1782,7 @@ 'DateTime::createFromInterface' => ['static', 'object' => 'DateTimeInterface'], 'DateTime::diff' => ['DateInterval|false', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string', 'format'=>'string'], -'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], +'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int'], 'DateTime::getTimezone' => ['DateTimeZone|false'], @@ -1793,27 +1793,10 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], -'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], -'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], -'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], -'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'DateTimeImmutable::createFromInterface' => ['static', 'object' => 'DateTimeInterface'], -'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], -'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], -'DateTimeImmutable::format' => ['string', 'format'=>'string'], -'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], -'DateTimeImmutable::getOffset' => ['int'], -'DateTimeImmutable::getTimestamp' => ['int'], -'DateTimeImmutable::getTimezone' => ['DateTimeZone|false'], -'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], -'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], -'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], -'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], -'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], -'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], -'DateTimeImmutable::sub' => ['static|false', 'interval'=>'DateInterval'], +'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], @@ -2096,18 +2079,18 @@ 'DOMComment::__construct' => ['void', 'value='=>'string'], 'DOMDocument::__construct' => ['void', 'version='=>'string', 'encoding='=>'string'], 'DOMDocument::createAttribute' => ['DOMAttr|false', 'name'=>'string'], -'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], +'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string'], 'DOMDocument::createCDATASection' => ['DOMCDATASection|false', 'data'=>'string'], 'DOMDocument::createComment' => ['DOMComment|false', 'data'=>'string'], 'DOMDocument::createDocumentFragment' => ['DOMDocumentFragment|false'], 'DOMDocument::createElement' => ['DOMElement|false', 'name'=>'string', 'value='=>'string'], -'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], +'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value='=>'string'], 'DOMDocument::createEntityReference' => ['DOMEntityReference|false', 'name'=>'string'], 'DOMDocument::createProcessingInstruction' => ['DOMProcessingInstruction|false', 'target'=>'string', 'data='=>'string'], 'DOMDocument::createTextNode' => ['DOMText|false', 'content'=>'string'], 'DOMDocument::getElementById' => ['?DOMElement', 'elementid'=>'string'], 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], -'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMDocument::importNode' => ['DOMNode|false', 'importednode'=>'DOMNode', 'deep='=>'bool'], 'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'], @@ -2139,23 +2122,23 @@ 'DOMElement::get_elements_by_tagname' => ['array', 'name'=>'string'], 'DOMElement::getAttribute' => ['string', 'name'=>'string'], 'DOMElement::getAttributeNode' => ['DOMAttr', 'name'=>'string'], -'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string', 'localname'=>'string'], -'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string|null', 'localname'=>'string'], +'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], -'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::has_attribute' => ['bool', 'name'=>'string'], 'DOMElement::hasAttribute' => ['bool', 'name'=>'string'], -'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::remove_attribute' => ['bool', 'name'=>'string'], 'DOMElement::removeAttribute' => ['bool', 'name'=>'string'], 'DOMElement::removeAttributeNode' => ['bool', 'oldnode'=>'DOMAttr'], -'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::set_attribute' => ['DomAttribute', 'name'=>'string', 'value'=>'string'], 'DOMElement::set_attribute_node' => ['DomNode', 'attr'=>'DOMNode'], 'DOMElement::setAttribute' => ['DOMAttr|false', 'name'=>'string', 'value'=>'string'], 'DOMElement::setAttributeNode' => ['?DOMAttr', 'attr'=>'DOMAttr'], 'DOMElement::setAttributeNodeNS' => ['DOMAttr', 'attr'=>'DOMAttr'], -'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value'=>'string'], +'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value'=>'string'], 'DOMElement::setIdAttribute' => ['void', 'name'=>'string', 'isid'=>'bool'], 'DOMElement::setIdAttributeNode' => ['void', 'attr'=>'DOMAttr', 'isid'=>'bool'], 'DOMElement::setIdAttributeNS' => ['void', 'namespaceuri'=>'string', 'localname'=>'string', 'isid'=>'bool'], @@ -3362,7 +3345,7 @@ 'ftp_put' => ['bool', 'ftp'=>'FTP\Connection', 'remote_filename'=>'string', 'local_filename'=>'string', 'mode='=>'int', 'offset='=>'int'], 'ftp_pwd' => ['string|false', 'ftp'=>'FTP\Connection'], 'ftp_quit' => ['bool', 'ftp'=>'FTP\Connection'], -'ftp_raw' => ['array', 'ftp'=>'FTP\Connection', 'command'=>'string'], +'ftp_raw' => ['?array', 'ftp'=>'FTP\Connection', 'command'=>'string'], 'ftp_rawlist' => ['array|false', 'ftp'=>'FTP\Connection', 'directory'=>'string', 'recursive='=>'bool'], 'ftp_rename' => ['bool', 'ftp'=>'FTP\Connection', 'from'=>'string', 'to'=>'string'], 'ftp_rmdir' => ['bool', 'ftp'=>'FTP\Connection', 'directory'=>'string'], @@ -3633,7 +3616,7 @@ 'GEOSGeometry::__toString' => ['string'], 'GEOSGeometry::project' => ['float', 'other'=>'GEOSGeometry', 'normalized'=>'bool'], 'GEOSGeometry::interpolate' => ['GEOSGeometry', 'dist'=>'float', 'normalized'=>'bool'], -'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray'=>'array'], +'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray='=>'array'], 'GEOSGeometry::offsetCurve' => ['GEOSGeometry', 'dist'=>'float', 'styleArray'=>'array'], 'GEOSGeometry::envelope' => ['GEOSGeometry'], 'GEOSGeometry::intersection' => ['GEOSGeometry', 'geom'=>'GEOSGeometry'], @@ -3646,7 +3629,7 @@ 'GEOSGeometry::centroid' => ['GEOSGeometry'], 'GEOSGeometry::relate' => ['string|bool', 'otherGeom'=>'GEOSGeometry', 'pattern'=>'string'], 'GEOSGeometry::relateBoundaryNodeRule' => ['string', 'otherGeom'=>'GEOSGeometry', 'rule'=>'int'], -'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology'=>'bool'], +'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology='=>'bool'], 'GEOSGeometry::normalize' => ['GEOSGeometry'], 'GEOSGeometry::extractUniquePoints' => ['GEOSGeometry'], 'GEOSGeometry::disjoint' => ['bool', 'geom'=>'GEOSGeometry'], @@ -4363,18 +4346,18 @@ 'HaruPage::stroke' => ['bool', 'close_path='=>'bool'], 'HaruPage::textOut' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], 'HaruPage::textRect' => ['bool', 'left'=>'float', 'top'=>'float', 'right'=>'float', 'bottom'=>'float', 'text'=>'string', 'align='=>'int'], -'hash' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array'], +'hash' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], 'hash_algos' => ['list'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], -'hash_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array'], -'hash_final' => ['string', 'context'=>'HashContext', 'binary='=>'bool'], -'hash_hkdf' => ['string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], -'hash_hmac' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], +'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], +'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'binary='=>'bool'], +'hash_hkdf' => ['non-empty-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], +'hash_hmac' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], 'hash_hmac_algos' => ['list'], -'hash_hmac_file' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], -'hash_init' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array'], -'hash_pbkdf2' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], +'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], +'hash_init' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array{seed:scalar}'], +'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'?resource'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'stream'=>'resource', 'length='=>'int'], @@ -5427,7 +5410,7 @@ 'imagegif' => ['bool', 'image'=>'GdImage', 'file='=>'string|resource|null'], 'imagegrabscreen' => ['false|GdImage'], 'imagegrabwindow' => ['false|GdImage', 'handle'=>'int', 'client_area='=>'int'], -'imageinterlace' => ['int|false', 'image'=>'GdImage', 'enable='=>'int'], +'imageinterlace' => ['bool', 'image'=>'GdImage', 'enable='=>'bool|null'], 'imageistruecolor' => ['bool', 'image'=>'GdImage'], 'imagejpeg' => ['bool', 'image'=>'GdImage', 'file='=>'string|resource|null', 'quality='=>'int'], 'imagelayereffect' => ['bool', 'image'=>'GdImage', 'effect'=>'int'], @@ -6034,7 +6017,7 @@ 'imap_close' => ['bool', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_create' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], -'imap_delete' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], +'imap_delete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_deletemailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], 'imap_expunge' => ['bool', 'imap'=>'IMAP\Connection'], @@ -6091,7 +6074,7 @@ 'imap_thread' => ['array|false', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_timeout' => ['int|bool', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int'], -'imap_undelete' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], +'imap_undelete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'string'=>'string'], 'imap_utf7_encode' => ['string', 'string'=>'string'], @@ -6153,7 +6136,7 @@ 'ini_get' => ['string|false', 'option'=>'string'], 'ini_get_all' => ['array|false', 'extension='=>'?string', 'details='=>'bool'], 'ini_restore' => ['void', 'option'=>'string'], -'ini_set' => ['string|false', 'option'=>'string', 'value'=>'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'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], @@ -6190,7 +6173,7 @@ 'intlcal_after' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_before' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_clear' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'int'], -'intlcal_create_instance' => ['IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], +'intlcal_create_instance' => ['?IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], 'intlcal_equals' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_field_difference' => ['int', 'calendar'=>'IntlCalendar', 'timestamp'=>'float', 'field'=>'int'], 'intlcal_from_date_time' => ['IntlCalendar', 'datetime'=>'DateTime|string'], @@ -6503,8 +6486,8 @@ 'IntlTimeZone::useDaylightTime' => ['bool'], 'intltz_count_equivalent_ids' => ['int', 'timezoneId'=>'string'], 'intltz_create_enumeration' => ['IntlIterator', 'countryOrRawOffset'=>'mixed'], -'intltz_create_time_zone' => ['IntlTimeZone', 'timezoneId'=>'string'], -'intltz_from_date_time_zone' => ['IntlTimeZone', 'timezone'=>'DateTimeZone'], +'intltz_create_time_zone' => ['?IntlTimeZone', 'timezoneId'=>'string'], +'intltz_from_date_time_zone' => ['?IntlTimeZone', 'timezone'=>'DateTimeZone'], 'intltz_get_canonical_id' => ['string', 'timezoneId'=>'string', '&isSystemId'=>'bool'], 'intltz_get_display_name' => ['string', 'timezone'=>'IntlTimeZone', 'dst'=>'bool', 'style'=>'int', 'locale'=>'string'], 'intltz_get_dst_savings' => ['int', 'timezone'=>'IntlTimeZone'], @@ -6923,22 +6906,22 @@ 'Locale::parseLocale' => ['array', 'locale'=>'string'], 'Locale::setDefault' => ['bool', 'locale'=>'string'], 'locale_accept_from_http' => ['string|false', 'header'=>'string'], -'locale_canonicalize' => ['string', 'locale'=>'string'], +'locale_canonicalize' => ['?string', 'locale'=>'string'], 'locale_compose' => ['string|false', 'subtags'=>'array'], -'locale_filter_matches' => ['bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'locale_get_all_variants' => ['array', 'locale'=>'string'], +'locale_filter_matches' => ['?bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'locale_get_all_variants' => ['?array', 'locale'=>'string'], 'locale_get_default' => ['string'], 'locale_get_display_language' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_name' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_region' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_script' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_variant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], -'locale_get_keywords' => ['array|false', 'locale'=>'string'], -'locale_get_primary_language' => ['string', 'locale'=>'string'], -'locale_get_region' => ['string', 'locale'=>'string'], -'locale_get_script' => ['string', 'locale'=>'string'], -'locale_lookup' => ['string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], -'locale_parse' => ['array', 'locale'=>'string'], +'locale_get_keywords' => ['array|false|null', 'locale'=>'string'], +'locale_get_primary_language' => ['?string', 'locale'=>'string'], +'locale_get_region' => ['?string', 'locale'=>'string'], +'locale_get_script' => ['?string', 'locale'=>'string'], +'locale_lookup' => ['?string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], +'locale_parse' => ['?array', 'locale'=>'string'], 'locale_set_default' => ['bool', 'locale'=>'string'], 'localeconv' => ['array'], 'localtime' => ['array', 'timestamp='=>'int', 'associative='=>'bool'], @@ -7279,7 +7262,7 @@ 'mb_ereg' => ['bool', 'pattern'=>'string', 'string'=>'string', '&w_matches='=>'array|null'], 'mb_ereg_match' => ['bool', 'pattern'=>'string', 'string'=>'string', 'options='=>'string|null'], 'mb_ereg_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string|null'], -'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], +'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], 'mb_ereg_search' => ['bool', 'pattern='=>'string|null', 'options='=>'string|null'], 'mb_ereg_search_getpos' => ['int'], 'mb_ereg_search_getregs' => ['string[]|false'], @@ -7710,256 +7693,314 @@ 'MongoDB::setReadPreference' => ['bool', 'read_preference'=>'string', 'tags='=>'array'], 'MongoDB::setSlaveOkay' => ['bool', 'ok='=>'bool'], 'MongoDB::setWriteConcern' => ['bool', 'w'=>'mixed', 'wtimeout='=>'int'], -'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type'=>'int'], -'MongoDB\BSON\Binary::__toString' => ['string'], +'MongoDB\BSON\Binary::__construct' => ['void', 'data' => 'string', 'type' => 'int'], 'MongoDB\BSON\Binary::getData' => ['string'], 'MongoDB\BSON\Binary::getType' => ['int'], -'MongoDB\BSON\binary::jsonSerialize' => ['mixed'], -'MongoDB\BSON\binary::serialize' => ['string'], -'MongoDB\BSON\binary::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\binaryinterface::__toString' => ['string'], -'MongoDB\BSON\binaryinterface::getData' => ['string'], -'MongoDB\BSON\binaryinterface::getType' => ['int'], -'MongoDB\BSON\dbpointer::__construct' => ['void'], -'MongoDB\BSON\dbpointer::__toString' => ['string'], -'MongoDB\BSON\dbpointer::jsonSerialize' => ['mixed'], -'MongoDB\BSON\dbpointer::serialize' => ['string'], -'MongoDB\BSON\dbpointer::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Decimal128::__construct' => ['void', 'value='=>'string'], +'MongoDB\BSON\Binary::__toString' => ['string'], +'MongoDB\BSON\Binary::serialize' => ['string'], +'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], +'MongoDB\BSON\BinaryInterface::getData' => ['string'], +'MongoDB\BSON\BinaryInterface::getType' => ['int'], +'MongoDB\BSON\BinaryInterface::__toString' => ['string'], +'MongoDB\BSON\DBPointer::__toString' => ['string'], +'MongoDB\BSON\DBPointer::serialize' => ['string'], +'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], -'MongoDB\BSON\decimal128::jsonSerialize' => ['mixed'], -'MongoDB\BSON\decimal128::serialize' => ['string'], -'MongoDB\BSON\decimal128::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\decimal128interface::__toString' => ['string'], -'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], -'MongoDB\BSON\fromPHP' => ['string', 'value'=>'array|object'], -'MongoDB\BSON\int64::__construct' => ['void'], -'MongoDB\BSON\int64::__toString' => ['string'], -'MongoDB\BSON\int64::jsonSerialize' => ['mixed'], -'MongoDB\BSON\int64::serialize' => ['string'], -'MongoDB\BSON\int64::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'array|object'], -'MongoDB\BSON\javascript::__toString' => ['string'], -'MongoDB\BSON\javascript::getCode' => ['string'], -'MongoDB\BSON\javascript::getScope' => ['?object'], -'MongoDB\BSON\javascript::jsonSerialize' => ['mixed'], -'MongoDB\BSON\javascript::serialize' => ['string'], -'MongoDB\BSON\javascript::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\javascriptinterface::__toString' => ['string'], -'MongoDB\BSON\javascriptinterface::getCode' => ['string'], -'MongoDB\BSON\javascriptinterface::getScope' => ['?object'], -'MongoDB\BSON\maxkey::__construct' => ['void'], -'MongoDB\BSON\maxkey::jsonSerialize' => ['mixed'], -'MongoDB\BSON\maxkey::serialize' => ['string'], -'MongoDB\BSON\maxkey::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\minkey::__construct' => ['void'], -'MongoDB\BSON\minkey::jsonSerialize' => ['mixed'], -'MongoDB\BSON\minkey::serialize' => ['string'], -'MongoDB\BSON\minkey::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'string'], +'MongoDB\BSON\Decimal128::serialize' => ['string'], +'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], +'MongoDB\BSON\Int64::__toString' => ['string'], +'MongoDB\BSON\Int64::serialize' => ['string'], +'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Javascript::__construct' => ['void', 'code' => 'string', 'scope=' => 'object|array|null'], +'MongoDB\BSON\Javascript::getCode' => ['string'], +'MongoDB\BSON\Javascript::getScope' => ['?object'], +'MongoDB\BSON\Javascript::__toString' => ['string'], +'MongoDB\BSON\Javascript::serialize' => ['string'], +'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], +'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], +'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], +'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], +'MongoDB\BSON\MaxKey::serialize' => ['string'], +'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\MinKey::serialize' => ['string'], +'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], +'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], -'MongoDB\BSON\objectid::getTimestamp' => ['int'], -'MongoDB\BSON\objectid::jsonSerialize' => ['mixed'], -'MongoDB\BSON\objectid::serialize' => ['string'], -'MongoDB\BSON\objectid::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\objectidinterface::__toString' => ['string'], -'MongoDB\BSON\objectidinterface::getTimestamp' => ['int'], -'MongoDB\BSON\Regex::__construct' => ['void', 'pattern'=>'string', 'flags='=>'string'], -'MongoDB\BSON\Regex::__toString' => ['string'], -'MongoDB\BSON\Regex::getFlags' => ['string'], +'MongoDB\BSON\ObjectId::serialize' => ['string'], +'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], +'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], +'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], -'MongoDB\BSON\regex::jsonSerialize' => ['mixed'], -'MongoDB\BSON\regex::serialize' => ['string'], -'MongoDB\BSON\regex::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\regexinterface::__toString' => ['string'], -'MongoDB\BSON\regexinterface::getFlags' => ['string'], -'MongoDB\BSON\regexinterface::getPattern' => ['string'], -'MongoDB\BSON\Serializable::bsonSerialize' => ['array|object'], -'MongoDB\BSON\symbol::__construct' => ['void'], -'MongoDB\BSON\symbol::__toString' => ['string'], -'MongoDB\BSON\symbol::jsonSerialize' => ['mixed'], -'MongoDB\BSON\symbol::serialize' => ['string'], -'MongoDB\BSON\symbol::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'int', 'timestamp'=>'int'], +'MongoDB\BSON\Regex::getFlags' => ['string'], +'MongoDB\BSON\Regex::__toString' => ['string'], +'MongoDB\BSON\Regex::serialize' => ['string'], +'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], +'MongoDB\BSON\RegexInterface::getPattern' => ['string'], +'MongoDB\BSON\RegexInterface::getFlags' => ['string'], +'MongoDB\BSON\RegexInterface::__toString' => ['string'], +'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], +'MongoDB\BSON\Symbol::__toString' => ['string'], +'MongoDB\BSON\Symbol::serialize' => ['string'], +'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], +'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], +'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], -'MongoDB\BSON\timestamp::getIncrement' => ['int'], -'MongoDB\BSON\timestamp::getTimestamp' => ['int'], -'MongoDB\BSON\timestamp::jsonSerialize' => ['mixed'], -'MongoDB\BSON\timestamp::serialize' => ['string'], -'MongoDB\BSON\timestamp::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\timestampinterface::__toString' => ['string'], -'MongoDB\BSON\timestampinterface::getIncrement' => ['int'], -'MongoDB\BSON\timestampinterface::getTimestamp' => ['int'], -'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], -'MongoDB\BSON\toPHP' => ['object', 'bson'=>'string', 'typeMap='=>'array'], -'MongoDB\BSON\undefined::__construct' => ['void'], -'MongoDB\BSON\undefined::__toString' => ['string'], -'MongoDB\BSON\undefined::jsonSerialize' => ['mixed'], -'MongoDB\BSON\undefined::serialize' => ['string'], -'MongoDB\BSON\undefined::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data'=>'array'], -'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'int|DateTimeInterface'], -'MongoDB\BSON\UTCDateTime::__toString' => ['string'], -'MongoDB\BSON\utcdatetime::jsonSerialize' => ['mixed'], -'MongoDB\BSON\utcdatetime::serialize' => ['string'], +'MongoDB\BSON\Timestamp::serialize' => ['string'], +'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], +'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], +'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], +'MongoDB\BSON\TimestampInterface::__toString' => ['string'], +'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds=' => 'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], -'MongoDB\BSON\utcdatetime::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\utcdatetimeinterface::__toString' => ['string'], -'MongoDB\BSON\utcdatetimeinterface::toDateTime' => ['DateTime'], -'MongoDB\Driver\BulkWrite::__construct' => ['void', 'ordered='=>'bool'], +'MongoDB\BSON\UTCDateTime::__toString' => ['string'], +'MongoDB\BSON\UTCDateTime::serialize' => ['string'], +'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], +'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], +'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], +'MongoDB\BSON\Undefined::__toString' => ['string'], +'MongoDB\BSON\Undefined::serialize' => ['string'], +'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], +'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], 'MongoDB\Driver\BulkWrite::count' => ['int'], -'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'array|object', 'deleteOptions='=>'array'], -'MongoDB\Driver\BulkWrite::insert' => ['void|MongoDB\BSON\ObjectId', 'document'=>'array|object'], -'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array'], -'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'array|object'], -'MongoDB\Driver\Cursor::__construct' => ['void', 'server'=>'Server', 'responseDocument'=>'string'], +'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter' => 'object|array', 'deleteOptions=' => '?array'], +'MongoDB\Driver\BulkWrite::insert' => ['mixed', 'document' => 'object|array'], +'MongoDB\Driver\BulkWrite::update' => ['void', 'filter' => 'object|array', 'newObj' => 'object|array', 'updateOptions=' => '?array'], +'MongoDB\Driver\ClientEncryption::__construct' => ['void', 'options' => 'array'], +'MongoDB\Driver\ClientEncryption::addKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], +'MongoDB\Driver\ClientEncryption::createDataKey' => ['MongoDB\BSON\Binary', 'kmsProvider' => 'string', 'options=' => '?array'], +'MongoDB\Driver\ClientEncryption::decrypt' => ['mixed', 'value' => 'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::deleteKey' => ['object', 'keyId' => 'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::encrypt' => ['MongoDB\BSON\Binary', 'value' => 'mixed', 'options=' => '?array'], +'MongoDB\Driver\ClientEncryption::getKey' => ['?object', 'keyId' => 'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::getKeyByAltName' => ['?object', 'keyAltName' => 'string'], +'MongoDB\Driver\ClientEncryption::getKeys' => ['MongoDB\Driver\Cursor'], +'MongoDB\Driver\ClientEncryption::removeKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], +'MongoDB\Driver\ClientEncryption::rewrapManyDataKey' => ['object', 'filter' => 'object|array', 'options=' => '?array'], +'MongoDB\Driver\Command::__construct' => ['void', 'document' => 'object|array', 'commandOptions=' => '?array'], +'MongoDB\Driver\Cursor::current' => ['object|array|null'], 'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Cursor::isDead' => ['bool'], -'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], +'MongoDB\Driver\Cursor::key' => ['?int'], +'MongoDB\Driver\Cursor::next' => ['void'], +'MongoDB\Driver\Cursor::rewind' => ['void'], +'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap' => 'array'], 'MongoDB\Driver\Cursor::toArray' => ['array'], -'MongoDB\Driver\CursorId::__construct' => ['void', 'id'=>'string'], +'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], -'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized'=>'string'], -'mongodb\driver\exception\commandexception::getResultDocument' => ['object'], -'MongoDB\Driver\Exception\RuntimeException::__clone' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], +'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], +'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\CursorInterface::isDead' => ['bool'], +'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap' => 'array'], +'MongoDB\Driver\CursorInterface::toArray' => ['array'], +'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], +'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], +'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], +'MongoDB\Driver\Exception\CommandException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\EncryptionException::__toString' => ['string'], +'MongoDB\Driver\Exception\Exception::__toString' => ['string'], +'MongoDB\Driver\Exception\ExecutionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\InvalidArgumentException::__toString' => ['string'], +'MongoDB\Driver\Exception\LogicException::__toString' => ['string'], +'MongoDB\Driver\Exception\RuntimeException::hasErrorLabel' => ['bool', 'errorLabel' => 'string'], 'MongoDB\Driver\Exception\RuntimeException::__toString' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::getCode' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getFile' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getLine' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getMessage' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\RuntimeException::getTrace' => ['list\',args?:array}>'], -'MongoDB\Driver\Exception\RuntimeException::getTraceAsString' => ['string'], -'mongodb\driver\exception\runtimeexception::hasErrorLabel' => ['bool', 'errorLabel'=>'string'], -'MongoDB\Driver\Exception\WriteException::__clone' => ['void'], -'MongoDB\Driver\Exception\WriteException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], -'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], -'MongoDB\Driver\Exception\WriteException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\WriteException::getCode' => ['int'], -'MongoDB\Driver\Exception\WriteException::getFile' => ['string'], -'MongoDB\Driver\Exception\WriteException::getLine' => ['int'], -'MongoDB\Driver\Exception\WriteException::getMessage' => ['string'], -'MongoDB\Driver\Exception\WriteException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\WriteException::getTrace' => ['list\',args?:array}>'], -'MongoDB\Driver\Exception\WriteException::getTraceAsString' => ['string'], +'MongoDB\Driver\Exception\SSLConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ServerException::__toString' => ['string'], +'MongoDB\Driver\Exception\UnexpectedValueException::__toString' => ['string'], 'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], -'MongoDB\Driver\Manager::__construct' => ['void', 'uri'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::executeDelete' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'deleteOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeInsert' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'document'=>'array|object', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'mongodb\driver\manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'mongodb\driver\manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'MongoDB\Driver\Manager::executeUpdate' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'mongodb\driver\manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], +'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], +'MongoDB\Driver\Manager::__construct' => ['void', 'uri=' => '?string', 'uriOptions=' => '?array', 'driverOptions=' => '?array'], +'MongoDB\Driver\Manager::addSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::createClientEncryption' => ['MongoDB\Driver\ClientEncryption', 'options' => 'array'], +'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulk' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Manager::getEncryptedFieldsMap' => ['object|array|null'], 'MongoDB\Driver\Manager::getReadConcern' => ['MongoDB\Driver\ReadConcern'], 'MongoDB\Driver\Manager::getReadPreference' => ['MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::getServers' => ['MongoDB\Driver\Server[]'], +'MongoDB\Driver\Manager::getServers' => ['array'], 'MongoDB\Driver\Manager::getWriteConcern' => ['MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference'=>'MongoDB\Driver\ReadPreference'], -'mongodb\driver\manager::startSession' => ['MongoDB\Driver\Session', 'options='=>'array'], -'mongodb\driver\monitoring\commandfailedevent::getCommandName' => ['string'], -'mongodb\driver\monitoring\commandfailedevent::getDurationMicros' => ['int'], -'mongodb\driver\monitoring\commandfailedevent::getError' => ['Exception'], -'mongodb\driver\monitoring\commandfailedevent::getOperationId' => ['string'], -'mongodb\driver\monitoring\commandfailedevent::getReply' => ['object'], -'mongodb\driver\monitoring\commandfailedevent::getRequestId' => ['string'], -'mongodb\driver\monitoring\commandfailedevent::getServer' => ['MongoDB\Driver\Server'], -'mongodb\driver\monitoring\commandstartedevent::getCommand' => ['object'], -'mongodb\driver\monitoring\commandstartedevent::getCommandName' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getDatabaseName' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getOperationId' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getRequestId' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getServer' => ['MongoDB\Driver\Server'], -'mongodb\driver\monitoring\commandsubscriber::commandFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandFailedEvent'], -'mongodb\driver\monitoring\commandsubscriber::commandStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandStartedEvent'], -'mongodb\driver\monitoring\commandsubscriber::commandSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandSucceededEvent'], -'mongodb\driver\monitoring\commandsucceededevent::getCommandName' => ['string'], -'mongodb\driver\monitoring\commandsucceededevent::getDurationMicros' => ['int'], -'mongodb\driver\monitoring\commandsucceededevent::getOperationId' => ['string'], -'mongodb\driver\monitoring\commandsucceededevent::getReply' => ['object'], -'mongodb\driver\monitoring\commandsucceededevent::getRequestId' => ['string'], -'mongodb\driver\monitoring\commandsucceededevent::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'array|object', 'queryOptions='=>'array'], -'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'string'], -'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object'], +'MongoDB\Driver\Manager::removeSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\Manager::startSession' => ['MongoDB\Driver\Session', 'options=' => '?array'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommand' => ['object'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getDatabaseName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandStartedEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandSucceededEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandFailedEvent'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerOpeningEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyOpeningEvent'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getNewDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPreviousDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getNewDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getPreviousDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Query::__construct' => ['void', 'filter' => 'object|array', 'queryOptions=' => '?array'], +'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level=' => '?string'], 'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], +'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], -'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'array', 'options='=>'array'], -'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object'], -'MongoDB\Driver\ReadPreference::getHedge' => ['object|null'], +'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], +'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], 'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], +'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], -'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\Driver\Server::__construct' => ['void', 'host'=>'string', 'port'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'zwrite'=>'BulkWrite'], -'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command'], -'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'zquery'=>'Query'], -'mongodb\driver\server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'mongodb\driver\server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'mongodb\driver\server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], +'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], 'MongoDB\Driver\Server::getHost' => ['string'], 'MongoDB\Driver\Server::getInfo' => ['array'], -'MongoDB\Driver\Server::getLatency' => ['int'], +'MongoDB\Driver\Server::getLatency' => ['?int'], 'MongoDB\Driver\Server::getPort' => ['int'], -'MongoDB\Driver\Server::getState' => [''], +'MongoDB\Driver\Server::getServerDescription' => ['MongoDB\Driver\ServerDescription'], 'MongoDB\Driver\Server::getTags' => ['array'], 'MongoDB\Driver\Server::getType' => ['int'], 'MongoDB\Driver\Server::isArbiter' => ['bool'], -'MongoDB\Driver\Server::isDelayed' => [''], 'MongoDB\Driver\Server::isHidden' => ['bool'], 'MongoDB\Driver\Server::isPassive' => ['bool'], 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], -'mongodb\driver\session::__construct' => ['void'], -'mongodb\driver\session::abortTransaction' => ['void'], -'mongodb\driver\session::advanceClusterTime' => ['void', 'clusterTime'=>'array|object'], -'mongodb\driver\session::advanceOperationTime' => ['void', 'operationTime'=>'MongoDB\BSON\TimestampInterface'], -'mongodb\driver\session::commitTransaction' => ['void'], -'mongodb\driver\session::endSession' => ['void'], -'mongodb\driver\session::getClusterTime' => ['?object'], -'mongodb\driver\session::getLogicalSessionId' => ['object'], -'mongodb\driver\session::getOperationTime' => ['MongoDB\BSON\Timestamp|null'], -'mongodb\driver\session::getTransactionOptions' => ['array|null'], -'mongodb\driver\session::getTransactionState' => ['string'], -'mongodb\driver\session::startTransaction' => ['void', 'options'=>'array|object'], -'MongoDB\Driver\WriteConcern::__construct' => ['void', 'wstring'=>'string|int', 'wtimeout='=>'int', 'journal='=>'bool'], -'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object'], +'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], +'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], +'MongoDB\Driver\ServerApi::serialize' => ['string'], +'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], +'MongoDB\Driver\ServerDescription::getHost' => ['string'], +'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], +'MongoDB\Driver\ServerDescription::getPort' => ['int'], +'MongoDB\Driver\ServerDescription::getRoundTripTime' => ['?int'], +'MongoDB\Driver\ServerDescription::getType' => ['string'], +'MongoDB\Driver\Session::abortTransaction' => ['void'], +'MongoDB\Driver\Session::advanceClusterTime' => ['void', 'clusterTime' => 'object|array'], +'MongoDB\Driver\Session::advanceOperationTime' => ['void', 'operationTime' => 'MongoDB\BSON\TimestampInterface'], +'MongoDB\Driver\Session::commitTransaction' => ['void'], +'MongoDB\Driver\Session::endSession' => ['void'], +'MongoDB\Driver\Session::getClusterTime' => ['?object'], +'MongoDB\Driver\Session::getLogicalSessionId' => ['object'], +'MongoDB\Driver\Session::getOperationTime' => ['?MongoDB\BSON\Timestamp'], +'MongoDB\Driver\Session::getServer' => ['?MongoDB\Driver\Server'], +'MongoDB\Driver\Session::getTransactionOptions' => ['?array'], +'MongoDB\Driver\Session::getTransactionState' => ['string'], +'MongoDB\Driver\Session::isDirty' => ['bool'], +'MongoDB\Driver\Session::isInTransaction' => ['bool'], +'MongoDB\Driver\Session::startTransaction' => ['void', 'options=' => '?array'], +'MongoDB\Driver\TopologyDescription::getServers' => ['array'], +'MongoDB\Driver\TopologyDescription::getType' => ['string'], +'MongoDB\Driver\TopologyDescription::hasReadableServer' => ['bool', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\TopologyDescription::hasWritableServer' => ['bool'], +'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w' => 'string|int', 'wtimeout=' => '?int', 'journal=' => '?bool'], 'MongoDB\Driver\WriteConcern::getJournal' => ['?bool'], -'MongoDB\Driver\WriteConcern::getJurnal' => ['?bool'], -'MongoDB\Driver\WriteConcern::getW' => ['int|null|string'], +'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], +'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], -'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized'=>'string'], +'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], -'MongoDB\Driver\WriteConcernError::getInfo' => ['mixed'], +'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], 'MongoDB\Driver\WriteError::getCode' => ['int'], 'MongoDB\Driver\WriteError::getIndex' => ['int'], -'MongoDB\Driver\WriteError::getInfo' => ['mixed'], +'MongoDB\Driver\WriteError::getInfo' => ['?object'], 'MongoDB\Driver\WriteError::getMessage' => ['string'], -'MongoDB\Driver\WriteException::getWriteResult' => [''], -'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getInfo' => [''], 'MongoDB\Driver\WriteResult::getInsertedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getMatchedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getModifiedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getUpsertedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\WriteResult::getUpsertedIds' => ['array'], -'MongoDB\Driver\WriteResult::getWriteConcernError' => ['MongoDB\Driver\WriteConcernError|null'], -'MongoDB\Driver\WriteResult::getWriteErrors' => ['MongoDB\Driver\WriteError[]'], +'MongoDB\Driver\WriteResult::getWriteConcernError' => ['?MongoDB\Driver\WriteConcernError'], +'MongoDB\Driver\WriteResult::getWriteErrors' => ['array'], 'MongoDB\Driver\WriteResult::isAcknowledged' => ['bool'], 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['?array', 'db'=>'MongoDB', 'ref'=>'array'], @@ -8167,7 +8208,7 @@ 'msg_send' => ['bool', 'queue'=>'resource', 'message_type'=>'int', 'message'=>'mixed', 'serialize='=>'bool', 'blocking='=>'bool', '&w_error_code='=>'int'], 'msg_set_queue' => ['bool', 'queue'=>'resource', 'data'=>'array'], 'msg_stat_queue' => ['array', 'queue'=>'resource'], -'msgfmt_create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], +'msgfmt_create' => ['?MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'msgfmt_format' => ['string|false', 'formatter'=>'MessageFormatter', 'values'=>'array'], 'msgfmt_format_message' => ['string|false', 'locale'=>'string', 'pattern'=>'string', 'values'=>'array'], 'msgfmt_get_error_code' => ['int', 'formatter'=>'MessageFormatter'], @@ -8481,7 +8522,7 @@ 'mysqli_field_tell' => ['int', 'result'=>'mysqli_result'], 'mysqli_free_result' => ['void', 'result'=>'mysqli_result'], 'mysqli_get_cache_stats' => ['array|false'], -'mysqli_get_charset' => ['object', 'mysql'=>'mysqli'], +'mysqli_get_charset' => ['?object', 'mysql'=>'mysqli'], 'mysqli_get_client_info' => ['string', 'mysql='=>'?mysqli'], 'mysqli_get_client_stats' => ['array'], 'mysqli_get_client_version' => ['int', 'link'=>'mysqli'], @@ -9028,7 +9069,7 @@ 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter|false', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], -'NumberFormatter::formatCurrency' => ['string', 'num'=>'float', 'currency'=>'string'], +'NumberFormatter::formatCurrency' => ['string|false', 'num'=>'float', 'currency'=>'string'], 'NumberFormatter::getAttribute' => ['int|false', 'attr'=>'int'], 'NumberFormatter::getErrorCode' => ['int'], 'NumberFormatter::getErrorMessage' => ['string'], @@ -11309,7 +11350,7 @@ 'RedisCluster::zScore' => ['float', 'key'=>'string', 'member'=>'string'], 'RedisCluster::zUnionStore' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], 'Reflection::export' => ['?string', 'r'=>'reflector', 'return='=>'bool'], -'Reflection::getModifierNames' => ['array', 'modifiers'=>'int'], +'Reflection::getModifierNames' => ['list', 'modifiers'=>'int'], 'ReflectionClass::__clone' => ['void'], 'ReflectionClass::__construct' => ['void', 'argument'=>'object|class-string'], 'ReflectionClass::__toString' => ['string'], @@ -11608,7 +11649,7 @@ 'ReflectionProperty::getModifiers' => ['int'], 'ReflectionProperty::getName' => ['string'], 'ReflectionProperty::getType' => ['?ReflectionType'], -'ReflectionProperty::getValue' => ['mixed', 'object='=>'object'], +'ReflectionProperty::getValue' => ['mixed', 'object='=>'null|object'], 'ReflectionProperty::hasType' => ['bool'], 'ReflectionProperty::isDefault' => ['bool'], 'ReflectionProperty::isPrivate' => ['bool'], @@ -11783,7 +11824,7 @@ 'SAMConnection::unsubscribe' => ['bool', 'subscriptionid'=>'string', 'targettopic='=>'string'], 'SAMMessage::body' => ['string'], 'SAMMessage::header' => ['object'], -'sapi_windows_cp_conv' => ['string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], +'sapi_windows_cp_conv' => ['?string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], 'sapi_windows_cp_get' => ['int'], 'sapi_windows_cp_is_utf8' => ['bool'], 'sapi_windows_cp_set' => ['bool', 'codepage'=>'int'], @@ -13786,7 +13827,7 @@ 'strcoll' => ['int', 'string1'=>'string', 'string2'=>'string'], 'strcspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'int'], 'stream_bucket_append' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], -'stream_bucket_make_writeable' => ['object', 'brigade'=>'resource'], +'stream_bucket_make_writeable' => ['?object', 'brigade'=>'resource'], 'stream_bucket_new' => ['object|false', 'stream'=>'resource', 'buffer'=>'string'], 'stream_bucket_prepend' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], 'stream_context_create' => ['resource', 'options='=>'array', 'params='=>'array'], @@ -14590,16 +14631,16 @@ 'tidy_config_count' => ['int', 'tidy'=>'tidy'], 'tidy_diagnose' => ['bool', 'tidy'=>'tidy'], 'tidy_error_count' => ['int', 'tidy'=>'tidy'], -'tidy_get_body' => ['tidyNode', 'tidy'=>'tidy'], +'tidy_get_body' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_config' => ['array', 'tidy'=>'tidy'], 'tidy_get_error_buffer' => ['string', 'tidy'=>'tidy'], -'tidy_get_head' => ['tidyNode', 'tidy'=>'tidy'], -'tidy_get_html' => ['tidyNode', 'tidy'=>'tidy'], +'tidy_get_head' => ['?tidyNode', 'tidy'=>'tidy'], +'tidy_get_html' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_html_ver' => ['int', 'tidy'=>'tidy'], 'tidy_get_opt_doc' => ['string', 'tidy'=>'tidy', 'option'=>'string'], 'tidy_get_output' => ['string', 'tidy'=>'tidy'], 'tidy_get_release' => ['string'], -'tidy_get_root' => ['tidyNode', 'tidy'=>'tidy'], +'tidy_get_root' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_status' => ['int', 'tidy'=>'tidy'], 'tidy_getopt' => ['mixed', 'tidy'=>'string', 'option'=>'tidy'], 'tidy_is_xhtml' => ['bool', 'tidy'=>'tidy'], @@ -14868,7 +14909,7 @@ 'Transliterator::transliterate' => ['string|false', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'transliterator_create_inverse' => ['Transliterator', 'transliterator'=>'Transliterator'], +'transliterator_create_inverse' => ['?Transliterator', 'transliterator'=>'Transliterator'], 'transliterator_get_error_code' => ['int', 'transliterator'=>'Transliterator'], 'transliterator_get_error_message' => ['string', 'transliterator'=>'Transliterator'], 'transliterator_list_ids' => ['array'], @@ -15597,7 +15638,7 @@ 'xhprof_sample_enable' => ['void'], 'xlswriter_get_author' => ['string'], 'xlswriter_get_version' => ['string'], -'xml_error_string' => ['string', 'error_code'=>'int'], +'xml_error_string' => ['?string', 'error_code'=>'int'], 'xml_get_current_byte_index' => ['int|false', 'parser'=>'XMLParser'], 'xml_get_current_column_number' => ['int|false', 'parser'=>'XMLParser'], 'xml_get_current_line_number' => ['int|false', 'parser'=>'XMLParser'], diff --git a/dictionaries/CallMap_71_delta.php b/dictionaries/CallMap_71_delta.php index 987bf2b861a..2630ce958c1 100644 --- a/dictionaries/CallMap_71_delta.php +++ b/dictionaries/CallMap_71_delta.php @@ -21,12 +21,12 @@ 'curl_share_errno' => ['int|false', 'sh'=>'resource'], 'curl_share_strerror' => ['?string', 'error_code'=>'int'], 'getenv\'1' => ['array'], - 'hash_hkdf' => ['string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], 'is_iterable' => ['bool', 'value'=>'mixed'], 'openssl_get_curve_names' => ['list'], 'pcntl_async_signals' => ['bool', 'enable='=>'bool'], 'pcntl_signal_get_handler' => ['int|string', 'signal'=>'int'], - 'sapi_windows_cp_conv' => ['string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], + 'sapi_windows_cp_conv' => ['?string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], 'sapi_windows_cp_get' => ['int'], 'sapi_windows_cp_is_utf8' => ['bool'], 'sapi_windows_cp_set' => ['bool', 'codepage'=>'int'], diff --git a/dictionaries/CallMap_72_delta.php b/dictionaries/CallMap_72_delta.php index fe0b3b249ed..9a7b5996ae7 100644 --- a/dictionaries/CallMap_72_delta.php +++ b/dictionaries/CallMap_72_delta.php @@ -141,8 +141,8 @@ 'new' => ['HashContext', 'context'=>'HashContext'], ], 'hash_final' => [ - 'old' => ['string', 'context'=>'resource', 'raw_output='=>'bool'], - 'new' => ['string', 'context'=>'HashContext', 'binary='=>'bool'], + 'old' => ['non-empty-string', 'context'=>'resource', 'raw_output='=>'bool'], + 'new' => ['non-empty-string', 'context'=>'HashContext', 'binary='=>'bool'], ], 'hash_init' => [ 'old' => ['resource', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 1125a4349b8..815d07edb21 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -49,14 +49,6 @@ 'old' => ['int|false'], 'new' => ['int'], ], - 'DateTimeImmutable::format' => [ - 'old' => ['string|false', 'format'=>'string'], - 'new' => ['string', 'format'=>'string'], - ], - 'DateTimeImmutable::getTimestamp' => [ - 'old' => ['int|false'], - 'new' => ['int'], - ], 'DateTimeZone::listIdentifiers' => [ 'old' => ['list|false', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'], 'new' => ['list', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'], @@ -125,6 +117,10 @@ 'old' => ['object', 'args='=>'list'], 'new' => ['object', 'args='=>'array'], ], + 'ReflectionProperty::getValue' => [ + 'old' => ['mixed', 'object='=>'object'], + 'new' => ['mixed', 'object='=>'null|object'], + ], 'XMLWriter::flush' => [ 'old' => ['string|int|false', 'empty='=>'bool'], 'new' => ['string|int', 'empty='=>'bool'], @@ -226,7 +222,7 @@ 'new' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'true'], ], 'count' => [ - 'old' => ['int', 'value'=>'Countable|array|SimpleXMLElement|ResourceBundle', 'mode='=>'int'], + 'old' => ['int', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'], 'new' => ['int', 'value'=>'Countable|array', 'mode='=>'int'], ], 'count_chars' => [ @@ -393,10 +389,22 @@ 'old' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'new' => ['string|false', 'format'=>'string', 'timestamp='=>'?int'], ], + 'hash' => [ + 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], + ], + 'hash_hmac' => [ + 'old' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + ], 'hash_init' => [ 'old' => ['HashContext|false', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], ], + 'hash_hkdf' => [ + 'old' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'new' => ['non-empty-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + ], 'hash_update_file' => [ 'old' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'resource'], 'new' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'?resource'], @@ -659,7 +667,7 @@ ], 'imageinterlace' => [ 'old' => ['int|false', 'image'=>'resource', 'enable='=>'int'], - 'new' => ['int|false', 'image'=>'GdImage', 'enable='=>'int'], + 'new' => ['int|bool', 'image'=>'GdImage', 'enable='=>'bool|null'], ], 'imageistruecolor' => [ 'old' => ['bool', 'image'=>'resource'], @@ -846,8 +854,8 @@ 'new' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string|null'], ], 'mb_ereg_replace_callback' => [ - 'old' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], - 'new' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], + 'old' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], + 'new' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], ], 'mb_ereg_search' => [ 'old' => ['bool', 'pattern='=>'string', 'options='=>'string'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index d07d2e44736..09334d59442 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -106,8 +106,8 @@ 'new' => ['bool', 'ftp' => 'FTP\Connection', 'command' => 'string'], ], 'ftp_raw' => [ - 'old' => ['array', 'ftp' => 'resource', 'command' => 'string'], - 'new' => ['array', 'ftp' => 'FTP\Connection', 'command' => 'string'], + 'old' => ['?array', 'ftp' => 'resource', 'command' => 'string'], + 'new' => ['?array', 'ftp' => 'FTP\Connection', 'command' => 'string'], ], 'ftp_mkdir' => [ 'old' => ['string|false', 'ftp' => 'resource', 'directory' => 'string'], @@ -222,16 +222,20 @@ 'new' => ['mixed|false', 'ftp' => 'FTP\Connection', 'option' => 'int'], ], 'hash' => [ - 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], - 'new' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array'], + 'old' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], ], 'hash_file' => [ - 'old' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], - 'new' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array'], + 'old' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], ], 'hash_init' => [ 'old' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], - 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array'], + 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array{seed:scalar}'], + ], + 'imageinterlace' => [ + 'old' => ['int|bool', 'image'=>'GdImage', 'enable='=>'bool|null'], + 'new' => ['bool', 'image'=>'GdImage', 'enable='=>'bool|null'], ], 'imap_append' => [ 'old' => ['bool', 'imap'=>'resource', 'folder'=>'string', 'message'=>'string', 'options='=>'string', 'internal_date='=>'string'], @@ -266,8 +270,8 @@ 'new' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], ], 'imap_delete' => [ - 'old' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], - 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], + 'old' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], ], 'imap_deletemailbox' => [ 'old' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], @@ -446,13 +450,17 @@ 'new' => ['int|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int'], ], 'imap_undelete' => [ - 'old' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], - 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], + 'old' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], ], 'imap_unsubscribe' => [ 'old' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'new' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], ], + 'ini_set' => [ + 'old' => ['string|false', 'option'=>'string', 'value'=>'string'], + 'new' => ['string|false', 'option'=>'string', 'value'=>'string|int|float|bool|null'], + ], 'ldap_add' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 5cb9eb09eed..97d739fa4d9 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -925,18 +925,18 @@ 'DOMComment::__construct' => ['void', 'value='=>'string'], 'DOMDocument::__construct' => ['void', 'version='=>'string', 'encoding='=>'string'], 'DOMDocument::createAttribute' => ['DOMAttr|false', 'name'=>'string'], - 'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], + 'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string'], 'DOMDocument::createCDATASection' => ['DOMCDATASection|false', 'data'=>'string'], 'DOMDocument::createComment' => ['DOMComment|false', 'data'=>'string'], 'DOMDocument::createDocumentFragment' => ['DOMDocumentFragment|false'], 'DOMDocument::createElement' => ['DOMElement|false', 'name'=>'string', 'value='=>'string'], - 'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], + 'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value='=>'string'], 'DOMDocument::createEntityReference' => ['DOMEntityReference|false', 'name'=>'string'], 'DOMDocument::createProcessingInstruction' => ['DOMProcessingInstruction|false', 'target'=>'string', 'data='=>'string'], 'DOMDocument::createTextNode' => ['DOMText|false', 'content'=>'string'], 'DOMDocument::getElementById' => ['?DOMElement', 'elementid'=>'string'], 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], - 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMDocument::importNode' => ['DOMNode|false', 'importednode'=>'DOMNode', 'deep='=>'bool'], 'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'], @@ -958,23 +958,23 @@ 'DOMDocumentFragment::appendXML' => ['bool', 'data'=>'string'], 'DOMElement::__construct' => ['void', 'name'=>'string', 'value='=>'string', 'uri='=>'string'], 'DOMElement::getAttribute' => ['string', 'name'=>'string'], - 'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::getAttributeNode' => ['DOMAttr', 'name'=>'string'], - 'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], - 'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::get_attribute' => ['string', 'name'=>'string'], 'DOMElement::get_attribute_node' => ['DomAttribute', 'name'=>'string'], 'DOMElement::get_elements_by_tagname' => ['array', 'name'=>'string'], 'DOMElement::hasAttribute' => ['bool', 'name'=>'string'], - 'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::has_attribute' => ['bool', 'name'=>'string'], 'DOMElement::removeAttribute' => ['bool', 'name'=>'string'], - 'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::removeAttributeNode' => ['bool', 'oldnode'=>'DOMAttr'], 'DOMElement::remove_attribute' => ['bool', 'name'=>'string'], 'DOMElement::setAttribute' => ['DOMAttr|false', 'name'=>'string', 'value'=>'string'], - 'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value'=>'string'], + 'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value'=>'string'], 'DOMElement::setAttributeNode' => ['?DOMAttr', 'attr'=>'DOMAttr'], 'DOMElement::setAttributeNodeNS' => ['DOMAttr', 'attr'=>'DOMAttr'], 'DOMElement::setIdAttribute' => ['void', 'name'=>'string', 'isid'=>'bool'], @@ -1044,7 +1044,7 @@ 'DateTime::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'DateTime::diff' => ['DateInterval|false', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string|false', 'format'=>'string'], - 'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], + 'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int|false'], 'DateTime::getTimezone' => ['DateTimeZone|false'], @@ -1055,26 +1055,9 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], - 'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], - 'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], - 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], - 'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], - 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], - 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], - 'DateTimeImmutable::format' => ['string|false', 'format'=>'string'], - 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::getOffset' => ['int'], - 'DateTimeImmutable::getTimestamp' => ['int|false'], - 'DateTimeImmutable::getTimezone' => ['DateTimeZone|false'], - 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], - 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], - 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], - 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], - 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], - 'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], - 'DateTimeImmutable::sub' => ['static|false', 'interval'=>'DateInterval'], + 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], @@ -1781,7 +1764,7 @@ 'GEOSGeometry::__toString' => ['string'], 'GEOSGeometry::area' => ['float'], 'GEOSGeometry::boundary' => ['GEOSGeometry'], - 'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray'=>'array'], + 'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray='=>'array'], 'GEOSGeometry::centroid' => ['GEOSGeometry'], 'GEOSGeometry::checkValidity' => ['array{valid: bool, reason?: string, location?: GEOSGeometry}'], 'GEOSGeometry::contains' => ['bool', 'geom'=>'GEOSGeometry'], @@ -1830,7 +1813,7 @@ 'GEOSGeometry::relate' => ['string|bool', 'otherGeom'=>'GEOSGeometry', 'pattern'=>'string'], 'GEOSGeometry::relateBoundaryNodeRule' => ['string', 'otherGeom'=>'GEOSGeometry', 'rule'=>'int'], 'GEOSGeometry::setSRID' => ['void', 'srid'=>'int'], - 'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology'=>'bool'], + 'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology='=>'bool'], 'GEOSGeometry::snapTo' => ['GEOSGeometry', 'geom'=>'GEOSGeometry', 'tolerance'=>'float'], 'GEOSGeometry::startPoint' => ['GEOSGeometry'], 'GEOSGeometry::symDifference' => ['GEOSGeometry', 'geom'=>'GEOSGeometry'], @@ -3982,213 +3965,314 @@ 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['?array', 'db'=>'MongoDB', 'ref'=>'array'], 'MongoDBRef::isRef' => ['bool', 'ref'=>'mixed'], - 'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type'=>'int'], - 'MongoDB\BSON\Binary::__toString' => ['string'], + 'MongoDB\BSON\Binary::__construct' => ['void', 'data' => 'string', 'type' => 'int'], 'MongoDB\BSON\Binary::getData' => ['string'], 'MongoDB\BSON\Binary::getType' => ['int'], - 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value='=>'string'], + 'MongoDB\BSON\Binary::__toString' => ['string'], + 'MongoDB\BSON\Binary::serialize' => ['string'], + 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\BinaryInterface::getData' => ['string'], + 'MongoDB\BSON\BinaryInterface::getType' => ['int'], + 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], + 'MongoDB\BSON\DBPointer::__toString' => ['string'], + 'MongoDB\BSON\DBPointer::serialize' => ['string'], + 'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], - 'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'array|object'], - 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'string'], + 'MongoDB\BSON\Decimal128::serialize' => ['string'], + 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], + 'MongoDB\BSON\Int64::__toString' => ['string'], + 'MongoDB\BSON\Int64::serialize' => ['string'], + 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Javascript::__construct' => ['void', 'code' => 'string', 'scope=' => 'object|array|null'], + 'MongoDB\BSON\Javascript::getCode' => ['string'], + 'MongoDB\BSON\Javascript::getScope' => ['?object'], + 'MongoDB\BSON\Javascript::__toString' => ['string'], + 'MongoDB\BSON\Javascript::serialize' => ['string'], + 'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], + 'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], + 'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], + 'MongoDB\BSON\MaxKey::serialize' => ['string'], + 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\MinKey::serialize' => ['string'], + 'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], + 'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], - 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern'=>'string', 'flags='=>'string'], - 'MongoDB\BSON\Regex::__toString' => ['string'], - 'MongoDB\BSON\Regex::getFlags' => ['string'], + 'MongoDB\BSON\ObjectId::serialize' => ['string'], + 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], + 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], + 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], - 'MongoDB\BSON\Serializable::bsonSerialize' => ['array|object'], - 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'int', 'timestamp'=>'int'], + 'MongoDB\BSON\Regex::getFlags' => ['string'], + 'MongoDB\BSON\Regex::__toString' => ['string'], + 'MongoDB\BSON\Regex::serialize' => ['string'], + 'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\RegexInterface::getPattern' => ['string'], + 'MongoDB\BSON\RegexInterface::getFlags' => ['string'], + 'MongoDB\BSON\RegexInterface::__toString' => ['string'], + 'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], + 'MongoDB\BSON\Symbol::__toString' => ['string'], + 'MongoDB\BSON\Symbol::serialize' => ['string'], + 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], + 'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], + 'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], - 'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'int|DateTimeInterface'], - 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], + 'MongoDB\BSON\Timestamp::serialize' => ['string'], + 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], + 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], + 'MongoDB\BSON\TimestampInterface::__toString' => ['string'], + 'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds=' => 'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], - 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data'=>'array'], - 'MongoDB\BSON\binary::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\binary::serialize' => ['string'], - 'MongoDB\BSON\binary::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\binaryinterface::__toString' => ['string'], - 'MongoDB\BSON\binaryinterface::getData' => ['string'], - 'MongoDB\BSON\binaryinterface::getType' => ['int'], - 'MongoDB\BSON\dbpointer::__construct' => ['void'], - 'MongoDB\BSON\dbpointer::__toString' => ['string'], - 'MongoDB\BSON\dbpointer::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\dbpointer::serialize' => ['string'], - 'MongoDB\BSON\dbpointer::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\decimal128::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\decimal128::serialize' => ['string'], - 'MongoDB\BSON\decimal128::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\decimal128interface::__toString' => ['string'], - 'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], - 'MongoDB\BSON\fromPHP' => ['string', 'value'=>'array|object'], - 'MongoDB\BSON\int64::__construct' => ['void'], - 'MongoDB\BSON\int64::__toString' => ['string'], - 'MongoDB\BSON\int64::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\int64::serialize' => ['string'], - 'MongoDB\BSON\int64::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\javascript::__toString' => ['string'], - 'MongoDB\BSON\javascript::getCode' => ['string'], - 'MongoDB\BSON\javascript::getScope' => ['?object'], - 'MongoDB\BSON\javascript::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\javascript::serialize' => ['string'], - 'MongoDB\BSON\javascript::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\javascriptinterface::__toString' => ['string'], - 'MongoDB\BSON\javascriptinterface::getCode' => ['string'], - 'MongoDB\BSON\javascriptinterface::getScope' => ['?object'], - 'MongoDB\BSON\maxkey::__construct' => ['void'], - 'MongoDB\BSON\maxkey::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\maxkey::serialize' => ['string'], - 'MongoDB\BSON\maxkey::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\minkey::__construct' => ['void'], - 'MongoDB\BSON\minkey::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\minkey::serialize' => ['string'], - 'MongoDB\BSON\minkey::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\objectid::getTimestamp' => ['int'], - 'MongoDB\BSON\objectid::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\objectid::serialize' => ['string'], - 'MongoDB\BSON\objectid::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\objectidinterface::__toString' => ['string'], - 'MongoDB\BSON\objectidinterface::getTimestamp' => ['int'], - 'MongoDB\BSON\regex::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\regex::serialize' => ['string'], - 'MongoDB\BSON\regex::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\regexinterface::__toString' => ['string'], - 'MongoDB\BSON\regexinterface::getFlags' => ['string'], - 'MongoDB\BSON\regexinterface::getPattern' => ['string'], - 'MongoDB\BSON\symbol::__construct' => ['void'], - 'MongoDB\BSON\symbol::__toString' => ['string'], - 'MongoDB\BSON\symbol::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\symbol::serialize' => ['string'], - 'MongoDB\BSON\symbol::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\timestamp::getIncrement' => ['int'], - 'MongoDB\BSON\timestamp::getTimestamp' => ['int'], - 'MongoDB\BSON\timestamp::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\timestamp::serialize' => ['string'], - 'MongoDB\BSON\timestamp::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\timestampinterface::__toString' => ['string'], - 'MongoDB\BSON\timestampinterface::getIncrement' => ['int'], - 'MongoDB\BSON\timestampinterface::getTimestamp' => ['int'], - 'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], - 'MongoDB\BSON\toPHP' => ['object', 'bson'=>'string', 'typeMap='=>'array'], - 'MongoDB\BSON\undefined::__construct' => ['void'], - 'MongoDB\BSON\undefined::__toString' => ['string'], - 'MongoDB\BSON\undefined::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\undefined::serialize' => ['string'], - 'MongoDB\BSON\undefined::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\utcdatetime::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\utcdatetime::serialize' => ['string'], - 'MongoDB\BSON\utcdatetime::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\utcdatetimeinterface::__toString' => ['string'], - 'MongoDB\BSON\utcdatetimeinterface::toDateTime' => ['DateTime'], - 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'ordered='=>'bool'], + 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], + 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], + 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], + 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], + 'MongoDB\BSON\Undefined::__toString' => ['string'], + 'MongoDB\BSON\Undefined::serialize' => ['string'], + 'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], + 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], 'MongoDB\Driver\BulkWrite::count' => ['int'], - 'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'array|object', 'deleteOptions='=>'array'], - 'MongoDB\Driver\BulkWrite::insert' => ['void|MongoDB\BSON\ObjectId', 'document'=>'array|object'], - 'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array'], - 'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'array|object'], - 'MongoDB\Driver\Cursor::__construct' => ['void', 'server'=>'Server', 'responseDocument'=>'string'], + 'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter' => 'object|array', 'deleteOptions=' => '?array'], + 'MongoDB\Driver\BulkWrite::insert' => ['mixed', 'document' => 'object|array'], + 'MongoDB\Driver\BulkWrite::update' => ['void', 'filter' => 'object|array', 'newObj' => 'object|array', 'updateOptions=' => '?array'], + 'MongoDB\Driver\ClientEncryption::__construct' => ['void', 'options' => 'array'], + 'MongoDB\Driver\ClientEncryption::addKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], + 'MongoDB\Driver\ClientEncryption::createDataKey' => ['MongoDB\BSON\Binary', 'kmsProvider' => 'string', 'options=' => '?array'], + 'MongoDB\Driver\ClientEncryption::decrypt' => ['mixed', 'value' => 'MongoDB\BSON\Binary'], + 'MongoDB\Driver\ClientEncryption::deleteKey' => ['object', 'keyId' => 'MongoDB\BSON\Binary'], + 'MongoDB\Driver\ClientEncryption::encrypt' => ['MongoDB\BSON\Binary', 'value' => 'mixed', 'options=' => '?array'], + 'MongoDB\Driver\ClientEncryption::getKey' => ['?object', 'keyId' => 'MongoDB\BSON\Binary'], + 'MongoDB\Driver\ClientEncryption::getKeyByAltName' => ['?object', 'keyAltName' => 'string'], + 'MongoDB\Driver\ClientEncryption::getKeys' => ['MongoDB\Driver\Cursor'], + 'MongoDB\Driver\ClientEncryption::removeKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], + 'MongoDB\Driver\ClientEncryption::rewrapManyDataKey' => ['object', 'filter' => 'object|array', 'options=' => '?array'], + 'MongoDB\Driver\Command::__construct' => ['void', 'document' => 'object|array', 'commandOptions=' => '?array'], + 'MongoDB\Driver\Cursor::current' => ['object|array|null'], 'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Cursor::isDead' => ['bool'], - 'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], + 'MongoDB\Driver\Cursor::key' => ['?int'], + 'MongoDB\Driver\Cursor::next' => ['void'], + 'MongoDB\Driver\Cursor::rewind' => ['void'], + 'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap' => 'array'], 'MongoDB\Driver\Cursor::toArray' => ['array'], - 'MongoDB\Driver\CursorId::__construct' => ['void', 'id'=>'string'], + 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], - 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\Driver\Exception\RuntimeException::__clone' => ['void'], - 'MongoDB\Driver\Exception\RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], + 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], + 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], + 'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap' => 'array'], + 'MongoDB\Driver\CursorInterface::toArray' => ['array'], + 'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], + 'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], + 'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], + 'MongoDB\Driver\Exception\CommandException::__toString' => ['string'], + 'MongoDB\Driver\Exception\ConnectionException::__toString' => ['string'], + 'MongoDB\Driver\Exception\ConnectionTimeoutException::__toString' => ['string'], + 'MongoDB\Driver\Exception\EncryptionException::__toString' => ['string'], + 'MongoDB\Driver\Exception\Exception::__toString' => ['string'], + 'MongoDB\Driver\Exception\ExecutionTimeoutException::__toString' => ['string'], + 'MongoDB\Driver\Exception\InvalidArgumentException::__toString' => ['string'], + 'MongoDB\Driver\Exception\LogicException::__toString' => ['string'], + 'MongoDB\Driver\Exception\RuntimeException::hasErrorLabel' => ['bool', 'errorLabel' => 'string'], 'MongoDB\Driver\Exception\RuntimeException::__toString' => ['string'], - 'MongoDB\Driver\Exception\RuntimeException::__wakeup' => ['void'], - 'MongoDB\Driver\Exception\RuntimeException::getCode' => ['int'], - 'MongoDB\Driver\Exception\RuntimeException::getFile' => ['string'], - 'MongoDB\Driver\Exception\RuntimeException::getLine' => ['int'], - 'MongoDB\Driver\Exception\RuntimeException::getMessage' => ['string'], - 'MongoDB\Driver\Exception\RuntimeException::getPrevious' => ['RuntimeException|Throwable'], - 'MongoDB\Driver\Exception\RuntimeException::getTrace' => ['list\',args?:array}>'], - 'MongoDB\Driver\Exception\RuntimeException::getTraceAsString' => ['string'], - 'MongoDB\Driver\Exception\WriteException::__clone' => ['void'], - 'MongoDB\Driver\Exception\WriteException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], - 'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], - 'MongoDB\Driver\Exception\WriteException::__wakeup' => ['void'], - 'MongoDB\Driver\Exception\WriteException::getCode' => ['int'], - 'MongoDB\Driver\Exception\WriteException::getFile' => ['string'], - 'MongoDB\Driver\Exception\WriteException::getLine' => ['int'], - 'MongoDB\Driver\Exception\WriteException::getMessage' => ['string'], - 'MongoDB\Driver\Exception\WriteException::getPrevious' => ['RuntimeException|Throwable'], - 'MongoDB\Driver\Exception\WriteException::getTrace' => ['list\',args?:array}>'], - 'MongoDB\Driver\Exception\WriteException::getTraceAsString' => ['string'], + 'MongoDB\Driver\Exception\SSLConnectionException::__toString' => ['string'], + 'MongoDB\Driver\Exception\ServerException::__toString' => ['string'], + 'MongoDB\Driver\Exception\UnexpectedValueException::__toString' => ['string'], 'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], - 'MongoDB\Driver\Manager::__construct' => ['void', 'uri'=>'string', 'options='=>'array', 'driverOptions='=>'array'], - 'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'readPreference='=>'MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Manager::executeDelete' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'deleteOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::executeInsert' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'document'=>'array|object', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'readPreference='=>'MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Manager::executeUpdate' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], + 'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], + 'MongoDB\Driver\Manager::__construct' => ['void', 'uri=' => '?string', 'uriOptions=' => '?array', 'driverOptions=' => '?array'], + 'MongoDB\Driver\Manager::addSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], + 'MongoDB\Driver\Manager::createClientEncryption' => ['MongoDB\Driver\ClientEncryption', 'options' => 'array'], + 'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulk' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], + 'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Manager::getEncryptedFieldsMap' => ['object|array|null'], 'MongoDB\Driver\Manager::getReadConcern' => ['MongoDB\Driver\ReadConcern'], 'MongoDB\Driver\Manager::getReadPreference' => ['MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Manager::getServers' => ['MongoDB\Driver\Server[]'], + 'MongoDB\Driver\Manager::getServers' => ['array'], 'MongoDB\Driver\Manager::getWriteConcern' => ['MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference'=>'MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'array|object', 'queryOptions='=>'array'], - 'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'string'], - 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object'], + 'MongoDB\Driver\Manager::removeSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], + 'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], + 'MongoDB\Driver\Manager::startSession' => ['MongoDB\Driver\Session', 'options=' => '?array'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getCommandName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getError' => ['Exception'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getOperationId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getReply' => ['object'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getRequestId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getServerConnectionId' => ['?int'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommand' => ['object'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommandName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getDatabaseName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getOperationId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getRequestId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getServerConnectionId' => ['?int'], + 'MongoDB\Driver\Monitoring\CommandSubscriber::commandStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandStartedEvent'], + 'MongoDB\Driver\Monitoring\CommandSubscriber::commandSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandSucceededEvent'], + 'MongoDB\Driver\Monitoring\CommandSubscriber::commandFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandFailedEvent'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getCommandName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getOperationId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getReply' => ['object'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getRequestId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerChangedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerClosedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerOpeningEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyChangedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyClosedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyOpeningEvent'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getNewDescription' => ['MongoDB\Driver\ServerDescription'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getPreviousDescription' => ['MongoDB\Driver\ServerDescription'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\ServerClosedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerClosedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getError' => ['Exception'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::isAwaited' => ['bool'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::isAwaited' => ['bool'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getReply' => ['object'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::isAwaited' => ['bool'], + 'MongoDB\Driver\Monitoring\ServerOpeningEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerOpeningEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\TopologyChangedEvent::getNewDescription' => ['MongoDB\Driver\TopologyDescription'], + 'MongoDB\Driver\Monitoring\TopologyChangedEvent::getPreviousDescription' => ['MongoDB\Driver\TopologyDescription'], + 'MongoDB\Driver\Monitoring\TopologyChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\TopologyClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\TopologyOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Query::__construct' => ['void', 'filter' => 'object|array', 'queryOptions=' => '?array'], + 'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level=' => '?string'], 'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], + 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], - 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'array', 'options='=>'array'], - 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object'], - 'MongoDB\Driver\ReadPreference::getHedge' => ['object|null'], + 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], + 'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], 'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], + 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], - 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\Driver\Server::__construct' => ['void', 'host'=>'string', 'port'=>'string', 'options='=>'array', 'driverOptions='=>'array'], - 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'zwrite'=>'BulkWrite'], - 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command'], - 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'zquery'=>'Query'], + 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], + 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], 'MongoDB\Driver\Server::getHost' => ['string'], 'MongoDB\Driver\Server::getInfo' => ['array'], - 'MongoDB\Driver\Server::getLatency' => ['int'], + 'MongoDB\Driver\Server::getLatency' => ['?int'], 'MongoDB\Driver\Server::getPort' => ['int'], - 'MongoDB\Driver\Server::getState' => [''], + 'MongoDB\Driver\Server::getServerDescription' => ['MongoDB\Driver\ServerDescription'], 'MongoDB\Driver\Server::getTags' => ['array'], 'MongoDB\Driver\Server::getType' => ['int'], 'MongoDB\Driver\Server::isArbiter' => ['bool'], - 'MongoDB\Driver\Server::isDelayed' => [''], 'MongoDB\Driver\Server::isHidden' => ['bool'], 'MongoDB\Driver\Server::isPassive' => ['bool'], 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], - 'MongoDB\Driver\WriteConcern::__construct' => ['void', 'wstring'=>'string|int', 'wtimeout='=>'int', 'journal='=>'bool'], - 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object'], + 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], + 'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], + 'MongoDB\Driver\ServerApi::serialize' => ['string'], + 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], + 'MongoDB\Driver\ServerDescription::getHost' => ['string'], + 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], + 'MongoDB\Driver\ServerDescription::getPort' => ['int'], + 'MongoDB\Driver\ServerDescription::getRoundTripTime' => ['?int'], + 'MongoDB\Driver\ServerDescription::getType' => ['string'], + 'MongoDB\Driver\Session::abortTransaction' => ['void'], + 'MongoDB\Driver\Session::advanceClusterTime' => ['void', 'clusterTime' => 'object|array'], + 'MongoDB\Driver\Session::advanceOperationTime' => ['void', 'operationTime' => 'MongoDB\BSON\TimestampInterface'], + 'MongoDB\Driver\Session::commitTransaction' => ['void'], + 'MongoDB\Driver\Session::endSession' => ['void'], + 'MongoDB\Driver\Session::getClusterTime' => ['?object'], + 'MongoDB\Driver\Session::getLogicalSessionId' => ['object'], + 'MongoDB\Driver\Session::getOperationTime' => ['?MongoDB\BSON\Timestamp'], + 'MongoDB\Driver\Session::getServer' => ['?MongoDB\Driver\Server'], + 'MongoDB\Driver\Session::getTransactionOptions' => ['?array'], + 'MongoDB\Driver\Session::getTransactionState' => ['string'], + 'MongoDB\Driver\Session::isDirty' => ['bool'], + 'MongoDB\Driver\Session::isInTransaction' => ['bool'], + 'MongoDB\Driver\Session::startTransaction' => ['void', 'options=' => '?array'], + 'MongoDB\Driver\TopologyDescription::getServers' => ['array'], + 'MongoDB\Driver\TopologyDescription::getType' => ['string'], + 'MongoDB\Driver\TopologyDescription::hasReadableServer' => ['bool', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], + 'MongoDB\Driver\TopologyDescription::hasWritableServer' => ['bool'], + 'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w' => 'string|int', 'wtimeout=' => '?int', 'journal=' => '?bool'], 'MongoDB\Driver\WriteConcern::getJournal' => ['?bool'], - 'MongoDB\Driver\WriteConcern::getJurnal' => ['?bool'], - 'MongoDB\Driver\WriteConcern::getW' => ['int|null|string'], + 'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], + 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], - 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized'=>'string'], + 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], - 'MongoDB\Driver\WriteConcernError::getInfo' => ['mixed'], + 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], 'MongoDB\Driver\WriteError::getCode' => ['int'], 'MongoDB\Driver\WriteError::getIndex' => ['int'], - 'MongoDB\Driver\WriteError::getInfo' => ['mixed'], + 'MongoDB\Driver\WriteError::getInfo' => ['?object'], 'MongoDB\Driver\WriteError::getMessage' => ['string'], - 'MongoDB\Driver\WriteException::getWriteResult' => [''], - 'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], - 'MongoDB\Driver\WriteResult::getInfo' => [''], 'MongoDB\Driver\WriteResult::getInsertedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getMatchedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getModifiedCount' => ['?int'], - 'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getUpsertedCount' => ['?int'], + 'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\WriteResult::getUpsertedIds' => ['array'], - 'MongoDB\Driver\WriteResult::getWriteConcernError' => ['MongoDB\Driver\WriteConcernError|null'], - 'MongoDB\Driver\WriteResult::getWriteErrors' => ['MongoDB\Driver\WriteError[]'], + 'MongoDB\Driver\WriteResult::getWriteConcernError' => ['?MongoDB\Driver\WriteConcernError'], + 'MongoDB\Driver\WriteResult::getWriteErrors' => ['array'], 'MongoDB\Driver\WriteResult::isAcknowledged' => ['bool'], 'MongoDate::__construct' => ['void', 'second='=>'int', 'usecond='=>'int'], 'MongoDate::__toString' => ['string'], @@ -4420,7 +4504,7 @@ 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter|false', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], - 'NumberFormatter::formatCurrency' => ['string', 'num'=>'float', 'currency'=>'string'], + 'NumberFormatter::formatCurrency' => ['string|false', 'num'=>'float', 'currency'=>'string'], 'NumberFormatter::getAttribute' => ['int|false', 'attr'=>'int'], 'NumberFormatter::getErrorCode' => ['int'], 'NumberFormatter::getErrorMessage' => ['string'], @@ -5911,7 +5995,7 @@ 'RedisCluster::zScore' => ['float', 'key'=>'string', 'member'=>'string'], 'RedisCluster::zUnionStore' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], 'Reflection::export' => ['?string', 'r'=>'reflector', 'return='=>'bool'], - 'Reflection::getModifierNames' => ['array', 'modifiers'=>'int'], + 'Reflection::getModifierNames' => ['list', 'modifiers'=>'int'], 'ReflectionClass::__clone' => ['void'], 'ReflectionClass::__construct' => ['void', 'argument'=>'object|class-string'], 'ReflectionClass::__toString' => ['string'], @@ -9909,7 +9993,7 @@ 'classkit_method_remove' => ['bool', 'classname'=>'string', 'methodname'=>'string'], 'classkit_method_rename' => ['bool', 'classname'=>'string', 'methodname'=>'string', 'newname'=>'string'], 'clearstatcache' => ['void', 'clear_realpath_cache='=>'bool', 'filename='=>'string'], - 'cli_get_process_title' => ['string'], + 'cli_get_process_title' => ['?string'], 'cli_set_process_title' => ['bool', 'title'=>'string'], 'closedir' => ['void', 'dir_handle='=>'resource'], 'closelog' => ['bool'], @@ -9920,7 +10004,7 @@ 'clusterObj::setGroup' => ['int', 'expression'=>'string'], 'collator_asort' => ['bool', 'object'=>'collator', '&rw_array'=>'array', 'flags='=>'int'], 'collator_compare' => ['int', 'object'=>'collator', 'string1'=>'string', 'string2'=>'string'], - 'collator_create' => ['Collator', 'locale'=>'string'], + 'collator_create' => ['?Collator', 'locale'=>'string'], 'collator_get_attribute' => ['int|false', 'object'=>'collator', 'attribute'=>'int'], 'collator_get_error_code' => ['int', 'object'=>'collator'], 'collator_get_error_message' => ['string', 'object'=>'collator'], @@ -10001,7 +10085,7 @@ 'copy' => ['bool', 'from'=>'string', 'to'=>'string', 'context='=>'resource'], 'cos' => ['float', 'num'=>'float'], 'cosh' => ['float', 'num'=>'float'], - 'count' => ['int', 'value'=>'Countable|array|SimpleXMLElement|ResourceBundle', 'mode='=>'int'], + 'count' => ['int', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'], 'count_chars' => ['array|false', 'input'=>'string', 'mode='=>'0|1|2'], 'count_chars\'1' => ['string|false', 'input'=>'string', 'mode='=>'3|4'], 'crack_check' => ['bool', 'dictionary'=>'', 'password'=>'string'], @@ -10170,7 +10254,7 @@ 'date_default_timezone_set' => ['bool', 'timezoneId'=>'string'], 'date_diff' => ['DateInterval|false', 'baseObject'=>'DateTimeInterface', 'targetObject'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string|false', 'object'=>'DateTimeInterface', 'format'=>'string'], - 'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], + 'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'date_interval_create_from_date_string' => ['DateInterval', 'datetime'=>'string'], 'date_interval_format' => ['string', 'object'=>'DateInterval', 'format'=>'string'], 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'dayOfWeek='=>'int|mixed'], @@ -10191,7 +10275,7 @@ 'datefmt_format' => ['string|false', 'formatter'=>'IntlDateFormatter', 'datetime'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'datetime'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int', 'formatter'=>'IntlDateFormatter'], - 'datefmt_get_calendar_object' => ['IntlCalendar', 'formatter'=>'IntlDateFormatter'], + 'datefmt_get_calendar_object' => ['IntlCalendar|false|null', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_datetype' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'formatter'=>'IntlDateFormatter'], @@ -10923,7 +11007,7 @@ 'ftp_put' => ['bool', 'ftp'=>'resource', 'remote_filename'=>'string', 'local_filename'=>'string', 'mode='=>'int', 'offset='=>'int'], 'ftp_pwd' => ['string|false', 'ftp'=>'resource'], 'ftp_quit' => ['bool', 'ftp'=>'resource'], - 'ftp_raw' => ['array', 'ftp'=>'resource', 'command'=>'string'], + 'ftp_raw' => ['?array', 'ftp'=>'resource', 'command'=>'string'], 'ftp_rawlist' => ['array|false', 'ftp'=>'resource', 'directory'=>'string', 'recursive='=>'bool'], 'ftp_rename' => ['bool', 'ftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ftp_rmdir' => ['bool', 'ftp'=>'resource', 'directory'=>'string'], @@ -11285,12 +11369,12 @@ 'hash_algos' => ['list'], 'hash_copy' => ['resource', 'context'=>'resource'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], - 'hash_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], - 'hash_final' => ['string', 'context'=>'resource', 'raw_output='=>'bool'], - 'hash_hmac' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], - 'hash_hmac_file' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + 'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], + 'hash_final' => ['non-empty-string', 'context'=>'resource', 'raw_output='=>'bool'], + 'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + 'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], 'hash_init' => ['resource', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], - 'hash_pbkdf2' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], + 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], 'hash_update' => ['bool', 'context'=>'resource', 'data'=>'string'], 'hash_update_file' => ['bool', 'hcontext'=>'resource', 'filename'=>'string', 'scontext='=>'resource'], 'hash_update_stream' => ['int', 'context'=>'resource', 'handle'=>'resource', 'length='=>'int'], @@ -12144,7 +12228,7 @@ 'imap_close' => ['bool', 'imap'=>'resource', 'flags='=>'int'], 'imap_create' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], - 'imap_delete' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], + 'imap_delete' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], 'imap_deletemailbox' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], 'imap_expunge' => ['bool', 'imap'=>'resource'], @@ -12201,7 +12285,7 @@ 'imap_thread' => ['array|false', 'imap'=>'resource', 'flags='=>'int'], 'imap_timeout' => ['int|bool', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'imap'=>'resource', 'message_num'=>'int'], - 'imap_undelete' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], + 'imap_undelete' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'string'=>'string'], 'imap_utf7_encode' => ['string', 'string'=>'string'], @@ -12272,7 +12356,7 @@ 'intlcal_after' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_before' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_clear' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'int'], - 'intlcal_create_instance' => ['IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], + 'intlcal_create_instance' => ['?IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], 'intlcal_equals' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_field_difference' => ['int', 'calendar'=>'IntlCalendar', 'timestamp'=>'float', 'field'=>'int'], 'intlcal_from_date_time' => ['IntlCalendar', 'datetime'=>'DateTime|string'], @@ -12317,8 +12401,8 @@ 'intlgregcal_set_gregorian_change' => ['void', 'calendar'=>'IntlGregorianCalendar', 'timestamp'=>'float'], 'intltz_count_equivalent_ids' => ['int', 'timezoneId'=>'string'], 'intltz_create_enumeration' => ['IntlIterator', 'countryOrRawOffset'=>'mixed'], - 'intltz_create_time_zone' => ['IntlTimeZone', 'timezoneId'=>'string'], - 'intltz_from_date_time_zone' => ['IntlTimeZone', 'timezone'=>'DateTimeZone'], + 'intltz_create_time_zone' => ['?IntlTimeZone', 'timezoneId'=>'string'], + 'intltz_from_date_time_zone' => ['?IntlTimeZone', 'timezone'=>'DateTimeZone'], 'intltz_getGMT' => ['IntlTimeZone'], 'intltz_get_canonical_id' => ['string', 'timezoneId'=>'string', '&isSystemId'=>'bool'], 'intltz_get_display_name' => ['string', 'timezone'=>'IntlTimeZone', 'dst'=>'bool', 'style'=>'int', 'locale'=>'string'], @@ -12558,22 +12642,22 @@ 'litespeed_request_headers' => ['array'], 'litespeed_response_headers' => ['array'], 'locale_accept_from_http' => ['string|false', 'header'=>'string'], - 'locale_canonicalize' => ['string', 'locale'=>'string'], + 'locale_canonicalize' => ['?string', 'locale'=>'string'], 'locale_compose' => ['string|false', 'subtags'=>'array'], - 'locale_filter_matches' => ['bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], - 'locale_get_all_variants' => ['array', 'locale'=>'string'], + 'locale_filter_matches' => ['?bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], + 'locale_get_all_variants' => ['?array', 'locale'=>'string'], 'locale_get_default' => ['string'], 'locale_get_display_language' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_name' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_region' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_script' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_variant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], - 'locale_get_keywords' => ['array|false', 'locale'=>'string'], - 'locale_get_primary_language' => ['string', 'locale'=>'string'], - 'locale_get_region' => ['string', 'locale'=>'string'], - 'locale_get_script' => ['string', 'locale'=>'string'], - 'locale_lookup' => ['string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], - 'locale_parse' => ['array', 'locale'=>'string'], + 'locale_get_keywords' => ['array|false|null', 'locale'=>'string'], + 'locale_get_primary_language' => ['?string', 'locale'=>'string'], + 'locale_get_region' => ['?string', 'locale'=>'string'], + 'locale_get_script' => ['?string', 'locale'=>'string'], + 'locale_lookup' => ['?string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], + 'locale_parse' => ['?array', 'locale'=>'string'], 'locale_set_default' => ['bool', 'locale'=>'string'], 'localeconv' => ['array'], 'localtime' => ['array', 'timestamp='=>'int', 'associative='=>'bool'], @@ -12893,7 +12977,7 @@ 'mb_ereg' => ['int|false', 'pattern'=>'string', 'string'=>'string', '&w_matches='=>'array|null'], 'mb_ereg_match' => ['bool', 'pattern'=>'string', 'string'=>'string', 'options='=>'string'], 'mb_ereg_replace' => ['string|false', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string'], - 'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], + 'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], 'mb_ereg_search' => ['bool', 'pattern='=>'string', 'options='=>'string'], 'mb_ereg_search_getpos' => ['int'], 'mb_ereg_search_getregs' => ['string[]|false'], @@ -13019,49 +13103,6 @@ 'mkdir' => ['bool', 'directory'=>'string', 'permissions='=>'int', 'recursive='=>'bool', 'context='=>'resource'], 'mktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], - 'mongodb\driver\exception\commandexception::getResultDocument' => ['object'], - 'mongodb\driver\exception\runtimeexception::hasErrorLabel' => ['bool', 'errorLabel'=>'string'], - 'mongodb\driver\manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\manager::startSession' => ['MongoDB\Driver\Session', 'options='=>'array'], - 'mongodb\driver\monitoring\commandfailedevent::getCommandName' => ['string'], - 'mongodb\driver\monitoring\commandfailedevent::getDurationMicros' => ['int'], - 'mongodb\driver\monitoring\commandfailedevent::getError' => ['Exception'], - 'mongodb\driver\monitoring\commandfailedevent::getOperationId' => ['string'], - 'mongodb\driver\monitoring\commandfailedevent::getReply' => ['object'], - 'mongodb\driver\monitoring\commandfailedevent::getRequestId' => ['string'], - 'mongodb\driver\monitoring\commandfailedevent::getServer' => ['MongoDB\Driver\Server'], - 'mongodb\driver\monitoring\commandstartedevent::getCommand' => ['object'], - 'mongodb\driver\monitoring\commandstartedevent::getCommandName' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getDatabaseName' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getOperationId' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getRequestId' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getServer' => ['MongoDB\Driver\Server'], - 'mongodb\driver\monitoring\commandsubscriber::commandFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandFailedEvent'], - 'mongodb\driver\monitoring\commandsubscriber::commandStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandStartedEvent'], - 'mongodb\driver\monitoring\commandsubscriber::commandSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandSucceededEvent'], - 'mongodb\driver\monitoring\commandsucceededevent::getCommandName' => ['string'], - 'mongodb\driver\monitoring\commandsucceededevent::getDurationMicros' => ['int'], - 'mongodb\driver\monitoring\commandsucceededevent::getOperationId' => ['string'], - 'mongodb\driver\monitoring\commandsucceededevent::getReply' => ['object'], - 'mongodb\driver\monitoring\commandsucceededevent::getRequestId' => ['string'], - 'mongodb\driver\monitoring\commandsucceededevent::getServer' => ['MongoDB\Driver\Server'], - 'mongodb\driver\server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\session::__construct' => ['void'], - 'mongodb\driver\session::abortTransaction' => ['void'], - 'mongodb\driver\session::advanceClusterTime' => ['void', 'clusterTime'=>'array|object'], - 'mongodb\driver\session::advanceOperationTime' => ['void', 'operationTime'=>'MongoDB\BSON\TimestampInterface'], - 'mongodb\driver\session::commitTransaction' => ['void'], - 'mongodb\driver\session::endSession' => ['void'], - 'mongodb\driver\session::getClusterTime' => ['?object'], - 'mongodb\driver\session::getLogicalSessionId' => ['object'], - 'mongodb\driver\session::getOperationTime' => ['MongoDB\BSON\Timestamp|null'], - 'mongodb\driver\session::getTransactionOptions' => ['array|null'], - 'mongodb\driver\session::getTransactionState' => ['string'], - 'mongodb\driver\session::startTransaction' => ['void', 'options'=>'array|object'], 'monitor_custom_event' => ['void', 'class'=>'string', 'text'=>'string', 'severe='=>'int', 'user_data='=>'mixed'], 'monitor_httperror_event' => ['void', 'error_code'=>'int', 'url'=>'string', 'severe='=>'int'], 'monitor_license_info' => ['array'], @@ -13122,7 +13163,7 @@ 'msg_send' => ['bool', 'queue'=>'resource', 'message_type'=>'int', 'message'=>'mixed', 'serialize='=>'bool', 'blocking='=>'bool', '&w_error_code='=>'int'], 'msg_set_queue' => ['bool', 'queue'=>'resource', 'data'=>'array'], 'msg_stat_queue' => ['array', 'queue'=>'resource'], - 'msgfmt_create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], + 'msgfmt_create' => ['?MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'msgfmt_format' => ['string|false', 'formatter'=>'MessageFormatter', 'values'=>'array'], 'msgfmt_format_message' => ['string|false', 'locale'=>'string', 'pattern'=>'string', 'values'=>'array'], 'msgfmt_get_error_code' => ['int', 'formatter'=>'MessageFormatter'], @@ -13418,7 +13459,7 @@ 'mysqli_field_tell' => ['int', 'result'=>'mysqli_result'], 'mysqli_free_result' => ['void', 'result'=>'mysqli_result'], 'mysqli_get_cache_stats' => ['array|false'], - 'mysqli_get_charset' => ['object', 'mysql'=>'mysqli'], + 'mysqli_get_charset' => ['?object', 'mysql'=>'mysqli'], 'mysqli_get_client_info' => ['string', 'mysql='=>'?mysqli'], 'mysqli_get_client_stats' => ['array'], 'mysqli_get_client_version' => ['int', 'link'=>'mysqli'], @@ -15221,7 +15262,7 @@ 'streamWrapper::unlink' => ['bool', 'path'=>'string'], 'streamWrapper::url_stat' => ['array', 'path'=>'string', 'flags'=>'int'], 'stream_bucket_append' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], - 'stream_bucket_make_writeable' => ['object', 'brigade'=>'resource'], + 'stream_bucket_make_writeable' => ['?object', 'brigade'=>'resource'], 'stream_bucket_new' => ['object|false', 'stream'=>'resource', 'buffer'=>'string'], 'stream_bucket_prepend' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], 'stream_context_create' => ['resource', 'options='=>'array', 'params='=>'array'], @@ -15739,16 +15780,16 @@ 'tidy_config_count' => ['int', 'tidy'=>'tidy'], 'tidy_diagnose' => ['bool', 'tidy'=>'tidy'], 'tidy_error_count' => ['int', 'tidy'=>'tidy'], - 'tidy_get_body' => ['tidyNode', 'tidy'=>'tidy'], + 'tidy_get_body' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_config' => ['array', 'tidy'=>'tidy'], 'tidy_get_error_buffer' => ['string', 'tidy'=>'tidy'], - 'tidy_get_head' => ['tidyNode', 'tidy'=>'tidy'], - 'tidy_get_html' => ['tidyNode', 'tidy'=>'tidy'], + 'tidy_get_head' => ['?tidyNode', 'tidy'=>'tidy'], + 'tidy_get_html' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_html_ver' => ['int', 'tidy'=>'tidy'], 'tidy_get_opt_doc' => ['string', 'tidy'=>'tidy', 'option'=>'string'], 'tidy_get_output' => ['string', 'tidy'=>'tidy'], 'tidy_get_release' => ['string'], - 'tidy_get_root' => ['tidyNode', 'tidy'=>'tidy'], + 'tidy_get_root' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_status' => ['int', 'tidy'=>'tidy'], 'tidy_getopt' => ['mixed', 'tidy'=>'string', 'option'=>'tidy'], 'tidy_is_xhtml' => ['bool', 'tidy'=>'tidy'], @@ -15945,7 +15986,7 @@ 'trait_exists' => ['bool', 'trait'=>'string', 'autoload='=>'bool'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], - 'transliterator_create_inverse' => ['Transliterator', 'transliterator'=>'Transliterator'], + 'transliterator_create_inverse' => ['?Transliterator', 'transliterator'=>'Transliterator'], 'transliterator_get_error_code' => ['int', 'transliterator'=>'Transliterator'], 'transliterator_get_error_message' => ['string', 'transliterator'=>'Transliterator'], 'transliterator_list_ids' => ['array'], @@ -16412,7 +16453,7 @@ 'xhprof_sample_enable' => ['void'], 'xlswriter_get_author' => ['string'], 'xlswriter_get_version' => ['string'], - 'xml_error_string' => ['string', 'error_code'=>'int'], + 'xml_error_string' => ['?string', 'error_code'=>'int'], 'xml_get_current_byte_index' => ['int|false', 'parser'=>'resource'], 'xml_get_current_column_number' => ['int|false', 'parser'=>'resource'], 'xml_get_current_line_number' => ['int|false', 'parser'=>'resource'], diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md index 132b2cf4628..5393cca2e7a 100644 --- a/docs/annotating_code/supported_annotations.md +++ b/docs/annotating_code/supported_annotations.md @@ -534,7 +534,7 @@ class User { ### `@psalm-require-extends` -The `@psalm-require-extends` annotation allows you to define a requirements that a trait imposes on the using class. +The `@psalm-require-extends` annotation allows you to define the requirements that a trait imposes on the using class. ```php */ diff --git a/docs/annotating_code/type_syntax/atomic_types.md b/docs/annotating_code/type_syntax/atomic_types.md index 91ca1017057..911e9a39565 100644 --- a/docs/annotating_code/type_syntax/atomic_types.md +++ b/docs/annotating_code/type_syntax/atomic_types.md @@ -22,6 +22,7 @@ Atomic types are the basic building block of all type information used in Psalm. ## [Object types](object_types.md) - [object](object_types.md) +- [object{foo: string}](object_types.md) - [Exception, Foo\MyClass and Foo\MyClass](object_types.md) - [Generator](object_types.md) diff --git a/docs/annotating_code/type_syntax/conditional_types.md b/docs/annotating_code/type_syntax/conditional_types.md index 3d57dab3357..00dacca29a4 100644 --- a/docs/annotating_code/type_syntax/conditional_types.md +++ b/docs/annotating_code/type_syntax/conditional_types.md @@ -18,7 +18,7 @@ Let's suppose we want to make a userland implementation of PHP's numeric additio foo; +} + +takesObject((object) ["foo" => "hello"]); +``` + +Optional properties can be denoted by a trailing `?`, e.g.: + +```php +/** @param object{optional?: string} */ +``` + #### Generic object types Psalm supports using generic object types like `ArrayObject`. Any generic object should be typehinted with appropriate [`@template` tags](../templated_annotations.md). diff --git a/docs/annotating_code/type_syntax/scalar_types.md b/docs/annotating_code/type_syntax/scalar_types.md index 1b328d1b606..a59b1be56ae 100644 --- a/docs/annotating_code/type_syntax/scalar_types.md +++ b/docs/annotating_code/type_syntax/scalar_types.md @@ -40,7 +40,7 @@ You can also parameterize `class-string` with an object name e.g. [`class-string ### trait-string -Psalm also supports a `trait-string` annotation denote a trait that exists. +Psalm also supports a `trait-string` annotation denoting a trait that exists. ### enum-string diff --git a/docs/contributing/editing_callmaps.md b/docs/contributing/editing_callmaps.md index b3e7431cbe1..0fe42918b87 100644 --- a/docs/contributing/editing_callmaps.md +++ b/docs/contributing/editing_callmaps.md @@ -41,7 +41,7 @@ version supported by Psalm, it needs to process delta files to arrive at a version of callmap matching the one that is used during analysis. Psalm uses the following process to do that: -1. Read `CallMap.php` (Note: it's the one having latest signatures). +1. Read `CallMap.php` (Note: it's the one having the latest signatures). 2. If it matches configured PHP version, use it. 3. If the callmap delta for previous PHP version exists, read that. 4. Take previous callmap delta and apply it in reverse order. That is, entries @@ -70,7 +70,7 @@ it exists in the latest PHP version). Here's [the PR that does it](https://githu ### Correcting the function signature -Assume you found incorrect signature, the one that was always different to what +Assume you found an incorrect signature, the one that was always different to what we currently have in Psalm. This will need a change to `CallMap_historical.php` (as the signature was always that way) and `CallMap.php` (as the signature is still valid). Here's [the PR that does it](https://github.com/vimeo/psalm/pull/6359/files). diff --git a/docs/contributing/how_psalm_works.md b/docs/contributing/how_psalm_works.md index 026950f53e9..36b2c8f4cd6 100644 --- a/docs/contributing/how_psalm_works.md +++ b/docs/contributing/how_psalm_works.md @@ -66,7 +66,7 @@ At each line the `Context` object may or may not be manipulated. At branching po The `NodeDataProvider` stores a type for each PhpParser node. -After all the statements have been analysed we gather up all the return types and compare to the given return type. +After all the statements have been analysed we gather up all the return types and compare them to the given return type. ### Type Reconciliation diff --git a/docs/contributing/philosophy.md b/docs/contributing/philosophy.md index dd5d10c61a7..f43f4fbb4fa 100644 --- a/docs/contributing/philosophy.md +++ b/docs/contributing/philosophy.md @@ -40,7 +40,7 @@ Psalm is almost always run on PHP code that parses a lint check (`php -l `. Defaults to `false`. +#### hideAllErrorsExceptPassedFiles +```xml + +``` +Whether or not to report issues only for files that were passed explicitly as arguments in CLI. This means any files that are loaded with require/include will not report either, if not set in CLI. Useful if you want to only check errors in a single or selected files. Defaults to `false`. + #### cacheDirectory ```xml ``` -Passing `');alert('injection');//` as a `GET` param here would would cause the `alert` to trigger. +Passing `');alert('injection');//` as a `GET` param here would cause the `alert` to trigger. ## Mitigations diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 341c824b26f..55a17ddfc1c 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -778,9 +778,9 @@ public function interfaceHasCorrectCasing(string $fq_interface_name): bool return $this->classlikes->interfaceHasCorrectCasing($fq_interface_name); } - public function traitHasCorrectCase(string $fq_trait_name): bool + public function traitHasCorrectCasing(string $fq_trait_name): bool { - return $this->classlikes->traitHasCorrectCase($fq_trait_name); + return $this->classlikes->traitHasCorrectCasing($fq_trait_name); } /** @@ -1073,7 +1073,7 @@ public function getSymbolInformation(string $file_path, string $symbol): ?array } if (strpos($symbol, '$') === 0) { - $type = VariableFetchAnalyzer::getGlobalType($symbol); + $type = VariableFetchAnalyzer::getGlobalType($symbol, $this->analysis_php_version_id); if (!$type->isMixed()) { return ['type' => 'getFileContents($file_path); - $class_name = preg_replace('/^.*\\\/', '', $fq_class_name); + $class_name = preg_replace('/^.*\\\/', '', $fq_class_name, 1); if ($aliases->uses_end) { $position = self::getPositionFromOffset($aliases->uses_end, $file_contents); diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 6c1d7fe4639..d5ae3a2583a 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -40,6 +40,7 @@ use Psalm\Plugin\PluginInterface; use Psalm\Progress\Progress; use Psalm\Progress\VoidProgress; +use RuntimeException; use SimpleXMLElement; use SimpleXMLIterator; use Symfony\Component\Filesystem\Path; @@ -57,6 +58,7 @@ use function basename; use function chdir; use function class_exists; +use function clearstatcache; use function count; use function dirname; use function explode; @@ -100,12 +102,10 @@ use function substr; use function substr_count; use function sys_get_temp_dir; -use function trigger_error; use function unlink; use function version_compare; use const DIRECTORY_SEPARATOR; -use const E_USER_ERROR; use const GLOB_NOSORT; use const JSON_THROW_ON_ERROR; use const LIBXML_ERR_ERROR; @@ -290,6 +290,11 @@ class Config */ public $hide_external_errors = false; + /** + * @var bool + */ + public $hide_all_errors_except_passed_files = false; + /** @var bool */ public $allow_includes = true; @@ -962,6 +967,7 @@ private static function fromXmlAndPaths( 'useDocblockPropertyTypes' => 'use_docblock_property_types', 'throwExceptionOnError' => 'throw_exception', 'hideExternalErrors' => 'hide_external_errors', + 'hideAllErrorsExceptPassedFiles' => 'hide_all_errors_except_passed_files', 'resolveFromConfigFile' => 'resolve_from_config_file', 'allowFileIncludes' => 'allow_includes', 'strictBinaryOperands' => 'strict_binary_operands', @@ -993,7 +999,6 @@ private static function fromXmlAndPaths( 'reportInfo' => 'report_info', 'restrictReturnTypes' => 'restrict_return_types', 'limitMethodComplexity' => 'limit_method_complexity', - 'triggerErrorExits' => 'trigger_error_exits', ]; foreach ($booleanAttributes as $xmlName => $internalName) { @@ -1088,8 +1093,19 @@ private static function fromXmlAndPaths( chdir($config->base_dir); } - if (is_dir($config->cache_directory) === false && @mkdir($config->cache_directory, 0777, true) === false) { - trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR); + if (!is_dir($config->cache_directory)) { + try { + if (mkdir($config->cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException('Failed to create Psalm cache directory for unknown reasons'); + } + } catch (RuntimeException $e) { + if (!is_dir($config->cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } + } } if ($cwd) { @@ -1158,6 +1174,13 @@ private static function fromXmlAndPaths( $config->infer_property_types_from_constructor = $attribute_text === 'true' || $attribute_text === '1'; } + if (isset($config_xml['triggerErrorExits'])) { + $attribute_text = (string) $config_xml['triggerErrorExits']; + if ($attribute_text === 'always' || $attribute_text === 'never') { + $config->trigger_error_exits = $attribute_text; + } + } + if (isset($config_xml->projectFiles)) { $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true); } @@ -1361,7 +1384,7 @@ public function setCustomErrorLevel(string $issue_key, string $error_level): voi private function loadFileExtensions(SimpleXMLElement $extensions): void { foreach ($extensions as $extension) { - $extension_name = preg_replace('/^\.?/', '', (string)$extension['name']); + $extension_name = preg_replace('/^\.?/', '', (string)$extension['name'], 1); $this->file_extensions[] = $extension_name; if (isset($extension['scanner'])) { @@ -1598,7 +1621,7 @@ private function getPluginClassForPath(Codebase $codebase, string $path, string public function shortenFileName(string $to): string { if (!is_file($to)) { - return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to); + return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to, 1); } $from = $this->base_dir; @@ -1649,6 +1672,13 @@ public function reportIssueInFile(string $issue_type, string $file_path): bool $project_analyzer = ProjectAnalyzer::getInstance(); + // if the option is set and at least one file is passed via CLI + if ($this->hide_all_errors_except_passed_files + && $project_analyzer->check_paths_files + && !in_array($file_path, $project_analyzer->check_paths_files, true)) { + return false; + } + $codebase = $project_analyzer->getCodebase(); if (!$this->hide_external_errors) { @@ -1772,7 +1802,7 @@ public static function getParentIssueType(string $issue_type): ?string } if (strpos($issue_type, 'Possibly') === 0) { - $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type); + $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type, 1); if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) { $stripped_issue_type = 'Invalid' . $stripped_issue_type; @@ -1786,7 +1816,7 @@ public static function getParentIssueType(string $issue_type): ?string } if (preg_match('/^(False|Null)[A-Z]/', $issue_type) && !strpos($issue_type, 'Reference')) { - return preg_replace('/^(False|Null)/', 'Invalid', $issue_type); + return preg_replace('/^(False|Null)/', 'Invalid', $issue_type, 1); } if ($issue_type === 'UndefinedInterfaceMethod') { @@ -2343,6 +2373,7 @@ public function getPotentialComposerFilePathForClassLike(string $class): ?string public static function removeCacheDirectory(string $dir): void { + clearstatcache(true, $dir); if (is_dir($dir)) { $objects = scandir($dir, SCANDIR_SORT_NONE); @@ -2351,17 +2382,38 @@ public static function removeCacheDirectory(string $dir): void } foreach ($objects as $object) { - if ($object !== '.' && $object !== '..') { - if (filetype($dir . '/' . $object) === 'dir') { - self::removeCacheDirectory($dir . '/' . $object); - } else { - unlink($dir . '/' . $object); + if ($object === '.' || $object === '..') { + continue; + } + + $full_path = $dir . '/' . $object; + + // if it was deleted in the meantime/race condition with other psalm process + if (!file_exists($full_path)) { + continue; + } + + if (filetype($full_path) === 'dir') { + self::removeCacheDirectory($full_path); + } else { + try { + unlink($full_path); + } catch (RuntimeException $e) { + clearstatcache(true, $full_path); + if (file_exists($full_path)) { + // rethrow the error with default message + // it contains the reason why deletion failed + throw $e; + } } } } - reset($objects); - rmdir($dir); + // may have been removed in the meantime + clearstatcache(true, $dir); + if (is_dir($dir)) { + rmdir($dir); + } } } diff --git a/src/Psalm/Config/Creator.php b/src/Psalm/Config/Creator.php index de415f9ea99..7ff92a57ef2 100644 --- a/src/Psalm/Config/Creator.php +++ b/src/Psalm/Config/Creator.php @@ -251,7 +251,7 @@ private static function getPsr4Or0Paths(string $current_dir, array $composer_jso continue; } - $path = preg_replace('@[\\\\/]$@', '', $path); + $path = preg_replace('@[/\\\]$@', '', $path, 1); if ($path !== 'tests') { $nodes[] = ''; diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index 45c59af7014..2d3187e7462 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -16,10 +16,10 @@ use function is_dir; use function is_iterable; use function preg_match; -use function preg_replace; use function readlink; use function realpath; use function restore_error_handler; +use function rtrim; use function set_error_handler; use function str_replace; use function stripos; @@ -448,7 +448,7 @@ private static function isRegularExpression(string $string): bool */ protected static function slashify(string $str): string { - return preg_replace('/\/?$/', DIRECTORY_SEPARATOR, $str); + return rtrim($str, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } public function allows(string $file_name, bool $case_sensitive = false): bool diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 286cbf72ee4..70b11d39fd9 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -848,7 +848,7 @@ public function hasVariable(string $var_name): bool return false; } - $stripped_var = preg_replace('/(->|\[).*$/', '', $var_name); + $stripped_var = preg_replace('/(->|\[).*$/', '', $var_name, 1); if ($stripped_var !== '$this' || $var_name !== $stripped_var) { $this->cond_referenced_var_ids[$var_name] = true; diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index ff9bed24448..943d0b235ed 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -505,6 +505,16 @@ public function analyze( $member_stmts[] = $stmt; foreach ($stmt->consts as $const) { + if ($const->name->toLowerString() === 'class') { + IssueBuffer::maybeAdd( + new ReservedWord( + 'A class constant cannot be named \'class\'', + new CodeLocation($this, $this->class), + $this->fq_class_name + ) + ); + } + $const_id = strtolower($this->fq_class_name) . '::' . $const->name; foreach ($codebase->class_constants_to_rename as $original_const_id => $new_const_name) { @@ -1365,7 +1375,7 @@ private function analyzeTraitUse( return false; } - if (!$codebase->traitHasCorrectCase($fq_trait_name)) { + if (!$codebase->traitHasCorrectCasing($fq_trait_name)) { if (IssueBuffer::accepts( new UndefinedTrait( 'Trait ' . $fq_trait_name . ' has wrong casing', diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index e964cce71db..a01207ad2eb 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -241,7 +241,7 @@ public static function checkFullyQualifiedClassLikeName( return null; } - $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name); + $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name, 1); if (in_array($fq_class_name, ['callable', 'iterable', 'self', 'static', 'parent'], true)) { return true; @@ -371,16 +371,14 @@ public static function checkFullyQualifiedClassLikeName( || ($interface_exists && !$codebase->interfaceHasCorrectCasing($fq_class_name)) || ($enum_exists && !$codebase->classlikes->enumHasCorrectCasing($fq_class_name)) ) { - if ($codebase->classlikes->isUserDefined(strtolower($aliased_name))) { - IssueBuffer::maybeAdd( - new InvalidClass( - 'Class, interface or enum ' . $fq_class_name . ' has wrong casing', - $code_location, - $fq_class_name - ), - $suppressed_issues - ); - } + IssueBuffer::maybeAdd( + new InvalidClass( + 'Class, interface or enum ' . $fq_class_name . ' has wrong casing', + $code_location, + $fq_class_name + ), + $suppressed_issues + ); } } diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 29fc51a3080..56b669a81fb 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -707,6 +707,7 @@ public function analyze( } } + $missingThrowsDocblockErrors = []; foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $codelocations) { $is_expected = false; @@ -720,6 +721,14 @@ public function analyze( } if (!$is_expected) { + $missing_docblock_exception = new TNamedObject($possibly_thrown_exception); + $missingThrowsDocblockErrors[] = $missing_docblock_exception->toNamespacedString( + $this->source->getNamespace(), + $this->source->getAliasedClassesFlipped(), + $this->source->getFQCLN(), + true + ); + foreach ($codelocations as $codelocation) { // issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc. IssueBuffer::maybeAdd( @@ -733,6 +742,17 @@ public function analyze( } } + if ($codebase->alter_code + && isset($project_analyzer->getIssuesToFix()['MissingThrowsDocblock']) + ) { + $manipulator = FunctionDocblockManipulator::getForFunction( + $project_analyzer, + $this->source->getFilePath(), + $this->function + ); + $manipulator->addThrowsDocblock($missingThrowsDocblockErrors); + } + if ($codebase->taint_flow_graph && $this->function instanceof ClassMethod && $cased_method_id diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index 06413e6bbbf..a3d658b6e01 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -221,7 +221,7 @@ public static function isWithinAny(string $calling_identifier, array $identifier */ public static function getNameSpaceRoot(string $fullyQualifiedClassName): string { - $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); + $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName, 1); if ($root_namespace === "") { throw new InvalidArgumentException("Invalid classname \"$fullyQualifiedClassName\""); } diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index d25d5ae6d72..d1d45117b6c 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -205,6 +205,11 @@ class ProjectAnalyzer */ public $provide_completion = false; + /** + * @var list + */ + public $check_paths_files = []; + /** * @var array */ @@ -399,6 +404,7 @@ public static function getFileReportOptions(array $report_file_paths, bool $show '.pylint' => Report::TYPE_PYLINT, '.console' => Report::TYPE_CONSOLE, '.sarif' => Report::TYPE_SARIF, + 'count.txt' => Report::TYPE_COUNT, ]; foreach ($report_file_paths as $report_file_path) { @@ -1181,6 +1187,7 @@ public function checkPaths(array $paths_to_check): void if (is_dir($path)) { $this->checkDirWithConfig($path, $this->config, true); } elseif (is_file($path)) { + $this->check_paths_files[] = $path; $this->codebase->addFilesToAnalyze([$path => $path]); $this->config->hide_external_errors = $this->config->isInProjectDirs($path); } @@ -1324,6 +1331,7 @@ public function setIssuesToFix(array $issues): void $supported_issues_to_fix[] = 'MissingImmutableAnnotation'; $supported_issues_to_fix[] = 'MissingPureAnnotation'; + $supported_issues_to_fix[] = 'MissingThrowsDocblock'; $unsupportedIssues = array_diff(array_keys($issues), $supported_issues_to_fix); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 0e5eb2b7435..09e411a90c4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -1893,6 +1893,10 @@ private static function getIsAssertion(string $function_name): ?Assertion return new IsType(new Atomic\TIterable()); case 'is_countable': return new IsCountable(); + case 'ctype_digit': + return new IsType(new Atomic\TNumericString); + case 'ctype_lower': + return new IsType(new Atomic\TNonEmptyLowercaseString); } return null; @@ -3724,8 +3728,8 @@ private static function getArrayKeyExistsAssertions( ) : null; - if ($array_root) { - if ($first_var_name === null && isset($expr->getArgs()[0])) { + if ($array_root && isset($expr->getArgs()[0])) { + if ($first_var_name === null) { $first_arg = $expr->getArgs()[0]; if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) { @@ -3756,7 +3760,10 @@ private static function getArrayKeyExistsAssertions( } else { $first_var_name = null; } - } elseif ($expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\Variable + } elseif (($expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\Variable + || $expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\PropertyFetch + || $expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\StaticPropertyFetch + ) && $source instanceof StatementsAnalyzer && ($first_var_type = $source->node_data->getType($expr->getArgs()[0]->value)) ) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index a4f3924df52..09e1c693993 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -198,6 +198,14 @@ public static function analyze( $numeric_type ); + $right_is_numeric = UnionTypeComparator::isContainedBy( + $codebase, + $right_type, + $numeric_type + ); + + $has_numeric_type = $left_is_numeric || $right_is_numeric; + if ($left_is_numeric) { $right_uint = new Union([new TIntRange(0, null)]); $right_is_uint = UnionTypeComparator::isContainedBy( @@ -228,16 +236,23 @@ public static function analyze( $non_empty_string = clone $numeric_type; $non_empty_string->addType(new TNonEmptyString()); - $has_non_empty = UnionTypeComparator::isContainedBy( + $left_non_empty = UnionTypeComparator::isContainedBy( $codebase, $left_type, $non_empty_string - ) || UnionTypeComparator::isContainedBy( + ); + + $right_non_empty = UnionTypeComparator::isContainedBy( $codebase, $right_type, $non_empty_string ); + $has_non_empty = $left_non_empty || $right_non_empty; + $all_non_empty = $left_non_empty && $right_non_empty; + + $has_numeric_and_non_empty = $has_numeric_type && $has_non_empty; + $all_literals = $left_type->allLiterals() && $right_type->allLiterals(); if ($has_non_empty) { @@ -246,7 +261,8 @@ public static function analyze( } elseif ($all_lowercase) { $result_type = Type::getNonEmptyLowercaseString(); } else { - $result_type = Type::getNonEmptyString(); + $result_type = $all_non_empty || $has_numeric_and_non_empty ? + Type::getNonFalsyString() : Type::getNonEmptyString(); } } else { if ($all_literals) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index e87585606f5..31c8ddc4bd2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -180,7 +180,7 @@ public static function checkArgumentMatches( IssueBuffer::maybeAdd( new InvalidLiteralArgument( 'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id - . ' expects a non-literal value, ' . $arg_value_type->getId() . ' provided', + . ' expects a non-literal value, but ' . $arg_value_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $arg->value), $cased_method_id ), @@ -974,7 +974,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new MixedArgumentTypeCoercion( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', parent type ' . $input_type->getId() . ' provided', + ', but parent type ' . $input_type->getId() . ' provided', $arg_location, $cased_method_id, $origin_location @@ -985,7 +985,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new ArgumentTypeCoercion( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', parent type ' . $input_type->getId() . ' provided', + ', but parent type ' . $input_type->getId() . ' provided', $arg_location, $cased_method_id ), @@ -998,7 +998,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new ImplicitToStringCast( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . - $param_type->getId() . ', ' . $input_type->getId() . ' provided with a __toString method', + $param_type->getId() . ', but ' . $input_type->getId() . ' provided with a __toString method', $arg_location ), $statements_analyzer->getSuppressedIssues() @@ -1020,7 +1020,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new InvalidScalarArgument( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . - $param_type->getId() . ', ' . $type . ' provided', + $param_type->getId() . ', but ' . $type . ' provided', $arg_location, $cased_method_id ), @@ -1031,7 +1031,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new PossiblyInvalidArgument( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', possibly different type ' . $type . ' provided', + ', but possibly different type ' . $type . ' provided', $arg_location, $cased_method_id ), @@ -1041,7 +1041,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new InvalidArgument( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', ' . $type . ' provided', + ', but ' . $type . ' provided', $arg_location, $cased_method_id ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index a34f9e6d030..d39fc69dce5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -891,7 +891,8 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new MixedArgumentTypeCoercion( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . + ', but parent type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), @@ -901,7 +902,8 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new ArgumentTypeCoercion( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . + ', but parent type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), @@ -921,7 +923,7 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new InvalidScalarArgument( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . ', but ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), @@ -931,7 +933,7 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new PossiblyInvalidArgument( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' - . $closure_param_type->getId() . ', possibly different type ' + . $closure_param_type->getId() . ', but possibly different type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id @@ -941,7 +943,7 @@ private static function checkClosureTypeArgs( } elseif (IssueBuffer::accepts( new InvalidArgument( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . ', but ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 96af6145b5d..ff7e03d122c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -753,7 +753,7 @@ private static function getAnalyzeNamedExpression( if (strpos($var_type_part->value, '::')) { $parts = explode('::', strtolower($var_type_part->value)); $fq_class_name = $parts[0]; - $fq_class_name = preg_replace('/^\\\\/', '', $fq_class_name); + $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name, 1); $potential_method_id = new MethodIdentifier($fq_class_name, $parts[1]); } else { $function_call_info->new_function_name = new VirtualFullyQualified( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 619868af958..7f02b1441a3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -57,6 +57,7 @@ use function array_filter; use function array_map; +use function array_values; use function assert; use function count; use function in_array; @@ -376,39 +377,15 @@ private static function handleNamedCall( if (!$naive_method_exists && $codebase->methods->existence_provider->has($fq_class_name) ) { - $method_exists = $codebase->methods->existence_provider->doesMethodExist( + $fake_method_exists = $codebase->methods->existence_provider->doesMethodExist( $fq_class_name, $method_id->method_name, $statements_analyzer, null - ); - - if ($method_exists) { - $fake_method_exists = true; - } - } - - if ($stmt->isFirstClassCallable()) { - $method_storage = ($class_storage->methods[$method_name_lc] ?? - ($class_storage->pseudo_static_methods[$method_name_lc] ?? null)); - - if ($method_storage) { - $return_type_candidate = new Union([new TClosure( - 'Closure', - $method_storage->params, - $method_storage->return_type, - $method_storage->pure - )]); - } else { - $return_type_candidate = Type::getClosure(); - } - - $statements_analyzer->node_data->setType($stmt, $return_type_candidate); - - return true; + ) ?? false; } - $args = $stmt->getArgs(); + $args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs(); if (!$naive_method_exists && $class_storage->mixin_declaring_fqcln @@ -512,6 +489,53 @@ private static function handleNamedCall( $method_name_lc ); + if ($stmt->isFirstClassCallable()) { + if ($found_method_and_class_storage) { + [ $method_storage ] = $found_method_and_class_storage; + + $return_type_candidate = new Union([new TClosure( + 'Closure', + $method_storage->params, + $method_storage->return_type, + $method_storage->pure + )]); + } else { + $method_exists = $naive_method_exists + || $fake_method_exists + || isset($class_storage->methods[$method_name_lc]) + || isset($class_storage->pseudo_static_methods[$method_name_lc]); + + if ($method_exists) { + $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id) ?? $method_id; + + $return_type_candidate = new Union([new TClosure( + 'Closure', + array_values($codebase->getMethodParams($method_id)), + $codebase->getMethodReturnType($method_id, $fq_class_name), + $codebase->methods->getStorage($declaring_method_id)->pure + )]); + } else { + // FIXME: perhaps Psalm should complain about nonexisting method here, or throw a logic exception? + $return_type_candidate = Type::getClosure(); + } + } + + $expanded_return_type = TypeExpander::expandUnion( + $codebase, + $return_type_candidate, + $context->self, + $class_storage->name, + $context->parent, + true, + false, + true + ); + + $statements_analyzer->node_data->setType($stmt, $expanded_return_type); + + return true; + } + if (!$naive_method_exists || !MethodAnalyzer::isMethodVisible( $method_id, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index f7e0d7e09aa..06ccbdaaab3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -554,6 +554,14 @@ private static function getMethodReturnType( ) { $static_type = $context->self; $context_final = $codebase->classlike_storage_provider->get($context->self)->final; + } elseif ($context->calling_method_id !== null) { + // differentiate between these cases: + // 1. "static" comes from the CALLED static method - use $fq_class_name. + // 2. "static" in return type comes from return type of the + // method CALLING the currently analyzed static method - use $context->self. + $static_type = self::hasStaticInType($return_type_candidate) + ? $fq_class_name + : $context->self; } else { $static_type = $fq_class_name; } @@ -616,4 +624,22 @@ private static function getMethodReturnType( return $return_type_candidate; } + + /** + * Dumb way to determine whether a type contains "static" somewhere inside. + */ + private static function hasStaticInType(Type\TypeNode $type): bool + { + if ($type instanceof TNamedObject && ($type->value === 'static' || $type->is_static)) { + return true; + } + + foreach ($type->getChildNodes() as $child_type) { + if (self::hasStaticInType($child_type)) { + return true; + } + } + + return false; + } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index ade1205332c..619d2ac00e6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -520,7 +520,7 @@ public static function getFunctionIdsFromCallableArg( } if ($callable_arg instanceof PhpParser\Node\Scalar\String_) { - $potential_id = preg_replace('/^\\\/', '', $callable_arg->value); + $potential_id = preg_replace('/^\\\/', '', $callable_arg->value, 1); if (preg_match('/^[A-Za-z0-9_]+(\\\[A-Za-z0-9_]+)*(::[A-Za-z0-9_]+)?$/', $potential_id)) { return [$potential_id]; @@ -550,7 +550,7 @@ public static function getFunctionIdsFromCallableArg( } if ($class_arg instanceof PhpParser\Node\Scalar\String_) { - return [preg_replace('/^\\\/', '', $class_arg->value) . '::' . $method_name_arg->value]; + return [preg_replace('/^\\\/', '', $class_arg->value, 1) . '::' . $method_name_arg->value]; } if ($class_arg instanceof PhpParser\Node\Expr\ClassConstFetch diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 48795d26f34..10258732bd0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -17,11 +17,14 @@ use Psalm\Issue\PossiblyInvalidCast; use Psalm\Issue\RedundantCast; use Psalm\Issue\RedundantCastGivenDocblockType; +use Psalm\Issue\RiskyCast; use Psalm\Issue\UnrecognizedExpression; use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; +use Psalm\Type\Atomic\TBool; +use Psalm\Type\Atomic\TClosedResource; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; @@ -32,6 +35,9 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; +use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; @@ -41,18 +47,28 @@ use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Atomic\TTrue; use Psalm\Type\Union; use function array_merge; use function array_pop; use function array_values; use function get_class; +use function strtolower; /** * @internal */ class CastAnalyzer { + /** @var string[] */ + private const PSEUDO_CASTABLE_CLASSES = [ + 'SimpleXMLElement', + 'DOMNode', + 'GMP', + 'Decimal\Decimal', + ]; + public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Cast $stmt, @@ -63,50 +79,23 @@ public static function analyze( return false; } - $valid_int_type = null; - $type_parent_nodes = null; $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); if ($maybe_type) { if ($maybe_type->isInt()) { - $valid_int_type = $maybe_type; if (!$maybe_type->from_calculation) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } - } elseif ($maybe_type->isSingleStringLiteral()) { - $valid_int_type = Type::getInt(false, (int)$maybe_type->getSingleStringLiteral()->value); - } - - if ($maybe_type->hasBool()) { - $casted_type = clone $maybe_type; - if (isset($casted_type->getAtomicTypes()['bool'])) { - $casted_type->addType(new TLiteralInt(0)); - $casted_type->addType(new TLiteralInt(1)); - } else { - if (isset($casted_type->getAtomicTypes()['true'])) { - $casted_type->addType(new TLiteralInt(1)); - } - if (isset($casted_type->getAtomicTypes()['false'])) { - $casted_type->addType(new TLiteralInt(0)); - } - } - $casted_type->removeType('bool'); - $casted_type->removeType('true'); - $casted_type->removeType('false'); - - if ($casted_type->isInt()) { - $valid_int_type = $casted_type; - } } - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { - $type_parent_nodes = $maybe_type->parent_nodes; - } - } - - $type = $valid_int_type ?? Type::getInt(); - if ($type_parent_nodes !== null) { - $type->parent_nodes = $type_parent_nodes; + $type = self::castIntAttempt( + $statements_analyzer, + $maybe_type, + $stmt->expr, + true + ); + } else { + $type = Type::getInt(); } $statements_analyzer->node_data->setType($stmt, $type); @@ -125,13 +114,15 @@ public static function analyze( if ($maybe_type->isFloat()) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } - } - - $type = Type::getFloat(); - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; + $type = self::castFloatAttempt( + $statements_analyzer, + $maybe_type, + $stmt->expr, + true + ); + } else { + $type = Type::getFloat(); } $statements_analyzer->node_data->setType($stmt, $type); @@ -309,6 +300,379 @@ public static function analyze( return false; } + public static function castIntAttempt( + StatementsAnalyzer $statements_analyzer, + Union $stmt_type, + PhpParser\Node\Expr $stmt, + bool $explicit_cast = false + ): Union { + $codebase = $statements_analyzer->getCodebase(); + + $risky_cast = []; + $invalid_casts = []; + $valid_ints = []; + $castable_types = []; + + $atomic_types = $stmt_type->getAtomicTypes(); + + $parent_nodes = []; + + if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { + $parent_nodes = $stmt_type->parent_nodes; + } + + while ($atomic_types) { + $atomic_type = array_pop($atomic_types); + + if ($atomic_type instanceof TInt) { + $valid_ints[] = $atomic_type; + + continue; + } + + if ($atomic_type instanceof TFloat) { + if ($atomic_type instanceof TLiteralFloat) { + $valid_ints[] = new TLiteralInt((int) $atomic_type->value); + } else { + $castable_types[] = new TInt(); + } + + continue; + } + + if ($atomic_type instanceof TString) { + if ($atomic_type instanceof TLiteralString) { + $valid_ints[] = new TLiteralInt((int) $atomic_type->value); + } elseif ($atomic_type instanceof TNumericString) { + $castable_types[] = new TInt(); + } else { + // any normal string is technically $valid_int[] = new TLiteralInt(0); + // however we cannot be certain that it's not inferred, therefore less strict + $castable_types[] = new TInt(); + } + + continue; + } + + if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse) { + $valid_ints[] = new TLiteralInt(0); + continue; + } + + if ($atomic_type instanceof TTrue) { + $valid_ints[] = new TLiteralInt(1); + continue; + } + + if ($atomic_type instanceof TBool) { + // do NOT use TIntRange here, as it will cause invalid behavior, e.g. bitwiseAssignment + $valid_ints[] = new TLiteralInt(0); + $valid_ints[] = new TLiteralInt(1); + continue; + } + + // could be invalid, but allow it, as it is allowed for TString below too + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + || $atomic_type instanceof Scalar + ) { + $castable_types[] = new TInt(); + + continue; + } + + if ($atomic_type instanceof TNamedObject) { + $intersection_types = [$atomic_type]; + + if ($atomic_type->extra_types) { + $intersection_types = array_merge($intersection_types, $atomic_type->extra_types); + } + + foreach ($intersection_types as $intersection_type) { + if (!$intersection_type instanceof TNamedObject) { + continue; + } + + // prevent "Could not get class storage for mixed" + if (!$codebase->classExists($intersection_type->value)) { + continue; + } + + foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) { + if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class) + || $codebase->classExtends( + $intersection_type->value, + $pseudo_castable_class + ) + ) { + $castable_types[] = new TInt(); + continue 3; + } + } + } + } + + if ($atomic_type instanceof TNonEmptyArray + || $atomic_type instanceof TNonEmptyList + ) { + $risky_cast[] = $atomic_type->getId(); + + $valid_ints[] = new TLiteralInt(1); + + continue; + } + + if ($atomic_type instanceof TArray + || $atomic_type instanceof TList + || $atomic_type instanceof TKeyedArray + ) { + // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not + // welcome to off-by-one hell if that happens :-) + $risky_cast[] = $atomic_type->getId(); + + $valid_ints[] = new TLiteralInt(0); + $valid_ints[] = new TLiteralInt(1); + + continue; + } + + if ($atomic_type instanceof TTemplateParam) { + $atomic_types = array_merge($atomic_types, $atomic_type->as->getAtomicTypes()); + + continue; + } + + // always 1 for "error" cases + $valid_ints[] = new TLiteralInt(1); + + $invalid_casts[] = $atomic_type->getId(); + } + + if ($invalid_casts) { + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to int', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($risky_cast) { + IssueBuffer::maybeAdd( + new RiskyCast( + 'Casting ' . $risky_cast[0] . ' to int has possibly unintended value of 0/1', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($explicit_cast && !$castable_types) { + // todo: emit error here + } + + $valid_types = array_merge($valid_ints, $castable_types); + + if (!$valid_types) { + $int_type = Type::getInt(); + } else { + $int_type = TypeCombiner::combine( + $valid_types, + $codebase + ); + } + + if ($statements_analyzer->data_flow_graph) { + $int_type->parent_nodes = $parent_nodes; + } + + return $int_type; + } + + public static function castFloatAttempt( + StatementsAnalyzer $statements_analyzer, + Union $stmt_type, + PhpParser\Node\Expr $stmt, + bool $explicit_cast = false + ): Union { + $codebase = $statements_analyzer->getCodebase(); + + $risky_cast = []; + $invalid_casts = []; + $valid_floats = []; + $castable_types = []; + + $atomic_types = $stmt_type->getAtomicTypes(); + + $parent_nodes = []; + + if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { + $parent_nodes = $stmt_type->parent_nodes; + } + + while ($atomic_types) { + $atomic_type = array_pop($atomic_types); + + if ($atomic_type instanceof TFloat) { + $valid_floats[] = $atomic_type; + + continue; + } + + if ($atomic_type instanceof TInt) { + if ($atomic_type instanceof TLiteralInt) { + $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); + } else { + $castable_types[] = new TFloat(); + } + + continue; + } + + if ($atomic_type instanceof TString) { + if ($atomic_type instanceof TLiteralString) { + $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); + } elseif ($atomic_type instanceof TNumericString) { + $castable_types[] = new TFloat(); + } else { + // any normal string is technically $valid_floats[] = new TLiteralFloat(0.0); + // however we cannot be certain that it's not inferred, therefore less strict + $castable_types[] = new TFloat(); + } + + continue; + } + + if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse) { + $valid_floats[] = new TLiteralFloat(0.0); + continue; + } + + if ($atomic_type instanceof TTrue) { + $valid_floats[] = new TLiteralFloat(1.0); + continue; + } + + if ($atomic_type instanceof TBool) { + $valid_floats[] = new TLiteralFloat(0.0); + $valid_floats[] = new TLiteralFloat(1.0); + continue; + } + + // could be invalid, but allow it, as it is allowed for TString below too + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + || $atomic_type instanceof Scalar + ) { + $castable_types[] = new TFloat(); + + continue; + } + + if ($atomic_type instanceof TNamedObject) { + $intersection_types = [$atomic_type]; + + if ($atomic_type->extra_types) { + $intersection_types = array_merge($intersection_types, $atomic_type->extra_types); + } + + foreach ($intersection_types as $intersection_type) { + if (!$intersection_type instanceof TNamedObject) { + continue; + } + + // prevent "Could not get class storage for mixed" + if (!$codebase->classExists($intersection_type->value)) { + continue; + } + + foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) { + if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class) + || $codebase->classExtends( + $intersection_type->value, + $pseudo_castable_class + ) + ) { + $castable_types[] = new TFloat(); + continue 3; + } + } + } + } + + if ($atomic_type instanceof TNonEmptyArray + || $atomic_type instanceof TNonEmptyList + ) { + $risky_cast[] = $atomic_type->getId(); + + $valid_floats[] = new TLiteralFloat(1.0); + + continue; + } + + if ($atomic_type instanceof TArray + || $atomic_type instanceof TList + || $atomic_type instanceof TKeyedArray + ) { + // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not + // welcome to off-by-one hell if that happens :-) + $risky_cast[] = $atomic_type->getId(); + + $valid_floats[] = new TLiteralFloat(0.0); + $valid_floats[] = new TLiteralFloat(1.0); + + continue; + } + + if ($atomic_type instanceof TTemplateParam) { + $atomic_types = array_merge($atomic_types, $atomic_type->as->getAtomicTypes()); + + continue; + } + + // always 1.0 for "error" cases + $valid_floats[] = new TLiteralFloat(1.0); + + $invalid_casts[] = $atomic_type->getId(); + } + + if ($invalid_casts) { + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to float', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($risky_cast) { + IssueBuffer::maybeAdd( + new RiskyCast( + 'Casting ' . $risky_cast[0] . ' to float has possibly unintended value of 0.0/1.0', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($explicit_cast && !$castable_types) { + // todo: emit error here + } + + $valid_types = array_merge($valid_floats, $castable_types); + + if (!$valid_types) { + $float_type = Type::getFloat(); + } else { + $float_type = TypeCombiner::combine( + $valid_types, + $codebase + ); + } + + if ($statements_analyzer->data_flow_graph) { + $float_type->parent_nodes = $parent_nodes; + } + + return $float_type; + } + public static function castStringAttempt( StatementsAnalyzer $statements_analyzer, Context $context, @@ -361,8 +725,28 @@ public static function castStringAttempt( continue; } + if ($atomic_type instanceof TTrue + ) { + $valid_strings[] = new TLiteralString('1'); + continue; + } + + if ($atomic_type instanceof TBool + ) { + $valid_strings[] = new TLiteralString('1'); + $valid_strings[] = new TLiteralString(''); + continue; + } + + if ($atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + ) { + $castable_types[] = new TNonEmptyString(); + + continue; + } + if ($atomic_type instanceof TMixed - || $atomic_type instanceof TResource || $atomic_type instanceof Scalar ) { $castable_types[] = new TString(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 331c818c709..740daa859d7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -13,6 +13,7 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\TaintSource; +use Psalm\Internal\Type\TypeCombiner; use Psalm\Issue\ImpureVariable; use Psalm\Issue\InvalidScope; use Psalm\Issue\PossiblyUndefinedGlobalVariable; @@ -22,12 +23,22 @@ use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\TArray; +use Psalm\Type\Atomic\TBool; +use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TIntRange; +use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; +use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; +use Psalm\Type\Atomic\TNonEmptyString; +use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use function in_array; use function is_string; +use function time; /** * @internal @@ -165,7 +176,7 @@ public static function analyze( return true; } - $type = self::getGlobalType($var_name); + $type = self::getGlobalType($var_name, $codebase->analysis_php_version_id); self::taintVariable($statements_analyzer, $var_name, $type, $stmt); @@ -540,7 +551,7 @@ public static function isSuperGlobal(string $var_id): bool ); } - public static function getGlobalType(string $var_id): Union + public static function getGlobalType(string $var_id, int $codebase_analysis_php_version_id): Union { $config = Config::getInstance(); @@ -549,26 +560,252 @@ public static function getGlobalType(string $var_id): Union } if ($var_id === '$argv') { - return new Union([ - new TArray([Type::getInt(), Type::getString()]), + // only in CLI, null otherwise + $argv_nullable = new Union([ + new TNonEmptyList(Type::getString()), + new TNull() ]); + // use TNull explicitly instead of this + // as it will cause weird errors due to ignore_nullable_issues true + // e.g. InvalidPropertyAssignmentValue + // $this->argv 'list' cannot be assigned type 'non-empty-list' + // $argv_nullable->possibly_undefined = true; + $argv_nullable->ignore_nullable_issues = true; + return $argv_nullable; } if ($var_id === '$argc') { - return Type::getInt(); + // only in CLI, null otherwise + $argc_nullable = new Union([ + new TIntRange(1, null), + new TNull() + ]); + // $argc_nullable->possibly_undefined = true; + $argc_nullable->ignore_nullable_issues = true; + return $argc_nullable; + } + + if (!self::isSuperGlobal($var_id)) { + return Type::getMixed(); } if ($var_id === '$http_response_header') { return new Union([ - new TList(Type::getString()) + new TList(Type::getNonEmptyString()) + ]); + } + + if ($var_id === '$GLOBALS') { + return new Union([ + new TNonEmptyArray([ + Type::getNonEmptyString(), + Type::getMixed() + ]) ]); } - if (self::isSuperGlobal($var_id)) { - $type = Type::getArray(); - if ($var_id === '$_SESSION') { - $type->possibly_undefined = true; + if ($var_id === '$_COOKIE') { + $type = new TArray( + [ + Type::getNonEmptyString(), + Type::getString(), + ] + ); + + return new Union([$type]); + } + + if (in_array($var_id, array('$_GET', '$_POST', '$_REQUEST'), true)) { + $array_key = new Union([new TNonEmptyString(), new TInt()]); + $array = new TNonEmptyArray( + [ + $array_key, + new Union([ + new TString(), + new TArray([ + $array_key, + Type::getMixed() + ]) + ]) + ] + ); + + $type = new TArray( + [ + $array_key, + new Union([new TString(), $array]), + ] + ); + + return new Union([$type]); + } + + if ($var_id === '$_SERVER' || $var_id === '$_ENV') { + $string_helper = Type::getString(); + $string_helper->possibly_undefined = true; + + $non_empty_string_helper = Type::getNonEmptyString(); + $non_empty_string_helper->possibly_undefined = true; + + $argv_helper = new Union([ + new TNonEmptyList(Type::getString()) + ]); + $argv_helper->possibly_undefined = true; + + $argc_helper = new Union([ + new TIntRange(1, null) + ]); + $argc_helper->possibly_undefined = true; + + $request_time_helper = new Union([ + new TIntRange(time(), null) + ]); + $request_time_helper->possibly_undefined = true; + + $request_time_float_helper = Type::getFloat(); + $request_time_float_helper->possibly_undefined = true; + + $bool_string_helper = new Union([new TBool(), new TString()]); + $bool_string_helper->possibly_undefined = true; + + $detailed_type = new TKeyedArray([ + // https://www.php.net/manual/en/reserved.variables.server.php + 'PHP_SELF' => $non_empty_string_helper, + 'argv' => $argv_helper, + 'argc' => $argc_helper, + 'GATEWAY_INTERFACE' => $non_empty_string_helper, + 'SERVER_ADDR' => $non_empty_string_helper, + 'SERVER_NAME' => $non_empty_string_helper, + 'SERVER_SOFTWARE' => $non_empty_string_helper, + 'SERVER_PROTOCOL' => $non_empty_string_helper, + 'REQUEST_METHOD' => $non_empty_string_helper, + 'REQUEST_TIME' => $request_time_helper, + 'REQUEST_TIME_FLOAT' => $request_time_float_helper, + 'QUERY_STRING' => $string_helper, + 'DOCUMENT_ROOT' => $non_empty_string_helper, + 'HTTP_ACCEPT' => $non_empty_string_helper, + 'HTTP_ACCEPT_CHARSET' => $non_empty_string_helper, + 'HTTP_ACCEPT_ENCODING' => $non_empty_string_helper, + 'HTTP_ACCEPT_LANGUAGE' => $non_empty_string_helper, + 'HTTP_CONNECTION' => $non_empty_string_helper, + 'HTTP_HOST' => $non_empty_string_helper, + 'HTTP_REFERER' => $non_empty_string_helper, + 'HTTP_USER_AGENT' => $non_empty_string_helper, + 'HTTPS' => $string_helper, + 'REMOTE_ADDR' => $non_empty_string_helper, + 'REMOTE_HOST' => $non_empty_string_helper, + 'REMOTE_PORT' => $string_helper, + 'REMOTE_USER' => $non_empty_string_helper, + 'REDIRECT_REMOTE_USER' => $non_empty_string_helper, + 'SCRIPT_FILENAME' => $non_empty_string_helper, + 'SERVER_ADMIN' => $non_empty_string_helper, + 'SERVER_PORT' => $non_empty_string_helper, + 'SERVER_SIGNATURE' => $non_empty_string_helper, + 'PATH_TRANSLATED' => $non_empty_string_helper, + 'SCRIPT_NAME' => $non_empty_string_helper, + 'REQUEST_URI' => $non_empty_string_helper, + 'PHP_AUTH_DIGEST' => $non_empty_string_helper, + 'PHP_AUTH_USER' => $non_empty_string_helper, + 'PHP_AUTH_PW' => $non_empty_string_helper, + 'AUTH_TYPE' => $non_empty_string_helper, + 'PATH_INFO' => $non_empty_string_helper, + 'ORIG_PATH_INFO' => $non_empty_string_helper, + // misc from RFC not included above already http://www.faqs.org/rfcs/rfc3875.html + 'CONTENT_LENGTH' => $string_helper, + 'CONTENT_TYPE' => $string_helper, + // common, misc stuff + 'FCGI_ROLE' => $non_empty_string_helper, + 'HOME' => $non_empty_string_helper, + 'HTTP_CACHE_CONTROL' => $non_empty_string_helper, + 'HTTP_COOKIE' => $non_empty_string_helper, + 'HTTP_PRIORITY' => $non_empty_string_helper, + 'PATH' => $non_empty_string_helper, + 'REDIRECT_STATUS' => $non_empty_string_helper, + 'REQUEST_SCHEME' => $non_empty_string_helper, + 'USER' => $non_empty_string_helper, + // common, misc headers + 'HTTP_UPGRADE_INSECURE_REQUESTS' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_PROTO' => $non_empty_string_helper, + 'HTTP_CLIENT_IP' => $non_empty_string_helper, + 'HTTP_X_REAL_IP' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_FOR' => $non_empty_string_helper, + 'HTTP_CF_CONNECTING_IP' => $non_empty_string_helper, + 'HTTP_CF_IPCOUNTRY' => $non_empty_string_helper, + 'HTTP_CF_VISITOR' => $non_empty_string_helper, + 'HTTP_CDN_LOOP' => $non_empty_string_helper, + // common, misc browser headers + 'HTTP_DNT' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_DEST' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_USER' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_MODE' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_SITE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA' => $non_empty_string_helper, + // phpunit + 'APP_DEBUG' => $bool_string_helper, + 'APP_ENV' => $string_helper, + ]); + + // generic case for all other elements + $detailed_type->previous_key_type = Type::getNonEmptyString(); + $detailed_type->previous_value_type = Type::getString(); + + return new Union([$detailed_type]); + } + + if ($var_id === '$_FILES') { + $values = [ + 'name' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'type' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'size' => new Union([ + new TIntRange(0, null), + new TNonEmptyList(Type::getInt()), + ]), + 'tmp_name' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'error' => new Union([ + new TIntRange(0, 8), + new TNonEmptyList(Type::getInt()), + ]), + ]; + + if ($codebase_analysis_php_version_id >= 8_10_00) { + $values['full_path'] = new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]); } + + $type = new TKeyedArray($values); + + // $_FILES['userfile']['...'] case + $named_type = new TArray([Type::getNonEmptyString(), new Union([$type])]); + + // by default $_FILES is an empty array + $default_type = new TArray([Type::getNever(), Type::getNever()]); + + // ideally we would have 4 separate arrays with distinct types, but that isn't possible with psalm atm + return TypeCombiner::combine([$default_type, $type, $named_type]); + } + + if ($var_id === '$_SESSION') { + // keys must be string + $type = new Union([ + new TArray([ + Type::getNonEmptyString(), + Type::getMixed(), + ]) + ]); + $type->possibly_undefined = true; return $type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php index d2db43f23cd..accdcc67fd0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php @@ -36,6 +36,7 @@ public static function analyze( ); } + $codebase = $statements_analyzer->getCodebase(); $source = $statements_analyzer->getSource(); $function_storage = $source instanceof FunctionLikeAnalyzer ? $source->getFunctionLikeStorage($statements_analyzer) @@ -47,7 +48,8 @@ public static function analyze( $var_id = '$' . $var->name; if ($var->name === 'argv' || $var->name === 'argc') { - $context->vars_in_scope[$var_id] = VariableFetchAnalyzer::getGlobalType($var_id); + $context->vars_in_scope[$var_id] = + VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); } elseif (isset($function_storage->global_types[$var_id])) { $context->vars_in_scope[$var_id] = clone $function_storage->global_types[$var_id]; $context->vars_possibly_in_scope[$var_id] = true; @@ -55,7 +57,7 @@ public static function analyze( $context->vars_in_scope[$var_id] = $global_context && $global_context->hasVariable($var_id) ? clone $global_context->vars_in_scope[$var_id] - : VariableFetchAnalyzer::getGlobalType($var_id); + : VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); $context->vars_possibly_in_scope[$var_id] = true; diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 327315ced54..50d1318cb5d 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -32,6 +32,7 @@ use function in_array; use function ini_set; use function is_array; +use function is_numeric; use function is_string; use function preg_replace; use function realpath; @@ -96,7 +97,7 @@ public static function run(array $argv): void array_map( static function (string $arg) use ($valid_long_options): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if (!in_array($arg_name, $valid_long_options, true) && !in_array($arg_name . ':', $valid_long_options, true) @@ -300,7 +301,7 @@ static function (string $arg) use ($valid_long_options): void { $find_unused_code = 'auto'; } - if (isset($options['disable-on-change'])) { + if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) { $project_analyzer->onchange_line_limit = (int) $options['disable-on-change']; } diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index e3aec2a4777..5fa057c06e3 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -434,7 +434,7 @@ private static function validateCliArguments(array $args): void array_map( static function (string $arg): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if (!in_array($arg_name, self::LONG_OPTIONS) && !in_array($arg_name . ':', self::LONG_OPTIONS) diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 7da3c7498f7..bfbe749fb61 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -44,6 +44,7 @@ use function ini_set; use function is_array; use function is_dir; +use function is_numeric; use function is_string; use function microtime; use function pathinfo; @@ -231,7 +232,7 @@ public static function run(array $argv): void chdir($current_dir); } - $threads = isset($options['threads']) ? (int)$options['threads'] : 1; + $threads = isset($options['threads']) && is_numeric($options['threads']) ? (int)$options['threads'] : 1; if (isset($options['no-cache'])) { $providers = new Providers( @@ -433,7 +434,7 @@ private static function validateCliArguments(array $args): void array_map( static function (string $arg): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if ($arg_name === 'alter') { // valid option for psalm, ignored by psalter diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 37385bed493..e6dd58f0efd 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -35,6 +35,7 @@ use function in_array; use function ini_set; use function is_array; +use function is_numeric; use function is_string; use function max; use function microtime; @@ -85,7 +86,7 @@ public static function run(array $argv): void array_map( static function (string $arg) use ($valid_long_options): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if ($arg_name === 'refactor') { // valid option for psalm, ignored by psalter @@ -286,7 +287,7 @@ static function (string $arg) use ($valid_long_options): void { chdir($current_dir); } - $threads = isset($options['threads']) + $threads = isset($options['threads']) && is_numeric($options['threads']) ? (int)$options['threads'] : max(1, ProjectAnalyzer::getCpuCount() - 2); diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index 8344aaff897..1239d28ed27 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -693,7 +693,7 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void $method_param_uses[$member_id] ); - $member_stub = preg_replace('/::.*$/', '::*', $member_id); + $member_stub = preg_replace('/::.*$/', '::*', $member_id, 1); if (isset($all_referencing_methods[$member_stub])) { $newly_invalidated_methods = array_merge( diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 9a78716c0dc..cae25acbaa4 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -130,7 +130,12 @@ class ClassLikes /** * @var array */ - private $classlike_aliases = []; + private $classlike_aliases_map = []; + + /** + * @var array + */ + private $existing_classlike_aliases = []; /** * @var array @@ -184,7 +189,7 @@ private function collectPredefinedClassLikes(): void $predefined_classes = get_declared_classes(); foreach ($predefined_classes as $predefined_class) { - $predefined_class = preg_replace('/^\\\/', '', $predefined_class); + $predefined_class = preg_replace('/^\\\/', '', $predefined_class, 1); /** @psalm-suppress ArgumentTypeCoercion */ $reflection_class = new ReflectionClass($predefined_class); @@ -200,7 +205,7 @@ private function collectPredefinedClassLikes(): void $predefined_interfaces = get_declared_interfaces(); foreach ($predefined_interfaces as $predefined_interface) { - $predefined_interface = preg_replace('/^\\\/', '', $predefined_interface); + $predefined_interface = preg_replace('/^\\\/', '', $predefined_interface, 1); /** @psalm-suppress ArgumentTypeCoercion */ $reflection_class = new ReflectionClass($predefined_interface); @@ -749,7 +754,7 @@ public function classHasCorrectCasing(string $fq_class_name): bool return true; } - if (isset($this->classlike_aliases[strtolower($fq_class_name)])) { + if (isset($this->existing_classlike_aliases[$fq_class_name])) { return true; } @@ -758,7 +763,7 @@ public function classHasCorrectCasing(string $fq_class_name): bool public function interfaceHasCorrectCasing(string $fq_interface_name): bool { - if (isset($this->classlike_aliases[strtolower($fq_interface_name)])) { + if (isset($this->existing_classlike_aliases[$fq_interface_name])) { return true; } @@ -767,30 +772,22 @@ public function interfaceHasCorrectCasing(string $fq_interface_name): bool public function enumHasCorrectCasing(string $fq_enum_name): bool { - if (isset($this->classlike_aliases[strtolower($fq_enum_name)])) { + if (isset($this->existing_classlike_aliases[$fq_enum_name])) { return true; } return isset($this->existing_enums[$fq_enum_name]); } - public function traitHasCorrectCase(string $fq_trait_name): bool + public function traitHasCorrectCasing(string $fq_trait_name): bool { - if (isset($this->classlike_aliases[strtolower($fq_trait_name)])) { + if (isset($this->existing_classlike_aliases[$fq_trait_name])) { return true; } return isset($this->existing_traits[$fq_trait_name]); } - /** - * @param lowercase-string $fq_class_name - */ - public function isUserDefined(string $fq_class_name): bool - { - return $this->classlike_storage_provider->get($fq_class_name)->user_defined; - } - public function getTraitNode(string $fq_trait_name): PhpParser\Node\Stmt\Trait_ { $fq_trait_name_lc = strtolower($fq_trait_name); @@ -830,12 +827,10 @@ public function getTraitNode(string $fq_trait_name): PhpParser\Node\Stmt\Trait_ throw new UnexpectedValueException('Could not locate trait statement'); } - /** - * @param lowercase-string $alias_name - */ public function addClassAlias(string $fq_class_name, string $alias_name): void { - $this->classlike_aliases[$alias_name] = $fq_class_name; + $this->classlike_aliases_map[strtolower($alias_name)] = $fq_class_name; + $this->existing_classlike_aliases[$alias_name] = true; } public function getUnAliasedName(string $alias_name): string @@ -845,7 +840,7 @@ public function getUnAliasedName(string $alias_name): string return $alias_name; } - $result = $this->classlike_aliases[$alias_name_lc] ?? $alias_name; + $result = $this->classlike_aliases_map[$alias_name_lc] ?? $alias_name; if ($result === $alias_name) { return $result; } diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index faa5f80daae..9ce746a5218 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -89,8 +89,9 @@ public function getStorage( $function_id = substr($function_id, 1); } + $from_stubs = false; if (isset(self::$stubbed_functions[$function_id])) { - return self::$stubbed_functions[$function_id]; + $from_stubs = self::$stubbed_functions[$function_id]; } $file_storage = null; @@ -122,6 +123,10 @@ public function getStorage( return $this->reflection->getFunctionStorage($function_id); } + if ($from_stubs) { + return $from_stubs; + } + throw new UnexpectedValueException( 'Expecting non-empty $root_file_path and $checked_file_path' ); @@ -140,6 +145,10 @@ public function getStorage( } } + if ($from_stubs) { + return $from_stubs; + } + throw new UnexpectedValueException( 'Expecting ' . $function_id . ' to have storage in ' . $checked_file_path ); @@ -150,6 +159,10 @@ public function getStorage( $declaring_file_storage = $this->file_storage_provider->get($declaring_file_path); if (!isset($declaring_file_storage->functions[$function_id])) { + if ($from_stubs) { + return $from_stubs; + } + throw new UnexpectedValueException( 'Not expecting ' . $function_id . ' to not have storage in ' . $declaring_file_path ); diff --git a/src/Psalm/Internal/Codebase/Properties.php b/src/Psalm/Internal/Codebase/Properties.php index 5e9dcefe6f3..b74a0bddcfd 100644 --- a/src/Psalm/Internal/Codebase/Properties.php +++ b/src/Psalm/Internal/Codebase/Properties.php @@ -15,7 +15,7 @@ use UnexpectedValueException; use function explode; -use function preg_replace; +use function ltrim; use function strtolower; /** @@ -83,8 +83,8 @@ public function propertyExists( ?Context $context = null, ?CodeLocation $code_location = null ): bool { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); $fq_class_name_lc = strtolower($fq_class_name); @@ -248,8 +248,8 @@ public function getAppearingClassForProperty( public function getStorage(string $property_id): PropertyStorage { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); @@ -269,8 +269,8 @@ public function getStorage(string $property_id): PropertyStorage public function hasStorage(string $property_id): bool { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); @@ -291,8 +291,8 @@ public function getPropertyType( ?StatementsSource $source = null, ?Context $context = null ): ?Union { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index 228472d4d62..0fd3e6a1a4b 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -597,7 +597,7 @@ private function scanFile( } foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { - $this->codebase->exhumeClassLikeStorage(strtolower($fq_classlike_name), $file_path); + $this->codebase->exhumeClassLikeStorage($fq_classlike_name, $file_path); } foreach ($file_storage->required_classes as $fq_classlike_name) { @@ -728,7 +728,7 @@ function () use ($fq_class_name): ?ReflectionClass { $new_fq_class_name_lc = strtolower($new_fq_class_name); if ($new_fq_class_name_lc !== $fq_class_name_lc) { - $classlikes->addClassAlias($new_fq_class_name, $fq_class_name_lc); + $classlikes->addClassAlias($new_fq_class_name, $fq_class_name); $fq_class_name_lc = $new_fq_class_name_lc; } diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index 3e8bbe7ae6d..3eddbad6244 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -14,7 +14,9 @@ use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Scanner\ParsedDocblock; +use function array_key_exists; use function array_merge; +use function array_reduce; use function count; use function is_string; use function ltrim; @@ -96,6 +98,9 @@ class FunctionDocblockManipulator /** @var bool */ private $is_pure = false; + /** @var list */ + private $throwsExceptions = []; + /** * @param Closure|Function_|ClassMethod|ArrowFunction $stmt */ @@ -395,6 +400,21 @@ private function getDocblock(): string $modified_docblock = true; $parsed_docblock->tags['psalm-pure'] = ['']; } + if (count($this->throwsExceptions) > 0) { + $modified_docblock = true; + $inferredThrowsClause = array_reduce( + $this->throwsExceptions, + function (string $throwsClause, string $exception) { + return $throwsClause === '' ? $exception : $throwsClause.'|'.$exception; + }, + '' + ); + if (array_key_exists('throws', $parsed_docblock->tags)) { + $parsed_docblock->tags['throws'][] = $inferredThrowsClause; + } else { + $parsed_docblock->tags['throws'] = [$inferredThrowsClause]; + } + } if ($this->new_phpdoc_return_type && $this->new_phpdoc_return_type !== $old_phpdoc_return_type) { @@ -528,6 +548,14 @@ public function makePure(): void $this->is_pure = true; } + /** + * @param list $exceptions + */ + public function addThrowsDocblock(array $exceptions): void + { + $this->throwsExceptions = $exceptions; + } + public static function clearCache(): void { self::$manipulators = []; diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php index b4336ec3a06..2c1dde57c95 100644 --- a/src/Psalm/Internal/MethodIdentifier.php +++ b/src/Psalm/Internal/MethodIdentifier.php @@ -6,7 +6,7 @@ use function explode; use function is_string; -use function preg_replace; +use function ltrim; use function strpos; use function strtolower; @@ -58,8 +58,8 @@ public static function fromMethodIdReference(string $method_id): self if (!static::isValidMethodIdReference($method_id)) { throw new InvalidArgumentException('Invalid method id reference provided: ' . $method_id); } - // remove trailing backslash if it exists - $method_id = preg_replace('/^\\\\/', '', $method_id); + // remove leading backslash if it exists + $method_id = ltrim($method_id, '\\'); $method_id_parts = explode('::', $method_id); return new self($method_id_parts[0], strtolower($method_id_parts[1])); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 770e1298779..f84bde3f889 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -506,7 +506,7 @@ protected static function addMagicPropertyToInfo( ) { $line_parts[1] = str_replace('&', '', $line_parts[1]); - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $end = $offset + strlen($line_parts[0]); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 2a99fff148d..fb7808c2c6f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1863,8 +1863,8 @@ private static function getTypeAliasesFromCommentLines( $type_string = str_replace("\n", '', implode('', $var_line_parts)); - $type_string = preg_replace('/>[^>^\}]*$/', '>', $type_string); - $type_string = preg_replace('/\}[^>^\}]*$/', '}', $type_string); + $type_string = preg_replace('/>[^>^\}]*$/', '>', $type_string, 1); + $type_string = preg_replace('/\}[^>^\}]*$/', '}', $type_string, 1); try { $type_tokens = TypeTokenizer::getFullyQualifiedTokens( diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 42af2598ecc..98b4146d945 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -260,8 +260,6 @@ private static function registerClassMapFunctionCall( $second_arg_value = substr($second_arg_value, 1); } - $second_arg_value = strtolower($second_arg_value); - $codebase->classlikes->addClassAlias( $first_arg_value, $second_arg_value diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 4de223c0ed7..2626f7742d5 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -79,7 +79,7 @@ public static function parse( ) { $line_parts[1] = str_replace('&', '', $line_parts[1]); - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $end = $offset + strlen($line_parts[0]); @@ -155,7 +155,7 @@ public static function parse( throw new IncorrectDocblockException('Misplaced variable'); } - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $info->params_out[] = [ 'name' => trim($line_parts[1]), @@ -343,7 +343,7 @@ public static function parse( throw new IncorrectDocblockException('Misplaced variable'); } - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $info->globals[] = [ 'name' => $line_parts[1], diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 1b153d46ed3..0c04fe9262d 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -615,6 +615,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $doc_comment = $param->getDocComment(); $var_comment_type = null; $var_comment_readonly = false; + $var_comment_allow_private_mutation = false; if ($doc_comment) { $var_comments = CommentAnalyzer::getTypeFromComment( $doc_comment, @@ -629,6 +630,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal if ($var_comment !== null) { $var_comment_type = $var_comment->type; $var_comment_readonly = $var_comment->readonly; + $var_comment_allow_private_mutation = $var_comment->allow_private_mutation; } } @@ -661,6 +663,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $property_storage->has_default = (bool)$param->default; $param_type_readonly = (bool)($param->flags & PhpParser\Node\Stmt\Class_::MODIFIER_READONLY); $property_storage->readonly = $param_type_readonly ?: $var_comment_readonly; + $property_storage->allow_private_mutation = $var_comment_allow_private_mutation; $param_storage->promoted_property = true; $property_storage->is_promoted = true; diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index a445de36b81..577ab40e86e 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -3,13 +3,14 @@ namespace Psalm\Internal\Provider; use Psalm\Config; +use Psalm\Internal\Provider\Providers; use Psalm\Storage\ClassLikeStorage; +use RuntimeException; use UnexpectedValueException; use function array_merge; use function dirname; use function file_exists; -use function file_get_contents; use function file_put_contents; use function filemtime; use function get_class; @@ -25,6 +26,7 @@ use function unserialize; use const DIRECTORY_SEPARATOR; +use const LOCK_EX; use const PHP_VERSION_ID; /** @@ -86,9 +88,9 @@ public function writeToCache(ClassLikeStorage $storage, string $file_path, strin $cache_location = $this->getCacheLocationForClass($fq_classlike_name_lc, $file_path, true); if ($this->config->use_igbinary) { - file_put_contents($cache_location, igbinary_serialize($storage)); + file_put_contents($cache_location, igbinary_serialize($storage), LOCK_EX); } else { - file_put_contents($cache_location, serialize($storage)); + file_put_contents($cache_location, serialize($storage), LOCK_EX); } } @@ -132,7 +134,7 @@ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path) if (file_exists($cache_location)) { if ($this->config->use_igbinary) { - $storage = igbinary_unserialize((string)file_get_contents($cache_location)); + $storage = igbinary_unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof ClassLikeStorage) { return $storage; @@ -141,7 +143,7 @@ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path) return null; } - $storage = unserialize((string)file_get_contents($cache_location)); + $storage = unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof ClassLikeStorage) { return $storage; @@ -167,7 +169,21 @@ private function getCacheLocationForClass( $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::CLASS_CACHE_DIRECTORY; if ($create_directory && !is_dir($parser_cache_directory)) { - mkdir($parser_cache_directory, 0777, true); + try { + if (mkdir($parser_cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException( + 'Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons' + ); + } + } catch (RuntimeException $e) { + // Race condition (#4483) + if (!is_dir($parser_cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } + } } $data = $file_path ? strtolower($file_path) . ' ' : ''; diff --git a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php index 7a0b7ad53f1..fdab4ac0e9a 100644 --- a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php @@ -4,20 +4,23 @@ use Psalm\Config; use Psalm\Internal\Codebase\Analyzer; +use Psalm\Internal\Provider\Providers; +use RuntimeException; use UnexpectedValueException; use function file_exists; -use function file_get_contents; use function file_put_contents; use function igbinary_serialize; use function igbinary_unserialize; use function is_array; +use function is_dir; use function is_readable; use function mkdir; use function serialize; use function unserialize; use const DIRECTORY_SEPARATOR; +use const LOCK_EX; /** * @psalm-import-type FileMapType from Analyzer @@ -86,9 +89,9 @@ public function getCachedFileReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -116,9 +119,9 @@ public function getCachedClassLikeFiles(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -146,9 +149,9 @@ public function getCachedNonMethodClassReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -176,9 +179,9 @@ public function getCachedMethodClassReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -205,7 +208,7 @@ public function getCachedMethodMemberReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -237,7 +240,7 @@ public function getCachedMethodDependencies(): ?array return null; } - $method_dependencies_cache = (string) file_get_contents($method_dependencies_cache_location); + $method_dependencies_cache = Providers::safeFileGetContents($method_dependencies_cache_location); if ($this->config->use_igbinary) { $method_dependencies_cache = igbinary_unserialize($method_dependencies_cache); } else { @@ -268,7 +271,7 @@ public function getCachedMethodPropertyReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -299,7 +302,7 @@ public function getCachedMethodMethodReturnReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -330,7 +333,7 @@ public function getCachedMethodMissingMemberReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -361,7 +364,7 @@ public function getCachedFileMemberReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -394,7 +397,7 @@ public function getCachedFilePropertyReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -427,7 +430,7 @@ public function getCachedFileMethodReturnReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -459,7 +462,7 @@ public function getCachedFileMissingMemberReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -491,9 +494,9 @@ public function getCachedMixedMemberNameReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -521,9 +524,9 @@ public function getCachedMethodParamUses(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -551,9 +554,9 @@ public function getCachedIssues(): ?array } if ($this->config->use_igbinary) { - $issues_cache = igbinary_unserialize((string) file_get_contents($issues_cache_location)); + $issues_cache = igbinary_unserialize(Providers::safeFileGetContents($issues_cache_location)); } else { - $issues_cache = unserialize((string) file_get_contents($issues_cache_location)); + $issues_cache = unserialize(Providers::safeFileGetContents($issues_cache_location)); } if (!is_array($issues_cache)) { @@ -574,9 +577,9 @@ public function setCachedFileReferences(array $file_references): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::REFERENCE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($file_references)); + file_put_contents($reference_cache_location, igbinary_serialize($file_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($file_references)); + file_put_contents($reference_cache_location, serialize($file_references), LOCK_EX); } } @@ -591,9 +594,9 @@ public function setCachedClassLikeFiles(array $file_references): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASSLIKE_FILE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($file_references)); + file_put_contents($reference_cache_location, igbinary_serialize($file_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($file_references)); + file_put_contents($reference_cache_location, serialize($file_references), LOCK_EX); } } @@ -608,9 +611,9 @@ public function setCachedNonMethodClassReferences(array $file_class_references): $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::NONMETHOD_CLASS_REFERENCE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($file_class_references)); + file_put_contents($reference_cache_location, igbinary_serialize($file_class_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($file_class_references)); + file_put_contents($reference_cache_location, serialize($file_class_references), LOCK_EX); } } @@ -625,9 +628,9 @@ public function setCachedMethodClassReferences(array $method_class_references): $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_CLASS_REFERENCE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($method_class_references)); + file_put_contents($reference_cache_location, igbinary_serialize($method_class_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($method_class_references)); + file_put_contents($reference_cache_location, serialize($method_class_references), LOCK_EX); } } @@ -642,9 +645,9 @@ public function setCachedMethodMemberReferences(array $member_references): void $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_METHOD_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -659,9 +662,9 @@ public function setCachedMethodDependencies(array $member_references): void $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_DEPENDENCIES_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -676,9 +679,9 @@ public function setCachedMethodPropertyReferences(array $property_references): v $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_PROPERTY_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($property_references)); + file_put_contents($member_cache_location, igbinary_serialize($property_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($property_references)); + file_put_contents($member_cache_location, serialize($property_references), LOCK_EX); } } @@ -693,9 +696,9 @@ public function setCachedMethodMethodReturnReferences(array $method_return_refer $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_METHOD_RETURN_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($method_return_references)); + file_put_contents($member_cache_location, igbinary_serialize($method_return_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($method_return_references)); + file_put_contents($member_cache_location, serialize($method_return_references), LOCK_EX); } } @@ -710,9 +713,9 @@ public function setCachedMethodMissingMemberReferences(array $member_references) $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_MISSING_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -727,9 +730,9 @@ public function setCachedFileMemberReferences(array $member_references): void $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_CLASS_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -744,9 +747,9 @@ public function setCachedFilePropertyReferences(array $property_references): voi $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_CLASS_PROPERTY_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($property_references)); + file_put_contents($member_cache_location, igbinary_serialize($property_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($property_references)); + file_put_contents($member_cache_location, serialize($property_references), LOCK_EX); } } @@ -761,9 +764,9 @@ public function setCachedFileMethodReturnReferences(array $method_return_referen $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_METHOD_RETURN_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($method_return_references)); + file_put_contents($member_cache_location, igbinary_serialize($method_return_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($method_return_references)); + file_put_contents($member_cache_location, serialize($method_return_references), LOCK_EX); } } @@ -778,9 +781,9 @@ public function setCachedFileMissingMemberReferences(array $member_references): $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_MISSING_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -795,9 +798,9 @@ public function setCachedMixedMemberNameReferences(array $references): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::UNKNOWN_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($references)); + file_put_contents($reference_cache_location, igbinary_serialize($references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($references)); + file_put_contents($reference_cache_location, serialize($references), LOCK_EX); } } @@ -812,9 +815,9 @@ public function setCachedMethodParamUses(array $uses): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_PARAM_USE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($uses)); + file_put_contents($reference_cache_location, igbinary_serialize($uses), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($uses)); + file_put_contents($reference_cache_location, serialize($uses), LOCK_EX); } } @@ -829,9 +832,9 @@ public function setCachedIssues(array $issues): void $issues_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::ISSUES_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($issues_cache_location, igbinary_serialize($issues)); + file_put_contents($issues_cache_location, igbinary_serialize($issues), LOCK_EX); } else { - file_put_contents($issues_cache_location, serialize($issues)); + file_put_contents($issues_cache_location, serialize($issues), LOCK_EX); } } @@ -849,10 +852,10 @@ public function getAnalyzedMethodCache() ) { if ($this->config->use_igbinary) { /** @var array> */ - return igbinary_unserialize(file_get_contents($analyzed_methods_cache_location)); + return igbinary_unserialize(Providers::safeFileGetContents($analyzed_methods_cache_location)); } else { /** @var array> */ - return unserialize(file_get_contents($analyzed_methods_cache_location)); + return unserialize(Providers::safeFileGetContents($analyzed_methods_cache_location)); } } @@ -872,9 +875,9 @@ public function setAnalyzedMethodCache(array $analyzed_methods): void . self::ANALYZED_METHODS_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($analyzed_methods_cache_location, igbinary_serialize($analyzed_methods)); + file_put_contents($analyzed_methods_cache_location, igbinary_serialize($analyzed_methods), LOCK_EX); } else { - file_put_contents($analyzed_methods_cache_location, serialize($analyzed_methods)); + file_put_contents($analyzed_methods_cache_location, serialize($analyzed_methods), LOCK_EX); } } } @@ -895,12 +898,12 @@ public function getFileMapCache() /** * @var array */ - $file_maps_cache = igbinary_unserialize(file_get_contents($file_maps_cache_location)); + $file_maps_cache = igbinary_unserialize(Providers::safeFileGetContents($file_maps_cache_location)); } else { /** * @var array */ - $file_maps_cache = unserialize(file_get_contents($file_maps_cache_location)); + $file_maps_cache = unserialize(Providers::safeFileGetContents($file_maps_cache_location)); } return $file_maps_cache; @@ -920,9 +923,9 @@ public function setFileMapCache(array $file_maps): void $file_maps_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_MAPS_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($file_maps_cache_location, igbinary_serialize($file_maps)); + file_put_contents($file_maps_cache_location, igbinary_serialize($file_maps), LOCK_EX); } else { - file_put_contents($file_maps_cache_location, serialize($file_maps)); + file_put_contents($file_maps_cache_location, serialize($file_maps), LOCK_EX); } } } @@ -941,10 +944,14 @@ public function getTypeCoverage() ) { if ($this->config->use_igbinary) { /** @var array */ - $type_coverage_cache = igbinary_unserialize(file_get_contents($type_coverage_cache_location)); + $type_coverage_cache = igbinary_unserialize( + Providers::safeFileGetContents($type_coverage_cache_location) + ); } else { /** @var array */ - $type_coverage_cache = unserialize(file_get_contents($type_coverage_cache_location)); + $type_coverage_cache = unserialize( + Providers::safeFileGetContents($type_coverage_cache_location) + ); } return $type_coverage_cache; @@ -964,9 +971,9 @@ public function setTypeCoverage(array $mixed_counts): void $type_coverage_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::TYPE_COVERAGE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($type_coverage_cache_location, igbinary_serialize($mixed_counts)); + file_put_contents($type_coverage_cache_location, igbinary_serialize($mixed_counts), LOCK_EX); } else { - file_put_contents($type_coverage_cache_location, serialize($mixed_counts)); + file_put_contents($type_coverage_cache_location, serialize($mixed_counts), LOCK_EX); } } } @@ -983,7 +990,7 @@ public function getConfigHashCache() if ($cache_directory && file_exists($config_hash_cache_location) ) { - return file_get_contents($config_hash_cache_location); + return Providers::safeFileGetContents($config_hash_cache_location); } return false; @@ -993,17 +1000,34 @@ public function setConfigHashCache(string $hash): void { $cache_directory = Config::getInstance()->getCacheDirectory(); - if ($cache_directory) { - if (!file_exists($cache_directory)) { - mkdir($cache_directory, 0777, true); + if (!$cache_directory) { + return; + } + + if (!is_dir($cache_directory)) { + try { + if (mkdir($cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException( + 'Failed to create ' . $cache_directory . ' cache directory for unknown reasons' + ); + } + } catch (RuntimeException $e) { + // Race condition (#4483) + if (!is_dir($cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } } + } - $config_hash_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CONFIG_HASH_CACHE_NAME; + $config_hash_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CONFIG_HASH_CACHE_NAME; - file_put_contents( - $config_hash_cache_location, - $hash - ); - } + file_put_contents( + $config_hash_cache_location, + $hash, + LOCK_EX + ); } } diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index edb6a7fe19b..8db64af0364 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -3,13 +3,14 @@ namespace Psalm\Internal\Provider; use Psalm\Config; +use Psalm\Internal\Provider\Providers; use Psalm\Storage\FileStorage; +use RuntimeException; use UnexpectedValueException; use function array_merge; use function dirname; use function file_exists; -use function file_get_contents; use function file_put_contents; use function filemtime; use function get_class; @@ -24,6 +25,7 @@ use function unserialize; use const DIRECTORY_SEPARATOR; +use const LOCK_EX; use const PHP_VERSION_ID; /** @@ -79,9 +81,9 @@ public function writeToCache(FileStorage $storage, string $file_contents): void $storage->hash = $this->getCacheHash($file_path, $file_contents); if ($this->config->use_igbinary) { - file_put_contents($cache_location, igbinary_serialize($storage)); + file_put_contents($cache_location, igbinary_serialize($storage), LOCK_EX); } else { - file_put_contents($cache_location, serialize($storage)); + file_put_contents($cache_location, serialize($storage), LOCK_EX); } } @@ -135,7 +137,7 @@ private function loadFromCache(string $file_path): ?FileStorage if (file_exists($cache_location)) { if ($this->config->use_igbinary) { - $storage = igbinary_unserialize((string)file_get_contents($cache_location)); + $storage = igbinary_unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof FileStorage) { return $storage; @@ -144,7 +146,7 @@ private function loadFromCache(string $file_path): ?FileStorage return null; } - $storage = unserialize((string)file_get_contents($cache_location)); + $storage = unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof FileStorage) { return $storage; @@ -167,7 +169,21 @@ private function getCacheLocationForPath(string $file_path, bool $create_directo $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_STORAGE_CACHE_DIRECTORY; if ($create_directory && !is_dir($parser_cache_directory)) { - mkdir($parser_cache_directory, 0777, true); + try { + if (mkdir($parser_cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException( + 'Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons' + ); + } + } catch (RuntimeException $e) { + // Race condition (#4483) + if (!is_dir($parser_cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } + } } if (PHP_VERSION_ID >= 8_01_00) { diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php index 197a4564f57..4af245e5127 100644 --- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Provider\ReturnTypeProvider\ClosureFromCallableReturnTypeProvider; +use Psalm\Internal\Provider\ReturnTypeProvider\DateTimeModifyReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\DomNodeAppendChild; use Psalm\Internal\Provider\ReturnTypeProvider\ImagickPixelColorReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\PdoStatementReturnTypeProvider; @@ -41,6 +42,7 @@ public function __construct() $this->registerClass(SimpleXmlElementAsXml::class); $this->registerClass(PdoStatementReturnTypeProvider::class); $this->registerClass(ClosureFromCallableReturnTypeProvider::class); + $this->registerClass(DateTimeModifyReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index 1918279554e..559a11ecf63 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -6,13 +6,17 @@ use PhpParser; use PhpParser\Node\Stmt; use Psalm\Config; +use Psalm\Internal\Provider\Providers; use RuntimeException; +use UnexpectedValueException; +use function clearstatcache; use function error_log; use function file_get_contents; use function file_put_contents; use function filemtime; use function gettype; +use function hash; use function igbinary_serialize; use function igbinary_unserialize; use function is_array; @@ -21,18 +25,17 @@ use function is_writable; use function json_decode; use function json_encode; -use function md5; use function mkdir; use function scandir; use function serialize; use function touch; -use function trigger_error; use function unlink; use function unserialize; use const DIRECTORY_SEPARATOR; -use const E_USER_ERROR; use const JSON_THROW_ON_ERROR; +use const LOCK_EX; +use const PHP_VERSION_ID; use const SCANDIR_SORT_NONE; /** @@ -44,31 +47,33 @@ class ParserCacheProvider private const PARSER_CACHE_DIRECTORY = 'php-parser'; private const FILE_CONTENTS_CACHE_DIRECTORY = 'file-caches'; + /** + * @var Config + */ + private $config; + /** * A map of filename hashes to contents hashes * * @var array|null */ - private $existing_file_content_hashes; + protected $existing_file_content_hashes; /** * A map of recently-added filename hashes to contents hashes * * @var array */ - private $new_file_content_hashes = []; + protected $new_file_content_hashes = []; /** * @var bool */ private $use_file_cache; - /** @var bool */ - private $use_igbinary; - public function __construct(Config $config, bool $use_file_cache = true) { - $this->use_igbinary = $config->use_igbinary; + $this->config = $config; $this->use_file_cache = $use_file_cache; } @@ -80,33 +85,27 @@ public function loadStatementsFromCache( int $file_modified_time, string $file_content_hash ): ?array { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { + if (!$this->use_file_cache) { return null; } - $file_cache_key = $this->getParserCacheKey( - $file_path - ); + $cache_location = $this->getCacheLocationForPath($file_path, self::PARSER_CACHE_DIRECTORY); - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY; + $file_cache_key = $this->getParserCacheKey($file_path); $file_content_hashes = $this->new_file_content_hashes + $this->getExistingFileContentHashes(); - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; - if (isset($file_content_hashes[$file_cache_key]) && $file_content_hash === $file_content_hashes[$file_cache_key] && is_readable($cache_location) && filemtime($cache_location) > $file_modified_time ) { - if ($this->use_igbinary) { + if ($this->config->use_igbinary) { /** @var list */ - $stmts = igbinary_unserialize((string)file_get_contents($cache_location)); + $stmts = igbinary_unserialize(Providers::safeFileGetContents($cache_location)); } else { /** @var list */ - $stmts = unserialize((string)file_get_contents($cache_location)); + $stmts = unserialize(Providers::safeFileGetContents($cache_location)); } return $stmts; @@ -120,28 +119,20 @@ public function loadStatementsFromCache( */ public function loadExistingStatementsFromCache(string $file_path): ?array { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { + if (!$this->use_file_cache) { return null; } - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; + $cache_location = $this->getCacheLocationForPath($file_path, self::PARSER_CACHE_DIRECTORY); if (is_readable($cache_location)) { - if ($this->use_igbinary) { + if ($this->config->use_igbinary) { /** @var list */ - return igbinary_unserialize((string)file_get_contents($cache_location)) ?: null; + return igbinary_unserialize(Providers::safeFileGetContents($cache_location)) ?: null; } /** @var list */ - return unserialize((string)file_get_contents($cache_location)) ?: null; + return unserialize(Providers::safeFileGetContents($cache_location)) ?: null; } return null; @@ -153,22 +144,10 @@ public function loadExistingFileContentsFromCache(string $file_path): ?string return null; } - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { - return null; - } - - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_CONTENTS_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; + $cache_location = $this->getCacheLocationForPath($file_path, self::FILE_CONTENTS_CACHE_DIRECTORY); if (is_readable($cache_location)) { - return file_get_contents($cache_location); + return Providers::safeFileGetContents($cache_location); } return null; @@ -179,13 +158,18 @@ public function loadExistingFileContentsFromCache(string $file_path): ?string */ private function getExistingFileContentHashes(): array { - $config = Config::getInstance(); - $root_cache_directory = $config->getCacheDirectory(); + if (!$this->use_file_cache) { + return []; + } if ($this->existing_file_content_hashes === null) { + $root_cache_directory = $this->config->getCacheDirectory(); $file_hashes_path = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES; - if ($root_cache_directory && is_readable($file_hashes_path)) { + if (!$root_cache_directory) { + throw new UnexpectedValueException('No cache directory defined'); + } + if (is_readable($file_hashes_path)) { $hashes_encoded = (string) file_get_contents($file_hashes_path); if (!$hashes_encoded) { @@ -216,6 +200,29 @@ private function getExistingFileContentHashes(): array } else { $this->existing_file_content_hashes = []; } + + if (!is_readable($file_hashes_path)) { + // might not exist yet + $this->existing_file_content_hashes = []; + return $this->existing_file_content_hashes; + } + + $hashes_encoded = Providers::safeFileGetContents($file_hashes_path); + if (!$hashes_encoded) { + throw new UnexpectedValueException('File content hashes should be in cache'); + } + + /** @psalm-suppress MixedAssignment */ + $hashes_decoded = json_decode($hashes_encoded, true); + + if (!is_array($hashes_decoded)) { + throw new UnexpectedValueException( + 'File content hashes are of invalid type ' . gettype($hashes_decoded) + ); + } + + /** @var array $hashes_decoded */ + $this->existing_file_content_hashes = $hashes_decoded; } return $this->existing_file_content_hashes; @@ -230,31 +237,18 @@ public function saveStatementsToCache( array $stmts, bool $touch_only ): void { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { - return; - } - - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; + $cache_location = $this->getCacheLocationForPath($file_path, self::PARSER_CACHE_DIRECTORY, !$touch_only); if ($touch_only) { touch($cache_location); } else { - $this->createCacheDirectory($parser_cache_directory); - - if ($this->use_igbinary) { - file_put_contents($cache_location, igbinary_serialize($stmts)); + if ($this->config->use_igbinary) { + file_put_contents($cache_location, igbinary_serialize($stmts), LOCK_EX); } else { - file_put_contents($cache_location, serialize($stmts)); + file_put_contents($cache_location, serialize($stmts), LOCK_EX); } + $file_cache_key = $this->getParserCacheKey($file_path); $this->new_file_content_hashes[$file_cache_key] = $file_content_hash; } } @@ -278,19 +272,32 @@ public function addNewFileContentHashes(array $file_content_hashes): void public function saveFileContentHashes(): void { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); + if (!$this->use_file_cache) { + return; + } + + $root_cache_directory = $this->config->getCacheDirectory(); if (!$root_cache_directory) { return; } + // directory was removed most likely due to a race condition + // with other psalm instances that were manually started at + // the same time + clearstatcache(true, $root_cache_directory); + if (!is_dir($root_cache_directory)) { + return; + } + $file_content_hashes = $this->new_file_content_hashes + $this->getExistingFileContentHashes(); $file_hashes_path = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES; file_put_contents( $file_hashes_path, - json_encode($file_content_hashes, JSON_THROW_ON_ERROR) + json_encode($file_content_hashes, JSON_THROW_ON_ERROR), + LOCK_EX ); } @@ -300,28 +307,17 @@ public function cacheFileContents(string $file_path, string $file_contents): voi return; } - $root_cache_directory = Config::getInstance()->getCacheDirectory(); + $cache_location = $this->getCacheLocationForPath($file_path, self::FILE_CONTENTS_CACHE_DIRECTORY, true); - if (!$root_cache_directory) { - return; - } - - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_CONTENTS_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; - - $this->createCacheDirectory($parser_cache_directory); - - file_put_contents($cache_location, $file_contents); + file_put_contents($cache_location, $file_contents, LOCK_EX); } public function deleteOldParserCaches(float $time_before): int { - $cache_directory = Config::getInstance()->getCacheDirectory(); + $cache_directory = $this->config->getCacheDirectory(); + + $this->existing_file_content_hashes = null; + $this->new_file_content_hashes = []; if (!$cache_directory) { return 0; @@ -351,22 +347,51 @@ public function deleteOldParserCaches(float $time_before): int return $removed_count; } - private function getParserCacheKey(string $file_name): string + private function getParserCacheKey(string $file_path): string { - return md5($file_name) . ($this->use_igbinary ? '-igbinary' : '') . '-r'; + if (PHP_VERSION_ID >= 8_01_00) { + $hash = hash('xxh128', $file_path); + } else { + $hash = hash('md4', $file_path); + } + + return $hash . ($this->config->use_igbinary ? '-igbinary' : '') . '-r'; } - private function createCacheDirectory(string $parser_cache_directory): void - { - if (!is_dir($parser_cache_directory)) { + + private function getCacheLocationForPath( + string $file_path, + string $subdirectory, + bool $create_directory = false + ): string { + $root_cache_directory = $this->config->getCacheDirectory(); + + if (!$root_cache_directory) { + throw new UnexpectedValueException('No cache directory defined'); + } + + $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . $subdirectory; + + if ($create_directory && !is_dir($parser_cache_directory)) { try { - mkdir($parser_cache_directory, 0777, true); + if (mkdir($parser_cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException( + 'Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons' + ); + } } catch (RuntimeException $e) { // Race condition (#4483) if (!is_dir($parser_cache_directory)) { - trigger_error('Could not create parser cache directory: ' . $parser_cache_directory, E_USER_ERROR); + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; } } } + + return $parser_cache_directory + . DIRECTORY_SEPARATOR + . $this->getParserCacheKey($file_path); } } diff --git a/src/Psalm/Internal/Provider/Providers.php b/src/Psalm/Internal/Provider/Providers.php index d42e1004342..864dd92a17f 100644 --- a/src/Psalm/Internal/Provider/Providers.php +++ b/src/Psalm/Internal/Provider/Providers.php @@ -2,6 +2,17 @@ namespace Psalm\Internal\Provider; +use RuntimeException; + +use function fclose; +use function filesize; +use function flock; +use function fopen; +use function fread; +use function usleep; + +use const LOCK_SH; + /** * @internal */ @@ -63,4 +74,38 @@ public function __construct( ); $this->file_reference_provider = new FileReferenceProvider($file_reference_cache_provider); } + + public static function safeFileGetContents(string $path): string + { + // no readable validation as that must be done in the caller + $fp = fopen($path, 'r'); + if ($fp === false) { + return ''; + } + $max_wait_cycles = 5; + $has_lock = false; + while ($max_wait_cycles > 0) { + if (flock($fp, LOCK_SH)) { + $has_lock = true; + break; + } + $max_wait_cycles--; + usleep(50_000); + } + + if (!$has_lock) { + fclose($fp); + throw new RuntimeException('Could not acquire lock for ' . $path); + } + + $file_size = filesize($path); + $content = ''; + if ($file_size > 0) { + $content = (string) fread($fp, $file_size); + } + + fclose($fp); + + return $content; + } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index 1864eb706f4..37151d19692 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -39,7 +39,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } - $row_shape = null; + $row_type = $row_shape = null; $input_array_not_empty = false; // calculate row shape @@ -48,7 +48,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && $first_arg_type->hasArray() ) { $input_array = $first_arg_type->getAtomicTypes()['array']; - $row_type = null; if ($input_array instanceof TKeyedArray) { $row_type = $input_array->getGenericArrayType()->type_params[1]; } elseif ($input_array instanceof TArray) { @@ -107,7 +106,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } $result_key_type = Type::getArrayKey(); - $result_element_type = null; + $result_element_type = null !== $row_type && $value_column_name_is_null ? $row_type : null; $have_at_least_one_res = false; // calculate results if ($row_shape instanceof TKeyedArray) { @@ -119,9 +118,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } //array_column skips undefined elements so resulting type is necessarily defined $result_element_type->possibly_undefined = false; - } elseif ($value_column_name_is_null) { - $result_element_type = new Union([$row_shape]); - } else { + } elseif (!$value_column_name_is_null) { $result_element_type = Type::getMixed(); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php index c8958f982b3..86601f80bac 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php @@ -103,7 +103,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $initial_type = $reduce_return_type; - if ($closure_types = $function_call_arg_type->getClosureTypes()) { + $closure_types = $function_call_arg_type->getClosureTypes() ?: $function_call_arg_type->getCallableTypes(); + + if ($closure_types) { $closure_atomic_type = reset($closure_types); $closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php new file mode 100644 index 00000000000..1b71f1a27db --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php @@ -0,0 +1,64 @@ +getSource(); + $call_args = $event->getCallArgs(); + $method_name_lowercase = $event->getMethodNameLowercase(); + if (!$statements_source instanceof StatementsAnalyzer + || $method_name_lowercase !== 'modify' + || !isset($call_args[0]) + ) { + return null; + } + + $first_arg = $call_args[0]->value; + $first_arg_type = $statements_source->node_data->getType($first_arg); + if (!$first_arg_type) { + return null; + } + + $has_date_time = false; + $has_false = false; + foreach ($first_arg_type->getAtomicTypes() as $type_part) { + if (!$type_part instanceof TLiteralString) { + return null; + } + + if (@(new DateTime())->modify($type_part->value) === false) { + $has_false = true; + } else { + $has_date_time = true; + } + } + + if ($has_false && !$has_date_time) { + return Type::getFalse(); + } + if ($has_date_time && !$has_false) { + return Type::parseString($event->getFqClasslikeName()); + } + + return null; + } +} diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php index 805c51770d9..3f9643448ea 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php @@ -9,6 +9,7 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TArray; +use Psalm\Type\Atomic\TDependentListKey; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TKeyedArray; @@ -95,6 +96,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } elseif (get_class($atomic_type) === TInt::class) { $min_bounds[] = null; $max_bounds[] = null; + } elseif ($atomic_type instanceof TDependentListKey) { + $min_bounds[] = 0; + $max_bounds[] = null; } else { throw new UnexpectedValueException('Unexpected type'); } diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index c4e00f748cb..6094a952726 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -27,10 +27,13 @@ use function array_merge; use function count; use function filemtime; +use function hash; use function md5; use function strlen; use function strpos; +use const PHP_VERSION_ID; + /** * @internal */ @@ -136,7 +139,11 @@ public function getStatementsForFile( $config = Config::getInstance(); - $file_content_hash = md5($version . $file_contents); + if (PHP_VERSION_ID >= 8_01_00) { + $file_content_hash = hash('xxh128', $version . $file_contents); + } else { + $file_content_hash = hash('md4', $version . $file_contents); + } if (!$this->parser_cache_provider || (!$config->isInProjectDirs($file_path) && strpos($file_path, 'vendor')) @@ -228,6 +235,7 @@ public function getStatementsForFile( $unchanged_members = array_fill_keys($unchanged_members, true); $unchanged_signature_members = array_fill_keys($unchanged_signature_members, true); + // do NOT change this to hash, it will fail on Windows for whatever reason $file_path_hash = md5($file_path); $changed_members = array_map( diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index 2322a3b8510..6015262e270 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -113,7 +113,7 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock // Strip the leading *, if present. $text = $lines[$k]; $text = str_replace("\t", ' ', $text); - $text = preg_replace('/^ *\*/', '', $text); + $text = preg_replace('/^ *\*/', '', $text, 1); $lines[$k] = $text; } @@ -144,7 +144,7 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock // Trim any empty lines off the front, but leave the indent level if there // is one. - $docblock = preg_replace('/^\s*\n/', '', $docblock); + $docblock = preg_replace('/^\s*\n/', '', $docblock, 1); $parsed = new ParsedDocblock($docblock, $special, $first_line_padding ?: ''); diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 3778bbb5a45..5782e8bdb4e 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -111,7 +111,7 @@ public static function reconcile( && is_string($key) && VariableFetchAnalyzer::isSuperGlobal($key) ) { - $existing_var_type = VariableFetchAnalyzer::getGlobalType($key); + $existing_var_type = VariableFetchAnalyzer::getGlobalType($key, $codebase->analysis_php_version_id); } if ($existing_var_type === null) { diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 5b3341d5570..72d56358e2a 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1165,7 +1165,7 @@ public static function getMappedGenericTypeParams( ): array { if ($input_type_part instanceof TGenericObject || $input_type_part instanceof TIterable) { $input_type_params = $input_type_part->type_params; - } else { + } elseif ($codebase->classlike_storage_provider->has($input_type_part->value)) { $class_storage = $codebase->classlike_storage_provider->get($input_type_part->value); $container_class = $container_type_part->value; @@ -1177,6 +1177,8 @@ public static function getMappedGenericTypeParams( } else { $input_type_params = array_fill(0, count($class_storage->template_types ?? []), Type::getMixed()); } + } else { + $input_type_params = []; } try { diff --git a/src/Psalm/Issue/RiskyCast.php b/src/Psalm/Issue/RiskyCast.php new file mode 100644 index 00000000000..6576d430aeb --- /dev/null +++ b/src/Psalm/Issue/RiskyCast.php @@ -0,0 +1,9 @@ +format); } return $output->create(); diff --git a/src/Psalm/Report.php b/src/Psalm/Report.php index e1956d1991e..a56469af089 100644 --- a/src/Psalm/Report.php +++ b/src/Psalm/Report.php @@ -28,24 +28,7 @@ abstract class Report public const TYPE_PHP_STORM = 'phpstorm'; public const TYPE_SARIF = 'sarif'; public const TYPE_CODECLIMATE = 'codeclimate'; - - public const SUPPORTED_OUTPUT_TYPES = [ - self::TYPE_COMPACT, - self::TYPE_CONSOLE, - self::TYPE_PYLINT, - self::TYPE_JSON, - self::TYPE_JSON_SUMMARY, - self::TYPE_SONARQUBE, - self::TYPE_EMACS, - self::TYPE_XML, - self::TYPE_JUNIT, - self::TYPE_CHECKSTYLE, - self::TYPE_TEXT, - self::TYPE_GITHUB_ACTIONS, - self::TYPE_PHP_STORM, - self::TYPE_SARIF, - self::TYPE_CODECLIMATE, - ]; + public const TYPE_COUNT = 'count'; /** * @var array diff --git a/src/Psalm/Report/CountReport.php b/src/Psalm/Report/CountReport.php new file mode 100644 index 00000000000..b044d851651 --- /dev/null +++ b/src/Psalm/Report/CountReport.php @@ -0,0 +1,39 @@ +issues_data as $issue_data) { + if (array_key_exists($issue_data->type, $issue_type_counts)) { + $issue_type_counts[$issue_data->type]++; + } else { + $issue_type_counts[$issue_data->type] = 1; + } + } + uksort($issue_type_counts, function (string $a, string $b) use ($issue_type_counts): int { + $cmp_result = $issue_type_counts[$a] <=> $issue_type_counts[$b]; + if ($cmp_result === 0) { + return $a <=> $b; + } else { + return $cmp_result; + } + }); + + $output = ''; + foreach ($issue_type_counts as $issue_type => $count) { + $output .= "{$issue_type}: {$count}\n"; + } + return $output; + } +} diff --git a/src/Psalm/Report/ReportOptions.php b/src/Psalm/Report/ReportOptions.php index 242caf4884a..d5032dbeeb1 100644 --- a/src/Psalm/Report/ReportOptions.php +++ b/src/Psalm/Report/ReportOptions.php @@ -22,7 +22,7 @@ final class ReportOptions public $show_info = true; /** - * @var value-of + * @var Report::TYPE_* */ public $format = Report::TYPE_CONSOLE; diff --git a/src/Psalm/Storage/FileStorage.php b/src/Psalm/Storage/FileStorage.php index aebb14d192f..fe818ff6d86 100644 --- a/src/Psalm/Storage/FileStorage.php +++ b/src/Psalm/Storage/FileStorage.php @@ -86,7 +86,7 @@ final class FileStorage public $type_aliases = []; /** - * @var array + * @var array */ public $classlike_aliases = []; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 8508caa2c15..3f82cb6f240 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -33,6 +33,7 @@ use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNonEmptyLowercaseString; use Psalm\Type\Atomic\TNonEmptyString; +use Psalm\Type\Atomic\TNonFalsyString; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; @@ -208,6 +209,13 @@ public static function getNonEmptyString(): Union return new Union([$type]); } + public static function getNonFalsyString(): Union + { + $type = new TNonFalsyString(); + + return new Union([$type]); + } + public static function getNumeric(): Union { $type = new TNumeric; diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 126ac673269..6f93e74706a 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -195,6 +195,7 @@ public static function create( case 'non-empty-string': return new TNonEmptyString(); + case 'truthy-string': case 'non-falsy-string': return new TNonFalsyString(); @@ -213,6 +214,15 @@ public static function create( case 'positive-int': return new TIntRange(1, null); + + case 'non-positive-int': + return new TIntRange(null, 0); + + case 'negative-int': + return new TIntRange(null, -1); + + case 'non-negative-int': + return new TIntRange(0, null); case 'numeric': return $analysis_php_version_id !== null ? new TNamedObject($value) : new TNumeric(); diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 0dae5ef34d2..f988fcf26c6 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -496,8 +496,20 @@ final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traver } -#[Attribute] +#[Attribute(Attribute::TARGET_METHOD)] 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() {} +} diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 32203cd33ef..c1a589f6c1e 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -343,6 +343,22 @@ function fclose(&$stream) : bool { } +/** + * @psalm-pure + * @template T as string + * @param T $string + * @return (T is non-empty-string ? non-empty-string : string) + */ +function sodium_bin2base64(string $string, int $id): string + +/** + * @psalm-pure + * @template T as string + * @param T $string + * @return (T is non-empty-string ? non-empty-string : string) + */ +function sodium_bin2hex(string $string): string {} + /** * @param string $string * @param-out null $string @@ -559,6 +575,12 @@ function strpos($haystack, $needle, int $offset = 0) : int {} /** * @psalm-pure * + * @return ( + * $string is class-string + * ? ($characters is '\\' ? class-string : string) + * : ($string is lowercase-string ? lowercase-string : string) + * ) + * * @psalm-flow ($string) -> return */ function trim(string $string, string $characters = " \t\n\r\0\x0B") : string {} @@ -566,7 +588,11 @@ function trim(string $string, string $characters = " \t\n\r\0\x0B") : string {} /** * @psalm-pure * - * @return ($string is class-string ? ($characters is '\\' ? class-string : string) : string) + * @return ( + * $string is class-string + * ? ($characters is '\\' ? class-string : string) + * : ($string is lowercase-string ? lowercase-string : string) + * ) * * @psalm-flow ($string) -> return */ @@ -575,6 +601,8 @@ function ltrim(string $string, string $characters = " \t\n\r\0\x0B") : string {} /** * @psalm-pure * + * @return ($string is lowercase-string ? lowercase-string : string) + * * @psalm-flow ($string) -> return */ function rtrim(string $string, string $characters = " \t\n\r\0\x0B") : string {} @@ -1290,7 +1318,7 @@ function pack(string $format, mixed ...$values) {} * @param string|int $out_codepage * @psalm-flow ($subject) -> return */ -function sapi_windows_cp_conv($in_codepage, $out_codepage, string $subject) : string {} +function sapi_windows_cp_conv($in_codepage, $out_codepage, string $subject) : ?string {} /** * @psalm-pure @@ -1323,9 +1351,21 @@ function base64_decode(string $string, bool $strict = false) {} * @psalm-pure * * @psalm-flow ($string) -> return + * @template T as string + * @param T $string + * @return (T is non-empty-string ? non-empty-string : string) */ function base64_encode(string $string) : string {} +/** + * @psalm-pure + * + * @template T as string + * @param T $string + * @return (T is non-empty-string ? non-empty-string : string) + */ +function bin2hex(string $string): string {} + /** * @psalm-pure * diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index f9c8d6e3e34..91c0a41c69c 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -1,11 +1,103 @@ + */ +class DatePeriod implements Traversable +{ + const EXCLUDE_START_DATE = 1; + /** + * @param Start $start + * @param (Start is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval + * @param (Start is string ? never : DateTimeInterface|positive-int) $end + * @param (Start is string ? never : 0|self::EXCLUDE_START_DATE) $options + */ + public function __construct($start, $interval = 0, $end = 1, $options = 0) {} +} + /** * @psalm-taint-specialize */ diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 877b156f3a0..001ebf17ae6 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -82,3 +82,24 @@ class ReflectionUnionType extends ReflectionType { } class UnhandledMatchError extends Error {} + +/** + * @psalm-immutable + * + * @template-covariant Start of string|DateTimeInterface + * @implements IteratorAggregate + */ +class DatePeriod implements IteratorAggregate +{ + const EXCLUDE_START_DATE = 1; + /** + * @param Start $start + * @param (Start is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval + * @param (Start is string ? never : DateTimeInterface|positive-int) $end + * @param (Start is string ? never : 0|self::EXCLUDE_START_DATE) $options + */ + public function __construct($start, $interval = 0, $end = 1, $options = 0) {} + + /** @psalm-return (Start is string ? (Traversable&Iterator) : (Traversable&Iterator)) */ + public function getIterator(): Iterator {} +} diff --git a/stubs/phpredis.phpstub b/stubs/phpredis.phpstub index 791a0cfb87c..0ad1780d22d 100644 --- a/stubs/phpredis.phpstub +++ b/stubs/phpredis.phpstub @@ -31,8 +31,8 @@ class Redis { */ public function acl(string $subcmd, ...$args); - /** @return int|Redis */ - public function append(string $key, mixed $value); + /** @return false|int|Redis */ + public function append(string $key, string $value); public function auth(mixed $credentials): bool; @@ -40,15 +40,15 @@ class Redis { public function bgrewriteaof(): bool; - /** @return int|Redis */ + /** @return false|int|Redis */ public function bitcount(string $key, int $start = 0, int $end = -1); /** - * @return int|Redis + * @return false|int|Redis */ public function bitop(string $operation, string $deskey, string $srckey, string ...$other_keys): int; - /** @return int|Redis */ + /** @return false|int|Redis */ public function bitpos(string $key, int $bit, int $start = 0, int $end = -1); public function blPop(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|null|false; @@ -57,9 +57,9 @@ class Redis { public function brpoplpush(string $src, string $dst, int $timeout): Redis|string|false; - public function bzPopMax(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array; + public function bzPopMax(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|false; - public function bzPopMin(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array; + public function bzPopMin(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|false; public function clearLastError(): bool; @@ -79,21 +79,21 @@ class Redis { public function debug(string $key): string; - /** @return int|Redis */ + /** @return false|int|Redis */ public function decr(string $key, int $by = 1); - /** @return int|Redis */ + /** @return false|int|Redis */ public function decrBy(string $key, int $value); /** - * @return int|Redis + * @return false|int|Redis */ public function del(array|string $key, string ...$other_keys); /** * @deprecated * @alias Redis::del - * @return int|Redis + * @return false|int|Redis */ public function delete(array|string $key, string ...$other_keys); @@ -101,7 +101,7 @@ class Redis { public function dump(string $key): string; - /** @return string|Redis */ + /** @return false|string|Redis */ public function echo(string $str); public function eval(string $script, array $keys = null, int $num_keys = 0): mixed; @@ -125,7 +125,7 @@ class Redis { public function geodist(string $key, string $src, string $dst, ?string $unit = null): Redis|float|false; - public function geohash(string $key, string $member, string ...$other_members): array; + public function geohash(string $key, string $member, string ...$other_members): array|false; public function geopos(string $key, string $member, string ...$other_members): Redis|array|false; @@ -137,16 +137,16 @@ class Redis { public function georadiusbymember_ro(string $key, string $member, float $radius, string $unit, array $options = []): Redis|mixed|false; - public function geosearch(string $key, array|string $position, array|int|float $shape, string $unit, array $options = []): array; + public function geosearch(string $key, array|string $position, array|int|float $shape, string $unit, array $options = []): array|false; - public function geosearchstore(string $dst, string $src, array|string $position, array|int|float $shape, string $unit, array $options = []): array; + public function geosearchstore(string $dst, string $src, array|string $position, array|int|float $shape, string $unit, array $options = []): array|false; - /** @return string|Redis */ + /** @return false|string|Redis */ public function get(string $key); public function getAuth(): mixed; - /** @return int|Redis */ + /** @return false|int|Redis */ public function getBit(string $key, int $idx); public function getDBNum(): int; @@ -163,13 +163,13 @@ class Redis { public function getPort(): int; - /** @return string|Redis */ + /** @return false|string|Redis */ public function getRange(string $key, int $start, int $end); public function getReadTimeout(): int; - /** @return string|Redis */ - public function getset(string $key, mixed $value); + /** @return false|string|Redis */ + public function getset(string $key, string $value); public function getTimeout(): int; @@ -193,7 +193,7 @@ class Redis { public function hMset(string $key, array $keyvals): Redis|bool|false; - public function hSet(string $key, string $member, mixed $value): Redis|int|false; + public function hSet(string $key, string $member, string $value): Redis|int|false; public function hSetNx(string $key, string $member, string $value): Redis|bool; @@ -203,25 +203,25 @@ class Redis { public function hscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): bool|array; - /** @return int|Redis */ + /** @return false|int|Redis */ public function incr(string $key, int $by = 1); - /** @return int|Redis */ + /** @return false|int|Redis */ public function incrBy(string $key, int $value); - /** @return int|Redis */ + /** @return false|int|Redis */ public function incrByFloat(string $key, float $value); public function info(string $opt = null): Redis|array|false; public function isConnected(): bool; - /** @return array|Redis */ + /** @return false|array|Redis */ public function keys(string $pattern); /** * @param mixed $elements - * @return int|Redis + * @return false|int|Redis */ public function lInsert(string $key, string $pos, mixed $pivot, mixed $value); @@ -230,28 +230,28 @@ class Redis { public function lMove(string $src, string $dst, string $wherefrom, string $whereto): string; - /** @return string|Redis */ + /** @return false|string|Redis */ public function lPop(string $key); /** * @param mixed $elements - * @return int|Redis + * @return false|int|Redis */ public function lPush(string $key, ...$elements); /** * @param mixed $elements - * @return int|Redis + * @return false|int|Redis */ public function rPush(string $key, ...$elements); - /** @return int|Redis */ - public function lPushx(string $key, mixed $value); + /** @return false|int|Redis */ + public function lPushx(string $key, string $value); - /** @return int|Redis */ - public function rPushx(string $key, mixed $value); + /** @return false|int|Redis */ + public function rPushx(string $key, string $value); - public function lSet(string $key, int $index, mixed $value): Redis|bool; + public function lSet(string $key, int $index, string $value): Redis|bool; public function lastSave(): int; @@ -262,20 +262,26 @@ class Redis { /** * @return int|Redis|false */ - public function lrem(string $key, mixed $value, int $count = 0); + public function lrem(string $key, string $value, int $count = 0); public function ltrim(string $key, int $start , int $end): Redis|bool; - /** @return array|Redis */ + /** @return false|list|Redis */ public function mget(array $keys); public function migrate(string $host, int $port, string $key, string $dst, int $timeout, bool $copy = false, bool $replace = false): bool; public function move(string $key, int $index): bool; - public function mset(array $key_values): Redis|bool; + /** + * @param array + */ + public function mset($key_values): Redis|bool; - public function msetnx(array $key_values): Redis|bool; + /** + * @param array + */ + public function msetnx($key_values): Redis|bool; public function multi(int $value = Redis::MULTI): bool|Redis; @@ -301,7 +307,7 @@ public function persist(string $key): bool; public function pfmerge(string $dst, array $keys): bool; - /** @return string|Redis */ + /** @return false|string|Redis */ public function ping(string $key = NULL); public function pipeline(): bool|Redis; @@ -313,7 +319,7 @@ public function persist(string $key): bool; public function popen(string $host, int $port = 6379, float $timeout = 0, string $persistent_id = NULL, int $retry_interval = 0, float $read_timeout = 0, array $context = NULL): bool; /** @return bool|Redis */ - public function psetex(string $key, int $expire, mixed $value); + public function psetex(string $key, int $expire, string $value); public function psubscribe(array $patterns): void; @@ -323,12 +329,12 @@ public function persist(string $key): bool; public function pubsub(string $command, mixed $arg = null): mixed; - public function punsubscribe(array $patterns): array; + public function punsubscribe(array $patterns): array|false; - /** @return string|Redis */ + /** @return false|string|Redis */ public function rPop(string $key); - /** @return string|Redis */ + /** @return false|string|Redis */ public function randomKey(); public function rawcommand(string $command, mixed ...$args): mixed; @@ -345,7 +351,7 @@ public function persist(string $key): bool; public function rpoplpush(string $src, string $dst): Redis|string|false; - public function sAdd(string $key, mixed $value, mixed ...$other_values): Redis|int|false; + public function sAdd(string $key, string $value, mixed ...$other_values): Redis|int|false; public function sAddArray(string $key, array $values): int; @@ -359,9 +365,9 @@ public function persist(string $key): bool; public function sMembers(string $key): Redis|array|false; - public function sMisMember(string $key, string $member, string ...$other_members): array; + public function sMisMember(string $key, string $member, string ...$other_members): array|false; - public function sMove(string $src, string $dst, mixed $value): Redis|bool; + public function sMove(string $src, string $dst, string $value): Redis|bool; public function sPop(string $key, int $count = 0): Redis|string|array|false; @@ -382,24 +388,24 @@ public function persist(string $key): bool; public function select(int $db): bool; /** @return bool|Redis */ - public function set(string $key, mixed $value, mixed $opt = NULL); + public function set(string $key, string $value, mixed $opt = NULL); - /** @return int|Redis */ + /** @return false|int|Redis */ public function setBit(string $key, int $idx, bool $value); - /** @return int|Redis */ + /** @return false|int|Redis */ public function setRange(string $key, int $start, string $value); public function setOption(int $option, mixed $value): bool; /** @return bool|Redis */ - public function setex(string $key, int $expire, mixed $value); + public function setex(string $key, int $expire, string $value); /** @return bool|array|Redis */ - public function setnx(string $key, mixed $value); + public function setnx(string $key, string $value); - public function sismember(string $key, mixed $value): Redis|bool; + public function sismember(string $key, string $value): Redis|bool; public function slaveof(string $host = null, int $port = 6379): bool; @@ -427,30 +433,30 @@ public function persist(string $key): bool; */ public function sortDescAlpha(string $key, ?string $pattern = null, mixed $get = null, int $offset = -1, int $count = -1, ?string $store = null): array; - public function srem(string $key, mixed $value, mixed ...$other_values): Redis|int|false; + public function srem(string $key, string $value, mixed ...$other_values): Redis|int|false; public function sscan(string $key, int &$iterator, ?string $pattern = null, int $count = 0): array|false; - /** @return int|Redis */ + /** @return false|int|Redis */ public function strlen(string $key); - public function subscribe(string $channel, string ...$other_channels): array; + public function subscribe(string $channel, string ...$other_channels): array|false; public function swapdb(string $src, string $dst): bool; - public function time(): array; + public function time(): array|false; public function ttl(string $key): Redis|int|false; - /** @return int|Redis */ + /** @return false|int|Redis */ public function type(string $key); /** - * @return int|Redis + * @return false|int|Redis */ public function unlink(array|string $key, string ...$other_keys); - public function unsubscribe(string $channel, string ...$other_channels): array; + public function unsubscribe(string $channel, string ...$other_channels): array|false; /** @return bool|Redis */ public function unwatch(); @@ -466,7 +472,7 @@ public function persist(string $key): bool; public function xadd(string $key, string $id, array $values, int $maxlen = 0, bool $approx = false): string|false; - public function xclaim(string $key, string $group, string $consumer, int $min_iddle, array $ids, array $options): string|array; + public function xclaim(string $key, string $group, string $consumer, int $min_iddle, array $ids, array $options): string|array|false; public function xdel(string $key, array $ids): Redis|int|false; @@ -498,25 +504,25 @@ public function persist(string $key): bool; public function zLexCount(string $key, string $min, string $max): Redis|int|false; - public function zMscore(string $key, string $member, string ...$other_members): array; + public function zMscore(string $key, string $member, string ...$other_members): array|false; - public function zPopMax(string $key, int $value = null): array; + public function zPopMax(string $key, int $value = null): array|false; - public function zPopMin(string $key, int $value = null): array; + public function zPopMin(string $key, int $value = null): array|false; public function zRange(string $key, int $start, int $end, mixed $scores = null): Redis|array|false; - public function zRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array; + public function zRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array|false; public function zRangeByScore(string $key, string $start, string $end, array $options = []): Redis|array|false; - public function zRandMember(string $key, array $options = null): string|array; + public function zRandMember(string $key, array $options = null): string|array|false; public function zRank(string $key, mixed $member): Redis|int|false; public function zRem(mixed $key, mixed $member, mixed ...$other_members): Redis|int|false; - public function zRemRangeByLex(string $key, string $min, string $max): int; + public function zRemRangeByLex(string $key, string $min, string $max): int|false; public function zRemRangeByRank(string $key, int $start, int $end): Redis|int|false; @@ -524,15 +530,15 @@ public function persist(string $key): bool; public function zRevRange(string $key, int $start, int $end, mixed $scores = null): Redis|array|false; - public function zRevRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array; + public function zRevRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array|false; - public function zRevRangeByScore(string $key, string $start, string $end, array $options = []): array; + public function zRevRangeByScore(string $key, string $start, string $end, array $options = []): array|false; public function zRevRank(string $key, mixed $member): Redis|int|false; public function zScore(string $key, mixed $member): Redis|float|false; - public function zdiff(array $keys, array $options = null): array; + public function zdiff(array $keys, array $options = null): array|false; public function zdiffstore(string $dst, array $keys, array $options = null): int; diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index f9836c7d5b6..bf370dcc8cf 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -727,7 +727,7 @@ public function offsetGet($offset) { 'mixedSwallowsArrayAssignment' => [ 'code' => ' 5, "b" => 12, "c" => null], function(?int $i) { - return $_GET["a"]; + return $GLOBALS["a"]; } );', 'error_message' => 'MixedArgumentTypeCoercion', diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index b8b858e5dff..adc2fc39fe8 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -374,7 +374,7 @@ function isInvalidString(?string $myVar) : bool { echo "Ma chaine " . $myString; }', ], - 'assertServerVar' => [ + 'assertSessionVar' => [ 'code' => ' [ @@ -512,7 +512,7 @@ function assertIntOrFoo($b) : void { } /** @psalm-suppress MixedAssignment */ - $a = $_GET["a"]; + $a = $GLOBALS["a"]; assertIntOrFoo($a); diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 155fd994401..7318ecabaaf 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -255,6 +255,32 @@ public function getIterator() 'ignored_issues' => [], 'php_version' => '8.1' ], + 'allowDynamicProperties' => [ + 'code' => ' [ + 'code' => ' [ 'code' => ' [], 'php_version' => '8.1', ], + 'sensitiveParameterOnMethod' => [ + 'code' => ' 'Attribute SensitiveParameter cannot be used on a method', + ], ]; } } diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 7ca6d31505e..b7eb0153e60 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -966,6 +966,32 @@ function toPositiveInt(int $i): int ', 'assertions' => ['$foo===' => 'float(3)'], ], + 'concatNonEmptyReturnNonFalsyString' => [ + 'code' => ' [ + '$a===' => 'non-falsy-string', + ], + ], + 'concatNumericWithNonEmptyReturnNonFalsyString' => [ + 'code' => ' [ + '$a===' => 'non-falsy-string', + '$b===' => 'non-falsy-string', + ], + ], ]; } diff --git a/tests/CastTest.php b/tests/CastTest.php index e14c6680993..2d20c4d3f02 100644 --- a/tests/CastTest.php +++ b/tests/CastTest.php @@ -13,7 +13,7 @@ class CastTest extends TestCase */ public function providerValidCodeParse(): iterable { - yield 'castFalseOrIntToInt' => [ + yield 'SKIPPED-castFalseOrIntToInt' => [ 'code' => ' */ $intOrFalse = 10; @@ -23,7 +23,7 @@ public function providerValidCodeParse(): iterable '$int===' => '0|int<10, 20>', ], ]; - yield 'castTrueOrIntToInt' => [ + yield 'SKIPPED-castTrueOrIntToInt' => [ 'code' => ' */ $intOrTrue = 10; @@ -33,7 +33,7 @@ public function providerValidCodeParse(): iterable '$int===' => '1|int<10, 20>', ], ]; - yield 'castBoolOrIntToInt' => [ + yield 'SKIPPED-castBoolOrIntToInt' => [ 'code' => ' */ $intOrBool = 10; diff --git a/tests/ClassTest.php b/tests/ClassTest.php index 782660aa593..6aacb0cde07 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -1004,6 +1004,18 @@ public function valid(): bool { ', 'error_message' => 'MissingTemplateParam', ], + 'cannotNameClassConstantClass' => [ + 'code' => ' */ + protected const CLASS = Bar::class; + } + + class Bar {} + ', + 'error_message' => 'ReservedWord', + ] ]; } } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 5213c86c647..53b3154cebc 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -565,6 +565,22 @@ function maker(string $className) { '$result' => 'array{stdClass}' ], ], + 'CallableWithArrayReduce' => [ + 'code' => ' [ + '$result' => 'int' + ], + ], 'FirstClassCallable:NamedFunction:is_int' => [ 'code' => ' [], 'php_version' => '8.1' ], + 'FirstClassCallable:InheritedStaticMethod' => [ + 'code' => ' [], + [], + '8.1', + ], + 'FirstClassCallable:InheritedStaticMethodWithStaticTypeParameter' => [ + 'code' => ' */ + public static function create(int $i): Holder + { + return new Holder(new static($i)); + } + } + + class C extends A {} + + /** @param \Closure(int):Holder $_ */ + function takesIntToHolder(\Closure $_): void {} + + takesIntToHolder(C::create(...));' + ], 'FirstClassCallable:WithArrayMap' => [ 'code' => ' 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, parent type A provided', + 'error_message' => 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but parent type A provided', ], 'closureByRefUseToMixed' => [ 'code' => ' [], 'php_version' => '8.0', ]; + yield 'Iterating over \DatePeriod (#5954) PHP7 Traversable' => [ + 'code' => 'format("Y-m-d"); + }', + 'assertions' => [ + '$period' => 'DatePeriod', + '$dt' => 'DateTimeInterface|null' + ], + 'error_levels' => [], + 'php_version' => '7.3', + ]; + yield 'Iterating over \DatePeriod (#5954) PHP8 IteratorAggregate' => [ + 'code' => 'format("Y-m-d"); + }', + 'assertions' => [ + '$period' => 'DatePeriod', + '$dt' => 'DateTimeImmutable|null' + ], + 'error_levels' => [], + 'php_version' => '8.0', + ]; + yield 'Iterating over \DatePeriod (#5954), ISO string' => [ + 'code' => 'format("Y-m-d"); + }', + 'assertions' => [ + '$period' => 'DatePeriod', + '$dt' => 'DateTime|null' + ], + 'error_levels' => [], + 'php_version' => '8.0', + ]; + yield 'DatePeriod implements only Traversable on PHP 7' => [ + 'code' => ' [], + 'error_levels' => [], + 'php_version' => '7.3', + ]; + yield 'DatePeriod implements IteratorAggregate on PHP 8' => [ + 'code' => ' [], + 'error_levels' => ['RedundantCondition'], + 'php_version' => '8.0', + ]; } } diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php new file mode 100644 index 00000000000..d5ae0d50d36 --- /dev/null +++ b/tests/DateTimeTest.php @@ -0,0 +1,96 @@ +, error_levels?: list}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'modify' => [ + 'code' => 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'DateTime|false', + '$b' => 'DateTimeImmutable|false', + ], + ], + 'modifyWithValidConstant' => [ + 'code' => 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'DateTime', + '$b' => 'DateTimeImmutable', + ], + ], + 'modifyWithInvalidConstant' => [ + 'code' => 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'false', + '$b' => 'false', + ], + ], + 'modifyWithBothConstant' => [ + 'code' => 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'DateTime|false', + '$b' => 'DateTimeImmutable|false', + ], + ], + ]; + } +} diff --git a/tests/FileManipulation/FileManipulationTestCase.php b/tests/FileManipulation/FileManipulationTestCase.php index 07464578647..98c604cc8de 100644 --- a/tests/FileManipulation/FileManipulationTestCase.php +++ b/tests/FileManipulation/FileManipulationTestCase.php @@ -82,6 +82,7 @@ public function testValidCode( $safe_types ); $this->project_analyzer->getCodebase()->allow_backwards_incompatible_changes = $allow_backwards_incompatible_changes; + $this->project_analyzer->getConfig()->check_for_throws_docblock = true; if (strpos(static::class, 'Unused') || strpos(static::class, 'Unnecessary')) { $this->project_analyzer->getCodebase()->reportUnusedCode(); diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php new file mode 100644 index 00000000000..467fe05b14b --- /dev/null +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -0,0 +1,225 @@ + + */ + public function providerValidCodeParse(): array + { + return [ + 'addThrowsAnnotationToFunction' => [ + 'input' => ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, + ], + 'addMultipleThrowsAnnotationToFunction' => [ + 'input' => ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, + ], + 'preservesExistingThrowsAnnotationToFunction' => [ + 'input' => ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, + ], + 'doesNotAddDuplicateThrows' => [ + 'input' => ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, + ], + 'addThrowsAnnotationToFunctionInNamespace' => [ + 'input' => ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, + ], + 'addThrowsAnnotationToFunctionFromFunctionFromOtherNamespace' => [ + 'input' => ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, + ], + 'addThrowsAnnotationAccountsForUseStatements' => [ + 'input' => ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, + ], + ]; + } +} diff --git a/tests/FileUpdates/TemporaryUpdateTest.php b/tests/FileUpdates/TemporaryUpdateTest.php index b7439e976bc..95147a9f569 100644 --- a/tests/FileUpdates/TemporaryUpdateTest.php +++ b/tests/FileUpdates/TemporaryUpdateTest.php @@ -217,7 +217,7 @@ public function foo() { } public function bar() { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -232,7 +232,7 @@ public function foo() : int { } public function bar() { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -247,7 +247,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -268,7 +268,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -285,7 +285,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -303,7 +303,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 2fcbd0784f2..9b4f21793c5 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -31,7 +31,6 @@ function filter(array $strings): array { } ' ], - 'typedArrayWithDefault' => [ 'code' => ' [ 'code' => ' [ '$a' => 'false|int', @@ -1494,7 +1493,7 @@ function test() : void { $y2 = date("Y", 10000); $F2 = date("F", 10000); /** @psalm-suppress MixedArgument */ - $F3 = date("F", $_GET["F3"]);', + $F3 = date("F", $GLOBALS["F3"]);', 'assertions' => [ '$y===' => 'numeric-string', '$m===' => 'numeric-string', @@ -1881,6 +1880,33 @@ function baz(string $s) : void { 'error_levels' => [], 'php_version' => '8.1', ], + 'trimSavesLowercaseAttribute' => [ + 'code' => ' [ + '$b===' => 'lowercase-string', + ], + ], + 'ltrimSavesLowercaseAttribute' => [ + 'code' => ' [ + '$b===' => 'lowercase-string', + ], + ], + 'rtrimSavesLowercaseAttribute' => [ + 'code' => ' [ + '$b===' => 'lowercase-string', + ], + ], ]; } diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php index 48181a66af7..d4f8c78bf05 100644 --- a/tests/IntRangeTest.php +++ b/tests/IntRangeTest.php @@ -698,7 +698,7 @@ function doAnalysis(): void /** @var string $secret */ $length = strlen($secret); if ($length > 16) { - throw new exception(""); + throw new Exception(""); } assert($length === 1); diff --git a/tests/Internal/CliUtilsTest.php b/tests/Internal/CliUtilsTest.php index f6f6ab0511e..928eb0152f1 100644 --- a/tests/Internal/CliUtilsTest.php +++ b/tests/Internal/CliUtilsTest.php @@ -12,7 +12,7 @@ class CliUtilsTest extends TestCase { /** - * @var array + * @var list */ private $argv = []; diff --git a/tests/Internal/Codebase/ClassLikesTest.php b/tests/Internal/Codebase/ClassLikesTest.php index b04beb38f41..8168d17a875 100644 --- a/tests/Internal/Codebase/ClassLikesTest.php +++ b/tests/Internal/Codebase/ClassLikesTest.php @@ -30,7 +30,7 @@ public function setUp(): void public function testWillDetectClassImplementingAliasedInterface(): void { - $this->classlikes->addClassAlias('Foo', 'bar'); + $this->classlikes->addClassAlias('Foo', 'Bar'); $classStorage = new ClassLikeStorage('Baz'); $classStorage->class_implements['bar'] = 'Bar'; @@ -42,9 +42,9 @@ public function testWillDetectClassImplementingAliasedInterface(): void public function testWillResolveAliasedAliases(): void { - $this->classlikes->addClassAlias('Foo', 'bar'); - $this->classlikes->addClassAlias('Bar', 'baz'); - $this->classlikes->addClassAlias('Baz', 'qoo'); + $this->classlikes->addClassAlias('Foo', 'Bar'); + $this->classlikes->addClassAlias('Bar', 'Baz'); + $this->classlikes->addClassAlias('Baz', 'Qoo'); self::assertSame('Foo', $this->classlikes->getUnAliasedName('Qoo')); } diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index e69385648bc..16eb66e9a3a 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -112,7 +112,6 @@ class InternalCallMapHandlerTest extends TestCase 'imagefilter', 'imagegd', 'imagegd2', - 'imageinterlace', 'imageopenpolygon', 'imagepolygon', 'imagerotate', @@ -120,11 +119,9 @@ class InternalCallMapHandlerTest extends TestCase 'imagettfbbox', 'imagettftext', 'imagexbm', - 'imap_delete', 'imap_open', 'imap_rfc822_write_address', 'imap_sort', - 'imap_undelete', 'inflate_add', 'inflate_get_read_len', 'inflate_get_status', @@ -343,6 +340,7 @@ class InternalCallMapHandlerTest extends TestCase 'cal_from_jd', 'collator_get_strength', 'curl_multi_init', + 'curl_multi_getcontent', // issue #8351 'date_add', 'date_date_set', 'date_diff', @@ -373,9 +371,6 @@ class InternalCallMapHandlerTest extends TestCase 'gzeof', 'gzopen', 'gzpassthru', - 'hash', - 'hash_hkdf', - 'hash_hmac', 'iconv_get_encoding', 'igbinary_serialize', 'imagecolorclosest', @@ -519,7 +514,13 @@ public function testIgnoresAreSortedAndUnique(): void /** @var string */ $function = is_int($key) ? $value : $key; - $this->assertGreaterThan(0, strcmp($function, $previousFunction)); + $diff = strcmp($function, $previousFunction); + if ($diff <= 0) { + // faster debugging errors in tests + echo "\n" . $previousFunction . "\n" . $function . "\n"; + } + + $this->assertGreaterThan(0, $diff); $previousFunction = $function; } } @@ -750,7 +751,7 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa $expectedType = $param->getType(); if (isset($expectedType) && !empty($normalizedEntry['type'])) { - $this->assertTypeValidity($expectedType, $normalizedEntry['type'], "Param '{$name}' has incorrect type"); + $this->assertTypeValidity($expectedType, $normalizedEntry['type'], false, "Param '{$name}' has incorrect type"); } } @@ -771,20 +772,19 @@ public function assertEntryReturnType(ReflectionFunction $function, string $entr return; } - $this->assertTypeValidity($expectedType, $entryReturnType, 'CallMap entry has incorrect return type'); + $this->assertTypeValidity($expectedType, $entryReturnType, true, 'CallMap entry has incorrect return type'); } /** * Since string equality is too strict, we do some extra checking here */ - private function assertTypeValidity(ReflectionType $reflected, string $specified, string $message): void + private function assertTypeValidity(ReflectionType $reflected, string $specified, bool $checkNullable, string $message): void { $expectedType = Reflection::getPsalmTypeFromReflectionType($reflected); - - $parsedType = Type::parseString($specified); + $callMapType = Type::parseString($specified); try { - $this->assertTrue(UnionTypeComparator::isContainedBy(self::$codebase, $parsedType, $expectedType), $message); + $this->assertTrue(UnionTypeComparator::isContainedBy(self::$codebase, $callMapType, $expectedType), $message); } catch (InvalidArgumentException $e) { if (preg_match('/^Could not get class storage for (.*)$/', $e->getMessage(), $matches) && !class_exists($matches[1]) @@ -792,5 +792,10 @@ private function assertTypeValidity(ReflectionType $reflected, string $specified $this->fail("Class used in CallMap does not exist: {$matches[1]}"); } } + + // Reflection::getPsalmTypeFromReflectionType adds |null to mixed types so skip comparison + if ($checkNullable && !$expectedType->hasMixed()) { + $this->assertSame($expectedType->isNullable(), $callMapType->isNullable(), $message); + } } } diff --git a/tests/Internal/Provider/FakeParserCacheProvider.php b/tests/Internal/Provider/FakeParserCacheProvider.php index 06b31afd84b..36b1dbb3d50 100644 --- a/tests/Internal/Provider/FakeParserCacheProvider.php +++ b/tests/Internal/Provider/FakeParserCacheProvider.php @@ -33,6 +33,14 @@ public function cacheFileContents(string $file_path, string $file_contents): voi { } + public function deleteOldParserCaches(float $time_before): int + { + $this->existing_file_content_hashes = null; + $this->new_file_content_hashes = []; + + return 0; + } + public function saveFileContentHashes(): void { } diff --git a/tests/Internal/Provider/ParserInstanceCacheProvider.php b/tests/Internal/Provider/ParserInstanceCacheProvider.php index 9b81bfcef87..766772cd600 100644 --- a/tests/Internal/Provider/ParserInstanceCacheProvider.php +++ b/tests/Internal/Provider/ParserInstanceCacheProvider.php @@ -82,6 +82,18 @@ public function cacheFileContents(string $file_path, string $file_contents): voi $this->file_contents_cache[$file_path] = $file_contents; } + public function deleteOldParserCaches(float $time_before): int + { + $this->existing_file_content_hashes = null; + $this->new_file_content_hashes = []; + + $this->file_contents_cache = []; + $this->file_content_hash = []; + $this->statements_cache = []; + $this->statements_cache_time = []; + return 0; + } + public function saveFileContentHashes(): void { } diff --git a/tests/JsonOutputTest.php b/tests/JsonOutputTest.php index d2f10959346..1ab0c0457d4 100644 --- a/tests/JsonOutputTest.php +++ b/tests/JsonOutputTest.php @@ -129,12 +129,12 @@ function fooFoo() { 'assertCancelsMixedAssignment' => [ 'code' => ' 1, - 'message' => 'Docblock-defined type int for $a is always int', + 'message' => 'Docblock-defined type string for $a is always string', 'line' => 4, - 'error' => 'is_int($a)', + 'error' => 'is_string($a)', ], 'singleIssueForTypeDifference' => [ 'code' => 'project_analyzer, 'somefile.php', 'somefile.php'); @@ -111,9 +111,9 @@ function qux(int $a, int $b) : int { $this->assertNotNull($information); $this->assertSame("getSymbolInformation('somefile.php', '$_SERVER'); + $information = $codebase->getSymbolInformation('somefile.php', '$_SESSION'); $this->assertNotNull($information); - $this->assertSame("", $information['type']); + $this->assertSame("", $information['type']); $information = $codebase->getSymbolInformation('somefile.php', '$my_global'); $this->assertNotNull($information); diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 76659e93c92..8e2eaa3275d 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -258,7 +258,6 @@ public static function main(): void { ], 'dateTimeImmutableStatic' => [ 'code' => 'modify("+3 hours");', 'assertions' => [ - '$yesterday' => 'MyDate|false', + '$yesterday' => 'MyDate', '$b' => 'DateTimeImmutable', ], ], @@ -974,7 +973,7 @@ public static function new() : self { class Datetime extends \DateTime { - public static function createFromInterface(\DatetimeInterface $datetime): static + public static function createFromInterface(\DateTimeInterface $datetime): static { return parent::createFromInterface($datetime); } diff --git a/tests/ReadonlyPropertyTest.php b/tests/ReadonlyPropertyTest.php index 0cd15bda500..b29548c518d 100644 --- a/tests/ReadonlyPropertyTest.php +++ b/tests/ReadonlyPropertyTest.php @@ -64,7 +64,7 @@ public function setBar(string $s) : void { echo (new A)->bar;' ], - 'readonlyPublicPropertySetInAnotherMEthod' => [ + 'readonlyPublicPropertySetInAnotherMethod' => [ 'code' => 'bar;' ], + 'docblockReadonlyWithPrivateMutationsAllowedConstructorPropertySetInAnotherMethod' => [ + 'code' => 'bar = $s; + } + } + + echo (new A)->bar;' + ], + 'readonlyPublicConstructorPropertySetInAnotherMethod' => [ + 'code' => 'bar = $s; + } + } + + echo (new A)->bar;' + ], 'readonlyPropertySetChildClass' => [ 'code' => 'analyzeFileForReport(); + + $report_options = new ReportOptions(); + $report_options->format = Report::TYPE_COUNT; + $expected_output = <<<'EOF' +MixedInferredReturnType: 1 +MixedReturnStatement: 1 +PossiblyUndefinedGlobalVariable: 1 +UndefinedConstant: 1 +UndefinedVariable: 1 + +EOF; + $this->assertSame( + $expected_output, + IssueBuffer::getOutput(IssueBuffer::getIssuesData(), $report_options) + ); + } + public function testEmptyReportIfNotError(): void { $this->addFile( diff --git a/tests/ReturnTypeProvider/ArrayColumnTest.php b/tests/ReturnTypeProvider/ArrayColumnTest.php index da1c085ffce..ea6a8d604f1 100644 --- a/tests/ReturnTypeProvider/ArrayColumnTest.php +++ b/tests/ReturnTypeProvider/ArrayColumnTest.php @@ -3,10 +3,12 @@ namespace Psalm\Tests\ReturnTypeProvider; use Psalm\Tests\TestCase; +use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; class ArrayColumnTest extends TestCase { + use InvalidCodeAnalysisTestTrait; use ValidCodeAnalysisTestTrait; public function providerValidCodeParse(): iterable @@ -61,5 +63,95 @@ function f(array $shape): array { } ', ]; + + yield 'arrayColumnWithObjectsAndColumnNameNull' => [ + 'code' => 'foo(); + } + ', + ]; + + yield 'arrayColumnWithIntersectionAndColumnNameNull' => [ + 'code' => 'foo(); + $instance->bar(); + } + ', + ]; + + yield 'arrayColumnWithArrayAndColumnNameNull' => [ + 'code' => ' "", "instance" => new C]], null, "name") as $array) { + $array["instance"]->foo(); + } + ', + ]; + + yield 'arrayColumnWithListOfObject' => [ + 'code' => ' $instances */ + $instances = []; + foreach (array_column($instances, null, "name") as $instance) { + foo($instance); + } + ', + ]; + + yield 'arrayColumnWithListOfArrays' => [ + 'code' => ' $arrays */ + $arrays = []; + foreach (array_column($arrays, null, "name") as $array) { + foo($array); + } + ', + ]; + } + + public function providerInvalidCodeParse(): iterable + { + yield 'arrayColumnWithArrayAndColumnNameNull' => [ + 'code' => ' $arrays */ + $arrays = []; + foreach (array_column($arrays, null, "name") as $array) { + $array["instance"]->foo(); + } + ', + 'error_message' => 'MixedMethodCall', + ]; } } diff --git a/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php b/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php new file mode 100644 index 00000000000..da67246e2f3 --- /dev/null +++ b/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php @@ -0,0 +1,53 @@ + [ + 'code' => ' [ + '$min' => 'int', + '$max' => 'int', + ], + ]; + yield 'nonInt' => [ + 'code' => ' [ + '$min' => 'string', + '$max' => 'string', + ], + ]; + yield 'maxIntRange' => [ + 'code' => ' $v) { + if ($v === "") $h0 = $i; + if ($v === "") $h1 = $i; + } + if ($h0 === null || $h1 === null) throw new \Exception(); + + $min = min($h0, $h1); + $max = max($h0, $h1); + ', + 'assertions' => [ + '$min' => 'int<0, max>', + '$max' => 'int<0, max>', + ], + ]; + } +} diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 45d207cd95f..2c01eed210c 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1213,7 +1213,7 @@ function fooFoo(): A { * @psalm-suppress UndefinedClass */ function fooFoo(): A { - return $_GET["a"]; + return $GLOBALS["a"]; } fooFoo()->bar();', @@ -1500,7 +1500,7 @@ function($iter) use ($predicate) { $res = map(function(int $i): string { return (string) $i; })([1,2,3]); ', - 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:54 - Argument 1 expects T:fn-map as mixed, int provided', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:54 - Argument 1 expects T:fn-map as mixed, but int provided', ], 'cannotInferReturnClosureWithDifferentReturnTypes' => [ 'code' => ' [ - 'code' => ' [ 'code' => ' [ 'code' => ' 'TaintedHtml', ], 'foreachArg' => [ diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 896cd11f971..53add8201d4 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -1444,7 +1444,7 @@ public function __construct(array $elements = []) } /** @psalm-suppress MixedArgument */ - $c = new ArrayCollection($_GET["a"]);', + $c = new ArrayCollection($GLOBALS["a"]);', 'assertions' => [ '$c' => 'ArrayCollection', ], @@ -3797,7 +3797,155 @@ public function __construct($data) { parent::__construct(new ArrayObject([$data] '$a===' => 'FutureB<123>', '$r===' => 'ArrayObject' ] - ] + ], + 'return TemplatedClass' => [ + 'code' => ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return Maybe */ + final public static function create(): Maybe + { + return Maybe::just(new static()); + } + }', + ], + 'return list created in a static method of another class' => [ + 'code' => ' + * + * @psalm-pure + */ + public static function mklist($value): array + { + return [ $value ]; + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return list */ + final public static function create(): array + { + return Lister::mklist(new static()); + } + }', + ], + 'use TemplatedClass as an intermediate variable inside a method' => [ + 'code' => ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + final public static function create(): static + { + $maybe = Maybe::just(new static()); + return $maybe->value; + } + }', + ], + 'static is the return type of an analyzed static method' => [ + 'code' => 'acceptA(B::create()); + } + + private function acceptA(A $_a): void + { + } + }', + ], + 'undefined class in function dockblock' => [ + 'code' => ' $baz + */ + function foobar(DoesNotExist $baz): void {} + + /** + * @psalm-suppress UndefinedDocblockClass, UndefinedClass + * @var DoesNotExist + */ + $baz = new DoesNotExist(); + foobar($baz);', + ], ]; } @@ -3908,7 +4056,7 @@ public function __construct(callable $closure) type($closure); } }', - 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:20:34 - Argument 1 of type expects string, callable(State):(T:AlmostFooMap as mixed)&Foo provided', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:20:34 - Argument 1 of type expects string, but callable(State):(T:AlmostFooMap as mixed)&Foo provided', ], 'templateWithNoReturn' => [ 'code' => ' 5, "name" => "Mario", "height" => 3.5]); $mario->ame = "Luigi";', - 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . "somefile.php:47:29 - Argument 1 of CharacterRow::__set expects 'height'|'id'|'name', 'ame' provided", + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . "somefile.php:47:29 - Argument 1 of CharacterRow::__set expects 'height'|'id'|'name', but 'ame' provided", ], 'specialiseTypeBeforeReturning' => [ 'code' => 'getAttribute("colour", "red"); // typed as string $b = (new A)->getAttribute(null); // typed as array /** @psalm-suppress MixedArgument */ - $c = (new A)->getAttribute($_GET["foo"]); // typed as string|array', + $c = (new A)->getAttribute($GLOBALS["foo"]); // typed as string|array', 'assertions' => [ '$a' => 'string', '$b' => 'array', diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index d61c0255ac2..2c14e072c33 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -489,6 +489,28 @@ public function __toString(): string ', 'error_message' => 'ImplicitToStringCast' ], + 'toStringTypecastNonString' => [ + 'code' => ' 'InvalidCast', + ], + 'riskyArrayToIntCast' => [ + 'code' => ' 'RiskyCast', + ], + 'riskyArrayToFloatCast' => [ + 'code' => ' 'RiskyCast', + ], ]; } } diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index 147ac2ab2be..4dabdf20134 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -717,6 +717,13 @@ public function providerTestValidTypeCombination(): array 'non-empty-string' ] ], + 'combineTruthyStringAndNonEmptyString' => [ + 'non-empty-string', + [ + 'truthy-string', + 'non-empty-string' + ] + ], 'combineNonFalsyNonEmptyString' => [ 'non-empty-string', [ diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 5eef13b14d3..0fd92b6342d 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -2,6 +2,8 @@ namespace Psalm\Tests\TypeReconciliation; +use Psalm\Config; +use Psalm\Context; use Psalm\Tests\TestCase; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; @@ -458,4 +460,56 @@ function go(array $options): void { ], ]; } + + public function testAllowPropertyFetchAsNeedle(): void + { + Config::getInstance()->ensure_array_int_offsets_exist = true; + + $this->addFile( + 'somefile.php', + ' $bar */ + $bar = []; + + if (array_key_exists($foo->status, $bar)) { + echo $bar[$foo->status]; + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } + + public function testAllowStaticPropertyFetchAsNeedle(): void + { + Config::getInstance()->ensure_array_int_offsets_exist = true; + + $this->addFile( + 'somefile.php', + ' $bar */ + $bar = []; + + if (array_key_exists(Foo::$status, $bar)) { + echo $bar[Foo::$status]; + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } } diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 07d4706204b..d5a320d9b90 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -783,10 +783,10 @@ function d(?iterable $foo): void { } }', ], - 'isStringServerVar' => [ + 'isStringSessionVar' => [ 'code' => ' [ @@ -2864,6 +2864,72 @@ function matches(string $value): bool { return true; }' ], + 'ctypeDigitMakesStringNumeric' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + '$int' => 'int<48, 57>|int<256, 1000>' + ] + ], + 'ctypeLowerMakesStringLowercase' => [ + 'code' => ' [ + 'code' => ' [ + '$int' => 'int<97, 122>' + ] + ], ]; } diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index d9d8161b232..66fed87a38f 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -200,7 +200,7 @@ function foo(int $t) : void { 'code' => ' 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:14:32 - Argument 1 of takesB expects B,' - . ' parent type A&static provided', + . ' but parent type A&static provided', ], 'intersectionTypeInterfaceCheckAfterInstanceof' => [ 'code' => ' 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:32 - Argument 1 of takesI expects I, A&static provided', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:32 - Argument 1 of takesI expects I, but A&static provided', ], ]; } diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index 7d48b481437..60f81a65c51 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -904,6 +904,11 @@ function foo(string $s) : void { if (empty($s)) {} }', ], + 'falseDateInterval' => [ + 'code' => ' [ 'code' => ' '5', ], ], - 'falseDateInterval' => [ - 'code' => ' 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array, parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41' + 'error_message' => 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array, but parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41' ], 'warnAboutUnusedVariableInTryReassignedInCatch' => [ 'code' => ' "$_GET['abc']-src/FileWithErrors.php:345-349" - "$_GET['abc']-src/FileWithErrors.php:345-349" -> "coalesce-src/FileWithErrors.php:345-363" + "$_GET:src/FileWithErrors.php:413" -> "$_GET['abc']-src/FileWithErrors.php:413-417" + "$_GET:src/FileWithErrors.php:440" -> "$_GET['abc']-src/FileWithErrors.php:440-444" + "$_GET:src/FileWithErrors.php:456" -> "$_GET['abc']-src/FileWithErrors.php:456-460" + "$_GET['abc']-src/FileWithErrors.php:440-444" -> "call to is_string-src/FileWithErrors.php:440-451" + "$_GET['abc']-src/FileWithErrors.php:456-460" -> "call to echo-src/FileWithErrors.php:407-473" "$s-src/FileWithErrors.php:109-110" -> "variable-use" -> "acme\sampleproject\bar" "$s-src/FileWithErrors.php:162-163" -> "variable-use" -> "acme\sampleproject\baz" "$s-src/FileWithErrors.php:215-216" -> "variable-use" -> "acme\sampleproject\bat" @@ -10,6 +13,8 @@ digraph Taints { "acme\sampleproject\bat#1" -> "$s-src/FileWithErrors.php:215-216" "acme\sampleproject\baz#1" -> "$s-src/FileWithErrors.php:162-163" "acme\sampleproject\foo#1" -> "$s-src/FileWithErrors.php:57-58" - "call to echo-src/FileWithErrors.php:335-364" -> "echo#1-src/filewitherrors.php:330" - "coalesce-src/FileWithErrors.php:345-363" -> "call to echo-src/FileWithErrors.php:335-364" + "call to echo-src/FileWithErrors.php:335-367" -> "echo#1-src/filewitherrors.php:330" + "call to echo-src/FileWithErrors.php:407-473" -> "echo#1-src/filewitherrors.php:402" + "call to is_string-src/FileWithErrors.php:440-451" -> "is_string#1-src/filewitherrors.php:430" + "coalesce-src/FileWithErrors.php:345-366" -> "call to echo-src/FileWithErrors.php:335-367" }