diff --git a/.github/workflows/bcc.yml b/.github/workflows/bcc.yml index 8bc58be9bb3..5cc8c319458 100644 --- a/.github/workflows/bcc.yml +++ b/.github/workflows/bcc.yml @@ -13,7 +13,7 @@ jobs: tools: composer:v2 coverage: none - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -24,7 +24,7 @@ jobs: echo "::set-output name=vcs_cache::$(composer config cache-vcs-dir)" - name: Cache composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ steps.composer-cache.outputs.files_cache }} 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/.gitignore b/.gitignore index a060c7ed342..3d4eb36dc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /tests/fixtures/symlinktest/* .idea/ +.vscode/ diff --git a/UPGRADING.md b/UPGRADING.md index 9f2ad3f0dd1..6e176c69ebd 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,14 @@ # Upgrading from Psalm 4 to Psalm 5 ## Changed +- [BC] `Psalm\Type\Union`s are now partially immutable, mutator methods were removed and moved into `Psalm\Type\MutableUnion`. + To modify a union type, use the new `Psalm\Type\Union::getBuilder` method to turn a `Psalm\Type\Union` into a `Psalm\Type\MutableUnion`: once you're done, use `Psalm\Type\MutableUnion::freeze` to get a new `Psalm\Type\Union`. + Methods removed from `Psalm\Type\Union` and moved into `Psalm\Type\MutableUnion`: + - `replaceTypes` + - `addType` + - `removeType` + - `substitute` + - `replaceClassLike` + - [BC] TPositiveInt has been removed and replaced by TIntRange - [BC] The parameter `$php_version` of `Psalm\Type\Atomic::create()` renamed 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/psalm-baseline.xml b/psalm-baseline.xml index 95396607e4d..a1dcc3a49a3 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -68,7 +68,7 @@ - + $assertion->rule[0] $assertion->rule[0] $assertion->rule[0] @@ -90,12 +90,6 @@ $expr->getArgs()[0] $expr->getArgs()[0] $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] $expr->getArgs()[1] $expr->getArgs()[1] $get_debug_type_expr->getArgs()[0] @@ -112,6 +106,9 @@ + + verifyType + $non_existent_method_ids[0] $parts[1] @@ -119,9 +116,8 @@ - + $arg_function_params[$argument_offset][0] - $array_type->getGenericArrayType()->getChildNodes()[0] @@ -253,10 +249,9 @@ - + $l[4] $r[4] - $var_line_parts[0] @@ -289,6 +284,26 @@ $cs[0] + + + $callable + + + TCallable|TClosure|null + + + + + get + get + get + getClassTemplateTypes + has + + + $candidate_param_type->from_template_default + + $combination->array_type_params[1] @@ -311,47 +326,153 @@ array_keys($template_type_map[$template_param_name])[0] - - - VirtualClass - - - - - VirtualFunction - - - - - VirtualInterface - - - - - VirtualTrait - - - - - VirtualConst - - + + classExtendsOrImplements + classExtendsOrImplements + classExtendsOrImplements + classOrInterfaceExists + classOrInterfaceExists + classOrInterfaceExists + getMappedGenericTypeParams + interfaceExtends + interfaceExtends + interfaceExtends + array_keys($template_type_map[$value])[0] + + + replace + replace + replace + replace + + + + getMappedGenericTypeParams + replace + replace + $this->type_params[1] + + + getMostSpecificTypeFromBounds + + + + + replace + + + + + getString + getString + replace + replace + + + $cloned->value_param + + + + + replace + + + + + combine + combine + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + replace + replace + + + $key_type->possibly_undefined + $value_type->possibly_undefined + $value_type->possibly_undefined + + + + + replace + replace + + + $cloned->type_param + + + + + replace + replace + + + $type->possibly_undefined + $type->possibly_undefined + + + + + replace + + + + + replace + + + + + replace + + + + + $allow_mutations + $by_ref + $failed_reconciliation + $from_template_default + $has_mutations + $initialized_class + $reference_free + + $type[0] $type[0][0] + + + $ignore_isset + + + + + allFloatLiterals + allFloatLiterals + + + + + UndefinedMethod + + $subNodes['expr'] 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..0afbe3de4e2 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -485,11 +485,14 @@ public function update( if ((!$new_type || !$old_type->equals($new_type)) && ($new_type || count($existing_type->getAtomicTypes()) > 1) ) { - $existing_type->substitute($old_type, $new_type); + $existing_type = $existing_type + ->getBuilder() + ->substitute($old_type, $new_type); if ($new_type && $new_type->from_docblock) { - $existing_type->setFromDocblock(); + $existing_type = $existing_type->setFromDocblock(); } + $existing_type = $existing_type->freeze(); $updated_vars[$var_id] = true; } @@ -770,18 +773,23 @@ public function removeDescendents( $statements_analyzer ); - foreach ($this->vars_in_scope as $var_id => $type) { + foreach ($this->vars_in_scope as $var_id => &$type) { if (preg_match('/' . preg_quote($remove_var_id, '/') . '[\]\[\-]/', $var_id)) { $this->remove($var_id, false); } + $builder = null; foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof DependentType && $atomic_type->getVarId() === $remove_var_id ) { - $type->addType($atomic_type->getReplacement()); + $builder ??= $type->getBuilder(); + $builder->addType($atomic_type->getReplacement()); } } + if ($builder) { + $type = $builder->freeze(); + } } } @@ -848,7 +856,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..d3eaf7107a4 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -295,8 +295,7 @@ public function analyze( $mixins = array_merge($storage->templatedMixins, $storage->namedMixins); $union = new Union($mixins); - $static_self = new TNamedObject($storage->name); - $static_self->is_static = true; + $static_self = new TNamedObject($storage->name, true); $union = TypeExpander::expandUnion( $codebase, @@ -505,6 +504,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) { @@ -758,7 +767,7 @@ public static function addContextProperties( // Get actual types used for templates (to support @template-covariant) $template_standins = new TemplateResult($lower_bounds, []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $guide_property_type, $template_standins, $codebase, @@ -791,12 +800,12 @@ public static function addContextProperties( $template_result = new TemplateResult([], $lower_bounds); - TemplateInferredTypeReplacer::replace( + $guide_property_type = TemplateInferredTypeReplacer::replace( $guide_property_type, $template_result, $codebase ); - TemplateInferredTypeReplacer::replace( + $property_type = TemplateInferredTypeReplacer::replace( $property_type, $template_result, $codebase @@ -1223,8 +1232,7 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { $method_context->collect_nonprivate_initializations = !$uninitialized_private_properties; $method_context->self = $fq_class_name; - $this_atomic_object_type = new TNamedObject($fq_class_name); - $this_atomic_object_type->is_static = !$storage->final; + $this_atomic_object_type = new TNamedObject($fq_class_name, !$storage->final); $method_context->vars_in_scope['$this'] = new Union([$this_atomic_object_type]); $method_context->vars_possibly_in_scope['$this'] = true; @@ -1294,8 +1302,12 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { ); } elseif (!$property_storage->has_default) { if (isset($this->inferred_property_types[$property_name])) { - $this->inferred_property_types[$property_name]->addType(new TNull()); - $this->inferred_property_types[$property_name]->setFromDocblock(); + $this->inferred_property_types[$property_name] = + $this->inferred_property_types[$property_name] + ->getBuilder() + ->addType(new TNull()) + ->setFromDocblock(true) + ->freeze(); } } } @@ -1365,7 +1377,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', @@ -1543,7 +1555,7 @@ private function analyzeProperty( } if ($suggested_type && !$property_storage->has_default && $property_storage->is_static) { - $suggested_type->addType(new TNull()); + $suggested_type = $suggested_type->getBuilder()->addType(new TNull())->freeze(); } if ($suggested_type && !$suggested_type->isNull()) { 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/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 4a3ce8a2fc3..59d1165c014 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -92,8 +92,7 @@ public static function analyzeExpression( /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ $use_context->vars_in_scope['$this'] = clone $context->vars_in_scope['$this']; } elseif ($context->self) { - $this_atomic = new TNamedObject($context->self); - $this_atomic->is_static = true; + $this_atomic = new TNamedObject($context->self, true); $use_context->vars_in_scope['$this'] = new Union([$this_atomic]); } diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 5af3a43e8a3..925af7f35d6 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -159,7 +159,8 @@ public static function arrayToDocblocks( $var_type_tokens, null, $template_type_map ?: [], - $type_aliases ?: [] + $type_aliases ?: [], + true ); } catch (TypeParseTreeException $e) { throw new DocblockParseException( @@ -173,8 +174,6 @@ public static function arrayToDocblocks( ); } - $defined_type->setFromDocblock(); - $var_comment = new VarDocblockComment(); $var_comment->type = $defined_type; $var_comment->var_id = $var_id; @@ -375,7 +374,7 @@ public static function splitDocLine(string $return_block): array $remaining = trim(preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); if ($remaining) { - return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining)); + return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining) ?: []); } return [$type]; diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 76e23c93eae..7e18ff9a250 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -168,8 +168,7 @@ public static function verifyReturnType( // only add null if we have a return statement elsewhere and it wasn't void foreach ($inferred_return_type_parts as $inferred_return_type_part) { if (!$inferred_return_type_part->isVoid()) { - $atomic_null = new TNull(); - $atomic_null->from_docblock = true; + $atomic_null = new TNull(true); $inferred_return_type_parts[] = new Union([$atomic_null]); break; } @@ -577,15 +576,13 @@ public static function verifyReturnType( return false; } } - } elseif (!$inferred_return_type->hasMixed() - && !UnionTypeComparator::isContainedBy( - $codebase, - $declared_return_type, - $inferred_return_type, - false, - false - ) - ) { + } elseif (!UnionTypeComparator::isContainedBy( + $codebase, + $declared_return_type, + $inferred_return_type, + false, + false + )) { if ($codebase->alter_code) { if (isset($project_analyzer->getIssuesToFix()['LessSpecificReturnType']) && !in_array('LessSpecificReturnType', $suppressed_issues) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 29fc51a3080..8b51864ea13 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -623,7 +623,7 @@ public function analyze( /** * @var TClosure */ - $closure_atomic = $function_type->getSingleAtomic(); + $closure_atomic = clone $function_type->getSingleAtomic(); if (($storage->return_type === $storage->signature_return_type) && (!$storage->return_type @@ -634,10 +634,14 @@ public function analyze( $storage->return_type )) ) { + /** @psalm-suppress InaccessibleProperty Acting on clone */ $closure_atomic->return_type = $closure_return_type; } + /** @psalm-suppress InaccessibleProperty Acting on clone */ $closure_atomic->is_pure = !$this->inferred_impure; + + $statements_analyzer->node_data->setType($this->function, new Union([$closure_atomic])); } } @@ -707,6 +711,7 @@ public function analyze( } } + $missingThrowsDocblockErrors = []; foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $codelocations) { $is_expected = false; @@ -720,6 +725,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 +746,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 @@ -992,8 +1016,9 @@ private function processParams( if ($signature_type && $signature_type_location && $signature_type->hasObjectType()) { $referenced_type = $signature_type; if ($referenced_type->isNullable()) { - $referenced_type = clone $referenced_type; + $referenced_type = $referenced_type->getBuilder(); $referenced_type->removeType('null'); + $referenced_type = $referenced_type->freeze(); } [$start, $end] = $signature_type_location->getSelectionBounds(); $codebase->analyzer->addOffsetReference( @@ -1825,18 +1850,21 @@ private function getFunctionInformation( $this_object_type = new TGenericObject( $context->self, - $template_params + $template_params, + false, + !$storage->final ); } else { - $this_object_type = new TNamedObject($context->self); + $this_object_type = new TNamedObject( + $context->self, + !$storage->final + ); } - $this_object_type->is_static = !$storage->final; - if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) { $template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Union([$this_object_type]), $template_result, $codebase, @@ -1844,9 +1872,9 @@ private function getFunctionInformation( $this->storage->if_this_is_type ); - foreach ($context->vars_in_scope as $var_name => $var_type) { + foreach ($context->vars_in_scope as $var_name => &$var_type) { if (0 === mb_strpos($var_name, '$this->')) { - TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase); + $var_type = TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase); } } @@ -1983,14 +2011,11 @@ private function getFunctionInformation( $closure_type = new TClosure( 'Closure', $storage->params, - $closure_return_type + $closure_return_type, + $storage instanceof FunctionStorage ? $storage->pure : null, + $storage instanceof FunctionStorage ? $storage->byref_uses : [], ); - if ($storage instanceof FunctionStorage) { - $closure_type->byref_uses = $storage->byref_uses; - $closure_type->is_pure = $storage->pure; - } - $type_provider->setType( $this->function, new Union([ diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 58476245364..f6bfe8db4b9 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -362,7 +362,7 @@ private static function compareMethodParams( $guide_param_signature_type = $guide_param->type; $or_null_guide_param_signature_type = $guide_param->signature_type - ? clone $guide_param->signature_type + ? $guide_param->signature_type->getBuilder() : null; if ($or_null_guide_param_signature_type) { @@ -729,29 +729,34 @@ private static function compareMethodDocblockParams( } } - foreach ($implementer_method_storage_param_type->getAtomicTypes() as $k => $t) { + $builder = $implementer_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { if ($t instanceof TTemplateParam && strpos($t->defining_class, 'fn-') === 0 ) { - $implementer_method_storage_param_type->removeType($k); + $builder->removeType($k); foreach ($t->as->getAtomicTypes() as $as_t) { - $implementer_method_storage_param_type->addType($as_t); + $builder->addType($as_t); } } } + $implementer_method_storage_param_type = $builder->freeze(); - foreach ($guide_method_storage_param_type->getAtomicTypes() as $k => $t) { + $builder = $guide_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { if ($t instanceof TTemplateParam && strpos($t->defining_class, 'fn-') === 0 ) { - $guide_method_storage_param_type->removeType($k); + $builder->removeType($k); foreach ($t->as->getAtomicTypes() as $as_t) { - $guide_method_storage_param_type->addType($as_t); + $builder->addType($as_t); } } } + $guide_method_storage_param_type = $builder->freeze(); + unset($builder); if ($implementer_classlike_storage->template_extended_params) { self::transformTemplates( @@ -1055,7 +1060,7 @@ private static function compareMethodDocblockReturnTypes( private static function transformTemplates( array $template_extended_params, string $base_class_name, - Union $templated_type, + Union &$templated_type, Codebase $codebase ): void { if (isset($template_extended_params[$base_class_name])) { @@ -1092,7 +1097,7 @@ private static function transformTemplates( $template_result = new TemplateResult([], $template_types); - TemplateInferredTypeReplacer::replace( + $templated_type = TemplateInferredTypeReplacer::replace( $templated_type, $template_result, $codebase 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/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 209080eefef..6939ec53888 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -550,11 +550,8 @@ public static function checkIteratorType( } } elseif ($iterator_atomic_type instanceof TIterable) { if ($iterator_atomic_type->extra_types) { - $iterator_atomic_type_copy = clone $iterator_atomic_type; - $iterator_atomic_type_copy->extra_types = []; - $iterator_atomic_types = [$iterator_atomic_type_copy]; $iterator_atomic_types = array_merge( - $iterator_atomic_types, + [$iterator_atomic_type->setIntersectionTypes([])], $iterator_atomic_type->extra_types ); } else { @@ -736,10 +733,10 @@ public static function handleIterable( bool &$has_valid_iterator ): void { if ($iterator_atomic_type->extra_types) { - $iterator_atomic_type_copy = clone $iterator_atomic_type; - $iterator_atomic_type_copy->extra_types = []; - $iterator_atomic_types = [$iterator_atomic_type_copy]; - $iterator_atomic_types = array_merge($iterator_atomic_types, $iterator_atomic_type->extra_types); + $iterator_atomic_types = array_merge( + [$iterator_atomic_type->setIntersectionTypes([])], + $iterator_atomic_type->extra_types + ); } else { $iterator_atomic_types = [$iterator_atomic_type]; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index 1160f615394..68fc8398a9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -273,7 +273,7 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { && $codebase->interfaceExists($fq_catch_class) && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') ) { - $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); + return $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); } return $catch_class_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index f33c0d82ad4..52eb2c41d08 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -120,14 +120,14 @@ public static function analyze( // if this array looks like an object-like array, let's return that instead if (count($array_creation_info->property_types) !== 0) { - $atomic_type = new TKeyedArray($array_creation_info->property_types, $array_creation_info->class_strings); - if ($array_creation_info->can_create_objectlike) { - $atomic_type->sealed = true; - } else { - $atomic_type->previous_key_type = $item_key_type ?? Type::getArrayKey(); - $atomic_type->previous_value_type = $item_value_type ?? Type::getMixed(); - } - $atomic_type->is_list = $array_creation_info->all_list; + $atomic_type = new TKeyedArray( + $array_creation_info->property_types, + $array_creation_info->class_strings, + $array_creation_info->can_create_objectlike, + $array_creation_info->can_create_objectlike ? null : ($item_key_type ?? Type::getArrayKey()), + $array_creation_info->can_create_objectlike ? null : ($item_value_type ?? Type::getMixed()), + $array_creation_info->all_list + ); $stmt_type = new Union([$atomic_type]); @@ -225,10 +225,10 @@ public static function analyze( } if ($bad_types && $good_types) { - $item_key_type->substitute( + $item_key_type = $item_key_type->getBuilder()->substitute( TypeCombiner::combine($bad_types, $codebase), TypeCombiner::combine($good_types, $codebase) - ); + )->freeze(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 0e5eb2b7435..670fae607b4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -68,6 +68,7 @@ use Psalm\Storage\Assertion\NonEmptyCountable; use Psalm\Storage\Assertion\NotNonEmptyCountable; use Psalm\Storage\Assertion\Truthy; +use Psalm\Storage\Possibilities; use Psalm\Storage\PropertyStorage; use Psalm\Type; use Psalm\Type\Atomic; @@ -805,10 +806,7 @@ public static function processFunctionCall( } } elseif ($class_exists_check_type = self::hasClassExistsCheck($expr)) { if ($first_var_name) { - $class_string_type = new TClassString(); - if ($class_exists_check_type === 1) { - $class_string_type->is_loaded = true; - } + $class_string_type = new TClassString('object', null, $class_exists_check_type === 1); $if_types[$first_var_name] = [[new IsType($class_string_type)]]; } } elseif ($class_exists_check_type = self::hasTraitExistsCheck($expr)) { @@ -821,14 +819,12 @@ public static function processFunctionCall( } } elseif (self::hasEnumExistsCheck($expr)) { if ($first_var_name) { - $class_string = new TClassString(); - $class_string->is_enum = true; + $class_string = new TClassString('object', null, false, false, true); $if_types[$first_var_name] = [[new IsType($class_string)]]; } } elseif (self::hasInterfaceExistsCheck($expr)) { if ($first_var_name) { - $class_string = new TClassString(); - $class_string->is_interface = true; + $class_string = new TClassString('object', null, false, true, false); $if_types[$first_var_name] = [[new IsType($class_string)]]; } } elseif (self::hasFunctionExistsCheck($expr)) { @@ -962,15 +958,15 @@ protected static function processCustomAssertion( foreach ($if_true_assertions as $assertion) { $if_types = []; - $assertion = clone $assertion; + $newRules = []; - foreach ($assertion->rule as $i => $rule) { + foreach ($assertion->rule as $rule) { $rule_type = $rule->getAtomicType(); if ($rule_type instanceof TClassConstant) { $codebase = $source->getCodebase(); - $assertion->rule[$i]->setAtomicType( + $newRules[] = $rule->setAtomicType( TypeExpander::expandAtomic( $codebase, $rule_type, @@ -979,9 +975,13 @@ protected static function processCustomAssertion( null )[0] ); + } else { + $newRules []= $rule; } } + $assertion = new Possibilities($assertion->var_id, $newRules); + if (is_int($assertion->var_id) && isset($expr->getArgs()[$assertion->var_id])) { if ($assertion->var_id === 0) { $var_name = $first_var_name; @@ -994,7 +994,7 @@ protected static function processCustomAssertion( } if ($var_name) { - $if_types[$var_name] = [[clone $assertion->rule[0]]]; + $if_types[$var_name] = [[$assertion->rule[0]]]; } } elseif ($assertion->var_id === '$this') { if (!$expr instanceof PhpParser\Node\Expr\MethodCall) { @@ -1014,7 +1014,7 @@ protected static function processCustomAssertion( ); if ($var_id) { - $if_types[$var_id] = [[clone $assertion->rule[0]]]; + $if_types[$var_id] = [[$assertion->rule[0]]]; } } elseif (is_string($assertion->var_id)) { $is_function = substr($assertion->var_id, -2) === '()'; @@ -1083,7 +1083,7 @@ protected static function processCustomAssertion( ); continue; } - $if_types[$assertion_var_id] = [[clone $assertion->rule[0]]]; + $if_types[$assertion_var_id] = [[$assertion->rule[0]]]; } if ($if_types) { @@ -1096,15 +1096,15 @@ protected static function processCustomAssertion( foreach ($if_false_assertions as $assertion) { $if_types = []; - $assertion = clone $assertion; + $newRules = []; - foreach ($assertion->rule as $i => $rule) { + foreach ($assertion->rule as $rule) { $rule_type = $rule->getAtomicType(); if ($rule_type instanceof TClassConstant) { $codebase = $source->getCodebase(); - $assertion->rule[$i]->setAtomicType( + $newRules []= $rule->setAtomicType( TypeExpander::expandAtomic( $codebase, $rule_type, @@ -1113,9 +1113,13 @@ protected static function processCustomAssertion( null )[0] ); + } else { + $newRules []= $rule; } } + $assertion = new Possibilities($assertion->var_id, $newRules); + if (is_int($assertion->var_id) && isset($expr->getArgs()[$assertion->var_id])) { if ($assertion->var_id === 0) { $var_name = $first_var_name; @@ -1128,7 +1132,7 @@ protected static function processCustomAssertion( } if ($var_name) { - $if_types[$var_name] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_name] = [[$assertion->rule[0]->getNegation()]]; } } elseif ($assertion->var_id === '$this' && $expr instanceof PhpParser\Node\Expr\MethodCall) { $var_id = ExpressionIdentifier::getExtendedVarId( @@ -1138,7 +1142,7 @@ protected static function processCustomAssertion( ); if ($var_id) { - $if_types[$var_id] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_id] = [[$assertion->rule[0]->getNegation()]]; } } elseif (is_string($assertion->var_id)) { $is_function = substr($assertion->var_id, -2) === '()'; @@ -1190,7 +1194,7 @@ protected static function processCustomAssertion( } } - $rule = clone $assertion->rule[0]->getNegation(); + $rule = $assertion->rule[0]->getNegation(); $assertion_var_id = str_replace($var_id, $arg_var_id, $assertion->var_id); @@ -1200,7 +1204,7 @@ protected static function processCustomAssertion( if (strpos($var_id, 'self::') === 0) { $var_id = $this_class_name.'::'.substr($var_id, 6); } - $if_types[$var_id] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_id] = [[$assertion->rule[0]->getNegation()]]; } else { IssueBuffer::maybeAdd( new InvalidDocblock( @@ -1245,10 +1249,10 @@ protected static function getInstanceOfAssertions( if ($this_class_name && (in_array(strtolower($stmt->class->parts[0]), ['self', 'static'], true))) { - $named_object =new TNamedObject($this_class_name); + $is_static = $stmt->class->parts[0] === 'static'; + $named_object = new TNamedObject($this_class_name, $is_static); - if ($stmt->class->parts[0] === 'static') { - $named_object->is_static = true; + if ($is_static) { return [new IsIdentical($named_object)]; } @@ -1893,6 +1897,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; @@ -3341,7 +3349,7 @@ private static function getGetclassEqualityAssertions( new IsIdentical(new TTemplateParam( $type_part->param_name, $type_part->as_type - ? new Union([clone $type_part->as_type]) + ? new Union([$type_part->as_type]) : Type::getObject(), $type_part->defining_class )) @@ -3529,8 +3537,7 @@ private static function getIsaAssertions( if ($class_node->parts === ['static']) { if ($this_class_name) { - $object = new TNamedObject($this_class_name); - $object->is_static = true; + $object = new TNamedObject($this_class_name, true); $if_types[$first_var_name] = [[new IsAClass($object, $third_arg_value === 'true')]]; } @@ -3724,8 +3731,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 +3763,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/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index c78f72ef44a..fecc1cda373 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -218,7 +218,9 @@ public static function updateArrayType( $new_child_type = $root_type; } + $new_child_type = $new_child_type->getBuilder(); $new_child_type->removeType('null'); + $new_child_type = $new_child_type->freeze(); if (!$root_type->hasObjectType()) { $root_type = $new_child_type; @@ -295,63 +297,74 @@ private static function updateTypeWithKeyValues( $has_matching_objectlike_property = false; $has_matching_string = false; - $child_stmt_type = clone $child_stmt_type; - + $changed = false; + $types = []; foreach ($child_stmt_type->getAtomicTypes() as $type) { + $old_type = $type; if ($type instanceof TTemplateParam) { - $type->as = self::updateTypeWithKeyValues( + $type = $type->replaceAs(self::updateTypeWithKeyValues( $codebase, $type->as, $current_type, $key_values - ); - + )); $has_matching_objectlike_property = true; - - $child_stmt_type->substitute(new Union([$type]), $type->as); - - continue; - } - - foreach ($key_values as $key_value) { - if ($type instanceof TKeyedArray) { - if (isset($type->properties[$key_value->value])) { + } elseif ($type instanceof TKeyedArray) { + $properties = $type->properties; + foreach ($key_values as $key_value) { + if (isset($properties[$key_value->value])) { $has_matching_objectlike_property = true; - $type->properties[$key_value->value] = clone $current_type; + $properties[$key_value->value] = clone $current_type; } - } elseif ($type instanceof TString - && $key_value instanceof TLiteralInt - ) { - $has_matching_string = true; - - if ($type instanceof TLiteralString - && $current_type->isSingleStringLiteral() - ) { - $new_char = $current_type->getSingleStringLiteral()->value; - - if (strlen($new_char) === 1) { - $type->value[0] = $new_char; + } + $type = $type->setProperties($properties); + } elseif ($type instanceof TString) { + foreach ($key_values as $key_value) { + if ($key_value instanceof TLiteralInt) { + $has_matching_string = true; + + if ($type instanceof TLiteralString + && $current_type->isSingleStringLiteral() + ) { + $new_char = $current_type->getSingleStringLiteral()->value; + + if (strlen($new_char) === 1 && $type->value[0] !== $new_char) { + $v = $type->value; + $v[0] = $new_char; + $changed = true; + $type = new TLiteralString($v); + break; + } } } - } elseif ($type instanceof TNonEmptyList - && $key_value instanceof TLiteralInt - && count($key_values) === 1 - ) { + } + } elseif ($type instanceof TNonEmptyList + && count($key_values) === 1 + && $key_values[0] instanceof TLiteralInt + ) { + $key_value = $key_values[0]; + $count = ($type->count ?? $type->min_count) ?? 1; + if ($key_value->value < $count) { $has_matching_objectlike_property = true; - $type->type_param = Type::combineUnionTypes( + $changed = true; + $type = $type->replaceTypeParam(Type::combineUnionTypes( clone $current_type, $type->type_param, $codebase, true, false - ); + )); } } + $types[$type->getKey()] = $type; + $changed = $changed || $old_type !== $type; } - $child_stmt_type->bustCache(); + if ($changed) { + $child_stmt_type = $child_stmt_type->getBuilder()->setTypes($types)->freeze(); + } if (!$has_matching_objectlike_property && !$has_matching_string) { if (count($key_values) === 1) { @@ -361,11 +374,10 @@ private static function updateTypeWithKeyValues( [$key_value->value => clone $current_type], $key_value instanceof TLiteralClassString ? [$key_value->value => true] - : null + : null, + true ); - $object_like->sealed = true; - $array_assignment_type = new Union([ $object_like, ]); @@ -511,9 +523,7 @@ private static function updateArrayAssignmentChildType( } } - $array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts( - $key_type - ); + $array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts($key_type); } else { $array_atomic_key_type = Type::getArrayKey(); } @@ -557,7 +567,7 @@ private static function updateArrayAssignmentChildType( ] ); - TemplateInferredTypeReplacer::replace( + $value_type = TemplateInferredTypeReplacer::replace( $value_type, $template_result, $codebase @@ -600,10 +610,12 @@ private static function updateArrayAssignmentChildType( } elseif ($atomic_root_types['array'] instanceof TNonEmptyArray || $atomic_root_types['array'] instanceof TNonEmptyList ) { + /** @psalm-suppress InaccessibleProperty We just created this object */ $array_atomic_type->count = $atomic_root_types['array']->count; } elseif ($atomic_root_types['array'] instanceof TKeyedArray && $atomic_root_types['array']->sealed ) { + /** @psalm-suppress InaccessibleProperty We just created this object */ $array_atomic_type->count = count($atomic_root_types['array']->properties); $from_countable_object_like = true; @@ -654,7 +666,9 @@ private static function updateArrayAssignmentChildType( || $atomic_root_types['array'] instanceof TNonEmptyList) && $atomic_root_types['array']->count !== null ) { - $atomic_root_types['array']->count++; + $atomic_root_types['array'] = + $atomic_root_types['array']->setCount($atomic_root_types['array']->count+1); + $new_child_type = new Union($atomic_root_types); } } @@ -742,17 +756,21 @@ private static function analyzeNestedArrayAssignment( $is_last = $i === count($child_stmts) - 1; + $child_stmt_dim_type_or_int = $child_stmt_dim_type ?? Type::getInt(); $child_stmt_type = ArrayFetchAnalyzer::getArrayAccessTypeGivenOffset( $statements_analyzer, $child_stmt, $array_type, - $child_stmt_dim_type ?? Type::getInt(), + $child_stmt_dim_type_or_int, true, $extended_var_id, $context, $assign_value, !$is_last ? null : $assignment_type ); + if ($child_stmt->dim) { + $statements_analyzer->node_data->setType($child_stmt->dim, $child_stmt_dim_type_or_int); + } $statements_analyzer->node_data->setType( $child_stmt, @@ -886,8 +904,10 @@ private static function analyzeNestedArrayAssignment( ); } + $new_child_type = $new_child_type->getBuilder(); $new_child_type->removeType('null'); $new_child_type->possibly_undefined = false; + $new_child_type = $new_child_type->freeze(); if (!$child_stmt_type->hasObjectType()) { $child_stmt_type = $new_child_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index cbbf381a410..ea9bf51f9ff 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -875,6 +875,8 @@ private static function analyzeRegularAssignment( /** * @param list $invalid_assignment_types + * + * @psalm-suppress ComplexMethod Unavoidably complex method */ private static function analyzeAtomicAssignment( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index c787b2443c9..da243bb3e9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -716,7 +716,7 @@ public static function assignTypeFromVarDocblock( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $var_comment_type->check( $statements_analyzer, @@ -1497,7 +1497,7 @@ private static function analyzeDestructuringAssignment( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $new_assign_type = $var_comment_type; break; @@ -1561,7 +1561,10 @@ private static function analyzeDestructuringAssignment( if (($context->error_suppressing && ($offset || $can_be_empty)) || $has_null ) { - $context->vars_in_scope[$list_var_id]->addType(new TNull); + $context->vars_in_scope[$list_var_id] = $context->vars_in_scope[$list_var_id] + ->getBuilder() + ->addType(new TNull) + ->freeze(); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 7a85011e61d..4e4668dd4ac 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -292,6 +292,8 @@ private static function getNumericalType($result): Union /** * @param string[] $invalid_left_messages * @param string[] $invalid_right_messages + * + * @psalm-suppress ComplexMethod Unavoidably complex method. */ private static function analyzeOperands( ?StatementsSource $statements_source, @@ -576,8 +578,11 @@ private static function analyzeOperands( } } - $new_keyed_array = new TKeyedArray($properties); - $new_keyed_array->sealed = $left_type_part->sealed && $right_type_part->sealed; + $new_keyed_array = new TKeyedArray( + $properties, + null, + $left_type_part->sealed && $right_type_part->sealed + ); $result_type_member = new Union([$new_keyed_array]); } else { $result_type_member = TypeCombiner::combine( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index a4f3924df52..67f0efb888f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -36,6 +36,7 @@ use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -189,15 +190,25 @@ public static function analyze( } if (!$literal_concat) { - $numeric_type = Type::getNumericString(); - $numeric_type->addType(new TInt()); - $numeric_type->addType(new TFloat()); + $numeric_type = new Union([ + new TNumericString, + new TInt, + new TFloat + ]); $left_is_numeric = UnionTypeComparator::isContainedBy( $codebase, $left_type, $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( @@ -212,8 +223,7 @@ public static function analyze( } } - $lowercase_type = clone $numeric_type; - $lowercase_type->addType(new TLowercaseString()); + $lowercase_type = $numeric_type->getBuilder()->addType(new TLowercaseString())->freeze(); $all_lowercase = UnionTypeComparator::isContainedBy( $codebase, @@ -225,19 +235,25 @@ public static function analyze( $lowercase_type ); - $non_empty_string = clone $numeric_type; - $non_empty_string->addType(new TNonEmptyString()); + $non_empty_string = $numeric_type->getBuilder()->addType(new TNonEmptyString())->freeze(); - $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 +262,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/BitwiseNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php index 878dc11eca8..0aae409e996 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -44,12 +44,13 @@ public static function analyze( $unacceptable_type = null; $has_valid_operand = false; + $stmt_expr_type = $stmt_expr_type->getBuilder(); foreach ($stmt_expr_type->getAtomicTypes() as $type_string => $type_part) { if ($type_part instanceof TInt || $type_part instanceof TString) { if ($type_part instanceof TLiteralInt) { - $type_part->value = ~$type_part->value; + $type_part = new TLiteralInt(~$type_part->value); } elseif ($type_part instanceof TLiteralString) { - $type_part->value = ~$type_part->value; + $type_part = new TLiteralString(~$type_part->value); } $acceptable_types[] = $type_part; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index fc3d6eb73fd..6a2078b02d9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -33,12 +33,11 @@ public static function analyze( $stmt_type = Type::getBool(); if ($expr_type) { if ($expr_type->isAlwaysTruthy()) { - $stmt_type = Type::getFalse(); + $stmt_type = Type::getFalse($expr_type->from_docblock); } elseif ($expr_type->isAlwaysFalsy()) { - $stmt_type = Type::getTrue(); + $stmt_type = Type::getTrue($expr_type->from_docblock); } - $stmt_type->from_docblock = $expr_type->from_docblock; $stmt_type->parent_nodes = $expr_type->parent_nodes; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index e87585606f5..5342ed14419 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -58,6 +58,7 @@ use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; use function count; @@ -180,7 +181,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 ), @@ -834,6 +835,7 @@ public static function verifyType( if ($param_type->hasCallableType() && $param_type->isSingle()) { // we do this replacement early because later we don't have access to the // $statements_analyzer, which is necessary to understand string function names + $input_type = $input_type->getBuilder(); foreach ($input_type->getAtomicTypes() as $key => $atomic_type) { if (!$atomic_type instanceof TLiteralString || InternalCallMapHandler::inCallMap($atomic_type->value) @@ -854,6 +856,7 @@ public static function verifyType( $input_type->addType($candidate_callable); } } + $input_type = $input_type->freeze(); } $union_comparison_results = new TypeComparisonResult(); @@ -974,7 +977,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 +988,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 +1001,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 +1023,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 +1034,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 +1044,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 ), @@ -1349,21 +1352,30 @@ private static function coerceValueAfterGatekeeperArgument( } if (!$input_type_changed && $param_type->from_docblock && !$input_type->hasMixed()) { - $input_type = clone $input_type; - + $types = $input_type->getAtomicTypes(); foreach ($param_type->getAtomicTypes() as $param_atomic_type) { if ($param_atomic_type instanceof TGenericObject) { - foreach ($input_type->getAtomicTypes() as $input_atomic_type) { + foreach ($types as &$input_atomic_type) { if ($input_atomic_type instanceof TGenericObject && $input_atomic_type->value === $param_atomic_type->value ) { + $new_type_params = []; foreach ($input_atomic_type->type_params as $i => $type_param) { if ($type_param->isNever() && isset($param_atomic_type->type_params[$i])) { $input_type_changed = true; - $input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i]; + $new_type_params[$i] = $param_atomic_type->type_params[$i]; } } + if ($new_type_params) { + $input_atomic_type = new TGenericObject( + $input_atomic_type->value, + [...$input_atomic_type->type_params, ...$new_type_params], + $input_atomic_type->remapped_params, + false, + $input_atomic_type->extra_types + ); + } } } } @@ -1372,6 +1384,8 @@ private static function coerceValueAfterGatekeeperArgument( if (!$input_type_changed) { return; } + + $input_type = new Union($types); } $var_id = ExpressionIdentifier::getVarId( @@ -1384,23 +1398,15 @@ private static function coerceValueAfterGatekeeperArgument( $was_cloned = false; if ($input_type->isNullable() && !$param_type->isNullable()) { - $input_type = clone $input_type; + $input_type = $input_type->getBuilder(); $was_cloned = true; $input_type->removeType('null'); + $input_type = $input_type->freeze(); } if ($input_type->getId() === $param_type->getId()) { if ($input_type->from_docblock) { - if (!$was_cloned) { - $was_cloned = true; - $input_type = clone $input_type; - } - - $input_type->from_docblock = false; - - foreach ($input_type->getAtomicTypes() as $atomic_type) { - $atomic_type->from_docblock = false; - } + $input_type = $input_type->setFromDocblock(false); } } elseif ($input_type->hasMixed() && $signature_param_type) { $was_cloned = true; @@ -1426,20 +1432,24 @@ private static function coerceValueAfterGatekeeperArgument( if ($unpack) { if ($unpacked_atomic_array instanceof TList) { - $unpacked_atomic_array = clone $unpacked_atomic_array; - $unpacked_atomic_array->type_param = $input_type; + $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParam($input_type); $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } elseif ($unpacked_atomic_array instanceof TArray) { - $unpacked_atomic_array = clone $unpacked_atomic_array; - $unpacked_atomic_array->type_params[1] = $input_type; + $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParams([ + $unpacked_atomic_array->type_params[0], + $input_type + ]); $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } elseif ($unpacked_atomic_array instanceof TKeyedArray && $unpacked_atomic_array->is_list ) { - $unpacked_atomic_array = $unpacked_atomic_array->getList(); - $unpacked_atomic_array->type_param = $input_type; + if ($unpacked_atomic_array->isNonEmpty()) { + $unpacked_atomic_array = new TNonEmptyList($input_type); + } else { + $unpacked_atomic_array = new TList($input_type); + } $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 68b90dc676d..a3d5f7f46ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -268,7 +268,7 @@ public static function analyze( if (null !== $inferred_arg_type && null !== $template_result && null !== $param && null !== $param->type) { $codebase = $statements_analyzer->getCodebase(); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->type, $template_result, $codebase, @@ -308,19 +308,6 @@ private static function handleArrayMapFilterArrayArg( ): void { $codebase = $statements_analyzer->getCodebase(); - $generic_param_type = new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TTemplateParam( - 'ArrayValue' . $argument_offset, - Type::getMixed(), - $method_id - ) - ]) - ]) - ]); - $template_types = ['ArrayValue' . $argument_offset => [$method_id => Type::getMixed()]]; $replace_template_result = new TemplateResult( @@ -330,8 +317,19 @@ private static function handleArrayMapFilterArrayArg( $existing_type = $statements_analyzer->node_data->getType($arg->value); - TemplateStandinTypeReplacer::replace( - $generic_param_type, + TemplateStandinTypeReplacer::fillTemplateResult( + new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TTemplateParam( + 'ArrayValue' . $argument_offset, + Type::getMixed(), + $method_id + ) + ]) + ]) + ]), $replace_template_result, $codebase, $statements_analyzer, @@ -494,7 +492,7 @@ private static function handleHighOrderFuncCallArg( // The map function expects callable(A):B as second param // We know that previous arg type is list where the int is the A template. // Then we can replace callable(A): B to callable(int):B using $inferred_template_result. - TemplateInferredTypeReplacer::replace( + $replaced_container_hof_atomic = TemplateInferredTypeReplacer::replace( $replaced_container_hof_atomic, $inferred_template_result, $codebase @@ -515,7 +513,7 @@ private static function handleHighOrderFuncCallArg( $actual_func_param->type->getTemplateTypes() && isset($container_hof_atomic->params[$offset]) ) { - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $actual_func_param->type, $high_order_template_result, $codebase, @@ -553,16 +551,18 @@ private static function handleClosureArg( $function_like_params = []; foreach ($template_result->lower_bounds as $template_name => $_) { + $t = new Union([ + new TTemplateParam( + $template_name, + Type::getMixed(), + $method_id + ) + ]); $function_like_params[] = new FunctionLikeParameter( 'function', false, - new Union([ - new TTemplateParam( - $template_name, - Type::getMixed(), - $method_id - ) - ]) + $t, + $t ); } @@ -601,7 +601,7 @@ private static function handleClosureArg( $context->calling_method_id ?: $context->calling_function_id ); - TemplateInferredTypeReplacer::replace( + $replaced_type = TemplateInferredTypeReplacer::replace( $replaced_type, $replace_template_result, $codebase @@ -726,7 +726,7 @@ public static function checkArgumentsMatch( $codebase = $statements_analyzer->getCodebase(); if ($method_id) { - if (!$in_call_map && $method_id instanceof MethodIdentifier) { + if ($method_id instanceof MethodIdentifier) { $fq_class_name = $method_id->fq_class_name; } @@ -769,7 +769,7 @@ public static function checkArgumentsMatch( } } - if ($function_params) { + if ($function_params && !$is_variadic) { foreach ($function_params as $function_param) { $is_variadic = $is_variadic || $function_param->is_variadic; } @@ -897,7 +897,8 @@ public static function checkArgumentsMatch( $array_type = $arg_value_type->getAtomicTypes()['array']; if ($array_type instanceof TKeyedArray) { - $key_types = $array_type->getGenericArrayType()->getChildNodes()[0]->getChildNodes(); + $array_type = $array_type->getGenericArrayType(); + $key_types = $array_type->type_params[0]->getAtomicTypes(); foreach ($key_types as $key_type) { if (!$key_type instanceof TLiteralString @@ -1234,7 +1235,7 @@ private static function handlePossiblyMatchingByRefParam( ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_type = TemplateInferredTypeReplacer::replace( $original_by_ref_type, $template_result, $codebase @@ -1259,7 +1260,7 @@ private static function handlePossiblyMatchingByRefParam( ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_out_type = TemplateInferredTypeReplacer::replace( $original_by_ref_out_type, $template_result, $codebase @@ -1386,16 +1387,18 @@ private static function evaluateArbitraryParam( $statements_analyzer ); - foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $type) { + $t = $context->vars_in_scope[$var_id]->getBuilder(); + foreach ($t->getAtomicTypes() as $type) { if ($type instanceof TArray && $type->isEmptyArray()) { - $context->vars_in_scope[$var_id]->removeType('array'); - $context->vars_in_scope[$var_id]->addType( + $t->removeType('array'); + $t->addType( new TArray( [Type::getArrayKey(), Type::getMixed()] ) ); } } + $context->vars_in_scope[$var_id] = $t->freeze(); } } @@ -1614,7 +1617,7 @@ private static function getProvisionalTemplateResultForFunctionLike( $calling_class_storage->final ?? false ); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $fleshed_out_param_type, $template_result, $codebase, @@ -1794,7 +1797,7 @@ private static function checkArgCount( $default_type = new Union([$default_type_atomic]); } - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->type, $template_result, $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index a34f9e6d030..87174eab776 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; @@ -35,7 +36,6 @@ use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; -use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; @@ -115,6 +115,7 @@ public static function checkArgumentsMatch( $max_closure_param_count = count($args) > 2 ? 2 : 1; } + $new = []; foreach ($closure_arg_type->getAtomicTypes() as $closure_type) { self::checkClosureType( $statements_analyzer, @@ -127,7 +128,13 @@ public static function checkArgumentsMatch( $array_arg_types, $check_functions ); + $new []= $closure_type; } + + $statements_analyzer->node_data->setType( + $closure_arg->value, + $closure_arg_type->getBuilder()->setTypes($new)->freeze() + ); } } @@ -266,7 +273,7 @@ public static function handleAddition( new Union([new TArray([$new_offset_type, Type::getMixed()])]) ); } elseif ($arg->unpack) { - $arg_value_type = clone $arg_value_type; + $arg_value_type = $arg_value_type->getBuilder(); foreach ($arg_value_type->getAtomicTypes() as $arg_value_atomic_type) { if ($arg_value_atomic_type instanceof TKeyedArray) { @@ -285,6 +292,7 @@ public static function handleAddition( $arg_value_type->addType($arg_value_atomic_type); } } + $arg_value_type = $arg_value_type->freeze(); $by_ref_type = Type::combineUnionTypes( $by_ref_type, @@ -292,9 +300,10 @@ public static function handleAddition( ); } else { if ($objectlike_list) { - array_unshift($objectlike_list->properties, $arg_value_type); + $properties = $objectlike_list->properties; + array_unshift($properties, $arg_value_type); - $by_ref_type = new Union([$objectlike_list]); + $by_ref_type = new Union([$objectlike_list->setProperties($properties)]); } elseif ($array_type instanceof TList) { $by_ref_type = Type::combineUnionTypes( $by_ref_type, @@ -508,33 +517,24 @@ public static function handleByRefArrayAdjustment( $context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer); if (isset($context->vars_in_scope[$var_id])) { - $array_type = clone $context->vars_in_scope[$var_id]; + $array_atomic_types = []; - $array_atomic_types = $array_type->getAtomicTypes(); - - foreach ($array_atomic_types as $array_atomic_type) { + foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $array_atomic_type) { if ($array_atomic_type instanceof TKeyedArray) { if ($is_array_shift && $array_atomic_type->is_list) { - $array_atomic_type = clone $array_atomic_type; - $array_properties = $array_atomic_type->properties; array_shift($array_properties); if (!$array_properties) { - $array_atomic_type = new TList( - $array_atomic_type->previous_value_type ?: Type::getMixed() - ); - - $array_type->addType($array_atomic_type); + $array_atomic_types []= new TList(Type::getNever()); } else { - $array_atomic_type->properties = $array_properties; + $array_atomic_types []= $array_atomic_type->setProperties($array_properties); } + continue; } - if ($array_atomic_type instanceof TKeyedArray) { - $array_atomic_type = $array_atomic_type->getGenericArrayType(); - } + $array_atomic_type = $array_atomic_type->getGenericArrayType(); } if ($array_atomic_type instanceof TNonEmptyArray) { @@ -542,38 +542,39 @@ public static function handleByRefArrayAdjustment( if ($array_atomic_type->count === 1) { $array_atomic_type = new TArray( [ - new Union([new TNever]), - new Union([new TNever]), + Type::getNever(), + Type::getNever(), ] ); } else { - $array_atomic_type->count--; + $array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1); } } else { $array_atomic_type = new TArray($array_atomic_type->type_params); } - $array_type->addType($array_atomic_type); + $array_atomic_types[] = $array_atomic_type; } elseif ($array_atomic_type instanceof TNonEmptyList) { if (!$context->inside_loop && $array_atomic_type->count !== null) { if ($array_atomic_type->count === 1) { - $array_atomic_type = new TArray( - [ - new Union([new TNever]), - new Union([new TNever]), - ] - ); + $array_atomic_type = new TList(Type::getNever()); } else { - $array_atomic_type->count--; + $array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1); } } else { $array_atomic_type = new TList($array_atomic_type->type_param); } - $array_type->addType($array_atomic_type); + $array_atomic_types[] = $array_atomic_type; + } else { + $array_atomic_types[] = $array_atomic_type; } } + if (!$array_atomic_types) { + throw new AssertionError("We must have some types here!"); + } + $array_type = new Union($array_atomic_types); $context->removeDescendents($var_id, $array_type); $context->vars_in_scope[$var_id] = $array_type; } @@ -582,13 +583,12 @@ public static function handleByRefArrayAdjustment( /** * @param (TArray|null)[] $array_arg_types - * */ private static function checkClosureType( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -724,10 +724,10 @@ private static function checkClosureType( } } } else { - $closure_types = [$closure_type]; + $closure_types = [&$closure_type]; } - foreach ($closure_types as $closure_type) { + foreach ($closure_types as &$closure_type) { if ($closure_type->params === null) { continue; } @@ -753,7 +753,7 @@ private static function checkClosureTypeArgs( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -836,9 +836,6 @@ private static function checkClosureTypeArgs( && $closure_type->return_type && $closure_param_type->hasTemplate() ) { - $closure_param_type = clone $closure_param_type; - $closure_type->return_type = clone $closure_type->return_type; - $template_result = new TemplateResult( [], [] @@ -861,7 +858,7 @@ private static function checkClosureTypeArgs( $context->calling_method_id ?: $context->calling_function_id ); - $closure_type->replaceTemplateTypesWithArgTypes( + $closure_type = $closure_type->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); @@ -891,7 +888,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 +899,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 +920,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 +930,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 +940,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/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index d498beff791..30022eb2a82 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -240,8 +240,7 @@ private static function resolveTemplateParam( } } else { if ($template_result !== null) { - $type_extends_atomic = clone $type_extends_atomic; - $type_extends_atomic->replaceTemplateTypesWithArgTypes( + $type_extends_atomic = $type_extends_atomic->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 96af6145b5d..a86f701f2f7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -564,7 +564,7 @@ private static function handleNamedFunction( } } catch (UnexpectedValueException $e) { $function_call_info->function_params = [ - new FunctionLikeParameter('args', false, null, null, null, false, false, true) + new FunctionLikeParameter('args', false, null, null, null, null, false, false, true) ]; } } else { @@ -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/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index ed207aded41..a084fb7380a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -41,6 +41,7 @@ use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; use UnexpectedValueException; @@ -172,7 +173,7 @@ public static function fetch( null ); - TemplateInferredTypeReplacer::replace( + $return_type = TemplateInferredTypeReplacer::replace( $return_type, $template_result, $codebase @@ -327,9 +328,7 @@ private static function getReturnTypeFromCallMapWithArgs( $keyed_array = new TKeyedArray([ Type::getInt(), Type::getInt() - ]); - $keyed_array->sealed = true; - $keyed_array->is_list = true; + ], null, true, null, null, true); return new Union([$keyed_array]); case 'get_called_class': @@ -440,9 +439,7 @@ private static function getReturnTypeFromCallMapWithArgs( $keyed_array = new TKeyedArray([ Type::getInt(), Type::getInt() - ]); - $keyed_array->sealed = true; - $keyed_array->is_list = true; + ], null, true, null, null, true); if ((string) $first_arg_type === 'false') { return new Union([$keyed_array]); @@ -501,8 +498,10 @@ private static function getReturnTypeFromCallMapWithArgs( break; case 'fgetcsv': - $string_type = Type::getString(); - $string_type->addType(new TNull); + $string_type = new Union([ + new TString, + new TNull + ]); $string_type->ignore_nullable_issues = true; $call_map_return_type = new Union([ @@ -609,10 +608,8 @@ private static function taintReturnType( $conditionally_removed_taints = []; foreach ($function_storage->conditionally_removed_taints as $conditionally_removed_taint) { - $conditionally_removed_taint = clone $conditionally_removed_taint; - - TemplateInferredTypeReplacer::replace( - $conditionally_removed_taint, + $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( + clone $conditionally_removed_taint, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 1c9edaf7607..70ecf984149 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -89,15 +89,15 @@ public static function analyze( $lhs_type_part->as->getAtomicTypes() )[0]; - $lhs_type_part->from_docblock = true; - if ($lhs_type_part instanceof TNamedObject) { - $lhs_type_part->extra_types = $extra_types; + $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types)->setFromDocblock(true); } elseif ($lhs_type_part instanceof TObject && $extra_types) { - $lhs_type_part = array_shift($extra_types); + $lhs_type_part = array_shift($extra_types)->setFromDocblock(true); if ($extra_types) { - $lhs_type_part->extra_types = $extra_types; + $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types); } + } else { + $lhs_type_part = $lhs_type_part->setFromDocblock(true); } $result->has_mixed_method_call = true; @@ -849,9 +849,7 @@ private static function handleRegularMixins( $lhs_var_id === '$this' ); - $lhs_type_part = clone $mixin; - - $lhs_type_part->replaceTemplateTypesWithArgTypes( + $lhs_type_part = $mixin->replaceTemplateTypesWithArgTypes( new TemplateResult([], $mixin_class_template_params ?: []), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index b16fadae07a..795b6b1c02c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -207,7 +207,7 @@ public static function analyze( if ($method_storage && $method_storage->if_this_is_type) { $method_template_result = new TemplateResult($method_storage->template_types ?: [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $method_storage->if_this_is_type, $method_template_result, $codebase, @@ -300,9 +300,11 @@ public static function analyze( if ($method_storage) { if ($method_storage->if_this_is_type) { $class_type = new Union([$lhs_type_part]); - $if_this_is_type = clone $method_storage->if_this_is_type; - - TemplateInferredTypeReplacer::replace($if_this_is_type, $template_result, $codebase); + $if_this_is_type = TemplateInferredTypeReplacer::replace( + clone $method_storage->if_this_is_type, + $template_result, + $codebase + ); if (!UnionTypeComparator::isContainedBy($codebase, $class_type, $if_this_is_type)) { IssueBuffer::maybeAdd( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index 2ed17b4bc67..d62ee2d694f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -618,7 +618,7 @@ public static function replaceTemplateTypes( null ); - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, $template_result, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 086a8d255dd..ac2346d95ee 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -149,7 +149,7 @@ public static function handleMagicMethod( $return_type_candidate = clone $pseudo_method_storage->return_type; if ($found_generic_params) { - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, new TemplateResult([], $found_generic_params), $codebase @@ -315,7 +315,7 @@ public static function handleMissingOrMagicMethod( $return_type_candidate = clone $pseudo_method_storage->return_type; if ($found_generic_params) { - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, new TemplateResult([], $found_generic_params), $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index a755115db7f..864b353b725 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; @@ -398,27 +399,24 @@ public static function analyze( && ($class_type->from_docblock || $class_type->isNullable()) && $real_method_call ) { - $keys_to_remove = []; + $types = $class_type->getAtomicTypes(); - $class_type = clone $class_type; - - foreach ($class_type->getAtomicTypes() as $key => $type) { + foreach ($types as $key => &$type) { if (!$type instanceof TNamedObject) { - $keys_to_remove[] = $key; + unset($types[$key]); } else { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } } - - foreach ($keys_to_remove as $key) { - $class_type->removeType($key); + if (!$types) { + throw new AssertionError("We must have some types here!"); } - $class_type->from_docblock = false; - $context->removeVarFromConflictingClauses($lhs_var_id, null, $statements_analyzer); - $context->vars_in_scope[$lhs_var_id] = $class_type; + $class_type = $class_type->getBuilder()->setTypes($types); + $class_type->from_docblock = false; + $context->vars_in_scope[$lhs_var_id] = $class_type->freeze(); } return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 324861eada9..d15daac482f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -525,11 +525,11 @@ private static function analyzeNamedConstructor( if ($generic_param_types) { $result_atomic_type = new TGenericObject( $fq_class_name, - $generic_param_types + $generic_param_types, + false, + $from_static ); - $result_atomic_type->is_static = $from_static; - $statements_analyzer->node_data->setType( $stmt, new Union([$result_atomic_type]) @@ -552,11 +552,11 @@ private static function analyzeNamedConstructor( static fn($map) => clone reset($map), $storage->template_types ) - ) + ), + false, + $from_static ); - $result_atomic_type->is_static = $from_static; - $statements_analyzer->node_data->setType( $stmt, new Union([$result_atomic_type]) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 772ad322c0c..e1754c01c93 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -298,10 +298,8 @@ public static function taintReturnType( if ($method_storage && $template_result) { foreach ($method_storage->conditionally_removed_taints as $conditionally_removed_taint) { - $conditionally_removed_taint = clone $conditionally_removed_taint; - - TemplateInferredTypeReplacer::replace( - $conditionally_removed_taint, + $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( + clone $conditionally_removed_taint, $template_result, $codebase ); 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..c1afc0fa770 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; - } + ) ?? false; } - 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; - } - - $args = $stmt->getArgs(); + $args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs(); if (!$naive_method_exists && $class_storage->mixin_declaring_fqcln @@ -469,13 +446,13 @@ private static function handleNamedCall( $tGenericMixin, $class_storage, $mixin_declaring_class_storage - ); + )->getBuilder(); foreach ($mixin_candidate_type->getAtomicTypes() as $type) { $new_mixin_candidate_type->addType($type); } - $mixin_candidate_type = $new_mixin_candidate_type; + $mixin_candidate_type = $new_mixin_candidate_type->freeze(); } $new_lhs_type = TypeExpander::expandUnion( @@ -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, @@ -720,7 +744,7 @@ private static function handleNamedCall( if (isset($context->vars_in_scope['$this']) && $method_call_type = $statements_analyzer->node_data->getType($stmt) ) { - $method_call_type = clone $method_call_type; + $method_call_type = $method_call_type->getBuilder(); foreach ($method_call_type->getAtomicTypes() as $name => $type) { if ($type instanceof TNamedObject && $type->is_static && $type->value === $fq_class_name) { @@ -730,7 +754,7 @@ private static function handleNamedCall( } } - $statements_analyzer->node_data->setType($stmt, $method_call_type); + $statements_analyzer->node_data->setType($stmt, $method_call_type->freeze()); } return true; 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..8cdfd6c7560 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -21,6 +21,7 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; +use Psalm\Internal\TypeVisitor\ContainsStaticVisitor; use Psalm\Issue\AbstractMethodCall; use Psalm\Issue\ImpureMethodCall; use Psalm\IssueBuffer; @@ -554,6 +555,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; } @@ -567,7 +576,7 @@ private static function getMethodReturnType( null ); - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, $template_result, $codebase @@ -616,4 +625,14 @@ 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 + { + $visitor = new ContainsStaticVisitor; + $visitor->traverse($type); + return $visitor->matches(); + } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index ade1205332c..a3f34607a6b 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 @@ -578,16 +578,14 @@ public static function getFunctionIdsFromCallableArg( if ($type_part instanceof TNamedObject) { $method_id = $type_part->value . '::' . $method_name_arg->value; - if ($type_part->extra_types) { - foreach ($type_part->extra_types as $extra_type) { - if ($extra_type instanceof TTemplateParam - || $extra_type instanceof TObjectWithProperties - ) { - throw new UnexpectedValueException('Shouldn’t get a generic param here'); - } - - $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value; + foreach ($type_part->extra_types as $extra_type) { + if ($extra_type instanceof TTemplateParam + || $extra_type instanceof TObjectWithProperties + ) { + throw new UnexpectedValueException('Shouldn’t get a generic param here'); } + + $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value; } $method_ids[] = '$' . $method_id; @@ -756,9 +754,8 @@ public static function applyAssertionsToContext( $assertion_type_atomic = $assertion_rule->getAtomicType(); if ($assertion_type_atomic) { - $assertion_type = new Union([clone $assertion_type_atomic]); - TemplateInferredTypeReplacer::replace( - $assertion_type, + $assertion_type = TemplateInferredTypeReplacer::replace( + new Union([clone $assertion_type_atomic]), $template_result, $codebase ); @@ -771,8 +768,7 @@ public static function applyAssertionsToContext( continue; } - $assertion_rule = clone $assertion_rule; - $assertion_rule->setAtomicType($atomic_type); + $assertion_rule = $assertion_rule->setAtomicType($atomic_type); $orred_rules[] = $assertion_rule; } } elseif (isset($context->vars_in_scope[$assertion_var_id])) { @@ -940,19 +936,7 @@ public static function applyAssertionsToContext( ); } - $op_vars_in_scope[$var_id]->from_docblock = true; - - foreach ($op_vars_in_scope[$var_id]->getAtomicTypes() as $changed_atomic_type) { - $changed_atomic_type->from_docblock = true; - - if ($changed_atomic_type instanceof TNamedObject - && $changed_atomic_type->extra_types - ) { - foreach ($changed_atomic_type->extra_types as $extra_type) { - $extra_type->from_docblock = true; - } - } - } + $op_vars_in_scope[$var_id] = $op_vars_in_scope[$var_id]->setFromDocblock(true); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 48795d26f34..a435018739f 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); @@ -253,9 +244,7 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type) { if ($type instanceof Scalar) { - $keyed_array = new TKeyedArray([new Union([$type])]); - $keyed_array->is_list = true; - $keyed_array->sealed = true; + $keyed_array = new TKeyedArray([new Union([$type])], null, true, null, null, true); $permissible_atomic_types[] = $keyed_array; } elseif ($type instanceof TNull) { $permissible_atomic_types[] = new TArray([Type::getNever(), Type::getNever()]); @@ -309,6 +298,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 +723,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/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 923829c562c..8360fb5ead0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -169,8 +169,7 @@ public static function analyzeFetch( } if ($first_part_lc === 'static') { - $static_named_object = new TNamedObject($fq_class_name); - $static_named_object->is_static = true; + $static_named_object = new TNamedObject($fq_class_name, true); $statements_analyzer->node_data->setType( $stmt, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 57bfbdce1f4..5b45cdc1d74 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -80,6 +80,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Atomic\TTrue; +use Psalm\Type\MutableUnion; use Psalm\Type\Union; use UnexpectedValueException; @@ -271,7 +272,7 @@ public static function analyze( && !$const_array_key_type->hasMixed() && !$stmt_dim_type->hasMixed() ) { - $new_offset_type = clone $stmt_dim_type; + $new_offset_type = $stmt_dim_type->getBuilder(); $const_array_key_atomic_types = $const_array_key_type->getAtomicTypes(); foreach ($new_offset_type->getAtomicTypes() as $offset_key => $offset_atomic_type) { @@ -295,6 +296,8 @@ public static function analyze( $new_offset_type->removeType($offset_key); } } + + $new_offset_type = $new_offset_type->freeze(); } } } @@ -456,14 +459,16 @@ public static function taintArrayFetch( public static function getArrayAccessTypeGivenOffset( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $array_type, - Union $offset_type, + Union &$array_type, + Union &$offset_type_original, bool $in_assignment, ?string $extended_var_id, Context $context, PhpParser\Node\Expr $assign_value = null, Union $replacement_type = null ): Union { + $offset_type = $offset_type_original->getBuilder(); + $codebase = $statements_analyzer->getCodebase(); $has_array_access = false; @@ -549,7 +554,10 @@ public static function getArrayAccessTypeGivenOffset( $has_valid_absolute_offset = true; } - foreach ($array_type->getAtomicTypes() as $type_string => $type) { + $types = $array_type->getAtomicTypes(); + $changed = false; + foreach ($types as $type_string => $type) { + $original_type_real = $type; $original_type = $type; if ($type instanceof TMixed @@ -574,6 +582,7 @@ public static function getArrayAccessTypeGivenOffset( } $type = clone $type->as->getSingleAtomic(); + $original_type = $type; } if ($type instanceof TNull) { @@ -623,12 +632,11 @@ public static function getArrayAccessTypeGivenOffset( $in_assignment, $type, $key_values, - $array_type, - $type_string, + $array_type->hasMixed(), $stmt, $replacement_type, $offset_type, - $original_type, + $original_type_real, $codebase, $extended_var_id, $context, @@ -639,6 +647,12 @@ public static function getArrayAccessTypeGivenOffset( $has_valid_expected_offset ); + if ($type !== $original_type) { + $changed = true; + unset($types[$type_string]); + $types[$type->getKey()] = $type; + } + continue; } @@ -689,6 +703,9 @@ public static function getArrayAccessTypeGivenOffset( $non_array_types[] = (string)$type; } } + if ($changed) { + $array_type = $array_type->getBuilder()->setTypes($types)->freeze(); + } if ($non_array_types) { if ($has_array_access) { @@ -847,6 +864,8 @@ public static function getArrayAccessTypeGivenOffset( } } + $offset_type_original = $offset_type->freeze(); + if ($array_access_type === null) { // shouldn’t happen, but don’t crash return Type::getMixed(); @@ -856,15 +875,11 @@ public static function getArrayAccessTypeGivenOffset( $array_access_type->by_ref = true; } - if ($in_assignment) { - $array_type->bustCache(); - } - return $array_access_type; } private static function checkLiteralIntArrayOffset( - Union $offset_type, + MutableUnion $offset_type, Union $expected_offset_type, ?string $extended_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, @@ -912,7 +927,7 @@ private static function checkLiteralIntArrayOffset( } private static function checkLiteralStringArrayOffset( - Union $offset_type, + MutableUnion $offset_type, Union $expected_offset_type, ?string $extended_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, @@ -961,26 +976,16 @@ private static function checkLiteralStringArrayOffset( public static function replaceOffsetTypeWithInts(Union $offset_type): Union { + $offset_type = $offset_type->getBuilder(); $offset_types = $offset_type->getAtomicTypes(); - $cloned = false; - foreach ($offset_types as $key => $offset_type_part) { if ($offset_type_part instanceof TLiteralString) { if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) { - if (!$cloned) { - $offset_type = clone $offset_type; - $cloned = true; - } $offset_type->addType(new TLiteralInt((int) $offset_type_part->value)); $offset_type->removeType($key); } } elseif ($offset_type_part instanceof TBool) { - if (!$cloned) { - $offset_type = clone $offset_type; - $cloned = true; - } - if ($offset_type_part instanceof TFalse) { if (!$offset_type->ignore_falsable_issues) { $offset_type->addType(new TLiteralInt(0)); @@ -997,7 +1002,7 @@ public static function replaceOffsetTypeWithInts(Union $offset_type): Union } } - return $offset_type; + return $offset_type->freeze(); } /** @@ -1079,17 +1084,19 @@ public static function handleMixedArrayAccess( /** * @param list $expected_offset_types * @param TArray|TKeyedArray|TList|TClassStringMap $type + * @param-out TArray|TKeyedArray|TList|TClassStringMap $type * @param list $key_values + * + * @psalm-suppress ConflictingReferenceConstraint Ignore */ private static function handleArrayAccessOnArray( bool $in_assignment, Atomic &$type, array &$key_values, - Union $array_type, - string $type_string, + bool $hasMixed, PhpParser\Node\Expr\ArrayDimFetch $stmt, ?Union $replacement_type, - Union &$offset_type, + MutableUnion $offset_type, Atomic $original_type, Codebase $codebase, ?string $extended_var_id, @@ -1113,25 +1120,19 @@ private static function handleArrayAccessOnArray( [$previous_key_type, $previous_value_type] = $type->type_params; // ok, type becomes an TKeyedArray - $array_type->removeType($type_string); - $type = new TKeyedArray([ - $single_atomic->value => $from_mixed_array ? Type::getMixed() : Type::getNever() - ]); - if ($single_atomic instanceof TLiteralClassString) { - $type->class_strings[$single_atomic->value] = true; - } - - $type->sealed = $from_empty_array; - - if (!$from_empty_array) { - $type->previous_value_type = clone $previous_value_type; - $type->previous_key_type = clone $previous_key_type; - } - - $array_type->addType($type); + $type = new TKeyedArray( + [ + $single_atomic->value => $from_mixed_array ? Type::getMixed() : Type::getNever(), + ], + $single_atomic instanceof TLiteralClassString ? [ + $single_atomic->value => true + ] : null, + $from_empty_array, + $from_empty_array ? null : $previous_key_type, + $from_empty_array ? null : $previous_value_type, + ); } elseif (!$stmt->dim && $from_empty_array && $replacement_type) { - $array_type->removeType($type_string); - $array_type->addType(new TNonEmptyList($replacement_type)); + $type = new TNonEmptyList($replacement_type); return; } } elseif ($type instanceof TKeyedArray @@ -1139,27 +1140,42 @@ private static function handleArrayAccessOnArray( && $type->previous_value_type->isMixed() && count($key_values) === 1 ) { - $type->properties[$key_values[0]->value] = Type::getMixed(); + $properties = $type->properties; + $properties[$key_values[0]->value] = Type::getMixed(); + $type = $type->setProperties($properties); } } - $offset_type = self::replaceOffsetTypeWithInts($offset_type); + $offset_type = self::replaceOffsetTypeWithInts($offset_type->freeze())->getBuilder(); if ($type instanceof TList && (($in_assignment && $stmt->dim) || $original_type instanceof TTemplateParam || !$offset_type->isInt()) ) { - $type = new TArray([Type::getInt(), $type->type_param]); - } - - if ($type instanceof TArray) { + $temp = new TArray([Type::getInt(), $type->type_param]); self::handleArrayAccessOnTArray( $statements_analyzer, $codebase, $context, $stmt, - $array_type, + $hasMixed, + $extended_var_id, + $temp, + $offset_type, + $in_assignment, + $expected_offset_types, + $array_access_type, + $original_type, + $has_valid_offset + ); + } elseif ($type instanceof TArray) { + self::handleArrayAccessOnTArray( + $statements_analyzer, + $codebase, + $context, + $stmt, + $hasMixed, $extended_var_id, $type, $offset_type, @@ -1206,9 +1222,8 @@ private static function handleArrayAccessOnArray( $extended_var_id, $context, $type, - $array_type, + $hasMixed, $expected_offset_types, - $type_string, $has_valid_offset ); } @@ -1220,16 +1235,17 @@ private static function handleArrayAccessOnArray( /** * @param list $expected_offset_types + * @param-out TArray $type */ private static function handleArrayAccessOnTArray( StatementsAnalyzer $statements_analyzer, Codebase $codebase, Context $context, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $array_type, + bool $hasMixed, ?string $extended_var_id, - TArray $type, - Union $offset_type, + TArray &$type, + MutableUnion $offset_type, bool $in_assignment, array &$expected_offset_types, ?Union &$array_access_type, @@ -1239,9 +1255,12 @@ private static function handleArrayAccessOnTArray( // if we're assigning to an empty array with a key offset, refashion that array if ($in_assignment) { if ($type->isEmptyArray()) { - $type->type_params[0] = $offset_type->isMixed() - ? Type::getArrayKey() - : $offset_type; + $type = $type->replaceTypeParams([ + $offset_type->isMixed() + ? Type::getArrayKey() + : $offset_type->freeze(), + $type->type_params[1] + ]); } } elseif (!$type->isEmptyArray()) { $expected_offset_type = $type->type_params[0]->hasMixed() @@ -1264,12 +1283,15 @@ private static function handleArrayAccessOnTArray( && $offset_as->param_name === $original_type->param_name && $offset_as->defining_class === $original_type->defining_class ) { - $type->type_params[1] = new Union([ - new TTemplateIndexedAccess( - $offset_as->param_name, - $templated_offset_type->param_name, - $offset_as->defining_class - ) + $type = $type->replaceTypeParams([ + $type->type_params[0], + new Union([ + new TTemplateIndexedAccess( + $offset_as->param_name, + $templated_offset_type->param_name, + $offset_as->defining_class + ) + ]) ]); $has_valid_offset = true; @@ -1278,7 +1300,7 @@ private static function handleArrayAccessOnTArray( } else { $offset_type_contained_by_expected = UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type, true, $offset_type->ignore_falsable_issues, @@ -1336,7 +1358,7 @@ private static function handleArrayAccessOnTArray( if (UnionTypeComparator::canExpressionTypesBeIdentical( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type )) { $has_valid_offset = true; @@ -1348,7 +1370,7 @@ private static function handleArrayAccessOnTArray( } if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) { - $type->count++; + $type = $type->setCount($type->count+1); } $array_access_type = Type::combineUnionTypes( @@ -1357,7 +1379,7 @@ private static function handleArrayAccessOnTArray( ); if ($array_access_type->isNever() - && !$array_type->hasMixed() + && !$hasMixed && !$in_assignment && !$context->inside_isset ) { @@ -1377,8 +1399,8 @@ private static function handleArrayAccessOnTArray( private static function handleArrayAccessOnClassStringMap( Codebase $codebase, - TClassStringMap $type, - Union $offset_type, + TClassStringMap &$type, + MutableUnion $offset_type, ?Union $replacement_type, ?Union &$array_access_type ): void { @@ -1438,27 +1460,27 @@ private static function handleArrayAccessOnClassStringMap( ); } - $expected_value_param_get = clone $type->value_param; - - TemplateInferredTypeReplacer::replace( - $expected_value_param_get, + $expected_value_param_get = TemplateInferredTypeReplacer::replace( + $type->value_param, $template_result_get, $codebase ); if ($replacement_type) { - $expected_value_param_set = clone $type->value_param; - - TemplateInferredTypeReplacer::replace( + $replacement_type = TemplateInferredTypeReplacer::replace( $replacement_type, $template_result_set, $codebase ); - $type->value_param = Type::combineUnionTypes( - $replacement_type, - $expected_value_param_set, - $codebase + $type = new TClassStringMap( + $type->param_name, + $type->as_type, + Type::combineUnionTypes( + $replacement_type, + $type->value_param, + $codebase + ) ); } @@ -1474,6 +1496,7 @@ private static function handleArrayAccessOnClassStringMap( /** * @param list $expected_offset_types * @param list $key_values + * @param-out TArray|TKeyedArray|TList $type */ private static function handleArrayAccessOnKeyedArray( StatementsAnalyzer $statements_analyzer, @@ -1483,13 +1506,12 @@ private static function handleArrayAccessOnKeyedArray( ?Union &$array_access_type, bool $in_assignment, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $offset_type, + MutableUnion $offset_type, ?string $extended_var_id, Context $context, - TKeyedArray $type, - Union $array_type, + TKeyedArray &$type, + bool $hasMixed, array &$expected_offset_types, - string $type_string, bool &$has_valid_offset ): void { $generic_key_type = $type->getGenericKeyType(); @@ -1499,27 +1521,28 @@ private static function handleArrayAccessOnKeyedArray( } if ($key_values) { + $properties = $type->properties; foreach ($key_values as $key_value) { - if (isset($type->properties[$key_value->value]) || $replacement_type) { + if (isset($properties[$key_value->value]) || $replacement_type) { $has_valid_offset = true; if ($replacement_type) { - $type->properties[$key_value->value] = Type::combineUnionTypes( - $type->properties[$key_value->value] ?? null, + $properties[$key_value->value] = Type::combineUnionTypes( + $properties[$key_value->value] ?? null, $replacement_type ); } $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $type->properties[$key_value->value] + clone $properties[$key_value->value] ); } elseif ($in_assignment) { - $type->properties[$key_value->value] = new Union([new TNever]); + $properties[$key_value->value] = new Union([new TNever]); $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $type->properties[$key_value->value] + clone $properties[$key_value->value] ); } elseif ($type->previous_value_type) { if ($codebase->config->ensure_array_string_offsets_exist) { @@ -1544,16 +1567,16 @@ private static function handleArrayAccessOnKeyedArray( ); } - $type->properties[$key_value->value] = clone $type->previous_value_type; + $properties[$key_value->value] = clone $type->previous_value_type; $array_access_type = clone $type->previous_value_type; - } elseif ($array_type->hasMixed()) { + } elseif ($hasMixed) { $has_valid_offset = true; $array_access_type = Type::getMixed(); } else { if ($type->sealed || !$context->inside_isset) { - $object_like_keys = array_keys($type->properties); + $object_like_keys = array_keys($properties); $last_key = array_pop($object_like_keys); @@ -1580,6 +1603,8 @@ private static function handleArrayAccessOnKeyedArray( $array_access_type = Type::getMixed(); } } + + $type = $type->setProperties($properties); } else { $key_type = $generic_key_type->hasMixed() ? Type::getArrayKey() @@ -1589,7 +1614,7 @@ private static function handleArrayAccessOnKeyedArray( $is_contained = UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $key_type, true, $offset_type->ignore_falsable_issues, @@ -1600,7 +1625,7 @@ private static function handleArrayAccessOnKeyedArray( $is_contained = UnionTypeComparator::isContainedBy( $codebase, $key_type, - $offset_type, + $offset_type->freeze(), true, $offset_type->ignore_falsable_issues ); @@ -1620,23 +1645,18 @@ private static function handleArrayAccessOnKeyedArray( $new_key_type = Type::combineUnionTypes( $generic_key_type, - $offset_type->isMixed() ? Type::getArrayKey() : $offset_type + $offset_type->isMixed() ? Type::getArrayKey() : $offset_type->freeze() ); $property_count = $type->sealed ? count($type->properties) : null; if (!$stmt->dim && $property_count) { ++$property_count; - $array_type->removeType($type_string); $type = new TNonEmptyArray([ $new_key_type, $generic_params, - ]); - $array_type->addType($type); - $type->count = $property_count; + ], $property_count); } else { - $array_type->removeType($type_string); - if (!$stmt->dim && $type->is_list) { $type = new TList($generic_params); } else { @@ -1645,8 +1665,6 @@ private static function handleArrayAccessOnKeyedArray( $generic_params, ]); } - - $array_type->addType($type); } $array_access_type = Type::combineUnionTypes( @@ -1676,13 +1694,14 @@ private static function handleArrayAccessOnKeyedArray( /** * @param list $expected_offset_types * @param list $key_values + * @param-out TList $type */ private static function handleArrayAccessOnList( StatementsAnalyzer $statements_analyzer, Codebase $codebase, PhpParser\Node\Expr\ArrayDimFetch $stmt, - TList $type, - Union $offset_type, + TList &$type, + MutableUnion $offset_type, ?string $extended_var_id, array $key_values, Context $context, @@ -1726,15 +1745,15 @@ private static function handleArrayAccessOnList( } if ($in_assignment && $type instanceof TNonEmptyList && $type->count !== null) { - $type->count++; + $type = $type->setCount($type->count+1); } if ($in_assignment && $replacement_type) { - $type->type_param = Type::combineUnionTypes( + $type = $type->replaceTypeParam(Type::combineUnionTypes( $type->type_param, $replacement_type, $codebase - ); + )); } $array_access_type = Type::combineUnionTypes( @@ -1897,7 +1916,7 @@ private static function handleArrayAccessOnString( Context $context, ?Union $replacement_type, TString $type, - Union $offset_type, + MutableUnion $offset_type, array &$expected_offset_types, ?Union &$array_access_type, bool &$has_valid_offset @@ -1960,7 +1979,7 @@ private static function handleArrayAccessOnString( if (!UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $valid_offset_type, true )) { @@ -1981,7 +2000,7 @@ private static function handleArrayAccessOnString( * @param Atomic[] $offset_types */ private static function checkArrayOffsetType( - Union $offset_type, + MutableUnion $offset_type, array $offset_types, Codebase $codebase ): bool { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index d9c83bdc4ee..ef24481d64e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -77,6 +77,8 @@ class AtomicPropertyFetchAnalyzer { /** * @param array $invalid_fetch_types $invalid_fetch_types + * + * @psalm-suppress ComplexMethod Unavoidably complex method. */ public static function analyze( StatementsAnalyzer $statements_analyzer, @@ -743,7 +745,7 @@ public static function localizePropertyType( } } - TemplateInferredTypeReplacer::replace( + $class_property_type = TemplateInferredTypeReplacer::replace( $class_property_type, new TemplateResult([], $template_types), $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php index 195bccced82..2e25c2cd7e5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php @@ -262,7 +262,8 @@ public static function analyze( $stmt_type = $statements_analyzer->node_data->getType($stmt); if ($stmt_var_type->isNullable() && !$context->inside_isset && $stmt_type) { - $stmt_type->addType(new TNull); + $stmt_type = $stmt_type->getBuilder()->addType(new TNull)->freeze(); + $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($stmt_var_type->ignore_nullable_issues) { $stmt_type->ignore_nullable_issues = true; @@ -388,7 +389,10 @@ private static function handleScopedProperty( $statements_analyzer->getSuppressedIssues() ); - $stmt_type->addType(new TNull); + $stmt_type = $stmt_type->getBuilder()->addType(new TNull)->freeze(); + + $context->vars_in_scope[$var_id] = $stmt_type; + $statements_analyzer->node_data->setType($stmt, $stmt_type); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 331c818c709..ee327d72eeb 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,254 @@ 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 (self::isSuperGlobal($var_id)) { - $type = Type::getArray(); - if ($var_id === '$_SESSION') { - $type->possibly_undefined = true; + if ($var_id === '$GLOBALS') { + return new Union([ + new TNonEmptyArray([ + Type::getNonEmptyString(), + Type::getMixed() + ]) + ]); + } + + 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, + ], + null, + false, + Type::getNonEmptyString(), + 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/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 12e98ae5d9d..4b6c3dae06f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Config; @@ -30,6 +31,7 @@ use function implode; use function in_array; use function is_string; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -379,6 +381,9 @@ public static function resolveIncludePath(string $file_name, string $current_dir ? preg_split('#(?getBuilder(); $invalidTypes->removeType('string'); $invalidTypes->removeType('int'); $invalidTypes->removeType('float'); @@ -430,19 +430,21 @@ public static function infer( return null; } + $new_types = []; foreach ($type_to_invert->getAtomicTypes() as $type_part) { if ($type_part instanceof TLiteralInt && $stmt instanceof PhpParser\Node\Expr\UnaryMinus ) { - $type_part->value = -$type_part->value; + $new_types []= new TLiteralInt(-$type_part->value); } elseif ($type_part instanceof TLiteralFloat && $stmt instanceof PhpParser\Node\Expr\UnaryMinus ) { - $type_part->value = -$type_part->value; + $new_types []= new TLiteralFloat(-$type_part->value); + } else { + $new_types []= $type_part; } } - - return $type_to_invert; + return new Union($new_types); } if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) { @@ -574,10 +576,12 @@ private static function inferArrayType( ) { $objectlike = new TKeyedArray( $array_creation_info->property_types, - $array_creation_info->class_strings + $array_creation_info->class_strings, + true, + null, + null, + $array_creation_info->all_list ); - $objectlike->sealed = true; - $objectlike->is_list = $array_creation_info->all_list; return new Union([$objectlike]); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php index b46ac3df06c..60b8e3f6593 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php @@ -18,6 +18,7 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TString; use Psalm\Type\Union; +use RuntimeException; /** * @internal @@ -45,39 +46,37 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type_part) { if ($type_part instanceof TInt || $type_part instanceof TFloat) { - if ($type_part instanceof TLiteralInt - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { - $type_part->value = -$type_part->value; - } elseif ($type_part instanceof TLiteralFloat - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { - $type_part->value = -$type_part->value; + if (!$stmt instanceof PhpParser\Node\Expr\UnaryMinus) { + $acceptable_types []= $type_part; + continue; } - - if ($type_part instanceof TIntRange - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { + if ($type_part instanceof TLiteralInt) { + $type_part = new TLiteralInt(-$type_part->value); + } elseif ($type_part instanceof TLiteralFloat) { + $type_part = new TLiteralFloat(-$type_part->value); + } elseif ($type_part instanceof TIntRange) { //we'll have to inverse min and max bound and negate any literal $old_min_bound = $type_part->min_bound; $old_max_bound = $type_part->max_bound; if ($old_min_bound === null) { //min bound is null, max bound will be null - $type_part->max_bound = null; + $new_max_bound = null; } elseif ($old_min_bound === 0) { - $type_part->max_bound = 0; + $new_max_bound = 0; } else { - $type_part->max_bound = -$old_min_bound; + $new_max_bound = -$old_min_bound; } if ($old_max_bound === null) { //max bound is null, min bound will be null - $type_part->min_bound = null; + $new_min_bound = null; } elseif ($old_max_bound === 0) { - $type_part->min_bound = 0; + $new_min_bound = 0; } else { - $type_part->min_bound = -$old_max_bound; + $new_min_bound = -$old_max_bound; } + + $type_part = new TIntRange($new_min_bound, $new_max_bound); } $acceptable_types[] = $type_part; @@ -89,6 +88,10 @@ public static function analyze( } } + if (!$acceptable_types) { + throw new RuntimeException("Impossible!"); + } + $statements_analyzer->node_data->setType($stmt, new Union($acceptable_types)); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index 9715f8ade7e..afb4da37625 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -216,7 +216,7 @@ public static function analyze( } if ($yield_type) { - $expression_type->substitute($expression_type, $yield_type); + $expression_type = $expression_type->getBuilder()->substitute($expression_type, $yield_type)->freeze(); } $statements_analyzer->node_data->setType($stmt, $expression_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/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index c063431a38f..2cf90031d27 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -284,10 +284,8 @@ public static function analyze( unset($found_generic_params[$template_name][$fq_class_name]); } - $local_return_type = clone $local_return_type; - - TemplateInferredTypeReplacer::replace( - $local_return_type, + $local_return_type = TemplateInferredTypeReplacer::replace( + clone $local_return_type, new TemplateResult([], $found_generic_params), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php index d537e8ca870..b3882121827 100644 --- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php @@ -99,7 +99,7 @@ public static function analyze( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $var_comment_type->check( $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index 2c9a1644902..3916fcccbb5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -57,76 +57,91 @@ public static function analyze( $statements_analyzer ); - if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) { - $root_type = clone $context->vars_in_scope[$root_var_id]; + $key_type = $statements_analyzer->node_data->getType($var->dim); + if ($root_var_id && isset($context->vars_in_scope[$root_var_id]) && $key_type) { + $root_types = []; - foreach ($root_type->getAtomicTypes() as $atomic_root_type) { + foreach ($context->vars_in_scope[$root_var_id]->getAtomicTypes() as $atomic_root_type) { if ($atomic_root_type instanceof TKeyedArray) { - if ($var->dim instanceof PhpParser\Node\Scalar\String_ - || $var->dim instanceof PhpParser\Node\Scalar\LNumber - ) { - if (isset($atomic_root_type->properties[$var->dim->value])) { - if ($atomic_root_type->is_list - && $var->dim->value !== count($atomic_root_type->properties)-1 + $key_value = null; + if ($key_type->isSingleIntLiteral()) { + $key_value = $key_type->getSingleIntLiteral()->value; + } elseif ($key_type->isSingleStringLiteral()) { + $key_value = $key_type->getSingleStringLiteral()->value; + } + if ($key_value !== null) { + $properties = $atomic_root_type->properties; + $is_list = $atomic_root_type->is_list; + if (isset($properties[$key_value])) { + if ($is_list + && $key_value !== count($properties)-1 ) { - $atomic_root_type->is_list = false; + $is_list = false; } - unset($atomic_root_type->properties[$var->dim->value]); - $root_type->bustCache(); //remove id cache + unset($properties[$key_value]); } - if (!$atomic_root_type->properties) { + /** @psalm-suppress DocblockTypeContradiction https://github.com/vimeo/psalm/issues/8518 */ + if (!$properties) { if ($atomic_root_type->previous_value_type) { - $root_type->addType( + $root_types [] = new TArray([ $atomic_root_type->previous_key_type ? clone $atomic_root_type->previous_key_type : new Union([new TArrayKey]), clone $atomic_root_type->previous_value_type, ]) - ); + ; } else { - $root_type->addType( + $root_types [] = new TArray([ new Union([new TNever]), new Union([new TNever]), ]) - ); + ; } + } else { + $root_types []= new TKeyedArray( + $properties, + null, + $atomic_root_type->sealed, + $atomic_root_type->previous_key_type, + $atomic_root_type->previous_value_type, + $is_list + ); } } else { + $properties = []; foreach ($atomic_root_type->properties as $key => $type) { - $atomic_root_type->properties[$key] = clone $type; - $atomic_root_type->properties[$key]->possibly_undefined = true; + $properties[$key] = clone $type; + $properties[$key]->possibly_undefined = true; } - - $atomic_root_type->sealed = false; - - $root_type->addType( - $atomic_root_type->getGenericArrayType(false) + $root_types []= new TKeyedArray( + $properties, + null, + false, + $atomic_root_type->previous_key_type, + $atomic_root_type->previous_value_type, + false, ); - - $atomic_root_type->is_list = false; } } elseif ($atomic_root_type instanceof TNonEmptyArray) { - $root_type->addType( - new TArray($atomic_root_type->type_params) - ); + $root_types []= new TArray($atomic_root_type->type_params); } elseif ($atomic_root_type instanceof TNonEmptyMixed) { - $root_type->addType( - new TMixed() - ); + $root_types []= new TMixed(); } elseif ($atomic_root_type instanceof TList) { - $root_type->addType( + $root_types []= new TArray([ Type::getInt(), $atomic_root_type->type_param ]) - ); + ; + } else { + $root_types []= $atomic_root_type; } } - $context->vars_in_scope[$root_var_id] = $root_type; + $context->vars_in_scope[$root_var_id] = new Union($root_types); $context->removeVarFromConflictingClauses( $root_var_id, 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..1e9f815ba89 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Cli; +use AssertionError; use Composer\XdebugHandler\XdebugHandler; use Psalm\Config; use Psalm\Exception\UnsupportedIssueToFixException; @@ -44,9 +45,11 @@ 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; +use function preg_last_error_msg; use function preg_replace; use function preg_split; use function realpath; @@ -231,7 +234,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 +436,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 @@ -498,6 +501,9 @@ private static function loadCodeowners(Providers $providers): array $codeowner_lines = array_map( static function (string $line): array { $line_parts = preg_split('/\s+/', $line); + if ($line_parts === false) { + throw new AssertionError("An error occurred: ".preg_last_error_msg()); + } $file_selector = substr(array_shift($line_parts), 1); return [$file_selector, $line_parts]; diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 37385bed493..9010b7c53b8 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Cli; +use AssertionError; use Composer\XdebugHandler\XdebugHandler; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\CliUtils; @@ -35,9 +36,11 @@ 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; +use function preg_last_error_msg; use function preg_replace; use function preg_split; use function realpath; @@ -85,7 +88,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 @@ -240,6 +243,9 @@ static function (string $arg) use ($valid_long_options): void { if ($operation === 'move_into') { $last_arg_parts = preg_split('/, ?/', $last_arg); + if ($last_arg_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } foreach ($last_arg_parts as $last_arg_part) { if (strpos($last_arg_part, '::')) { @@ -286,7 +292,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/CliUtils.php b/src/Psalm/Internal/CliUtils.php index 12592a39030..05dea134a7a 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -12,6 +12,7 @@ use Psalm\Exception\ConfigNotFoundException; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Report; +use RuntimeException; use function array_slice; use function assert; @@ -30,6 +31,7 @@ use function is_dir; use function is_string; use function json_decode; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -298,6 +300,9 @@ public static function getPathsToCheck($f_paths): ?array stream_set_blocking(STDIN, false); if ($stdin = fgets(STDIN)) { $filtered_input_paths = preg_split('/\s+/', trim($stdin)); + if ($filtered_input_paths === false) { + throw new RuntimeException('Invalid paths: '.preg_last_error_msg()); + } } $blocked = $meta['blocked']; stream_set_blocking(STDIN, $blocked); 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..cb4ed8ffd50 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; } @@ -1460,9 +1455,10 @@ public function handleDocblockTypeInMigration( foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) { if ($type->containsClassLike($old_fq_class_name)) { - $type = clone $type; - - $type->replaceClassLike($old_fq_class_name, $new_fq_class_name); + $type = $type->replaceClassLike( + $old_fq_class_name, + $new_fq_class_name + ); $bounds = $type_location->getSelectionBounds(); @@ -1500,9 +1496,10 @@ public function handleDocblockTypeInMigration( $destination_class = $codebase->classes_to_move[$fq_class_name_lc]; if ($type->containsClassLike($fq_class_name_lc)) { - $type = clone $type; - - $type->replaceClassLike($fq_class_name_lc, $destination_class); + $type = $type->replaceClassLike( + $fq_class_name_lc, + $destination_class + ); } $this->airliftClassDefinedDocblockType( diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 88c9e03b189..e6e0fcae640 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -139,8 +139,11 @@ public static function resolve( } if ($left instanceof TKeyedArray && $right instanceof TKeyedArray) { - $type = new TKeyedArray($left->properties + $right->properties); - $type->sealed = true; + $type = new TKeyedArray( + $left->properties + $right->properties, + null, + true + ); return $type; } @@ -263,10 +266,7 @@ public static function resolve( new Union([new TNever()]), ]); } else { - $resolved_type = new TKeyedArray($properties); - - $resolved_type->is_list = $is_list; - $resolved_type->sealed = true; + $resolved_type = new TKeyedArray($properties, null, true, null, null, $is_list); } return $resolved_type; @@ -352,9 +352,7 @@ public static function getLiteralTypeFromScalarValue($value, bool $sealed_array foreach ($value as $key => $val) { $types[$key] = new Union([self::getLiteralTypeFromScalarValue($val, $sealed_array)]); } - $type = new TKeyedArray($types); - $type->sealed = $sealed_array; - return $type; + return new TKeyedArray($types, null, $sealed_array); } if (is_string($value)) { 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/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index a41fa171f0c..5cd02209614 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -305,6 +305,7 @@ public static function getCallablesFromCallMap(string $function_id): ?array $arg_name, $by_reference, $param_type, + $param_type, null, null, $optional, diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index ba56971366e..e92134bcf82 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -19,29 +19,24 @@ use Psalm\Internal\Provider\MethodVisibilityProvider; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TypeExpander; +use Psalm\Internal\TypeVisitor\TypeLocalizer; use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; use Psalm\Storage\MethodStorage; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TEnumCase; -use Psalm\Type\Atomic\TGenericObject; -use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Union; use UnexpectedValueException; use function array_pop; -use function array_values; use function assert; use function count; use function explode; @@ -494,7 +489,7 @@ public function getMethodParams( if ($params[$i]->signature_type && $params[$i]->signature_type->isNullable() ) { - $params[$i]->type->addType(new TNull); + $params[$i]->type = $params[$i]->type->getBuilder()->addType(new TNull)->freeze(); } $params[$i]->type_location = $overridden_storage->params[$i]->type_location; @@ -520,105 +515,10 @@ public static function localizeType( return $type; } - $type = clone $type; - - foreach ($type->getAtomicTypes() as $key => $atomic_type) { - if ($atomic_type instanceof TTemplateParam - && ($atomic_type->defining_class === $base_fq_class_name - || isset($extends[$atomic_type->defining_class])) - ) { - $types_to_add = self::getExtendedTemplatedTypes( - $atomic_type, - $extends - ); - - if ($types_to_add) { - $type->removeType($key); - - foreach ($types_to_add as $extra_added_type) { - $type->addType($extra_added_type); - } - } - } - - if ($atomic_type instanceof TTemplateParamClass) { - if ($atomic_type->defining_class === $base_fq_class_name) { - if (isset($extends[$base_fq_class_name][$atomic_type->param_name])) { - $extended_param = $extends[$base_fq_class_name][$atomic_type->param_name]; - - $types = array_values($extended_param->getAtomicTypes()); - - if (count($types) === 1 && $types[0] instanceof TNamedObject) { - $atomic_type->as_type = $types[0]; - } else { - $atomic_type->as_type = null; - } - } - } - } - - if ($atomic_type instanceof TArray - || $atomic_type instanceof TIterable - || $atomic_type instanceof TGenericObject - ) { - foreach ($atomic_type->type_params as &$type_param) { - $type_param = self::localizeType( - $codebase, - $type_param, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - - if ($atomic_type instanceof TList) { - $atomic_type->type_param = self::localizeType( - $codebase, - $atomic_type->type_param, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - - if ($atomic_type instanceof TKeyedArray) { - foreach ($atomic_type->properties as &$property_type) { - $property_type = self::localizeType( - $codebase, - $property_type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - - if ($atomic_type instanceof TCallable - || $atomic_type instanceof TClosure - ) { - if ($atomic_type->params) { - foreach ($atomic_type->params as $param) { - if ($param->type) { - $param->type = self::localizeType( - $codebase, - $param->type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - } - - if ($atomic_type->return_type) { - $atomic_type->return_type = self::localizeType( - $codebase, - $atomic_type->return_type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - } - - $type->bustCache(); + (new TypeLocalizer( + $extends, + $base_fq_class_name + ))->traverse($type); return $type; } @@ -725,9 +625,7 @@ public function getMethodReturnType( $types[] = new Union([new TEnumCase($original_fq_class_name, $case_name)]); } - $list = new TKeyedArray($types); - $list->is_list = true; - $list->sealed = true; + $list = new TKeyedArray($types, null, true, null, null, true); return new Union([$list]); } } @@ -889,12 +787,17 @@ public function getMethodReturnType( if ((!$old_contained_by_new && !$new_contained_by_old) || ($old_contained_by_new && $new_contained_by_old) ) { + $attempted_intersection = null; if ($old_contained_by_new) { //implicitly $new_contained_by_old as well - $attempted_intersection = Type::intersectUnionTypes( - $candidate_type, - $overridden_storage->return_type, - $source_analyzer->getCodebase() - ); + try { + $attempted_intersection = Type::intersectUnionTypes( + $candidate_type, + $overridden_storage->return_type, + $source_analyzer->getCodebase() + ); + } catch (InvalidArgumentException $e) { + // TODO: fix + } } else { $attempted_intersection = Type::intersectUnionTypes( $overridden_storage->return_type, 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/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index 893a02b7ec4..7bd20893404 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -343,6 +343,7 @@ private function getReflectionParamData(ReflectionParameter $param): FunctionLik $param_name, $param->isPassedByReference(), $param_type, + $param_type, null, null, $is_optional, 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/LanguageServer/ProtocolStreamReader.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php index e1391b4c6eb..bb1759675b8 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php @@ -119,7 +119,7 @@ private function readMessages(string $buffer): int ++$emitted_messages; $this->emit('message', [$msg]); /** - * @psalm-suppress DocblockTypeContradiction + * @psalm-suppress TypeDoesNotContainType */ if (!$this->is_accepting_new_requests) { // If we fork, don't read any bytes in the input buffer from the worker process. 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..cf8febd6575 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -30,6 +30,7 @@ use function explode; use function implode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -66,6 +67,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg()); + } $template_name = array_shift($template_type); @@ -106,6 +110,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template-covariant'])) { foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg()); + } $template_name = array_shift($template_type); @@ -506,7 +513,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..b67b7e18a54 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -499,9 +499,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $yield_type_tokens, null, $storage->template_types ?: [], - $this->type_aliases + $this->type_aliases, + true ); - $yield_type->setFromDocblock(); $yield_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -559,9 +559,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $pseudo_property_type_tokens, null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); - $pseudo_property_type->setFromDocblock(); $pseudo_property_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -660,7 +660,8 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); $mixin_type->queueClassLikesForScanning( @@ -669,8 +670,6 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $storage->template_types ?: [] ); - $mixin_type->setFromDocblock(); - if ($mixin_type->isSingle()) { $mixin_type = $mixin_type->getSingleAtomic(); @@ -797,11 +796,10 @@ public function finish(PhpParser\Node\Stmt\ClassLike $node): ClassLikeStorage $type->replacement_tokens, null, [], - $this->type_aliases + $this->type_aliases, + true ); - $union->setFromDocblock(); - $converted_aliases[$key] = new ClassTypeAlias(array_values($union->getAtomicTypes())); } catch (Exception $e) { $classlike_storage->docblock_issues[] = new InvalidDocblock( @@ -924,7 +922,8 @@ private function extendTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -942,8 +941,6 @@ private function extendTemplatedType( ); } - $extended_union_type->setFromDocblock(); - $extended_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1008,7 +1005,8 @@ private function implementTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -1028,8 +1026,6 @@ private function implementTemplatedType( return; } - $implemented_union_type->setFromDocblock(); - $implemented_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1094,7 +1090,8 @@ private function useTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -1114,8 +1111,6 @@ private function useTemplatedType( return; } - $used_union_type->setFromDocblock(); - $used_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1543,7 +1538,6 @@ private function visitPropertyDeclaration( if ($doc_var_group_type) { $doc_var_group_type->queueClassLikesForScanning($this->codebase, $this->file_storage); - $doc_var_group_type->setFromDocblock(); } foreach ($stmt->props as $property) { @@ -1616,6 +1610,7 @@ private function visitPropertyDeclaration( foreach ($property_storage->type->getAtomicTypes() as $key => $type) { if (isset($signature_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $type->from_docblock = false; } else { $all_typehint_types_match = false; @@ -1629,7 +1624,7 @@ private function visitPropertyDeclaration( if ($property_storage->signature_type->isNullable() && !$property_storage->type->isNullable() ) { - $property_storage->type->addType(new TNull()); + $property_storage->type = $property_storage->type->getBuilder()->addType(new TNull())->freeze(); } } @@ -1849,6 +1844,10 @@ private static function getTypeAliasesFromCommentLines( array_shift($var_line_parts); } + if (!isset($var_line_parts[0])) { + continue; + } + if ($var_line_parts[0] === '=') { array_shift($var_line_parts); } @@ -1863,8 +1862,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..efd06cc0a7d 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\PhpVisitor\Reflector; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\DocComment; @@ -20,6 +21,7 @@ use function explode; use function implode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -79,7 +81,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 +157,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]), @@ -229,6 +231,9 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-taint-sink'])) { foreach ($parsed_docblock->tags['psalm-taint-sink'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if (count($param_parts) >= 2) { $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; @@ -240,6 +245,9 @@ public static function parse( if (isset($parsed_docblock->tags['param-taint'])) { foreach ($parsed_docblock->tags['param-taint'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if (count($param_parts) === 2) { $taint_type = $param_parts[1]; @@ -264,6 +272,9 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-taint-source'])) { foreach ($parsed_docblock->tags['psalm-taint-source'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if ($param_parts[0]) { $info->taint_source_types[] = $param_parts[0]; @@ -273,6 +284,9 @@ public static function parse( // support for MediaWiki taint plugin foreach ($parsed_docblock->tags['return-taint'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if ($param_parts[0]) { if ($param_parts[0] === 'tainted') { @@ -343,7 +357,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], @@ -429,6 +443,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new AssertionError(preg_last_error_msg()); + } $template_name = array_shift($template_type); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 084a6ce1bb7..2b9316afa4c 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\PhpVisitor\Reflector; +use AssertionError; use PhpParser; use Psalm\Aliases; use Psalm\CodeLocation; @@ -52,6 +53,7 @@ use function count; use function explode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -765,6 +767,7 @@ private static function improveParamsFromDocblock( null, null, null, + null, false, false, true, @@ -785,7 +788,8 @@ private static function improveParamsFromDocblock( ), null, $function_template_types + $class_template_types, - $type_aliases + $type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -797,7 +801,6 @@ private static function improveParamsFromDocblock( } $storage_param->has_docblock_type = true; - $new_param_type->setFromDocblock(); $new_param_type->queueClassLikesForScanning( $codebase, @@ -846,7 +849,7 @@ private static function improveParamsFromDocblock( && !$new_param_type->isNullable() && !$new_param_type->hasTemplate() ) { - $new_param_type->addType(new TNull()); + $new_param_type = $new_param_type->getBuilder()->addType(new TNull())->freeze(); } $config = Config::getInstance(); @@ -870,6 +873,7 @@ private static function improveParamsFromDocblock( foreach ($new_param_type->getAtomicTypes() as $key => $type) { if (isset($storage_param_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $type->from_docblock = false; if ($storage_param_atomic_types[$key] instanceof TArray @@ -888,7 +892,7 @@ private static function improveParamsFromDocblock( } if ($existing_param_type_nullable && !$new_param_type->isNullable()) { - $new_param_type->addType(new TNull()); + $new_param_type = $new_param_type->getBuilder()->addType(new TNull())->freeze(); } $storage_param->type = $new_param_type; @@ -966,11 +970,10 @@ private static function handleReturn( array_values($fixed_type_tokens), null, $function_template_types + $class_template_types, - $type_aliases + $type_aliases, + true ); - $storage->return_type->setFromDocblock(); - if ($storage instanceof MethodStorage) { $storage->has_docblock_return_type = true; } @@ -981,6 +984,7 @@ private static function handleReturn( foreach ($storage->return_type->getAtomicTypes() as $key => $type) { if (isset($signature_return_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this atomic type */ $type->from_docblock = false; } else { $all_typehint_types_match = false; @@ -1010,7 +1014,7 @@ private static function handleReturn( $storage->signature_return_type ) ) { - $storage->return_type->addType(new TNull()); + $storage->return_type = $storage->return_type->getBuilder()->addType(new TNull())->freeze(); } } } @@ -1079,6 +1083,9 @@ private static function handleTaintFlow( if ($source_param_string[0] === '(' && substr($source_param_string, -1) === ')') { $source_params = preg_split('/, ?/', substr($source_param_string, 1, -1)); + if ($source_params === false) { + throw new AssertionError(preg_last_error_msg()); + } foreach ($source_params as $source_param) { $source_param = substr($source_param, 1); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 1b153d46ed3..ef7c2f7f207 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; @@ -843,7 +846,7 @@ private function getTranslatedFunctionParam( ); if ($is_nullable) { - $param_type->addType(new TNull); + $param_type = $param_type->getBuilder()->addType(new TNull)->freeze(); } else { $is_nullable = $param_type->isNullable(); } @@ -881,6 +884,7 @@ private function getTranslatedFunctionParam( $param->var->name, $param->byRef, $param_type, + $param_type, new CodeLocation( $this->file_scanner, $fake_method ? $stmt : $param->var, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 9265d3c8b15..336d8984cff 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -165,11 +165,12 @@ public static function resolve( if ($type_string) { $atomic_type = $type->getSingleAtomic(); + /** @psalm-suppress InaccessibleProperty We just created this type */ $atomic_type->text = $type_string; } if ($is_nullable) { - $type->addType(new TNull); + $type = $type->getBuilder()->addType(new TNull)->freeze(); } return $type; 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/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index be8e069e7e6..759053d5cd0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -88,8 +88,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if (!isset($call_args[1]) && !$first_arg_array->previous_value_type) { $had_one = count($first_arg_array->properties) === 1; - $first_arg_array = clone $first_arg_array; - $new_properties = array_filter( array_map( static function ($keyed_type) use ($statements_source, $context) { @@ -119,12 +117,14 @@ static function ($keyed_type) use ($statements_source, $context) { return Type::getEmptyArray(); } - $first_arg_array->properties = $new_properties; - - $first_arg_array->is_list = $first_arg_array->is_list && $had_one; - $first_arg_array->sealed = false; - - return new Union([$first_arg_array]); + return new Union([new TKeyedArray( + $new_properties, + null, + false, + null, + null, + $first_arg_array->is_list && $had_one + )]); } } @@ -153,11 +153,11 @@ static function ($keyed_type) use ($statements_source, $context) { } if ($key_type->getLiteralStrings()) { - $key_type->addType(new TString); + $key_type = $key_type->getBuilder()->addType(new TString)->freeze(); } if ($key_type->getLiteralInts()) { - $key_type->addType(new TInt); + $key_type = $key_type->getBuilder()->addType(new TInt)->freeze(); } if ($inner_type->isUnionEmpty()) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 5c55b900ce6..46a6bf10bd1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -197,12 +197,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev array_map( static fn(Union $_): Union => clone $mapping_return_type, $array_arg_atomic_type->properties - ) + ), + null, + $array_arg_atomic_type->sealed, + $array_arg_atomic_type->previous_key_type, + $mapping_return_type, + $array_arg_atomic_type->is_list ); - $atomic_type->is_list = $array_arg_atomic_type->is_list; - $atomic_type->sealed = $array_arg_atomic_type->sealed; - $atomic_type->previous_key_type = $array_arg_atomic_type->previous_key_type; - $atomic_type->previous_value_type = $mapping_return_type; return new Union([$atomic_type]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 90257156ecd..36970468665 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -235,20 +235,14 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && ($generic_property_count < $max_keyed_array_size * 2 || $generic_property_count < 16) ) { - $objectlike = new TKeyedArray($generic_properties); - - if ($class_strings !== []) { - $objectlike->class_strings = $class_strings; - } - - if ($all_nonempty_lists || $all_int_offsets) { - $objectlike->is_list = true; - } - - if (!$all_keyed_arrays) { - $objectlike->previous_key_type = $inner_key_type; - $objectlike->previous_value_type = $inner_value_type; - } + $objectlike = new TKeyedArray( + $generic_properties, + $class_strings ?: null, + false, + $all_keyed_arrays ? null : $inner_key_type, + $all_keyed_arrays ? null : $inner_value_type, + $all_nonempty_lists || $all_int_offsets + ); return new Union([$objectlike]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index 5ac91d639da..f89b9012e08 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -86,7 +86,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($value_type->isNever()) { $value_type = Type::getFalse(); } elseif (($function_id !== 'reset' && $function_id !== 'end') || !$definitely_has_items) { - $value_type->addType(new TFalse); + $value_type = $value_type->getBuilder()->addType(new TFalse)->freeze(); $codebase = $statements_source->getCodebase(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php index 0ea580949d7..64266953d8f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php @@ -85,7 +85,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($nullable) { - $value_type->addType(new TNull); + $value_type = $value_type->getBuilder()->addType(new TNull)->freeze(); $codebase = $statements_source->getCodebase(); 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/ArrayUniqueReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php index eb6ef09c806..766895b9c4e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php @@ -51,10 +51,8 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($first_arg_array instanceof TArray) { - $first_arg_array = clone $first_arg_array; - if ($first_arg_array instanceof TNonEmptyArray) { - $first_arg_array->count = null; + $first_arg_array = $first_arg_array->setCount(null); } return new Union([$first_arg_array]); 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/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index e24f5a3e531..afa872b72ca 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -104,7 +104,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $options_array->properties['default'] ); } else { - $filter_type->addType(new TFalse); + $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); } if (isset($atomic_type->properties['flags']) @@ -116,20 +116,20 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($filter_type->hasBool() && $filter_flag_type->value === FILTER_NULL_ON_FAILURE ) { - $filter_type->addType(new TNull); + $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); } } } elseif ($atomic_type instanceof TLiteralInt) { if ($atomic_type->value === FILTER_NULL_ON_FAILURE) { $filter_null = true; - $filter_type->addType(new TNull); + $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); } } } } if (!$has_object_like && !$filter_null && $filter_type) { - $filter_type->addType(new TFalse); + $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php index 00f633bd858..2a3d18e8048 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; /** @@ -34,15 +35,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } - $return_type = Type::getString(); - if (($first_arg_type = $statements_source->node_data->getType($call_args[0]->value)) && $first_arg_type->isString() ) { - return $return_type; + return new Union([new TString]); } - $return_type->addType(new TNull); + $return_type = new Union([new TString, new TNull]); $return_type->ignore_nullable_issues = true; return $return_type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php index a5176b1a1ef..f3732474502 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php @@ -109,7 +109,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $template_types = $key_type->getTemplateTypes(); $template_type = array_shift($template_types); if ($template_type->as->hasMixed()) { - $template_type->as = Type::getArrayKey(); + $template_type = $template_type->replaceAs(Type::getArrayKey()); $key_type = new Union([$template_type]); } } 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/ReturnTypeProvider/PdoStatementSetFetchMode.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php index c8eba436197..75c6d475910 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php @@ -52,6 +52,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'mode', false, Type::getInt(), + Type::getInt(), null, null, false @@ -66,6 +67,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'colno', false, Type::getInt(), + Type::getInt(), null, null, false @@ -77,6 +79,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'classname', false, Type::getClassString(), + Type::getClassString(), null, null, false @@ -86,6 +89,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'ctorargs', false, Type::getArray(), + Type::getArray(), null, null, true @@ -97,6 +101,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'object', false, Type::getObject(), + Type::getObject(), null, null, false diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php index 6061ddf0d34..2dfa3681691 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; use function count; @@ -50,7 +51,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $return_type = Type::getString(); if (in_array($function_id, ['preg_replace', 'preg_replace_callback'], true)) { - $return_type->addType(new TNull()); + $return_type = new Union([new TString, new TNull()]); $codebase = $statements_source->getCodebase(); 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/ReferenceConstraint.php b/src/Psalm/Internal/ReferenceConstraint.php index 14bb1707b41..a77ec7ec648 100644 --- a/src/Psalm/Internal/ReferenceConstraint.php +++ b/src/Psalm/Internal/ReferenceConstraint.php @@ -18,19 +18,21 @@ class ReferenceConstraint public function __construct(?Union $type = null) { if ($type) { - $this->type = clone $type; + $type = $type->getBuilder(); - if ($this->type->getLiteralStrings()) { - $this->type->addType(new TString); + if ($type->getLiteralStrings()) { + $type->addType(new TString); } - if ($this->type->getLiteralInts()) { - $this->type->addType(new TInt); + if ($type->getLiteralInts()) { + $type->addType(new TInt); } - if ($this->type->getLiteralFloats()) { - $this->type->addType(new TFloat); + if ($type->getLiteralFloats()) { + $type->addType(new TFloat); } + + $this->type = $type->freeze(); } } } 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/Stubs/Generator/StubsGenerator.php b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php index e65c343324a..df5c8d40e7e 100644 --- a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php @@ -177,10 +177,6 @@ public static function getAll( continue; } - if ($type->isMixed()) { - continue; - } - $name_parts = explode('\\', $fq_name); $constant_name = array_pop($name_parts); diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 3778bbb5a45..d39fe490e64 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) { @@ -282,7 +282,7 @@ private static function refine( StatementsAnalyzer $statements_analyzer, Assertion $assertion, Atomic $new_type_part, - Union $existing_var_type, + Union &$existing_var_type, ?string $key, bool $negated, ?CodeLocation $code_location, @@ -344,9 +344,11 @@ private static function refine( } if ($acceptable_atomic_types) { - $new_type_part->as = new Union($acceptable_atomic_types); - - return new Union([$new_type_part]); + $acceptable_atomic_types = + count($acceptable_atomic_types) === count($existing_var_type->getAtomicTypes()) + ? $existing_var_type + : new Union($acceptable_atomic_types); + return new Union([$new_type_part->replaceAs($acceptable_atomic_types)]); } } @@ -359,13 +361,10 @@ private static function refine( $existing_var_type_part->properties, $new_type_part->properties )) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->properties = array_merge( + $acceptable_atomic_types[] = $existing_var_type_part->setProperties(array_merge( $existing_var_type_part->properties, $new_type_part->properties - ); - - $acceptable_atomic_types[] = $existing_var_type_part; + )); } } } @@ -402,14 +401,12 @@ private static function refine( && ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value)) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } if ($existing_var_type_part instanceof TTemplateParam) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } @@ -530,7 +527,7 @@ private static function refine( */ private static function filterTypeWithAnother( Codebase $codebase, - Union $existing_type, + Union &$existing_type, Union $new_type, bool &$any_scalar_type_match_found = false ): ?Union { @@ -538,8 +535,9 @@ private static function filterTypeWithAnother( $new_type = clone $new_type; + $existing_types = $existing_type->getAtomicTypes(); foreach ($new_type->getAtomicTypes() as $new_type_part) { - foreach ($existing_type->getAtomicTypes() as $existing_type_part) { + foreach ($existing_types as &$existing_type_part) { $matching_atomic_type = self::filterAtomicWithAnother( $existing_type_part, $new_type_part, @@ -552,9 +550,9 @@ private static function filterTypeWithAnother( } } } + $existing_type = $existing_type->setTypes($existing_types); if ($matching_atomic_types) { - $existing_type->bustCache(); return new Union($matching_atomic_types); } @@ -562,7 +560,7 @@ private static function filterTypeWithAnother( } private static function filterAtomicWithAnother( - Atomic $type_1_atomic, + Atomic &$type_1_atomic, Atomic $type_2_atomic, Codebase $codebase, bool &$any_scalar_type_match_found @@ -575,7 +573,7 @@ private static function filterAtomicWithAnother( } if ($type_1_atomic instanceof TNamedObject) { - $type_1_atomic->is_static = false; + $type_1_atomic = $type_1_atomic->setIsStatic(false); } $atomic_comparison_results = new TypeComparisonResult(); @@ -625,10 +623,7 @@ private static function filterAtomicWithAnother( && ($codebase->interfaceExists($type_1_atomic->value) || $codebase->interfaceExists($type_2_atomic->value)) ) { - $matching_atomic_type = clone $type_2_atomic; - $matching_atomic_type->extra_types[$type_1_atomic->getKey()] = $type_1_atomic; - - return $matching_atomic_type; + return $type_2_atomic->addIntersectionType($type_1_atomic); } if ($type_2_atomic instanceof TKeyedArray @@ -638,23 +633,27 @@ private static function filterAtomicWithAnother( $type_2_value = $type_2_atomic->getGenericValueType(); if (!$type_2_key->hasString()) { + $type_1_type_param = $type_1_atomic->type_param; $type_2_value = self::filterTypeWithAnother( $codebase, - $type_1_atomic->type_param, + $type_1_type_param, $type_2_value, $any_scalar_type_match_found ); + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_type_param); if ($type_2_value === null) { return null; } - $hybrid_type_part = new TKeyedArray($type_2_atomic->properties); - $hybrid_type_part->previous_key_type = Type::getInt(); - $hybrid_type_part->previous_value_type = $type_2_value; - $hybrid_type_part->is_list = true; - - return $hybrid_type_part; + return new TKeyedArray( + $type_2_atomic->properties, + null, + false, + Type::getInt(), + $type_2_value, + true + ); } } elseif ($type_1_atomic instanceof TKeyedArray && $type_2_atomic instanceof TList @@ -663,9 +662,10 @@ private static function filterAtomicWithAnother( $type_1_value = $type_1_atomic->getGenericValueType(); if (!$type_1_key->hasString()) { + $type_2_type_param = $type_2_atomic->type_param; $type_1_value = self::filterTypeWithAnother( $codebase, - $type_2_atomic->type_param, + $type_2_type_param, $type_1_value, $any_scalar_type_match_found ); @@ -674,12 +674,14 @@ private static function filterAtomicWithAnother( return null; } - $hybrid_type_part = new TKeyedArray($type_1_atomic->properties); - $hybrid_type_part->previous_key_type = Type::getInt(); - $hybrid_type_part->previous_value_type = $type_1_value; - $hybrid_type_part->is_list = true; - - return $hybrid_type_part; + return new TKeyedArray( + $type_1_atomic->properties, + null, + false, + Type::getInt(), + $type_1_value, + true + ); } } @@ -689,11 +691,7 @@ private static function filterAtomicWithAnother( && $type_2_atomic->as->hasObject() && $type_1_atomic->as->hasObject() ) { - $matching_atomic_type = clone $type_2_atomic; - - $matching_atomic_type->extra_types[$type_1_atomic->getKey()] = $type_1_atomic; - - return $matching_atomic_type; + return $type_2_atomic->addIntersectionType($type_1_atomic); } //we filter both types of standard iterables @@ -705,8 +703,9 @@ private static function filterAtomicWithAnother( || $type_1_atomic instanceof TIterable) && count($type_2_atomic->type_params) === count($type_1_atomic->type_params) ) { + $type_1_params = $type_1_atomic->type_params; foreach ($type_2_atomic->type_params as $i => $type_2_param) { - $type_1_param = $type_1_atomic->type_params[$i]; + $type_1_param = $type_1_params[$i]; $type_2_param_id = $type_2_param->getId(); @@ -721,12 +720,16 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->type_params[$i]->getId() !== $type_2_param_id) { - /** @psalm-suppress PropertyTypeCoercion */ - $type_1_atomic->type_params[$i] = $type_2_param; + if ($type_1_params[$i]->getId() !== $type_2_param_id) { + $type_1_params[$i] = $type_2_param; } } + /** @psalm-suppress ArgumentTypeCoercion */ + $type_1_atomic = $type_1_atomic->replaceTypeParams( + $type_1_params + ); + $matching_atomic_type = $type_1_atomic; $atomic_comparison_results->type_coerced = true; } @@ -750,8 +753,10 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->type_param->getId() !== $type_2_param->getId()) { - $type_1_atomic->type_param = $type_2_param; + if ($type_1_param->getId() !== $type_2_param->getId()) { + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_2_param); + } elseif ($type_1_param !== $type_1_atomic->type_param) { + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_param); } $matching_atomic_type = $type_1_atomic; @@ -764,7 +769,8 @@ private static function filterAtomicWithAnother( && $type_1_atomic instanceof TKeyedArray ) { $type_2_param = $type_2_atomic->type_params[1]; - foreach ($type_1_atomic->properties as $property_key => $type_1_param) { + $type_1_properties = $type_1_atomic->properties; + foreach ($type_1_properties as &$type_1_param) { $type_2_param = self::filterTypeWithAnother( $codebase, $type_1_param, @@ -776,12 +782,12 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->properties[$property_key]->getId() !== $type_2_param->getId()) { - $type_1_atomic->properties[$property_key] = $type_2_param; + if ($type_1_param->getId() !== $type_2_param->getId()) { + $type_1_param = $type_2_param; } } - $matching_atomic_type = $type_1_atomic; + $matching_atomic_type = $type_1_atomic->setProperties($type_1_properties); $atomic_comparison_results->type_coerced = true; } @@ -833,10 +839,10 @@ private static function refineContainedAtomicWithAnother( && $type_1_atomic instanceof TTemplateParam && $type_1_atomic->as->hasObjectType() ) { - $type_1_atomic = clone $type_1_atomic; + $type_1_as_init = $type_1_atomic->as; $type_1_as = self::filterTypeWithAnother( $codebase, - $type_1_atomic->as, + $type_1_as_init, new Union([$type_2_atomic]) ); @@ -844,9 +850,7 @@ private static function refineContainedAtomicWithAnother( return null; } - $type_1_atomic->as = $type_1_as; - - return $type_1_atomic; + return $type_1_atomic->replaceAs($type_1_as); } else { return clone $type_2_atomic; } @@ -943,6 +947,7 @@ private static function handleLiteralEquality( $can_be_equal = false; $did_remove_type = false; + $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_atomic_types as $atomic_key => $atomic_type) { if (get_class($atomic_type) === TNamedObject::class && $atomic_type->value === $fq_enum_name @@ -958,6 +963,7 @@ private static function handleLiteralEquality( $can_be_equal = true; } } + $existing_var_type = $existing_var_type->freeze(); if ($var_id && $code_location @@ -1053,18 +1059,18 @@ private static function handleLiteralEqualityWithInt( return $compatible_int_type; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1195,18 +1201,18 @@ private static function handleLiteralEqualityWithString( return $literal_asserted_type_string; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1337,18 +1343,18 @@ private static function handleLiteralEqualityWithFloat( return $literal_asserted_type; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1615,8 +1621,7 @@ private static function handleIsA( if ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index b267ffca9e4..5948512b93b 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -111,6 +111,7 @@ public static function isContainedBy( && $input_type_part instanceof TNonEmptyArray && $input_type_part->type_params[0]->isSingleIntLiteral() && $input_type_part->type_params[0]->getSingleIntLiteral()->value === 0 + && isset($input_type_part->type_params[1]) ) { //this is a special case where the only offset value of an non empty array is 0, so it's a non empty list return UnionTypeComparator::isContainedBy( diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 28064420305..870026ff6fa 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -63,10 +63,10 @@ public static function isContainedBy( if (($container_type_part instanceof TTemplateParam || ($container_type_part instanceof TNamedObject - && isset($container_type_part->extra_types))) + && $container_type_part->extra_types)) && ($input_type_part instanceof TTemplateParam || ($input_type_part instanceof TNamedObject - && isset($input_type_part->extra_types))) + && $input_type_part->extra_types)) ) { return ObjectComparator::isShallowlyContainedBy( $codebase, diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index e89269c3e0d..d67e7087d41 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Union; use UnexpectedValueException; use function end; @@ -258,20 +259,20 @@ public static function getCallableFromAtomic( $params = []; foreach ($function_storage->params as $param) { - $param = clone $param; - if ($param->type) { - $param->type = TypeExpander::expandUnion( - $codebase, - $param->type, - null, - null, - null, - true, - true, - false, - false, - true + $param = $param->replaceType( + TypeExpander::expandUnion( + $codebase, + $param->type, + null, + null, + null, + true, + true, + false, + false, + true + ) ); } @@ -325,7 +326,7 @@ public static function getCallableFromAtomic( } } - $matching_callable = InternalCallMapHandler::getCallableFromCallMapById( + $matching_callable = clone InternalCallMapHandler::getCallableFromCallMapById( $codebase, $input_type_part->value, $args, @@ -334,6 +335,7 @@ public static function getCallableFromAtomic( $must_use = false; + /** @psalm-suppress InaccessibleProperty We just cloned this object */ $matching_callable->is_pure = $codebase->functions->isCallMapFunctionPure( $codebase, $statements_analyzer->node_data ?? null, @@ -408,7 +410,7 @@ public static function getCallableFromAtomic( $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Type\Union([$input_with_templates]), $template_result, $codebase, @@ -440,15 +442,11 @@ public static function getCallableFromAtomic( ); if ($template_result) { - $replaced_callable = clone $callable; - - TemplateInferredTypeReplacer::replace( - new Type\Union([$replaced_callable]), + $callable = TemplateInferredTypeReplacer::replace( + new Union([clone $callable]), $template_result, $codebase - ); - - $callable = $replaced_callable; + )->getSingleAtomic(); } return $callable; @@ -500,6 +498,7 @@ public static function getCallableMethodIdFromTKeyedArray( } if ($member_id) { + /** @psalm-suppress PossiblyNullArgument Psalm bug */ $codebase->analyzer->addMixedMemberName( strtolower($member_id) . '::', $calling_method_id ?: $file_name diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index 6d40f771857..c6de9ca06be 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -57,6 +57,16 @@ public static function isContainedBy( $container_type_params_covariant ); + $atomic_comparison_result_type_params = null; + if ($atomic_comparison_result) { + if (!$atomic_comparison_result->replacement_atomic_type) { + $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; + } + + if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { + $atomic_comparison_result_type_params = $atomic_comparison_result->replacement_atomic_type->type_params; + } + } foreach ($input_type_params as $i => $input_param) { if (!isset($container_type_part->type_params[$i])) { break; @@ -65,16 +75,8 @@ public static function isContainedBy( $container_param = $container_type_part->type_params[$i]; if ($input_param->isNever()) { - if ($atomic_comparison_result) { - if (!$atomic_comparison_result->replacement_atomic_type) { - $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; - } - - if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { - /** @psalm-suppress PropertyTypeCoercion */ - $atomic_comparison_result->replacement_atomic_type->type_params[$i] - = clone $container_param; - } + if ($atomic_comparison_result_type_params !== null) { + $atomic_comparison_result_type_params[$i] = clone $container_param; } continue; @@ -136,16 +138,8 @@ public static function isContainedBy( && !$input_param->hasTemplate() ) { if ($input_param->containsAnyLiteral()) { - if ($atomic_comparison_result) { - if (!$atomic_comparison_result->replacement_atomic_type) { - $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; - } - - if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { - /** @psalm-suppress PropertyTypeCoercion */ - $atomic_comparison_result->replacement_atomic_type->type_params[$i] - = clone $container_param; - } + if ($atomic_comparison_result_type_params !== null) { + $atomic_comparison_result_type_params[$i] = clone $container_param; } } else { if (!($container_type_params_covariant[$i] ?? false) @@ -179,6 +173,16 @@ public static function isContainedBy( } } + if ($atomic_comparison_result + && $atomic_comparison_result->replacement_atomic_type instanceof TGenericObject + && $atomic_comparison_result_type_params + ) { + /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ + $atomic_comparison_result->replacement_atomic_type = + $atomic_comparison_result->replacement_atomic_type + ->replaceTypeParams($atomic_comparison_result_type_params); + } + if ($all_types_contain) { if ($atomic_comparison_result) { $atomic_comparison_result->to_string_cast = false; diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index 6b98ebb6c4c..96ed14f299c 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -84,6 +84,8 @@ public static function isContainedByUnion( * This method receives an array of atomics from the container and a range. * The goal is to use values in atomics in order to reduce the range. * Once the range is empty, it means that every value in range was covered by some atomics combination + * + * @psalm-suppress InaccessibleProperty $reduced_range was already cloned * @param array $container_atomic_types */ private static function reduceRangeIncrementally(array &$container_atomic_types, TIntRange $reduced_range): ?bool diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5e3aff80828..a0a15900294 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -100,8 +100,7 @@ private static function getIntersectionTypes(Atomic $type_part): array // T1 as T2 as object becomes (T1 as object) & (T2 as object) if ($as_atomic_type instanceof TTemplateParam) { $intersection_types += self::getIntersectionTypes($as_atomic_type); - $type_part = clone $type_part; - $type_part->as = $as_atomic_type->as; + $type_part = $type_part->replaceAs($as_atomic_type->as); $intersection_types[$type_part->getKey()] = $type_part; return $intersection_types; @@ -112,10 +111,8 @@ private static function getIntersectionTypes(Atomic $type_part): array return [$type_part->getKey() => $type_part]; } - $type_part = clone $type_part; - $extra_types = $type_part->extra_types; - $type_part->extra_types = null; + $type_part = $type_part->setIntersectionTypes([]); $extra_types[$type_part->getKey()] = $type_part; @@ -199,11 +196,14 @@ private static function isIntersectionShallowlyContainedBy( } if ($intersection_input_type instanceof TTemplateParam) { - $intersection_container_type = clone $intersection_container_type; - - if ($intersection_container_type instanceof TNamedObject) { + if ($intersection_container_type instanceof TNamedObject && $intersection_container_type->is_static) { // this is extra check is redundant since we're comparing to a template as type - $intersection_container_type->is_static = false; + $intersection_container_type = new TNamedObject( + $intersection_container_type->value, + false, + $intersection_container_type->definite_class, + $intersection_container_type->extra_types, + ); } return UnionTypeComparator::isContainedBy( diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index 2d0a6531d8b..9ceabd5bd07 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -174,14 +174,15 @@ public static function isContainedBy( && $atomic_comparison_result->replacement_atomic_type ) { if (!$union_comparison_result->replacement_union_type) { - $union_comparison_result->replacement_union_type = clone $input_type; + $union_comparison_result->replacement_union_type = $input_type; } - $union_comparison_result->replacement_union_type->removeType($input_type->getKey()); - - $union_comparison_result->replacement_union_type->addType( + $replacement = $union_comparison_result->replacement_union_type->getBuilder(); + $replacement->removeType($input_type->getKey()); + $replacement->addType( $atomic_comparison_result->replacement_atomic_type ); + $union_comparison_result->replacement_union_type = $replacement->freeze(); } } @@ -321,10 +322,10 @@ public static function isContainedByInPhp( return false; } - $input_type_not_null = clone $input_type; + $input_type_not_null = $input_type->getBuilder(); $input_type_not_null->removeType('null'); - $container_type_not_null = clone $container_type; + $container_type_not_null = $container_type->getBuilder(); $container_type_not_null->removeType('null'); if ($input_type_not_null->getId() === $container_type_not_null->getId()) { diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index efdf02d80c6..dfbf026a917 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -91,18 +91,20 @@ public static function reconcile( } $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); + $existing_var_type = $existing_var_type->getBuilder(); if ($assertion_type instanceof TFalse && isset($existing_var_atomic_types['bool'])) { $existing_var_type->removeType('bool'); $existing_var_type->addType(new TTrue); } elseif ($assertion_type instanceof TTrue && isset($existing_var_atomic_types['bool'])) { + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_type->removeType('bool'); $existing_var_type->addType(new TFalse); } else { $simple_negated_type = SimpleNegatedAssertionReconciler::reconcile( $statements_analyzer->getCodebase(), $assertion, - $existing_var_type, + $existing_var_type->freeze(), $key, $negated, $code_location, @@ -142,7 +144,7 @@ public static function reconcile( $existing_var_type->from_calculation = false; - return $existing_var_type; + return $existing_var_type->freeze(); } if (!$is_equality @@ -158,7 +160,7 @@ public static function reconcile( $existing_var_type->addType(new TNamedObject('DateTime')); } - return $existing_var_type; + return $existing_var_type->freeze(); } if (!$is_equality && $assertion_type instanceof TNamedObject) { @@ -251,6 +253,8 @@ public static function reconcile( } } + $existing_var_type = $existing_var_type->freeze(); + if ($assertion instanceof IsNotIdentical && ($key !== '$this' || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)) @@ -322,6 +326,7 @@ private static function handleLiteralNegatedEquality( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); $did_remove_type = false; @@ -443,6 +448,8 @@ private static function handleLiteralNegatedEquality( } } + $existing_var_type = $existing_var_type->freeze(); + if ($key && $code_location) { if ($did_match_literal_type && (!$did_remove_type || count($existing_var_atomic_types) === 1) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 62a0473fdb8..95a2bc30c04 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Type; +use AssertionError; use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Internal\Codebase\ClassConstantByWildcardResolver; @@ -74,7 +75,7 @@ use Psalm\Type\Reconciler; use Psalm\Type\Union; -use function assert; +use function array_merge; use function count; use function explode; use function get_class; @@ -473,14 +474,16 @@ public static function reconcile( if ($existing_var_type->isSingle() && $existing_var_type->hasTemplate() ) { - foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + $types = $existing_var_type->getAtomicTypes(); + foreach ($types as $k => $atomic_type) { if ($atomic_type instanceof TTemplateParam && $assertion_type) { if ($atomic_type->as->hasMixed() || $atomic_type->as->hasObject() ) { - $atomic_type->as = new Union([clone $assertion_type]); - - return $existing_var_type; + unset($types[$k]); + $atomic_type = $atomic_type->replaceAs(new Union([clone $assertion_type])); + $types[$atomic_type->getKey()] = $atomic_type; + return new Union($types); } } } @@ -504,6 +507,7 @@ private static function reconcileIsset( bool $is_equality, bool $inside_loop ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); // if key references an array offset @@ -554,7 +558,7 @@ private static function reconcileIsset( $existing_var_type->possibly_undefined_from_try = false; $existing_var_type->ignore_isset = false; - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -570,6 +574,7 @@ private static function reconcileNonEmptyCountable( bool $is_equality ): Union { $old_var_type_string = $existing_var_type->getId(); + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('array')) { $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; @@ -584,13 +589,11 @@ private static function reconcileNonEmptyCountable( $existing_var_type->removeType('array'); } else { $non_empty_array = new TNonEmptyArray( - $array_atomic_type->type_params + $array_atomic_type->type_params, + null, + $assertion instanceof HasAtLeastCount ? $assertion->count : null ); - if ($assertion instanceof HasAtLeastCount) { - $non_empty_array->min_count = $assertion->count; - } - $existing_var_type->addType($non_empty_array); } @@ -602,13 +605,11 @@ private static function reconcileNonEmptyCountable( && $array_atomic_type->count < $assertion->count) ) { $non_empty_list = new TNonEmptyList( - $array_atomic_type->type_param + $array_atomic_type->type_param, + null, + $assertion instanceof HasAtLeastCount ? $assertion->count : null ); - if ($assertion instanceof HasAtLeastCount) { - $non_empty_list->min_count = $assertion->count; - } - $did_remove_type = true; $existing_var_type->addType($non_empty_list); } @@ -633,10 +634,14 @@ private static function reconcileNonEmptyCountable( // this means a redundant condition } else { $did_remove_type = true; + $properties = $array_atomic_type->properties; for ($i = $prop_count; $i < $assertion->count; $i++) { - $array_atomic_type->properties[$i] + $properties[$i] = clone ($array_atomic_type->previous_value_type ?: Type::getMixed()); } + $array_atomic_type = $array_atomic_type->setProperties($properties); + $existing_var_type->removeType('array'); + $existing_var_type->addType($array_atomic_type); } } else { $did_remove_type = true; @@ -665,7 +670,7 @@ private static function reconcileNonEmptyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -675,33 +680,32 @@ private static function reconcileExactlyCountable( Union $existing_var_type, int $count ): Union { + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('array')) { $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; if ($array_atomic_type instanceof TArray) { $non_empty_array = new TNonEmptyArray( - $array_atomic_type->type_params + $array_atomic_type->type_params, + $count ); - $non_empty_array->count = $count; - $existing_var_type->addType( $non_empty_array ); } elseif ($array_atomic_type instanceof TList) { $non_empty_list = new TNonEmptyList( - $array_atomic_type->type_param + $array_atomic_type->type_param, + $count ); - $non_empty_list->count = $count; - $existing_var_type->addType( $non_empty_list ); } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -729,45 +733,50 @@ private static function reconcileHasMethod( if ($type instanceof TNamedObject && $codebase->classOrInterfaceExists($type->value) ) { - $object_types[] = $type; - if (!$codebase->methodExists($type->value . '::' . $method_name)) { $match_found = false; - if ($type->extra_types) { - foreach ($type->extra_types as $extra_type) { - if ($extra_type instanceof TNamedObject - && $codebase->classOrInterfaceExists($extra_type->value) - && $codebase->methodExists($extra_type->value . '::' . $method_name) - ) { - $match_found = true; - } elseif ($extra_type instanceof TObjectWithProperties) { - $match_found = true; - - if (!isset($extra_type->methods[$method_name])) { - $extra_type->methods[$method_name] = 'object::' . $method_name; - $did_remove_type = true; - } + $extra_types = $type->extra_types; + foreach ($type->extra_types as $k => $extra_type) { + if ($extra_type instanceof TNamedObject + && $codebase->classOrInterfaceExists($extra_type->value) + && $codebase->methodExists($extra_type->value . '::' . $method_name) + ) { + $match_found = true; + } elseif ($extra_type instanceof TObjectWithProperties) { + $match_found = true; + + if (!isset($extra_type->methods[$method_name])) { + unset($extra_types[$k]); + $extra_type = $extra_type->setMethods(array_merge($extra_type->methods, [ + $method_name => 'object::' . $method_name + ])); + $extra_types[$extra_type->getKey()] = $extra_type; + $did_remove_type = true; } } } if (!$match_found) { - $obj = new TObjectWithProperties( + $extra_type = new TObjectWithProperties( [], [$method_name => $type->value . '::' . $method_name] ); - $type->extra_types[$obj->getKey()] = $obj; + $extra_types[$extra_type->getKey()] = $extra_type; $did_remove_type = true; } + + $type = $type->setIntersectionTypes($extra_types); } - } elseif ($type instanceof TObjectWithProperties) { $object_types[] = $type; - + } elseif ($type instanceof TObjectWithProperties) { if (!isset($type->methods[$method_name])) { - $type->methods[$method_name] = 'object::' . $method_name; + $type = $type->setMethods(array_merge($type->methods, [ + $method_name => 'object::' . $method_name + ])); $did_remove_type = true; } + $object_types[] = $type; } elseif ($type instanceof TObject || $type instanceof TMixed) { $object_types[] = new TObjectWithProperties( [], @@ -842,11 +851,10 @@ private static function reconcileString( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TString) { - $string_types[] = $type; - if (get_class($type) === TString::class) { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } + $string_types[] = $type; } elseif ($type instanceof TCallable) { $string_types[] = new TCallableString; $did_remove_type = true; @@ -858,9 +866,7 @@ private static function reconcileString( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasString() || $type->as->hasMixed() || $type->as->hasScalar()) { - $type = clone $type; - - $type->as = self::reconcileString( + $type = $type->replaceAs(self::reconcileString( $assertion, $type->as, null, @@ -869,7 +875,7 @@ private static function reconcileString( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $string_types[] = $type; } @@ -935,12 +941,12 @@ private static function reconcileInt( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TInt) { - $int_types[] = $type; - if (get_class($type) === TInt::class) { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } + $int_types[] = $type; + if ($existing_var_type->from_calculation) { $did_remove_type = true; } @@ -952,9 +958,7 @@ private static function reconcileInt( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasInt() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileInt( + $type = $type->replaceAs(self::reconcileInt( $assertion, $type->as, null, @@ -962,7 +966,7 @@ private static function reconcileInt( null, $suppressed_issues, $failed_reconciliation - ); + )); $int_types[] = $type; } @@ -1028,16 +1032,14 @@ private static function reconcileBool( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TBool) { + $type = $type->setFromDocblock(false); $bool_types[] = $type; - $type->from_docblock = false; } elseif ($type instanceof TScalar) { $bool_types[] = new TBool; $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasBool() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileBool( + $type = $type->replaceAs(self::reconcileBool( $assertion, $type->as, null, @@ -1046,7 +1048,7 @@ private static function reconcileBool( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $bool_types[] = $type; } @@ -1112,9 +1114,7 @@ private static function reconcileScalar( $scalar_types[] = $type; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasScalar() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileScalar( + $type = $type->replaceAs(self::reconcileScalar( $assertion, $type->as, null, @@ -1123,7 +1123,7 @@ private static function reconcileScalar( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $scalar_types[] = $type; } @@ -1177,6 +1177,7 @@ private static function reconcileNumeric( if ($existing_var_type->hasMixed()) { return Type::getNumeric(); } + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); @@ -1206,9 +1207,7 @@ private static function reconcileNumeric( $numeric_types[] = new TNumericString(); } elseif ($type instanceof TTemplateParam) { if ($type->as->hasNumeric() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileNumeric( + $type = $type->replaceAs(self::reconcileNumeric( $assertion, $type->as, null, @@ -1217,7 +1216,7 @@ private static function reconcileNumeric( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $numeric_types[] = $type; } @@ -1287,15 +1286,12 @@ private static function reconcileObject( } elseif ($type instanceof TTemplateParam && $type->as->isMixed() ) { - $type = clone $type; - $type->as = Type::getObject(); + $type = $type->replaceAs(Type::getObject()); $object_types[] = $type; $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasObject() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileObject( + $type = $type->replaceAs(self::reconcileObject( $assertion, $type->as, null, @@ -1304,18 +1300,20 @@ private static function reconcileObject( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $object_types[] = $type; } $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); - $object_types[] = new TGenericObject('Traversable', $clone_type->type_params); + $object_types[] = new TGenericObject( + 'Traversable', + $params + ); $did_remove_type = true; } else { @@ -1445,7 +1443,7 @@ private static function reconcileCountable( $did_remove_type = true; } elseif ($type instanceof TNamedObject || $type instanceof TIterable) { $countable = new TNamedObject('Countable'); - $type->extra_types[$countable->getKey()] = $countable; + $type = $type->addIntersectionType($countable); $iterable_types[] = $type; $did_remove_type = true; } else { @@ -1594,7 +1592,8 @@ private static function reconcileHasArrayKey( HasArrayKey $assertion ): Union { $assertion = $assertion->key; - foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + $types = $existing_var_type->getAtomicTypes(); + foreach ($types as &$atomic_type) { if ($atomic_type instanceof TKeyedArray) { $is_class_string = false; @@ -1606,16 +1605,24 @@ private static function reconcileHasArrayKey( if (isset($atomic_type->properties[$assertion])) { $atomic_type->properties[$assertion]->possibly_undefined = false; } else { - $atomic_type->properties[$assertion] = Type::getMixed(); - - if ($is_class_string) { - $atomic_type->class_strings[$assertion] = true; - } + $atomic_type = new TKeyedArray( + array_merge( + $atomic_type->properties, + [$assertion => Type::getMixed()] + ), + $is_class_string ? array_merge( + $atomic_type->class_strings ?? [], + [$assertion => true] + ) : $atomic_type->class_strings, + $atomic_type->sealed, + $atomic_type->previous_key_type, + $atomic_type->previous_value_type, + $atomic_type->is_list + ); } } } - - return $existing_var_type; + return $existing_var_type->setTypes($types); } /** @@ -1631,6 +1638,7 @@ private static function reconcileIsGreaterThan( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); //we add 1 from the assertion value because we're on a strict operator $assertion_value = $assertion->value + 1; @@ -1651,15 +1659,19 @@ private static function reconcileIsGreaterThan( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->min_bound === null) { - $atomic_type->min_bound = $assertion_value; + $min_bound = $atomic_type->min_bound; + if ($min_bound === null) { + $min_bound = $assertion_value; } else { - $atomic_type->min_bound = TIntRange::getNewHighestBound( + $min_bound = TIntRange::getNewHighestBound( $assertion_value, - $atomic_type->min_bound + $min_bound ); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $min_bound, + $atomic_type->max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the type must be removed $did_remove_type = true; @@ -1720,7 +1732,7 @@ private static function reconcileIsGreaterThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1738,6 +1750,7 @@ private static function reconcileIsLessThan( ): Union { //we remove 1 from the assertion value because we're on a strict operator $assertion_value = $assertion->value - 1; + $existing_var_type = $existing_var_type->getBuilder(); $did_remove_type = false; @@ -1756,12 +1769,16 @@ private static function reconcileIsLessThan( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->max_bound === null) { - $atomic_type->max_bound = $assertion_value; + $max_bound = $atomic_type->max_bound; + if ($max_bound === null) { + $max_bound = $assertion_value; } else { - $atomic_type->max_bound = min($atomic_type->max_bound, $assertion_value); + $max_bound = min($max_bound, $assertion_value); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $atomic_type->min_bound, + $max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the check is redundant } elseif ($atomic_type->isGreaterThan($assertion_value)) { @@ -1822,7 +1839,7 @@ private static function reconcileIsLessThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1863,7 +1880,7 @@ private static function reconcileTraversable( $did_remove_type = true; } elseif ($type instanceof TNamedObject) { $traversable = new TNamedObject('Traversable'); - $type->extra_types[$traversable->getKey()] = $traversable; + $type = $type->addIntersectionType($traversable); $traversable_types[] = $type; $did_remove_type = true; } else { @@ -1933,18 +1950,14 @@ private static function reconcileArray( $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); - - $array_types[] = new TArray($clone_type->type_params); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); + $array_types[] = new TArray($params); $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasArray() || $type->as->hasIterable() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileArray( + $type = $type->replaceAs(self::reconcileArray( $assertion, $type->as, null, @@ -1953,7 +1966,7 @@ private static function reconcileArray( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $array_types[] = $type; } @@ -2290,9 +2303,7 @@ private static function reconcileCallable( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasCallableType() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileCallable( + $type = $type->replaceAs(self::reconcileCallable( $assertion, $codebase, $type->as, @@ -2302,7 +2313,7 @@ private static function reconcileCallable( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); } $did_remove_type = true; @@ -2354,6 +2365,7 @@ private static function reconcileTruthyOrNonEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); //empty is used a lot to check for array offset existence, so we have to silent errors a lot @@ -2362,12 +2374,12 @@ private static function reconcileTruthyOrNonEmpty( $did_remove_type = $existing_var_type->possibly_undefined || $existing_var_type->possibly_undefined_from_try; - foreach ($existing_var_type->getAtomicTypes() as $existing_var_type_key => $existing_var_type_part) { + foreach ($types as $existing_var_type_key => $existing_var_type_part) { //if any atomic in the union is either always falsy, we remove it. If not always truthy, we mark the check //as not redundant. if ($existing_var_type_part->isFalsy()) { $did_remove_type = true; - $existing_var_type->removeType($existing_var_type_key); + unset($types[$existing_var_type_key]); } elseif ($existing_var_type->possibly_undefined || $existing_var_type->possibly_undefined_from_try || !$existing_var_type_part->isTruthy() @@ -2376,7 +2388,7 @@ private static function reconcileTruthyOrNonEmpty( } } - if ($did_remove_type && $existing_var_type->isUnionEmpty()) { + if ($did_remove_type && !$types) { //every type was removed, this is an impossible assertion if ($code_location && $key && !$is_empty_assertion && !$recursive_check) { self::triggerIssueForImpossible( @@ -2412,74 +2424,66 @@ private static function reconcileTruthyOrNonEmpty( $failed_reconciliation = 1; - return $existing_var_type; + if (!$types) { + throw new AssertionError("We must have some types here!"); + } + return $existing_var_type->setTypes($types); } - $existing_var_type->possibly_undefined = false; - $existing_var_type->possibly_undefined_from_try = false; - - if ($existing_var_type->hasType('bool')) { - $existing_var_type->removeType('bool'); - $existing_var_type->addType(new TTrue()); + if (isset($types['bool'])) { + unset($types['bool']); + $types []= new TTrue; } - if ($existing_var_type->hasArray()) { - $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; + if (isset($types['array'])) { + $array_atomic_type = $types['array']; if ($array_atomic_type instanceof TArray && !$array_atomic_type instanceof TNonEmptyArray ) { - $existing_var_type->removeType('array'); - $existing_var_type->addType( - new TNonEmptyArray( - $array_atomic_type->type_params - ) - ); + unset($types['array']); + $types [] = new TNonEmptyArray($array_atomic_type->type_params); } elseif ($array_atomic_type instanceof TList && !$array_atomic_type instanceof TNonEmptyList ) { - $existing_var_type->removeType('array'); - $existing_var_type->addType( - new TNonEmptyList( - $array_atomic_type->type_param - ) - ); + unset($types['array']); + $types [] = new TNonEmptyList($array_atomic_type->type_param); } } - if ($existing_var_type->hasMixed()) { - $mixed_atomic_type = $existing_var_type->getAtomicTypes()['mixed']; + if (isset($types['mixed'])) { + $mixed_atomic_type = $types['mixed']; if (get_class($mixed_atomic_type) === TMixed::class) { - $existing_var_type->removeType('mixed'); - $existing_var_type->addType(new TNonEmptyMixed()); + unset($types['mixed']); + $types []= new TNonEmptyMixed(); } } - if ($existing_var_type->hasScalar()) { - $scalar_atomic_type = $existing_var_type->getAtomicTypes()['scalar']; + if (isset($types['scalar'])) { + $scalar_atomic_type = $types['scalar']; if (get_class($scalar_atomic_type) === TScalar::class) { - $existing_var_type->removeType('scalar'); - $existing_var_type->addType(new TNonEmptyScalar()); + unset($types['scalar']); + $types []= new TNonEmptyScalar(); } } - if ($existing_var_type->hasType('string')) { - $string_atomic_type = $existing_var_type->getAtomicTypes()['string']; + if (isset($types['string'])) { + $string_atomic_type = $types['string']; if (get_class($string_atomic_type) === TString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonFalsyString()); + unset($types['string']); + $types []= new TNonFalsyString(); } elseif (get_class($string_atomic_type) === TLowercaseString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonEmptyLowercaseString()); + unset($types['string']); + $types []= new TNonEmptyLowercaseString(); } elseif (get_class($string_atomic_type) === TNonspecificLiteralString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonEmptyNonspecificLiteralString()); + unset($types['string']); + $types []= new TNonEmptyNonspecificLiteralString(); } elseif (get_class($string_atomic_type) === TNonEmptyString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonFalsyString()); + unset($types['string']); + $types []= new TNonFalsyString(); } } @@ -2489,30 +2493,24 @@ private static function reconcileTruthyOrNonEmpty( if ($existing_range_types) { foreach ($existing_range_types as $int_key => $literal_type) { if ($literal_type->contains(0)) { - $existing_var_type->removeType($int_key); + unset($types[$int_key]); if ($literal_type->min_bound === null || $literal_type->min_bound <= -1) { - $existing_var_type->addType(new TIntRange($literal_type->min_bound, -1)); + $types []= new TIntRange($literal_type->min_bound, -1); } if ($literal_type->max_bound === null || $literal_type->max_bound >= 1) { - $existing_var_type->addType(new TIntRange(1, $literal_type->max_bound)); + $types []= new TIntRange(1, $literal_type->max_bound); } } } } - - if ($existing_var_type->isSingle()) { - return $existing_var_type; - } } - foreach ($existing_var_type->getAtomicTypes() as $type_key => $existing_var_atomic_type) { + foreach ($types as $type_key => $existing_var_atomic_type) { if ($existing_var_atomic_type instanceof TTemplateParam) { if (!$existing_var_atomic_type->as->isMixed()) { $template_did_fail = 0; - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::reconcileTruthyOrNonEmpty( + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs(self::reconcileTruthyOrNonEmpty( $assertion, $existing_var_atomic_type->as, $key, @@ -2521,18 +2519,29 @@ private static function reconcileTruthyOrNonEmpty( $suppressed_issues, $template_did_fail, true - ); + )); if (!$template_did_fail) { - $existing_var_type->removeType($type_key); - $existing_var_type->addType($existing_var_atomic_type); + unset($types[$type_key]); + $types []= $existing_var_atomic_type; } } } } - assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + if (!$types) { + throw new AssertionError("We must have some types here!"); + } + $new = $existing_var_type->setTypes($types); + if ($new === $existing_var_type && ($new->possibly_undefined || $new->possibly_undefined_from_try)) { + $new = clone $existing_var_type; + $new->possibly_undefined = false; + $new->possibly_undefined_from_try = false; + } else { + $new->possibly_undefined = false; + $new->possibly_undefined_from_try = false; + } + return $new; } /** diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index ff1b3dcdfad..e744f456ad1 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -413,6 +413,7 @@ public static function reconcile( private static function reconcileCallable( Union $existing_var_type ): Union { + $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) { if ($type instanceof TLiteralString && InternalCallMapHandler::inCallMap($type->value) @@ -425,7 +426,7 @@ private static function reconcileCallable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -513,6 +514,7 @@ private static function reconcileNotNonEmptyCountable( bool $is_equality, ?int $min_count ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); @@ -570,7 +572,7 @@ private static function reconcileNotNonEmptyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -587,17 +589,18 @@ private static function reconcileNull( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; - if ($existing_var_type->hasType('null')) { + if (isset($types['null'])) { $did_remove_type = true; - $existing_var_type->removeType('null'); + unset($types['null']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileNull( + $new = $type->replaceAs(self::reconcileNull( $assertion, $type->as, null, @@ -606,14 +609,17 @@ private static function reconcileNull( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); + // $did_remove_type = $did_remove_type || $new !== $type; + // TODO: This is technically wrong, but for some reason we get a + // duplicated assertion here when using template types. $did_remove_type = true; - $existing_var_type->bustCache(); + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -632,8 +638,8 @@ private static function reconcileNull( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -657,17 +663,18 @@ private static function reconcileFalse( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); - $did_remove_type = $existing_var_type->hasScalar(); + $did_remove_type = false; - if ($existing_var_type->hasType('false')) { + if (isset($types['false'])) { $did_remove_type = true; - $existing_var_type->removeType('false'); + unset($types['false']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileFalse( + $new = $type->replaceAs(self::reconcileFalse( $assertion, $type->as, null, @@ -676,14 +683,14 @@ private static function reconcileFalse( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); - $did_remove_type = true; - $existing_var_type->bustCache(); + $did_remove_type = $did_remove_type || $new !== $type; + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -702,8 +709,8 @@ private static function reconcileFalse( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -728,6 +735,7 @@ private static function reconcileFalsyOrEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = $existing_var_type->possibly_undefined @@ -786,7 +794,7 @@ private static function reconcileFalsyOrEmpty( $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } if ($existing_var_type->hasType('bool')) { @@ -872,9 +880,7 @@ private static function reconcileFalsyOrEmpty( if (!$existing_var_atomic_type->as->isMixed()) { $template_did_fail = 0; - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::reconcileFalsyOrEmpty( + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs(self::reconcileFalsyOrEmpty( $assertion, $existing_var_atomic_type->as, $key, @@ -883,7 +889,7 @@ private static function reconcileFalsyOrEmpty( $suppressed_issues, $template_did_fail, $recursive_check - ); + )); if (!$template_did_fail) { $existing_var_type->removeType($type_key); @@ -893,8 +899,9 @@ private static function reconcileFalsyOrEmpty( } } + /** @psalm-suppress RedundantCondition Psalm bug */ assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -920,9 +927,7 @@ private static function reconcileScalar( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileScalar( + $type = $type->replaceAs(self::reconcileScalar( $assertion, $type->as, null, @@ -931,7 +936,7 @@ private static function reconcileScalar( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1010,9 +1015,7 @@ private static function reconcileObject( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileObject( + $type = $type->replaceAs(self::reconcileObject( $assertion, $type->as, null, @@ -1021,7 +1024,7 @@ private static function reconcileObject( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1040,11 +1043,9 @@ private static function reconcileObject( $non_object_types[] = new TCallableString(); $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); - - $non_object_types[] = new TArray($clone_type->type_params); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); + $non_object_types[] = new TArray($params); $did_remove_type = true; } elseif (!$type->isObjectType()) { @@ -1116,9 +1117,7 @@ private static function reconcileNumeric( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileNumeric( + $type = $type->replaceAs(self::reconcileNumeric( $assertion, $type->as, null, @@ -1127,7 +1126,7 @@ private static function reconcileNumeric( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1209,9 +1208,7 @@ private static function reconcileInt( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileInt( + $type = $type->replaceAs(self::reconcileInt( $assertion, $type->as, null, @@ -1220,7 +1217,7 @@ private static function reconcileInt( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1313,9 +1310,7 @@ private static function reconcileFloat( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileFloat( + $type = $type->replaceAs(self::reconcileFloat( $assertion, $type->as, null, @@ -1324,7 +1319,7 @@ private static function reconcileFloat( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1412,9 +1407,7 @@ private static function reconcileString( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileString( + $type = $type->replaceAs(self::reconcileString( $assertion, $type->as, null, @@ -1423,7 +1416,7 @@ private static function reconcileString( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1520,9 +1513,7 @@ private static function reconcileArray( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileArray( + $type = $type->replaceAs(self::reconcileArray( $assertion, $type->as, null, @@ -1531,7 +1522,7 @@ private static function reconcileArray( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1616,17 +1607,18 @@ private static function reconcileResource( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; - if ($existing_var_type->hasType('resource')) { + if (isset($types['resource'])) { $did_remove_type = true; - $existing_var_type->removeType('resource'); + unset($types['resource']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileResource( + $new = $type->replaceAs(self::reconcileResource( $assertion, $type->as, null, @@ -1635,14 +1627,14 @@ private static function reconcileResource( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); - $did_remove_type = true; - $existing_var_type->bustCache(); + $did_remove_type = $new !== $type; + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -1661,8 +1653,8 @@ private static function reconcileResource( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1685,6 +1677,7 @@ private static function reconcileIsLessThanOrEqualTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $assertion_value = $assertion->value; $did_remove_type = false; @@ -1705,14 +1698,17 @@ private static function reconcileIsLessThanOrEqualTo( $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); if ($atomic_type->max_bound === null) { - $atomic_type->max_bound = $assertion_value; + $max_bound = $assertion_value; } else { - $atomic_type->max_bound = TIntRange::getNewLowestBound( + $max_bound = TIntRange::getNewLowestBound( $assertion_value, $atomic_type->max_bound ); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $atomic_type->min_bound, + $max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the check is redundant } elseif ($atomic_type->isGreaterThan($assertion_value)) { @@ -1773,7 +1769,7 @@ private static function reconcileIsLessThanOrEqualTo( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1789,6 +1785,7 @@ private static function reconcileIsGreaterThanOrEqualTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $assertion_value = $assertion->value; $did_remove_type = false; @@ -1808,12 +1805,16 @@ private static function reconcileIsGreaterThanOrEqualTo( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->min_bound === null) { - $atomic_type->min_bound = $assertion_value; + $min_bound = $atomic_type->min_bound; + if ($min_bound === null) { + $min_bound = $assertion_value; } else { - $atomic_type->min_bound = max($atomic_type->min_bound, $assertion_value); + $min_bound = max($min_bound, $assertion_value); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $min_bound, + $atomic_type->max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the type must be removed $did_remove_type = true; @@ -1874,6 +1875,6 @@ private static function reconcileIsGreaterThanOrEqualTo( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 6d4d753157b..7e0e76f0597 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -42,22 +42,25 @@ class TemplateInferredTypeReplacer { /** * This replaces template types in unions with the inferred types they should be + * + * @psalm-external-mutation-free */ public static function replace( Union $union, TemplateResult $template_result, ?Codebase $codebase - ): void { - $keys_to_unset = []; - + ): Union { $new_types = []; $is_mixed = false; $inferred_lower_bounds = $template_result->lower_bounds ?: []; + $types = []; + foreach ($union->getAtomicTypes() as $key => $atomic_type) { - $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $should_set = true; + $atomic_type = $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); if ($atomic_type instanceof TTemplateParam) { $template_type = self::replaceTemplateParam( @@ -68,7 +71,7 @@ public static function replace( ); if ($template_type) { - $keys_to_unset[] = $key; + $should_set = false; foreach ($template_type->getAtomicTypes() as $template_type_part) { if ($template_type_part instanceof TMixed) { @@ -113,11 +116,11 @@ public static function replace( } if ($class_template_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $class_template_type; } } elseif ($atomic_type instanceof TTemplateIndexedAccess) { - $keys_to_unset[] = $key; + $should_set = false; $template_type = null; @@ -175,7 +178,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TTemplatePropertiesOf) { @@ -186,7 +189,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TConditional @@ -199,43 +202,41 @@ public static function replace( $inferred_lower_bounds ); - $keys_to_unset[] = $key; + $should_set = false; foreach ($class_template_type->getAtomicTypes() as $class_template_atomic_type) { $new_types[] = $class_template_atomic_type; } } + if ($should_set) { + $types []= $atomic_type; + } } - $union->bustCache(); - if ($is_mixed) { if (!$new_types) { throw new UnexpectedValueException('This array should be full'); } - $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $new_types, $codebase )->getAtomicTypes() - ); - - return; + )->freeze(); } - foreach ($keys_to_unset as $key) { - $union->removeType($key); + $atomic_types = array_merge($types, $new_types); + if (!$atomic_types) { + throw new UnexpectedValueException('This array should be full'); } - $atomic_types = array_values(array_merge($union->getAtomicTypes(), $new_types)); - - $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $atomic_types, $codebase )->getAtomicTypes() - ); + )->freeze(); } /** @@ -260,34 +261,35 @@ private static function replaceTemplateParam( if ($traversed_type) { $template_type = $traversed_type; - if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { - $template_type = clone $atomic_type->as; - } else { - $template_type = clone $template_type; + if ($template_type->isMixed() && !$atomic_type->as->isMixed()) { + $template_type = $atomic_type->as; } if ($atomic_type->extra_types) { - foreach ($template_type->getAtomicTypes() as $template_type_key => $atomic_template_type) { + $types = []; + foreach ($template_type->getAtomicTypes() as $atomic_template_type) { if ($atomic_template_type instanceof TNamedObject || $atomic_template_type instanceof TTemplateParam || $atomic_template_type instanceof TIterable || $atomic_template_type instanceof TObjectWithProperties ) { - $atomic_template_type->extra_types = array_merge( + $types []= $atomic_template_type->setIntersectionTypes(array_merge( $atomic_type->extra_types, - $atomic_template_type->extra_types ?: [] - ); + $atomic_template_type->extra_types + )); } elseif ($atomic_template_type instanceof TObject) { $first_atomic_type = array_shift($atomic_type->extra_types); if ($atomic_type->extra_types) { - $first_atomic_type->extra_types = $atomic_type->extra_types; + $first_atomic_type = $first_atomic_type->setIntersectionTypes($atomic_type->extra_types); } - $template_type->removeType($template_type_key); - $template_type->addType($first_atomic_type); + $types []= $first_atomic_type; + } else { + $types []= $atomic_template_type; } } + $template_type = $template_type->getBuilder()->setTypes($types)->freeze(); } } elseif ($codebase) { foreach ($inferred_lower_bounds as $template_type_map) { @@ -382,8 +384,7 @@ private static function replaceTemplatePropertiesOf( } return new TPropertiesOf( - (string) $classlike_type, - clone $classlike_type, + $classlike_type, $atomic_type->visibility_filter ); } @@ -394,7 +395,7 @@ private static function replaceTemplatePropertiesOf( private static function replaceConditional( TemplateResult $template_result, Codebase $codebase, - TConditional $atomic_type, + TConditional &$atomic_type, array $inferred_lower_bounds ): Union { $template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) @@ -407,16 +408,19 @@ private static function replaceConditional( $if_template_type = null; $else_template_type = null; - $atomic_type = clone $atomic_type; + $as_type = $atomic_type->as_type; + $conditional_type = $atomic_type->conditional_type; + $if_type = $atomic_type->if_type; + $else_type = $atomic_type->else_type; if ($template_type) { - self::replace( - $atomic_type->as_type, + $as_type = self::replace( + $as_type, $template_result, $codebase ); - if ($atomic_type->as_type->isNullable() && $template_type->isVoid()) { + if ($as_type->isNullable() && $template_type->isVoid()) { $template_type = Type::getNull(); } @@ -427,7 +431,7 @@ private static function replaceConditional( if (UnionTypeComparator::isContainedBy( $codebase, new Union([$candidate_atomic_type]), - $atomic_type->conditional_type, + $conditional_type, false, false, null, @@ -435,12 +439,12 @@ private static function replaceConditional( false ) && (!$candidate_atomic_type instanceof TInt - || $atomic_type->conditional_type->getId() !== 'float') + || $conditional_type->getId() !== 'float') ) { $matching_if_types[] = $candidate_atomic_type; } elseif (!UnionTypeComparator::isContainedBy( $codebase, - $atomic_type->conditional_type, + $conditional_type, new Union([$candidate_atomic_type]), false, false, @@ -459,7 +463,7 @@ private static function replaceConditional( && UnionTypeComparator::isContainedBy( $codebase, $if_candidate_type, - $atomic_type->conditional_type, + $conditional_type, false, false, null, @@ -467,7 +471,7 @@ private static function replaceConditional( false ) ) { - $if_template_type = clone $atomic_type->if_type; + $if_template_type = clone $if_type; $refined_template_result = clone $template_result; @@ -478,7 +482,7 @@ private static function replaceConditional( ) ]; - self::replace( + $if_template_type = self::replace( $if_template_type, $refined_template_result, $codebase @@ -489,7 +493,7 @@ private static function replaceConditional( && UnionTypeComparator::isContainedBy( $codebase, $else_candidate_type, - $atomic_type->as_type, + $as_type, false, false, null, @@ -497,7 +501,7 @@ private static function replaceConditional( false ) ) { - $else_template_type = clone $atomic_type->else_type; + $else_template_type = clone $else_type; $refined_template_result = clone $template_result; @@ -508,7 +512,7 @@ private static function replaceConditional( ) ]; - self::replace( + $else_template_type = self::replace( $else_template_type, $refined_template_result, $codebase @@ -517,21 +521,21 @@ private static function replaceConditional( } if (!$if_template_type && !$else_template_type) { - self::replace( - $atomic_type->if_type, + $if_type = self::replace( + $if_type, $template_result, $codebase ); - self::replace( - $atomic_type->else_type, + $else_type = self::replace( + $else_type, $template_result, $codebase ); $class_template_type = Type::combineUnionTypes( - $atomic_type->if_type, - $atomic_type->else_type, + $if_type, + $else_type, $codebase ); } else { @@ -542,6 +546,13 @@ private static function replaceConditional( ); } + $atomic_type = $atomic_type->replaceTypes( + $as_type, + $conditional_type, + $if_type, + $else_type + ); + return $class_template_type; } } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 5b3341d5570..68a61684cf0 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -54,6 +54,39 @@ */ class TemplateStandinTypeReplacer { + /** + * This method fills in the values in $template_result based on how the various atomic types + * of $union_type match up to the types inside $input_type. + */ + public static function fillTemplateResult( + Union $union_type, + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer, + ?Union $input_type, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + ?string $bound_equality_classlike = null, + int $depth = 1 + ): void { + self::replace( + $union_type, + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $bound_equality_classlike, + $depth + ); + } /** * This replaces template types in unions with standins (normally the template as type) * @@ -61,6 +94,7 @@ class TemplateStandinTypeReplacer * * This method fills in the values in $template_result based on how the various atomic types * of $union_type match up to the types inside $input_type + * */ public static function replace( Union $union_type, @@ -84,7 +118,7 @@ public static function replace( // when they're also in the union type, so those shared atomic // types will never be inferred as part of the generic type if ($input_type && !$input_type->isSingle()) { - $new_input_type = clone $input_type; + $new_input_type = $input_type->getBuilder(); foreach ($original_atomic_types as $key => $_) { if ($new_input_type->hasType($key)) { @@ -93,7 +127,7 @@ public static function replace( } if (!$new_input_type->isUnionEmpty()) { - $input_type = $new_input_type; + $input_type = $new_input_type->freeze(); } else { return $union_type; } @@ -340,9 +374,9 @@ private static function handleAtomicStandin( return [$atomic_type]; } + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, $atomic_type is not a reference */ $atomic_type = new TPropertiesOf( - (string) $classlike_type, - clone $classlike_type, + $classlike_type, $atomic_type->visibility_filter ); return [$atomic_type]; @@ -361,6 +395,7 @@ private static function handleAtomicStandin( } if (!$matching_atomic_types) { + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, $atomic_type is not a reference */ $atomic_type = $atomic_type->replaceTemplateTypesWithStandins( $template_result, $codebase, @@ -566,7 +601,7 @@ private static function findMatchingAtomicTypesForTemplate( * @return list */ private static function handleTemplateParamStandin( - TTemplateParam $atomic_type, + TTemplateParam &$atomic_type, string $key, ?Union $input_type, ?int $input_arg_offset, @@ -729,7 +764,7 @@ private static function handleTemplateParamStandin( $matching_input_keys = []; - $atomic_type->as = TypeExpander::expandUnion( + $as = TypeExpander::expandUnion( $codebase, $atomic_type->as, $calling_class, @@ -737,8 +772,8 @@ private static function handleTemplateParamStandin( null ); - $atomic_type->as = self::replace( - $atomic_type->as, + $as = self::replace( + $as, $template_result, $codebase, $statements_analyzer, @@ -752,6 +787,8 @@ private static function handleTemplateParamStandin( $depth + 1 ); + $atomic_type = $atomic_type->replaceAs($as); + if ($input_type && !$template_result->readonly && ( @@ -766,7 +803,7 @@ private static function handleTemplateParamStandin( ) ) ) { - $generic_param = clone $input_type; + $generic_param = $input_type->getBuilder(); if ($matching_input_keys) { $generic_param_keys = array_keys($generic_param->getAtomicTypes()); @@ -777,12 +814,11 @@ private static function handleTemplateParamStandin( } } } - if ($add_lower_bound) { return array_values($generic_param->getAtomicTypes()); } - $generic_param->setFromDocblock(); + $generic_param = $generic_param->setFromDocblock()->freeze(); if (isset( $template_result->lower_bounds[$param_name_key][$atomic_type->defining_class] @@ -830,16 +866,15 @@ private static function handleTemplateParamStandin( } } - foreach ($atomic_types as &$atomic_type) { - if ($atomic_type instanceof TNamedObject - || $atomic_type instanceof TTemplateParam - || $atomic_type instanceof TIterable - || $atomic_type instanceof TObjectWithProperties + foreach ($atomic_types as &$t) { + if ($t instanceof TNamedObject + || $t instanceof TTemplateParam + || $t instanceof TIterable + || $t instanceof TObjectWithProperties ) { - $atomic_type->extra_types = $extra_types; - } elseif ($atomic_type instanceof TObject && $extra_types) { - $atomic_type = reset($extra_types); - $atomic_type->extra_types = array_slice($extra_types, 1); + $t = $t->setIntersectionTypes($extra_types); + } elseif ($t instanceof TObject && $extra_types) { + $t = reset($extra_types)->setIntersectionTypes(array_slice($extra_types, 1)); } } @@ -858,7 +893,7 @@ private static function handleTemplateParamStandin( $matching_input_keys ) ) { - $generic_param = clone $input_type; + $generic_param = $input_type->getBuilder(); if ($matching_input_keys) { $generic_param_keys = array_keys($generic_param->getAtomicTypes()); @@ -869,6 +904,7 @@ private static function handleTemplateParamStandin( } } } + $generic_param = $generic_param->freeze(); $upper_bound = $template_result->upper_bounds [$param_name_key] @@ -936,13 +972,18 @@ public static function handleTemplateParamClassStandin( $atomic_types = []; + $as_type = $atomic_type->as_type; if ($input_type && !$template_result->readonly) { $valid_input_atomic_types = []; foreach ($input_type->getAtomicTypes() as $input_atomic_type) { if ($input_atomic_type instanceof TLiteralClassString) { $valid_input_atomic_types[] = new TNamedObject( - $input_atomic_type->value + $input_atomic_type->value, + false, + false, + [], + true ); } elseif ($input_atomic_type instanceof TTemplateParamClass) { $valid_input_atomic_types[] = new TTemplateParam( @@ -952,20 +993,26 @@ public static function handleTemplateParamClassStandin( : ($input_atomic_type->as === 'object' ? Type::getObject() : Type::getMixed()), - $input_atomic_type->defining_class + $input_atomic_type->defining_class, + [], + true ); } elseif ($input_atomic_type instanceof TClassString) { if ($input_atomic_type->as_type) { - $valid_input_atomic_types[] = clone $input_atomic_type->as_type; + $valid_input_atomic_types[] = $input_atomic_type->as_type->setFromDocblock(true); } elseif ($input_atomic_type->as !== 'object') { $valid_input_atomic_types[] = new TNamedObject( - $input_atomic_type->as + $input_atomic_type->as, + false, + false, + [], + true ); } else { - $valid_input_atomic_types[] = new TObject(); + $valid_input_atomic_types[] = new TObject(true); } } elseif ($input_atomic_type instanceof TDependentGetClass) { - $valid_input_atomic_types[] = new TObject(); + $valid_input_atomic_types[] = new TObject(true); } } @@ -973,16 +1020,15 @@ public static function handleTemplateParamClassStandin( if ($valid_input_atomic_types) { $generic_param = new Union($valid_input_atomic_types); - $generic_param->setFromDocblock(); } elseif ($was_single) { $generic_param = Type::getMixed(); } - if ($atomic_type->as_type) { + if ($as_type) { // sometimes templated class-strings can contain nested templates // in the as type that need to be resolved as well. $as_type_union = self::replace( - new Union([$atomic_type->as_type]), + new Union([$as_type]), $template_result, $codebase, $statements_analyzer, @@ -999,9 +1045,9 @@ public static function handleTemplateParamClassStandin( $first = $as_type_union->getSingleAtomic(); if (count($as_type_union->getAtomicTypes()) === 1 && $first instanceof TNamedObject) { - $atomic_type->as_type = $first; + $as_type = $first; } else { - $atomic_type->as_type = null; + $as_type = null; } } @@ -1046,7 +1092,7 @@ public static function handleTemplateParamClassStandin( } } - $class_string = new TClassString($atomic_type->as, $atomic_type->as_type); + $class_string = new TClassString($atomic_type->as, $as_type); if (!$atomic_types) { $atomic_types[] = $class_string; @@ -1155,6 +1201,7 @@ public static function getMostSpecificTypeFromBounds(array $lower_bounds, ?Codeb /** * @param TGenericObject|TNamedObject|TIterable $input_type_part * @param TGenericObject|TIterable $container_type_part + * @psalm-external-mutation-free * @return list */ public static function getMappedGenericTypeParams( @@ -1165,7 +1212,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 +1224,8 @@ public static function getMappedGenericTypeParams( } else { $input_type_params = array_fill(0, count($class_storage->template_types ?? []), Type::getMixed()); } + } else { + $input_type_params = []; } try { @@ -1258,7 +1307,7 @@ public static function getMappedGenericTypeParams( $new_input_param = clone $new_input_param; - TemplateInferredTypeReplacer::replace( + $new_input_param = TemplateInferredTypeReplacer::replace( $new_input_param, new TemplateResult([], $replacement_templates), $codebase diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index c7770a32c26..2d7c74b4af5 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -81,9 +81,9 @@ class TypeCombination public $class_string_types = []; /** - * @var array|null + * @var array */ - public $extra_types; + public $extra_types = []; /** @var ?bool */ public $all_arrays_lists; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 6d25b1f40c2..b0348134cdf 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -85,10 +85,13 @@ class TypeCombiner * - and `array + array = array` * - and `array + array = array` * + * @psalm-external-mutation-free + * + * @psalm-suppress ImpurePropertyAssignment We're not actually mutating any external instance + * * @param non-empty-list $types * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar - * */ public static function combine( array $types, @@ -98,13 +101,7 @@ public static function combine( int $literal_limit = 500 ): Union { if (count($types) === 1) { - $union_type = new Union([$types[0]]); - - if ($types[0]->from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return new Union([$types[0]], $types[0]->from_docblock); } $combination = new TypeCombination(); @@ -144,23 +141,11 @@ public static function combine( && !$combination->floats ) { if (isset($combination->value_types['false'])) { - $union_type = Type::getFalse(); - - if ($from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return Type::getFalse($from_docblock); } if (isset($combination->value_types['true'])) { - $union_type = Type::getTrue(); - - if ($from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return Type::getTrue($from_docblock); } } elseif (isset($combination->value_types['void'])) { unset($combination->value_types['void']); @@ -253,10 +238,14 @@ public static function combine( if ($generic_type === 'iterable') { $new_types[] = new TIterable($generic_type_params); } else { - $generic_object = new TGenericObject($generic_type, $generic_type_params); - - /** @psalm-suppress PropertyTypeCoercion */ - $generic_object->extra_types = $combination->extra_types; + /** @psalm-suppress ArgumentTypeCoercion Caused by the PropertyTypeCoercion above */ + $generic_object = new TGenericObject( + $generic_type, + $generic_type_params, + false, + false, + $combination->extra_types + ); $new_types[] = $generic_object; if ($combination->named_object_types) { @@ -268,14 +257,15 @@ public static function combine( foreach ($combination->object_type_params as $generic_type => $generic_type_params) { $generic_type = substr($generic_type, 0, (int) strpos($generic_type, '<')); - $generic_object = new TGenericObject($generic_type, $generic_type_params); - - if ($combination->object_static[$generic_type] ?? false) { - $generic_object->is_static = true; - } + /** @psalm-suppress ArgumentTypeCoercion Caused by the PropertyTypeCoercion above */ + $generic_object = new TGenericObject( + $generic_type, + $generic_type_params, + false, + $combination->object_static[$generic_type] ?? false, + $combination->extra_types + ); - /** @psalm-suppress PropertyTypeCoercion */ - $generic_object->extra_types = $combination->extra_types; $new_types[] = $generic_object; } @@ -376,6 +366,9 @@ public static function combine( return $union_type; } + /** + * @psalm-suppress ComplexMethod Unavoidably complex method + */ private static function scrapeTypeProperties( Atomic $type, TypeCombination $combination, @@ -511,7 +504,7 @@ private static function scrapeTypeProperties( ) { if ($type->extra_types) { $combination->extra_types = array_merge( - $combination->extra_types ?: [], + $combination->extra_types, $type->extra_types ); } @@ -795,11 +788,12 @@ private static function scrapeTypeProperties( $existing_template_type = $combination->value_types[$type_key]; if (!$existing_template_type->as->equals($type->as)) { - $existing_template_type->as = Type::combineUnionTypes( - clone $type->as, + $existing_template_type = $existing_template_type->replaceAs(Type::combineUnionTypes( + $type->as, $existing_template_type->as, $codebase - ); + )); + $combination->value_types[$type_key] = $existing_template_type; } return null; @@ -1180,18 +1174,28 @@ private static function scrapeIntProperties( if (isset($combination->value_types['int'])) { $current_int_type = $combination->value_types['int']; if ($current_int_type instanceof TIntRange) { + $min_bound = $current_int_type->min_bound; + $max_bound = $current_int_type->max_bound; foreach ($combination->ints as $int) { if (!$current_int_type->contains($int->value)) { - $current_int_type->min_bound = TIntRange::getNewLowestBound( - $current_int_type->min_bound, + $min_bound = TIntRange::getNewLowestBound( + $min_bound, $int->value ); - $current_int_type->max_bound = TIntRange::getNewHighestBound( - $current_int_type->max_bound, + $max_bound = TIntRange::getNewHighestBound( + $max_bound, $int->value ); } } + if ($min_bound !== $current_int_type->min_bound + || $max_bound !== $current_int_type->max_bound + ) { + $combination->value_types['int'] = new TIntRange( + $min_bound, + $max_bound + ); + } } } @@ -1214,14 +1218,16 @@ private static function scrapeIntProperties( $combination->value_types['int'] = new TInt(); } } elseif ($type instanceof TIntRange) { - $type = clone $type; + $min_bound = $type->min_bound; + $max_bound = $type->max_bound; if ($combination->ints) { foreach ($combination->ints as $int) { if (!$type->contains($int->value)) { - $type->min_bound = TIntRange::getNewLowestBound($type->min_bound, $int->value); - $type->max_bound = TIntRange::getNewHighestBound($type->max_bound, $int->value); + $min_bound = TIntRange::getNewLowestBound($min_bound, $int->value); + $max_bound = TIntRange::getNewHighestBound($max_bound, $int->value); } } + $type = new TIntRange($min_bound, $max_bound); $combination->value_types['int'] = $type; } elseif (!isset($combination->value_types['int'])) { @@ -1229,8 +1235,9 @@ private static function scrapeIntProperties( } else { $old_type = $combination->value_types['int']; if ($old_type instanceof TIntRange) { - $type->min_bound = TIntRange::getNewLowestBound($old_type->min_bound, $type->min_bound); - $type->max_bound = TIntRange::getNewHighestBound($old_type->max_bound, $type->max_bound); + $min_bound = TIntRange::getNewLowestBound($old_type->min_bound, $min_bound); + $max_bound = TIntRange::getNewHighestBound($old_type->max_bound, $max_bound); + $type = new TIntRange($min_bound, $max_bound); } else { $type = new TInt(); } @@ -1356,34 +1363,42 @@ private static function handleKeyedArrayEntries( } if ($combination->objectlike_entries) { - if ($combination->all_arrays_callable) { - $objectlike = new TCallableKeyedArray($combination->objectlike_entries); - } else { - $objectlike = new TKeyedArray($combination->objectlike_entries); - } - - if ($combination->objectlike_sealed && !$combination->array_type_params) { - $objectlike->sealed = true; - } - + $previous_key_type = null; if ($combination->objectlike_key_type) { - $objectlike->previous_key_type = $combination->objectlike_key_type; + $previous_key_type = $combination->objectlike_key_type; } elseif ($combination->array_type_params && $combination->array_type_params[0]->isArrayKey() ) { - $objectlike->previous_key_type = $combination->array_type_params[0]; + $previous_key_type = $combination->array_type_params[0]; } + $previous_value_type = null; if ($combination->objectlike_value_type) { - $objectlike->previous_value_type = $combination->objectlike_value_type; + $previous_value_type = $combination->objectlike_value_type; } elseif ($combination->array_type_params && $combination->array_type_params[1]->isMixed() ) { - $objectlike->previous_value_type = $combination->array_type_params[1]; + $previous_value_type = $combination->array_type_params[1]; } - if ($combination->all_arrays_lists) { - $objectlike->is_list = true; + if ($combination->all_arrays_callable) { + $objectlike = new TCallableKeyedArray( + $combination->objectlike_entries, + null, + $combination->objectlike_sealed && !$combination->array_type_params, + $previous_key_type, + $previous_value_type, + (bool)$combination->all_arrays_lists + ); + } else { + $objectlike = new TKeyedArray( + $combination->objectlike_entries, + null, + $combination->objectlike_sealed && !$combination->array_type_params, + $previous_key_type, + $previous_value_type, + (bool)$combination->all_arrays_lists + ); } $new_types[] = $objectlike; @@ -1488,35 +1503,37 @@ private static function getArrayTypeFromGenericParams( if ($combination->objectlike_entries && $combination->objectlike_sealed ) { - $array_type = new TKeyedArray([$generic_type_params[1]]); - $array_type->previous_key_type = Type::getInt(); - $array_type->previous_value_type = $combination->array_type_params[1]; - $array_type->is_list = true; + $array_type = new TKeyedArray( + [$generic_type_params[1]], + null, + false, + Type::getInt(), + $combination->array_type_params[1], + true + ); } else { - $array_type = new TNonEmptyList($generic_type_params[1]); - - if ($combination->array_counts && count($combination->array_counts) === 1) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->count = array_keys($combination->array_counts)[0]; - } - - if ($combination->array_min_counts) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->min_count = min(array_keys($combination->array_min_counts)); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $array_type = new TNonEmptyList( + $generic_type_params[1], + $combination->array_counts && count($combination->array_counts) === 1 + ? array_keys($combination->array_counts)[0] + : null, + $combination->array_min_counts + ? min(array_keys($combination->array_min_counts)) + : null + ); } } else { - $array_type = new TNonEmptyArray($generic_type_params); - - if ($combination->array_counts && count($combination->array_counts) === 1) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->count = array_keys($combination->array_counts)[0]; - } - - if ($combination->array_min_counts) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->min_count = min(array_keys($combination->array_min_counts)); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $array_type = new TNonEmptyArray( + $generic_type_params, + $combination->array_counts && count($combination->array_counts) === 1 + ? array_keys($combination->array_counts)[0] + : null, + $combination->array_min_counts + ? min(array_keys($combination->array_min_counts)) + : null + ); } } else { if ($combination->all_arrays_class_string_maps diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index babaf30aa77..404ed996f34 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -5,11 +5,11 @@ use Psalm\Codebase; use Psalm\Exception\CircularReferenceException; use Psalm\Exception\UnresolvableConstantException; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer; use Psalm\Internal\Type\SimpleAssertionReconciler; use Psalm\Internal\Type\SimpleNegatedAssertionReconciler; use Psalm\Internal\Type\TypeParser; use Psalm\Storage\Assertion\IsType; -use Psalm\Storage\PropertyStorage; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; @@ -58,7 +58,7 @@ class TypeExpander { /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type */ public static function expandUnion( Codebase $codebase, @@ -126,10 +126,11 @@ public static function expandUnion( } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type - * + * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param-out Atomic $return_type * @return non-empty-list * + * @psalm-suppress ConflictingReferenceConstraint, ReferenceConstraintViolation The output type is always Atomic * @psalm-suppress ComplexMethod */ public static function expandAtomic( @@ -151,7 +152,8 @@ public static function expandAtomic( if ($return_type->extra_types) { $new_intersection_types = []; - foreach ($return_type->extra_types as &$extra_type) { + $extra_types = []; + foreach ($return_type->extra_types as $extra_type) { self::expandAtomic( $codebase, $extra_type, @@ -170,13 +172,13 @@ public static function expandAtomic( $new_intersection_types, $extra_type->extra_types ); - $extra_type->extra_types = []; + $extra_type = $extra_type->setIntersectionTypes([]); } + $extra_types[$extra_type->getKey()] = $extra_type; } - if ($new_intersection_types) { - $return_type->extra_types = array_merge($return_type->extra_types, $new_intersection_types); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $return_type = $return_type->setIntersectionTypes(array_merge($extra_types, $new_intersection_types)); } if ($return_type instanceof TNamedObject) { @@ -195,7 +197,7 @@ public static function expandAtomic( if ($return_type instanceof TClassString && $return_type->as_type ) { - $new_as_type = clone $return_type->as_type; + $new_as_type = $return_type->as_type; self::expandAtomic( $codebase, @@ -211,9 +213,12 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); - if ($new_as_type instanceof TNamedObject) { + if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on clone */ $return_type->as_type = $new_as_type; - $return_type->as = $return_type->as_type->value; + /** @psalm-suppress InaccessibleProperty Acting on clone */ + $return_type->as = $new_as_type->value; } } elseif ($return_type instanceof TTemplateParam) { $new_as_type = self::expandUnion( @@ -234,16 +239,21 @@ public static function expandAtomic( return array_values($new_as_type->getAtomicTypes()); } - $return_type->as = $new_as_type; + $return_type = $return_type->replaceAs($new_as_type); } if ($return_type instanceof TClassConstant) { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; + if ($self_class) { + $return_type = $return_type->replaceClassLike( + 'self', + $self_class + ); } - - if ($return_type->fq_classlike_name === 'static' && $self_class) { - $return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class; + if (is_string($static_class_type) || $self_class) { + $return_type = $return_type->replaceClassLike( + 'static', + is_string($static_class_type) ? $static_class_type : $self_class + ); } if ($evaluate_class_constants && $codebase->classOrInterfaceOrEnumExists($return_type->fq_classlike_name)) { @@ -469,9 +479,9 @@ public static function expandAtomic( || $return_type instanceof TGenericObject || $return_type instanceof TIterable ) { - foreach ($return_type->type_params as $k => $type_param) { - /** @psalm-suppress PropertyTypeCoercion */ - $return_type->type_params[$k] = self::expandUnion( + $type_params = $return_type->type_params; + foreach ($type_params as &$type_param) { + $type_param = self::expandUnion( $codebase, $type_param, $self_class, @@ -485,8 +495,11 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ + $return_type = $return_type->replaceTypeParams($type_params); } elseif ($return_type instanceof TKeyedArray) { - foreach ($return_type->properties as &$property_type) { + $properties = $return_type->properties; + foreach ($properties as &$property_type) { $property_type = self::expandUnion( $codebase, $property_type, @@ -501,8 +514,9 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + $return_type = $return_type->setProperties($properties); } elseif ($return_type instanceof TList) { - $return_type->type_param = self::expandUnion( + $return_type = $return_type->replaceTypeParam(self::expandUnion( $codebase, $return_type->type_param, $self_class, @@ -514,11 +528,12 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, - ); + )); } if ($return_type instanceof TObjectWithProperties) { - foreach ($return_type->properties as &$property_type) { + $properties = $return_type->properties; + foreach ($properties as &$property_type) { $property_type = self::expandUnion( $codebase, $property_type, @@ -533,15 +548,17 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + $return_type = $return_type->setProperties($properties); } if ($return_type instanceof TCallable || $return_type instanceof TClosure ) { - if ($return_type->params) { - foreach ($return_type->params as $param) { + $params = $return_type->params; + if ($params) { + foreach ($params as &$param) { if ($param->type) { - $param->type = self::expandUnion( + $param = $param->replaceType(self::expandUnion( $codebase, $param->type, $self_class, @@ -553,14 +570,15 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, - ); + )); } } } - if ($return_type->return_type) { - $return_type->return_type = self::expandUnion( + $sub_return_type = $return_type->return_type; + if ($sub_return_type) { + $sub_return_type = self::expandUnion( $codebase, - $return_type->return_type, + $sub_return_type, $self_class, $static_class_type, $parent_class, @@ -572,18 +590,26 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + + if ($sub_return_type !== $return_type->return_type || $params !== $return_type->params) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty We just cloned this */ + $return_type->return_type = $sub_return_type; + /** @psalm-suppress InaccessibleProperty We just cloned this */ + $return_type->params = $params; + } } return [$return_type]; } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type * @return TNamedObject|TTemplateParam */ private static function expandNamedObject( Codebase $codebase, - TNamedObject $return_type, + TNamedObject &$return_type, ?string $self_class, $static_class_type, ?string $parent_class, @@ -625,11 +651,15 @@ private static function expandNamedObject( if ($static_class_type && ($return_type_lc === 'static' || $return_type_lc === '$this')) { if (is_string($static_class_type)) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type; } else { if ($return_type instanceof TGenericObject && $static_class_type instanceof TGenericObject ) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type->value; } else { $return_type = clone $static_class_type; @@ -637,48 +667,62 @@ private static function expandNamedObject( } if (!$final && $return_type instanceof TNamedObject) { + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->is_static = true; } } elseif ($return_type->is_static && ($static_class_type instanceof TNamedObject || $static_class_type instanceof TTemplateParam) ) { - $return_type = clone $return_type; - $cloned_static = clone $static_class_type; - $extra_static = $cloned_static->extra_types ?: []; - $cloned_static->extra_types = null; + $return_type_types = $return_type->getIntersectionTypes(); + $cloned_static = $static_class_type->setIntersectionTypes([]); + $extra_static = $static_class_type->extra_types; if ($cloned_static->getKey(false) !== $return_type->getKey(false)) { - $return_type->extra_types[$static_class_type->getKey()] = clone $cloned_static; + $return_type_types[$cloned_static->getKey()] = $cloned_static; } foreach ($extra_static as $extra_static_type) { if ($extra_static_type->getKey(false) !== $return_type->getKey(false)) { - $return_type->extra_types[$extra_static_type->getKey()] = clone $extra_static_type; + $return_type_types[$extra_static_type->getKey()] = clone $extra_static_type; } } + $return_type = $return_type->setIntersectionTypes($return_type_types); } elseif ($return_type->is_static && is_string($static_class_type) && $final) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->is_static = false; } elseif ($self_class && $return_type_lc === 'self') { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $self_class; } elseif ($parent_class && $return_type_lc === 'parent') { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $parent_class; } else { - $return_type->value = $codebase->classlikes->getUnAliasedName($return_type->value); + $new_value = $codebase->classlikes->getUnAliasedName($return_type->value); + if ($return_type->value !== $new_value) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ + $return_type->value = $new_value; + } } + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, we are never assigning a TTemplateParam to $return_type */ return $return_type; } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type * * @return non-empty-list */ private static function expandConditional( Codebase $codebase, - TConditional $return_type, + TConditional &$return_type, ?string $self_class, $static_class_type, ?string $parent_class, @@ -703,8 +747,6 @@ private static function expandConditional( $throw_on_unresolvable_constant, ); - $return_type->as_type = $new_as_type; - if ($evaluate_conditional_types) { $assertion = null; @@ -825,9 +867,7 @@ private static function expandConditional( ); if (count($all_conditional_return_types) !== $number_of_types) { - $null_type = new TNull(); - $null_type->from_docblock = true; - $all_conditional_return_types[] = $null_type; + $all_conditional_return_types[] = new TNull(true); } } @@ -837,52 +877,54 @@ private static function expandConditional( $codebase ); + $return_type = $return_type->replaceTypes($new_as_type); + return array_values($combined->getAtomicTypes()); } } - $return_type->conditional_type = self::expandUnion( - $codebase, - $return_type->conditional_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $return_type->if_type = self::expandUnion( - $codebase, - $return_type->if_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $return_type->else_type = self::expandUnion( - $codebase, - $return_type->else_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, + $return_type = $return_type->replaceTypes( + $new_as_type, + self::expandUnion( + $codebase, + $return_type->conditional_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ), + self::expandUnion( + $codebase, + $return_type->if_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ), + self::expandUnion( + $codebase, + $return_type->else_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ) ); - return [$return_type]; } @@ -892,62 +934,68 @@ private static function expandConditional( */ private static function expandPropertiesOf( Codebase $codebase, - TPropertiesOf $return_type, + TPropertiesOf &$return_type, ?string $self_class, $static_class_type ): array { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; + if ($self_class) { + $return_type = $return_type->replaceClassLike( + 'self', + $self_class + ); + $return_type = $return_type->replaceClassLike( + 'static', + is_string($static_class_type) ? $static_class_type : $self_class + ); } - if ($return_type->fq_classlike_name === 'static' && $self_class) { - $return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class; + $class_storage = null; + if ($codebase->classExists($return_type->classlike_type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($return_type->classlike_type->value); + } else { + foreach ($return_type->classlike_type->extra_types as $type) { + if ($type instanceof TNamedObject && $codebase->classExists($type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($type->value); + break; + } + } } - if (!$codebase->classExists($return_type->fq_classlike_name)) { + if (!$class_storage) { return [$return_type]; } - // Get and merge all properties from parent classes - $class_storage = $codebase->classlike_storage_provider->get($return_type->fq_classlike_name); - $properties_types = $class_storage->properties; - foreach ($class_storage->parent_classes as $parent_class) { - if (!$codebase->classOrInterfaceExists($parent_class)) { + $properties = []; + foreach ([$class_storage->name, ...array_values($class_storage->parent_classes)] as $class) { + if (!$codebase->classExists($class)) { continue; } - $parent_class_storage = $codebase->classlike_storage_provider->get($parent_class); - $properties_types = array_merge( - $properties_types, - $parent_class_storage->properties - ); - } - - // Filter only non-static properties, and check visibility filter - $properties_types = array_filter( - $properties_types, - function (PropertyStorage $property) use ($return_type): bool { + $storage = $codebase->classlike_storage_provider->get($class); + foreach ($storage->properties as $key => $property) { + if (isset($properties[$key])) { + continue; + } if ($return_type->visibility_filter !== null && $property->visibility !== $return_type->visibility_filter ) { - return false; + continue; } - return !$property->is_static; - } - ); - - // Return property names as literal string - $properties = array_map( - function (PropertyStorage $property): ?Union { - return $property->type; - }, - $properties_types - ); - $properties = array_filter( - $properties, - function (?Union $property_type): bool { - return $property_type !== null; + if ($property->is_static || !$property->type) { + continue; + } + $type = $return_type->classlike_type instanceof TGenericObject + ? AtomicPropertyFetchAnalyzer::localizePropertyType( + $codebase, + $property->type, + $return_type->classlike_type, + $storage, + $storage + ) + : $property->type + ; + $properties[$key] = $type; } - ); + } if ($properties === []) { return [$return_type]; @@ -994,8 +1042,8 @@ private static function expandKeyOfValueOf( continue; } - if ($type_param->fq_classlike_name === 'self' && $self_class) { - $type_param->fq_classlike_name = $self_class; + if ($self_class) { + $type_param = $type_param->replaceClassLike('self', $self_class); } if ($throw_on_unresolvable_constant diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 71a2ddd1e58..e810718075d 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -96,6 +96,7 @@ use function substr; /** + * @psalm-suppress InaccessibleProperty Allowed during construction * @internal */ class TypeParser @@ -113,7 +114,8 @@ public static function parseTokens( array $type_tokens, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + bool $from_docblock = false ): Union { if (count($type_tokens) === 1) { $only_token = $type_tokens[0]; @@ -129,12 +131,18 @@ public static function parseTokens( } else { $only_token[0] = TypeTokenizer::fixScalarTerms($only_token[0], $analysis_php_version_id); - $atomic = Atomic::create($only_token[0], $analysis_php_version_id, $template_type_map, $type_aliases); - $atomic->offset_start = 0; - $atomic->offset_end = strlen($only_token[0]); - $atomic->text = isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null; + $atomic = Atomic::create( + $only_token[0], + $analysis_php_version_id, + $template_type_map, + $type_aliases, + 0, + strlen($only_token[0]), + isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null, + $from_docblock + ); - return new Union([$atomic]); + return new Union([$atomic], $from_docblock); } } @@ -145,11 +153,12 @@ public static function parseTokens( $codebase, $analysis_php_version_id, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!($parsed_type instanceof Union)) { - $parsed_type = new Union([$parsed_type]); + $parsed_type = new Union([$parsed_type], $from_docblock); } return $parsed_type; @@ -166,27 +175,47 @@ public static function getTypeFromTree( Codebase $codebase, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + bool $from_docblock = false ): TypeNode { if ($parse_tree instanceof GenericTree) { return self::getTypeFromGenericTree( $parse_tree, $codebase, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } if ($parse_tree instanceof UnionTree) { - return self::getTypeFromUnionTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromUnionTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof IntersectionTree) { - return self::getTypeFromIntersectionTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromIntersectionTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof KeyedArrayTree) { - return self::getTypeFromKeyedArrayTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromKeyedArrayTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof CallableWithReturnTypeTree) { @@ -195,7 +224,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!$callable_type instanceof TCallable && !$callable_type instanceof TClosure) { @@ -211,16 +241,26 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); - $callable_type->return_type = $return_type instanceof Union ? $return_type : new Union([$return_type]); + $callable_type->return_type = $return_type instanceof Union + ? $return_type + : new Union([$return_type], $from_docblock) + ; return $callable_type; } if ($parse_tree instanceof CallableTree) { - return self::getTypeFromCallableTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromCallableTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof EncapsulationTree) { @@ -237,7 +277,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -251,17 +292,18 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($non_nullable_type instanceof Union) { - $non_nullable_type->addType(new TNull); + $non_nullable_type = $non_nullable_type->getBuilder()->addType(new TNull($from_docblock))->freeze(); return $non_nullable_type; } return TypeCombiner::combine([ - new TNull, + new TNull($from_docblock), $non_nullable_type, ]); } @@ -273,15 +315,18 @@ public static function getTypeFromTree( } if ($parse_tree instanceof IndexedAccessTree) { - return self::getTypeFromIndexAccessTree($parse_tree, $template_type_map); + return self::getTypeFromIndexAccessTree($parse_tree, $template_type_map, $from_docblock); } if ($parse_tree instanceof TemplateAsTree) { - return new TTemplateParam( + $result = new TTemplateParam( $parse_tree->param_name, new Union([new TNamedObject($parse_tree->as)]), - 'class-string-map' + 'class-string-map', + [], + $from_docblock ); + return $result; } if ($parse_tree instanceof ConditionalTree) { @@ -302,7 +347,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $if_type = self::getTypeFromTree( @@ -310,7 +356,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $else_type = self::getTypeFromTree( @@ -318,19 +365,20 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($conditional_type instanceof Atomic) { - $conditional_type = new Union([$conditional_type]); + $conditional_type = new Union([$conditional_type], $from_docblock); } if ($if_type instanceof Atomic) { - $if_type = new Union([$if_type]); + $if_type = new Union([$if_type], $from_docblock); } if ($else_type instanceof Atomic) { - $else_type = new Union([$else_type]); + $else_type = new Union([$else_type], $from_docblock); } return new TConditional( @@ -339,7 +387,8 @@ public static function getTypeFromTree( $template_type_map[$template_param_name][$first_class], $conditional_type, $if_type, - $else_type + $else_type, + $from_docblock ); } @@ -348,7 +397,7 @@ public static function getTypeFromTree( } if ($parse_tree->value[0] === '"' || $parse_tree->value[0] === '\'') { - return new TLiteralString(substr($parse_tree->value, 1, -1)); + return new TLiteralString(substr($parse_tree->value, 1, -1), $from_docblock); } if (strpos($parse_tree->value, '::')) { @@ -360,23 +409,24 @@ public static function getTypeFromTree( return self::getGenericParamClass( $fq_classlike_name, $template_type_map[$fq_classlike_name][$first_class], - $first_class + $first_class, + $from_docblock ); } if ($const_name === 'class') { - return new TLiteralClassString($fq_classlike_name); + return new TLiteralClassString($fq_classlike_name, false, $from_docblock); } - return new TClassConstant($fq_classlike_name, $const_name); + return new TClassConstant($fq_classlike_name, $const_name, $from_docblock); } if (preg_match('/^\-?(0|[1-9][0-9]*)(\.[0-9]{1,})$/', $parse_tree->value)) { - return new TLiteralFloat((float) $parse_tree->value); + return new TLiteralFloat((float) $parse_tree->value, $from_docblock); } if (preg_match('/^\-?(0|[1-9][0-9]*)$/', $parse_tree->value)) { - return new TLiteralInt((int) $parse_tree->value); + return new TLiteralInt((int) $parse_tree->value, $from_docblock); } if (!preg_match('@^(\$this|\\\\?[a-zA-Z_\x7f-\xff][\\\\\-0-9a-zA-Z_\x7f-\xff]*)$@', $parse_tree->value)) { @@ -385,26 +435,31 @@ public static function getTypeFromTree( $atomic_type_string = TypeTokenizer::fixScalarTerms($parse_tree->value, $analysis_php_version_id); - $atomic_type = Atomic::create($atomic_type_string, $analysis_php_version_id, $template_type_map, $type_aliases); - - $atomic_type->offset_start = $parse_tree->offset_start; - $atomic_type->offset_end = $parse_tree->offset_end; - $atomic_type->text = $parse_tree->text; - - return $atomic_type; + return Atomic::create( + $atomic_type_string, + $analysis_php_version_id, + $template_type_map, + $type_aliases, + $parse_tree->offset_start, + $parse_tree->offset_end, + $parse_tree->text, + $from_docblock + ); } private static function getGenericParamClass( string $param_name, - Union $as, - string $defining_class + Union &$as, + string $defining_class, + bool $from_docblock = false ): TTemplateParamClass { if ($as->hasMixed()) { return new TTemplateParamClass( $param_name, 'object', null, - $defining_class + $defining_class, + $from_docblock ); } @@ -420,23 +475,29 @@ private static function getGenericParamClass( $param_name, 'object', null, - $defining_class + $defining_class, + $from_docblock ); } if ($t instanceof TIterable) { $traversable = new TGenericObject( 'Traversable', - $t->type_params + $t->type_params, + false, + false, + [], + $from_docblock ); - $as->substitute(new Union([$t]), new Union([$traversable])); + $as = $as->getBuilder()->substitute(new Union([$t]), new Union([$traversable]))->freeze(); return new TTemplateParamClass( $param_name, $traversable->value, $traversable, - $defining_class + $defining_class, + $from_docblock ); } @@ -451,7 +512,8 @@ private static function getGenericParamClass( $t->param_name, $t_atomic_type->value ?? 'object', $t_atomic_type, - $t->defining_class + $t->defining_class, + $from_docblock ); } @@ -465,7 +527,8 @@ private static function getGenericParamClass( $param_name, $t->value, $t, - $defining_class + $defining_class, + $from_docblock ); } @@ -476,7 +539,7 @@ private static function getGenericParamClass( * @param non-empty-list $potential_ints * @return non-empty-list */ - public static function getComputedIntsFromMask(array $potential_ints): array + public static function getComputedIntsFromMask(array $potential_ints, bool $from_docblock = false): array { /** @var list */ $potential_values = []; @@ -499,7 +562,7 @@ public static function getComputedIntsFromMask(array $potential_ints): array $potential_values = array_unique($potential_values); return array_map( - static fn($int): TLiteralInt => new TLiteralInt($int), + static fn($int): TLiteralInt => new TLiteralInt($int, $from_docblock), array_values($potential_values) ); } @@ -515,7 +578,8 @@ private static function getTypeFromGenericTree( GenericTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock = false ) { $generic_type = $parse_tree->value; @@ -527,7 +591,8 @@ private static function getTypeFromGenericTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($generic_type === 'class-string-map' @@ -540,7 +605,7 @@ private static function getTypeFromGenericTree( } } - $generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type]); + $generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type], $from_docblock); } $generic_type_value = TypeTokenizer::fixScalarTerms($generic_type); @@ -550,7 +615,7 @@ private static function getTypeFromGenericTree( || $generic_type_value === 'associative-array') && count($generic_params) === 1 ) { - array_unshift($generic_params, new Union([new TArrayKey])); + array_unshift($generic_params, new Union([new TArrayKey($from_docblock)])); } elseif (count($generic_params) === 1 && in_array( $generic_type_value, @@ -558,14 +623,14 @@ private static function getTypeFromGenericTree( true ) ) { - array_unshift($generic_params, new Union([new TMixed])); + array_unshift($generic_params, new Union([new TMixed(false, $from_docblock)])); } elseif ($generic_type_value === 'Generator') { if (count($generic_params) === 1) { - array_unshift($generic_params, new Union([new TMixed])); + array_unshift($generic_params, new Union([new TMixed(false, $from_docblock)])); } for ($i = 0, $l = 4 - count($generic_params); $i < $l; ++$i) { - $generic_params[] = new Union([new TMixed]); + $generic_params[] = new Union([new TMixed(false, $from_docblock)]); } } @@ -575,49 +640,54 @@ private static function getTypeFromGenericTree( if ($generic_type_value === 'array' || $generic_type_value === 'associative-array') { if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey(); + $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { throw new TypeParseTreeException('Too many template parameters for array'); } - return new TArray($generic_params); + return new TArray($generic_params, $from_docblock); } if ($generic_type_value === 'arraylike-object') { - $traversable = new TGenericObject('Traversable', $generic_params); - $array_acccess = new TGenericObject('ArrayAccess', $generic_params); - $countable = new TNamedObject('Countable'); - - $traversable->extra_types[$array_acccess->getKey()] = $array_acccess; - $traversable->extra_types[$countable->getKey()] = $countable; - - return $traversable; + $array_acccess = new TGenericObject('ArrayAccess', $generic_params, false, false, [], $from_docblock); + $countable = new TNamedObject('Countable', false, false, [], $from_docblock); + return new TGenericObject( + 'Traversable', + $generic_params, + false, + false, + [ + $array_acccess->getKey() => $array_acccess, + $countable->getKey() => $countable + ], + $from_docblock + ); } if ($generic_type_value === 'non-empty-array') { if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey(); + $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { throw new TypeParseTreeException('Too many template parameters for non-empty-array'); } - return new TNonEmptyArray($generic_params); + return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); } if ($generic_type_value === 'iterable') { - return new TIterable($generic_params); + return new TIterable($generic_params, [], $from_docblock); } if ($generic_type_value === 'list') { - return new TList($generic_params[0]); + return new TList($generic_params[0], $from_docblock); } if ($generic_type_value === 'non-empty-list') { - return new TNonEmptyList($generic_params[0]); + return new TNonEmptyList($generic_params[0], null, null, $from_docblock); } if ($generic_type_value === 'class-string' @@ -632,7 +702,8 @@ private static function getTypeFromGenericTree( return self::getGenericParamClass( $class_name, $template_type_map[$class_name][$first_class], - $first_class + $first_class, + $from_docblock ); } @@ -646,7 +717,7 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Class string param should be a named object'); } - return new TClassString($class_name, $param_union_types[0]); + return new TClassString($class_name, $param_union_types[0], false, false, false, $from_docblock); } if ($generic_type_value === 'class-string-map') { @@ -683,7 +754,8 @@ private static function getTypeFromGenericTree( return new TClassStringMap( $template_param_name, $template_as_type, - $generic_params[1] + $generic_params[1], + $from_docblock ); } @@ -703,12 +775,19 @@ private static function getTypeFromGenericTree( $generic_type_value . '<' . $param_name . '> must be a TTemplateParam.' ); } + if ($template_param->getIntersectionTypes()) { + throw new TypeParseTreeException( + $generic_type_value . '<' . $param_name . '> must be a TTemplateParam' + . ' with no intersection types.' + ); + } return new TTemplatePropertiesOf( $param_name, $defining_class, $template_param, - TPropertiesOf::filterForTokenName($generic_type_value) + TPropertiesOf::filterForTokenName($generic_type_value), + $from_docblock ); } @@ -723,9 +802,9 @@ private static function getTypeFromGenericTree( } return new TPropertiesOf( - $param_name, $param_union_types[0], - TPropertiesOf::filterForTokenName($generic_type_value) + TPropertiesOf::filterForTokenName($generic_type_value), + $from_docblock ); } @@ -738,7 +817,8 @@ private static function getTypeFromGenericTree( return new TTemplateKeyOf( $param_name, $defining_class, - $generic_params[0] + $generic_params[0], + $from_docblock ); } @@ -748,7 +828,7 @@ private static function getTypeFromGenericTree( ); } - return new TKeyOf($generic_params[0]); + return new TKeyOf($generic_params[0], $from_docblock); } if ($generic_type_value === 'value-of') { @@ -760,7 +840,8 @@ private static function getTypeFromGenericTree( return new TTemplateValueOf( $param_name, $defining_class, - $generic_params[0] + $generic_params[0], + $from_docblock ); } @@ -798,7 +879,7 @@ private static function getTypeFromGenericTree( ); } - $atomic_type = new TLiteralInt($constant_value); + $atomic_type = new TLiteralInt($constant_value, $from_docblock); } else { throw new TypeParseTreeException( 'int-mask types must all be integer values' @@ -822,13 +903,13 @@ private static function getTypeFromGenericTree( foreach ($atomic_types as $atomic_type) { if (!$atomic_type instanceof TLiteralInt) { - return new TIntMask($atomic_types); + return new TIntMask($atomic_types, $from_docblock); } $potential_ints[] = $atomic_type->value; } - return new Union(self::getComputedIntsFromMask($potential_ints)); + return new Union(self::getComputedIntsFromMask($potential_ints, $from_docblock)); } if ($generic_type_value === 'int-mask-of') { @@ -855,7 +936,7 @@ private static function getTypeFromGenericTree( ); } - return new TIntMaskOf($param_type); + return new TIntMaskOf($param_type, $from_docblock); } if ($generic_type_value === 'int') { @@ -885,7 +966,7 @@ private static function getTypeFromGenericTree( $max_bound = $get_int_range_bound($parse_tree->children[1], $generic_params[1], TIntRange::BOUND_MAX); if ($min_bound === null && $max_bound === null) { - return new TInt(); + return new TInt($from_docblock); } if (is_int($min_bound) && is_int($max_bound) && $min_bound > $max_bound) { @@ -900,7 +981,7 @@ private static function getTypeFromGenericTree( ); } - return new TIntRange($min_bound, $max_bound); + return new TIntRange($min_bound, $max_bound, $from_docblock); } if (isset(TypeTokenizer::PSALM_RESERVED_WORDS[$generic_type_value]) @@ -910,7 +991,7 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Cannot create generic object with reserved word'); } - return new TGenericObject($generic_type_value, $generic_params); + return new TGenericObject($generic_type_value, $generic_params, false, false, [], $from_docblock); } /** @@ -922,7 +1003,8 @@ private static function getTypeFromUnionTree( UnionTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ): Union { $has_null = false; @@ -939,7 +1021,8 @@ private static function getTypeFromUnionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $has_null = true; } else { @@ -948,7 +1031,8 @@ private static function getTypeFromUnionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -964,7 +1048,7 @@ private static function getTypeFromUnionTree( } if ($has_null) { - $atomic_types[] = new TNull; + $atomic_types[] = new TNull($from_docblock); } if (!$atomic_types) { @@ -985,7 +1069,8 @@ private static function getTypeFromIntersectionTree( IntersectionTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ): Atomic { $intersection_types = []; @@ -995,7 +1080,8 @@ private static function getTypeFromIntersectionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!$atomic_type instanceof Atomic) { @@ -1059,17 +1145,25 @@ private static function getTypeFromIntersectionTree( } } - $keyed_array = new TKeyedArray($properties); - + $previous_key_type = null; + $previous_value_type = null; if ($first_type instanceof TArray) { - $keyed_array->previous_key_type = $first_type->type_params[0]; - $keyed_array->previous_value_type = $first_type->type_params[1]; + $previous_key_type = $first_type->type_params[0]; + $previous_value_type = $first_type->type_params[1]; } elseif ($last_type instanceof TArray) { - $keyed_array->previous_key_type = $last_type->type_params[0]; - $keyed_array->previous_value_type = $last_type->type_params[1]; + $previous_key_type = $last_type->type_params[0]; + $previous_value_type = $last_type->type_params[1]; } - return $keyed_array; + return new TKeyedArray( + $properties, + null, + false, + $previous_key_type ?? null, + $previous_value_type ?? null, + false, + $from_docblock + ); } $keyed_intersection_types = []; @@ -1089,7 +1183,7 @@ private static function getTypeFromIntersectionTree( $first_type = array_shift($keyed_intersection_types); if ($keyed_intersection_types) { - $first_type->extra_types = $keyed_intersection_types; + return $first_type->setIntersectionTypes($keyed_intersection_types); } } else { foreach ($intersection_types as $intersection_type) { @@ -1117,7 +1211,7 @@ private static function getTypeFromIntersectionTree( } if (!$keyed_intersection_types && $intersect_static) { - return new TNamedObject('static'); + return new TNamedObject('static', false, false, [], $from_docblock); } $first_type = array_shift($keyed_intersection_types); @@ -1129,7 +1223,7 @@ private static function getTypeFromIntersectionTree( } if ($keyed_intersection_types) { - $first_type->extra_types = $keyed_intersection_types; + return $first_type->setIntersectionTypes($keyed_intersection_types); } } @@ -1146,7 +1240,8 @@ private static function getTypeFromCallableTree( CallableTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ) { $params = []; @@ -1161,10 +1256,11 @@ private static function getTypeFromCallableTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } else { - $tree_type = new TMixed(); + $tree_type = new TMixed(false, $from_docblock); } $is_variadic = $child_tree->variadic; @@ -1179,7 +1275,8 @@ private static function getTypeFromCallableTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -1189,24 +1286,22 @@ private static function getTypeFromCallableTree( $tree_type instanceof Union ? $tree_type : new Union([$tree_type]), null, null, + null, $is_optional, false, $is_variadic ); - // type is not authoritative - $param->signature_type = null; - $params[] = $param; } $pure = strpos($parse_tree->value, 'pure-') === 0 ? true : null; if (in_array(strtolower($parse_tree->value), ['closure', '\closure', 'pure-closure'], true)) { - return new TClosure('Closure', $params, null, $pure); + return new TClosure('Closure', $params, null, $pure, [], [], $from_docblock); } - return new TCallable('callable', $params, null, $pure); + return new TCallable('callable', $params, null, $pure, $from_docblock); } /** @@ -1215,7 +1310,8 @@ private static function getTypeFromCallableTree( */ private static function getTypeFromIndexAccessTree( IndexedAccessTree $parse_tree, - array $template_type_map + array $template_type_map, + bool $from_docblock ): TTemplateIndexedAccess { if (!isset($parse_tree->children[0]) || !$parse_tree->children[0] instanceof Value) { throw new TypeParseTreeException('Unrecognised indexed access'); @@ -1258,7 +1354,8 @@ private static function getTypeFromIndexAccessTree( return new TTemplateIndexedAccess( $array_param_name, $offset_param_name, - $array_defining_class + $array_defining_class, + $from_docblock ); } @@ -1272,7 +1369,8 @@ private static function getTypeFromKeyedArrayTree( KeyedArrayTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ) { $properties = []; $class_strings = []; @@ -1290,7 +1388,8 @@ private static function getTypeFromKeyedArrayTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $property_maybe_undefined = false; $property_key = (string)$i; @@ -1300,7 +1399,8 @@ private static function getTypeFromKeyedArrayTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $property_maybe_undefined = $property_branch->possibly_undefined; if (strpos($property_branch->value, '::')) { @@ -1326,7 +1426,7 @@ private static function getTypeFromKeyedArrayTree( } if (!$property_type instanceof Union) { - $property_type = new Union([$property_type]); + $property_type = new Union([$property_type], $from_docblock); } if ($property_maybe_undefined) { @@ -1344,24 +1444,26 @@ private static function getTypeFromKeyedArrayTree( } if (!$properties) { - return new TArray([Type::getNever(), Type::getNever()]); + return new TArray([Type::getNever($from_docblock), Type::getNever($from_docblock)], $from_docblock); } if ($type === 'object') { - return new TObjectWithProperties($properties); + return new TObjectWithProperties($properties, [], [], $from_docblock); } + $class = TKeyedArray::class; if ($type === 'callable-array') { - return new TCallableKeyedArray($properties); + $class = TCallableKeyedArray::class; } - $object_like = new TKeyedArray($properties, $class_strings); - - if ($is_tuple) { - $object_like->sealed = true; - $object_like->is_list = true; - } - - return $object_like; + return new $class( + $properties, + $class_strings, + $is_tuple, + null, + null, + $is_tuple, + $from_docblock + ); } } diff --git a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php new file mode 100644 index 00000000000..c1bc1280fd4 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php @@ -0,0 +1,53 @@ +old = strtolower($old); + $this->new = $new; + } + + /** + * @psalm-suppress InaccessibleProperty Acting on clones + */ + protected function enterNode(TypeNode &$type): ?int + { + if ($type instanceof TClassConstant) { + if (strtolower($type->fq_classlike_name) === $this->old) { + $type = clone $type; + $type->fq_classlike_name = $this->new; + } + } elseif ($type instanceof TClassString) { + if ($type->as !== 'object' && strtolower($type->as) === $this->old) { + $type = clone $type; + $type->as = $this->new; + } + } elseif ($type instanceof TNamedObject || $type instanceof TLiteralClassString) { + if (strtolower($type->value) === $this->old) { + $type = clone $type; + $type->value = $this->new; + } + } + return null; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php index fb57c5c2059..472f61f93d6 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php @@ -5,15 +5,16 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use function strtolower; /** * @internal */ -class ContainsClassLikeVisitor extends NodeVisitor +class ContainsClassLikeVisitor extends ImmutableTypeVisitor { /** * @var lowercase-string @@ -26,6 +27,7 @@ class ContainsClassLikeVisitor extends NodeVisitor private $contains_classlike = false; /** + * @psalm-external-mutation-free * @param lowercase-string $fq_classlike_name */ public function __construct(string $fq_classlike_name) @@ -33,32 +35,38 @@ public function __construct(string $fq_classlike_name) $this->fq_classlike_name = $fq_classlike_name; } + /** + * @psalm-external-mutation-free + */ protected function enterNode(TypeNode $type): ?int { if ($type instanceof TNamedObject) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } if ($type instanceof TClassConstant) { if (strtolower($type->fq_classlike_name) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } if ($type instanceof TLiteralClassString) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } return null; } + /** + * @psalm-mutation-free + */ public function matches(): bool { return $this->contains_classlike; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php index 8fb36b79c2b..c340207e57d 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php @@ -8,13 +8,14 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TTrue; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; /** * @internal */ -class ContainsLiteralVisitor extends NodeVisitor +class ContainsLiteralVisitor extends ImmutableTypeVisitor { /** * @var bool @@ -30,12 +31,12 @@ protected function enterNode(TypeNode $type): ?int || $type instanceof TFalse ) { $this->contains_literal = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } if ($type instanceof TArray && $type->isEmptyArray()) { $this->contains_literal = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php new file mode 100644 index 00000000000..da0a266b0e7 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php @@ -0,0 +1,30 @@ +value === 'static' || $type->is_static)) { + $this->contains_static = true; + return TypeVisitor::STOP_TRAVERSAL; + } + return null; + } + + public function matches(): bool + { + return $this->contains_static; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php index 2f4a7b7cff0..ce7250a7150 100644 --- a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php +++ b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php @@ -4,29 +4,40 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\NodeVisitor; +use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; /** * @internal */ -class FromDocblockSetter extends NodeVisitor +class FromDocblockSetter extends TypeVisitor { + private bool $from_docblock; + public function __construct(bool $from_docblock) + { + $this->from_docblock = $from_docblock; + } /** - * @psalm-suppress MoreSpecificImplementedParamType - * - * @param Atomic|Union $type * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ - protected function enterNode(TypeNode $type): ?int + protected function enterNode(TypeNode &$type): ?int { - $type->from_docblock = true; + if (!$type instanceof Atomic && !$type instanceof Union && !$type instanceof MutableUnion) { + return null; + } + if ($type->from_docblock === $this->from_docblock) { + return null; + } + $type = clone $type; + /** @psalm-suppress InaccessibleProperty Acting on clone */ + $type->from_docblock = $this->from_docblock; if ($type instanceof TTemplateParam && $type->as->isMixed() ) { - return NodeVisitor::DONT_TRAVERSE_CHILDREN; + return TypeVisitor::DONT_TRAVERSE_CHILDREN; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php index 5806638342d..de02f7b70f5 100644 --- a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php +++ b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php @@ -6,14 +6,14 @@ use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use Psalm\Type\Union; /** * @internal */ -class TemplateTypeCollector extends NodeVisitor +class TemplateTypeCollector extends ImmutableTypeVisitor { /** * @var list diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index af0ad30c33c..dcf43591cd2 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -25,8 +25,10 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; +use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; use ReflectionProperty; @@ -40,7 +42,7 @@ /** * @internal */ -class TypeChecker extends NodeVisitor +class TypeChecker extends ImmutableTypeVisitor { /** * @var StatementsSource @@ -107,15 +109,16 @@ public function __construct( } /** - * @psalm-suppress MoreSpecificImplementedParamType - * - * @param Atomic|Union $type * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ protected function enterNode(TypeNode $type): ?int { + if (!$type instanceof Atomic && !$type instanceof Union && !$type instanceof MutableUnion) { + return null; + } + if ($type->checked) { - return NodeVisitor::DONT_TRAVERSE_CHILDREN; + return TypeVisitor::DONT_TRAVERSE_CHILDREN; } if ($type instanceof TNamedObject) { @@ -128,6 +131,7 @@ protected function enterNode(TypeNode $type): ?int $this->checkResource($type); } + /** @psalm-suppress InaccessibleProperty Doesn't affect anything else */ $type->checked = true; return null; diff --git a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php new file mode 100644 index 00000000000..658cf6c9da0 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php @@ -0,0 +1,98 @@ +> + */ + private array $extends; + private string $base_fq_class_name; + + /** + * @param array> $extends + */ + public function __construct( + array $extends, + string $base_fq_class_name + ) { + $this->extends = $extends; + $this->base_fq_class_name = $base_fq_class_name; + } + + /** + * @psalm-suppress InaccessibleProperty Acting on clones + */ + protected function enterNode(TypeNode &$type): ?int + { + if ($type instanceof TTemplateParamClass) { + if ($type->defining_class === $this->base_fq_class_name) { + if (isset($this->extends[$this->base_fq_class_name][$type->param_name])) { + $extended_param = $this->extends[$this->base_fq_class_name][$type->param_name]; + + $types = array_values($extended_param->getAtomicTypes()); + + if (count($types) === 1 && $types[0] instanceof TNamedObject) { + $type = clone $type; + $type->as_type = $types[0]; + } elseif ($type->as_type !== null) { + $type = clone $type; + $type->as_type = null; + } + } + } + } + + if ($type instanceof Union) { + $union = $type->getBuilder(); + } elseif ($type instanceof MutableUnion) { + $union = $type; + } else { + return null; + } + + foreach ($union->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TTemplateParam + && ($atomic_type->defining_class === $this->base_fq_class_name + || isset($this->extends[$atomic_type->defining_class])) + ) { + $types_to_add = Methods::getExtendedTemplatedTypes( + $atomic_type, + $this->extends + ); + + if ($types_to_add) { + $union->removeType($key); + + foreach ($types_to_add as $extra_added_type) { + $union->addType($extra_added_type); + } + } + } + } + + if ($type instanceof Union) { + $type = $union->freeze(); + } else { + $type = $union; + } + + return null; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/TypeScanner.php b/src/Psalm/Internal/TypeVisitor/TypeScanner.php index 48f6796ef28..73b2687124c 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeScanner.php +++ b/src/Psalm/Internal/TypeVisitor/TypeScanner.php @@ -7,7 +7,7 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use function strtolower; @@ -15,13 +15,16 @@ /** * @internal */ -class TypeScanner extends NodeVisitor +class TypeScanner extends ImmutableTypeVisitor { - private $scanner; + private Scanner $scanner; - private $file_storage; + private ?FileStorage $file_storage; - private $phantom_classes; + /** + * @var array + */ + private array $phantom_classes; /** * @param array $phantom_classes 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/Plugin/EventHandler/Event/StringInterpreterEvent.php b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php index 1f39a9d398e..db782f4d69d 100644 --- a/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php @@ -12,6 +12,8 @@ final class StringInterpreterEvent /** * Called after a statement has been checked * + * @psalm-external-mutation-free + * * @internal */ public function __construct(string $value) 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/Assertion.php b/src/Psalm/Storage/Assertion.php index ed7dca2542d..2fcc4324bbc 100644 --- a/src/Psalm/Storage/Assertion.php +++ b/src/Psalm/Storage/Assertion.php @@ -4,12 +4,15 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ abstract class Assertion { - /** @psalm-mutation-free */ + use ImmutableNonCloneableTrait; + abstract public function getNegation(): Assertion; - /** @psalm-mutation-free */ abstract public function isNegationOf(self $assertion): bool; abstract public function __toString(): string; @@ -19,19 +22,21 @@ public function isNegation(): bool return false; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return false; } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return null; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { + return $this; } } diff --git a/src/Psalm/Storage/Assertion/Any.php b/src/Psalm/Storage/Assertion/Any.php index 4aab3fda16a..c5d61036253 100644 --- a/src/Psalm/Storage/Assertion/Any.php +++ b/src/Psalm/Storage/Assertion/Any.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Any extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return $this; @@ -17,7 +19,6 @@ public function __toString(): string return 'mixed'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php index 43a6f0bd557..e8fc33cb2ed 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class ArrayKeyDoesNotExist extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new ArrayKeyExists(); @@ -22,7 +24,6 @@ public function __toString(): string return '!array-key-exists'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof ArrayKeyExists; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyExists.php b/src/Psalm/Storage/Assertion/ArrayKeyExists.php index 0b803cd4dd9..6ef5e84e244 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyExists.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyExists.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class ArrayKeyExists extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new ArrayKeyDoesNotExist(); @@ -17,7 +19,6 @@ public function __toString(): string return 'array-key-exists'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof ArrayKeyDoesNotExist; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php index 297e61ddb83..7913cf60164 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveAtLeastCount extends Assertion { /** @var positive-int */ @@ -15,13 +18,11 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasAtLeastCount($this->count); } - /** @psalm-mutation-free */ public function isNegation(): bool { return true; @@ -32,7 +33,6 @@ public function __toString(): string return '!has-at-least-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasAtLeastCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php index 950bf72eba6..349e1533de4 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveExactCount extends Assertion { /** @var positive-int */ @@ -20,7 +23,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasExactCount($this->count); @@ -31,7 +33,6 @@ public function __toString(): string return '!has-exact-count-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasExactCount && $assertion->count === $this->count; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php index eef48187543..64dc6fb4c95 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveMethod extends Assertion { public string $method; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasMethod($this->method); @@ -29,7 +31,6 @@ public function __toString(): string return '!method-exists-' . $this->method; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasMethod && $assertion->method === $this->method; diff --git a/src/Psalm/Storage/Assertion/Empty_.php b/src/Psalm/Storage/Assertion/Empty_.php index 91613b0f4a2..f7e14b675b7 100644 --- a/src/Psalm/Storage/Assertion/Empty_.php +++ b/src/Psalm/Storage/Assertion/Empty_.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Empty_ extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NonEmpty(); @@ -22,7 +24,6 @@ public function __toString(): string return '!non-empty'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NonEmpty; diff --git a/src/Psalm/Storage/Assertion/Falsy.php b/src/Psalm/Storage/Assertion/Falsy.php index 13c85088eeb..e555c2d16ea 100644 --- a/src/Psalm/Storage/Assertion/Falsy.php +++ b/src/Psalm/Storage/Assertion/Falsy.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Falsy extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Truthy(); @@ -22,7 +24,6 @@ public function __toString(): string return 'falsy'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Truthy; diff --git a/src/Psalm/Storage/Assertion/HasArrayKey.php b/src/Psalm/Storage/Assertion/HasArrayKey.php index 7442c4a39a7..713eda3fe1e 100644 --- a/src/Psalm/Storage/Assertion/HasArrayKey.php +++ b/src/Psalm/Storage/Assertion/HasArrayKey.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasArrayKey extends Assertion { public $key; @@ -14,7 +17,6 @@ public function __construct(string $key) $this->key = $key; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -25,7 +27,6 @@ public function __toString(): string return 'has-array-key-' . $this->key; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/HasAtLeastCount.php b/src/Psalm/Storage/Assertion/HasAtLeastCount.php index 46ec83abfeb..3a30e1d5666 100644 --- a/src/Psalm/Storage/Assertion/HasAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/HasAtLeastCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasAtLeastCount extends Assertion { /** @var positive-int */ @@ -15,7 +18,6 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveAtLeastCount($this->count); @@ -26,7 +28,6 @@ public function __toString(): string return 'has-at-least-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveAtLeastCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/HasExactCount.php b/src/Psalm/Storage/Assertion/HasExactCount.php index 36d0400c29c..b76cfc6144e 100644 --- a/src/Psalm/Storage/Assertion/HasExactCount.php +++ b/src/Psalm/Storage/Assertion/HasExactCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasExactCount extends Assertion { /** @var positive-int */ @@ -15,13 +18,11 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveExactCount($this->count); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -32,7 +33,6 @@ public function __toString(): string return '=has-exact-count-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveExactCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php index 88620c8b0f8..4bcafad2312 100644 --- a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php @@ -5,9 +5,11 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasIntOrStringArrayAccess extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -18,7 +20,6 @@ public function __toString(): string return 'has-string-or-int-array-access'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/HasMethod.php b/src/Psalm/Storage/Assertion/HasMethod.php index 94508472404..2aef8f39814 100644 --- a/src/Psalm/Storage/Assertion/HasMethod.php +++ b/src/Psalm/Storage/Assertion/HasMethod.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasMethod extends Assertion { public string $method; @@ -13,7 +16,6 @@ public function __construct(string $method) $this->method = $method; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveMethod($this->method); @@ -24,7 +26,6 @@ public function __toString(): string return 'method-exists-' . $this->method; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveMethod && $this->method === $assertion->method; diff --git a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php index d7595287857..d4aeb63c6e8 100644 --- a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php @@ -5,9 +5,11 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasStringArrayAccess extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -18,7 +20,6 @@ public function __toString(): string return 'has-string-array-access'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/InArray.php b/src/Psalm/Storage/Assertion/InArray.php index b5e1abc2ea7..ac7220da007 100644 --- a/src/Psalm/Storage/Assertion/InArray.php +++ b/src/Psalm/Storage/Assertion/InArray.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Union; +/** + * @psalm-immutable + */ final class InArray extends Assertion { public Union $type; @@ -14,7 +17,6 @@ public function __construct(Union $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NotInArray($this->type); @@ -25,7 +27,6 @@ public function __toString(): string return 'in-array-' . $this->type->getId(); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NotInArray && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsAClass.php b/src/Psalm/Storage/Assertion/IsAClass.php index 3b6c5473952..ca5409e9268 100644 --- a/src/Psalm/Storage/Assertion/IsAClass.php +++ b/src/Psalm/Storage/Assertion/IsAClass.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsAClass extends Assertion { /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ @@ -18,13 +21,11 @@ public function __construct(Atomic $type, bool $allow_string) $this->allow_string = $allow_string; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotAClass($this->type, $this->allow_string); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; @@ -35,7 +36,6 @@ public function __toString(): string return 'isa-' . ($this->allow_string ? 'string-' : '') . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotAClass diff --git a/src/Psalm/Storage/Assertion/IsClassEqual.php b/src/Psalm/Storage/Assertion/IsClassEqual.php index da5307aa7f5..a7c92f2aab2 100644 --- a/src/Psalm/Storage/Assertion/IsClassEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassEqual.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsClassEqual extends Assertion { public string $type; @@ -13,13 +16,11 @@ public function __construct(string $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsClassNotEqual($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -30,7 +31,6 @@ public function __toString(): string return '=get-class-' . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsClassNotEqual && $this->type === $assertion->type; diff --git a/src/Psalm/Storage/Assertion/IsClassNotEqual.php b/src/Psalm/Storage/Assertion/IsClassNotEqual.php index c8d05dc8b77..ae4ed1329c7 100644 --- a/src/Psalm/Storage/Assertion/IsClassNotEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassNotEqual.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsClassNotEqual extends Assertion { public string $type; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsClassEqual($this->type); @@ -29,7 +31,6 @@ public function __toString(): string return '!=get-class-' . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsClassEqual && $this->type === $assertion->type; diff --git a/src/Psalm/Storage/Assertion/IsCountable.php b/src/Psalm/Storage/Assertion/IsCountable.php index dea9295b28c..3933c4a13dd 100644 --- a/src/Psalm/Storage/Assertion/IsCountable.php +++ b/src/Psalm/Storage/Assertion/IsCountable.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsCountable extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotCountable(true); @@ -17,7 +19,6 @@ public function __toString(): string return 'countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotCountable && $assertion->is_negatable; diff --git a/src/Psalm/Storage/Assertion/IsEqualIsset.php b/src/Psalm/Storage/Assertion/IsEqualIsset.php index bded7c5ce06..8f754b375cf 100644 --- a/src/Psalm/Storage/Assertion/IsEqualIsset.php +++ b/src/Psalm/Storage/Assertion/IsEqualIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsEqualIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Any(); @@ -17,13 +19,11 @@ public function __toString(): string return '=isset'; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/IsGreaterThan.php b/src/Psalm/Storage/Assertion/IsGreaterThan.php index 131c3fc94a9..ed58ecabc99 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThan.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThan.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsGreaterThan extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLessThanOrEqualTo($this->value); @@ -24,7 +26,6 @@ public function __toString(): string return '>' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThanOrEqualTo && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php index ce8590eafc8..20a3c05d189 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsGreaterThanOrEqualTo extends Assertion { public int $value; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLessThan($this->value); @@ -29,7 +31,6 @@ public function __toString(): string return '!<' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThan && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsIdentical.php b/src/Psalm/Storage/Assertion/IsIdentical.php index 9f6dccfae10..8730bda852d 100644 --- a/src/Psalm/Storage/Assertion/IsIdentical.php +++ b/src/Psalm/Storage/Assertion/IsIdentical.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsIdentical extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotIdentical($this->type); @@ -25,24 +27,24 @@ public function __toString(): string return '=' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIdentical && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsIsset.php b/src/Psalm/Storage/Assertion/IsIsset.php index 7b5fd06423b..219649181a1 100644 --- a/src/Psalm/Storage/Assertion/IsIsset.php +++ b/src/Psalm/Storage/Assertion/IsIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotIsset(); @@ -17,7 +19,6 @@ public function __toString(): string return 'isset'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIsset; diff --git a/src/Psalm/Storage/Assertion/IsLessThan.php b/src/Psalm/Storage/Assertion/IsLessThan.php index 0f26f5c53e1..8d78d4d4669 100644 --- a/src/Psalm/Storage/Assertion/IsLessThan.php +++ b/src/Psalm/Storage/Assertion/IsLessThan.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsLessThan extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsGreaterThanOrEqualTo($this->value); @@ -24,7 +26,6 @@ public function __toString(): string return '<' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThanOrEqualTo && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php index 36afbe8e282..2dd565915a1 100644 --- a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsLessThanOrEqualTo extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsGreaterThan($this->value); @@ -29,7 +31,6 @@ public function __toString(): string return '!>' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThan && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsLooselyEqual.php b/src/Psalm/Storage/Assertion/IsLooselyEqual.php index 35b1e6d6259..c0b23dbde7c 100644 --- a/src/Psalm/Storage/Assertion/IsLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsLooselyEqual.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsLooselyEqual extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotLooselyEqual($this->type); @@ -25,24 +27,24 @@ public function __toString(): string return '~' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotLooselyEqual && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotAClass.php b/src/Psalm/Storage/Assertion/IsNotAClass.php index 45cbd3442ef..7c542e1684c 100644 --- a/src/Psalm/Storage/Assertion/IsNotAClass.php +++ b/src/Psalm/Storage/Assertion/IsNotAClass.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotAClass extends Assertion { /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ @@ -23,7 +26,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsAClass($this->type, $this->allow_string); @@ -34,7 +36,6 @@ public function __toString(): string return 'isa-' . ($this->allow_string ? 'string-' : '') . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsAClass diff --git a/src/Psalm/Storage/Assertion/IsNotCountable.php b/src/Psalm/Storage/Assertion/IsNotCountable.php index b09a7a7dcdc..c76fe24e26e 100644 --- a/src/Psalm/Storage/Assertion/IsNotCountable.php +++ b/src/Psalm/Storage/Assertion/IsNotCountable.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsNotCountable extends Assertion { public $is_negatable; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsCountable(); @@ -29,7 +31,6 @@ public function __toString(): string return '!countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsCountable; diff --git a/src/Psalm/Storage/Assertion/IsNotIdentical.php b/src/Psalm/Storage/Assertion/IsNotIdentical.php index 2c023300a02..978ca956df6 100644 --- a/src/Psalm/Storage/Assertion/IsNotIdentical.php +++ b/src/Psalm/Storage/Assertion/IsNotIdentical.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotIdentical extends Assertion { public Atomic $type; @@ -19,13 +22,11 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsIdentical($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -36,18 +37,19 @@ public function __toString(): string return '!=' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsIdentical && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotIsset.php b/src/Psalm/Storage/Assertion/IsNotIsset.php index defa8479a32..d42486326e6 100644 --- a/src/Psalm/Storage/Assertion/IsNotIsset.php +++ b/src/Psalm/Storage/Assertion/IsNotIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsNotIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsIsset(); @@ -22,7 +24,6 @@ public function __toString(): string return '!isset'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIsset; diff --git a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php index bcf77c78514..846afb19075 100644 --- a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotLooselyEqual extends Assertion { public Atomic $type; @@ -19,13 +22,11 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLooselyEqual($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -36,18 +37,19 @@ public function __toString(): string return '!~' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLooselyEqual && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotType.php b/src/Psalm/Storage/Assertion/IsNotType.php index 321dd744e0c..24d1ee9c380 100644 --- a/src/Psalm/Storage/Assertion/IsNotType.php +++ b/src/Psalm/Storage/Assertion/IsNotType.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotType extends Assertion { public Atomic $type; @@ -19,7 +22,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsType($this->type); @@ -30,18 +32,19 @@ public function __toString(): string return '!' . $this->type->getId(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsType && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsType.php b/src/Psalm/Storage/Assertion/IsType.php index 2c822a79d40..501a5e06cca 100644 --- a/src/Psalm/Storage/Assertion/IsType.php +++ b/src/Psalm/Storage/Assertion/IsType.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsType extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotType($this->type); @@ -25,18 +27,19 @@ public function __toString(): string return $this->type->getId(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotType && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/NestedAssertions.php b/src/Psalm/Storage/Assertion/NestedAssertions.php index c7fa0a48e62..20f56ba585f 100644 --- a/src/Psalm/Storage/Assertion/NestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NestedAssertions.php @@ -8,6 +8,9 @@ use const JSON_THROW_ON_ERROR; +/** + * @psalm-immutable + */ final class NestedAssertions extends Assertion { /** @var array>> */ @@ -19,7 +22,6 @@ public function __construct(array $assertions) $this->assertions = $assertions; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NotNestedAssertions($this->assertions); @@ -30,7 +32,6 @@ public function __toString(): string return '@' . json_encode($this->assertions, JSON_THROW_ON_ERROR); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/NonEmpty.php b/src/Psalm/Storage/Assertion/NonEmpty.php index bb662d9591f..b45076d90c6 100644 --- a/src/Psalm/Storage/Assertion/NonEmpty.php +++ b/src/Psalm/Storage/Assertion/NonEmpty.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NonEmpty extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Empty_(); @@ -17,7 +19,6 @@ public function __toString(): string return 'non-empty'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Empty_; diff --git a/src/Psalm/Storage/Assertion/NonEmptyCountable.php b/src/Psalm/Storage/Assertion/NonEmptyCountable.php index c42eca24d57..8c191d22c0a 100644 --- a/src/Psalm/Storage/Assertion/NonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NonEmptyCountable.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NonEmptyCountable extends Assertion { public $is_negatable; @@ -13,13 +16,11 @@ public function __construct(bool $is_negatable) $this->is_negatable = $is_negatable; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return $this->is_negatable ? new NotNonEmptyCountable() : new Any(); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return !$this->is_negatable; @@ -30,7 +31,6 @@ public function __toString(): string return ($this->is_negatable ? '' : '=') . 'non-empty-countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $this->is_negatable && $assertion instanceof NotNonEmptyCountable; diff --git a/src/Psalm/Storage/Assertion/NotInArray.php b/src/Psalm/Storage/Assertion/NotInArray.php index 2dbbb71161b..73c352d47dd 100644 --- a/src/Psalm/Storage/Assertion/NotInArray.php +++ b/src/Psalm/Storage/Assertion/NotInArray.php @@ -5,8 +5,14 @@ use Psalm\Storage\Assertion; use Psalm\Type\Union; +/** + * @psalm-immutable + */ final class NotInArray extends Assertion { + /** + * @readonly + */ public Union $type; public function __construct(Union $type) @@ -14,7 +20,6 @@ public function __construct(Union $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new InArray($this->type); @@ -30,7 +35,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof InArray && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/NotNestedAssertions.php b/src/Psalm/Storage/Assertion/NotNestedAssertions.php index ad6f46f9ef6..7f2d33564c3 100644 --- a/src/Psalm/Storage/Assertion/NotNestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NotNestedAssertions.php @@ -8,6 +8,9 @@ use const JSON_THROW_ON_ERROR; +/** + * @psalm-immutable + */ final class NotNestedAssertions extends Assertion { /** @var array>> */ @@ -24,7 +27,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NestedAssertions($this->assertions); @@ -35,7 +37,6 @@ public function __toString(): string return '!@' . json_encode($this->assertions, JSON_THROW_ON_ERROR); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php index d4a59fbd1a6..48fc743c373 100644 --- a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NotNonEmptyCountable extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NonEmptyCountable(true); @@ -22,7 +24,6 @@ public function __toString(): string return '!non-empty-countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NonEmptyCountable && $assertion->is_negatable; diff --git a/src/Psalm/Storage/Assertion/Truthy.php b/src/Psalm/Storage/Assertion/Truthy.php index 1d92be6e158..ef853b0b3b3 100644 --- a/src/Psalm/Storage/Assertion/Truthy.php +++ b/src/Psalm/Storage/Assertion/Truthy.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Truthy extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Falsy(); @@ -17,7 +19,6 @@ public function __toString(): string return '!falsy'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Falsy; 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/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 4a5e0e7b2ce..f7bc9ca0662 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -4,9 +4,10 @@ use Psalm\CodeLocation; use Psalm\Internal\Scanner\UnresolvedConstantComponent; +use Psalm\Type\TypeNode; use Psalm\Type\Union; -final class FunctionLikeParameter implements HasAttributesInterface +final class FunctionLikeParameter implements HasAttributesInterface, TypeNode { use CustomMetadataTrait; @@ -111,23 +112,26 @@ final class FunctionLikeParameter implements HasAttributesInterface public $description; /** + * @psalm-external-mutation-free * @param Union|UnresolvedConstantComponent|null $default_type */ public function __construct( string $name, bool $by_ref, ?Union $type = null, + ?Union $signature_type = null, ?CodeLocation $location = null, ?CodeLocation $type_location = null, bool $is_optional = true, bool $is_nullable = false, bool $is_variadic = false, - $default_type = null + $default_type = null, + ?Union $out_type = null ) { $this->name = $name; $this->by_ref = $by_ref; $this->type = $type; - $this->signature_type = $type; + $this->signature_type = $signature_type; $this->is_optional = $is_optional; $this->is_nullable = $is_nullable; $this->is_variadic = $is_variadic; @@ -135,8 +139,10 @@ public function __construct( $this->type_location = $type_location; $this->signature_type_location = $type_location; $this->default_type = $default_type; + $this->out_type = $out_type; } + /** @psalm-mutation-free */ public function getId(): string { return ($this->type ? $this->type->getId() : 'mixed') @@ -144,14 +150,29 @@ public function getId(): string . ($this->is_optional ? '=' : ''); } - public function __clone() + /** @psalm-mutation-free */ + public function replaceType(Union $type): self { - if ($this->type) { - $this->type = clone $this->type; + if ($this->type === $type) { + return $this; } + $cloned = clone $this; + $cloned->type = $type; + return $cloned; + } + + /** @psalm-mutation-free */ + public function getChildNodeKeys(): array + { + $result = ['type', 'signature_type', 'out_type']; + if ($this->default_type instanceof Union) { + $result []= 'default_type'; + } + return $result; } /** + * @psalm-mutation-free * @return list */ public function getAttributeStorages(): array diff --git a/src/Psalm/Storage/ImmutableNonCloneableTrait.php b/src/Psalm/Storage/ImmutableNonCloneableTrait.php new file mode 100644 index 00000000000..7609a73a8a4 --- /dev/null +++ b/src/Psalm/Storage/ImmutableNonCloneableTrait.php @@ -0,0 +1,13 @@ +getAtomicTypes() as $atomic_type) { - $assertion = clone $assertion; - $assertion->setAtomicType($atomic_type); + $assertion = $assertion->setAtomicType($atomic_type); $assertion_rules[] = $assertion; } } else { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 8508caa2c15..7dcfc543a5b 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; @@ -45,6 +46,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Atomic\TVoid; +use Psalm\Type\MutableUnion; use Psalm\Type\Union; use UnexpectedValueException; @@ -174,6 +176,9 @@ public static function getStringFromFQCLN( return '\\' . $value; } + /** + * @psalm-pure + */ public static function getInt(bool $from_calculation = false, ?int $value = null): Union { if ($value !== null) { @@ -182,11 +187,15 @@ public static function getInt(bool $from_calculation = false, ?int $value = null $union = new Union([new TInt()]); } + /** @psalm-suppress ImpurePropertyAssignment We just created this object */ $union->from_calculation = $from_calculation; return $union; } + /** + * @psalm-pure + */ public static function getLowercaseString(): Union { $type = new TLowercaseString(); @@ -194,6 +203,9 @@ public static function getLowercaseString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyLowercaseString(): Union { $type = new TNonEmptyLowercaseString(); @@ -201,6 +213,9 @@ public static function getNonEmptyLowercaseString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyString(): Union { $type = new TNonEmptyString(); @@ -208,6 +223,19 @@ public static function getNonEmptyString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ + public static function getNonFalsyString(): Union + { + $type = new TNonFalsyString(); + + return new Union([$type]); + } + + /** + * @psalm-pure + */ public static function getNumeric(): Union { $type = new TNumeric; @@ -215,6 +243,9 @@ public static function getNumeric(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNumericString(): Union { $type = new TNumericString; @@ -249,6 +280,9 @@ public static function getString(?string $value = null): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getSingleLetter(): Union { $type = new TSingleLetter; @@ -256,6 +290,9 @@ public static function getSingleLetter(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getClassString(string $extends = 'object'): Union { return new Union([ @@ -268,6 +305,9 @@ public static function getClassString(string $extends = 'object'): Union ]); } + /** + * @psalm-pure + */ public static function getLiteralClassString(string $class_type, bool $definite_class = false): Union { $type = new TLiteralClassString($class_type, $definite_class); @@ -275,52 +315,73 @@ public static function getLiteralClassString(string $class_type, bool $definite_ return new Union([$type]); } - public static function getNull(): Union + /** + * @psalm-pure + */ + public static function getNull(bool $from_docblock = false): Union { - $type = new TNull; + $type = new TNull($from_docblock); return new Union([$type]); } - public static function getMixed(bool $from_loop_isset = false): Union + /** + * @psalm-pure + */ + public static function getMixed(bool $from_loop_isset = false, bool $from_docblock = false): Union { - $type = new TMixed($from_loop_isset); + $type = new TMixed($from_loop_isset, $from_docblock); return new Union([$type]); } - public static function getScalar(): Union + /** + * @psalm-pure + */ + public static function getScalar(bool $from_docblock = false): Union { - $type = new TScalar(); + $type = new TScalar($from_docblock); return new Union([$type]); } - public static function getNever(): Union + /** + * @psalm-pure + */ + public static function getNever(bool $from_docblock = false): Union { - $type = new TNever(); + $type = new TNever($from_docblock); return new Union([$type]); } - public static function getBool(): Union + /** + * @psalm-pure + */ + public static function getBool(bool $from_docblock = false): Union { - $type = new TBool; + $type = new TBool($from_docblock); return new Union([$type]); } - public static function getFloat(?float $value = null): Union + /** + * @psalm-pure + */ + public static function getFloat(?float $value = null, bool $from_docblock = false): Union { if ($value !== null) { - $type = new TLiteralFloat($value); + $type = new TLiteralFloat($value, $from_docblock); } else { - $type = new TFloat(); + $type = new TFloat($from_docblock); } return new Union([$type]); } + /** + * @psalm-pure + */ public static function getObject(): Union { $type = new TObject; @@ -328,6 +389,9 @@ public static function getObject(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getClosure(): Union { $type = new TClosure('Closure'); @@ -335,13 +399,19 @@ public static function getClosure(): Union return new Union([$type]); } - public static function getArrayKey(): Union + /** + * @psalm-pure + */ + public static function getArrayKey(bool $from_docblock = false): Union { - $type = new TArrayKey(); + $type = new TArrayKey($from_docblock); return new Union([$type]); } + /** + * @psalm-pure + */ public static function getArray(): Union { $type = new TArray( @@ -354,6 +424,9 @@ public static function getArray(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getEmptyArray(): Union { $array_type = new TArray( @@ -368,6 +441,9 @@ public static function getEmptyArray(): Union ]); } + /** + * @psalm-pure + */ public static function getList(): Union { $type = new TList(new Union([new TMixed])); @@ -375,6 +451,9 @@ public static function getList(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyList(): Union { $type = new TNonEmptyList(new Union([new TMixed])); @@ -382,33 +461,46 @@ public static function getNonEmptyList(): Union return new Union([$type]); } - public static function getVoid(): Union + /** + * @psalm-pure + */ + public static function getVoid(bool $from_docblock = false): Union { - $type = new TVoid; + $type = new TVoid($from_docblock); return new Union([$type]); } - public static function getFalse(): Union + /** + * @psalm-pure + */ + public static function getFalse(bool $from_docblock = false): Union { - $type = new TFalse; + $type = new TFalse($from_docblock); return new Union([$type]); } - public static function getTrue(): Union + /** + * @psalm-pure + */ + public static function getTrue(bool $from_docblock = false): Union { - $type = new TTrue; + $type = new TTrue($from_docblock); return new Union([$type]); } - public static function getResource(): Union + /** + * @psalm-pure + */ + public static function getResource(bool $from_docblock = false): Union { - return new Union([new TResource]); + return new Union([new TResource($from_docblock)]); } /** + * @psalm-external-mutation-free * @param non-empty-list $union_types */ public static function combineUnionTypeArray(array $union_types, ?Codebase $codebase): Union @@ -428,6 +520,9 @@ public static function combineUnionTypeArray(array $union_types, ?Codebase $code * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar * + * @psalm-external-mutation-free + * + * @psalm-suppress ImpurePropertyAssignment We're not mutating external instances */ public static function combineUnionTypes( ?Union $type_1, @@ -601,13 +696,16 @@ public static function intersectUnionTypes( if (null !== $intersection_atomic) { if (null === $combined_type) { - $combined_type = new Union([$intersection_atomic]); + $combined_type = new MutableUnion([$intersection_atomic]); } else { $combined_type->addType($intersection_atomic); } } } } + if ($combined_type) { + $combined_type = $combined_type->freeze(); + } } //if a type is contained by the other, the intersection is the narrowest type @@ -769,26 +867,24 @@ private static function intersectAtomicTypes( .' Check the preceding code for errors.' ); } - if (!$intersection_atomic->extra_types) { - $intersection_atomic->extra_types = []; - } $intersection_performed = true; - $wider_type_clone = clone $wider_type; - - $wider_type_clone->extra_types = []; + $wider_type_clone = $wider_type->setIntersectionTypes([]); - $intersection_atomic->extra_types[$wider_type_clone->getKey()] = $wider_type_clone; + $final_intersection = array_merge( + [$wider_type_clone->getKey() => $wider_type_clone], + $intersection_atomic->getIntersectionTypes() + ); $wider_type_intersection_types = $wider_type->getIntersectionTypes(); - if ($wider_type_intersection_types !== null) { - foreach ($wider_type_intersection_types as $wider_type_intersection_type) { - $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] - = clone $wider_type_intersection_type; - } + foreach ($wider_type_intersection_types as $wider_type_intersection_type) { + $final_intersection[$wider_type_intersection_type->getKey()] + = clone $wider_type_intersection_type; } + + return $intersection_atomic->setIntersectionTypes($final_intersection); } return $intersection_atomic; diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 126ac673269..3ec161a2b3a 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -10,6 +10,7 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias; +use Psalm\Internal\TypeVisitor\ClasslikeReplacer; use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; @@ -20,7 +21,6 @@ use Psalm\Type\Atomic\TCallableList; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; -use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClassStringMap; use Psalm\Type\Atomic\TClosedResource; @@ -76,8 +76,15 @@ use function strpos; use function strtolower; +/** + * @psalm-immutable + */ abstract class Atomic implements TypeNode { + public function __construct(bool $from_docblock = false) + { + $this->from_docblock = $from_docblock; + } /** * Whether or not the type has been checked yet * @@ -108,6 +115,35 @@ abstract class Atomic implements TypeNode public $text; /** + * @return static + */ + public function setFromDocblock(bool $from_docblock): self + { + if ($from_docblock === $this->from_docblock) { + return $this; + } + $cloned = clone $this; + $cloned->from_docblock = $from_docblock; + return $cloned; + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type = $this; + /** @psalm-suppress ImpureMethodCall ClasslikeReplacer will always clone */ + (new ClasslikeReplacer( + $old, + $new + ))->traverse($type); + return $type; + } + + /** + * @psalm-suppress InaccessibleProperty Allowed during construction + * * @param int $analysis_php_version_id contains php version when the type comes from signature * @param array> $template_type_map * @param array $type_aliases @@ -116,7 +152,32 @@ public static function create( string $value, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + ?int $offset_start = null, + ?int $offset_end = null, + ?string $text = null, + bool $from_docblock = false + ): Atomic { + $result = self::createInner($value, $analysis_php_version_id, $template_type_map, $type_aliases); + $result->offset_start = $offset_start; + $result->offset_end = $offset_end; + $result->text = $text; + $result->from_docblock = $from_docblock; + return $result; + } + /** + * @psalm-suppress InaccessibleProperty Allowed during construction + * + * @param int $analysis_php_version_id contains php version when the type comes from signature + * @param array> $template_type_map + * @param array $type_aliases + */ + private static function createInner( + string $value, + ?int $analysis_php_version_id = null, + array $template_type_map = [], + array $type_aliases = [], + bool $from_docblock = false ): Atomic { switch ($value) { case 'int': @@ -178,23 +239,33 @@ public static function create( case 'array': case 'associative-array': - return new TArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'non-empty-array': - return new TNonEmptyArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TNonEmptyArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'callable-array': - return new TCallableArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TCallableArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'list': - return new TList(Type::getMixed()); + return new TList(Type::getMixed(false, $from_docblock)); case 'non-empty-list': - return new TNonEmptyList(Type::getMixed()); + return new TNonEmptyList(Type::getMixed(false, $from_docblock)); case 'non-empty-string': return new TNonEmptyString(); + case 'truthy-string': case 'non-falsy-string': return new TNonFalsyString(); @@ -213,6 +284,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(); @@ -354,7 +434,7 @@ public function isNamedObjectType(): bool || ($this instanceof TTemplateParam && ($this->as->hasNamedObjectType() || array_filter( - $this->extra_types ?: [], + $this->extra_types, static fn($extra_type): bool => $extra_type->isNamedObjectType() ) ) @@ -512,108 +592,16 @@ public function hasArrayAccessInterface(Codebase $codebase): bool ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { return []; } - public function replaceClassLike(string $old, string $new): void - { - if ($this instanceof TNamedObject) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TNamedObject - || $this instanceof TIterable - || $this instanceof TTemplateParam - ) { - if ($this->extra_types) { - foreach ($this->extra_types as $extra_type) { - $extra_type->replaceClassLike($old, $new); - } - } - } - - if ($this instanceof TClassConstant) { - if (strtolower($this->fq_classlike_name) === $old) { - $this->fq_classlike_name = $new; - } - } - - if ($this instanceof TClassString && $this->as !== 'object') { - if (strtolower($this->as) === $old) { - $this->as = $new; - } - } - - if ($this instanceof TTemplateParam) { - $this->as->replaceClassLike($old, $new); - } - - if ($this instanceof TLiteralClassString) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TArray - || $this instanceof TGenericObject - || $this instanceof TIterable - ) { - foreach ($this->type_params as $type_param) { - $type_param->replaceClassLike($old, $new); - } - } - - if ($this instanceof TKeyedArray) { - foreach ($this->properties as $property_type) { - $property_type->replaceClassLike($old, $new); - } - } - - if ($this instanceof TClosure - || $this instanceof TCallable - ) { - if ($this->params) { - foreach ($this->params as $param) { - if ($param->type) { - $param->type->replaceClassLike($old, $new); - } - } - } - - if ($this->return_type) { - $this->return_type->replaceClassLike($old, $new); - } - } - } - final public function __toString(): string { return $this->getId(); } - public function __clone() - { - if ($this instanceof TNamedObject - || $this instanceof TTemplateParam - || $this instanceof TIterable - || $this instanceof TObjectWithProperties - ) { - if ($this->extra_types) { - foreach ($this->extra_types as &$type) { - $type = clone $type; - } - } - } - - if ($this instanceof TTemplateParam) { - $this->as = clone $this->as; - } - } - /** * This is the true identifier for the type. It defaults to self::getKey() but can be overrided to be more precise */ @@ -660,6 +648,9 @@ abstract public function toPhpString( abstract public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool; + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -672,14 +663,19 @@ public function replaceTemplateTypesWithStandins( bool $add_lower_bound = false, int $depth = 0 ): self { + // do nothing return $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): self { // do nothing + return $this; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 8e8e1e633e8..17980ae840e 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -9,12 +9,14 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type\Atomic; -use Psalm\Type\TypeNode; use Psalm\Type\Union; use function count; use function implode; +/** + * @psalm-immutable + */ trait CallableTrait { /** @@ -41,23 +43,14 @@ public function __construct( string $value = 'callable', ?array $params = null, ?Union $return_type = null, - ?bool $is_pure = null + ?bool $is_pure = null, + bool $from_docblock = false ) { $this->value = $value; $this->params = $params; $this->return_type = $return_type; $this->is_pure = $is_pure; - } - - public function __clone() - { - if ($this->params) { - foreach ($this->params as &$param) { - $param = clone $param; - } - } - - $this->return_type = $this->return_type ? clone $this->return_type : null; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -187,7 +180,10 @@ public function getId(bool $exact = true, bool $nested = false): string . $this->value . $param_string . $return_type_string; } - public function replaceTemplateTypesWithStandins( + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -198,11 +194,15 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $callable = clone $this; + ): ?array { + $replaced = false; + $params = $this->params; + if ($params) { + foreach ($params as $offset => $param) { + if (!$param->type) { + continue; + } - if ($callable->params) { - foreach ($callable->params as $offset => $param) { $input_param_type = null; if (($input_type instanceof TClosure || $input_type instanceof TCallable) @@ -211,11 +211,7 @@ public function replaceTemplateTypesWithStandins( $input_param_type = $input_type->params[$offset]->type; } - if (!$param->type) { - continue; - } - - $param->type = TemplateStandinTypeReplacer::replace( + $new_param = $param->replaceType(TemplateStandinTypeReplacer::replace( $param->type, $template_result, $codebase, @@ -228,13 +224,16 @@ public function replaceTemplateTypesWithStandins( !$add_lower_bound, null, $depth - ); + )); + $replaced = $replaced || $new_param !== $param; + $params[$offset] = $new_param; } } - if ($callable->return_type) { - $callable->return_type = TemplateStandinTypeReplacer::replace( - $callable->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateStandinTypeReplacer::replace( + $return_type, $template_result, $codebase, $statements_analyzer, @@ -247,57 +246,60 @@ public function replaceTemplateTypesWithStandins( $replace, $add_lower_bound ); + $replaced = $replaced || $this->return_type !== $return_type; } - return $callable; + if ($replaced) { + return [$params, $return_type]; + } + return null; } - public function replaceTemplateTypesWithArgTypes( + + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - if ($this->params) { - foreach ($this->params as $param) { - if (!$param->type) { - continue; - } + ): ?array { + $replaced = false; - TemplateInferredTypeReplacer::replace( - $param->type, - $template_result, - $codebase - ); + $params = $this->params; + if ($params) { + foreach ($params as $k => $param) { + if ($param->type) { + $new_param = $param->replaceType(TemplateInferredTypeReplacer::replace( + $param->type, + $template_result, + $codebase + )); + $replaced = $replaced || $new_param !== $param; + $params[$k] = $new_param; + } } } - if ($this->return_type) { - TemplateInferredTypeReplacer::replace( - $this->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateInferredTypeReplacer::replace( + $return_type, $template_result, $codebase ); + $replaced = $replaced || $return_type !== $this->return_type; + } + if ($replaced) { + return [$params, $return_type]; } + return null; } /** - * @return list + * @return list */ - public function getChildNodes(): array + protected function getCallableChildNodeKeys(): array { - $child_nodes = []; - - if ($this->params) { - foreach ($this->params as $param) { - if ($param->type) { - $child_nodes[] = $param->type; - } - } - } - - if ($this->return_type) { - $child_nodes[] = $this->return_type; - } - - return $child_nodes; + return ['params', 'return_type']; } } diff --git a/src/Psalm/Type/Atomic/DependentType.php b/src/Psalm/Type/Atomic/DependentType.php index 4ebefe03b38..98cf7a5749c 100644 --- a/src/Psalm/Type/Atomic/DependentType.php +++ b/src/Psalm/Type/Atomic/DependentType.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ interface DependentType { public function getVarId(): string; diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index ba4df1e9873..87767a38fec 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -9,7 +9,6 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\TypeNode; use Psalm\Type\Union; use function array_map; @@ -19,8 +18,27 @@ use function strpos; use function substr; +/** + * @template TTypeParams as array + * @psalm-immutable + */ trait GenericTrait { + /** + * @param TTypeParams $type_params + * + * @return static + */ + public function replaceTypeParams(array $type_params): self + { + if ($this->type_params === $type_params) { + return $this; + } + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + public function getId(bool $exact = true, bool $nested = false): string { $s = ''; @@ -139,22 +157,10 @@ public function toNamespacedString( '>' . $extra_types; } - public function __clone() - { - foreach ($this->type_params as &$type_param) { - $type_param = clone $type_param; - } - } - /** - * @return array + * @return TTypeParams|null */ - public function getChildNodes(): array - { - return $this->type_params; - } - - public function replaceTemplateTypesWithStandins( + protected function replaceTypeParamsTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -165,7 +171,7 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { + ): ?array { if ($input_type instanceof TList) { $input_type = new TArray([Type::getInt(), $input_type->type_param]); } @@ -185,9 +191,9 @@ public function replaceTemplateTypesWithStandins( ); } - $atomic = clone $this; + $type_params = $this->type_params; - foreach ($atomic->type_params as $offset => $type_param) { + foreach ($type_params as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TIterable @@ -208,7 +214,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_object_type_params[$offset]; } - $atomic->type_params[$offset] = TemplateStandinTypeReplacer::replace( + $type_params[$offset] = TemplateStandinTypeReplacer::replace( $type_param, $template_result, $codebase, @@ -227,31 +233,31 @@ public function replaceTemplateTypesWithStandins( ); } - return $atomic; + return $type_params === $this->type_params ? null : $type_params; } - public function replaceTemplateTypesWithArgTypes( + /** + * @return TTypeParams|null + */ + protected function replaceTypeParamsTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->type_params as $offset => $type_param) { - TemplateInferredTypeReplacer::replace( + ): ?array { + $type_params = $this->type_params; + foreach ($type_params as $offset => $type_param) { + $type_param = TemplateInferredTypeReplacer::replace( $type_param, $template_result, $codebase ); if ($this instanceof TArray && $offset === 0 && $type_param->isMixed()) { - $this->type_params[0] = Type::getArrayKey(); + $type_param = Type::getArrayKey(); } - } - if ($this instanceof TGenericObject) { - $this->remapped_params = true; + $type_params[$offset] = $type_param; } - if ($this instanceof TGenericObject || $this instanceof TIterable) { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); - } + return $type_params === $this->type_params ? null : $type_params; } } diff --git a/src/Psalm/Type/Atomic/HasIntersectionTrait.php b/src/Psalm/Type/Atomic/HasIntersectionTrait.php index 720379d9295..7889a0111a8 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -3,19 +3,24 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type\Atomic; use function array_map; +use function array_merge; use function implode; +/** + * @psalm-immutable + */ trait HasIntersectionTrait { /** - * @var array|null + * @var array */ - public $extra_types; + public array $extra_types = []; /** * @param array $aliased_classes @@ -49,26 +54,49 @@ private function getNamespacedIntersectionTypes( /** * @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $type + * + * @return static */ - public function addIntersectionType(Atomic $type): void + public function addIntersectionType(Atomic $type): self { - $this->extra_types[$type->getKey()] = $type; + return $this->setIntersectionTypes(array_merge( + $this->extra_types, + [$type->getKey() => $type] + )); } /** - * @return array|null + * @param array $types + * + * @return static */ - public function getIntersectionTypes(): ?array + public function setIntersectionTypes(array $types): self + { + if ($types === $this->extra_types) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $types; + return $cloned; + } + + /** + * @return array + */ + public function getIntersectionTypes(): array { return $this->extra_types; } - public function replaceIntersectionTemplateTypesWithArgTypes( + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): ?array { if (!$this->extra_types) { - return; + return null; } $new_types = []; @@ -90,11 +118,49 @@ public function replaceIntersectionTemplateTypesWithArgTypes( } } } else { - $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $extra_type = $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); $new_types[$extra_type->getKey()] = $extra_type; } } - $this->extra_types = $new_types; + return $new_types === $this->extra_types ? null : $new_types; + } + + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): ?array { + if (!$this->extra_types) { + return null; + } + $new_types = []; + foreach ($this->extra_types as $type) { + $type = $type->replaceTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $new_types[$type->getKey()] = $type; + } + + return $new_types === $this->extra_types ? null : $new_types; } } diff --git a/src/Psalm/Type/Atomic/Scalar.php b/src/Psalm/Type/Atomic/Scalar.php index dbc2f1a4691..764b34af86a 100644 --- a/src/Psalm/Type/Atomic/Scalar.php +++ b/src/Psalm/Type/Atomic/Scalar.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ abstract class Scalar extends Atomic { public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool diff --git a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php index 1a29e4d27a4..a6da68a042c 100644 --- a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php +++ b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php @@ -4,6 +4,7 @@ /** * Denotes an anonymous class (i.e. `new class{}`) with potential methods + * @psalm-immutable */ final class TAnonymousClassInstance extends TNamedObject { @@ -14,10 +15,15 @@ final class TAnonymousClassInstance extends TNamedObject /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, ?string $extends = null) - { - parent::__construct($value, $is_static); + public function __construct( + string $value, + bool $is_static = false, + ?string $extends = null, + array $extra_types = [] + ) { + parent::__construct($value, $is_static, false, $extra_types); $this->extends = $extends; } diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 88041e209a8..0b51e220fbf 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -10,15 +13,19 @@ /** * Denotes a simple array of the form `array`. It expects an array with two elements, both union types. + * @psalm-immutable */ class TArray extends Atomic { + /** + * @use GenericTrait + */ use GenericTrait; /** * @var array{Union, Union} */ - public $type_params; + public array $type_params; /** * @var string @@ -30,9 +37,10 @@ class TArray extends Atomic * * @param array{Union, Union} $type_params */ - public function __construct(array $type_params) + public function __construct(array $type_params, bool $from_docblock = false) { $this->type_params = $type_params; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -96,4 +104,61 @@ public function isEmptyArray(): bool { return $this->type_params[1]->isNever(); } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $type_params = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + public function getChildNodeKeys(): array + { + return ['type_params']; + } } diff --git a/src/Psalm/Type/Atomic/TArrayKey.php b/src/Psalm/Type/Atomic/TArrayKey.php index bf317292958..9938d6a7082 100644 --- a/src/Psalm/Type/Atomic/TArrayKey.php +++ b/src/Psalm/Type/Atomic/TArrayKey.php @@ -4,6 +4,7 @@ /** * Denotes the `array-key` type, used for something that could be the offset of an `array`. + * @psalm-immutable */ class TArrayKey extends Scalar { diff --git a/src/Psalm/Type/Atomic/TBool.php b/src/Psalm/Type/Atomic/TBool.php index ec7cbc06258..2312152afbd 100644 --- a/src/Psalm/Type/Atomic/TBool.php +++ b/src/Psalm/Type/Atomic/TBool.php @@ -4,6 +4,7 @@ /** * Denotes the `bool` type where the exact value is unknown. + * @psalm-immutable */ class TBool extends Scalar { diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index c0dc900af9e..18c62ed283e 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -2,10 +2,14 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; /** * Denotes the `callable` type. Can result from an `is_callable` check. + * @psalm-immutable */ final class TCallable extends Atomic { @@ -32,4 +36,63 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return $this->params === null && $this->return_type === null; } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + + public function getChildNodeKeys(): array + { + return $this->getCallableChildNodeKeys(); + } } diff --git a/src/Psalm/Type/Atomic/TCallableArray.php b/src/Psalm/Type/Atomic/TCallableArray.php index 4dba9674653..332dfb1118c 100644 --- a/src/Psalm/Type/Atomic/TCallableArray.php +++ b/src/Psalm/Type/Atomic/TCallableArray.php @@ -4,6 +4,7 @@ /** * Denotes an array that is _also_ `callable`. + * @psalm-immutable */ final class TCallableArray extends TNonEmptyArray { diff --git a/src/Psalm/Type/Atomic/TCallableKeyedArray.php b/src/Psalm/Type/Atomic/TCallableKeyedArray.php index 91c89e0600e..21a774e454e 100644 --- a/src/Psalm/Type/Atomic/TCallableKeyedArray.php +++ b/src/Psalm/Type/Atomic/TCallableKeyedArray.php @@ -4,6 +4,7 @@ /** * Denotes an object-like array that is _also_ `callable`. + * @psalm-immutable */ final class TCallableKeyedArray extends TKeyedArray { diff --git a/src/Psalm/Type/Atomic/TCallableList.php b/src/Psalm/Type/Atomic/TCallableList.php index 1429d36ed0e..67c490e173a 100644 --- a/src/Psalm/Type/Atomic/TCallableList.php +++ b/src/Psalm/Type/Atomic/TCallableList.php @@ -4,6 +4,7 @@ /** * Denotes a list that is _also_ `callable`. + * @psalm-immutable */ final class TCallableList extends TNonEmptyList { diff --git a/src/Psalm/Type/Atomic/TCallableObject.php b/src/Psalm/Type/Atomic/TCallableObject.php index 2bb7e3b2791..9114a313de2 100644 --- a/src/Psalm/Type/Atomic/TCallableObject.php +++ b/src/Psalm/Type/Atomic/TCallableObject.php @@ -4,6 +4,7 @@ /** * Denotes an object that is also `callable` (i.e. it has `__invoke` defined). + * @psalm-immutable */ final class TCallableObject extends TObject { diff --git a/src/Psalm/Type/Atomic/TCallableString.php b/src/Psalm/Type/Atomic/TCallableString.php index 1f52a5da8a5..c95456a0ede 100644 --- a/src/Psalm/Type/Atomic/TCallableString.php +++ b/src/Psalm/Type/Atomic/TCallableString.php @@ -4,6 +4,7 @@ /** * Denotes the `callable-string` type, used to represent an unknown string that is also `callable`. + * @psalm-immutable */ final class TCallableString extends TNonFalsyString { diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index 79bd6497246..dea31daa0c9 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -7,6 +7,7 @@ /** * Denotes a class constant whose value might not yet be known. + * @psalm-immutable */ final class TClassConstant extends Atomic { @@ -16,10 +17,11 @@ final class TClassConstant extends Atomic /** @var string */ public $const_name; - public function __construct(string $fq_classlike_name, string $const_name) + public function __construct(string $fq_classlike_name, string $const_name, bool $from_docblock = false) { $this->fq_classlike_name = $fq_classlike_name; $this->const_name = $const_name; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index 1124c9e6a78..b8eec740027 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -20,6 +20,7 @@ /** * Denotes the `class-string` type, used to describe a string representing a valid PHP class. * The parent type from which the classes descend may or may not be specified in the constructor. + * @psalm-immutable */ class TClassString extends TString { @@ -28,10 +29,7 @@ class TClassString extends TString */ public $as; - /** - * @var ?TNamedObject - */ - public $as_type; + public ?TNamedObject $as_type; /** @var bool */ public $is_loaded = false; @@ -42,12 +40,21 @@ class TClassString extends TString /** @var bool */ public $is_enum = false; - public function __construct(string $as = 'object', ?TNamedObject $as_type = null) - { + public function __construct( + string $as = 'object', + ?TNamedObject $as_type = null, + bool $is_loaded = false, + bool $is_interface = false, + bool $is_enum = false, + bool $from_docblock = false + ) { $this->as = $as; $this->as_type = $as_type; + $this->is_loaded = $is_loaded; + $this->is_interface = $is_interface; + $this->is_enum = $is_enum; + $this->from_docblock = $from_docblock; } - public function getKey(bool $include_extra = true): string { if ($this->is_interface) { @@ -128,11 +135,14 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->as_type ? [$this->as_type] : []; + return $this->as_type ? ['as_type'] : []; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -144,11 +154,9 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $class_string = clone $this; - - if (!$class_string->as_type) { - return $class_string; + ): self { + if (!$this->as_type) { + return $this; } if ($input_type instanceof TLiteralClassString) { @@ -160,7 +168,7 @@ public function replaceTemplateTypesWithStandins( } $as_type = TemplateStandinTypeReplacer::replace( - new Union([$class_string->as_type]), + new Union([$this->as_type]), $template_result, $codebase, $statements_analyzer, @@ -176,15 +184,19 @@ public function replaceTemplateTypesWithStandins( $as_type_types = array_values($as_type->getAtomicTypes()); - $class_string->as_type = count($as_type_types) === 1 + $as_type = count($as_type_types) === 1 && $as_type_types[0] instanceof TNamedObject ? $as_type_types[0] : null; - if (!$class_string->as_type) { - $class_string->as = 'object'; + if ($this->as_type === $as_type) { + return $this; } - - return $class_string; + $cloned = clone $this; + $cloned->as_type = $as_type; + if (!$cloned->as_type) { + $cloned->as = 'object'; + } + return $cloned; } } diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index aadcad0b7ce..fe3e1c3e39c 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -16,6 +16,7 @@ /** * Represents an array where the type of each value * is a function of its string key value + * @psalm-immutable */ final class TClassStringMap extends Atomic { @@ -24,10 +25,7 @@ final class TClassStringMap extends Atomic */ public $param_name; - /** - * @var ?TNamedObject - */ - public $as_type; + public ?TNamedObject $as_type; /** * @var Union @@ -37,11 +35,16 @@ final class TClassStringMap extends Atomic /** * Constructs a new instance of a list */ - public function __construct(string $param_name, ?TNamedObject $as_type, Union $value_param) - { - $this->value_param = $value_param; + public function __construct( + string $param_name, + ?TNamedObject $as_type, + Union $value_param, + bool $from_docblock = false + ) { $this->param_name = $param_name; $this->as_type = $as_type; + $this->value_param = $value_param; + $this->from_docblock = $from_docblock; } public function getId(bool $exact = true, bool $nested = false): string @@ -56,11 +59,6 @@ public function getId(bool $exact = true, bool $nested = false): string . '>'; } - public function __clone() - { - $this->value_param = clone $this->value_param; - } - /** * @param array $aliased_classes * @@ -117,6 +115,10 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @psalm-suppress InaccessibleProperty We're only acting on cloned instances + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -128,10 +130,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $map = clone $this; + ): self { + $cloned = null; - foreach ([Type::getString(), $map->value_param] as $offset => $type_param) { + foreach ([Type::getString(), $this->value_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -170,28 +172,40 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $map->value_param = $value_param; + if ($offset === 1 && ($cloned || $this->value_param !== $value_param)) { + $cloned ??= clone $this; + $cloned->value_param = $value_param; } } - return $map; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $value_param = TemplateInferredTypeReplacer::replace( $this->value_param, $template_result, $codebase ); + if ($value_param === $this->value_param) { + return $this; + } + return new static( + $this->param_name, + $this->as_type, + $value_param + ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->value_param]; + return ['value_param']; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TClosedResource.php b/src/Psalm/Type/Atomic/TClosedResource.php index 5d4828fbf25..3d867d73818 100644 --- a/src/Psalm/Type/Atomic/TClosedResource.php +++ b/src/Psalm/Type/Atomic/TClosedResource.php @@ -6,6 +6,7 @@ /** * Denotes the `resource` type that has been closed (e.g. a file handle through `fclose()`). + * @psalm-immutable */ final class TClosedResource extends Atomic { diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index fda5f17726e..7b0345e9eb3 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -2,8 +2,18 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\FunctionLikeParameter; +use Psalm\Type\Atomic; +use Psalm\Type\Union; + +use function array_merge; + /** * Represents a closure where we know the return type and params + * @psalm-immutable */ final class TClosure extends TNamedObject { @@ -12,8 +22,110 @@ final class TClosure extends TNamedObject /** @var array */ public $byref_uses = []; + /** + * @param list $params + * @param array $byref_uses + * @param array $extra_types + */ + public function __construct( + string $value = 'callable', + ?array $params = null, + ?Union $return_type = null, + ?bool $is_pure = null, + array $byref_uses = [], + array $extra_types = [], + bool $from_docblock = false + ) { + $this->value = $value; + $this->params = $params; + $this->return_type = $return_type; + $this->is_pure = $is_pure; + $this->byref_uses = $byref_uses; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes( + TemplateResult $template_result, + ?Codebase $codebase + ): self { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + public function getChildNodeKeys(): array + { + return array_merge(parent::getChildNodeKeys(), $this->getCallableChildNodeKeys()); + } } diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 070f7ac2d71..b1c2d6b9794 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -10,6 +10,7 @@ /** * Internal representation of a conditional return type in phpdoc. For example ($param1 is int ? int : string) + * @psalm-immutable */ final class TConditional extends Atomic { @@ -49,7 +50,8 @@ public function __construct( Union $as_type, Union $conditional_type, Union $if_type, - Union $else_type + Union $else_type, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; @@ -57,14 +59,33 @@ public function __construct( $this->conditional_type = $conditional_type; $this->if_type = $if_type; $this->else_type = $else_type; + $this->from_docblock = $from_docblock; } - public function __clone() - { - $this->conditional_type = clone $this->conditional_type; - $this->if_type = clone $this->if_type; - $this->else_type = clone $this->else_type; - $this->as_type = clone $this->as_type; + public function replaceTypes( + ?Union $as_type, + ?Union $conditional_type = null, + ?Union $if_type = null, + ?Union $else_type = null + ): self { + $as_type ??= $this->as_type; + $conditional_type ??= $this->conditional_type; + $if_type ??= $this->if_type; + $else_type ??= $this->else_type; + + if ($as_type === $this->as_type + && $conditional_type === $this->conditional_type + && $if_type === $this->if_type + && $else_type === $this->else_type + ) { + return $this; + } + $cloned = clone $this; + $cloned->as_type = $as_type; + $cloned->conditional_type = $conditional_type; + $cloned->if_type = $if_type; + $cloned->else_type = $else_type; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -114,9 +135,9 @@ public function toNamespacedString( return ''; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->conditional_type, $this->if_type, $this->else_type]; + return ['conditional_type', 'if_type', 'else_type']; } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -124,14 +145,28 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $conditional = TemplateInferredTypeReplacer::replace( $this->conditional_type, $template_result, $codebase ); + if ($conditional === $this->conditional_type) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $this->as_type, + $conditional, + $this->if_type, + $this->else_type + ); } } diff --git a/src/Psalm/Type/Atomic/TDependentGetClass.php b/src/Psalm/Type/Atomic/TDependentGetClass.php index b1aacc2ceb0..bde7432c18b 100644 --- a/src/Psalm/Type/Atomic/TDependentGetClass.php +++ b/src/Psalm/Type/Atomic/TDependentGetClass.php @@ -2,11 +2,11 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; use Psalm\Type\Union; /** * Represents a string whose value is a fully-qualified class found by get_class($var) + * @psalm-immutable */ final class TDependentGetClass extends TString implements DependentType { @@ -51,7 +51,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TClassString { return new TClassString(); } diff --git a/src/Psalm/Type/Atomic/TDependentGetDebugType.php b/src/Psalm/Type/Atomic/TDependentGetDebugType.php index aa51ceb6f84..fd56a78cbc9 100644 --- a/src/Psalm/Type/Atomic/TDependentGetDebugType.php +++ b/src/Psalm/Type/Atomic/TDependentGetDebugType.php @@ -2,10 +2,9 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a string whose value is that of a type found by get_debug_type($var) + * @psalm-immutable */ final class TDependentGetDebugType extends TString implements DependentType { @@ -34,7 +33,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TString { return new TString(); } diff --git a/src/Psalm/Type/Atomic/TDependentGetType.php b/src/Psalm/Type/Atomic/TDependentGetType.php index 6d53ef62bbd..44181f9d573 100644 --- a/src/Psalm/Type/Atomic/TDependentGetType.php +++ b/src/Psalm/Type/Atomic/TDependentGetType.php @@ -4,6 +4,7 @@ /** * Represents a string whose value is that of a type found by gettype($var) + * @psalm-immutable */ final class TDependentGetType extends TString { diff --git a/src/Psalm/Type/Atomic/TDependentListKey.php b/src/Psalm/Type/Atomic/TDependentListKey.php index 22f2e1c95bc..c18109e6ae7 100644 --- a/src/Psalm/Type/Atomic/TDependentListKey.php +++ b/src/Psalm/Type/Atomic/TDependentListKey.php @@ -2,10 +2,9 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a list key created from foreach ($list as $key => $value) + * @psalm-immutable */ final class TDependentListKey extends TInt implements DependentType { @@ -39,7 +38,7 @@ public function getAssertionString(): string return 'int'; } - public function getReplacement(): Atomic + public function getReplacement(): TInt { return new TInt(); } diff --git a/src/Psalm/Type/Atomic/TEmptyMixed.php b/src/Psalm/Type/Atomic/TEmptyMixed.php index adbe375cfbd..fb4b5c297b6 100644 --- a/src/Psalm/Type/Atomic/TEmptyMixed.php +++ b/src/Psalm/Type/Atomic/TEmptyMixed.php @@ -5,6 +5,7 @@ /** * Denotes the `mixed` type, but empty. * Generated for `$x` inside the `if` statement `if (!$x) {...}` when `$x` is `mixed` outside. + * @psalm-immutable */ final class TEmptyMixed extends TMixed { diff --git a/src/Psalm/Type/Atomic/TEmptyNumeric.php b/src/Psalm/Type/Atomic/TEmptyNumeric.php index 7199f6821e2..1a3ce1f67c8 100644 --- a/src/Psalm/Type/Atomic/TEmptyNumeric.php +++ b/src/Psalm/Type/Atomic/TEmptyNumeric.php @@ -4,6 +4,7 @@ /** * Denotes the `numeric` type that's also empty (which can also result from an `is_numeric` and `empty` check). + * @psalm-immutable */ final class TEmptyNumeric extends TNumeric { diff --git a/src/Psalm/Type/Atomic/TEmptyScalar.php b/src/Psalm/Type/Atomic/TEmptyScalar.php index 036d28bef1d..cb2543810d9 100644 --- a/src/Psalm/Type/Atomic/TEmptyScalar.php +++ b/src/Psalm/Type/Atomic/TEmptyScalar.php @@ -4,6 +4,7 @@ /** * Denotes a `scalar` type that is also empty. + * @psalm-immutable */ final class TEmptyScalar extends TScalar { diff --git a/src/Psalm/Type/Atomic/TEnumCase.php b/src/Psalm/Type/Atomic/TEnumCase.php index 5522714948e..614a59c1d5d 100644 --- a/src/Psalm/Type/Atomic/TEnumCase.php +++ b/src/Psalm/Type/Atomic/TEnumCase.php @@ -4,6 +4,7 @@ /** * Denotes an enum with a specific value + * @psalm-immutable */ final class TEnumCase extends TNamedObject { diff --git a/src/Psalm/Type/Atomic/TFalse.php b/src/Psalm/Type/Atomic/TFalse.php index dbcb4ec8a6b..2ccd395d002 100644 --- a/src/Psalm/Type/Atomic/TFalse.php +++ b/src/Psalm/Type/Atomic/TFalse.php @@ -4,6 +4,7 @@ /** * Denotes the `false` value type + * @psalm-immutable */ final class TFalse extends TBool { diff --git a/src/Psalm/Type/Atomic/TFloat.php b/src/Psalm/Type/Atomic/TFloat.php index f30592dd8fd..d856df6e2cc 100644 --- a/src/Psalm/Type/Atomic/TFloat.php +++ b/src/Psalm/Type/Atomic/TFloat.php @@ -4,6 +4,7 @@ /** * Denotes the `float` type, where the exact value is unknown. + * @psalm-immutable */ class TFloat extends Scalar { diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 44f885e0757..7e78bd1324e 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -13,15 +16,19 @@ /** * Denotes an object type that has generic parameters e.g. `ArrayObject` + * @psalm-immutable */ final class TGenericObject extends TNamedObject { + /** + * @use GenericTrait> + */ use GenericTrait; /** * @var non-empty-list */ - public $type_params; + public array $type_params; /** @var bool if the parameters have been remapped to another class */ public $remapped_params = false; @@ -29,15 +36,26 @@ final class TGenericObject extends TNamedObject /** * @param string $value the name of the object * @param non-empty-list $type_params + * @param array $extra_types */ - public function __construct(string $value, array $type_params) - { + public function __construct( + string $value, + array $type_params, + bool $remapped_params = false, + bool $is_static = false, + array $extra_types = [], + bool $from_docblock = false + ) { if ($value[0] === '\\') { $value = substr($value, 1); } $this->value = $value; $this->type_params = $type_params; + $this->remapped_params = $remapped_params; + $this->is_static = $is_static; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -103,8 +121,84 @@ public function getAssertionString(): string return $this->value; } - public function getChildNodes(): array + public function getChildNodeKeys(): array + { + return array_merge(parent::getChildNodeKeys(), ['type_params']); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $this->value, + $types ?? $this->type_params, + $this->remapped_params, + $this->is_static, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self { - return array_merge($this->type_params, $this->extra_types ?? []); + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + $this->value, + $type_params ?? $this->type_params, + true, + $this->is_static, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TInt.php b/src/Psalm/Type/Atomic/TInt.php index 95f6506509a..fd7ad05cb7d 100644 --- a/src/Psalm/Type/Atomic/TInt.php +++ b/src/Psalm/Type/Atomic/TInt.php @@ -4,6 +4,7 @@ /** * Denotes the `int` type, where the exact value is unknown. + * @psalm-immutable */ class TInt extends Scalar { diff --git a/src/Psalm/Type/Atomic/TIntMask.php b/src/Psalm/Type/Atomic/TIntMask.php index 4d3dca8340a..a77babf1062 100644 --- a/src/Psalm/Type/Atomic/TIntMask.php +++ b/src/Psalm/Type/Atomic/TIntMask.php @@ -7,6 +7,7 @@ /** * Represents the type that is the result of a bitmask combination of its parameters. * `int-mask<1, 2, 4>` corresponds to `0|1|2|3|4|5|6|7` + * @psalm-immutable */ final class TIntMask extends TInt { @@ -14,9 +15,10 @@ final class TIntMask extends TInt public $values; /** @param non-empty-array $values */ - public function __construct(array $values) + public function __construct(array $values, bool $from_docblock = false) { $this->values = $values; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index 3e210e409e8..b1fd8c987f0 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -8,6 +8,7 @@ * Represents the type that is the result of a bitmask combination of its parameters. * This is the same concept as TIntMask but TIntMaskOf is used with a reference to constants in code * `int-mask-of` will corresponds to `0|1|2|3|4|5|6|7` if there are three constant 1, 2 and 4 + * @psalm-immutable */ final class TIntMaskOf extends TInt { @@ -17,9 +18,10 @@ final class TIntMaskOf extends TInt /** * @param TClassConstant|TKeyOf|TValueOf $value */ - public function __construct(Atomic $value) + public function __construct(Atomic $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -45,6 +47,11 @@ public function toNamespacedString( . '>'; } + public function getChildNodeKeys(): array + { + return ['value']; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; diff --git a/src/Psalm/Type/Atomic/TIntRange.php b/src/Psalm/Type/Atomic/TIntRange.php index eec4e671f65..57fe237f520 100644 --- a/src/Psalm/Type/Atomic/TIntRange.php +++ b/src/Psalm/Type/Atomic/TIntRange.php @@ -7,6 +7,7 @@ /** * Denotes an interval of integers between two bounds + * @psalm-immutable */ final class TIntRange extends TInt { @@ -22,10 +23,11 @@ final class TIntRange extends TInt */ public $max_bound; - public function __construct(?int $min_bound, ?int $max_bound) + public function __construct(?int $min_bound, ?int $max_bound, bool $from_docblock = false) { $this->min_bound = $min_bound; $this->max_bound = $max_bound; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index ed8462a5924..91f75bc9fb8 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -2,27 +2,33 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; use function count; use function implode; use function substr; /** * denotes the `iterable` type(which can also result from an `is_iterable` check). + * @psalm-immutable */ final class TIterable extends Atomic { use HasIntersectionTrait; + /** + * @use GenericTrait + */ use GenericTrait; /** * @var array{Union, Union} */ - public $type_params; + public array $type_params; /** * @var string @@ -35,16 +41,19 @@ final class TIterable extends Atomic public $has_docblock_params = false; /** - * @param list $type_params + * @param array{Union, Union}|array $type_params + * @param array $extra_types */ - public function __construct(array $type_params = []) + public function __construct(array $type_params = [], array $extra_types = [], bool $from_docblock = false) { - if (count($type_params) === 2) { + if (isset($type_params[0], $type_params[1])) { $this->has_docblock_params = true; $this->type_params = $type_params; } else { $this->type_params = [Type::getMixed(), Type::getMixed()]; } + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -113,8 +122,75 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } - public function getChildNodes(): array + public function getChildNodeKeys(): array + { + return ['type_params', 'extra_types']; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self { - return array_merge($this->type_params, $this->extra_types ?? []); + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + return new static( + $type_params ?? $this->type_params, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $types ?? $this->type_params, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TKeyOf.php b/src/Psalm/Type/Atomic/TKeyOf.php index 027a0e04fb5..b8a4b33f854 100644 --- a/src/Psalm/Type/Atomic/TKeyOf.php +++ b/src/Psalm/Type/Atomic/TKeyOf.php @@ -10,15 +10,17 @@ /** * Represents an offset of an array. + * @psalm-immutable */ final class TKeyOf extends TArrayKey { /** @var Union */ public $type; - public function __construct(Union $type) + public function __construct(Union $type, bool $from_docblock = false) { $this->type = $type; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b92926ffb1b..99e14a64deb 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -31,6 +31,7 @@ /** * Represents an 'object-like array' - an array with known keys. + * @psalm-immutable */ class TKeyedArray extends Atomic { @@ -77,10 +78,37 @@ class TKeyedArray extends Atomic * @param non-empty-array $properties * @param array $class_strings */ - public function __construct(array $properties, ?array $class_strings = null) - { + public function __construct( + array $properties, + ?array $class_strings = null, + bool $sealed = false, + ?Union $previous_key_type = null, + ?Union $previous_value_type = null, + bool $is_list = false, + bool $from_docblock = false + ) { $this->properties = $properties; $this->class_strings = $class_strings; + $this->sealed = $sealed; + $this->previous_key_type = $previous_key_type; + $this->previous_value_type = $previous_value_type; + $this->is_list = $is_list; + $this->from_docblock = $from_docblock; + } + + /** + * @param non-empty-array $properties + * + * @return static + */ + public function setProperties(array $properties): self + { + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; } public function getId(bool $exact = true, bool $nested = false): string @@ -213,6 +241,9 @@ public function getGenericValueType(): Union return $value_type; } + /** + * @return TArray|TNonEmptyArray + */ public function getGenericArrayType(bool $allow_non_empty = true): TArray { $key_types = []; @@ -263,19 +294,15 @@ public function isNonEmpty(): bool return false; } - public function __clone() - { - foreach ($this->properties as &$property) { - $property = clone $property; - } - } - public function getKey(bool $include_extra = true): string { /** @var string */ return static::KEY; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -287,10 +314,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = $this->properties; - foreach ($this->properties as $offset => $property) { + foreach ($properties as $offset => $property) { $input_type_param = null; if ($input_type instanceof TKeyedArray @@ -299,7 +326,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $properties[$offset] = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -315,25 +342,40 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + ): self { + $properties = $this->properties; + foreach ($properties as $offset => $property) { + $properties[$offset] = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase ); } + if ($properties !== $this->properties) { + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + return $this; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->properties; + return ['properties']; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 44d6578ee41..78beb798b34 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -18,6 +18,8 @@ * - its keys are integers * - they start at 0 * - they are consecutive and go upwards (no negative int) + * + * @psalm-immutable */ class TList extends Atomic { @@ -32,19 +34,28 @@ class TList extends Atomic /** * Constructs a new instance of a list */ - public function __construct(Union $type_param) + public function __construct(Union $type_param, bool $from_docblock = false) { $this->type_param = $type_param; + $this->from_docblock = $from_docblock; } - public function getId(bool $exact = true, bool $nested = false): string + /** + * @return static + */ + public function replaceTypeParam(Union $type_param): self { - return static::KEY . '<' . $this->type_param->getId($exact) . '>'; + if ($type_param === $this->type_param) { + return $this; + } + $cloned = clone $this; + $cloned->type_param = $type_param; + return $cloned; } - public function __clone() + public function getId(bool $exact = true, bool $nested = false): string { - $this->type_param = clone $this->type_param; + return static::KEY . '<' . $this->type_param->getId($exact) . '>'; } /** @@ -100,6 +111,10 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @psalm-suppress InaccessibleProperty We're only acting on cloned instances + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -111,10 +126,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $list = clone $this; + ): self { + $cloned = null; - foreach ([Type::getInt(), $list->type_param] as $offset => $type_param) { + foreach ([Type::getInt(), $this->type_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -153,23 +168,27 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $list->type_param = $type_param; + if ($offset === 1 && ($cloned || $this->type_param !== $type_param)) { + $cloned ??= clone $this; + $cloned->type_param = $type_param; } } - return $list; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + return $this->replaceTypeParam(TemplateInferredTypeReplacer::replace( $this->type_param, $template_result, $codebase - ); + )); } public function equals(Atomic $other_type, bool $ensure_source_equality): bool @@ -194,8 +213,8 @@ public function getAssertionString(): string return $this->getId(); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->type_param]; + return ['type_param']; } } diff --git a/src/Psalm/Type/Atomic/TLiteralClassString.php b/src/Psalm/Type/Atomic/TLiteralClassString.php index 6339a5757b3..cc17a77d61e 100644 --- a/src/Psalm/Type/Atomic/TLiteralClassString.php +++ b/src/Psalm/Type/Atomic/TLiteralClassString.php @@ -10,6 +10,7 @@ /** * Denotes a specific class string, generated by expressions like `A::class`. + * @psalm-immutable */ final class TLiteralClassString extends TLiteralString { @@ -19,9 +20,9 @@ final class TLiteralClassString extends TLiteralString */ public $definite_class = false; - public function __construct(string $value, bool $definite_class = false) + public function __construct(string $value, bool $definite_class = false, bool $from_docblock = false) { - parent::__construct($value); + parent::__construct($value, $from_docblock); $this->definite_class = $definite_class; } diff --git a/src/Psalm/Type/Atomic/TLiteralFloat.php b/src/Psalm/Type/Atomic/TLiteralFloat.php index 4e11468304f..991625ec328 100644 --- a/src/Psalm/Type/Atomic/TLiteralFloat.php +++ b/src/Psalm/Type/Atomic/TLiteralFloat.php @@ -4,15 +4,17 @@ /** * Denotes a floating point value where the exact numeric value is known. + * @psalm-immutable */ final class TLiteralFloat extends TFloat { /** @var float */ public $value; - public function __construct(float $value) + public function __construct(float $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLiteralInt.php b/src/Psalm/Type/Atomic/TLiteralInt.php index 390606ebb1b..6a506061d3f 100644 --- a/src/Psalm/Type/Atomic/TLiteralInt.php +++ b/src/Psalm/Type/Atomic/TLiteralInt.php @@ -4,15 +4,17 @@ /** * Denotes an integer value where the exact numeric value is known. + * @psalm-immutable */ final class TLiteralInt extends TInt { /** @var int */ public $value; - public function __construct(int $value) + public function __construct(int $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLiteralString.php b/src/Psalm/Type/Atomic/TLiteralString.php index 71ae7f73551..9f000b90aa9 100644 --- a/src/Psalm/Type/Atomic/TLiteralString.php +++ b/src/Psalm/Type/Atomic/TLiteralString.php @@ -8,15 +8,17 @@ /** * Denotes a string whose value is known. + * @psalm-immutable */ class TLiteralString extends TString { /** @var string */ public $value; - public function __construct(string $value) + public function __construct(string $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLowercaseString.php b/src/Psalm/Type/Atomic/TLowercaseString.php index a9eecb9f362..b65ac5e9dcb 100644 --- a/src/Psalm/Type/Atomic/TLowercaseString.php +++ b/src/Psalm/Type/Atomic/TLowercaseString.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class TLowercaseString extends TString { public function getId(bool $exact = true, bool $nested = false): string diff --git a/src/Psalm/Type/Atomic/TMixed.php b/src/Psalm/Type/Atomic/TMixed.php index 0c029645a47..d122a432b16 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -6,15 +6,18 @@ /** * Denotes the `mixed` type, used when you don’t know the type of an expression. + * + * @psalm-immutable */ class TMixed extends Atomic { /** @var bool */ public $from_loop_isset = false; - public function __construct(bool $from_loop_isset = false) + public function __construct(bool $from_loop_isset = false, bool $from_docblock = false) { $this->from_loop_isset = $from_loop_isset; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 88643acd73f..0323952391f 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -3,6 +3,7 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; @@ -14,6 +15,7 @@ /** * Denotes an object type where the type of the object is known e.g. `Exception`, `Throwable`, `Foo\Bar` + * @psalm-immutable */ class TNamedObject extends Atomic { @@ -37,9 +39,15 @@ class TNamedObject extends Atomic /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, bool $definite_class = false) - { + public function __construct( + string $value, + bool $is_static = false, + bool $definite_class = false, + array $extra_types = [], + bool $from_docblock = false + ) { if ($value[0] === '\\') { $value = substr($value, 1); } @@ -47,6 +55,18 @@ public function __construct(string $value, bool $is_static = false, bool $defini $this->value = $value; $this->is_static = $is_static; $this->definite_class = $definite_class; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + public function setIsStatic(bool $is_static): self + { + if ($this->is_static === $is_static) { + return $this; + } + $cloned = clone $this; + $cloned->is_static = $is_static; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -134,15 +154,58 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return ($this->value !== 'static' && $this->is_static === false) || $analysis_php_version_id >= 8_00_00; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; } - public function getChildNodes(): array + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($intersection) { + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; + } + return $this; + } + public function getChildNodeKeys(): array { - return $this->extra_types ?? []; + return ['extra_types']; } } diff --git a/src/Psalm/Type/Atomic/TNever.php b/src/Psalm/Type/Atomic/TNever.php index b10bfc10cf6..9c468445de7 100644 --- a/src/Psalm/Type/Atomic/TNever.php +++ b/src/Psalm/Type/Atomic/TNever.php @@ -7,6 +7,7 @@ /** * Denotes the `no-return`/`never-return` type for functions that never return, either throwing an exception or * terminating (like the builtin `exit()`). + * @psalm-immutable */ final class TNever extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index d81b5dfe9c8..2c2fba076c8 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -2,9 +2,12 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Denotes array known to be non-empty of the form `non-empty-array`. * It expects an array with two elements, both union types. + * @psalm-immutable */ class TNonEmptyArray extends TArray { @@ -22,4 +25,38 @@ class TNonEmptyArray extends TArray * @var string */ public $value = 'non-empty-array'; + + /** + * @param array{Union, Union} $type_params + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct( + array $type_params, + ?int $count = null, + ?int $min_count = null, + string $value = 'non-empty-array', + bool $from_docblock = false + ) { + $this->type_params = $type_params; + $this->count = $count; + $this->min_count = $min_count; + $this->value = $value; + $this->from_docblock = $from_docblock; + } + + /** + * @param positive-int|null $count + * + * @return static + */ + public function setCount(?int $count): self + { + if ($count === $this->count) { + return $this; + } + $cloned = clone $this; + $cloned->count = $count; + return $cloned; + } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyList.php b/src/Psalm/Type/Atomic/TNonEmptyList.php index 9e6892ba855..c56bd0b5672 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyList.php +++ b/src/Psalm/Type/Atomic/TNonEmptyList.php @@ -2,8 +2,11 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Represents a non-empty list + * @psalm-immutable */ class TNonEmptyList extends TList { @@ -20,6 +23,39 @@ class TNonEmptyList extends TList /** @var non-empty-lowercase-string */ public const KEY = 'non-empty-list'; + /** + * Constructs a new instance of a list + * + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct( + Union $type_param, + ?int $count = null, + ?int $min_count = null, + bool $from_docblock = false + ) { + $this->type_param = $type_param; + $this->count = $count; + $this->min_count = $min_count; + $this->from_docblock = $from_docblock; + } + + /** + * @param positive-int|null $count + * + * @return static + */ + public function setCount(?int $count): self + { + if ($count === $this->count) { + return $this; + } + $cloned = clone $this; + $cloned->count = $count; + return $cloned; + } + public function getAssertionString(): string { return 'non-empty-list'; diff --git a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php index 52812e75783..ab39bc3d497 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php @@ -4,6 +4,7 @@ /** * Denotes a non-empty-string where every character is lowercased. (which can also result from a `strtolower` call). + * @psalm-immutable */ final class TNonEmptyLowercaseString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TNonEmptyMixed.php b/src/Psalm/Type/Atomic/TNonEmptyMixed.php index 1b96eb47f88..b7c829e079c 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyMixed.php +++ b/src/Psalm/Type/Atomic/TNonEmptyMixed.php @@ -5,6 +5,7 @@ /** * Denotes the `mixed` type, but not empty. * Generated for `$x` inside the `if` statement `if ($x) {...}` when `$x` is `mixed` outside. + * @psalm-immutable */ final class TNonEmptyMixed extends TMixed { diff --git a/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php b/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php index 4802a72a261..6cfd2526f34 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-string` type, where the exact value is unknown but * we know that the string is not from user input + * @psalm-immutable */ final class TNonEmptyNonspecificLiteralString extends TNonspecificLiteralString { diff --git a/src/Psalm/Type/Atomic/TNonEmptyScalar.php b/src/Psalm/Type/Atomic/TNonEmptyScalar.php index ec3d56d6fb7..7de002334cd 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyScalar.php +++ b/src/Psalm/Type/Atomic/TNonEmptyScalar.php @@ -4,6 +4,7 @@ /** * Denotes a `scalar` type that is also non-empty. + * @psalm-immutable */ final class TNonEmptyScalar extends TScalar { diff --git a/src/Psalm/Type/Atomic/TNonEmptyString.php b/src/Psalm/Type/Atomic/TNonEmptyString.php index 2a1dbdcf8c4..5aeedb303ba 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyString.php @@ -4,6 +4,7 @@ /** * Denotes a string, that is also non-empty (every string except '') + * @psalm-immutable */ class TNonEmptyString extends TString { diff --git a/src/Psalm/Type/Atomic/TNonFalsyString.php b/src/Psalm/Type/Atomic/TNonFalsyString.php index 467951c1848..e352d1d9f71 100644 --- a/src/Psalm/Type/Atomic/TNonFalsyString.php +++ b/src/Psalm/Type/Atomic/TNonFalsyString.php @@ -4,6 +4,7 @@ /** * Denotes a string, that is also non-falsy (every string except '' and '0') + * @psalm-immutable */ class TNonFalsyString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php index 08c2f4d7af8..fbc5e71f24f 100644 --- a/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php +++ b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-int` type, where the exact value is unknown but * we know that the int is not from user input + * @psalm-immutable */ final class TNonspecificLiteralInt extends TInt { diff --git a/src/Psalm/Type/Atomic/TNonspecificLiteralString.php b/src/Psalm/Type/Atomic/TNonspecificLiteralString.php index b946f0cc944..b6c73cc20ee 100644 --- a/src/Psalm/Type/Atomic/TNonspecificLiteralString.php +++ b/src/Psalm/Type/Atomic/TNonspecificLiteralString.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-string` type, where the exact value is unknown but * we know that the string is not from user input + * @psalm-immutable */ class TNonspecificLiteralString extends TString { diff --git a/src/Psalm/Type/Atomic/TNull.php b/src/Psalm/Type/Atomic/TNull.php index 959a53d0e4a..685509d9678 100644 --- a/src/Psalm/Type/Atomic/TNull.php +++ b/src/Psalm/Type/Atomic/TNull.php @@ -6,6 +6,7 @@ /** * Denotes the `null` type + * @psalm-immutable */ final class TNull extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNumeric.php b/src/Psalm/Type/Atomic/TNumeric.php index 2a0c70acf7f..942a1df0e2c 100644 --- a/src/Psalm/Type/Atomic/TNumeric.php +++ b/src/Psalm/Type/Atomic/TNumeric.php @@ -4,6 +4,7 @@ /** * Denotes the `numeric` type (which can also result from an `is_numeric` check). + * @psalm-immutable */ class TNumeric extends Scalar { diff --git a/src/Psalm/Type/Atomic/TNumericString.php b/src/Psalm/Type/Atomic/TNumericString.php index 8f2109ec0d0..ed1040a3cb4 100644 --- a/src/Psalm/Type/Atomic/TNumericString.php +++ b/src/Psalm/Type/Atomic/TNumericString.php @@ -4,6 +4,7 @@ /** * Denotes a string that's also a numeric value e.g. `"5"`. It can result from `is_string($s) && is_numeric($s)`. + * @psalm-immutable */ final class TNumericString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TObject.php b/src/Psalm/Type/Atomic/TObject.php index 1725c136869..bffa66a5413 100644 --- a/src/Psalm/Type/Atomic/TObject.php +++ b/src/Psalm/Type/Atomic/TObject.php @@ -6,6 +6,7 @@ /** * Denotes the `object` type + * @psalm-immutable */ class TObject extends Atomic { diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 81d3fc76f66..96f1e2d805e 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -12,13 +12,12 @@ use function array_keys; use function array_map; -use function array_merge; -use function array_values; use function count; use function implode; /** * Denotes an object with specified member variables e.g. `object{foo:int, bar:string}`. + * @psalm-immutable */ final class TObjectWithProperties extends TObject { @@ -39,11 +38,44 @@ final class TObjectWithProperties extends TObject * * @param array $properties * @param array $methods + * @param array $extra_types */ - public function __construct(array $properties, array $methods = []) - { + public function __construct( + array $properties, + array $methods = [], + array $extra_types = [], + bool $from_docblock = false + ) { $this->properties = $properties; $this->methods = $methods; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + /** + * @param array $properties + */ + public function setProperties(array $properties): self + { + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + + /** + * @param array $methods + */ + public function setMethods(array $methods): self + { + if ($methods === $this->methods) { + return $this; + } + $cloned = clone $this; + $cloned->methods = $methods; + return $cloned; } public function getId(bool $exact = true, bool $nested = false): string @@ -58,6 +90,7 @@ public function getId(bool $exact = true, bool $nested = false): string ', ', array_map( /** + * @psalm-pure * @param string|int $name */ static fn($name, Union $type): string => $name . ($type->possibly_undefined ? '?' : '') . ':' @@ -70,6 +103,9 @@ public function getId(bool $exact = true, bool $nested = false): string $methods_string = implode( ', ', array_map( + /** + * @psalm-pure + */ static fn(string $name): string => $name . '()', array_keys($this->methods) ) @@ -100,6 +136,7 @@ public function toNamespacedString( ', ', array_map( /** + * @psalm-pure * @param string|int $name */ static fn($name, Union $type): string => @@ -136,13 +173,6 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function __clone() - { - foreach ($this->properties as &$property) { - $property = clone $property; - } - } - public function equals(Atomic $other_type, bool $ensure_source_equality): bool { if (!$other_type instanceof self) { @@ -170,6 +200,9 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -181,8 +214,8 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = []; foreach ($this->properties as $offset => $property) { $input_type_param = null; @@ -193,7 +226,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $properties[$offset] = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -209,25 +242,56 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static($properties, $this->methods, $intersection ?? $this->extra_types); } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + ): self { + $properties = $this->properties; + foreach ($properties as $offset => $property) { + $properties[$offset] = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase ); } + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static( + $properties, + $this->methods, + $intersection ?? $this->extra_types + ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return array_merge($this->properties, $this->extra_types !== null ? array_values($this->extra_types) : []); + return ['properties', 'extra_types']; } public function getAssertionString(): string diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index b3e009c6c92..ffee4f946c5 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -9,8 +9,10 @@ * their apropriate types as values. * * @psalm-type TokenName = 'properties-of'|'public-properties-of'|'protected-properties-of'|'private-properties-of' + * + * @psalm-immutable */ -class TPropertiesOf extends Atomic +final class TPropertiesOf extends Atomic { // These should match the values of // `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are @@ -19,14 +21,7 @@ class TPropertiesOf extends Atomic public const VISIBILITY_PROTECTED = 2; public const VISIBILITY_PRIVATE = 3; - /** - * @var string - */ - public $fq_classlike_name; - /** - * @var TNamedObject - */ - public $classlike_type; + public TNamedObject $classlike_type; /** * @var self::VISIBILITY_*|null */ @@ -45,6 +40,19 @@ public static function tokenNames(): array ]; } + /** + * @param self::VISIBILITY_*|null $visibility_filter + */ + public function __construct( + TNamedObject $classlike_type, + ?int $visibility_filter, + bool $from_docblock = false + ) { + $this->classlike_type = $classlike_type; + $this->visibility_filter = $visibility_filter; + $this->from_docblock = $from_docblock; + } + /** * @param TokenName $tokenName * @return self::VISIBILITY_*|null @@ -64,6 +72,7 @@ public static function filterForTokenName(string $token_name): ?int } /** + * @psalm-pure * @return TokenName */ public static function tokenNameForFilter(?int $visibility_filter): string @@ -80,17 +89,9 @@ public static function tokenNameForFilter(?int $visibility_filter): string } } - /** - * @param self::VISIBILITY_*|null $visibility_filter - */ - public function __construct( - string $fq_classlike_name, - TNamedObject $classlike_type, - ?int $visibility_filter - ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->classlike_type = $classlike_type; - $this->visibility_filter = $visibility_filter; + public function getChildNodeKeys(): array + { + return ['classlike_type']; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TResource.php b/src/Psalm/Type/Atomic/TResource.php index e7ca229412c..bb7c8f3a95d 100644 --- a/src/Psalm/Type/Atomic/TResource.php +++ b/src/Psalm/Type/Atomic/TResource.php @@ -6,6 +6,7 @@ /** * Denotes the `resource` type (e.g. a file handle). + * @psalm-immutable */ final class TResource extends Atomic { diff --git a/src/Psalm/Type/Atomic/TScalar.php b/src/Psalm/Type/Atomic/TScalar.php index 5fc10c5b0a8..de32876a374 100644 --- a/src/Psalm/Type/Atomic/TScalar.php +++ b/src/Psalm/Type/Atomic/TScalar.php @@ -5,6 +5,7 @@ /** * Denotes the `scalar` super type (which can also result from an `is_scalar` check). * This type encompasses `float`, `int`, `bool` and `string`. + * @psalm-immutable */ class TScalar extends Scalar { diff --git a/src/Psalm/Type/Atomic/TSingleLetter.php b/src/Psalm/Type/Atomic/TSingleLetter.php index 80613302479..23e8a354b64 100644 --- a/src/Psalm/Type/Atomic/TSingleLetter.php +++ b/src/Psalm/Type/Atomic/TSingleLetter.php @@ -4,6 +4,7 @@ /** * Denotes a string that has a length of 1 + * @psalm-immutable */ final class TSingleLetter extends TString { diff --git a/src/Psalm/Type/Atomic/TString.php b/src/Psalm/Type/Atomic/TString.php index ee08f110f43..2cfd5f51474 100644 --- a/src/Psalm/Type/Atomic/TString.php +++ b/src/Psalm/Type/Atomic/TString.php @@ -4,6 +4,7 @@ /** * Denotes the `string` type, where the exact value is unknown. + * @psalm-immutable */ class TString extends Scalar { diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index ac3edfd4717..0ef28969bab 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class TTemplateIndexedAccess extends Atomic { /** @@ -24,11 +27,13 @@ final class TTemplateIndexedAccess extends Atomic public function __construct( string $array_param_name, string $offset_param_name, - string $defining_class + string $defining_class, + bool $from_docblock = false ) { $this->array_param_name = $array_param_name; $this->offset_param_name = $offset_param_name; $this->defining_class = $defining_class; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index bb2f5f947e0..9f667a225ca 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TKeyOf when the type of the array is a template + * @psalm-immutable */ final class TTemplateKeyOf extends Atomic { @@ -31,11 +32,13 @@ final class TTemplateKeyOf extends Atomic public function __construct( string $param_name, string $defining_class, - Union $as + Union $as, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -81,14 +84,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index b34d9e4d49f..775f52993e2 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -12,6 +12,7 @@ /** * denotes a template parameter that has been previously specified in a `@template` tag. + * @psalm-immutable */ final class TTemplateParam extends Atomic { @@ -32,11 +33,34 @@ final class TTemplateParam extends Atomic */ public $defining_class; - public function __construct(string $param_name, Union $extends, string $defining_class) - { + /** + * @param array $extra_types + */ + public function __construct( + string $param_name, + Union $extends, + string $defining_class, + array $extra_types = [], + bool $from_docblock = false + ) { $this->param_name = $param_name; $this->as = $extends; $this->defining_class = $defining_class; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + /** + * @return static + */ + public function replaceAs(Union $as): self + { + if ($as === $this->as) { + return $this; + } + $cloned = clone $this; + $cloned->as = $as; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -113,9 +137,9 @@ public function toNamespacedString( return $this->param_name . $intersection_types; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->as]; + return ['as', 'extra_types']; } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -123,10 +147,19 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; } } diff --git a/src/Psalm/Type/Atomic/TTemplateParamClass.php b/src/Psalm/Type/Atomic/TTemplateParamClass.php index a64aadef247..62544ab6217 100644 --- a/src/Psalm/Type/Atomic/TTemplateParamClass.php +++ b/src/Psalm/Type/Atomic/TTemplateParamClass.php @@ -4,6 +4,7 @@ /** * Denotes a `class-string` corresponding to a template parameter previously specified in a `@template` tag. + * @psalm-immutable */ final class TTemplateParamClass extends TClassString { @@ -21,12 +22,14 @@ public function __construct( string $param_name, string $as, ?TNamedObject $as_type, - string $defining_class + string $defining_class, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->as = $as; $this->as_type = $as_type; $this->defining_class = $defining_class; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 5a1ac50e5b8..59fbb539153 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TPropertiesOf when the type of the array is a template + * @psalm-immutable */ final class TTemplatePropertiesOf extends Atomic { @@ -37,12 +38,14 @@ public function __construct( string $param_name, string $defining_class, TTemplateParam $as, - ?int $visibility_filter + ?int $visibility_filter, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; $this->visibility_filter = $visibility_filter; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -76,14 +79,30 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( - new Union([$this->as]), - $template_result, - $codebase + ): self { + $param = new TTemplateParam( + $this->as->param_name, + TemplateInferredTypeReplacer::replace( + new Union([$this->as]), + $template_result, + $codebase, + ), + $this->as->defining_class + ); + if ($param->as === $this->as->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $param, + $this->visibility_filter ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index d00cb597484..bcae2992e87 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TValueOf when the type of the array or enum is a template + * @psalm-immutable */ final class TTemplateValueOf extends Atomic { @@ -31,11 +32,13 @@ final class TTemplateValueOf extends Atomic public function __construct( string $param_name, string $defining_class, - Union $as + Union $as, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -81,14 +84,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTraitString.php b/src/Psalm/Type/Atomic/TTraitString.php index bb4cf93b684..eb0f4b44593 100644 --- a/src/Psalm/Type/Atomic/TTraitString.php +++ b/src/Psalm/Type/Atomic/TTraitString.php @@ -4,6 +4,7 @@ /** * Denotes the `trait-string` type, used to describe a string representing a valid PHP trait. + * @psalm-immutable */ final class TTraitString extends TString { diff --git a/src/Psalm/Type/Atomic/TTrue.php b/src/Psalm/Type/Atomic/TTrue.php index 64545045e17..fa0293dd01d 100644 --- a/src/Psalm/Type/Atomic/TTrue.php +++ b/src/Psalm/Type/Atomic/TTrue.php @@ -4,6 +4,7 @@ /** * Denotes the `true` value type + * @psalm-immutable */ final class TTrue extends TBool { diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 8e70ff71951..8174ba7ac23 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -7,6 +7,9 @@ use function array_map; use function implode; +/** + * @psalm-immutable + */ final class TTypeAlias extends Atomic { /** @@ -20,10 +23,28 @@ final class TTypeAlias extends Atomic /** @var string */ public $alias_name; - public function __construct(string $declaring_fq_classlike_name, string $alias_name) + /** + * @param array|null $extra_types + */ + public function __construct(string $declaring_fq_classlike_name, string $alias_name, ?array $extra_types = null) { $this->declaring_fq_classlike_name = $declaring_fq_classlike_name; $this->alias_name = $alias_name; + $this->extra_types = $extra_types; + } + /** + * @param array|null $extra_types + */ + public function setIntersectionTypes(?array $extra_types): self + { + if ($extra_types === $this->extra_types) { + return $this; + } + return new self( + $this->declaring_fq_classlike_name, + $this->alias_name, + $extra_types + ); } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 7cabc8ffad2..486021b8257 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -15,15 +15,17 @@ /** * Represents a value of an array or enum. + * @psalm-immutable */ final class TValueOf extends Atomic { /** @var Union */ public $type; - public function __construct(Union $type) + public function __construct(Union $type, bool $from_docblock = false) { $this->type = $type; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TVoid.php b/src/Psalm/Type/Atomic/TVoid.php index f885fe03370..dfd0657c8d7 100644 --- a/src/Psalm/Type/Atomic/TVoid.php +++ b/src/Psalm/Type/Atomic/TVoid.php @@ -6,6 +6,7 @@ /** * Denotes the `void` type, normally just used to annotate a function/method that returns nothing + * @psalm-immutable */ final class TVoid extends Atomic { diff --git a/src/Psalm/Type/NodeVisitor.php b/src/Psalm/Type/ImmutableTypeVisitor.php similarity index 50% rename from src/Psalm/Type/NodeVisitor.php rename to src/Psalm/Type/ImmutableTypeVisitor.php index d33325a5db0..7578277f2c7 100644 --- a/src/Psalm/Type/NodeVisitor.php +++ b/src/Psalm/Type/ImmutableTypeVisitor.php @@ -2,7 +2,9 @@ namespace Psalm\Type; -abstract class NodeVisitor +use function is_array; + +abstract class ImmutableTypeVisitor { public const STOP_TRAVERSAL = 1; public const DONT_TRAVERSE_CHILDREN = 2; @@ -27,8 +29,22 @@ public function traverse(TypeNode $node): bool return false; } - foreach ($node->getChildNodes() as $child_node) { - if ($this->traverse($child_node) === false) { + foreach ($node->getChildNodeKeys() as $key) { + if ($node instanceof Union || $node instanceof MutableUnion) { + $child_node = $node->getAtomicTypes(); + } else { + /** @var TypeNode|non-empty-array|null */ + $child_node = $node->{$key}; + } + if ($child_node === null) { + continue; + } + if (is_array($child_node)) { + $visitor_result = $this->traverseArray($child_node); + } else { + $visitor_result = $this->traverse($child_node); + } + if ($visitor_result === false) { return false; } } @@ -37,14 +53,15 @@ public function traverse(TypeNode $node): bool } /** - * @param array $nodes + * @param non-empty-array $nodes */ - public function traverseArray(array $nodes): void + public function traverseArray(array $nodes): bool { foreach ($nodes as $node) { if ($this->traverse($node) === false) { - return; + return false; } } + return true; } } diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php new file mode 100644 index 00000000000..a187a3be98e --- /dev/null +++ b/src/Psalm/Type/MutableUnion.php @@ -0,0 +1,500 @@ + + */ + private $types; + + /** + * Whether the type originated in a docblock + * + * @var bool + */ + public $from_docblock = false; + + /** + * Whether the type originated from integer calculation + * + * @var bool + */ + public $from_calculation = false; + + /** + * Whether the type originated from a property + * + * This helps turn isset($foo->bar) into a different sort of issue + * + * @var bool + */ + public $from_property = false; + + /** + * Whether the type originated from *static* property + * + * Unlike non-static properties, static properties have no prescribed place + * like __construct() to be initialized in + * + * @var bool + */ + public $from_static_property = false; + + /** + * Whether the property that this type has been derived from has been initialized in a constructor + * + * @var bool + */ + public $initialized = true; + + /** + * Which class the type was initialised in + * + * @var ?string + */ + public $initialized_class; + + /** + * Whether or not the type has been checked yet + * + * @var bool + */ + public $checked = false; + + /** + * @var bool + */ + public $failed_reconciliation = false; + + /** + * Whether or not to ignore issues with possibly-null values + * + * @var bool + */ + public $ignore_nullable_issues = false; + + /** + * Whether or not to ignore issues with possibly-false values + * + * @var bool + */ + public $ignore_falsable_issues = false; + + /** + * Whether or not to ignore issues with isset on this type + * + * @var bool + */ + public $ignore_isset = false; + + /** + * Whether or not this variable is possibly undefined + * + * @var bool + */ + public $possibly_undefined = false; + + /** + * Whether or not this variable is possibly undefined + * + * @var bool + */ + public $possibly_undefined_from_try = false; + + /** + * Whether or not this union had a template, since replaced + * + * @var bool + */ + public $had_template = false; + + /** + * Whether or not this union comes from a template "as" default + * + * @var bool + */ + public $from_template_default = false; + + /** + * @var array + */ + private $literal_string_types = []; + + /** + * @var array + */ + private $typed_class_strings = []; + + /** + * @var array + */ + private $literal_int_types = []; + + /** + * @var array + */ + private $literal_float_types = []; + + /** + * True if the type was passed or returned by reference, or if the type refers to an object's + * property or an item in an array. Note that this is not true for locally created references + * that don't refer to properties or array items (see Context::$references_in_scope). + * + * @var bool + */ + public $by_ref = false; + + /** + * @var bool + */ + public $reference_free = false; + + /** + * @var bool + */ + public $allow_mutations = true; + + /** + * @var bool + */ + public $has_mutations = true; + + /** + * This is a cache of getId on non-exact mode + * @var null|string + */ + private $id; + + /** + * This is a cache of getId on exact mode + * @var null|string + */ + private $exact_id; + + + /** + * @var array + */ + public $parent_nodes = []; + + /** + * @var bool + */ + public $different = false; + + /** + * @psalm-external-mutation-free + * @param non-empty-array $types + */ + public function setTypes(array $types): self + { + $this->literal_float_types = []; + $this->literal_int_types = []; + $this->literal_string_types = []; + $this->typed_class_strings = []; + + $from_docblock = false; + $keyed_types = []; + + foreach ($types as $type) { + $key = $type->getKey(); + $keyed_types[$key] = $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + + $from_docblock = $from_docblock || $type->from_docblock; + } + + $this->types = $keyed_types; + $this->from_docblock = $from_docblock; + + return $this; + } + + /** + * @psalm-external-mutation-free + */ + public function addType(Atomic $type): self + { + $this->types[$type->getKey()] = $type; + + if ($type instanceof TLiteralString) { + $this->literal_string_types[$type->getKey()] = $type; + } elseif ($type instanceof TLiteralInt) { + $this->literal_int_types[$type->getKey()] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$type->getKey()] = $type; + } elseif ($type instanceof TString && $this->literal_string_types) { + foreach ($this->literal_string_types as $key => $_) { + unset($this->literal_string_types[$key], $this->types[$key]); + } + if (!$type instanceof TClassString + || (!$type->as_type && !$type instanceof TTemplateParamClass) + ) { + foreach ($this->typed_class_strings as $key => $_) { + unset($this->typed_class_strings[$key], $this->types[$key]); + } + } + } elseif ($type instanceof TInt && $this->literal_int_types) { + //we remove any literal that is already included in a wider type + $int_type_in_range = TIntRange::convertToIntRange($type); + foreach ($this->literal_int_types as $key => $literal_int_type) { + if ($int_type_in_range->contains($literal_int_type->value)) { + unset($this->literal_int_types[$key], $this->types[$key]); + } + } + } elseif ($type instanceof TFloat && $this->literal_float_types) { + foreach ($this->literal_float_types as $key => $_) { + unset($this->literal_float_types[$key], $this->types[$key]); + } + } + + $this->bustCache(); + + return $this; + } + + /** + * @psalm-external-mutation-free + */ + public function removeType(string $type_string): bool + { + if (isset($this->types[$type_string])) { + unset($this->types[$type_string]); + + if (strpos($type_string, '(')) { + unset( + $this->literal_string_types[$type_string], + $this->literal_int_types[$type_string], + $this->literal_float_types[$type_string] + ); + } + + $this->bustCache(); + + return true; + } + + if ($type_string === 'string') { + if ($this->literal_string_types) { + foreach ($this->literal_string_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_string_types = []; + } + + if ($this->typed_class_strings) { + foreach ($this->typed_class_strings as $typed_class_key => $_) { + unset($this->types[$typed_class_key]); + } + $this->typed_class_strings = []; + } + + unset($this->types['class-string'], $this->types['trait-string']); + } elseif ($type_string === 'int' && $this->literal_int_types) { + foreach ($this->literal_int_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_int_types = []; + } elseif ($type_string === 'float' && $this->literal_float_types) { + foreach ($this->literal_float_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_float_types = []; + } + + return false; + } + + public function setFromDocblock(bool $fromDocblock = true): self + { + $this->from_docblock = $fromDocblock; + + (new FromDocblockSetter($fromDocblock))->traverseArray($this->types); + + return $this; + } + + /** + * @psalm-external-mutation-free + */ + public function bustCache(): void + { + $this->id = null; + $this->exact_id = null; + } + + /** + * @psalm-external-mutation-free + * @param Union|MutableUnion $old_type + * @param Union|MutableUnion|null $new_type + */ + public function substitute($old_type, $new_type = null): self + { + if ($this->hasMixed() && !$this->isEmptyMixed()) { + return $this; + } + $old_type = $old_type->getBuilder(); + if ($new_type) { + $new_type = $new_type->getBuilder(); + } + + if ($new_type && $new_type->ignore_nullable_issues) { + $this->ignore_nullable_issues = true; + } + + if ($new_type && $new_type->ignore_falsable_issues) { + $this->ignore_falsable_issues = true; + } + + foreach ($old_type->types as $old_type_part) { + $had = isset($this->types[$old_type_part->getKey()]); + $this->removeType($old_type_part->getKey()); + if (!$had) { + if ($old_type_part instanceof TFalse + && isset($this->types['bool']) + && !isset($this->types['true']) + ) { + $this->removeType('bool'); + $this->types['true'] = new TTrue; + } elseif ($old_type_part instanceof TTrue + && isset($this->types['bool']) + && !isset($this->types['false']) + ) { + $this->removeType('bool'); + $this->types['false'] = new TFalse; + } elseif (isset($this->types['iterable'])) { + if ($old_type_part instanceof TNamedObject + && $old_type_part->value === 'Traversable' + && !isset($this->types['array']) + ) { + $this->removeType('iterable'); + $this->types['array'] = new TArray([Type::getArrayKey(), Type::getMixed()]); + } + + if ($old_type_part instanceof TArray + && !isset($this->types['traversable']) + ) { + $this->removeType('iterable'); + $this->types['traversable'] = new TNamedObject('Traversable'); + } + } elseif (isset($this->types['array-key'])) { + if ($old_type_part instanceof TString + && !isset($this->types['int']) + ) { + $this->removeType('array-key'); + $this->types['int'] = new TInt(); + } + + if ($old_type_part instanceof TInt + && !isset($this->types['string']) + ) { + $this->removeType('array-key'); + $this->types['string'] = new TString(); + } + } + } + } + + if ($new_type) { + foreach ($new_type->types as $key => $new_type_part) { + if (!isset($this->types[$key]) + || ($new_type_part instanceof Scalar + && get_class($new_type_part) === get_class($this->types[$key])) + ) { + $this->types[$key] = $new_type_part; + } else { + $this->types[$key] = TypeCombiner::combine([$new_type_part, $this->types[$key]])->getSingleAtomic(); + } + } + } elseif (count($this->types) === 0) { + $this->types['mixed'] = new TMixed(); + } + + $this->bustCache(); + + return $this; + } + + /** + * @psalm-mutation-free + */ + public function getBuilder(): self + { + return $this; + } + + /** + * @psalm-mutation-free + */ + public function freeze(): Union + { + $union = new Union($this->getAtomicTypes()); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + continue; + } + if ($key === 'id') { + continue; + } + if ($key === 'exact_id') { + continue; + } + if ($key === 'literal_string_types') { + continue; + } + if ($key === 'typed_class_strings') { + continue; + } + if ($key === 'literal_int_types') { + continue; + } + if ($key === 'literal_float_types') { + continue; + } + /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ + $union->{$key} = $value; + } + return $union; + } +} diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 19b706bcb62..c1585ac75ca 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -50,7 +50,6 @@ use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; -use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use ReflectionProperty; @@ -283,7 +282,7 @@ public static function reconcileKeyedTypes( ); if ($result_type_candidate->isUnionEmpty()) { - $result_type_candidate->addType(new TNever); + $result_type_candidate = $result_type_candidate->getBuilder()->addType(new TNever)->freeze(); } $orred_type = Type::combineUnionTypes( @@ -715,7 +714,9 @@ private static function getValueForKey( if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { + $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); + $new_base_type_candidate = $new_base_type_candidate->freeze(); } $new_base_type_candidate->possibly_undefined = true; @@ -729,7 +730,10 @@ private static function getValueForKey( if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { - $new_base_type_candidate->addType(new TNull); + $new_base_type_candidate = $new_base_type_candidate + ->getBuilder() + ->addType(new TNull) + ->freeze(); } $new_base_type_candidate->possibly_undefined = true; @@ -963,11 +967,11 @@ private static function getPropertyType( } /** + * @param Union|MutableUnion $existing_var_type * @param string[] $suppressed_issues - * */ protected static function triggerIssueForImpossible( - Union $existing_var_type, + $existing_var_type, string $old_var_type_string, string $key, Assertion $assertion, @@ -1132,13 +1136,11 @@ private static function adjustTKeyedArrayType( [ $array_key_offset => clone $result_type, ], - null + null, + false, + $previous_key_type->isNever() ? null : $previous_key_type, + $previous_value_type ); - - if (!$previous_key_type->isNever()) { - $base_atomic_type->previous_key_type = $previous_key_type; - } - $base_atomic_type->previous_value_type = $previous_value_type; } elseif ($base_atomic_type instanceof TList) { $previous_key_type = Type::getInt(); $previous_value_type = clone $base_atomic_type->type_param; @@ -1147,21 +1149,21 @@ private static function adjustTKeyedArrayType( [ $array_key_offset => clone $result_type, ], - null + null, + false, + $previous_key_type, + $previous_value_type, + true ); - - $base_atomic_type->is_list = true; - - $base_atomic_type->previous_key_type = $previous_key_type; - $base_atomic_type->previous_value_type = $previous_value_type; } elseif ($base_atomic_type instanceof TClassStringMap) { // do nothing } else { - $base_atomic_type = clone $base_atomic_type; - $base_atomic_type->properties[$array_key_offset] = clone $result_type; + $properties = $base_atomic_type->properties; + $properties[$array_key_offset] = clone $result_type; + $base_atomic_type = $base_atomic_type->setProperties($properties); } - $new_base_type->addType($base_atomic_type); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; @@ -1181,24 +1183,34 @@ private static function adjustTKeyedArrayType( } } - protected static function refineArrayKey(Union $key_type): void + protected static function refineArrayKey(Union $key_type): Union { - foreach ($key_type->getAtomicTypes() as $key => $cat) { + return self::refineArrayKeyInner($key_type) ?? $key_type; + } + private static function refineArrayKeyInner(Union $key_type): ?Union + { + $refined = false; + $types = []; + foreach ($key_type->getAtomicTypes() as $cat) { if ($cat instanceof TTemplateParam) { - self::refineArrayKey($cat->as); - $key_type->bustCache(); - } elseif ($cat instanceof TScalar || $cat instanceof TMixed) { - $key_type->removeType($key); - $key_type->addType(new TArrayKey()); - } elseif (!$cat instanceof TString && !$cat instanceof TInt) { - $key_type->removeType($key); - $key_type->addType(new TArrayKey()); + $as = self::refineArrayKeyInner($cat->as); + if ($as) { + $refined = true; + $types []= $cat->replaceAs($as); + } else { + $types []= $cat; + } + } elseif ($cat instanceof TArrayKey || $cat instanceof TString || $cat instanceof TInt) { + $types []= $cat; + } else { + $refined = true; + $types []= new TArrayKey; } } - if ($key_type->isUnionEmpty()) { - // this should ideally prompt some sort of error - $key_type->addType(new TArrayKey()); + if ($refined) { + return $key_type->getBuilder()->setTypes($types)->freeze(); } + return null; } } diff --git a/src/Psalm/Type/TypeNode.php b/src/Psalm/Type/TypeNode.php index 6bdc054c28c..f8d8cf4a506 100644 --- a/src/Psalm/Type/TypeNode.php +++ b/src/Psalm/Type/TypeNode.php @@ -5,7 +5,7 @@ interface TypeNode { /** - * @return array + * @return list */ - public function getChildNodes(): array; + public function getChildNodeKeys(): array; } diff --git a/src/Psalm/Type/TypeVisitor.php b/src/Psalm/Type/TypeVisitor.php new file mode 100644 index 00000000000..c4e98bcf517 --- /dev/null +++ b/src/Psalm/Type/TypeVisitor.php @@ -0,0 +1,104 @@ +enterNode($node); + + if ($visitor_result === self::DONT_TRAVERSE_CHILDREN) { + return true; + } + + if ($visitor_result === self::STOP_TRAVERSAL) { + return false; + } + + $cloned = $node !== $old; + foreach ($node->getChildNodeKeys() as $key) { + if ($node instanceof Union || $node instanceof MutableUnion) { + $child_node = $node->getAtomicTypes(); + } else { + /** @var TypeNode|non-empty-array|null */ + $child_node = $node->{$key}; + } + if ($child_node === null) { + continue; + } + $orig = $child_node; + if (is_array($child_node)) { + $visitor_result = $this->traverseArray($child_node); + } else { + $visitor_result = $this->traverse($child_node); + } + if ($child_node !== $orig) { + if ($node instanceof Union) { + /** @var non-empty-array $child_node */ + $node = $node->getBuilder()->setTypes($child_node)->freeze(); + } elseif ($node instanceof MutableUnion) { + // This mutates in-place + /** @var non-empty-array $child_node */ + $node->setTypes($child_node); + } else { + if (!$cloned) { + $cloned = true; + $node = clone $node; + } + if ($key === 'extra_types' && is_array($child_node)) { + $new = []; + /** @var Union */ + foreach ($child_node as $value) { + $new[$value->getKey()] = $value; + } + $child_node = $new; + } + $node->{$key} = $child_node; + } + } + if ($visitor_result === false) { + return false; + } + } + + return true; + } + + /** + * @template T as array + * @param T $nodes + * @param-out T $nodes + */ + public function traverseArray(array &$nodes): bool + { + foreach ($nodes as &$node) { + if ($this->traverse($node) === false) { + return false; + } + } + return true; + } +} diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 9d004d386ce..cdcfc5d9ed0 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -2,60 +2,22 @@ namespace Psalm\Type; -use InvalidArgumentException; -use Psalm\CodeLocation; -use Psalm\Codebase; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\Type\TypeCombiner; -use Psalm\Internal\TypeVisitor\ContainsClassLikeVisitor; -use Psalm\Internal\TypeVisitor\ContainsLiteralVisitor; use Psalm\Internal\TypeVisitor\FromDocblockSetter; -use Psalm\Internal\TypeVisitor\TemplateTypeCollector; -use Psalm\Internal\TypeVisitor\TypeChecker; -use Psalm\Internal\TypeVisitor\TypeScanner; -use Psalm\StatementsSource; -use Psalm\Storage\FileStorage; -use Psalm\Type; -use Psalm\Type\Atomic\Scalar; -use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClassString; -use Psalm\Type\Atomic\TClassStringMap; -use Psalm\Type\Atomic\TClosure; -use Psalm\Type\Atomic\TConditional; -use Psalm\Type\Atomic\TEmptyMixed; -use Psalm\Type\Atomic\TFalse; -use Psalm\Type\Atomic\TFloat; -use Psalm\Type\Atomic\TInt; -use Psalm\Type\Atomic\TIntRange; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; -use Psalm\Type\Atomic\TLowercaseString; -use Psalm\Type\Atomic\TMixed; -use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\Atomic\TNonEmptyLowercaseString; -use Psalm\Type\Atomic\TNonspecificLiteralInt; -use Psalm\Type\Atomic\TNonspecificLiteralString; -use Psalm\Type\Atomic\TString; -use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\Atomic\TTrue; +use Stringable; -use function array_filter; -use function array_unique; -use function count; -use function get_class; -use function implode; -use function ksort; -use function reset; -use function sort; -use function strpos; +use function get_object_vars; -final class Union implements TypeNode +final class Union implements TypeNode, Stringable { + use UnionTrait; + /** + * @psalm-readonly * @var non-empty-array */ private $types; @@ -236,1423 +198,59 @@ final class Union implements TypeNode public $different = false; /** - * Constructs an Union instance - * - * @param non-empty-array $types + * @psalm-mutation-free + * @param non-empty-array $types */ - public function __construct(array $types) + public function setTypes(array $types): self { - $from_docblock = false; - - $keyed_types = []; - - foreach ($types as $type) { - $key = $type->getKey(); - $keyed_types[$key] = $type; - - if ($type instanceof TLiteralInt) { - $this->literal_int_types[$key] = $type; - } elseif ($type instanceof TLiteralString) { - $this->literal_string_types[$key] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$key] = $type; - } elseif ($type instanceof TClassString - && ($type->as_type || $type instanceof TTemplateParamClass) - ) { - $this->typed_class_strings[$key] = $type; - } - - $from_docblock = $from_docblock || $type->from_docblock; + if ($types === $this->types) { + return $this; } - - $this->types = $keyed_types; - - $this->from_docblock = $from_docblock; - } - - /** - * @param non-empty-array $types - */ - public function replaceTypes(array $types): void - { - $this->types = $types; + return $this->getBuilder()->setTypes($types)->freeze(); } /** * @psalm-mutation-free - * @return non-empty-array */ - public function getAtomicTypes(): array - { - return $this->types; - } - - public function addType(Atomic $type): void - { - $this->types[$type->getKey()] = $type; - - if ($type instanceof TLiteralString) { - $this->literal_string_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralInt) { - $this->literal_int_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$type->getKey()] = $type; - } elseif ($type instanceof TString && $this->literal_string_types) { - foreach ($this->literal_string_types as $key => $_) { - unset($this->literal_string_types[$key], $this->types[$key]); - } - if (!$type instanceof TClassString - || (!$type->as_type && !$type instanceof TTemplateParamClass) - ) { - foreach ($this->typed_class_strings as $key => $_) { - unset($this->typed_class_strings[$key], $this->types[$key]); - } - } - } elseif ($type instanceof TInt && $this->literal_int_types) { - //we remove any literal that is already included in a wider type - $int_type_in_range = TIntRange::convertToIntRange($type); - foreach ($this->literal_int_types as $key => $literal_int_type) { - if ($int_type_in_range->contains($literal_int_type->value)) { - unset($this->literal_int_types[$key], $this->types[$key]); - } - } - } elseif ($type instanceof TFloat && $this->literal_float_types) { - foreach ($this->literal_float_types as $key => $_) { - unset($this->literal_float_types[$key], $this->types[$key]); - } - } - - $this->bustCache(); - } - - public function __clone() - { - $this->literal_string_types = []; - $this->literal_int_types = []; - $this->literal_float_types = []; - $this->typed_class_strings = []; - - foreach ($this->types as $key => &$type) { - $type = clone $type; - - if ($type instanceof TLiteralInt) { - $this->literal_int_types[$key] = $type; - } elseif ($type instanceof TLiteralString) { - $this->literal_string_types[$key] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$key] = $type; - } elseif ($type instanceof TClassString - && ($type->as_type || $type instanceof TTemplateParamClass) - ) { - $this->typed_class_strings[$key] = $type; - } - } - } - - public function __toString(): string - { - $types = []; - - $printed_int = false; - $printed_float = false; - $printed_string = false; - - foreach ($this->types as $type) { - if ($type instanceof TLiteralFloat) { - if ($printed_float) { - continue; - } - - $printed_float = true; - } elseif ($type instanceof TLiteralString) { - if ($printed_string) { - continue; - } - - $printed_string = true; - } elseif ($type instanceof TLiteralInt) { - if ($printed_int) { - continue; - } - - $printed_int = true; - } - - $types[] = $type->getId(false); - } - - sort($types); - return implode('|', $types); - } - - public function getKey(): string - { - $types = []; - - $printed_int = false; - $printed_float = false; - $printed_string = false; - - foreach ($this->types as $type) { - if ($type instanceof TLiteralFloat) { - if ($printed_float) { - continue; - } - - $types[] = 'float'; - $printed_float = true; - } elseif ($type instanceof TLiteralString) { - if ($printed_string) { - continue; - } - - $types[] = 'string'; - $printed_string = true; - } elseif ($type instanceof TLiteralInt) { - if ($printed_int) { - continue; - } - - $types[] = 'int'; - $printed_int = true; - } else { - $types[] = $type->getKey(); - } - } - - sort($types); - return implode('|', $types); - } - - public function getId(bool $exact = true): string + public function getBuilder(): MutableUnion { - if ($exact && $this->exact_id) { - return $this->exact_id; - } elseif (!$exact && $this->id) { - return $this->id; - } - - $types = []; - foreach ($this->types as $type) { - $types[] = $type->getId($exact); - } - $types = array_unique($types); - sort($types); - - if (count($types) > 1) { - foreach ($types as $i => $type) { - if (strpos($type, ' as ') && strpos($type, '(') === false) { - $types[$i] = '(' . $type . ')'; - } - } - } - - $id = implode('|', $types); - - if ($exact) { - $this->exact_id = $id; - } else { - $this->id = $id; - } - - return $id; - } - - /** - * @param array $aliased_classes - * - */ - public function toNamespacedString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - bool $use_phpdoc_format - ): string { - $other_types = []; - - $literal_ints = []; - $literal_strings = []; - - $has_non_literal_int = false; - $has_non_literal_string = false; - - foreach ($this->types as $type) { - $type_string = $type->toNamespacedString($namespace, $aliased_classes, $this_class, $use_phpdoc_format); - if ($type instanceof TLiteralInt) { - $literal_ints[] = $type_string; - } elseif ($type instanceof TLiteralString) { - $literal_strings[] = $type_string; - } else { - if (get_class($type) === TString::class) { - $has_non_literal_string = true; - } elseif (get_class($type) === TInt::class) { - $has_non_literal_int = true; - } - $other_types[] = $type_string; - } - } - - if (count($literal_ints) <= 3 && !$has_non_literal_int) { - $other_types = [...$other_types, ...$literal_ints]; - } else { - $other_types[] = 'int'; - } - - if (count($literal_strings) <= 3 && !$has_non_literal_string) { - $other_types = [...$other_types, ...$literal_strings]; - } else { - $other_types[] = 'string'; - } - - sort($other_types); - return implode('|', array_unique($other_types)); - } - - /** - * @param array $aliased_classes - */ - public function toPhpString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - int $analysis_php_version_id - ): ?string { - if (!$this->isSingleAndMaybeNullable()) { - if ($analysis_php_version_id < 8_00_00) { - return null; - } - } elseif ($analysis_php_version_id < 7_00_00 - || (isset($this->types['null']) && $analysis_php_version_id < 7_01_00) - ) { - return null; - } - - $types = $this->types; - - $nullable = false; - - if (isset($types['null']) && count($types) > 1) { - unset($types['null']); - - $nullable = true; - } - - $falsable = false; - - if (isset($types['false']) && count($types) > 1) { - unset($types['false']); - - $falsable = true; - } - - $php_types = []; - - foreach ($types as $atomic_type) { - $php_type = $atomic_type->toPhpString( - $namespace, - $aliased_classes, - $this_class, - $analysis_php_version_id - ); - - if (!$php_type) { - return null; - } - - $php_types[] = $php_type; - } - - if ($falsable) { - if ($nullable) { - $php_types['null'] = 'null'; + $union = new MutableUnion($this->getAtomicTypes()); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + continue; } - $php_types['false'] = 'false'; - ksort($php_types); - return implode('|', array_unique($php_types)); - } - - if ($analysis_php_version_id < 8_00_00) { - return ($nullable ? '?' : '') . implode('|', array_unique($php_types)); - } - if ($nullable) { - $php_types['null'] = 'null'; - } - return implode('|', array_unique($php_types)); - } - - public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool - { - if (!$this->isSingleAndMaybeNullable() && $analysis_php_version_id < 8_00_00) { - return false; - } - - $types = $this->types; - - if (isset($types['null'])) { - if (count($types) > 1) { - unset($types['null']); - } else { - return false; + if ($key === 'id') { + continue; } - } - - return !array_filter( - $types, - static fn($atomic_type): bool => !$atomic_type->canBeFullyExpressedInPhp($analysis_php_version_id) - ); - } - - public function removeType(string $type_string): bool - { - if (isset($this->types[$type_string])) { - unset($this->types[$type_string]); - - if (strpos($type_string, '(')) { - unset( - $this->literal_string_types[$type_string], - $this->literal_int_types[$type_string], - $this->literal_float_types[$type_string] - ); + if ($key === 'exact_id') { + continue; } - - $this->bustCache(); - - return true; - } - - if ($type_string === 'string') { - if ($this->literal_string_types) { - foreach ($this->literal_string_types as $literal_key => $_) { - unset($this->types[$literal_key]); - } - $this->literal_string_types = []; + if ($key === 'literal_string_types') { + continue; } - - if ($this->typed_class_strings) { - foreach ($this->typed_class_strings as $typed_class_key => $_) { - unset($this->types[$typed_class_key]); - } - $this->typed_class_strings = []; + if ($key === 'typed_class_strings') { + continue; } - - unset($this->types['class-string'], $this->types['trait-string']); - } elseif ($type_string === 'int' && $this->literal_int_types) { - foreach ($this->literal_int_types as $literal_key => $_) { - unset($this->types[$literal_key]); + if ($key === 'literal_int_types') { + continue; } - $this->literal_int_types = []; - } elseif ($type_string === 'float' && $this->literal_float_types) { - foreach ($this->literal_float_types as $literal_key => $_) { - unset($this->types[$literal_key]); + if ($key === 'literal_float_types') { + continue; } - $this->literal_float_types = []; + /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ + $union->{$key} = $value; } - - return false; - } - - public function bustCache(): void - { - $this->id = null; - $this->exact_id = null; - } - - public function hasType(string $type_string): bool - { - return isset($this->types[$type_string]); - } - - public function hasArray(): bool - { - return isset($this->types['array']); - } - - public function hasIterable(): bool - { - return isset($this->types['iterable']); - } - - public function hasList(): bool - { - return isset($this->types['array']) && $this->types['array'] instanceof TList; - } - - public function hasClassStringMap(): bool - { - return isset($this->types['array']) && $this->types['array'] instanceof TClassStringMap; - } - - public function isTemplatedClassString(): bool - { - return $this->isSingle() - && count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TTemplateParamClass - ) - ) === 1; - } - - public function hasArrayAccessInterface(Codebase $codebase): bool - { - return (bool)array_filter( - $this->types, - static fn($type): bool => $type->hasArrayAccessInterface($codebase) - ); - } - - public function hasCallableType(): bool - { - return $this->getCallableTypes() || $this->getClosureTypes(); - } - - /** - * @return array - */ - public function getCallableTypes(): array - { - return array_filter( - $this->types, - static fn($type): bool => $type instanceof TCallable - ); + return $union; } /** - * @return array + * @psalm-mutation-free */ - public function getClosureTypes(): array - { - return array_filter( - $this->types, - static fn($type): bool => $type instanceof TClosure - ); - } - - public function hasObject(): bool - { - return isset($this->types['object']); - } - - public function hasObjectType(): bool - { - foreach ($this->types as $type) { - if ($type->isObjectType()) { - return true; - } - } - - return false; - } - - public function isObjectType(): bool - { - foreach ($this->types as $type) { - if (!$type->isObjectType()) { - return false; - } - } - - return true; - } - - public function hasNamedObjectType(): bool - { - foreach ($this->types as $type) { - if ($type->isNamedObjectType()) { - return true; - } - } - - return false; - } - - public function isStaticObject(): bool - { - foreach ($this->types as $type) { - if (!$type instanceof TNamedObject - || !$type->is_static - ) { - return false; - } - } - - return true; - } - - public function hasStaticObject(): bool - { - foreach ($this->types as $type) { - if ($type instanceof TNamedObject - && $type->is_static - ) { - return true; - } - } - - return false; - } - - public function isNullable(): bool - { - if (isset($this->types['null'])) { - return true; - } - - foreach ($this->types as $type) { - if ($type instanceof TTemplateParam && $type->as->isNullable()) { - return true; - } - } - - return false; - } - - public function isFalsable(): bool - { - if (isset($this->types['false'])) { - return true; - } - - foreach ($this->types as $type) { - if ($type instanceof TTemplateParam && $type->as->isFalsable()) { - return true; - } - } - - return false; - } - - public function hasBool(): bool - { - return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); - } - - public function hasString(): bool - { - return isset($this->types['string']) - || isset($this->types['class-string']) - || isset($this->types['trait-string']) - || isset($this->types['numeric-string']) - || isset($this->types['callable-string']) - || isset($this->types['array-key']) - || $this->literal_string_types - || $this->typed_class_strings; - } - - public function hasLowercaseString(): bool - { - return isset($this->types['string']) - && ($this->types['string'] instanceof TLowercaseString - || $this->types['string'] instanceof TNonEmptyLowercaseString); - } - - public function hasLiteralClassString(): bool - { - return count($this->typed_class_strings) > 0; - } - - public function hasInt(): bool - { - return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types - || array_filter($this->types, static fn(Atomic $type): bool => $type instanceof TIntRange); - } - - public function hasArrayKey(): bool - { - return isset($this->types['array-key']); - } - - public function hasFloat(): bool - { - return isset($this->types['float']) || $this->literal_float_types; - } - - public function hasScalar(): bool - { - return isset($this->types['scalar']); - } - - public function hasNumeric(): bool - { - return isset($this->types['numeric']); - } - - public function hasScalarType(): bool - { - return isset($this->types['int']) - || isset($this->types['float']) - || isset($this->types['string']) - || isset($this->types['class-string']) - || isset($this->types['trait-string']) - || isset($this->types['bool']) - || isset($this->types['false']) - || isset($this->types['true']) - || isset($this->types['numeric']) - || isset($this->types['numeric-string']) - || $this->literal_int_types - || $this->literal_float_types - || $this->literal_string_types - || $this->typed_class_strings; - } - - public function hasTemplate(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TTemplateParam - || ($type instanceof TNamedObject - && $type->extra_types - && array_filter( - $type->extra_types, - static fn($t): bool => $t instanceof TTemplateParam - ) - ) - ); - } - - public function hasConditional(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TConditional - ); - } - - public function hasTemplateOrStatic(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TTemplateParam - || ($type instanceof TNamedObject - && ($type->is_static - || ($type->extra_types - && array_filter( - $type->extra_types, - static fn($t): bool => $t instanceof TTemplateParam - ) - ) - ) - ) - ); - } - - public function hasMixed(): bool - { - return isset($this->types['mixed']); - } - - public function isMixed(): bool - { - return isset($this->types['mixed']) && count($this->types) === 1; - } - - public function isEmptyMixed(): bool - { - return isset($this->types['mixed']) - && $this->types['mixed'] instanceof TEmptyMixed - && count($this->types) === 1; - } - - public function isVanillaMixed(): bool - { - return isset($this->types['mixed']) - && get_class($this->types['mixed']) === TMixed::class - && !$this->types['mixed']->from_loop_isset - && count($this->types) === 1; - } - - public function isArrayKey(): bool - { - return isset($this->types['array-key']) && count($this->types) === 1; - } - - public function isNull(): bool - { - return count($this->types) === 1 && isset($this->types['null']); - } - - public function isFalse(): bool - { - return count($this->types) === 1 && isset($this->types['false']); - } - - public function isAlwaysFalsy(): bool - { - foreach ($this->getAtomicTypes() as $atomic_type) { - if (!$atomic_type->isFalsy()) { - return false; - } - } - - return true; - } - - public function isTrue(): bool - { - return count($this->types) === 1 && isset($this->types['true']); - } - - public function isAlwaysTruthy(): bool - { - if ($this->possibly_undefined || $this->possibly_undefined_from_try) { - return false; - } - - foreach ($this->getAtomicTypes() as $atomic_type) { - if (!$atomic_type->isTruthy()) { - return false; - } - } - - return true; - } - - public function isVoid(): bool - { - return isset($this->types['void']) && count($this->types) === 1; - } - - public function isNever(): bool - { - return isset($this->types['never']) && count($this->types) === 1; - } - - public function isGenerator(): bool - { - return count($this->types) === 1 - && (($single_type = reset($this->types)) instanceof TNamedObject) - && ($single_type->value === 'Generator'); - } - - public function substitute(Union $old_type, ?Union $new_type = null): void - { - if ($this->hasMixed() && !$this->isEmptyMixed()) { - return; - } - - if ($new_type && $new_type->ignore_nullable_issues) { - $this->ignore_nullable_issues = true; - } - - if ($new_type && $new_type->ignore_falsable_issues) { - $this->ignore_falsable_issues = true; - } - - foreach ($old_type->types as $old_type_part) { - if (!$this->removeType($old_type_part->getKey())) { - if ($old_type_part instanceof TFalse - && isset($this->types['bool']) - && !isset($this->types['true']) - ) { - $this->removeType('bool'); - $this->types['true'] = new TTrue; - } elseif ($old_type_part instanceof TTrue - && isset($this->types['bool']) - && !isset($this->types['false']) - ) { - $this->removeType('bool'); - $this->types['false'] = new TFalse; - } elseif (isset($this->types['iterable'])) { - if ($old_type_part instanceof TNamedObject - && $old_type_part->value === 'Traversable' - && !isset($this->types['array']) - ) { - $this->removeType('iterable'); - $this->types['array'] = new TArray([Type::getArrayKey(), Type::getMixed()]); - } - - if ($old_type_part instanceof TArray - && !isset($this->types['traversable']) - ) { - $this->removeType('iterable'); - $this->types['traversable'] = new TNamedObject('Traversable'); - } - } elseif (isset($this->types['array-key'])) { - if ($old_type_part instanceof TString - && !isset($this->types['int']) - ) { - $this->removeType('array-key'); - $this->types['int'] = new TInt(); - } - - if ($old_type_part instanceof TInt - && !isset($this->types['string']) - ) { - $this->removeType('array-key'); - $this->types['string'] = new TString(); - } - } - } - } - - if ($new_type) { - foreach ($new_type->types as $key => $new_type_part) { - if (!isset($this->types[$key]) - || ($new_type_part instanceof Scalar - && get_class($new_type_part) === get_class($this->types[$key])) - ) { - $this->types[$key] = $new_type_part; - } else { - $this->types[$key] = TypeCombiner::combine([$new_type_part, $this->types[$key]])->getSingleAtomic(); - } - } - } elseif (count($this->types) === 0) { - $this->types['mixed'] = new TMixed(); - } - - $this->bustCache(); - } - - public function isSingle(): bool - { - $type_count = count($this->types); - - $int_literal_count = count($this->literal_int_types); - $string_literal_count = count($this->literal_string_types); - $float_literal_count = count($this->literal_float_types); - - if (($int_literal_count && $string_literal_count) - || ($int_literal_count && $float_literal_count) - || ($string_literal_count && $float_literal_count) - ) { - return false; - } - - if ($int_literal_count || $string_literal_count || $float_literal_count) { - $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; - } - - return $type_count === 1; - } - - public function isSingleAndMaybeNullable(): bool - { - $is_nullable = isset($this->types['null']); - - $type_count = count($this->types); - - if ($type_count === 1 && $is_nullable) { - return false; - } - - $int_literal_count = count($this->literal_int_types); - $string_literal_count = count($this->literal_string_types); - $float_literal_count = count($this->literal_float_types); - - if (($int_literal_count && $string_literal_count) - || ($int_literal_count && $float_literal_count) - || ($string_literal_count && $float_literal_count) - ) { - return false; - } - - if ($int_literal_count || $string_literal_count || $float_literal_count) { - $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; - } - - return ($type_count - (int) $is_nullable) === 1; - } - - /** - * @return bool true if this is an int - */ - public function isInt(bool $check_templates = false): bool - { - return count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TInt - || ($check_templates - && $type instanceof TTemplateParam - && $type->as->isInt() - ) - ) - ) === count($this->types); - } - - /** - * @return bool true if this is a float - */ - public function isFloat(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['float']) || $this->literal_float_types; - } - - /** - * @return bool true if this is a string - */ - public function isString(bool $check_templates = false): bool - { - return count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TString - || ($check_templates - && $type instanceof TTemplateParam - && $type->as->isString() - ) - ) - ) === count($this->types); - } - - /** - * @return bool true if this is a boolean - */ - public function isBool(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['bool']); - } - - /** - * @return bool true if this is an array - */ - public function isArray(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['array']); - } - - /** - * @return bool true if this is a string literal with only one possible value - */ - public function isSingleStringLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_string_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleStringLiteral is false - * - * @return TLiteralString the only string literal represented by this union type - */ - public function getSingleStringLiteral(): TLiteralString - { - if (count($this->types) !== 1 || count($this->literal_string_types) !== 1) { - throw new InvalidArgumentException('Not a string literal'); - } - - return reset($this->literal_string_types); - } - - public function allStringLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString) { - return false; - } - } - - return true; - } - - public function allIntLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralInt) { - return false; - } - } - - return true; - } - - /** - * @psalm-suppress PossiblyUnusedMethod Public API - */ - public function allFloatLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralFloat) { - return false; - } - } - - return true; - } - - /** - * @psalm-assert-if-true array< - * array-key, - * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue - * > $this->getAtomicTypes() - */ - public function allSpecificLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString - && !$atomic_key_type instanceof TLiteralInt - && !$atomic_key_type instanceof TLiteralFloat - && !$atomic_key_type instanceof TFalse - && !$atomic_key_type instanceof TTrue - ) { - return false; - } - } - - return true; - } - - /** - * @psalm-assert-if-true array< - * array-key, - * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue - * > $this->getAtomicTypes() - */ - public function allLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString - && !$atomic_key_type instanceof TLiteralInt - && !$atomic_key_type instanceof TLiteralFloat - && !$atomic_key_type instanceof TNonspecificLiteralString - && !$atomic_key_type instanceof TNonspecificLiteralInt - && !$atomic_key_type instanceof TFalse - && !$atomic_key_type instanceof TTrue - ) { - return false; - } - } - - return true; - } - - public function hasLiteralValue(): bool - { - return $this->literal_int_types - || $this->literal_string_types - || $this->literal_float_types - || isset($this->types['false']) - || isset($this->types['true']); - } - - public function isSingleLiteral(): bool - { - return count($this->types) === 1 - && count($this->literal_int_types) - + count($this->literal_string_types) - + count($this->literal_float_types) === 1 - ; - } - - /** - * @return TLiteralInt|TLiteralString|TLiteralFloat - */ - public function getSingleLiteral() - { - if (!$this->isSingleLiteral()) { - throw new InvalidArgumentException("Not a single literal"); - } - - return ($literal = reset($this->literal_int_types)) !== false - ? $literal - : (($literal = reset($this->literal_string_types)) !== false - ? $literal - : reset($this->literal_float_types)) - ; - } - - public function hasLiteralString(): bool - { - return count($this->literal_string_types) > 0; - } - - public function hasLiteralInt(): bool - { - return count($this->literal_int_types) > 0; - } - - /** - * @return bool true if this is a int literal with only one possible value - */ - public function isSingleIntLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_int_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleIntLiteral is false - * - * @return TLiteralInt the only int literal represented by this union type - */ - public function getSingleIntLiteral(): TLiteralInt - { - if (count($this->types) !== 1 || count($this->literal_int_types) !== 1) { - throw new InvalidArgumentException('Not an int literal'); - } - - return reset($this->literal_int_types); - } - - /** - * @param array $suppressed_issues - * @param array $phantom_classes - * - */ - public function check( - StatementsSource $source, - CodeLocation $code_location, - array $suppressed_issues, - array $phantom_classes = [], - bool $inferred = true, - bool $inherited = false, - bool $prevent_template_covariance = false, - ?string $calling_method_id = null - ): bool { - if ($this->checked) { - return true; - } - - $checker = new TypeChecker( - $source, - $code_location, - $suppressed_issues, - $phantom_classes, - $inferred, - $inherited, - $prevent_template_covariance, - $calling_method_id - ); - - $checker->traverseArray($this->types); - - $this->checked = true; - - return !$checker->hasErrors(); - } - - /** - * @param array $phantom_classes - * - */ - public function queueClassLikesForScanning( - Codebase $codebase, - ?FileStorage $file_storage = null, - array $phantom_classes = [] - ): void { - $scanner_visitor = new TypeScanner( - $codebase->scanner, - $file_storage, - $phantom_classes - ); - - $scanner_visitor->traverseArray($this->types); - } - - /** - * @param lowercase-string $fq_class_like_name - */ - public function containsClassLike(string $fq_class_like_name): bool - { - $classlike_visitor = new ContainsClassLikeVisitor($fq_class_like_name); - - $classlike_visitor->traverseArray($this->types); - - return $classlike_visitor->matches(); - } - - public function containsAnyLiteral(): bool - { - $literal_visitor = new ContainsLiteralVisitor(); - - $literal_visitor->traverseArray($this->types); - - return $literal_visitor->matches(); - } - - /** - * @return list - */ - public function getTemplateTypes(): array - { - $template_type_collector = new TemplateTypeCollector(); - - $template_type_collector->traverseArray($this->types); - - return $template_type_collector->getTemplateTypes(); - } - - public function setFromDocblock(): void - { - $this->from_docblock = true; - - (new FromDocblockSetter())->traverseArray($this->types); - } - - public function replaceClassLike(string $old, string $new): void - { - foreach ($this->types as $key => $atomic_type) { - $atomic_type->replaceClassLike($old, $new); - - $this->removeType($key); - $this->addType($atomic_type); - } - } - - public function equals(Union $other_type, bool $ensure_source_equality = true): bool - { - if ($other_type === $this) { - return true; - } - - if ($other_type->id && $this->id && $other_type->id !== $this->id) { - return false; - } - - if ($other_type->exact_id && $this->exact_id && $other_type->exact_id !== $this->exact_id) { - return false; - } - - if ($this->possibly_undefined !== $other_type->possibly_undefined) { - return false; - } - - if ($this->had_template !== $other_type->had_template) { - return false; - } - - if ($this->possibly_undefined_from_try !== $other_type->possibly_undefined_from_try) { - return false; - } - - if ($this->from_calculation !== $other_type->from_calculation) { - return false; - } - - if ($this->initialized !== $other_type->initialized) { - return false; - } - - if ($ensure_source_equality && $this->from_docblock !== $other_type->from_docblock) { - return false; - } - - if (count($this->types) !== count($other_type->types)) { - return false; - } - - if ($this->parent_nodes !== $other_type->parent_nodes) { - return false; - } - - if ($this->different || $other_type->different) { - return false; - } - - $other_atomic_types = $other_type->types; - - foreach ($this->types as $key => $atomic_type) { - if (!isset($other_atomic_types[$key])) { - return false; - } - - if (!$atomic_type->equals($other_atomic_types[$key], $ensure_source_equality)) { - return false; - } - } - - return true; - } - - /** - * @return array - */ - public function getLiteralStrings(): array - { - return $this->literal_string_types; - } - - /** - * @return array - */ - public function getLiteralInts(): array - { - return $this->literal_int_types; - } - - /** - * @return array - */ - public function getRangeInts(): array - { - $ranges = []; - foreach ($this->getAtomicTypes() as $atomic) { - if ($atomic instanceof TIntRange) { - $ranges[$atomic->getKey()] = $atomic; - } - } - - return $ranges; - } - - /** - * @return array - */ - public function getLiteralFloats(): array - { - return $this->literal_float_types; - } - - /** - * @return array - */ - public function getChildNodes(): array - { - return $this->types; - } - - /** - * @return bool true if this is a float literal with only one possible value - */ - public function isSingleFloatLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_float_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleFloatLiteral is false - * - * @return TLiteralFloat the only float literal represented by this union type - */ - public function getSingleFloatLiteral(): TLiteralFloat - { - if (count($this->types) !== 1 || count($this->literal_float_types) !== 1) { - throw new InvalidArgumentException('Not a float literal'); - } - - return reset($this->literal_float_types); - } - - public function hasLiteralFloat(): bool - { - return count($this->literal_float_types) > 0; - } - - public function getSingleAtomic(): Atomic - { - return reset($this->types); - } - - public function isEmptyArray(): bool - { - return count($this->types) === 1 - && isset($this->types['array']) - && $this->types['array'] instanceof TArray - && $this->types['array']->isEmptyArray(); - } - - public function isUnionEmpty(): bool + public function setFromDocblock(bool $fromDocblock = true): self { - return $this->types === []; + $cloned = clone $this; + /** @psalm-suppress ImpureMethodCall Acting on clone */ + (new FromDocblockSetter($fromDocblock))->traverse($cloned); + return $cloned; } } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php new file mode 100644 index 00000000000..ce7ff78965f --- /dev/null +++ b/src/Psalm/Type/UnionTrait.php @@ -0,0 +1,1469 @@ + $types + */ + public function __construct(array $types, bool $from_docblock = false) + { + $keyed_types = []; + + foreach ($types as $type) { + $key = $type->getKey(); + $keyed_types[$key] = $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + + $from_docblock = $from_docblock || $type->from_docblock; + } + + $this->types = $keyed_types; + + $this->from_docblock = $from_docblock; + } + + /** + * @psalm-mutation-free + * @return non-empty-array + */ + public function getAtomicTypes(): array + { + return $this->types; + } + + /** + * @psalm-mutation-free + */ + public function __toString(): string + { + $types = []; + + $printed_int = false; + $printed_float = false; + $printed_string = false; + + foreach ($this->types as $type) { + if ($type instanceof TLiteralFloat) { + if ($printed_float) { + continue; + } + + $printed_float = true; + } elseif ($type instanceof TLiteralString) { + if ($printed_string) { + continue; + } + + $printed_string = true; + } elseif ($type instanceof TLiteralInt) { + if ($printed_int) { + continue; + } + + $printed_int = true; + } + + $types[] = $type->getId(false); + } + + sort($types); + return implode('|', $types); + } + + /** + * @psalm-mutation-free + */ + public function getKey(): string + { + $types = []; + + $printed_int = false; + $printed_float = false; + $printed_string = false; + + foreach ($this->types as $type) { + if ($type instanceof TLiteralFloat) { + if ($printed_float) { + continue; + } + + $types[] = 'float'; + $printed_float = true; + } elseif ($type instanceof TLiteralString) { + if ($printed_string) { + continue; + } + + $types[] = 'string'; + $printed_string = true; + } elseif ($type instanceof TLiteralInt) { + if ($printed_int) { + continue; + } + + $types[] = 'int'; + $printed_int = true; + } else { + $types[] = $type->getKey(); + } + } + + sort($types); + return implode('|', $types); + } + + /** + * @psalm-mutation-free + */ + public function getId(bool $exact = true): string + { + if ($exact && $this->exact_id) { + return $this->exact_id; + } elseif (!$exact && $this->id) { + return $this->id; + } + + $types = []; + foreach ($this->types as $type) { + $types[] = $type->getId($exact); + } + $types = array_unique($types); + sort($types); + + if (count($types) > 1) { + foreach ($types as $i => $type) { + if (strpos($type, ' as ') && strpos($type, '(') === false) { + $types[$i] = '(' . $type . ')'; + } + } + } + + $id = implode('|', $types); + + if ($exact) { + /** @psalm-suppress ImpurePropertyAssignment Cache */ + $this->exact_id = $id; + } else { + /** @psalm-suppress ImpurePropertyAssignment Cache */ + $this->id = $id; + } + + return $id; + } + + /** + * @param array $aliased_classes + * @psalm-mutation-free + */ + public function toNamespacedString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + bool $use_phpdoc_format + ): string { + $other_types = []; + + $literal_ints = []; + $literal_strings = []; + + $has_non_literal_int = false; + $has_non_literal_string = false; + + foreach ($this->types as $type) { + $type_string = $type->toNamespacedString($namespace, $aliased_classes, $this_class, $use_phpdoc_format); + if ($type instanceof TLiteralInt) { + $literal_ints[] = $type_string; + } elseif ($type instanceof TLiteralString) { + $literal_strings[] = $type_string; + } else { + if (get_class($type) === TString::class) { + $has_non_literal_string = true; + } elseif (get_class($type) === TInt::class) { + $has_non_literal_int = true; + } + $other_types[] = $type_string; + } + } + + if (count($literal_ints) <= 3 && !$has_non_literal_int) { + $other_types = array_merge($other_types, $literal_ints); + } else { + $other_types[] = 'int'; + } + + if (count($literal_strings) <= 3 && !$has_non_literal_string) { + $other_types = array_merge($other_types, $literal_strings); + } else { + $other_types[] = 'string'; + } + + sort($other_types); + return implode('|', array_unique($other_types)); + } + + /** + * @psalm-mutation-free + * @param array $aliased_classes + */ + public function toPhpString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + int $analysis_php_version_id + ): ?string { + if (!$this->isSingleAndMaybeNullable()) { + if ($analysis_php_version_id < 8_00_00) { + return null; + } + } elseif ($analysis_php_version_id < 7_00_00 + || (isset($this->types['null']) && $analysis_php_version_id < 7_01_00) + ) { + return null; + } + + $types = $this->types; + + $nullable = false; + + if (isset($types['null']) && count($types) > 1) { + unset($types['null']); + + $nullable = true; + } + + $falsable = false; + + if (isset($types['false']) && count($types) > 1) { + unset($types['false']); + + $falsable = true; + } + + $php_types = []; + + foreach ($types as $atomic_type) { + $php_type = $atomic_type->toPhpString( + $namespace, + $aliased_classes, + $this_class, + $analysis_php_version_id + ); + + if (!$php_type) { + return null; + } + + $php_types[] = $php_type; + } + + if ($falsable) { + if ($nullable) { + $php_types['null'] = 'null'; + } + $php_types['false'] = 'false'; + ksort($php_types); + return implode('|', array_unique($php_types)); + } + + if ($analysis_php_version_id < 8_00_00) { + return ($nullable ? '?' : '') . implode('|', array_unique($php_types)); + } + if ($nullable) { + $php_types['null'] = 'null'; + } + return implode('|', array_unique($php_types)); + } + + /** + * @psalm-mutation-free + */ + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool + { + if (!$this->isSingleAndMaybeNullable() && $analysis_php_version_id < 8_00_00) { + return false; + } + + $types = $this->types; + + if (isset($types['null'])) { + if (count($types) > 1) { + unset($types['null']); + } else { + return false; + } + } + + return !array_filter( + $types, + static fn($atomic_type): bool => !$atomic_type->canBeFullyExpressedInPhp($analysis_php_version_id) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasType(string $type_string): bool + { + return isset($this->types[$type_string]); + } + + /** + * @psalm-mutation-free + */ + public function hasArray(): bool + { + return isset($this->types['array']); + } + + /** + * @psalm-mutation-free + */ + public function hasIterable(): bool + { + return isset($this->types['iterable']); + } + + /** + * @psalm-mutation-free + */ + public function hasList(): bool + { + return isset($this->types['array']) && $this->types['array'] instanceof TList; + } + + /** + * @psalm-mutation-free + */ + public function hasClassStringMap(): bool + { + return isset($this->types['array']) && $this->types['array'] instanceof TClassStringMap; + } + + /** + * @psalm-mutation-free + */ + public function isTemplatedClassString(): bool + { + return $this->isSingle() + && count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TTemplateParamClass + ) + ) === 1; + } + + /** + * @psalm-mutation-free + */ + public function hasArrayAccessInterface(Codebase $codebase): bool + { + return (bool)array_filter( + $this->types, + static fn($type): bool => $type->hasArrayAccessInterface($codebase) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasCallableType(): bool + { + return $this->getCallableTypes() || $this->getClosureTypes(); + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getCallableTypes(): array + { + return array_filter( + $this->types, + static fn($type): bool => $type instanceof TCallable + ); + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getClosureTypes(): array + { + return array_filter( + $this->types, + static fn($type): bool => $type instanceof TClosure + ); + } + + /** + * @psalm-mutation-free + */ + public function hasObject(): bool + { + return isset($this->types['object']); + } + + /** + * @psalm-mutation-free + */ + public function hasObjectType(): bool + { + foreach ($this->types as $type) { + if ($type->isObjectType()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isObjectType(): bool + { + foreach ($this->types as $type) { + if (!$type->isObjectType()) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function hasNamedObjectType(): bool + { + foreach ($this->types as $type) { + if ($type->isNamedObjectType()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isStaticObject(): bool + { + foreach ($this->types as $type) { + if (!$type instanceof TNamedObject + || !$type->is_static + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function hasStaticObject(): bool + { + foreach ($this->types as $type) { + if ($type instanceof TNamedObject + && $type->is_static + ) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isNullable(): bool + { + if (isset($this->types['null'])) { + return true; + } + + foreach ($this->types as $type) { + if ($type instanceof TTemplateParam && $type->as->isNullable()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isFalsable(): bool + { + if (isset($this->types['false'])) { + return true; + } + + foreach ($this->types as $type) { + if ($type instanceof TTemplateParam && $type->as->isFalsable()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function hasBool(): bool + { + return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); + } + + /** + * @psalm-mutation-free + */ + public function hasString(): bool + { + return isset($this->types['string']) + || isset($this->types['class-string']) + || isset($this->types['trait-string']) + || isset($this->types['numeric-string']) + || isset($this->types['callable-string']) + || isset($this->types['array-key']) + || $this->literal_string_types + || $this->typed_class_strings; + } + + /** + * @psalm-mutation-free + */ + public function hasLowercaseString(): bool + { + return isset($this->types['string']) + && ($this->types['string'] instanceof TLowercaseString + || $this->types['string'] instanceof TNonEmptyLowercaseString); + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralClassString(): bool + { + return count($this->typed_class_strings) > 0; + } + + /** + * @psalm-mutation-free + */ + public function hasInt(): bool + { + return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types + || array_filter($this->types, static fn(Atomic $type): bool => $type instanceof TIntRange); + } + + /** + * @psalm-mutation-free + */ + public function hasArrayKey(): bool + { + return isset($this->types['array-key']); + } + + /** + * @psalm-mutation-free + */ + public function hasFloat(): bool + { + return isset($this->types['float']) || $this->literal_float_types; + } + + /** + * @psalm-mutation-free + */ + public function hasScalar(): bool + { + return isset($this->types['scalar']); + } + + /** + * @psalm-mutation-free + */ + public function hasNumeric(): bool + { + return isset($this->types['numeric']); + } + + /** + * @psalm-mutation-free + */ + public function hasScalarType(): bool + { + return isset($this->types['int']) + || isset($this->types['float']) + || isset($this->types['string']) + || isset($this->types['class-string']) + || isset($this->types['trait-string']) + || isset($this->types['bool']) + || isset($this->types['false']) + || isset($this->types['true']) + || isset($this->types['numeric']) + || isset($this->types['numeric-string']) + || $this->literal_int_types + || $this->literal_float_types + || $this->literal_string_types + || $this->typed_class_strings; + } + + /** + * @psalm-mutation-free + */ + public function hasTemplate(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TTemplateParam + || ($type instanceof TNamedObject + && $type->extra_types + && array_filter( + $type->extra_types, + static fn($t): bool => $t instanceof TTemplateParam + ) + ) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasConditional(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TConditional + ); + } + + /** + * @psalm-mutation-free + */ + public function hasTemplateOrStatic(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TTemplateParam + || ($type instanceof TNamedObject + && ($type->is_static + || ($type->extra_types + && array_filter( + $type->extra_types, + static fn($t): bool => $t instanceof TTemplateParam + ) + ) + ) + ) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasMixed(): bool + { + return isset($this->types['mixed']); + } + + /** + * @psalm-mutation-free + */ + public function isMixed(): bool + { + return isset($this->types['mixed']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isEmptyMixed(): bool + { + return isset($this->types['mixed']) + && $this->types['mixed'] instanceof TEmptyMixed + && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isVanillaMixed(): bool + { + return isset($this->types['mixed']) + && get_class($this->types['mixed']) === TMixed::class + && !$this->types['mixed']->from_loop_isset + && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isArrayKey(): bool + { + return isset($this->types['array-key']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isNull(): bool + { + return count($this->types) === 1 && isset($this->types['null']); + } + + /** + * @psalm-mutation-free + */ + public function isFalse(): bool + { + return count($this->types) === 1 && isset($this->types['false']); + } + + /** + * @psalm-mutation-free + */ + public function isAlwaysFalsy(): bool + { + foreach ($this->getAtomicTypes() as $atomic_type) { + if (!$atomic_type->isFalsy()) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function isTrue(): bool + { + return count($this->types) === 1 && isset($this->types['true']); + } + + /** + * @psalm-mutation-free + */ + public function isAlwaysTruthy(): bool + { + if ($this->possibly_undefined || $this->possibly_undefined_from_try) { + return false; + } + + foreach ($this->getAtomicTypes() as $atomic_type) { + if (!$atomic_type->isTruthy()) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function isVoid(): bool + { + return isset($this->types['void']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isNever(): bool + { + return isset($this->types['never']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isGenerator(): bool + { + return count($this->types) === 1 + && (($single_type = reset($this->types)) instanceof TNamedObject) + && ($single_type->value === 'Generator'); + } + + /** + * @psalm-mutation-free + */ + public function isSingle(): bool + { + $type_count = count($this->types); + + $int_literal_count = count($this->literal_int_types); + $string_literal_count = count($this->literal_string_types); + $float_literal_count = count($this->literal_float_types); + + if (($int_literal_count && $string_literal_count) + || ($int_literal_count && $float_literal_count) + || ($string_literal_count && $float_literal_count) + ) { + return false; + } + + if ($int_literal_count || $string_literal_count || $float_literal_count) { + $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; + } + + return $type_count === 1; + } + + /** + * @psalm-mutation-free + */ + public function isSingleAndMaybeNullable(): bool + { + $is_nullable = isset($this->types['null']); + + $type_count = count($this->types); + + if ($type_count === 1 && $is_nullable) { + return false; + } + + $int_literal_count = count($this->literal_int_types); + $string_literal_count = count($this->literal_string_types); + $float_literal_count = count($this->literal_float_types); + + if (($int_literal_count && $string_literal_count) + || ($int_literal_count && $float_literal_count) + || ($string_literal_count && $float_literal_count) + ) { + return false; + } + + if ($int_literal_count || $string_literal_count || $float_literal_count) { + $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; + } + + return ($type_count - (int) $is_nullable) === 1; + } + + /** + * @psalm-mutation-free + * @return bool true if this is an int + */ + public function isInt(bool $check_templates = false): bool + { + return count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TInt + || ($check_templates + && $type instanceof TTemplateParam + && $type->as->isInt() + ) + ) + ) === count($this->types); + } + + /** + * @psalm-mutation-free + * @return bool true if this is a float + */ + public function isFloat(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['float']) || $this->literal_float_types; + } + + /** + * @psalm-mutation-free + * @return bool true if this is a string + */ + public function isString(bool $check_templates = false): bool + { + return count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TString + || ($check_templates + && $type instanceof TTemplateParam + && $type->as->isString() + ) + ) + ) === count($this->types); + } + + /** + * @psalm-mutation-free + * @return bool true if this is a boolean + */ + public function isBool(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['bool']); + } + + /** + * @psalm-mutation-free + * @return bool true if this is an array + */ + public function isArray(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['array']); + } + + /** + * @psalm-mutation-free + * @return bool true if this is a string literal with only one possible value + */ + public function isSingleStringLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_string_types) === 1; + } + + /** + * @throws InvalidArgumentException if isSingleStringLiteral is false + * @psalm-mutation-free + * @return TLiteralString the only string literal represented by this union type + */ + public function getSingleStringLiteral(): TLiteralString + { + if (count($this->types) !== 1 || count($this->literal_string_types) !== 1) { + throw new InvalidArgumentException('Not a string literal'); + } + + return reset($this->literal_string_types); + } + + /** + * @psalm-mutation-free + */ + public function allStringLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function allIntLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralInt) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function allFloatLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralFloat) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue + * > $this->getAtomicTypes() + */ + public function allSpecificLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString + && !$atomic_key_type instanceof TLiteralInt + && !$atomic_key_type instanceof TLiteralFloat + && !$atomic_key_type instanceof TFalse + && !$atomic_key_type instanceof TTrue + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue + * > $this->getAtomicTypes() + */ + public function allLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString + && !$atomic_key_type instanceof TLiteralInt + && !$atomic_key_type instanceof TLiteralFloat + && !$atomic_key_type instanceof TNonspecificLiteralString + && !$atomic_key_type instanceof TNonspecificLiteralInt + && !$atomic_key_type instanceof TFalse + && !$atomic_key_type instanceof TTrue + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralValue(): bool + { + return $this->literal_int_types + || $this->literal_string_types + || $this->literal_float_types + || isset($this->types['false']) + || isset($this->types['true']); + } + + /** + * @psalm-mutation-free + */ + public function isSingleLiteral(): bool + { + return count($this->types) === 1 + && count($this->literal_int_types) + + count($this->literal_string_types) + + count($this->literal_float_types) === 1 + ; + } + + /** + * @psalm-mutation-free + * @return TLiteralInt|TLiteralString|TLiteralFloat + */ + public function getSingleLiteral() + { + if (!$this->isSingleLiteral()) { + throw new InvalidArgumentException("Not a single literal"); + } + + return ($literal = reset($this->literal_int_types)) !== false + ? $literal + : (($literal = reset($this->literal_string_types)) !== false + ? $literal + : reset($this->literal_float_types)) + ; + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralString(): bool + { + return count($this->literal_string_types) > 0; + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralInt(): bool + { + return count($this->literal_int_types) > 0; + } + + /** + * @psalm-mutation-free + * @return bool true if this is a int literal with only one possible value + */ + public function isSingleIntLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_int_types) === 1; + } + + /** + * @throws InvalidArgumentException if isSingleIntLiteral is false + * @psalm-mutation-free + * @return TLiteralInt the only int literal represented by this union type + */ + public function getSingleIntLiteral(): TLiteralInt + { + if (count($this->types) !== 1 || count($this->literal_int_types) !== 1) { + throw new InvalidArgumentException('Not an int literal'); + } + + return reset($this->literal_int_types); + } + + /** + * @param array $suppressed_issues + * @param array $phantom_classes + */ + public function check( + StatementsSource $source, + CodeLocation $code_location, + array $suppressed_issues, + array $phantom_classes = [], + bool $inferred = true, + bool $inherited = false, + bool $prevent_template_covariance = false, + ?string $calling_method_id = null + ): bool { + if ($this->checked) { + return true; + } + + $checker = new TypeChecker( + $source, + $code_location, + $suppressed_issues, + $phantom_classes, + $inferred, + $inherited, + $prevent_template_covariance, + $calling_method_id + ); + + $checker->traverseArray($this->types); + + $this->checked = true; + + return !$checker->hasErrors(); + } + + /** + * @param array $phantom_classes + */ + public function queueClassLikesForScanning( + Codebase $codebase, + ?FileStorage $file_storage = null, + array $phantom_classes = [] + ): void { + $scanner_visitor = new TypeScanner( + $codebase->scanner, + $file_storage, + $phantom_classes + ); + + $scanner_visitor->traverseArray($this->types); + } + + /** + * @param lowercase-string $fq_class_like_name + * @psalm-mutation-free + */ + public function containsClassLike(string $fq_class_like_name): bool + { + $classlike_visitor = new ContainsClassLikeVisitor($fq_class_like_name); + + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ + $classlike_visitor->traverseArray($this->types); + + return $classlike_visitor->matches(); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type = $this; + (new ClasslikeReplacer( + $old, + $new + ))->traverse($type); + return $type; + } + + /** @psalm-mutation-free */ + public function containsAnyLiteral(): bool + { + $literal_visitor = new ContainsLiteralVisitor(); + + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ + $literal_visitor->traverseArray($this->types); + + return $literal_visitor->matches(); + } + + /** + * @psalm-mutation-free + * @return list + */ + public function getTemplateTypes(): array + { + $template_type_collector = new TemplateTypeCollector(); + + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ + $template_type_collector->traverseArray($this->types); + + return $template_type_collector->getTemplateTypes(); + } + + /** + * @psalm-mutation-free + */ + public function equals(self $other_type, bool $ensure_source_equality = true): bool + { + if ($other_type === $this) { + return true; + } + + if ($other_type->id && $this->id && $other_type->id !== $this->id) { + return false; + } + + if ($other_type->exact_id && $this->exact_id && $other_type->exact_id !== $this->exact_id) { + return false; + } + + if ($this->possibly_undefined !== $other_type->possibly_undefined) { + return false; + } + + if ($this->had_template !== $other_type->had_template) { + return false; + } + + if ($this->possibly_undefined_from_try !== $other_type->possibly_undefined_from_try) { + return false; + } + + if ($this->from_calculation !== $other_type->from_calculation) { + return false; + } + + if ($this->initialized !== $other_type->initialized) { + return false; + } + + if ($ensure_source_equality && $this->from_docblock !== $other_type->from_docblock) { + return false; + } + + if (count($this->types) !== count($other_type->types)) { + return false; + } + + if ($this->parent_nodes !== $other_type->parent_nodes) { + return false; + } + + if ($this->different || $other_type->different) { + return false; + } + + $other_atomic_types = $other_type->types; + + foreach ($this->types as $key => $atomic_type) { + if (!isset($other_atomic_types[$key])) { + return false; + } + + if (!$atomic_type->equals($other_atomic_types[$key], $ensure_source_equality)) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getLiteralStrings(): array + { + return $this->literal_string_types; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getLiteralInts(): array + { + return $this->literal_int_types; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getRangeInts(): array + { + $ranges = []; + foreach ($this->getAtomicTypes() as $atomic) { + if ($atomic instanceof TIntRange) { + $ranges[$atomic->getKey()] = $atomic; + } + } + + return $ranges; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getLiteralFloats(): array + { + return $this->literal_float_types; + } + + /** + * @psalm-mutation-free + * @return list + */ + public function getChildNodeKeys(): array + { + return ['types']; + } + + /** + * @psalm-mutation-free + * @return bool true if this is a float literal with only one possible value + */ + public function isSingleFloatLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_float_types) === 1; + } + + /** + * @psalm-mutation-free + * @throws InvalidArgumentException if isSingleFloatLiteral is false + * + * @return TLiteralFloat the only float literal represented by this union type + */ + public function getSingleFloatLiteral(): TLiteralFloat + { + if (count($this->types) !== 1 || count($this->literal_float_types) !== 1) { + throw new InvalidArgumentException('Not a float literal'); + } + + return reset($this->literal_float_types); + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralFloat(): bool + { + return count($this->literal_float_types) > 0; + } + + /** + * @psalm-mutation-free + */ + public function getSingleAtomic(): Atomic + { + return reset($this->types); + } + + /** + * @psalm-mutation-free + */ + public function isEmptyArray(): bool + { + return count($this->types) === 1 + && isset($this->types['array']) + && $this->types['array'] instanceof TArray + && $this->types['array']->isEmptyArray(); + } + + /** + * @psalm-mutation-free + */ + public function isUnionEmpty(): bool + { + return $this->types === []; + } +} 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/AlgebraTest.php b/tests/AlgebraTest.php index 6571701c436..a4b7eb8085e 100644 --- a/tests/AlgebraTest.php +++ b/tests/AlgebraTest.php @@ -83,7 +83,7 @@ public function testNegateFormulaWithUnreconcilableTerm(): void $a1 = new IsType(new TInt()); $formula = [ new Clause(['$a' => [(string)$a1 => $a1]], 1, 1), - new Clause(['$b' => [(string)$a1 => clone $a1]], 1, 2, false, false), + new Clause(['$b' => [(string)$a1 => $a1]], 1, 2, false, false), ]; $negated_formula = Algebra::negateFormula($formula); diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 76003499ced..fdf1f43914d 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -564,11 +564,20 @@ function f(array $p): void ], 'unsetTKeyedArrayOffset' => [ 'code' => ' "value"]; - unset($x["a"]); - $x[] = 5; - takesInt($x[0]);', + $x1 = ["a" => "value"]; + unset($x1["a"]); + + $x2 = ["a" => "value", "b" => "value"]; + unset($x2["a"]); + + $x3 = ["a" => "value", "b" => "value"]; + $k = "a"; + unset($x3[$k]);', + 'assertions' => [ + '$x1===' => 'array', + '$x2===' => "array{b: 'value'}", + '$x3===' => "array{b: 'value'}", + ] ], 'domNodeListAccessible' => [ 'code' => ' [ '$_arr1===' => 'non-empty-array<1, 5>', - '$_arr2===' => 'non-empty-array<1, 5>', + '$_arr2===' => 'array{1: 5}', ] ], 'accessArrayWithSingleStringLiteralOffset' => [ 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' => 'params = [ ...array_map( function (TCallable $expected, int $offset) { - $param = new FunctionLikeParameter('fn' . $offset, false, new Union([$expected])); + $t = new Union([$expected]); + $param = new FunctionLikeParameter('fn' . $offset, false, $t, $t); $param->is_optional = false; return $param; @@ -71,10 +72,15 @@ function (TCallable $expected, int $offset) { private static function createLastArrayMapParam(Union $input_array_type): FunctionLikeParameter { - $last_array_map_param = new FunctionLikeParameter('input', false, $input_array_type); - $last_array_map_param->is_optional = false; - - return $last_array_map_param; + return new FunctionLikeParameter( + 'input', + false, + $input_array_type, + $input_array_type, + null, + null, + false + ); } /** @@ -106,13 +112,13 @@ private static function createExpectedCallable( DynamicTemplateProvider $template_provider, int $return_template_offset = 0 ): TCallable { - $expected_callable = new TCallable('callable'); - $expected_callable->params = [new FunctionLikeParameter('a', false, $input_type)]; - $expected_callable->return_type = new Union([ - $template_provider->createTemplate('T' . $return_template_offset) - ]); - - return $expected_callable; + return new TCallable( + 'callable', + [new FunctionLikeParameter('a', false, $input_type, $input_type)], + new Union([ + $template_provider->createTemplate('T' . $return_template_offset) + ]) + ); } /** diff --git a/tests/Config/Plugin/Hook/FooMethodProvider.php b/tests/Config/Plugin/Hook/FooMethodProvider.php index 222a51ed537..b5d150e0326 100644 --- a/tests/Config/Plugin/Hook/FooMethodProvider.php +++ b/tests/Config/Plugin/Hook/FooMethodProvider.php @@ -43,7 +43,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array { $method_name_lowercase = $event->getMethodNameLowercase(); if ($method_name_lowercase === 'magicmethod' || $method_name_lowercase === 'magicmethod2') { - return [new FunctionLikeParameter('first', false, Type::getString())]; + return [new FunctionLikeParameter('first', false, Type::getString(), Type::getString())]; } return null; diff --git a/tests/Config/Plugin/Hook/MagicFunctionProvider.php b/tests/Config/Plugin/Hook/MagicFunctionProvider.php index b9ec9ae1d6c..9ffa3706ccf 100644 --- a/tests/Config/Plugin/Hook/MagicFunctionProvider.php +++ b/tests/Config/Plugin/Hook/MagicFunctionProvider.php @@ -36,7 +36,7 @@ public static function doesFunctionExist(FunctionExistenceProviderEvent $event): */ public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array { - return [new FunctionLikeParameter('first', false, Type::getString())]; + return [new FunctionLikeParameter('first', false, Type::getString(), Type::getString())]; } public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index dc531abb562..5a60a348b5d 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -33,5 +33,76 @@ public function providerValidCodeParse(): iterable 'ignored_issues' => [], '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/DocumentationTest.php b/tests/DocumentationTest.php index 890b920c155..8dd2a40cb0b 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -437,6 +437,9 @@ public function testIssuesIndex(): void } $issues_index_contents = file($issues_index, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES); + if ($issues_index_contents === false) { + throw new UnexpectedValueException("Issues index returned false"); + } array_shift($issues_index_contents); // Remove title $issues_index_list = array_map(function (string $issues_line) { 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..6181f6611ec 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,14 +751,11 @@ 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"); } } - /** - * - * @psalm-suppress UndefinedMethod - */ + /** @psalm-suppress UndefinedMethod */ public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void { if (version_compare(PHP_VERSION, '8.1.0', '>=')) { @@ -771,20 +769,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 +789,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/PropertiesOfTest.php b/tests/PropertiesOfTest.php index 9f5d3668552..77d6c248d59 100644 --- a/tests/PropertiesOfTest.php +++ b/tests/PropertiesOfTest.php @@ -16,6 +16,44 @@ class PropertiesOfTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'propertiesOfIntersection' => [ + 'code' => ' + */ + function test1($a) {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test2() {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test3() {} + + /** @var i $i */ + assert($i instanceof b); + $result1 = test1($i); + $result2 = test2(); + $result3 = test3(); + ', + 'assertions' => [ + '$result1===' => 'array{a: int}', + '$result2===' => 'array{a: int}', + '$result3===' => 'array{a: int}', + ] + ], 'publicPropertiesOf' => [ 'code' => '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..282380d4f2f 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', ], @@ -2793,6 +2793,49 @@ public static function fromStrings(string $class, array $ids): self } }' ], + 'doNotForgetAssertion' => [ + 'code' => 'test($container); + + if ($container->expr) { + if (random_int(0, 1)) { + self::test( + $container, + ); + } + return $container->expr; + } + return 0; + } + + private static function test( + a $_, + ): void { + } + }' + ], 'noCrashTemplateInsideGenerator' => [ 'code' => ' '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 +4099,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' => 'setCallback(function() { return "b";});', - 'error_message' => 'InvalidScalarArgument', + 'error_message' => 'InvalidArgument', ], 'preventBoundsMismatchDifferentContainers' => [ '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/Template/FunctionClassStringTemplateTest.php b/tests/Template/FunctionClassStringTemplateTest.php index 223a291e794..34edd8fc5df 100644 --- a/tests/Template/FunctionClassStringTemplateTest.php +++ b/tests/Template/FunctionClassStringTemplateTest.php @@ -727,6 +727,55 @@ function bar(string $_fooClass): void {} bar(Bar::class); ', ], + 'classStringNestedTemplate' => [ + 'code' => ' + */ + final class AObject extends MyObject {} + /** + * @extends MyMapper + */ + final class AMapper extends MyMapper {} + + + /** + * @extends MyObject + */ + final class BObject extends MyObject {} + /** + * @extends MyMapper + */ + final class BMapper extends MyMapper {} + + + /** + * Get source, asserting class type + * + * @template T as MyObject + * + * @param class-string $class + * @param AObject|BObject $source + * + * @return T + */ + function getSourceAssertType(string $class, MyObject $source): MyObject { + if (!$source instanceof $class) { + throw new RuntimeException("Invalid class!"); + } + return $source; + }' + ] ]; } diff --git a/tests/Template/PropertiesOfTemplateTest.php b/tests/Template/PropertiesOfTemplateTest.php index 35f16c8e06a..3ec94ab7563 100644 --- a/tests/Template/PropertiesOfTemplateTest.php +++ b/tests/Template/PropertiesOfTemplateTest.php @@ -51,6 +51,37 @@ class A { $cConcat = $c . "foo"; ', ], + 'propertiesOfTemplateParamWithTemplate' => [ + 'code' => ' + */ + function asArray($obj) { + /** @var properties-of */ + $properties = []; + return $properties; + } + + /** @template T */ + class A { + /** @var bool */ + private $b = true; + /** @var string */ + protected $c = "c"; + + /** @param T $a */ + public function __construct(public $a) {} + } + + $obj = new A(42); + $objAsArray = asArray($obj); + ', + 'assertions' => [ + '$objAsArray===' => 'array{a: 42, b: bool, c: string}' + ] + ], 'privatePropertiesPicksPrivate' => [ 'code' => ' 'ImplicitToStringCast' ], + 'toStringTypecastNonString' => [ + 'code' => ' 'InvalidCast', + ], + 'riskyArrayToIntCast' => [ + 'code' => ' 'RiskyCast', + ], + 'riskyArrayToFloatCast' => [ + 'code' => ' 'RiskyCast', + ], ]; } } diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index 147ac2ab2be..5f656d9c830 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -24,6 +24,7 @@ public function testValidTypeCombination($expected, $types): void foreach ($types as $type) { $converted_type = self::getAtomic($type); + /** @psalm-suppress InaccessibleProperty */ $converted_type->from_docblock = true; $converted_types[] = $converted_type; } @@ -717,6 +718,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' => ' [ 'code' => '> $arr */ + /** + * @param non-empty-list> $arr + * @param-out non-empty-list> $arr + */ function foo(array &$arr): void { $b = 5; $arr[0][0] = $b; @@ -1968,7 +1971,10 @@ function foo(array &$arr): void { ], 'nestedReferencesToByRefParam' => [ 'code' => '> $arr */ + /** + * @param non-empty-list> $arr + * @param-out non-empty-list> $arr + */ function foo(array &$arr): void { $a = &$arr[0]; $b = &$a[0]; @@ -3579,7 +3585,7 @@ function takesArray($a) : void { $arr = [$a]; takesArrayOfString($arr); }', - 'error_message' => '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" }