diff --git a/.circleci/config.yml b/.circleci/config.yml index 341a05bb9ef..648eed2dd4b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,7 +47,8 @@ jobs: command: bin/build-phar.sh - run: name: Smoke test Phar file - command: build/psalm.phar --version + # Change the root away from the project root to avoid conflicts with the Composer autoloader + command: build/psalm.phar --version --root build - store_artifacts: path: build/psalm.phar - run: @@ -63,7 +64,7 @@ jobs: # The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass resource_class: large test-with-real-projects: - executor: php-81 + executor: php-82 steps: - checkout # used here just for the side effect of loading the github public ssh key so we can clone other stuff - attach_workspace: diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index 40da1ef7eb8..4d1142813e6 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -20,7 +20,7 @@ jobs: should_skip: ${{ steps.skip_check.outputs.should_skip }} steps: - id: skip_check - uses: fkirc/skip-duplicate-actions@v5.3.0 + uses: fkirc/skip-duplicate-actions@v5.3.1 with: concurrent_skipping: always cancel_others: true diff --git a/UPGRADING.md b/UPGRADING.md index f99f9ed277e..2622afea877 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -12,8 +12,11 @@ - [BC] The only optional boolean parameter of `TKeyedArray::getGenericArrayType` was removed, and was replaced with a string parameter with a different meaning. - [BC] The `TDependentListKey` type was removed and replaced with an optional property of the `TIntRange` type. +- - [BC] `TCallableArray` and `TCallableList` removed and replaced with `TCallableKeyedArray`. +- [BC] Class `Psalm\Issue\MixedInferredReturnType` was removed + - [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well. - [BC] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint` @@ -47,6 +50,14 @@ - [BC] `Psalm\CodeLocation\Raw`, `Psalm\CodeLocation\ParseErrorLocation`, `Psalm\CodeLocation\DocblockTypeLocation`, `Psalm\Report\CountReport`, `Psalm\Type\Atomic\TNonEmptyArray` are now all final. +- [BC] `Psalm\Config` is now final. + +- [BC] The return type of `Psalm\Plugin\ArgTypeInferer::infer` changed from `Union|false` to `Union|null` + +- [BC] The `extra_types` property and `setIntersectionTypes` method of `Psalm\Type\Atomic\TTypeAlias` were removed. + +- [BC] Methods `convertSeverity` and `calculateFingerprint` of `Psalm\Report\CodeClimateReport` were removed. + # Upgrading from Psalm 4 to Psalm 5 ## Changed diff --git a/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh index 9fd6483b0e6..923312bbea6 100755 --- a/bin/test-with-real-projects.sh +++ b/bin/test-with-real-projects.sh @@ -38,8 +38,11 @@ psl) cd endtoend-test-psl git checkout 2.3.x composer install - "$PSALM" --monochrome -c config/psalm.xml - "$PSALM" --monochrome -c config/psalm.xml tests/static-analysis + # Avoid conflicts with old psalm when running phar tests + rm -rf vendor/vimeo/psalm + sed 's/ErrorOutputBehavior::Packed, ErrorOutputBehavior::Discard/ErrorOutputBehavior::Discard/g' -i src/Psl/Shell/execute.php + "$PSALM_PHAR" --monochrome -c config/psalm.xml + "$PSALM_PHAR" --monochrome -c config/psalm.xml tests/static-analysis ;; laravel) diff --git a/bin/tests-github-actions.sh b/bin/tests-github-actions.sh index f180d9ca6d5..4296f70591a 100755 --- a/bin/tests-github-actions.sh +++ b/bin/tests-github-actions.sh @@ -24,7 +24,23 @@ exit "$exit_code"' mkdir -p build/parallel/ build/phpunit/logs/ find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random) > build/tests_all - split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split + # split incorrectly splits the lines by byte size, which means that the number of tests per file are as evenly distributed as possible + #split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split + local -r lines=$(wc -l build/tests_split + parallel --group -j"$parallel_processes" --rpl {_}\ s/\\//_/g --joblog build/parallel/jobs.log "$phpunit_cmd" < build/tests_split } diff --git a/composer.json b/composer.json index 0a36e057c1f..84e67a0850f 100644 --- a/composer.json +++ b/composer.json @@ -36,8 +36,8 @@ "nikic/php-parser": "^4.16", "sebastian/diff": "^4.0 || ^5.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", - "symfony/console": "^4.1.6 || ^5.0 || ^6.0", - "symfony/filesystem": "^5.4 || ^6.0" + "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" }, "conflict": { "nikic/php-parser": "4.17.0" @@ -60,7 +60,7 @@ "psalm/plugin-phpunit": "^0.18", "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^4.4 || ^5.0 || ^6.0" + "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { "ext-igbinary": "^2.0.5 is required, used to serialize caching data", diff --git a/config.xsd b/config.xsd index dcd1f22b864..412cc3c4396 100644 --- a/config.xsd +++ b/config.xsd @@ -47,6 +47,7 @@ + @@ -333,7 +334,6 @@ - @@ -495,6 +495,7 @@ + diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 782ed5d8fdf..4e7166ec105 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -371,7 +371,7 @@ 'array_diff_ukey\'1' => ['array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_fill' => ['array', 'start_index'=>'int', 'count'=>'int', 'value'=>'mixed'], 'array_fill_keys' => ['array', 'keys'=>'array', 'value'=>'mixed'], -'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar|null', 'mode='=>'int'], +'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed|null', 'mode='=>'int'], 'array_flip' => ['array', 'array'=>'array'], 'array_intersect' => ['array', 'array'=>'array', '...arrays='=>'array'], 'array_intersect_assoc' => ['array', 'array'=>'array', '...arrays='=>'array'], @@ -7060,19 +7060,19 @@ 'MongoDB\BSON\Binary::getType' => ['int'], 'MongoDB\BSON\Binary::__toString' => ['string'], 'MongoDB\BSON\Binary::serialize' => ['string'], -'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Binary::unserialize' => ['void', 'data' => '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::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], 'MongoDB\BSON\Decimal128::serialize' => ['string'], -'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Decimal128::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Document::fromBSON' => ['MongoDB\BSON\Document', 'bson' => 'string'], @@ -7084,13 +7084,17 @@ 'MongoDB\BSON\Document::toPHP' => ['object|array', 'typeMap=' => '?array'], 'MongoDB\BSON\Document::toCanonicalExtendedJSON' => ['string'], 'MongoDB\BSON\Document::toRelaxedExtendedJSON' => ['string'], +'MongoDB\BSON\Document::offsetExists' => ['bool', 'offset' => 'mixed'], +'MongoDB\BSON\Document::offsetGet' => ['mixed', 'offset' => 'mixed'], +'MongoDB\BSON\Document::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], +'MongoDB\BSON\Document::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\Document::__toString' => ['string'], 'MongoDB\BSON\Document::serialize' => ['string'], -'MongoDB\BSON\Document::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Document::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::__construct' => ['void', 'value' => 'string|int'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], -'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Int64::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Iterator::current' => ['mixed'], 'MongoDB\BSON\Iterator::key' => ['string|int'], @@ -7102,22 +7106,22 @@ '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::unserialize' => ['void', 'data' => '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::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\MinKey::serialize' => ['string'], -'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\MinKey::unserialize' => ['void', 'data' => '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::serialize' => ['string'], -'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\ObjectId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], @@ -7126,30 +7130,35 @@ 'MongoDB\BSON\PackedArray::getIterator' => ['MongoDB\BSON\Iterator'], 'MongoDB\BSON\PackedArray::has' => ['bool', 'index' => 'int'], 'MongoDB\BSON\PackedArray::toPHP' => ['object|array', 'typeMap=' => '?array'], +'MongoDB\BSON\PackedArray::offsetExists' => ['bool', 'offset' => 'mixed'], +'MongoDB\BSON\PackedArray::offsetGet' => ['mixed', 'offset' => 'mixed'], +'MongoDB\BSON\PackedArray::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], +'MongoDB\BSON\PackedArray::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\PackedArray::__toString' => ['string'], 'MongoDB\BSON\PackedArray::serialize' => ['string'], -'MongoDB\BSON\PackedArray::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\PackedArray::unserialize' => ['void', 'data' => 'string'], +'MongoDB\BSON\Persistable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|array'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], '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::unserialize' => ['void', 'data' => '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\Serializable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], -'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Symbol::unserialize' => ['void', 'data' => '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::serialize' => ['string'], -'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Timestamp::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], @@ -7158,13 +7167,13 @@ 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], -'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'data' => '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::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], @@ -7197,7 +7206,7 @@ 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], -'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\CursorId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], @@ -7266,6 +7275,7 @@ '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\LogSubscriber::log' => ['void', 'level' => 'int', 'domain' => 'string', 'message' => 'string'], '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'], @@ -7308,18 +7318,18 @@ '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::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], -'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'data' => '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::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], -'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'data' => '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'], @@ -7339,9 +7349,9 @@ 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], -'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], +'MongoDB\Driver\ServerApi::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ServerApi::serialize' => ['string'], -'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ServerApi::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], 'MongoDB\Driver\ServerDescription::getHost' => ['string'], 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], @@ -7371,9 +7381,9 @@ '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::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], -'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], @@ -9696,97 +9706,6 @@ 'RarException::setUsingExceptions' => ['RarEntry', 'using_exceptions'=>'bool'], 'rawurldecode' => ['string', 'string'=>'string'], 'rawurlencode' => ['string', 'string'=>'string'], -'rd_kafka_err2str' => ['string', 'err'=>'int'], -'rd_kafka_errno' => ['int'], -'rd_kafka_errno2err' => ['int', 'errnox'=>'int'], -'rd_kafka_offset_tail' => ['int', 'cnt'=>'int'], -'RdKafka::addBrokers' => ['int', 'broker_list'=>'string'], -'RdKafka::flush' => ['int', 'timeout_ms'=>'int'], -'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], -'RdKafka::getOutQLen' => ['int'], -'RdKafka::newQueue' => ['RdKafka\Queue'], -'RdKafka::newTopic' => ['RdKafka\Topic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], -'RdKafka::poll' => ['void', 'timeout_ms'=>'int'], -'RdKafka::setLogLevel' => ['void', 'level'=>'int'], -'RdKafka\Conf::dump' => ['array'], -'RdKafka\Conf::set' => ['void', 'name'=>'string', 'value'=>'string'], -'RdKafka\Conf::setDefaultTopicConf' => ['void', 'topic_conf'=>'RdKafka\TopicConf'], -'RdKafka\Conf::setDrMsgCb' => ['void', 'callback'=>'callable'], -'RdKafka\Conf::setErrorCb' => ['void', 'callback'=>'callable'], -'RdKafka\Conf::setRebalanceCb' => ['void', 'callback'=>'callable'], -'RdKafka\Conf::setStatsCb' => ['void', 'callback'=>'callable'], -'RdKafka\Consumer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], -'RdKafka\Consumer::addBrokers' => ['int', 'broker_list'=>'string'], -'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], -'RdKafka\Consumer::getOutQLen' => ['int'], -'RdKafka\Consumer::newQueue' => ['RdKafka\Queue'], -'RdKafka\Consumer::newTopic' => ['RdKafka\ConsumerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], -'RdKafka\Consumer::poll' => ['void', 'timeout_ms'=>'int'], -'RdKafka\Consumer::setLogLevel' => ['void', 'level'=>'int'], -'RdKafka\ConsumerTopic::__construct' => ['void'], -'RdKafka\ConsumerTopic::consume' => ['RdKafka\Message', 'partition'=>'int', 'timeout_ms'=>'int'], -'RdKafka\ConsumerTopic::consumeQueueStart' => ['void', 'partition'=>'int', 'offset'=>'int', 'queue'=>'RdKafka\Queue'], -'RdKafka\ConsumerTopic::consumeStart' => ['void', 'partition'=>'int', 'offset'=>'int'], -'RdKafka\ConsumerTopic::consumeStop' => ['void', 'partition'=>'int'], -'RdKafka\ConsumerTopic::getName' => ['string'], -'RdKafka\ConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], -'RdKafka\KafkaConsumer::__construct' => ['void', 'conf'=>'RdKafka\Conf'], -'RdKafka\KafkaConsumer::assign' => ['void', 'topic_partitions='=>'RdKafka\TopicPartition[]|null'], -'RdKafka\KafkaConsumer::commit' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], -'RdKafka\KafkaConsumer::commitAsync' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], -'RdKafka\KafkaConsumer::consume' => ['RdKafka\Message', 'timeout_ms'=>'int'], -'RdKafka\KafkaConsumer::getAssignment' => ['RdKafka\TopicPartition[]'], -'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'], -'RdKafka\KafkaConsumer::getSubscription' => ['array'], -'RdKafka\KafkaConsumer::subscribe' => ['void', 'topics'=>'array'], -'RdKafka\KafkaConsumer::unsubscribe' => ['void'], -'RdKafka\KafkaConsumerTopic::getName' => ['string'], -'RdKafka\KafkaConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], -'RdKafka\Message::errstr' => ['string'], -'RdKafka\Metadata::getBrokers' => ['RdKafka\Metadata\Collection'], -'RdKafka\Metadata::getOrigBrokerId' => ['int'], -'RdKafka\Metadata::getOrigBrokerName' => ['string'], -'RdKafka\Metadata::getTopics' => ['RdKafka\Metadata\Collection|RdKafka\Metadata\Topic[]'], -'RdKafka\Metadata\Collection::__construct' => ['void'], -'RdKafka\Metadata\Collection::count' => ['int'], -'RdKafka\Metadata\Collection::current' => ['mixed'], -'RdKafka\Metadata\Collection::key' => ['mixed'], -'RdKafka\Metadata\Collection::next' => ['void'], -'RdKafka\Metadata\Collection::rewind' => ['void'], -'RdKafka\Metadata\Collection::valid' => ['bool'], -'RdKafka\Metadata\Partition::getErr' => ['mixed'], -'RdKafka\Metadata\Partition::getId' => ['int'], -'RdKafka\Metadata\Partition::getIsrs' => ['mixed'], -'RdKafka\Metadata\Partition::getLeader' => ['mixed'], -'RdKafka\Metadata\Partition::getReplicas' => ['mixed'], -'RdKafka\Metadata\Topic::getErr' => ['mixed'], -'RdKafka\Metadata\Topic::getPartitions' => ['RdKafka\Metadata\Partition[]'], -'RdKafka\Metadata\Topic::getTopic' => ['string'], -'RdKafka\Producer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], -'RdKafka\Producer::addBrokers' => ['int', 'broker_list'=>'string'], -'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], -'RdKafka\Producer::getOutQLen' => ['int'], -'RdKafka\Producer::newQueue' => ['RdKafka\Queue'], -'RdKafka\Producer::newTopic' => ['RdKafka\ProducerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], -'RdKafka\Producer::poll' => ['void', 'timeout_ms'=>'int'], -'RdKafka\Producer::setLogLevel' => ['void', 'level'=>'int'], -'RdKafka\ProducerTopic::__construct' => ['void'], -'RdKafka\ProducerTopic::getName' => ['string'], -'RdKafka\ProducerTopic::produce' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string'], -'RdKafka\ProducerTopic::producev' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string', 'headers='=>'?array', 'timestamp_ms='=>'?int', 'opaque='=>'?string'], -'RdKafka\Queue::__construct' => ['void'], -'RdKafka\Queue::consume' => ['?RdKafka\Message', 'timeout_ms'=>'string'], -'RdKafka\Topic::getName' => ['string'], -'RdKafka\TopicConf::dump' => ['array'], -'RdKafka\TopicConf::set' => ['void', 'name'=>'string', 'value'=>'string'], -'RdKafka\TopicConf::setPartitioner' => ['void', 'partitioner'=>'int'], -'RdKafka\TopicPartition::__construct' => ['void', 'topic'=>'string', 'partition'=>'int', 'offset='=>'int'], -'RdKafka\TopicPartition::getOffset' => ['int'], -'RdKafka\TopicPartition::getPartition' => ['int'], -'RdKafka\TopicPartition::getTopic' => ['string'], -'RdKafka\TopicPartition::setOffset' => ['void', 'offset'=>'string'], -'RdKafka\TopicPartition::setPartition' => ['void', 'partition'=>'string'], -'RdKafka\TopicPartition::setTopic' => ['void', 'topic_name'=>'string'], 'readdir' => ['string|false', 'dir_handle='=>'resource'], 'readfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], 'readgzfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'int'], @@ -10727,16 +10646,21 @@ 'ReflectionProperty::__toString' => ['string'], 'ReflectionProperty::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], 'ReflectionProperty::getDeclaringClass' => ['ReflectionClass'], +'ReflectionProperty::getDefaultValue' => ['mixed'], 'ReflectionProperty::getDocComment' => ['string|false'], 'ReflectionProperty::getModifiers' => ['int'], 'ReflectionProperty::getName' => ['string'], 'ReflectionProperty::getType' => ['?ReflectionType'], 'ReflectionProperty::getValue' => ['mixed', 'object='=>'null|object'], +'ReflectionProperty::hasDefaultValue' => ['bool'], 'ReflectionProperty::hasType' => ['bool'], 'ReflectionProperty::isDefault' => ['bool'], +'ReflectionProperty::isInitialized' => ['bool', 'object='=>'null|object'], 'ReflectionProperty::isPrivate' => ['bool'], +'ReflectionProperty::isPromoted' => ['bool'], 'ReflectionProperty::isProtected' => ['bool'], 'ReflectionProperty::isPublic' => ['bool'], +'ReflectionProperty::isReadonly' => ['bool'], 'ReflectionProperty::isStatic' => ['bool'], 'ReflectionProperty::setAccessible' => ['void', 'accessible'=>'bool'], 'ReflectionProperty::setValue' => ['void', 'object'=>'null|object', 'value'=>''], diff --git a/dictionaries/CallMap_74_delta.php b/dictionaries/CallMap_74_delta.php index 87872004a22..9fdb508aebb 100644 --- a/dictionaries/CallMap_74_delta.php +++ b/dictionaries/CallMap_74_delta.php @@ -17,6 +17,7 @@ return [ 'added' => [ 'ReflectionProperty::getType' => ['?ReflectionType'], + 'ReflectionProperty::isInitialized' => ['bool', 'object'=>'object'], 'mb_str_split' => ['list|false', 'string'=>'string', 'length='=>'positive-int', 'encoding='=>'string'], 'openssl_x509_verify' => ['int', 'certificate'=>'string|resource', 'public_key'=>'string|array|resource'], ], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 29227b632bc..1f8ec529d84 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -27,6 +27,9 @@ 'ReflectionFunctionAbstract::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], 'ReflectionParameter::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], 'ReflectionProperty::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], + 'ReflectionProperty::getDefaultValue' => ['mixed'], + 'ReflectionProperty::hasDefaultValue' => ['bool'], + 'ReflectionProperty::isPromoted' => ['bool'], 'ReflectionUnionType::getTypes' => ['list'], 'SplFixedArray::getIterator' => ['Iterator'], 'WeakMap::count' => ['int'], @@ -424,6 +427,10 @@ 'old' => ['mixed', 'object='=>'object'], 'new' => ['mixed', 'object='=>'null|object'], ], + 'ReflectionProperty::isInitialized' => [ + 'old' => ['bool', 'object'=>'object'], + 'new' => ['bool', 'object='=>'null|object'], + ], 'SplFileInfo::getFileInfo' => [ 'old' => ['SplFileInfo', 'class='=>'class-string'], 'new' => ['SplFileInfo', 'class='=>'?class-string'], @@ -553,8 +560,8 @@ 'new' => ['array', 'array'=>'array', '...arrays='=>'array'], ], 'array_filter' => [ - 'old' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'], - 'new' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar|null', 'mode='=>'int'], + 'old' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed', 'mode='=>'int'], + 'new' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed|null', 'mode='=>'int'], ], 'array_key_exists' => [ 'old' => ['bool', 'key'=>'string|int', 'array'=>'array|object'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 18cc1ae4792..bfb2da40bb6 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -52,6 +52,7 @@ 'ReflectionFunctionAbstract::hasTentativeReturnType' => ['bool'], 'ReflectionFunctionAbstract::isStatic' => ['bool'], 'ReflectionObject::isEnum' => ['bool'], + 'ReflectionProperty::isReadonly' => ['bool'], 'sodium_crypto_stream_xchacha20' => ['non-empty-string', 'length'=>'positive-int', 'nonce'=>'non-empty-string', 'key'=>'non-empty-string'], 'sodium_crypto_stream_xchacha20_keygen' => ['non-empty-string'], 'sodium_crypto_stream_xchacha20_xor' => ['string', 'message'=>'string', 'nonce'=>'non-empty-string', 'key'=>'non-empty-string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 5ff4033aff0..afb21ba72df 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -3744,19 +3744,19 @@ 'MongoDB\BSON\Binary::getType' => ['int'], 'MongoDB\BSON\Binary::__toString' => ['string'], 'MongoDB\BSON\Binary::serialize' => ['string'], - 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Binary::unserialize' => ['void', 'data' => '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::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], 'MongoDB\BSON\Decimal128::serialize' => ['string'], - 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Document::fromBSON' => ['MongoDB\BSON\Document', 'bson' => 'string'], @@ -3768,13 +3768,17 @@ 'MongoDB\BSON\Document::toPHP' => ['object|array', 'typeMap=' => '?array'], 'MongoDB\BSON\Document::toCanonicalExtendedJSON' => ['string'], 'MongoDB\BSON\Document::toRelaxedExtendedJSON' => ['string'], + 'MongoDB\BSON\Document::offsetExists' => ['bool', 'offset' => 'mixed'], + 'MongoDB\BSON\Document::offsetGet' => ['mixed', 'offset' => 'mixed'], + 'MongoDB\BSON\Document::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], + 'MongoDB\BSON\Document::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\Document::__toString' => ['string'], 'MongoDB\BSON\Document::serialize' => ['string'], - 'MongoDB\BSON\Document::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Document::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::__construct' => ['void', 'value' => 'string|int'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], - 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Int64::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Iterator::current' => ['mixed'], 'MongoDB\BSON\Iterator::key' => ['string|int'], @@ -3786,22 +3790,22 @@ '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::unserialize' => ['void', 'data' => '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::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\MinKey::serialize' => ['string'], - 'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\MinKey::unserialize' => ['void', 'data' => '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::serialize' => ['string'], - 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], @@ -3810,30 +3814,35 @@ 'MongoDB\BSON\PackedArray::getIterator' => ['MongoDB\BSON\Iterator'], 'MongoDB\BSON\PackedArray::has' => ['bool', 'index' => 'int'], 'MongoDB\BSON\PackedArray::toPHP' => ['object|array', 'typeMap=' => '?array'], + 'MongoDB\BSON\PackedArray::offsetExists' => ['bool', 'offset' => 'mixed'], + 'MongoDB\BSON\PackedArray::offsetGet' => ['mixed', 'offset' => 'mixed'], + 'MongoDB\BSON\PackedArray::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], + 'MongoDB\BSON\PackedArray::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\PackedArray::__toString' => ['string'], 'MongoDB\BSON\PackedArray::serialize' => ['string'], - 'MongoDB\BSON\PackedArray::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\PackedArray::unserialize' => ['void', 'data' => 'string'], + 'MongoDB\BSON\Persistable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|array'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], '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::unserialize' => ['void', 'data' => '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\Serializable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], - 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Symbol::unserialize' => ['void', 'data' => '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::serialize' => ['string'], - 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], @@ -3842,13 +3851,13 @@ 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], - 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'data' => '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::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], @@ -3881,7 +3890,7 @@ 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], - 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\CursorId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], @@ -3950,6 +3959,7 @@ '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\LogSubscriber::log' => ['void', 'level' => 'int', 'domain' => 'string', 'message' => 'string'], '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'], @@ -3992,18 +4002,18 @@ '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::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], - 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'data' => '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::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], - 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'data' => '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'], @@ -4023,9 +4033,9 @@ 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], - 'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], + 'MongoDB\Driver\ServerApi::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ServerApi::serialize' => ['string'], - 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], 'MongoDB\Driver\ServerDescription::getHost' => ['string'], 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], @@ -4055,9 +4065,9 @@ '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::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], - 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], @@ -5070,93 +5080,6 @@ 'RarException::getTraceAsString' => ['string'], 'RarException::isUsingExceptions' => ['bool'], 'RarException::setUsingExceptions' => ['RarEntry', 'using_exceptions'=>'bool'], - 'RdKafka::addBrokers' => ['int', 'broker_list'=>'string'], - 'RdKafka::flush' => ['int', 'timeout_ms'=>'int'], - 'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], - 'RdKafka::getOutQLen' => ['int'], - 'RdKafka::newQueue' => ['RdKafka\Queue'], - 'RdKafka::newTopic' => ['RdKafka\Topic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], - 'RdKafka::poll' => ['void', 'timeout_ms'=>'int'], - 'RdKafka::setLogLevel' => ['void', 'level'=>'int'], - 'RdKafka\Conf::dump' => ['array'], - 'RdKafka\Conf::set' => ['void', 'name'=>'string', 'value'=>'string'], - 'RdKafka\Conf::setDefaultTopicConf' => ['void', 'topic_conf'=>'RdKafka\TopicConf'], - 'RdKafka\Conf::setDrMsgCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Conf::setErrorCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Conf::setRebalanceCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Conf::setStatsCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Consumer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], - 'RdKafka\Consumer::addBrokers' => ['int', 'broker_list'=>'string'], - 'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], - 'RdKafka\Consumer::getOutQLen' => ['int'], - 'RdKafka\Consumer::newQueue' => ['RdKafka\Queue'], - 'RdKafka\Consumer::newTopic' => ['RdKafka\ConsumerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], - 'RdKafka\Consumer::poll' => ['void', 'timeout_ms'=>'int'], - 'RdKafka\Consumer::setLogLevel' => ['void', 'level'=>'int'], - 'RdKafka\ConsumerTopic::__construct' => ['void'], - 'RdKafka\ConsumerTopic::consume' => ['RdKafka\Message', 'partition'=>'int', 'timeout_ms'=>'int'], - 'RdKafka\ConsumerTopic::consumeQueueStart' => ['void', 'partition'=>'int', 'offset'=>'int', 'queue'=>'RdKafka\Queue'], - 'RdKafka\ConsumerTopic::consumeStart' => ['void', 'partition'=>'int', 'offset'=>'int'], - 'RdKafka\ConsumerTopic::consumeStop' => ['void', 'partition'=>'int'], - 'RdKafka\ConsumerTopic::getName' => ['string'], - 'RdKafka\ConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], - 'RdKafka\KafkaConsumer::__construct' => ['void', 'conf'=>'RdKafka\Conf'], - 'RdKafka\KafkaConsumer::assign' => ['void', 'topic_partitions='=>'RdKafka\TopicPartition[]|null'], - 'RdKafka\KafkaConsumer::commit' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], - 'RdKafka\KafkaConsumer::commitAsync' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], - 'RdKafka\KafkaConsumer::consume' => ['RdKafka\Message', 'timeout_ms'=>'int'], - 'RdKafka\KafkaConsumer::getAssignment' => ['RdKafka\TopicPartition[]'], - 'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'], - 'RdKafka\KafkaConsumer::getSubscription' => ['array'], - 'RdKafka\KafkaConsumer::subscribe' => ['void', 'topics'=>'array'], - 'RdKafka\KafkaConsumer::unsubscribe' => ['void'], - 'RdKafka\KafkaConsumerTopic::getName' => ['string'], - 'RdKafka\KafkaConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], - 'RdKafka\Message::errstr' => ['string'], - 'RdKafka\Metadata::getBrokers' => ['RdKafka\Metadata\Collection'], - 'RdKafka\Metadata::getOrigBrokerId' => ['int'], - 'RdKafka\Metadata::getOrigBrokerName' => ['string'], - 'RdKafka\Metadata::getTopics' => ['RdKafka\Metadata\Collection|RdKafka\Metadata\Topic[]'], - 'RdKafka\Metadata\Collection::__construct' => ['void'], - 'RdKafka\Metadata\Collection::count' => ['int'], - 'RdKafka\Metadata\Collection::current' => ['mixed'], - 'RdKafka\Metadata\Collection::key' => ['mixed'], - 'RdKafka\Metadata\Collection::next' => ['void'], - 'RdKafka\Metadata\Collection::rewind' => ['void'], - 'RdKafka\Metadata\Collection::valid' => ['bool'], - 'RdKafka\Metadata\Partition::getErr' => ['mixed'], - 'RdKafka\Metadata\Partition::getId' => ['int'], - 'RdKafka\Metadata\Partition::getIsrs' => ['mixed'], - 'RdKafka\Metadata\Partition::getLeader' => ['mixed'], - 'RdKafka\Metadata\Partition::getReplicas' => ['mixed'], - 'RdKafka\Metadata\Topic::getErr' => ['mixed'], - 'RdKafka\Metadata\Topic::getPartitions' => ['RdKafka\Metadata\Partition[]'], - 'RdKafka\Metadata\Topic::getTopic' => ['string'], - 'RdKafka\Producer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], - 'RdKafka\Producer::addBrokers' => ['int', 'broker_list'=>'string'], - 'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], - 'RdKafka\Producer::getOutQLen' => ['int'], - 'RdKafka\Producer::newQueue' => ['RdKafka\Queue'], - 'RdKafka\Producer::newTopic' => ['RdKafka\ProducerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], - 'RdKafka\Producer::poll' => ['void', 'timeout_ms'=>'int'], - 'RdKafka\Producer::setLogLevel' => ['void', 'level'=>'int'], - 'RdKafka\ProducerTopic::__construct' => ['void'], - 'RdKafka\ProducerTopic::getName' => ['string'], - 'RdKafka\ProducerTopic::produce' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string'], - 'RdKafka\ProducerTopic::producev' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string', 'headers='=>'?array', 'timestamp_ms='=>'?int', 'opaque='=>'?string'], - 'RdKafka\Queue::__construct' => ['void'], - 'RdKafka\Queue::consume' => ['?RdKafka\Message', 'timeout_ms'=>'string'], - 'RdKafka\Topic::getName' => ['string'], - 'RdKafka\TopicConf::dump' => ['array'], - 'RdKafka\TopicConf::set' => ['void', 'name'=>'string', 'value'=>'string'], - 'RdKafka\TopicConf::setPartitioner' => ['void', 'partitioner'=>'int'], - 'RdKafka\TopicPartition::__construct' => ['void', 'topic'=>'string', 'partition'=>'int', 'offset='=>'int'], - 'RdKafka\TopicPartition::getOffset' => ['int'], - 'RdKafka\TopicPartition::getPartition' => ['int'], - 'RdKafka\TopicPartition::getTopic' => ['string'], - 'RdKafka\TopicPartition::setOffset' => ['void', 'offset'=>'string'], - 'RdKafka\TopicPartition::setPartition' => ['void', 'partition'=>'string'], - 'RdKafka\TopicPartition::setTopic' => ['void', 'topic_name'=>'string'], 'RecursiveArrayIterator::__construct' => ['void', 'array='=>'array|object', 'flags='=>'int'], 'RecursiveArrayIterator::append' => ['void', 'value'=>'mixed'], 'RecursiveArrayIterator::asort' => ['true', 'flags='=>'int'], @@ -9375,7 +9298,7 @@ 'array_diff_ukey\'1' => ['array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_fill' => ['array', 'start_index'=>'int', 'count'=>'int', 'value'=>'mixed'], 'array_fill_keys' => ['array', 'keys'=>'array', 'value'=>'mixed'], - 'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'], + 'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed', 'mode='=>'int'], 'array_flip' => ['array', 'array'=>'array'], 'array_intersect' => ['array', 'array'=>'array', '...arrays'=>'array'], 'array_intersect_assoc' => ['array', 'array'=>'array', '...arrays'=>'array'], @@ -13778,10 +13701,6 @@ 'rar_wrapper_cache_stats' => ['string'], 'rawurldecode' => ['string', 'string'=>'string'], 'rawurlencode' => ['string', 'string'=>'string'], - 'rd_kafka_err2str' => ['string', 'err'=>'int'], - 'rd_kafka_errno' => ['int'], - 'rd_kafka_errno2err' => ['int', 'errnox'=>'int'], - 'rd_kafka_offset_tail' => ['int', 'cnt'=>'int'], 'read_exif_data' => ['array', 'filename'=>'string', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'readdir' => ['string|false', 'dir_handle='=>'resource'], 'readfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], diff --git a/dictionaries/PropertyMap.php b/dictionaries/PropertyMap.php index 7f07ea657de..c5755235e91 100644 --- a/dictionaries/PropertyMap.php +++ b/dictionaries/PropertyMap.php @@ -384,7 +384,7 @@ 'items' => 'array', ], 'phpparser\\node\\expr\\shellexec' => [ - 'parts' => 'list', + 'parts' => 'list', ], 'phpparser\\node\\matcharm' => [ 'conds' => 'null|non-empty-list', diff --git a/dictionaries/scripts/update_signaturemap_from_other_tool.php b/dictionaries/scripts/update_signaturemap_from_other_tool.php index 21fb61166d5..d50e88ee0a8 100644 --- a/dictionaries/scripts/update_signaturemap_from_other_tool.php +++ b/dictionaries/scripts/update_signaturemap_from_other_tool.php @@ -30,7 +30,7 @@ $removed_foreign_functions ); -uksort($new_local, fn($a, $b) => strtolower($a) <=> strtolower($b)); +uksort($new_local, static fn($a, $b) => strtolower($a) <=> strtolower($b)); foreach ($new_local as $name => $data) { if (!is_array($data)) { diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md index 346b525f878..ba14e7d67c9 100644 --- a/docs/annotating_code/supported_annotations.md +++ b/docs/annotating_code/supported_annotations.md @@ -202,7 +202,7 @@ takesFoo(getFoo()); This provides the same, but for `false`. Psalm uses this internally for functions like `preg_replace`, which can return false if the given input has encoding errors, but where 99.9% of the time the function operates as expected. -### `@psalm-seal-properties`, `@psalm-no-seal-properties` +### `@psalm-seal-properties`, `@psalm-no-seal-properties`, `@seal-properties`, `@no-seal-properties` If you have a magic property getter/setter, you can use `@psalm-seal-properties` to instruct Psalm to disallow getting and setting any properties not contained in a list of `@property` (or `@property-read`/`@property-write`) annotations. This is automatically enabled with the configuration option `sealAllProperties` and can be disabled for a class with `@psalm-no-seal-properties` @@ -211,7 +211,7 @@ This is automatically enabled with the configuration option `sealAllProperties` bar = 5; // this call fails ``` -### `@psalm-seal-methods`, `@psalm-no-seal-methods` +### `@psalm-seal-methods`, `@psalm-no-seal-methods`, `@seal-methods`, `@no-seal-methods` If you have a magic method caller, you can use `@psalm-seal-methods` to instruct Psalm to disallow calling any methods not contained in a list of `@method` annotations. This is automatically enabled with the configuration option `sealAllMethods` and can be disabled for a class with `@psalm-no-seal-methods` @@ -236,7 +236,7 @@ This is automatically enabled with the configuration option `sealAllMethods` and + + + +``` diff --git a/docs/security_analysis/annotations.md b/docs/security_analysis/annotations.md index dd552743187..d3650922c9e 100644 --- a/docs/security_analysis/annotations.md +++ b/docs/security_analysis/annotations.md @@ -19,3 +19,7 @@ See [Unescaping statements](avoiding_false_negatives.md#unescaping-statements). ## `@psalm-taint-specialize` See [Specializing taints in functions](avoiding_false_positives.md#specializing-taints-in-functions) and [Specializing taints in classes](avoiding_false_positives.md#specializing-taints-in-classes). + +## `@psalm-flow [proxy ] ( , [ , ] ) [ -> return ]` + +See [Taint Flow](taint_flow.md#optimized-taint-flow) diff --git a/docs/security_analysis/taint_flow.md b/docs/security_analysis/taint_flow.md new file mode 100644 index 00000000000..2c77c07950b --- /dev/null +++ b/docs/security_analysis/taint_flow.md @@ -0,0 +1,66 @@ +# Taint Flow + +## Optimized Taint Flow + +When dealing with frameworks, keeping track of the data flow might involve different layers +and even other 3rd party components. Using the `@psalm-flow` annotation allows PsalmPHP to +take a shortcut and to make a tainted data flow more explicit. + +### Proxy hint + +```php + return + */ +function inputOutputHandler(string $value, string ...$items): string +{ + // lots of complicated magic +} + +echo inputOutputHandler('first', 'second', $_GET['malicious'] ?? ''); +``` + +The example above states, that the function parameters `$value` and `$items` are reflected +again in the return value. Thus, in case any of the input parameters to the function +`inputOutputHandler` is tainted, then the resulting return value is as well. In this +example `TaintedHtml` would be detected due to using `echo`. + +### Combined proxy & return value hint + +```php + return + */ +function handleInput(string $value, string ...$items): string +{ + // lots of complicated magic +} + +echo handleInput($_GET['malicious'] ?? ''); +``` + +The example above combines both previous examples and shows, that the `@psalm-flow` annotation +can be used multiple times. Here, it would lead to detecting both `TaintedHtml` and `TaintedShell`. diff --git a/examples/TemplateScanner.php b/examples/TemplateScanner.php index 681bf61bdc7..254b06dc338 100644 --- a/examples/TemplateScanner.php +++ b/examples/TemplateScanner.php @@ -14,7 +14,7 @@ use function preg_match; use function trim; -class TemplateScanner extends Psalm\Internal\Scanner\FileScanner +final class TemplateScanner extends Psalm\Internal\Scanner\FileScanner { final public const VIEW_CLASS = 'Your\\View\\Class'; diff --git a/examples/plugins/ClassUnqualifier.php b/examples/plugins/ClassUnqualifier.php index d2b05332329..44104376292 100644 --- a/examples/plugins/ClassUnqualifier.php +++ b/examples/plugins/ClassUnqualifier.php @@ -44,7 +44,7 @@ public static function afterClassLikeExistenceCheck( $new_candidate_type = implode( '', array_map( - fn($f) => $f[0], + static fn($f) => $f[0], $type_tokens, ), ); diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5bb552ae813..f00d500b709 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -16,6 +16,9 @@ $deprecated_element_xml + + addAttribute + $this diff --git a/psalm.xml.dist b/psalm.xml.dist index 6400089acb8..f5df5950da4 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -14,6 +14,7 @@ errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" findUnusedBaselineEntry="true" + findUnusedIssueHandlerSuppression="true" > @@ -65,24 +66,6 @@ - - - - - - - - - - - - - - - - - - @@ -106,12 +89,6 @@ - - - - - - diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 1fc0d7f165f..32e20c58bf8 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -20,6 +20,7 @@ use Psalm\CodeLocation\Raw; use Psalm\Exception\UnanalyzedFileException; use Psalm\Exception\UnpopulatedClasslikeException; +use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; @@ -71,6 +72,7 @@ use UnexpectedValueException; use function array_combine; +use function array_key_exists; use function array_pop; use function array_reverse; use function array_values; @@ -1430,6 +1432,9 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio $offset = $position->toOffset($file_contents); + $literal_part = $this->getBeginedLiteralPart($file_path, $position); + $begin_literal_offset = $offset - strlen($literal_part); + [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); if (!$reference_map && !$type_map) { @@ -1464,7 +1469,7 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio } } - if ($offset - $end_pos === 2 || $offset - $end_pos === 3) { + if ($begin_literal_offset - $end_pos === 2) { $candidate_gap = substr($file_contents, $end_pos, 2); if ($candidate_gap === '->' || $candidate_gap === '::') { @@ -1489,6 +1494,11 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio return [$possible_reference, '::', $offset]; } + if ($offset <= $end_pos && substr($file_contents, $begin_literal_offset - 2, 2) === '::') { + $class_name = explode('::', $possible_reference)[0]; + return [$class_name, '::', $offset]; + } + // Only continue for references that are partial / don't exist. if ($possible_reference[0] !== '*') { continue; @@ -1504,6 +1514,23 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio return null; } + public function getBeginedLiteralPart(string $file_path, Position $position): string + { + $is_open = $this->file_provider->isOpen($file_path); + + if (!$is_open) { + throw new UnanalyzedFileException($file_path . ' is not open'); + } + + $file_contents = $this->getFileContents($file_path); + + $offset = $position->toOffset($file_contents); + + preg_match('/\$?\w+$/', substr($file_contents, 0, $offset), $matches); + + return $matches[0] ?? ''; + } + public function getTypeContextAtPosition(string $file_path, Position $position): ?Union { $file_contents = $this->getFileContents($file_path); @@ -1530,13 +1557,26 @@ public function getTypeContextAtPosition(string $file_path, Position $position): } /** + * @param list $allow_visibilities + * @param list $ignore_fq_class_names * @return list */ public function getCompletionItemsForClassishThing( string $type_string, string $gap, bool $snippets_supported = false, + array $allow_visibilities = null, + array $ignore_fq_class_names = [], ): array { + if ($allow_visibilities === null) { + $allow_visibilities = [ + ClassLikeAnalyzer::VISIBILITY_PUBLIC, + ClassLikeAnalyzer::VISIBILITY_PROTECTED, + ClassLikeAnalyzer::VISIBILITY_PRIVATE, + ]; + } + $allow_visibilities[] = null; + $completion_items = []; $type = Type::parseString($type_string); @@ -1546,9 +1586,32 @@ public function getCompletionItemsForClassishThing( try { $class_storage = $this->classlike_storage_provider->get($atomic_type->value); - foreach ($class_storage->appearing_method_ids as $declaring_method_id) { - $method_storage = $this->methods->getStorage($declaring_method_id); + $method_storages = []; + foreach ($class_storage->declaring_method_ids as $declaring_method_id) { + try { + $method_storages[] = $this->methods->getStorage($declaring_method_id); + } catch (UnexpectedValueException $e) { + error_log($e->getMessage()); + } + } + if ($gap === '->') { + $method_storages += $class_storage->pseudo_methods; + } + if ($gap === '::') { + $method_storages += $class_storage->pseudo_static_methods; + } + $had = []; + foreach ($method_storages as $method_storage) { + if (!in_array($method_storage->visibility, $allow_visibilities)) { + continue; + } + if ($method_storage->cased_name !== null) { + if (array_key_exists($method_storage->cased_name, $had)) { + continue; + } + $had[$method_storage->cased_name] = true; + } if ($method_storage->is_static || $gap === '->') { $completion_item = new CompletionItem( $method_storage->cased_name, @@ -1577,43 +1640,51 @@ public function getCompletionItemsForClassishThing( } } - $pseudo_property_types = []; - foreach ($class_storage->pseudo_property_get_types as $property_name => $type) { - $pseudo_property_types[$property_name] = new CompletionItem( - str_replace('$', '', $property_name), - CompletionItemKind::PROPERTY, - $type->__toString(), - null, - '1', //sort text - str_replace('$', '', $property_name), - ($gap === '::' ? '$' : '') . + if ($gap === '->') { + $pseudo_property_types = []; + foreach ($class_storage->pseudo_property_get_types as $property_name => $type) { + $pseudo_property_types[$property_name] = new CompletionItem( str_replace('$', '', $property_name), - ); - } - - foreach ($class_storage->pseudo_property_set_types as $property_name => $type) { - $pseudo_property_types[$property_name] = new CompletionItem( - str_replace('$', '', $property_name), - CompletionItemKind::PROPERTY, - $type->__toString(), - null, - '1', - str_replace('$', '', $property_name), - ($gap === '::' ? '$' : '') . + CompletionItemKind::PROPERTY, + $type->__toString(), + null, + '1', //sort text str_replace('$', '', $property_name), - ); + str_replace('$', '', $property_name), + ); + } + + foreach ($class_storage->pseudo_property_set_types as $property_name => $type) { + $pseudo_property_types[$property_name] = new CompletionItem( + str_replace('$', '', $property_name), + CompletionItemKind::PROPERTY, + $type->__toString(), + null, + '1', + str_replace('$', '', $property_name), + str_replace('$', '', $property_name), + ); + } + + $completion_items = [...$completion_items, ...array_values($pseudo_property_types)]; } - $completion_items = [...$completion_items, ...array_values($pseudo_property_types)]; - foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { - $property_storage = $this->properties->getStorage( - $declaring_class . '::$' . $property_name, - ); + try { + $property_storage = $this->properties->getStorage( + $declaring_class . '::$' . $property_name, + ); + } catch (UnexpectedValueException $e) { + error_log($e->getMessage()); + continue; + } - if ($property_storage->is_static || $gap === '->') { + if (!in_array($property_storage->visibility, $allow_visibilities)) { + continue; + } + if ($property_storage->is_static === ($gap === '::')) { $completion_items[] = new CompletionItem( - '$' . $property_name, + $property_name, CompletionItemKind::PROPERTY, $property_storage->getInfo(), $property_storage->description, @@ -1635,6 +1706,22 @@ public function getCompletionItemsForClassishThing( $const_name, ); } + + if ($gap === '->') { + foreach ($class_storage->namedMixins as $mixin) { + if (in_array($mixin->value, $ignore_fq_class_names)) { + continue; + } + $mixin_completion_items = $this->getCompletionItemsForClassishThing( + $mixin->value, + $gap, + $snippets_supported, + [ClassLikeAnalyzer::VISIBILITY_PUBLIC], + [$type_string, ...$ignore_fq_class_names], + ); + $completion_items = [...$completion_items, ...$mixin_completion_items]; + } + } } catch (Exception $e) { error_log($e->getMessage()); continue; @@ -1645,6 +1732,26 @@ public function getCompletionItemsForClassishThing( return $completion_items; } + /** + * @param list $items + * @return list + */ + public function filterCompletionItemsByBeginLiteralPart(array $items, string $literal_part): array + { + if (!$literal_part) { + return $items; + } + + $res = []; + foreach ($items as $item) { + if ($item->insertText && strpos($item->insertText, $literal_part) === 0) { + $res[] = $item; + } + } + + return $res; + } + /** * @return list */ diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 0ce775939da..bc69bd9f79d 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -23,6 +23,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FileAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\CliUtils; use Psalm\Internal\Composer; use Psalm\Internal\EventDispatcher; use Psalm\Internal\IncludeCollector; @@ -156,7 +157,6 @@ final class Config 'MixedArrayTypeCoercion', 'MixedAssignment', 'MixedFunctionCall', - 'MixedInferredReturnType', 'MixedMethodCall', 'MixedOperand', 'MixedPropertyFetch', @@ -230,6 +230,8 @@ final class Config */ public string $base_dir; + public ?string $source_filename = null; + /** * The PHP version to assume as declared in the config file */ @@ -369,6 +371,8 @@ final class Config public bool $find_unused_baseline_entry = true; + public bool $find_unused_issue_handler_suppression = true; + public bool $run_taint_analysis = false; public bool $use_phpstorm_meta_path = true; @@ -477,6 +481,7 @@ final class Config "mysqli" => null, "pdo" => null, "random" => null, + "rdkafka" => null, "redis" => null, "simplexml" => null, "soap" => null, @@ -935,6 +940,7 @@ private static function fromXmlAndPaths( 'allowNamedArgumentCalls' => 'allow_named_arg_calls', 'findUnusedPsalmSuppress' => 'find_unused_psalm_suppress', 'findUnusedBaselineEntry' => 'find_unused_baseline_entry', + 'findUnusedIssueHandlerSuppression' => 'find_unused_issue_handler_suppression', 'reportInfo' => 'report_info', 'restrictReturnTypes' => 'restrict_return_types', 'limitMethodComplexity' => 'limit_method_complexity', @@ -950,6 +956,7 @@ private static function fromXmlAndPaths( } } + $config->source_filename = $config_path; if ($config->resolve_from_config_file) { $config->base_dir = $base_dir; } else { @@ -1137,6 +1144,66 @@ private static function fromXmlAndPaths( $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true); } + // any paths passed via CLI should be added to the projectFiles + // as they're getting analyzed like if they are part of the project + // ProjectAnalyzer::getInstance()->check_paths_files is not populated at this point in time + $paths_to_check = CliUtils::getPathsToCheck(null); + if ($paths_to_check !== null) { + $paths_to_add_to_project_files = array(); + foreach ($paths_to_check as $path) { + // if we have an .xml arg here, the files passed are invalid + // valid cases (in which we don't want to add CLI passed files to projectFiles though) + // are e.g. if running phpunit tests for psalm itself + if (substr($path, -4) === '.xml') { + $paths_to_add_to_project_files = array(); + break; + } + + // we need an absolute path for checks + if ($path[0] !== '/' && DIRECTORY_SEPARATOR === '/') { + $prospective_path = $base_dir . DIRECTORY_SEPARATOR . $path; + } else { + $prospective_path = $path; + } + + // will report an error when config is loaded anyway + if (!file_exists($prospective_path)) { + continue; + } + + if ($config->isInProjectDirs($prospective_path)) { + continue; + } + + $paths_to_add_to_project_files[] = $prospective_path; + } + + if ($paths_to_add_to_project_files !== array() && !isset($config_xml->projectFiles)) { + if ($config_xml === null) { + $config_xml = new SimpleXMLElement(''); + } + $config_xml->addChild('projectFiles'); + } + + if ($paths_to_add_to_project_files !== array() && isset($config_xml->projectFiles)) { + foreach ($paths_to_add_to_project_files as $path) { + if (is_dir($path)) { + $child = $config_xml->projectFiles->addChild('directory'); + } else { + $child = $config_xml->projectFiles->addChild('file'); + } + + $child->addAttribute('name', $path); + } + + $config->project_files = ProjectFileFilter::loadFromXMLElement( + $config_xml->projectFiles, + $base_dir, + true, + ); + } + } + if (isset($config_xml->extraFiles)) { $config->extra_files = ProjectFileFilter::loadFromXMLElement($config_xml->extraFiles, $base_dir, true); } @@ -1311,6 +1378,12 @@ public function setComposerClassLoader(?ClassLoader $loader = null): void $this->composer_class_loader = $loader; } + /** @return array */ + public function getIssueHandlers(): array + { + return $this->issue_handlers; + } + public function setAdvancedErrorLevel(string $issue_key, array $config, ?string $default_error_level = null): void { $this->issue_handlers[$issue_key] = new IssueHandler(); @@ -1858,6 +1931,30 @@ public static function getParentIssueType(string $issue_type): ?string return null; } + /** @return array{type: string, index: int, count: int}[] */ + public function getIssueHandlerSuppressions(): array + { + $suppressions = []; + foreach ($this->issue_handlers as $key => $handler) { + foreach ($handler->getFilters() as $index => $filter) { + $suppressions[] = [ + 'type' => $key, + 'index' => $index, + 'count' => $filter->suppressions, + ]; + } + } + return $suppressions; + } + + /** @param array{type: string, index: int, count: int}[] $filters */ + public function combineIssueHandlerSuppressions(array $filters): void + { + foreach ($filters as $filter) { + $this->issue_handlers[$filter['type']]->getFilters()[$filter['index']]->suppressions += $filter['count']; + } + } + public function getReportingLevelForFile(string $issue_type, string $file_path): string { if (isset($this->issue_handlers[$issue_type])) { diff --git a/src/Psalm/Config/ErrorLevelFileFilter.php b/src/Psalm/Config/ErrorLevelFileFilter.php index 1778ccfae35..de3ed732c19 100644 --- a/src/Psalm/Config/ErrorLevelFileFilter.php +++ b/src/Psalm/Config/ErrorLevelFileFilter.php @@ -15,6 +15,8 @@ final class ErrorLevelFileFilter extends FileFilter { private string $error_level = ''; + public int $suppressions = 0; + public static function loadFromArray( array $config, string $base_dir, diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php index a5af5aefe4b..aba87f0232b 100644 --- a/src/Psalm/Config/IssueHandler.php +++ b/src/Psalm/Config/IssueHandler.php @@ -25,7 +25,7 @@ final class IssueHandler private string $error_level = Config::REPORT_ERROR; /** - * @var array + * @var list */ private array $custom_levels = []; @@ -50,6 +50,12 @@ public static function loadFromXMLElement(SimpleXMLElement $e, string $base_dir) return $handler; } + /** @return list */ + public function getFilters(): array + { + return $this->custom_levels; + } + public function setCustomLevels(array $customLevels, string $base_dir): void { /** @var array $customLevel */ @@ -71,6 +77,7 @@ public function getReportingLevelForFile(string $file_path): string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allows($file_path)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -82,6 +89,7 @@ public function getReportingLevelForClass(string $fq_classlike_name): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsClass($fq_classlike_name)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -93,6 +101,7 @@ public function getReportingLevelForMethod(string $method_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsMethod(strtolower($method_id))) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -115,6 +124,7 @@ public function getReportingLevelForArgument(string $function_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsMethod(strtolower($function_id))) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -126,6 +136,7 @@ public function getReportingLevelForProperty(string $property_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsProperty($property_id)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -137,6 +148,7 @@ public function getReportingLevelForClassConstant(string $constant_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsClassConstant($constant_id)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -148,6 +160,7 @@ public function getReportingLevelForVariable(string $var_name): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsVariable($var_name)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index f6d03381690..ae4b2480902 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -258,8 +258,6 @@ public function analyze( IssueBuffer::maybeAdd($docblock_issue); } - $classlike_storage_provider = $codebase->classlike_storage_provider; - $parent_fq_class_name = $this->parent_fq_class_name; if ($class instanceof PhpParser\Node\Stmt\Class_ && $class->extends && $parent_fq_class_name) { @@ -626,43 +624,7 @@ public function analyze( } $pseudo_methods = $storage->pseudo_methods + $storage->pseudo_static_methods; - - foreach ($pseudo_methods as $pseudo_method_name => $pseudo_method_storage) { - $pseudo_method_id = new MethodIdentifier( - $this->fq_class_name, - $pseudo_method_name, - ); - - $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id); - - if ($overridden_method_ids - && $pseudo_method_name !== '__construct' - && $pseudo_method_storage->location - ) { - foreach ($overridden_method_ids as $overridden_method_id) { - $parent_method_storage = $codebase->methods->getStorage($overridden_method_id); - - $overridden_fq_class_name = $overridden_method_id->fq_class_name; - - $parent_storage = $classlike_storage_provider->get($overridden_fq_class_name); - - MethodComparator::compare( - $codebase, - null, - $storage, - $parent_storage, - $pseudo_method_storage, - $parent_method_storage, - $this->fq_class_name, - $pseudo_method_storage->visibility ?: 0, - $storage->location ?: $pseudo_method_storage->location, - $storage->suppressed_issues, - true, - false, - ); - } - } - } + MethodComparator::comparePseudoMethods($pseudo_methods, $this->fq_class_name, $codebase, $storage); $event = new AfterClassLikeAnalysisEvent( $class, diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 40a6f778b25..47e716f2225 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -704,7 +704,7 @@ protected function checkTemplateParams( && $storage->template_types && $storage->template_covariants && ($local_offset - = array_search($t->param_name, array_keys($storage->template_types))) + = array_search($t->param_name, array_keys($storage->template_types), true)) !== false && !empty($storage->template_covariants[$local_offset]) ) { diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 8f0264e7caf..572df0f5078 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -261,9 +261,10 @@ private static function decorateVarDocblockComment( public static function sanitizeDocblockType(string $docblock_type): string { $docblock_type = (string) preg_replace('@^[ \t]*\*@m', '', $docblock_type); - $docblock_type = (string) preg_replace('/,\n\s+}/', '}', $docblock_type); + $docblock_type = (string) preg_replace('/,[\n\s]+}/', '}', $docblock_type); + $docblock_type = (string) preg_replace('/[ \t]+/', ' ', $docblock_type); - return str_replace("\n", '', $docblock_type); + return trim(str_replace("\n", '', $docblock_type)); } /** @@ -431,6 +432,10 @@ public static function getVarComments( $var_comments = []; try { + $file_path = $statements_analyzer->getRootFilePath(); + $file_storage_provider = $codebase->file_storage_provider; + $file_storage = $file_storage_provider->get($file_path); + $var_comments = $codebase->config->disable_var_parsing ? [] : self::arrayToDocblocks( @@ -439,6 +444,7 @@ public static function getVarComments( $statements_analyzer->getSource(), $statements_analyzer->getSource()->getAliases(), $statements_analyzer->getSource()->getTemplateTypeMap(), + $file_storage->type_aliases, ); } catch (IncorrectDocblockException $e) { IssueBuffer::maybeAdd( diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 9f8d8864525..65d38b25096 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -40,7 +40,6 @@ use Psalm\Issue\MismatchingDocblockReturnType; use Psalm\Issue\MissingClosureReturnType; use Psalm\Issue\MissingReturnType; -use Psalm\Issue\MixedInferredReturnType; use Psalm\Issue\MixedReturnTypeCoercion; use Psalm\Issue\MoreSpecificReturnType; use Psalm\Issue\UnresolvableConstant; @@ -516,17 +515,6 @@ public static function verifyReturnType( } if ($inferred_return_type->hasMixed()) { - if (IssueBuffer::accepts( - new MixedInferredReturnType( - 'Could not verify return type \'' . $declared_return_type . '\' for ' . - $cased_method_id, - $return_type_location, - ), - $suppressed_issues, - )) { - return false; - } - return null; } diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index 2a2ec630c34..5e3f4151fb7 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -217,6 +217,10 @@ public function analyze(): void } } + $pseudo_methods = $class_storage->pseudo_methods + $class_storage->pseudo_static_methods; + + MethodComparator::comparePseudoMethods($pseudo_methods, $this->fq_class_name, $codebase, $class_storage); + $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider()); $statements_analyzer->analyze($member_stmts, $interface_context, null, true); diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index e9eab0707dc..84cd30b3880 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -9,6 +9,7 @@ use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Config; +use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\PhpVisitor\ParamReplacementVisitor; @@ -23,6 +24,8 @@ use Psalm\Issue\LessSpecificImplementedReturnType; use Psalm\Issue\MethodSignatureMismatch; use Psalm\Issue\MethodSignatureMustProvideReturnType; +use Psalm\Issue\MismatchingDocblockParamType; +use Psalm\Issue\MismatchingDocblockReturnType; use Psalm\Issue\MissingImmutableAnnotation; use Psalm\Issue\MoreSpecificImplementedParamType; use Psalm\Issue\OverriddenMethodAccess; @@ -39,6 +42,7 @@ use Psalm\Type\Union; use function array_filter; +use function count; use function in_array; use function str_starts_with; use function strtolower; @@ -118,13 +122,13 @@ public static function compare( ); } + // CallMapHandler needed due to https://github.com/vimeo/psalm/issues/10378 if (!$guide_classlike_storage->user_defined && $implementer_classlike_storage->user_defined && $codebase->analysis_php_version_id >= 8_01_00 - && ($guide_method_storage->return_type + && (($guide_method_storage->return_type && InternalCallMapHandler::inCallMap($cased_guide_method_id)) || $guide_method_storage->signature_return_type - ) - && !$implementer_method_storage->signature_return_type + ) && !$implementer_method_storage->signature_return_type && !array_filter( $implementer_method_storage->attributes, static fn(AttributeStorage $s): bool => $s->fq_class_name === 'ReturnTypeWillChange', @@ -132,7 +136,7 @@ public static function compare( ) { IssueBuffer::maybeAdd( new MethodSignatureMustProvideReturnType( - 'Method ' . $cased_implementer_method_id . ' must have a return type signature!', + 'Method ' . $cased_implementer_method_id . ' must have a return type signature', $implementer_method_storage->location ?: $code_location, ), $suppressed_issues + $implementer_classlike_storage->suppressed_issues, @@ -202,8 +206,8 @@ public static function compare( } if (($guide_classlike_storage->is_interface - || $guide_classlike_storage->preserve_constructor_signature - || $implementer_method_storage->cased_name !== '__construct') + || $guide_classlike_storage->preserve_constructor_signature + || $implementer_method_storage->cased_name !== '__construct') && $implementer_method_storage->required_param_count > $guide_method_storage->required_param_count ) { if ($implementer_method_storage->cased_name !== '__construct') { @@ -237,6 +241,56 @@ public static function compare( return null; } + /** + * @param array $pseudo_methods + */ + public static function comparePseudoMethods( + array $pseudo_methods, + string $fq_class_name, + Codebase $codebase, + ClassLikeStorage $class_storage, + ): void { + foreach ($pseudo_methods as $pseudo_method_name => $pseudo_method_storage) { + $pseudo_method_id = new MethodIdentifier( + $fq_class_name, + $pseudo_method_name, + ); + + $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id); + if (isset($class_storage->methods[$pseudo_method_id->method_name])) { + $overridden_method_ids[$class_storage->name] = $pseudo_method_id; + } + + if ($overridden_method_ids + && $pseudo_method_name !== '__construct' + && $pseudo_method_storage->location + ) { + foreach ($overridden_method_ids as $overridden_method_id) { + $parent_method_storage = $codebase->methods->getStorage($overridden_method_id); + + $overridden_fq_class_name = $overridden_method_id->fq_class_name; + + $parent_storage = $codebase->classlike_storage_provider->get($overridden_fq_class_name); + + self::compare( + $codebase, + null, + $class_storage, + $parent_storage, + $pseudo_method_storage, + $parent_method_storage, + $fq_class_name, + $pseudo_method_storage->visibility ?: 0, + $class_storage->location ?: $pseudo_method_storage->location, + $class_storage->suppressed_issues, + true, + false, + ); + } + } + } + } + /** * @param string[] $suppressed_issues */ @@ -362,10 +416,20 @@ private static function compareMethodParams( CodeLocation $code_location, array $suppressed_issues, ): void { + // ignore errors from stubbed/out of project files + $config = Config::getInstance(); + if (!$implementer_classlike_storage->user_defined + && (!$implementer_param->location + || !$config->isInProjectDirs( + $implementer_param->location->file_path, + ) + )) { + return; + } + if ($prevent_method_signature_mismatch) { if (!$guide_classlike_storage->user_defined - && $guide_param->type - ) { + && $guide_param->type) { $implementer_param_type = $implementer_param->signature_type; $guide_param_signature_type = $guide_param->type; @@ -387,8 +451,6 @@ private static function compareMethodParams( && !$guide_param->type->from_docblock && ($implementer_param_type || $guide_param_signature_type) ) { - $config = Config::getInstance(); - if ($implementer_param_type && (!$guide_param_signature_type || strtolower($implementer_param_type->getId()) @@ -439,11 +501,8 @@ private static function compareMethodParams( } } - $config = Config::getInstance(); - if ($guide_param->name !== $implementer_param->name && $guide_method_storage->allow_named_arg_calls - && $guide_classlike_storage->user_defined && $implementer_classlike_storage->user_defined && $implementer_param->location && $guide_method_storage->cased_name @@ -452,7 +511,10 @@ private static function compareMethodParams( $implementer_param->location->file_path, ) ) { - if ($config->allow_named_arg_calls + if (!$guide_classlike_storage->user_defined && $i === 0 && count($guide_method_storage->params) < 2) { + // if it's third party defined and a single arg, renaming is unnecessary + // if we still want to psalter it, move this if and change the else below to elseif + } elseif ($config->allow_named_arg_calls || ($guide_classlike_storage->location && !$config->isInProjectDirs($guide_classlike_storage->location->file_path) ) @@ -494,6 +556,7 @@ private static function compareMethodParams( if ($guide_classlike_storage->user_defined && $implementer_param->signature_type + && $guide_param->signature_type ) { self::compareMethodSignatureParams( $codebase, @@ -534,8 +597,6 @@ private static function compareMethodParams( } if ($implementer_param->by_ref !== $guide_param->by_ref) { - $config = Config::getInstance(); - IssueBuffer::maybeAdd( new MethodSignatureMismatch( 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' is' . @@ -586,6 +647,50 @@ private static function compareMethodSignatureParams( ) : null; + // CallMapHandler needed due to https://github.com/vimeo/psalm/issues/10378 + if (!$guide_param->signature_type + && $guide_param->type + && InternalCallMapHandler::inCallMap($cased_guide_method_id)) { + $guide_method_storage_param_type = TypeExpander::expandUnion( + $codebase, + $guide_param->type, + $guide_classlike_storage->is_trait && $guide_method_storage->abstract + ? $implementer_classlike_storage->name + : $guide_classlike_storage->name, + $guide_classlike_storage->is_trait && $guide_method_storage->abstract + ? $implementer_classlike_storage->name + : $guide_classlike_storage->name, + $guide_classlike_storage->is_trait && $guide_method_storage->abstract + ? $implementer_classlike_storage->parent_class + : $guide_classlike_storage->parent_class, + ); + + $builder = $guide_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { + if ($t instanceof TTemplateParam) { + $builder->removeType($k); + + foreach ($t->as->getAtomicTypes() as $as_t) { + $builder->addType($as_t); + } + } + } + + if ($builder->hasMixed()) { + foreach ($builder->getAtomicTypes() as $k => $_) { + if ($k !== 'mixed') { + $builder->removeType($k); + } + } + } + $guide_method_storage_param_type = $builder->freeze(); + unset($builder); + + if (!$guide_method_storage_param_type->hasMixed() || $codebase->analysis_php_version_id >= 8_00_00) { + $guide_param_signature_type = $guide_method_storage_param_type; + } + } + $implementer_param_signature_type = TypeExpander::expandUnion( $codebase, $implementer_param_signature_type, @@ -822,6 +927,18 @@ private static function compareMethodDocblockParams( ), $suppressed_issues + $implementer_classlike_storage->suppressed_issues, ); + } elseif ($guide_class_name == $implementer_called_class_name) { + IssueBuffer::maybeAdd( + new MismatchingDocblockParamType( + 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id + . ' has wrong type \'' . + $implementer_method_storage_param_type->getId() . '\' in @method annotation, expecting \'' . + $guide_method_storage_param_type->getId() . '\'', + $implementer_method_storage->params[$i]->location + ?: $code_location, + ), + $suppressed_issues + $implementer_classlike_storage->suppressed_issues, + ); } else { IssueBuffer::maybeAdd( new ImplementedParamTypeMismatch( @@ -901,12 +1018,18 @@ private static function compareMethodSignatureReturnTypes( ); if (!$is_contained_by) { - if ($codebase->analysis_php_version_id >= 8_00_00 - || $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait - || !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits) - || $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name - || (!$implementer_method_storage->abstract - && !$guide_method_storage->abstract) + if ($implementer_signature_return_type === null + && array_filter( + $implementer_method_storage->attributes, + static fn(AttributeStorage $s): bool => $s->fq_class_name === 'ReturnTypeWillChange', + )) { + // no error if return type will change and no signature set at all + } elseif ($codebase->analysis_php_version_id >= 8_00_00 + || $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait + || !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits) + || $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name + || (!$implementer_method_storage->abstract + && !$guide_method_storage->abstract) ) { IssueBuffer::maybeAdd( new MethodSignatureMismatch( @@ -1047,6 +1170,17 @@ private static function compareMethodDocblockReturnTypes( ), $suppressed_issues + $implementer_classlike_storage->suppressed_issues, ); + } elseif ($guide_class_name == $implementer_called_class_name) { + IssueBuffer::maybeAdd( + new MismatchingDocblockReturnType( + 'The inherited return type \'' . $guide_method_storage_return_type->getId() + . '\' for ' . $cased_guide_method_id . ' is different to the corresponding ' + . '@method annotation \'' . $implementer_method_storage_return_type->getId() . '\'', + $implementer_method_storage->return_type_location + ?: $code_location, + ), + $suppressed_issues + $implementer_classlike_storage->suppressed_issues, + ); } else { IssueBuffer::maybeAdd( new ImplementedReturnTypeMismatch( diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index fe12ae02391..2be20c66fe2 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1027,6 +1027,9 @@ public function checkFile(string $file_path): void */ public function checkPaths(array $paths_to_check): void { + $this->progress->write($this->generatePHPVersionMessage()); + $this->progress->startScanningFiles(); + $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); $this->visitAutoloadFiles(); @@ -1046,9 +1049,6 @@ public function checkPaths(array $paths_to_check): void $this->file_reference_provider->loadReferenceCache(); - $this->progress->write($this->generatePHPVersionMessage()); - $this->progress->startScanningFiles(); - $this->config->initializePlugins($this); diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index c11dba95c61..d71628cdfb9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -1091,7 +1091,7 @@ private static function getExtendedType( ): ?Union { if ($calling_class === $template_class) { if (isset($class_template_types[$template_name]) && $calling_type_params) { - $offset = array_search($template_name, array_keys($class_template_types)); + $offset = array_search($template_name, array_keys($class_template_types), true); if ($offset !== false && isset($calling_type_params[$offset])) { return $calling_type_params[$offset]; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 7434148b3c2..2e28081e8e5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -530,21 +530,23 @@ public static function analyze( } if ($context->vars_in_scope[$var_id]->isNever()) { - if (IssueBuffer::accepts( + if (!IssueBuffer::accepts( new NoValue( 'All possible types for this assignment were invalidated - This may be dead code', new CodeLocation($statements_analyzer->getSource(), $assign_var), ), $statements_analyzer->getSuppressedIssues(), )) { - return null; - } - - $context->vars_in_scope[$var_id] = Type::getNever(); - - $context->inside_assignment = $was_in_assignment; + // if the error is suppressed, do not treat it as never anymore + $new_mutable = $context->vars_in_scope[$var_id]->getBuilder()->addType(new TMixed); + $new_mutable->removeType('never'); + $context->vars_in_scope[$var_id] = $new_mutable->freeze(); + $context->has_returned = false; + } else { + $context->inside_assignment = $was_in_assignment; - return $context->vars_in_scope[$var_id]; + return $context->vars_in_scope[$var_id]; + } } if ($statements_analyzer->data_flow_graph) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 288c3e16cfd..04e1c9e2e48 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -24,6 +24,8 @@ use Psalm\Issue\PossiblyNullOperand; use Psalm\Issue\StringIncrement; use Psalm\IssueBuffer; +use Psalm\Node\Expr\BinaryOp\VirtualMinus; +use Psalm\Node\Expr\BinaryOp\VirtualPlus; use Psalm\StatementsSource; use Psalm\Type; use Psalm\Type\Atomic; @@ -820,6 +822,28 @@ private static function analyzeOperands( $result_type = Type::getInt(); } } + } elseif ($parent instanceof VirtualPlus || $parent instanceof VirtualMinus) { + $sum = $parent instanceof VirtualPlus ? 1 : -1; + if ($context && $context->inside_loop && $left_type_part instanceof TLiteralInt) { + if ($parent instanceof VirtualPlus) { + $new_type = new TIntRange($left_type_part->value + $sum, null); + } else { + $new_type = new TIntRange(null, $left_type_part->value + $sum); + } + } elseif ($left_type_part instanceof TLiteralInt) { + $new_type = new TLiteralInt($left_type_part->value + $sum); + } elseif ($left_type_part instanceof TIntRange) { + $start = $left_type_part->min_bound === null ? null : $left_type_part->min_bound + $sum; + $end = $left_type_part->max_bound === null ? null : $left_type_part->max_bound + $sum; + $new_type = new TIntRange($start, $end); + } else { + $new_type = new TInt(); + } + + $result_type = Type::combineUnionTypes( + new Union([$new_type], ['from_calculation' => true]), + $result_type, + ); } else { $result_type = Type::combineUnionTypes( $always_positive ? Type::getIntRange(1, null) : Type::getInt(true), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 3735bb65c94..5cd1d15baa3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -19,7 +19,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\TraitAnalyzer; use Psalm\Internal\Codebase\ConstantTypeResolver; -use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; @@ -805,13 +804,16 @@ public static function verifyType( } if ($input_type->isNever()) { - IssueBuffer::maybeAdd( + if (!IssueBuffer::accepts( new NoValue( 'All possible types for this argument were invalidated - This may be dead code', $arg_location, ), $statements_analyzer->getSuppressedIssues(), - ); + )) { + // if the error is suppressed, do not treat it as exited anymore + $context->has_returned = false; + } return null; } @@ -836,21 +838,52 @@ public static function verifyType( // $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) - ) { - continue; - } + $container_callable_type = $param_type->getSingleAtomic(); + $container_callable_type = $container_callable_type instanceof TCallable + ? $container_callable_type + : null; $candidate_callable = CallableTypeComparator::getCallableFromAtomic( $codebase, $atomic_type, - null, + $container_callable_type, $statements_analyzer, true, ); - if ($candidate_callable) { + if ($candidate_callable && $candidate_callable !== $atomic_type) { + // if we had an array callable, mark it as used now, since it's not possible later + $potential_method_id = null; + + if ($atomic_type instanceof TKeyedArray) { + $potential_method_id = CallableTypeComparator::getCallableMethodIdFromTKeyedArray( + $atomic_type, + $codebase, + $context->calling_method_id, + $statements_analyzer->getFilePath(), + ); + } elseif ($atomic_type instanceof TLiteralString + && strpos($atomic_type->value, '::') + ) { + $parts = explode('::', $atomic_type->value); + $potential_method_id = new MethodIdentifier( + $parts[0], + strtolower($parts[1]), + ); + } + + if ($potential_method_id && $potential_method_id !== 'not-callable') { + $codebase->methods->methodExists( + $potential_method_id, + $context->calling_method_id, + $arg_location, + $statements_analyzer, + $statements_analyzer->getFilePath(), + true, + $context->insideUse(), + ); + } + $input_type->removeType($key); $input_type->addType($candidate_callable); } @@ -918,6 +951,7 @@ public static function verifyType( && strpos($input_type_part->value, '::') ) { $parts = explode('::', $input_type_part->value); + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ $potential_method_ids[] = new MethodIdentifier( $parts[0], strtolower($parts[1]), @@ -929,7 +963,7 @@ public static function verifyType( $codebase->methods->methodExists( $potential_method_id, $context->calling_method_id, - null, + $arg_location, $statements_analyzer, $statements_analyzer->getFilePath(), true, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 122ca5387fa..0a683f7893d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -51,6 +51,7 @@ use UnexpectedValueException; use function array_map; +use function array_reduce; use function array_reverse; use function array_slice; use function array_values; @@ -1030,7 +1031,26 @@ private static function handlePossiblyMatchingByRefParam( $check_null_ref = true; if ($last_param) { - if ($argument_offset < count($function_params)) { + if ($arg->name !== null) { + $function_param = array_reduce( + $function_params, + static function ( + ?FunctionLikeParameter $function_param, + FunctionLikeParameter $param, + ) use ( + $arg, + ) { + if ($param->name === $arg->name->name) { + return $param; + } + return $function_param; + }, + null, + ); + if ($function_param === null) { + return false; + } + } elseif ($argument_offset < count($function_params)) { $function_param = $function_params[$argument_offset]; } else { $function_param = $last_param; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index f23cb61a17d..67f8134eba7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -206,7 +206,9 @@ public static function analyze( $statements_analyzer->node_data, ); - $function_call_info->function_params = $function_callable->params; + if (!$codebase->functions->params_provider->has($function_call_info->function_id)) { + $function_call_info->function_params = $function_callable->params; + } } } 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 74847d3861c..1e956a9039f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -752,6 +752,7 @@ private static function handleTemplatedMixins( $param_position = array_search( $mixin->param_name, $template_type_keys, + true, ); if ($param_position !== false 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 6241127b579..14026d3d1ab 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -435,6 +435,12 @@ private static function findPseudoMethodAndClassStorages( } $ancestors = $static_class_storage->class_implements; + foreach ($static_class_storage->namedMixins as $namedObject) { + $type = $namedObject->value; + if ($type) { + $ancestors[$type] = true; + } + } foreach ($ancestors as $fq_class_name => $_) { $class_storage = $codebase->classlikes->getStorageFor($fq_class_name); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 7917a188b89..6bf6d89bbd6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -312,7 +312,6 @@ public static function checkMethodArgs( $declaring_method_id = $class_storage->declaring_method_ids[$method_name]; $declaring_fq_class_name = $declaring_method_id->fq_class_name; - $declaring_method_name = $declaring_method_id->method_name; if ($declaring_fq_class_name !== $fq_class_name) { $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_fq_class_name); @@ -320,11 +319,7 @@ public static function checkMethodArgs( $declaring_class_storage = $class_storage; } - if (!isset($declaring_class_storage->methods[$declaring_method_name])) { - throw new UnexpectedValueException('Storage should not be empty here'); - } - - $method_storage = $declaring_class_storage->methods[$declaring_method_name]; + $method_storage = $codebase->methods->getStorage($declaring_method_id); if ($declaring_class_storage->user_defined && !$method_storage->has_docblock_param_types diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 9d55139838d..95d86401724 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -787,6 +787,7 @@ public static function localizePropertyType( $position = array_search( $param_name, array_keys($property_class_storage->template_types), + true, ); } @@ -999,7 +1000,7 @@ private static function handleEnumName( empty($relevant_enum_case_names) ? Type::getNonEmptyString() : new Union(array_map( - fn(string $name): TString => Type::getAtomicStringFromLiteral($name), + static fn(string $name): TString => Type::getAtomicStringFromLiteral($name), $relevant_enum_case_names, )), ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 659a7fdb42b..cba0730a470 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -12,6 +12,7 @@ use Psalm\FileSource; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\ArithmeticOpAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Internal\Type\TypeCombiner; @@ -144,6 +145,17 @@ public static function infer( $fq_classlike_name, ); + if (!$stmt_left_type + && $file_source instanceof StatementsAnalyzer + && $stmt->left instanceof PhpParser\Node\Expr\ConstFetch) { + $stmt_left_type = ConstFetchAnalyzer::getConstType( + $file_source, + $stmt->left->name->toString(), + true, + null, + ); + } + $stmt_right_type = self::infer( $codebase, $nodes, @@ -154,6 +166,17 @@ public static function infer( $fq_classlike_name, ); + if (!$stmt_right_type + && $file_source instanceof StatementsAnalyzer + && $stmt->right instanceof PhpParser\Node\Expr\ConstFetch) { + $stmt_right_type = ConstFetchAnalyzer::getConstType( + $file_source, + $stmt->right->name->toString(), + true, + null, + ); + } + if (!$stmt_left_type || !$stmt_right_type) { return null; } @@ -261,23 +284,28 @@ public static function infer( } if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { - $name = strtolower($stmt->name->getFirst()); - if ($name === 'false') { + $name = $stmt->name->getFirst(); + $name_lowercase = strtolower($name); + if ($name_lowercase === 'false') { return Type::getFalse(); } - if ($name === 'true') { + if ($name_lowercase === 'true') { return Type::getTrue(); } - if ($name === 'null') { + if ($name_lowercase === 'null') { return Type::getNull(); } - if ($stmt->name->getFirst() === '__NAMESPACE__') { + if ($name === '__NAMESPACE__') { return Type::getString($aliases->namespace); } + if ($type = ConstFetchAnalyzer::getGlobalConstType($codebase, $name, $name)) { + return $type; + } + return null; } diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 7b167153efd..e82c928ddbd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -15,7 +15,6 @@ use Psalm\Internal\Analyzer\Statements\Expression\BinaryOpAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\BitwiseNotAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\BooleanNotAnalyzer; -use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\NewAnalyzer; @@ -45,20 +44,18 @@ use Psalm\Internal\Analyzer\Statements\Expression\YieldAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\YieldFromAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; -use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\Type\TemplateResult; -use Psalm\Issue\ForbiddenCode; use Psalm\Issue\UnrecognizedExpression; use Psalm\Issue\UnsupportedReferenceUsage; use Psalm\IssueBuffer; +use Psalm\Node\Expr\VirtualFuncCall; +use Psalm\Node\Scalar\VirtualEncapsed; +use Psalm\Node\VirtualArg; +use Psalm\Node\VirtualName; use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent; use Psalm\Plugin\EventHandler\Event\BeforeExpressionAnalysisEvent; -use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; -use Psalm\Type\TaintKind; use function in_array; use function strtolower; @@ -379,80 +376,20 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\ShellExec) { - if ($statements_analyzer->data_flow_graph) { - $call_location = new CodeLocation($statements_analyzer->getSource(), $stmt); - - if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $sink = TaintSink::getForMethodArgument( - 'shell_exec', - 'shell_exec', - 0, - null, - $call_location, - ); - - $sink->taints = [TaintKind::INPUT_SHELL]; - - $statements_analyzer->data_flow_graph->addSink($sink); - } - - foreach ($stmt->parts as $part) { - if ($part instanceof PhpParser\Node\Expr\Variable) { - if (self::analyze($statements_analyzer, $part, $context) === false) { - break; - } - - $expr_type = $statements_analyzer->node_data->getType($part); - if ($expr_type === null) { - break; - } - - $shell_exec_param = new FunctionLikeParameter( - 'var', - false, - ); - - if (ArgumentAnalyzer::verifyType( - $statements_analyzer, - $expr_type, - Type::getString(), - null, - 'shell_exec', - null, - 0, - $call_location, - $stmt, - $context, - $shell_exec_param, - false, - null, - true, - true, - new CodeLocation($statements_analyzer, $stmt), - ) === false) { - return false; - } - - foreach ($expr_type->parent_nodes as $parent_node) { - $statements_analyzer->data_flow_graph->addPath( - $parent_node, - new DataFlowNode('variable-use', 'variable use', null), - 'variable-use', - ); - } - } - } - } - - IssueBuffer::maybeAdd( - new ForbiddenCode( - 'Use of shell_exec', - new CodeLocation($statements_analyzer->getSource(), $stmt), - ), - $statements_analyzer->getSuppressedIssues(), + $concat = new VirtualEncapsed($stmt->parts, $stmt->getAttributes()); + $virtual_call = new VirtualFuncCall(new VirtualName(['shell_exec']), [ + new VirtualArg($concat), + ], $stmt->getAttributes()); + return self::handleExpression( + $statements_analyzer, + $virtual_call, + $context, + $array_assignment, + $global_context, + $from_stmt, + $template_result, + $assigned_to_reference, ); - - return true; } if ($stmt instanceof PhpParser\Node\Expr\Print_) { diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index afa7a0beaab..0ccadac662e 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -677,7 +677,7 @@ private static function analyzeStatement( $check_type_string, $statements_analyzer->getAliases(), ); - $check_type = Type::parseString($fq_check_type_string); + $check_type = Type::parseString(CommentAnalyzer::sanitizeDocblockType($fq_check_type_string)); /** @psalm-suppress InaccessibleProperty We just created this type */ $check_type->possibly_undefined = $possibly_undefined; @@ -865,7 +865,7 @@ public function checkUnreferencedVars(array $stmts, Context $context): void } if ($function_storage) { - $param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup)); + $param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup), true); if ($param_index !== false) { $param = $function_storage->params[$param_index]; diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index dc455ae9942..0d3160fd465 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -287,7 +287,14 @@ public static function getPathsToCheck(string|array|false|null $f_paths): ?array } if (str_starts_with($input_path, '--') && strlen($input_path) > 2) { - if (substr($input_path, 2) === 'config') { + // ignore --config psalm.xml + // ignore common phpunit args that accept a class instead of a path, as this can cause issues on Windows + $ignored_arguments = array( + 'config', + 'printer', + ); + + if (in_array(substr($input_path, 2), $ignored_arguments, true)) { ++$i; } continue; diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index d286f98389a..aeaa864835f 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -92,6 +92,7 @@ * used_suppressions: array>, * function_docblock_manipulators: array>, * mutable_classes: array, + * issue_handlers: array{type: string, index: int, count: int}[], * } */ @@ -418,6 +419,10 @@ static function (): void { IssueBuffer::addUsedSuppressions($pool_data['used_suppressions']); } + if ($codebase->config->find_unused_issue_handler_suppression) { + $codebase->config->combineIssueHandlerSuppressions($pool_data['issue_handlers']); + } + if ($codebase->taint_flow_graph && $pool_data['taint_data']) { $codebase->taint_flow_graph->addGraph($pool_data['taint_data']); } @@ -1645,6 +1650,7 @@ private function getWorkerData(): array 'used_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUsedSuppressions() : [], 'function_docblock_manipulators' => FunctionDocblockManipulator::getManipulators(), 'mutable_classes' => $codebase->analyzer->mutable_classes, + 'issue_handlers' => $this->config->getIssueHandlerSuppressions() ]; // @codingStandardsIgnoreEnd } diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 8fcf208cef3..c93735ab0ae 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -450,6 +450,13 @@ public function getMethodParams( foreach ($params as $i => $param) { if (isset($overridden_storage->params[$i]->type) && $overridden_storage->params[$i]->has_docblock_type + && ( + ! $param->type + || $param->type->equals( + $overridden_storage->params[$i]->signature_type + ?? $overridden_storage->params[$i]->type, + ) + ) ) { $params[$i] = clone $param; /** @var Union $params[$i]->type */ @@ -1132,14 +1139,18 @@ public function getStorage(MethodIdentifier $method_id): MethodStorage } $method_name = $method_id->method_name; + $method_storage = $class_storage->methods[$method_name] + ?? $class_storage->pseudo_methods[$method_name] + ?? $class_storage->pseudo_static_methods[$method_name] + ?? null; - if (!isset($class_storage->methods[$method_name])) { + if (! $method_storage) { throw new UnexpectedValueException( '$storage should not be null for ' . $method_id, ); } - return $class_storage->methods[$method_name]; + return $method_storage; } /** @psalm-mutation-free */ diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index c0753196f49..4093c263db5 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -352,7 +352,9 @@ private function populateOverriddenMethods( $declaring_method_name = $declaring_method_id->method_name; $declaring_class_storage = $declaring_class_storages[$declaring_class]; - $declaring_method_storage = $declaring_class_storage->methods[$declaring_method_name]; + $declaring_method_storage = $declaring_class_storage->methods[$declaring_method_name] + ?? $declaring_class_storage->pseudo_methods[$declaring_method_name] + ?? $declaring_class_storage->pseudo_static_methods[$declaring_method_name]; if (($declaring_method_storage->has_docblock_param_types || $declaring_method_storage->has_docblock_return_type) @@ -437,6 +439,8 @@ private function populateDataFromTrait( $storage->pseudo_property_get_types += $trait_storage->pseudo_property_get_types; $storage->pseudo_property_set_types += $trait_storage->pseudo_property_set_types; + $storage->pseudo_static_methods += $trait_storage->pseudo_static_methods; + $storage->pseudo_methods += $trait_storage->pseudo_methods; $storage->declaring_pseudo_method_ids += $trait_storage->declaring_pseudo_method_ids; } @@ -541,8 +545,21 @@ private function populateDataFromParentClass( $parent_storage->dependent_classlikes[strtolower($storage->name)] = true; - $storage->pseudo_methods += $parent_storage->pseudo_methods; - $storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids; + foreach ($parent_storage->pseudo_static_methods as $method_name => $pseudo_method) { + if (!isset($storage->methods[$method_name])) { + $storage->pseudo_static_methods[$method_name] = $pseudo_method; + } + } + foreach ($parent_storage->pseudo_methods as $method_name => $pseudo_method) { + if (!isset($storage->methods[$method_name])) { + $storage->pseudo_methods[$method_name] = $pseudo_method; + } + } + foreach ($parent_storage->declaring_pseudo_method_ids as $method_name => $pseudo_method_id) { + if (!isset($storage->methods[$method_name])) { + $storage->declaring_pseudo_method_ids[$method_name] = $pseudo_method_id; + }; + } } private function populateInterfaceData( @@ -990,7 +1007,11 @@ private function inheritMethodsFromParent( $implementing_method_id->fq_class_name, ); - if (!$implementing_class_storage->methods[$implementing_method_id->method_name]->abstract + $method = $implementing_class_storage->methods[$implementing_method_id->method_name] + ?? $implementing_class_storage->pseudo_methods[$implementing_method_id->method_name] + ?? $implementing_class_storage->pseudo_static_methods[$implementing_method_id->method_name]; + + if (!$method->abstract || !empty($storage->methods[$implementing_method_id->method_name]->abstract) ) { continue; diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index 17740d9f9a7..eabb6673e90 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -296,6 +296,7 @@ private function scanFilePaths(int $pool_size): bool $pool_size = 1; } + $this->progress->expand(count($files_to_scan)); if ($pool_size > 1) { $process_file_paths = []; @@ -334,7 +335,6 @@ function (): void { */ function () { $this->progress->debug('Collecting data from forked scanner process' . PHP_EOL); - $project_analyzer = ProjectAnalyzer::getInstance(); $codebase = $project_analyzer->getCodebase(); $statements_provider = $codebase->statements_provider; @@ -356,6 +356,9 @@ function () { 'taint_data' => $codebase->taint_flow_graph, ]; }, + function (): void { + $this->progress->taskDone(0); + }, ); // Wait for all tasks to complete and collect the results. @@ -406,6 +409,7 @@ function () { $i = 0; foreach ($files_to_scan as $file_path => $_) { + $this->progress->taskDone(0); $this->scanAPath($i, $file_path); ++$i; } diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index fe63e1e38aa..3004060d044 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -412,7 +412,7 @@ private function getDocblock(): string $modified_docblock = true; $inferredThrowsClause = array_reduce( $this->throwsExceptions, - fn(string $throwsClause, string $exception) => $throwsClause === '' + static fn(string $throwsClause, string $exception) => $throwsClause === '' ? $exception : $throwsClause.'|'.$exception, '', diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index df2f691cd68..a573fc01e5b 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -200,7 +200,7 @@ function (Message $msg): void { $this->protocolReader->on( 'readMessageGroup', - function (): void { + static function (): void { //$this->verboseLog('Received message group'); //$this->doAnalysis(); }, @@ -486,6 +486,36 @@ public function initialize( */ $serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "]; } + /** + * The server provides document symbol support. + * Support "Find all symbols" + */ + $serverCapabilities->documentSymbolProvider = false; + /** + * The server provides workspace symbol support. + * Support "Find all symbols in workspace" + */ + $serverCapabilities->workspaceSymbolProvider = false; + /** + * The server provides goto definition support. + * Support "Go to definition" + */ + $serverCapabilities->definitionProvider = true; + /** + * The server provides find references support. + * Support "Find all references" + */ + $serverCapabilities->referencesProvider = false; + /** + * The server provides hover support. + * Support "Hover" + */ + $serverCapabilities->hoverProvider = true; + /** + * The server does not support documentHighlight-ing + * Ref: https://github.com/vimeo/psalm/issues/10397 + */ + $serverCapabilities->documentHighlightProvider = false; /** * Whether code action supports the `data` property which is @@ -712,7 +742,7 @@ function (IssueData $issue_data): Diagnostic { return $diagnostic; }, array_filter( - array_map(function (IssueData $issue_data) use (&$issue_baseline) { + array_map(static function (IssueData $issue_data) use (&$issue_baseline) { if (empty($issue_baseline)) { return $issue_data; } diff --git a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php index 2ef4a32c6f0..3df8d6a7f63 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php @@ -15,7 +15,7 @@ */ final class ClassLikeStorageCacheProvider extends InternalClassLikeStorageCacheProvider { - /** @var array */ + /** @var array */ private array $cache = []; public function __construct() @@ -28,6 +28,9 @@ public function writeToCache(ClassLikeStorage $storage, ?string $file_path, ?str $this->cache[$fq_classlike_name_lc] = $storage; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ public function getLatestFromCache( string $fq_classlike_name_lc, ?string $file_path, @@ -42,6 +45,9 @@ public function getLatestFromCache( return $cached_value; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function loadFromCache(string $fq_classlike_name_lc): ?ClassLikeStorage { return $this->cache[$fq_classlike_name_lc] ?? null; diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 7d35bd9cfab..73a6781fa55 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -281,6 +281,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit try { $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); + $literal_part = $this->codebase->getBeginedLiteralPart($file_path, $position); if ($completion_data) { [$recent_type, $gap, $offset] = $completion_data; @@ -289,6 +290,8 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit ->textDocument->completion->completionItem->snippetSupport ?? false; $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); + $completion_items = + $this->codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); } elseif ($gap === '[') { $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); } else { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index e94a64ba10f..be0ccb6102f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -69,9 +69,9 @@ public static function parse( $templates = []; if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line)); if ($template_type === false) { - throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg()); + throw new IncorrectDocblockException('Invalid @template tag: '.preg_last_error_msg()); } $template_name = array_shift($template_type); @@ -112,7 +112,7 @@ 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]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line)); if ($template_type === false) { throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg()); } @@ -172,20 +172,16 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-require-extends']) && count($extension_requirements = $parsed_docblock->tags['psalm-require-extends']) > 0) { - $info->extension_requirement = trim((string) preg_replace( - '@^[ \t]*\*@m', - '', + $info->extension_requirement = CommentAnalyzer::sanitizeDocblockType( $extension_requirements[array_key_first($extension_requirements)], - )); + ); } if (isset($parsed_docblock->tags['psalm-require-implements'])) { foreach ($parsed_docblock->tags['psalm-require-implements'] as $implementation_requirement) { - $info->implementation_requirements[] = trim((string) preg_replace( - '@^[ \t]*\*@m', - '', + $info->implementation_requirements[] = CommentAnalyzer::sanitizeDocblockType( $implementation_requirement, - )); + ); } } @@ -200,7 +196,7 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-yield'])) { $yield = (string) reset($parsed_docblock->tags['psalm-yield']); - $info->yield = trim((string) preg_replace('@^[ \t]*\*@m', '', $yield)); + $info->yield = CommentAnalyzer::sanitizeDocblockType($yield); } if (isset($parsed_docblock->tags['deprecated'])) { @@ -241,18 +237,20 @@ public static function parse( } } - if (isset($parsed_docblock->tags['psalm-seal-properties'])) { - $info->sealed_properties = true; - } - if (isset($parsed_docblock->tags['psalm-no-seal-properties'])) { - $info->sealed_properties = false; - } + foreach (['', 'psalm-'] as $prefix) { + if (isset($parsed_docblock->tags[$prefix . 'seal-properties'])) { + $info->sealed_properties = true; + } + if (isset($parsed_docblock->tags[$prefix . 'no-seal-properties'])) { + $info->sealed_properties = false; + } - if (isset($parsed_docblock->tags['psalm-seal-methods'])) { - $info->sealed_methods = true; - } - if (isset($parsed_docblock->tags['psalm-no-seal-methods'])) { - $info->sealed_methods = false; + if (isset($parsed_docblock->tags[$prefix . 'seal-methods'])) { + $info->sealed_methods = true; + } + if (isset($parsed_docblock->tags[$prefix . 'no-seal-methods'])) { + $info->sealed_methods = false; + } } if (isset($parsed_docblock->tags['psalm-inheritors'])) { @@ -553,7 +551,7 @@ private static function addMagicPropertyToInfo( $end = $offset + strlen($line_parts[0]); - $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); if ($line_parts[0] === '' || ($line_parts[0][0] === '$' diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 1af238739af..1c055466eb3 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -79,7 +79,6 @@ use function count; use function implode; use function preg_match; -use function preg_replace; use function preg_split; use function str_replace; use function strtolower; @@ -161,6 +160,15 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool if ($this->codebase->classlike_storage_provider->has($fq_classlike_name_lc)) { $duplicate_storage = $this->codebase->classlike_storage_provider->get($fq_classlike_name_lc); + // don't override data from files that are getting analyzed with data from stubs + // if the stubs contain the same class + if (!$duplicate_storage->stubbed + && $this->codebase->register_stub_files + && $duplicate_storage->stmt_location + && $this->config->isInProjectDirs($duplicate_storage->stmt_location->file_path)) { + return false; + } + if (!$this->codebase->register_stub_files) { if (!$duplicate_storage->stmt_location || $duplicate_storage->stmt_location->file_path !== $this->file_path @@ -608,11 +616,16 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $storage->pseudo_static_methods[$lc_method_name] = $pseudo_method_storage; } else { $storage->pseudo_methods[$lc_method_name] = $pseudo_method_storage; - $storage->declaring_pseudo_method_ids[$lc_method_name] = new MethodIdentifier( - $fq_classlike_name, - $lc_method_name, - ); } + $method_identifier = new MethodIdentifier( + $fq_classlike_name, + $lc_method_name, + ); + $storage->inheritable_method_ids[$lc_method_name] = $method_identifier; + if (!isset($storage->overridden_method_ids[$lc_method_name])) { + $storage->overridden_method_ids[$lc_method_name] = []; + } + $storage->declaring_pseudo_method_ids[$lc_method_name] = $method_identifier; } @@ -924,7 +937,7 @@ public function handleTraitUse(PhpParser\Node\Stmt\TraitUse $node): void $this->useTemplatedType( $storage, $node, - trim((string) preg_replace('@^[ \t]*\*@m', '', $template_line)), + CommentAnalyzer::sanitizeDocblockType($template_line), ); } } @@ -1895,10 +1908,7 @@ private static function getTypeAliasesFromCommentLines( continue; } - $var_line = (string) preg_replace('/[ \t]+/', ' ', (string) preg_replace('@^[ \t]*\*@m', '', $var_line)); - $var_line = (string) preg_replace('/,\n\s+\}/', '}', $var_line); - $var_line = str_replace("\n", '', $var_line); - + $var_line = CommentAnalyzer::sanitizeDocblockType($var_line); $var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); if (!$var_line_parts) { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 8d60023a4fc..03a5d8c8d0f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -153,11 +153,7 @@ public static function parse( $line_parts[1] = substr($line_parts[1], 1); } - $line_parts[0] = str_replace( - "\n", - '', - (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]), - ); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); if ($line_parts[0] === '' || ($line_parts[0][0] === '$' @@ -196,14 +192,10 @@ public static function parse( $line_parts = CommentAnalyzer::splitDocLine($param); if (count($line_parts) > 0) { - $line_parts[0] = str_replace( - "\n", - '', - (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]), - ); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); $info->self_out = [ - 'type' => str_replace("\n", '', $line_parts[0]), + 'type' => $line_parts[0], 'line_number' => $comment->getStartLine() + substr_count( $comment_text, "\n", @@ -227,10 +219,10 @@ public static function parse( foreach ($parsed_docblock->tags['psalm-if-this-is'] as $offset => $param) { $line_parts = CommentAnalyzer::splitDocLine($param); - $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); $info->if_this_is = [ - 'type' => str_replace("\n", '', $line_parts[0]), + 'type' => $line_parts[0], 'line_number' => $comment->getStartLine() + substr_count( $comment->getText(), "\n", @@ -456,7 +448,7 @@ public static function parse( $templates = []; if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line)); if ($template_type === false) { throw new AssertionError(preg_last_error_msg()); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 9b769b5700a..54c42971897 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -903,6 +903,7 @@ private function createStorageForFunctionLike( $storage->is_static = $stmt->isStatic(); $storage->final = $this->classlike_storage && $this->classlike_storage->final; $storage->final_from_docblock = $this->classlike_storage && $this->classlike_storage->final_from_docblock; + $storage->visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC; } elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) { $cased_function_id = ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name->name; diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index bc911d8cd6f..b8bde65c8ea 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -141,13 +141,13 @@ public function enterNode(PhpParser\Node $node): ?int $this->namespace_name, ); - $this->classlike_node_scanners[] = $classlike_node_scanner; - if ($classlike_node_scanner->start($node) === false) { $this->bad_classes[spl_object_id($node)] = true; - return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return PhpParser\NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } + $this->classlike_node_scanners[] = $classlike_node_scanner; + $this->type_aliases = [...$this->type_aliases, ...$classlike_node_scanner->type_aliases]; } elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) { foreach ($node->catches as $catch) { diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index 73975ea2110..266284ee376 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -77,6 +77,9 @@ public function writeToCache(ClassLikeStorage $storage, string $file_path, strin $this->cache->saveItem($cache_location, $storage); } + /** + * @param lowercase-string $fq_classlike_name_lc + */ public function getLatestFromCache( string $fq_classlike_name_lc, ?string $file_path, @@ -108,6 +111,9 @@ private function getCacheHash(?string $_unused_file_path, ?string $file_contents return hash('xxh128', $data); } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path): ?ClassLikeStorage { $storage = $this->cache->getItem($this->getCacheLocationForClass($fq_classlike_name_lc, $file_path)); @@ -118,6 +124,9 @@ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path) return null; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function getCacheLocationForClass( string $fq_classlike_name_lc, ?string $file_path, diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index ee02fadad81..a8b6f83a539 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -103,7 +103,7 @@ public function getLatestFromCache(string $file_path, string $file_contents): ?F public function removeCacheForFile(string $file_path): void { - $this->cache->deleteItem($this->getCacheLocationForPath($file_path)); + $this->cache->deleteItem($this->getCacheLocationForPath(strtolower($file_path))); } private function getCacheHash(string $_unused_file_path, string $file_contents): string diff --git a/src/Psalm/Internal/Provider/FunctionParamsProvider.php b/src/Psalm/Internal/Provider/FunctionParamsProvider.php index ce852dc887c..9a627bdb3c7 100644 --- a/src/Psalm/Internal/Provider/FunctionParamsProvider.php +++ b/src/Psalm/Internal/Provider/FunctionParamsProvider.php @@ -8,6 +8,8 @@ use PhpParser\Node\Arg; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Internal\Provider\ParamsProvider\ArrayFilterParamsProvider; +use Psalm\Internal\Provider\ParamsProvider\ArrayMultisortParamsProvider; use Psalm\Plugin\EventHandler\Event\FunctionParamsProviderEvent; use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface; use Psalm\StatementsSource; @@ -31,6 +33,9 @@ final class FunctionParamsProvider public function __construct() { self::$handlers = []; + + $this->registerClass(ArrayFilterParamsProvider::class); + $this->registerClass(ArrayMultisortParamsProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php new file mode 100644 index 00000000000..e89abe8978f --- /dev/null +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php @@ -0,0 +1,265 @@ + + */ + public static function getFunctionIds(): array + { + return [ + 'array_filter', + ]; + } + + /** + * @return ?list + */ + public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array + { + $call_args = $event->getCallArgs(); + if (!isset($call_args[0]) || !isset($call_args[1])) { + return null; + } + + $statements_source = $event->getStatementsSource(); + if (!($statements_source instanceof StatementsAnalyzer)) { + // this is practically impossible + // but the type in the caller is parent type StatementsSource + // even though all callers provide StatementsAnalyzer + return null; + } + + $code_location = $event->getCodeLocation(); + if ($call_args[1]->value instanceof ConstFetch + && strtolower($call_args[1]->value->name->toString()) === 'null' + && isset($call_args[2]) + ) { + if ($code_location) { + // using e.g. ARRAY_FILTER_USE_KEY as 3rd arg won't have any effect if the 2nd arg is null + // as it will still filter on the values + IssueBuffer::maybeAdd( + new InvalidArgument( + 'The 3rd argument of array_filter is not used, when the 2nd argument is null', + $code_location, + 'array_filter', + ), + $statements_source->getSuppressedIssues(), + ); + } + + return null; + } + + // currently only supports literal types and variables (but not function calls) + // due to https://github.com/vimeo/psalm/issues/8905 + $first_arg_type = SimpleTypeInferer::infer( + $statements_source->getCodebase(), + $statements_source->node_data, + $call_args[0]->value, + $statements_source->getAliases(), + $statements_source, + ); + + if (!$first_arg_type) { + $extended_var_id = ExpressionIdentifier::getExtendedVarId( + $call_args[0]->value, + null, + $statements_source, + ); + + $first_arg_type = $event->getContext()->vars_in_scope[$extended_var_id] ?? null; + } + + $fallback = new TArray([Type::getArrayKey(), Type::getMixed()]); + if (!$first_arg_type || $first_arg_type->isMixed()) { + $first_arg_array = $fallback; + } else { + $first_arg_array = $first_arg_type->hasType('array') + && ($array_atomic_type = $first_arg_type->getArray()) + && ($array_atomic_type instanceof TArray + || $array_atomic_type instanceof TKeyedArray) + ? $array_atomic_type + : $fallback; + } + + if ($first_arg_array instanceof TArray) { + $inner_type = $first_arg_array->type_params[1]; + $key_type = $first_arg_array->type_params[0]; + } else { + $inner_type = $first_arg_array->getGenericValueType(); + $key_type = $first_arg_array->getGenericKeyType(); + } + + $has_both = false; + if (isset($call_args[2])) { + $mode_type = SimpleTypeInferer::infer( + $statements_source->getCodebase(), + $statements_source->node_data, + $call_args[2]->value, + $statements_source->getAliases(), + $statements_source, + ); + + if (!$mode_type && $call_args[2]->value instanceof ConstFetch) { + $mode_type = ConstFetchAnalyzer::getConstType( + $statements_source, + $call_args[2]->value->name->toString(), + true, + $event->getContext(), + ); + } elseif (!$mode_type) { + $extended_var_id = ExpressionIdentifier::getExtendedVarId( + $call_args[2]->value, + null, + $statements_source, + ); + + $mode_type = $event->getContext()->vars_in_scope[$extended_var_id] ?? null; + } + + if (!$mode_type || !$mode_type->allIntLiterals()) { + // if we have multiple possible types, keep the default args + return null; + } + + if ($mode_type->isSingleIntLiteral()) { + $mode = $mode_type->getSingleIntLiteral()->value; + } else { + $mode = 0; + foreach ($mode_type->getLiteralInts() as $atomic) { + if ($atomic->value === ARRAY_FILTER_USE_BOTH) { + // we have one which uses both keys and values and one that uses only keys/values + $has_both = true; + continue; + } + + if ($atomic->value === ARRAY_FILTER_USE_KEY) { + // if one of them is ARRAY_FILTER_USE_KEY, all the other types will behave like mode 0 + $inner_type = Type::combineUnionTypes( + $inner_type, + $key_type, + $statements_source->getCodebase(), + ); + + continue; + } + + // to report an error later on + if ($mode === 0 && $atomic->value !== 0) { + $mode = $atomic->value; + } + } + } + + if ($mode > ARRAY_FILTER_USE_KEY || $mode < 0) { + if ($code_location) { + IssueBuffer::maybeAdd( + new PossiblyInvalidArgument( + 'The provided 3rd argument of array_filter contains a value of ' . $mode + . ', which will behave like 0 and filter on values only', + $code_location, + 'array_filter', + ), + $statements_source->getSuppressedIssues(), + ); + } + + $mode = 0; + } + } else { + $mode = 0; + } + + $callback_arg_value = new FunctionLikeParameter( + 'value', + false, + $inner_type, + null, + null, + null, + false, + ); + + $callback_arg_key = new FunctionLikeParameter( + 'key', + false, + $key_type, + null, + null, + null, + false, + ); + + if ($mode === ARRAY_FILTER_USE_BOTH) { + $callback_arg = [ + $callback_arg_value, + $callback_arg_key, + ]; + } elseif ($mode === ARRAY_FILTER_USE_KEY) { + $callback_arg = [ + $callback_arg_key, + ]; + } elseif ($has_both) { + // if we have both + other flags, the 2nd arg is optional + $callback_arg_key->is_optional = true; + $callback_arg = [ + $callback_arg_value, + $callback_arg_key, + ]; + } else { + $callback_arg = [ + $callback_arg_value, + ]; + } + + $callable = new TCallable( + 'callable', + $callback_arg, + Type::getMixed(), + ); + + return [ + new FunctionLikeParameter( + 'array', + false, + Type::getArray(), + Type::getArray(), + null, + null, + false, + ), + new FunctionLikeParameter('callback', false, new Union([$callable])), + new FunctionLikeParameter('mode', false, Type::getInt(), Type::getInt()), + ]; + } +} diff --git a/src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php b/src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php new file mode 100644 index 00000000000..a27b79566cd --- /dev/null +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php @@ -0,0 +1,311 @@ + + */ + public static function getFunctionIds(): array + { + return [ + 'array_multisort', + ]; + } + + /** + * @return ?list + */ + public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array + { + $call_args = $event->getCallArgs(); + if (!isset($call_args[0])) { + return null; + } + + $statements_source = $event->getStatementsSource(); + if (!($statements_source instanceof StatementsAnalyzer)) { + // this is practically impossible + // but the type in the caller is parent type StatementsSource + // even though all callers provide StatementsAnalyzer + return null; + } + + $code_location = $event->getCodeLocation(); + $params = []; + $previous_param = false; + $last_array_index = 0; + $last_by_ref_index = -1; + $first_non_ref_index_after_by_ref = -1; + foreach ($call_args as $key => $call_arg) { + $param_type = SimpleTypeInferer::infer( + $statements_source->getCodebase(), + $statements_source->node_data, + $call_arg->value, + $statements_source->getAliases(), + $statements_source, + ); + + if (!$param_type && $call_arg->value instanceof ConstFetch) { + $param_type = ConstFetchAnalyzer::getConstType( + $statements_source, + $call_arg->value->name->toString(), + true, + $event->getContext(), + ); + } + + // @todo currently assumes any function calls are for array types not for sort order/flags + // actually need to check the return type + // which isn't possible atm due to https://github.com/vimeo/psalm/issues/8905 + if (!$param_type && ($call_arg->value instanceof FuncCall || $call_arg->value instanceof MethodCall)) { + if ($first_non_ref_index_after_by_ref < $last_by_ref_index) { + $first_non_ref_index_after_by_ref = $key; + } + + $last_array_index = $key; + $previous_param = 'array'; + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1), + // function calls will not be used by reference + false, + Type::getArray(), + $key === 0 ? Type::getArray() : null, + ); + + continue; + } + + $extended_var_id = null; + if (!$param_type) { + $extended_var_id = ExpressionIdentifier::getExtendedVarId( + $call_arg->value, + null, + $statements_source, + ); + + $param_type = $event->getContext()->vars_in_scope[$extended_var_id] ?? null; + } + + if (!$param_type) { + return null; + } + + if ($key === 0 && !$param_type->isArray()) { + return null; + } + + if ($param_type->isArray() && $extended_var_id) { + $last_by_ref_index = $key; + $last_array_index = $key; + $previous_param = 'array'; + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1), + true, + $param_type, + $key === 0 ? Type::getArray() : null, + ); + + continue; + } + + if ($param_type->allIntLiterals()) { + $sort_order = [ + SORT_ASC, + SORT_DESC, + ]; + + $sort_flags = [ + SORT_REGULAR, + SORT_NUMERIC, + SORT_STRING, + SORT_LOCALE_STRING, + SORT_NATURAL, + SORT_STRING|SORT_FLAG_CASE, + SORT_NATURAL|SORT_FLAG_CASE, + ]; + + $sort_param = false; + foreach ($param_type->getLiteralInts() as $atomic) { + if (in_array($atomic->value, $sort_order, true)) { + if ($sort_param === 'sort_order_flags') { + continue; + } + + if ($sort_param === 'sort_order') { + continue; + } + + if ($sort_param === 'sort_flags') { + $sort_param = 'sort_order_flags'; + continue; + } + + $sort_param = 'sort_order'; + + continue; + } + + if (in_array($atomic->value, $sort_flags, true)) { + if ($sort_param === 'sort_order_flags') { + continue; + } + + if ($sort_param === 'sort_flags') { + continue; + } + + if ($sort_param === 'sort_order') { + $sort_param = 'sort_order_flags'; + continue; + } + + $sort_param = 'sort_flags'; + + continue; + } + + if ($code_location) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ( $key + 1 ) + . ' of array_multisort sort order/flag contains an invalid value of ' . $atomic->value, + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + } + + if ($sort_param === false) { + return null; + } + + if (($sort_param === 'sort_order' || $sort_param === 'sort_order_flags') + && $previous_param !== 'array') { + if ($code_location) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ( $key + 1 ) + . ' of array_multisort contains sort order flags' + . ' and can only be used after an array parameter', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + + return null; + } + + if ($sort_param === 'sort_flags' && $previous_param !== 'array' && $previous_param !== 'sort_order') { + if ($code_location) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ( $key + 1 ) + . ' of array_multisort are sort flags' + . ' and cannot be used after a parameter with sort flags', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + + return null; + } + + if ($sort_param === 'sort_order_flags') { + $previous_param = 'sort_order'; + } else { + $previous_param = $sort_param; + } + + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1) . '_' . $previous_param, + false, + Type::getInt(), + ); + + continue; + } + + if (!$param_type->isArray()) { + // too complex for now + return null; + } + + if ($first_non_ref_index_after_by_ref < $last_by_ref_index) { + $first_non_ref_index_after_by_ref = $key; + } + + $last_array_index = $key; + $previous_param = 'array'; + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1), + false, + Type::getArray(), + ); + } + + if ($code_location) { + if ($last_by_ref_index === - 1) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'At least 1 array argument of array_multisort must be a variable,' + . ' since the sorting happens by reference and otherwise this function call does nothing', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } elseif ($first_non_ref_index_after_by_ref > $last_by_ref_index) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'All arguments of array_multisort after argument ' . $first_non_ref_index_after_by_ref + . ', which are after the last by reference passed array argument and its flags,' + . ' are redundant and can be removed, since the sorting happens by reference', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + } + + return $params; + } +} diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index cf30ac9fd83..83715df28df 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -60,19 +60,22 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } + $fallback = new TArray([Type::getArrayKey(), Type::getMixed()]); $array_arg = $call_args[0]->value ?? null; - - $first_arg_array = $array_arg - && ($first_arg_type = $statements_source->node_data->getType($array_arg)) - && $first_arg_type->hasType('array') - && ($array_atomic_type = $first_arg_type->getArray()) - && ($array_atomic_type instanceof TArray - || $array_atomic_type instanceof TKeyedArray) - ? $array_atomic_type - : null; - - if (!$first_arg_array) { - return Type::getArray(); + if (!$array_arg) { + $first_arg_array = $fallback; + } else { + $first_arg_type = $statements_source->node_data->getType($array_arg); + if (!$first_arg_type || $first_arg_type->isMixed()) { + $first_arg_array = $fallback; + } else { + $first_arg_array = $first_arg_type->hasType('array') + && ($array_atomic_type = $first_arg_type->getArray()) + && ($array_atomic_type instanceof TArray + || $array_atomic_type instanceof TKeyedArray) + ? $array_atomic_type + : $fallback; + } } if ($first_arg_array instanceof TArray) { @@ -166,14 +169,34 @@ static function ($keyed_type) use ($statements_source, $context) { if (!isset($call_args[2])) { $function_call_arg = $call_args[1]; + $callable_extended_var_id = ExpressionIdentifier::getExtendedVarId( + $function_call_arg->value, + null, + $statements_source, + ); + + $mapping_function_ids = array(); + if ($callable_extended_var_id) { + $possibly_function_ids = $context->vars_in_scope[$callable_extended_var_id] ?? null; + // @todo for array callables + if ($possibly_function_ids && $possibly_function_ids->allStringLiterals()) { + foreach ($possibly_function_ids->getLiteralStrings() as $atomic) { + $mapping_function_ids[] = $atomic->value; + } + } + } + if ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_ || $function_call_arg->value instanceof PhpParser\Node\Expr\Array_ || $function_call_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat + || $mapping_function_ids !== array() ) { - $mapping_function_ids = CallAnalyzer::getFunctionIdsFromCallableArg( - $statements_source, - $function_call_arg->value, - ); + if ($mapping_function_ids === array()) { + $mapping_function_ids = CallAnalyzer::getFunctionIdsFromCallableArg( + $statements_source, + $function_call_arg->value, + ); + } if ($array_arg && $mapping_function_ids) { $assertions = []; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 50f98b3af74..795615b983f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -115,9 +115,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $array_arg_types = array_map(null, ...$array_arg_types); $array_arg_types = array_map( /** @param non-empty-array $sub */ - function (array $sub) use ($null) { + static function (array $sub) use ($null) { $sub = array_map( - fn(?Union $t) => $t ?? $null, + static fn(?Union $t) => $t ?? $null, $sub, ); return new Union([new TKeyedArray($sub, null, null, true)]); diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 380496d12de..047ddd08bda 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -30,6 +30,7 @@ use UnexpectedValueException; use function array_slice; +use function count; use function end; use function strtolower; use function substr; @@ -464,6 +465,7 @@ public static function getCallableMethodIdFromTKeyedArray( ): string|MethodIdentifier|null { if (!isset($input_type_part->properties[0]) || !isset($input_type_part->properties[1]) + || count($input_type_part->properties) > 2 ) { return 'not-callable'; } diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index b62662f7b44..3a1aeff89fe 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -147,7 +147,7 @@ public static function isContainedBy( $container_all_param_count = count($container_type_part->params); $container_required_param_count = 0; foreach ($container_type_part->params as $index => $container_param) { - if ($container_param->is_optional === false) { + if (!$container_param->is_optional) { $container_required_param_count = $index + 1; } @@ -163,7 +163,8 @@ public static function isContainedBy( } else { $input_all_param_count = count($input_type_part->params); foreach ($input_type_part->params as $index => $input_param) { - if ($input_param->is_optional === false) { + // can be false or not set at all + if (!$input_param->is_optional) { $input_required_param_count = $index + 1; } @@ -174,8 +175,10 @@ public static function isContainedBy( } // too few or too many non-optional params provided in callback - if ($container_required_param_count > $input_all_param_count - || $container_all_param_count < $input_required_param_count + if ($container_all_param_count > $input_all_param_count + || $container_required_param_count > $input_all_param_count + || $input_required_param_count > $container_all_param_count + || $input_required_param_count > $container_required_param_count ) { continue; } diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index ef33c256ff9..b6e85303f01 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -303,9 +303,7 @@ public static function reconcile( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } return $existing_var_type; diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 83708e1cc00..cd853ce62d2 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -31,6 +31,7 @@ use function in_array; use function preg_match; use function strlen; +use function strpos; use function strtolower; /** @@ -139,6 +140,7 @@ public function create(): ParseTree case 'is': case 'as': + case 'of': $this->handleIsOrAs($type_token); break; @@ -767,7 +769,7 @@ private function handleIsOrAs(array $type_token): void array_pop($current_parent->children); } - if ($type_token[0] === 'as') { + if ($type_token[0] === 'as' || $type_token[0] == 'of') { $next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; if (!$this->current_leaf instanceof Value @@ -820,13 +822,30 @@ private function handleValue(array $type_token): void break; case '{': + ++$this->t; + + $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; + + if ($nexter_token && strpos($nexter_token[0], '@') !== false) { + $this->t = $this->type_token_count; + if ($type_token[0] === '$this') { + $type_token[0] = 'static'; + } + + $new_leaf = new Value( + $type_token[0], + $type_token[1], + $type_token[1] + strlen($type_token[0]), + $type_token[2] ?? null, + $new_parent, + ); + break; + } + $new_leaf = new KeyedArrayTree( $type_token[0], $new_parent, ); - ++$this->t; - - $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; if ($nexter_token !== null && $nexter_token[0] === '}') { $new_leaf->terminated = true; diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index c748d3edcd0..cec91f27f89 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -9,6 +9,7 @@ use Psalm\Codebase; use Psalm\Internal\Codebase\ClassConstantByWildcardResolver; use Psalm\Internal\Codebase\InternalCallMapHandler; +use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Storage\Assertion; use Psalm\Storage\Assertion\Any; use Psalm\Storage\Assertion\ArrayKeyExists; @@ -41,7 +42,6 @@ use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; -use Psalm\Type\Atomic\TEmptyMixed; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TGenericObject; @@ -683,7 +683,7 @@ private static function reconcileNonEmptyCountable( $existing_var_type->removeType('array'); $existing_var_type->addType($array_atomic_type->setProperties( array_map( - fn(Union $union) => $union->setPossiblyUndefined(false), + static fn(Union $union) => $union->setPossiblyUndefined(false), $array_atomic_type->properties, ), )); @@ -803,7 +803,7 @@ private static function reconcileExactlyCountable( $existing_var_type->removeType('array'); $existing_var_type->addType($array_atomic_type->setProperties( array_map( - fn(Union $union) => $union->setPossiblyUndefined(false), + static fn(Union $union) => $union->setPossiblyUndefined(false), $array_atomic_type->properties, ), )); @@ -973,9 +973,7 @@ private static function reconcileHasMethod( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? new Union([new TEmptyMixed()]) - : Type::getNever(); + return Type::getNever(); } /** @@ -1037,6 +1035,10 @@ private static function reconcileString( $string_types[] = $type; } + $redundant = false; + } elseif ($type instanceof TInt && $assertion instanceof IsLooselyEqual) { + // don't change the type of an int for non-strict comparisons + $string_types[] = $type; $redundant = false; } else { $redundant = false; @@ -1064,9 +1066,7 @@ private static function reconcileString( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? new Union([new TEmptyMixed()]) - : Type::getNever(); + return Type::getNever(); } /** @@ -1158,9 +1158,7 @@ private static function reconcileInt( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? new Union([new TEmptyMixed()]) - : Type::getNever(); + return Type::getNever(); } /** @@ -1237,9 +1235,7 @@ private static function reconcileBool( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1322,9 +1318,7 @@ private static function reconcileFalse( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1407,9 +1401,7 @@ private static function reconcileTrue( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1482,9 +1474,7 @@ private static function reconcileScalar( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1575,9 +1565,7 @@ private static function reconcileNumeric( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1695,9 +1683,7 @@ private static function reconcileObject( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1753,9 +1739,7 @@ private static function reconcileResource( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1825,9 +1809,7 @@ private static function reconcileCountable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1888,9 +1870,7 @@ private static function reconcileIterable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1932,9 +1912,7 @@ private static function reconcileInArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } return $intersection; @@ -2250,9 +2228,7 @@ private static function reconcileTraversable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2362,9 +2338,7 @@ private static function reconcileArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2469,9 +2443,7 @@ private static function reconcileList( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2677,6 +2649,13 @@ private static function reconcileCallable( $redundant = false; $callable_types[] = $type; + } elseif ($candidate_callable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $type, + )) { + $redundant = false; + + $callable_types[] = $candidate_callable; } else { $redundant = false; } @@ -2703,9 +2682,7 @@ private static function reconcileCallable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index eb5a4f03e83..daedd9307fb 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -7,6 +7,7 @@ use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Internal\Codebase\InternalCallMapHandler; +use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Issue\DocblockTypeContradiction; use Psalm\Issue\RedundantPropertyInitializationCheck; use Psalm\Issue\TypeDoesNotContainType; @@ -142,9 +143,7 @@ public static function reconcile( } } - return $existing_var_type->from_docblock - ? Type::getNull() - : Type::getNever(); + return Type::getNever(); } return Type::getNull(); @@ -417,6 +416,8 @@ public static function reconcile( if ($assertion_type instanceof TCallable) { return self::reconcileCallable( $existing_var_type, + $codebase, + $assertion_type, ); } @@ -425,6 +426,8 @@ public static function reconcile( private static function reconcileCallable( Union $existing_var_type, + Codebase $codebase, + TCallable $assertion_type, ): Union { $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) { @@ -432,10 +435,22 @@ private static function reconcileCallable( && InternalCallMapHandler::inCallMap($type->value) ) { $existing_var_type->removeType($atomic_key); + continue; } if ($type->isCallableType()) { $existing_var_type->removeType($atomic_key); + continue; + } + + $candidate_callable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $type, + $assertion_type, + ); + + if ($candidate_callable) { + $existing_var_type->removeType($atomic_key); } } @@ -508,9 +523,7 @@ private static function reconcileBool( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -704,9 +717,7 @@ private static function reconcileNull( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -786,9 +797,7 @@ private static function reconcileFalse( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -868,9 +877,7 @@ private static function reconcileTrue( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -925,9 +932,7 @@ private static function reconcileFalsyOrEmpty( $failed_reconciliation = 2; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } if ($redundant) { @@ -1140,9 +1145,7 @@ private static function reconcileScalar( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1241,9 +1244,7 @@ private static function reconcileObject( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1337,9 +1338,7 @@ private static function reconcileNumeric( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1439,9 +1438,7 @@ private static function reconcileInt( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1536,9 +1533,7 @@ private static function reconcileFloat( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1642,9 +1637,7 @@ private static function reconcileString( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1744,9 +1737,7 @@ private static function reconcileArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1816,9 +1807,7 @@ private static function reconcileResource( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index fca39e97927..aa01a7ae93e 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1333,6 +1333,7 @@ public static function getMappedGenericTypeParams( $old_params_offset = (int) array_search( $template->param_name, array_keys($input_class_storage->template_types), + true, ); $candidate_param_types[] = ($input_type_params[$old_params_offset] ?? Type::getMixed()) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 51edb33080c..143255333c2 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1515,7 +1515,7 @@ private static function getArrayTypeFromGenericParams( $generic_type_params[1], $objectlike_generic_type, $codebase, - $overwrite_empty_array, + false, $allow_mixed_union, ); } diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index f12029cafcb..da20ad4cc55 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -960,7 +960,11 @@ private static function getTypeFromGenericTree( } assert(count($parse_tree->children) === 2); - $get_int_range_bound = function (ParseTree $parse_tree, Union $generic_param, string $bound_name): ?int { + $get_int_range_bound = static function ( + ParseTree $parse_tree, + Union $generic_param, + string $bound_name, + ): ?int { if (!$parse_tree instanceof Value || count($generic_param->getAtomicTypes()) > 1 || (!$generic_param->getSingleAtomic() instanceof TLiteralInt @@ -972,7 +976,6 @@ private static function getTypeFromGenericTree( "Invalid type \"{$generic_param->getId()}\" as int $bound_name boundary", ); } - $generic_param_atomic = $generic_param->getSingleAtomic(); return $generic_param_atomic instanceof TLiteralInt ? $generic_param_atomic->value : null; }; diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index 57bb5c8b6c8..1b2dafe7031 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -9,9 +9,11 @@ use Psalm\Internal\Type\TypeAlias\InlineTypeAlias; use Psalm\Type; +use function array_slice; use function array_splice; use function array_unshift; use function count; +use function implode; use function in_array; use function is_numeric; use function preg_match; @@ -146,11 +148,9 @@ public static function tokenize(string $string_type, bool $ignore_space = true): $type_tokens[++$rtc] = [' ', $i - 1]; $type_tokens[++$rtc] = ['', $i]; } elseif ($was_space - && ($char === 'a' || $char === 'i') - && ($chars[$i + 1] ?? null) === 's' - && ($chars[$i + 2] ?? null) === ' ' + && in_array(implode('', array_slice($chars, $i, 3)), ['as ', 'is ', 'of ']) ) { - $type_tokens[++$rtc] = [$char . 's', $i - 1]; + $type_tokens[++$rtc] = [$char . $chars[$i+1], $i - 1]; $type_tokens[++$rtc] = ['', ++$i]; $was_char = false; continue; diff --git a/src/Psalm/Issue/MixedInferredReturnType.php b/src/Psalm/Issue/MixedInferredReturnType.php deleted file mode 100644 index b3943899f12..00000000000 --- a/src/Psalm/Issue/MixedInferredReturnType.php +++ /dev/null @@ -1,13 +0,0 @@ -disable_suppress_all ? false - : array_search('all', $suppressed_issues); + : array_search('all', $suppressed_issues, true); if ($suppress_all_position !== false) { if (is_int($suppress_all_position)) { @@ -644,6 +645,43 @@ public static function finish( } } + if ($codebase->config->find_unused_issue_handler_suppression) { + foreach ($codebase->config->getIssueHandlers() as $type => $handler) { + foreach ($handler->getFilters() as $filter) { + if ($filter->suppressions > 0 && $filter->getErrorLevel() == Config::REPORT_SUPPRESS) { + continue; + } + $issues_data['config'][] = new IssueData( + IssueData::SEVERITY_ERROR, + 0, + 0, + UnusedIssueHandlerSuppression::getIssueType(), + sprintf( + 'Suppressed issue type "%s" for %s was not thrown.', + $type, + str_replace( + $codebase->config->base_dir, + '', + implode(', ', [...$filter->getFiles(), ...$filter->getDirectories()]), + ), + ), + $codebase->config->source_filename ?? '', + '', + '', + '', + 0, + 0, + 0, + 0, + 0, + 0, + UnusedIssueHandlerSuppression::SHORTCODE, + UnusedIssueHandlerSuppression::ERROR_LEVEL, + ); + } + } + } + echo self::getOutput( $issues_data, $project_analyzer->stdout_report_options, @@ -950,7 +988,7 @@ public static function getOutput( return $output->create(); } - private static function alreadyEmitted(string $message): bool + public static function alreadyEmitted(string $message): bool { $sham = sha1($message); diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php index e40e26ce96b..46f1163f062 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php @@ -35,7 +35,7 @@ public function getFunctionId(): string } /** - * @return PhpParser\Node\Arg[] + * @return list */ public function getCallArgs(): array { diff --git a/src/Psalm/Progress/DefaultProgress.php b/src/Psalm/Progress/DefaultProgress.php index 57024cbf72f..64788f224cd 100644 --- a/src/Psalm/Progress/DefaultProgress.php +++ b/src/Psalm/Progress/DefaultProgress.php @@ -22,7 +22,7 @@ class DefaultProgress extends LongProgress public function taskDone(int $level): void { - if ($this->number_of_tasks > self::TOO_MANY_FILES) { + if ($this->fixed_size && $this->number_of_tasks > self::TOO_MANY_FILES) { ++$this->progress; // Source for rate limiting: diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index 4bfa064d8e4..6184afab39d 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -21,22 +21,27 @@ class LongProgress extends Progress protected int $progress = 0; + protected bool $fixed_size = false; + public function __construct(protected bool $print_errors = true, protected bool $print_infos = true) { } public function startScanningFiles(): void { + $this->fixed_size = false; $this->write('Scanning files...' . "\n"); } public function startAnalyzingFiles(): void { - $this->write('Analyzing files...' . "\n\n"); + $this->fixed_size = true; + $this->write("\n" . 'Analyzing files...' . "\n\n"); } public function startAlteringFiles(): void { + $this->fixed_size = true; $this->write('Altering files...' . "\n"); } @@ -51,8 +56,30 @@ public function start(int $number_of_tasks): void $this->progress = 0; } + public function expand(int $number_of_tasks): void + { + $this->number_of_tasks += $number_of_tasks; + } + public function taskDone(int $level): void { + if ($this->number_of_tasks === null) { + throw new LogicException('Progress::start() should be called before Progress::taskDone()'); + } + + ++$this->progress; + + if (!$this->fixed_size) { + if ($this->progress == 1 || $this->progress == $this->number_of_tasks || $this->progress % 10 == 0) { + $this->write(sprintf( + "\r%s / %s?", + $this->progress, + $this->number_of_tasks, + )); + } + return; + } + if ($level === 0 || ($level === 1 && !$this->print_infos) || !$this->print_errors) { $this->write(self::doesTerminalSupportUtf8() ? '░' : '_'); } elseif ($level === 1) { @@ -61,7 +88,6 @@ public function taskDone(int $level): void $this->write('E'); } - ++$this->progress; if (($this->progress % self::NUMBER_OF_COLUMNS) !== 0) { return; diff --git a/src/Psalm/Progress/Progress.php b/src/Psalm/Progress/Progress.php index f6313214775..248878ff0a1 100644 --- a/src/Psalm/Progress/Progress.php +++ b/src/Psalm/Progress/Progress.php @@ -46,6 +46,10 @@ public function start(int $number_of_tasks): void { } + public function expand(int $number_of_tasks): void + { + } + public function taskDone(int $level): void { } diff --git a/src/Psalm/Report/ByIssueLevelAndTypeReport.php b/src/Psalm/Report/ByIssueLevelAndTypeReport.php index 69445ce89e6..35f21b36dcc 100644 --- a/src/Psalm/Report/ByIssueLevelAndTypeReport.php +++ b/src/Psalm/Report/ByIssueLevelAndTypeReport.php @@ -182,7 +182,7 @@ private function sortIssuesByLevelAndType(): void { usort( $this->issues_data, - fn(IssueData $left, IssueData $right): int => [$left->error_level > 0, -$left->error_level, + static fn(IssueData $left, IssueData $right): int => [$left->error_level > 0, -$left->error_level, $left->type, $left->file_path, $left->file_name, $left->line_from] <=> [$right->error_level > 0, -$right->error_level, $right->type, $right->file_path, $right->file_name, $right->line_from], diff --git a/src/Psalm/Report/CountReport.php b/src/Psalm/Report/CountReport.php index a0ec59602ba..eb47b4c884b 100644 --- a/src/Psalm/Report/CountReport.php +++ b/src/Psalm/Report/CountReport.php @@ -21,7 +21,7 @@ public function create(): string $issue_type_counts[$issue_data->type] = 1; } } - uksort($issue_type_counts, function (string $a, string $b) use ($issue_type_counts): int { + uksort($issue_type_counts, static 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; diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 5fd48feb486..b3880b5b982 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -196,7 +196,7 @@ public function getHoverMarkdown(): string $params = count($this->params) > 0 ? "\n" . implode( ",\n", array_map( - function (FunctionLikeParameter $param): string { + static function (FunctionLikeParameter $param): string { $realType = $param->type ?: 'mixed'; return " {$realType} \${$param->name}"; }, @@ -224,7 +224,7 @@ public function getCompletionSignature(): string $symbol_text = 'function ' . $this->cased_name . '(' . implode( ',', array_map( - fn(FunctionLikeParameter $param): string => ($param->type ?: 'mixed') . ' $' . $param->name, + static fn(FunctionLikeParameter $param): string => ($param->type ?: 'mixed') . ' $' . $param->name, $this->params, ), ) . ') : ' . ($this->return_type ?: 'mixed'); diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 71afca107e3..38e09570959 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -39,9 +39,9 @@ private static function getValueTypeForNamedObject(array $cases, TNamedObject $a } return new Union(array_map( - function (EnumCaseStorage $case): Atomic { - assert($case->value !== null); // Backed enum must have a value - + static function (EnumCaseStorage $case): Atomic { + assert($case->value !== null); + // Backed enum must have a value return $case->value; }, array_values($cases), diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 3c7fbc7198d..735953ed454 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1819,3 +1819,9 @@ function time_nanosleep(int $seconds, int $nanoseconds): array|bool {} * @psalm-taint-sink sleep $timestamp */ function time_sleep_until(float $timestamp): bool {} + +/** + * @return ($return_array is true ? array|false : object|false) + * @psalm-ignore-falsable-return + */ +function get_browser(?string $user_agent = null, bool $return_array = false): object|array|false {} diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 43a7bb1f85c..0e3a935bd78 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -477,7 +477,7 @@ class EmptyIterator implements Iterator { } /** - * @template-extends SeekableIterator + * @template-extends DirectoryIterator */ class FilesystemIterator extends DirectoryIterator { @@ -523,7 +523,7 @@ class FilesystemIterator extends DirectoryIterator /** - * @template-extends SeekableIterator + * @template-extends FilesystemIterator */ class GlobIterator extends FilesystemIterator implements Countable { /** @@ -774,7 +774,7 @@ class RecursiveArrayIterator extends ArrayIterator implements RecursiveIterator const CHILD_ARRAYS_ONLY = 4 ; /** - * @return RecursiveArrayIterator + * @return ?RecursiveArrayIterator */ public function getChildren() {} diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 3e86431e581..4007be3f007 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -496,7 +496,7 @@ class ReflectionProperty implements Reflector public function isDefault(): bool {} /** - * @return int-mask-of + * @return int-mask-of * @psalm-pure */ public function getModifiers(): int {} diff --git a/stubs/extensions/decimal.phpstub b/stubs/extensions/decimal.phpstub index 90b329dab7f..7d699e8fc6b 100644 --- a/stubs/extensions/decimal.phpstub +++ b/stubs/extensions/decimal.phpstub @@ -3,6 +3,9 @@ namespace Decimal; /** * Copied from https://github.com/php-decimal/stubs/blob/master/Decimal.php + * with prefixed param replaced regular param using regex: + * @(?:psalm|phpstan)-param (.+?) (\$\w+)[^@]+?@param .+?\2 + * @param $1 $2 * * The MIT License (MIT) * Copyright (c) 2018 Rudi Theunissen @@ -52,8 +55,8 @@ final class Decimal implements \JsonSerializable * * Initializes a new instance using a given value and minimum precision. * - * @param Decimal|string|int $value - * @param int $precision + * @param Decimal|numeric-string|int $value + * @param int $precision * * @throws \BadMethodCallException if already constructed. * @throws \TypeError if the value is not a decimal, string, or integer. @@ -119,7 +122,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of adding this decimal to the given value. * @@ -135,7 +138,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of subtracting a given value from this decimal. * @@ -151,7 +154,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of multiplying this decimal by the given value. * @@ -167,7 +170,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of dividing this decimal by the given value. * @@ -187,7 +190,7 @@ final class Decimal implements \JsonSerializable * * @see Decimal::rem for the decimal remainder. * - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the remainder after dividing the integer value of this * decimal by the integer value of the given value @@ -204,7 +207,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the remainder after dividing this decimal by a given value. * @@ -222,7 +225,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $exponent The power to raise this decimal to. + * @param Decimal|numeric-string|int $exponent The power to raise this decimal to. * * @return Decimal the result of raising this decimal to a given power. * @@ -488,5 +491,5 @@ final class Decimal implements \JsonSerializable * * @return string */ - public function jsonSerialize() {} + public function jsonSerialize(): string {} } diff --git a/stubs/extensions/rdkafka.phpstub b/stubs/extensions/rdkafka.phpstub new file mode 100644 index 00000000000..af8c3a76e9e --- /dev/null +++ b/stubs/extensions/rdkafka.phpstub @@ -0,0 +1,1195 @@ + + */ + public function dump() + { + } + + /** + * @param string $name + * @param string $value + * + * @return void + */ + public function set($name, $value) + { + } + + /** + * @param TopicConf $topic_conf + * + * @return void + * @deprecated Set default topic settings normally like global configuration settings. + * + */ + public function setDefaultTopicConf(TopicConf $topic_conf) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setDrMsgCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setErrorCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setRebalanceCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setStatsCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setConsumeCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setOffsetCommitCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setLogCb(callable $callback) + { + } + } + + class Consumer extends \RdKafka + { + /** + * @param Conf $conf + */ + public function __construct(Conf $conf = null) + { + } + + /** + * @param string $topic_name + * @param TopicConf $topic_conf + * + * @return ConsumerTopic + */ + public function newTopic($topic_name, TopicConf $topic_conf = null) + { + } + + /** + * @return Queue + */ + public function newQueue() + { + } + } + + class ConsumerTopic extends Topic + { + private function __construct() + { + } + + /** + * @param int $partition + * @param int $timeout_ms + * + * @return Message|null + */ + public function consume($partition, $timeout_ms) + { + } + + /** + * @param int $partition + * @param int $offset + * @param Queue $queue + * + * @return void + */ + public function consumeQueueStart($partition, $offset, Queue $queue) + { + } + + /** + * @param int $partition + * @param int $offset + * + * @return void + */ + public function consumeStart($partition, $offset) + { + } + + /** + * @param int $partition + * + * @return void + */ + public function consumeStop($partition) + { + } + + /** + * @param int $partition + * @param int $offset + * + * @return void + */ + public function offsetStore($partition, $offset) + { + } + + /** + * @param int $partition + * @param int $timeout_ms + * @param int $batch_size + * + * @return array + * @throws \InvalidArgumentException + * @throws Exception + */ + public function consumeBatch($partition, $timeout_ms, $batch_size) + { + } + + /** + * @param int $partition + * @param int $timeout_ms + * @param callable $callback + * + * @return void + */ + public function consumeCallback($partition, $timeout_ms, callable $callback) + { + } + } + + class Exception extends \Exception + { + } + + class KafkaConsumer + { + /** + * @param Conf $conf + */ + public function __construct(Conf $conf) + { + } + + /** + * @param TopicPartition[] $topic_partitions + * + * @return void + * @throws Exception + */ + public function assign($topic_partitions = null) + { + } + + /** + * @param null|Message|TopicPartition[] $message_or_offsets + * + * @return void + * @throws Exception + */ + public function commit($message_or_offsets = null) + { + } + + /** + * @param string $message_or_offsets + * + * @return void + * @throws Exception + */ + public function commitAsync($message_or_offsets = null) + { + } + + /** + * @param int $timeout_ms + * + * @return Message + * @throws \InvalidArgumentException + * @throws Exception + */ + public function consume($timeout_ms) + { + } + + /** + * @return TopicPartition[] + * @throws Exception + */ + public function getAssignment() + { + } + + /** + * @param bool $all_topics + * @param KafkaConsumerTopic|null $only_topic + * @param int $timeout_ms + * + * @return Metadata + * @throws Exception + */ + public function getMetadata($all_topics, $only_topic, $timeout_ms) + { + } + + /** + * @return array + */ + public function getSubscription() + { + } + + /** + * @param string $topic_name + * @param TopicConf $topic_conf + * + * @return KafkaConsumerTopic + */ + public function newTopic($topic_name, TopicConf $topic_conf = null) + { + } + + /** + * @param array $topics + * + * @return void + * @throws Exception + */ + public function subscribe($topics) + { + } + + /** + * @return void + * @throws Exception + */ + public function unsubscribe() + { + } + + /** + * @param TopicPartition[] $topicPartitions + * @param int $timeout_ms Timeout in milliseconds + * + * @return TopicPartition[] + * @throws Exception + */ + public function getCommittedOffsets($topicPartitions, $timeout_ms) + { + } + + /** + * @param array $topicPartitions + * @param int $timeout_ms + * @return array + */ + public function offsetsForTimes($topicPartitions, $timeout_ms) + { + } + + /** + * @param string $topic + * @param int $partition + * @param int $low + * @param int $high + * @param int $timeout_ms + */ + public function queryWatermarkOffsets($topic, $partition, &$low, &$high, $timeout_ms) + { + } + + /** + * @param array $topics + * + * @return array + * @throws Exception + */ + public function getOffsetPositions($topics) + { + } + + /** + * @return void + */ + public function close() + { + } + + /** + * @param TopicPartition[] $topic_partitions + * @return TopicPartition[] + */ + public function pausePartitions($topic_partitions) + { + } + + /** + * @param TopicPartition[] $topic_partitions + * @return TopicPartition[] + */ + public function resumePartitions($topic_partitions) + { + } + } + + class KafkaConsumerTopic extends Topic + { + /** + * @param int $partition + * @param int $offset + * + * @return void + */ + public function offsetStore($partition, $offset) + { + } + } + + class KafkaErrorException extends \Exception + { + /** + * @param string $message + * @param int $code + * @param string $errorString + * @param boolean $isFatal + * @param boolean $isRetriable + * @param boolean $transactionRequiresAbort + */ + public function __construct($message, $code, $errorString, $isFatal, $isRetriable, $transactionRequiresAbort) + { + parent::__construct($message, $code); + } + + /** + * @returns string + */ + public function getErrorString() + { + } + + /** + * @returns boolean + */ + public function isFatal() + { + } + + /** + * @returns boolean + */ + public function isRetriable() + { + } + + /** + * @returns boolean + */ + public function transactionRequiresAbort() + { + } + } + + class Message + { + /** + * @var int + */ + public $err; + + /** + * @var string + */ + public $topic_name; + + /** + * @var int + */ + public $partition; + + /** + * @var string|null + */ + public $payload; + + /** + * @var int|null + */ + public $len; + + /** + * @var string|null + */ + public $key; + + /** + * @var int + */ + public $offset; + + /** + * @var int + */ + public $timestamp; + + /** + * @var array|null + */ + public $headers; + + /** + * @var string|null + */ + public $opaque; + + /** + * @return string + */ + public function errstr() + { + } + } + + class Metadata + { + /** + * @return \RdKafka\Metadata\Collection|\RdKafka\Metadata\Broker[] + */ + public function getBrokers() + { + } + + /** + * @return \RdKafka\Metadata\Collection|\RdKafka\Metadata\Topic[] + */ + public function getTopics() + { + } + + /** + * @return int + */ + public function getOrigBrokerId() + { + } + + /** + * @return string + */ + public function getOrigBrokerName() + { + } + } + + class Producer extends \RdKafka + { + /** + * @param Conf $conf + */ + public function __construct(Conf $conf = null) + { + } + + /** + * @param string $topic_name + * @param TopicConf $topic_conf + * + * @return ProducerTopic + */ + public function newTopic($topic_name, TopicConf $topic_conf = null) + { + } + + /** + * @param int $timeoutMs + * + * @return void + * @throws KafkaErrorException + */ + public function initTransactions(int $timeoutMs) + { + } + + /** + * @return void + * @throws KafkaErrorException + */ + public function beginTransaction() + { + } + + /** + * @param int $timeoutMs + * + * @return void + * @throws KafkaErrorException + */ + public function commitTransaction(int $timeoutMs) + { + } + + /** + * @param int $timeoutMs + * + * @return void + * @throws KafkaErrorException + */ + public function abortTransaction(int $timeoutMs) + { + } + } + + class ProducerTopic extends Topic + { + private function __construct() + { + } + + /** + * @param int $partition + * @param int $msgflags + * @param string $payload + * @param string $key + * + * @return void + * @throws Exception + */ + public function produce($partition, $msgflags, $payload, $key = null) + { + } + + /** + * @param int $partition + * @param int $msgflags + * @param string $payload + * @param string|null $key + * @param array|null $headers + * @param int $timestamp_ms + * + * @throws Exception + */ + public function producev($partition, $msgflags, $payload, $key = null, $headers = null, $timestamp_ms = null) + { + } + } + + class Queue + { + private function __construct() + { + } + + /** + * @param string $timeout_ms + * + * @return Message|null + */ + public function consume($timeout_ms) + { + } + } + + abstract class Topic + { + /** + * @return string + */ + public function getName() + { + } + } + + /** + * Configuration reference: https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md + */ + class TopicConf + { + /** + * @return array + */ + public function dump() + { + } + + /** + * @param string $name + * @param string $value + * + * @return void + */ + public function set($name, $value) + { + } + + /** + * @param int $partitioner + * + * @return void + */ + public function setPartitioner($partitioner) + { + } + } + + class TopicPartition + { + /** + * @param string $topic + * @param int $partition + * @param int $offset + */ + public function __construct($topic, $partition, $offset = null) + { + } + + /** + * @return int + */ + public function getOffset() + { + } + + /** + * @return int + */ + public function getPartition() + { + } + + /** + * @return string + */ + public function getTopic() + { + } + + /** + * @param int $offset + * + * @return void + */ + public function setOffset($offset) + { + } + + /** + * @param int $partition + * + * @return void + */ + public function setPartition($partition) + { + } + + /** + * @param string $topic_name + * + * @return void + */ + public function setTopic($topic_name) + { + } + } +} + +namespace RdKafka\Metadata { + + class Broker + { + /** + * @return int + */ + public function getId() + { + } + + /** + * @return string + */ + public function getHost() + { + } + + /** + * @return int + */ + public function getPort() + { + } + } + + class Collection implements \Iterator, \Countable + { + /** + * @return mixed + */ + public function current() + { + } + + /** + * @return void + */ + public function next() + { + } + + /** + * @return mixed + */ + public function key() + { + } + + /** + * @return boolean + */ + public function valid() + { + } + + /** + * @return void + */ + public function rewind() + { + } + + /** + * @return int + */ + public function count() + { + } + } + + class Partition + { + /** + * @return int + */ + public function getId() + { + } + + /** + * @return mixed + */ + public function getErr() + { + } + + /** + * @return mixed + */ + public function getLeader() + { + } + + /** + * @return mixed + */ + public function getReplicas() + { + } + + /** + * @return mixed + */ + public function getIsrs() + { + } + } + + class Topic + { + /** + * @return string + */ + public function getTopic() + { + } + + /** + * @return Partition[] + */ + public function getPartitions() + { + } + + /** + * @return mixed + */ + public function getErr() + { + } + } +} \ No newline at end of file diff --git a/stubs/extensions/redis.phpstub b/stubs/extensions/redis.phpstub index 161482da359..e7512809981 100644 --- a/stubs/extensions/redis.phpstub +++ b/stubs/extensions/redis.phpstub @@ -275,9 +275,15 @@ class Redis { public function move(string $key, int $index): bool {} - public function mset(array $key_values): bool {} + /** + * @param array + */ + public function mset($key_values): Redis|bool {} - public function msetnx(array $key_values): bool {} + /** + * @param array + */ + public function msetnx($key_values): Redis|bool {} public function multi(int $value = Redis::MULTI): bool|Redis {} diff --git a/stubs/extensions/soap.phpstub b/stubs/extensions/soap.phpstub index 8a3fafa4dcd..dac3ece837c 100644 --- a/stubs/extensions/soap.phpstub +++ b/stubs/extensions/soap.phpstub @@ -1,5 +1,95 @@ [ + 'code' => ' [ 'code' => ' 'InvalidDocblock', ], - 'noCrashOnInvalidClassTemplateAsType' => [ + 'SKIPPED-noCrashOnInvalidClassTemplateAsType' => [ 'code' => ' 'InvalidDocblock', ], - 'noCrashOnInvalidFunctionTemplateAsType' => [ + 'SKIPPED-noCrashOnInvalidFunctionTemplateAsType' => [ 'code' => 'data[] = $value; } else { - $this->data[$name] = $value; + $this->data[$offset] = $value; } } @@ -1055,12 +1055,12 @@ public function offsetGet($name) } /** - * @param string $name + * @param string $offset * @param mixed $value */ - public function offsetSet($name, $value) : void + public function offsetSet($offset, $value) : void { - $this->data[$name] = $value; + $this->data[$offset] = $value; } public function __isset(string $name) : bool diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 91a319f2fb3..990a3bccbe9 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1045,17 +1045,20 @@ function foo(array $arr) : void { * @template-implements ArrayAccess */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(?int $offset, string $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'checkEmptinessAfterConditionalArrayAdjustment' => [ 'code' => ' */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(int $offset, string $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'conditionalRestrictedDocblockKeyAssignment' => [ 'code' => ' [ + 'code' => '> $l*/ + $l = []; + $l[] = [];', + 'assertions' => [ + '$l===' => 'non-empty-array>', + ], + ], ]; } @@ -2250,9 +2265,6 @@ function getCachedMixed(array $cache, string $locale) : string { ], 'mergeWithDeeplyNestedArray' => [ 'code' => ' 'array|null>', ], ], + 'arrayFilterObject' => [ + 'code' => ' [ + '$e' => 'array, object>', + ], + ], + 'arrayFilterStringCallable' => [ + 'code' => ' $bar + */ + $keys = array_keys($bar); + $strings = array_filter($keys, $arg);', + 'assertions' => [ + '$strings' => 'array, string>', + ], + ], + 'arrayFilterMixed' => [ + 'code' => ' [ + '$x' => 'array', + ], + ], 'positiveIntArrayFilter' => [ 'code' => ' [ + 'code' => ' $arg + */ + $a = array_filter($arg, "strlen", ARRAY_FILTER_USE_KEY);', + 'assertions' => [ + '$a' => 'array', + ], + ], + 'arrayFilterUseBothCallback' => [ + 'code' => ' $arg + */ + $a = array_filter($arg, function (int $v, int $k) { return ($v > $k);}, ARRAY_FILTER_USE_BOTH);', + 'assertions' => [ + '$a' => 'array, int>', + ], + ], 'arrayKeysNonEmpty' => [ 'code' => ' 1, "b" => 2]);', @@ -2483,9 +2536,30 @@ function bar(array $list) : void { * @param array $foos */ function foo(array $foos): void { - array_multisort($formLayoutFields, SORT_ASC, array_column($foos, "y")); + array_multisort(array_column($foos, "y"), SORT_ASC, $foos); }', ], + 'arrayMultisortSortRestByRef' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + SORT_NATURAL|SORT_FLAG_CASE, + $test + );', + 'assertions' => [ + '$test' => 'non-empty-array', + ], + ], + 'arrayMultisortSort' => [ + 'code' => ' $test */ + array_multisort($test);', + 'assertions' => [ + '$test' => 'non-empty-array', + ], + ], 'arrayMapGenericObject' => [ 'code' => ' [ - 'code' => ' 5, "b" => 12, "c" => null], - function(?int $i) { - return $GLOBALS["a"]; - } - );', - 'error_message' => 'MixedArgumentTypeCoercion', - 'ignored_issues' => ['MissingClosureParamType', 'MissingClosureReturnType'], - ], 'arrayFilterUseMethodOnInferrableInt' => [ 'code' => 'foo(); });', 'error_message' => 'InvalidMethodCall', ], + 'arrayFilterThirdArgWillNotBeUsedWhenSecondNull' => [ + 'code' => ' 'InvalidArgument', + 'ignored_issues' => [], + 'php_version' => '8.0', + ], + 'arrayFilterThirdArgInvalidBehavesLike0' => [ + 'code' => ' 'PossiblyInvalidArgument', + ], + 'arrayFilterCallbackValidationThirdArg0' => [ + 'code' => ' $arg + */ + array_filter($arg, "abs", 0);', + 'error_message' => 'InvalidArgument', + ], + 'arrayFilterKeyCallbackLiteral' => [ + 'code' => ' 5, "b" => 12, "c" => null], "abs", ARRAY_FILTER_USE_KEY);', + 'error_message' => 'InvalidArgument', + ], + 'arrayFilterBothCallback' => [ + 'code' => ' $arg + */ + array_filter($arg, "strlen", ARRAY_FILTER_USE_BOTH);', + 'error_message' => 'InvalidArgument', + ], + 'arrayFilterKeyCallback' => [ + 'code' => ' $arg + */ + array_filter($arg, "strlen", ARRAY_FILTER_USE_KEY);', + 'error_message' => 'InvalidScalarArgument', + ], 'arrayMapUseMethodOnInferrableInt' => [ 'code' => 'foo(); }, [1, 2, 3, 4]);', @@ -2681,7 +2785,7 @@ function foo(int $i, string $s) : bool { } array_filter([1, 2, 3], "foo");', - 'error_message' => 'TooFewArguments', + 'error_message' => 'InvalidArgument', ], 'arrayMapBadArgs' => [ 'code' => ' 'InvalidArgument', ], + 'arrayMultisortInvalidFlag' => [ + 'code' => '> $test */ + array_multisort( + $test, + SORT_FLAG_CASE, + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 2 of array_multisort sort order/flag contains an invalid value of 8', + ], + 'arrayMultisortInvalidSortFlags' => [ + 'code' => '> $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + SORT_ASC, + $test + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 3 of array_multisort contains sort order flags and can only be used after an array parameter', + ], + 'arrayMultisortInvalidSortAfterFlags' => [ + 'code' => '> $test */ + array_multisort( + array_column($test, "s"), + SORT_NATURAL, + SORT_DESC, + $test + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 3 of array_multisort contains sort order flags and can only be used after an array parameter', + ], + 'arrayMultisortInvalidFlagsAfterFlags' => [ + 'code' => '> $test */ + array_multisort( + array_column($test, "s"), + $test, + SORT_NATURAL|SORT_FLAG_CASE, + SORT_LOCALE_STRING, + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 4 of array_multisort are sort flags and cannot be used after a parameter with sort flags', + ], + 'arrayMultisortNoByRef' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + array_column($test, "id") + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - At least 1 array argument of array_multisort must be a variable, since the sorting happens by reference and otherwise this function call does nothing', + ], + 'arrayMultisortNotByRefAfterLastByRef' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + $test, + SORT_ASC, + array_column($test, "id"), + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - All arguments of array_multisort after argument 4, which are after the last by reference passed array argument and its flags, are redundant and can be removed, since the sorting happens by reference', + ], + 'arrayMultisortNotByRefAfterLastByRefWithFlag' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + $test, + SORT_ASC, + array_column($test, "id"), + SORT_NATURAL + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - All arguments of array_multisort after argument 4, which are after the last by reference passed array argument and its flags, are redundant and can be removed, since the sorting happens by reference', + ], ]; } } diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index 0e89d76594e..09c0eb81037 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -154,7 +154,7 @@ public static function assertArrayKeysAreStrings(array $array, string $message = */ public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void { - $isZeroOrString = /** @param mixed $key */ fn($key): bool => $key === 0 || is_string($key); + $isZeroOrString = /** @param mixed $key */ static fn($key): bool => $key === 0 || is_string($key); $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 1603c7d8436..072779059b0 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -254,6 +254,32 @@ public function getIterator() 'ignored_issues' => [], 'php_version' => '8.1', ], + 'returnTypeWillChangeNoSignatureType' => [ + 'code' => ' $arg + * @return string + */ + public function run($arg) : string { + return implode("s", $arg); + } + } + + class Bar extends Foo { + /** + * @param array $arg + * @return string + */ + #[ReturnTypeWillChange] + public function run($arg) { + return implode(" ", $arg); + } + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'allowDynamicProperties' => [ 'code' => ' 'float|int', ], ], + 'incrementInLoop' => [ + 'code' => ' [ + '$i' => 'int<0, 10>', + '$j' => 'int<100, 110>', + ], + ], + 'decrementInLoop' => [ + 'code' => ' 0; $i--) { + if (rand(0,1)) { + break; + } + } + for ($j = 110; $j > 100; $j--) { + if (rand(0,1)) { + break; + } + }', + 'assertions' => [ + '$i' => 'int<0, 10>', + '$j' => 'int<100, 110>', + ], + ], 'coalesceFilterOutNullEvenWithTernary' => [ 'code' => ' [ "UndefinedThisPropertyFetch: Instance property A::\$foo is not defined", "MixedReturnStatement: Could not infer a return type", - "MixedInferredReturnType: Could not verify return type 'string' for A::bar", ], ], ], diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 4dc185ca6d2..a2e292ce0d4 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1807,6 +1807,30 @@ function foo($arg): void {} foo(["a", "b"]);', ], + 'callableOptionalOrAdditionalOptional' => [ + 'code' => ' [], + 'ignored_issues' => ['InvalidReturnType'], + ], 'abstractInvokeInTrait' => [ 'code' => ' 'InvalidFunctionCall', - 'ignored_issues' => ['UndefinedClass', 'MixedInferredReturnType'], + 'ignored_issues' => ['UndefinedClass'], ], 'undefinedCallableMethodFullString' => [ 'code' => ' 'InvalidArgument', ], + 'invalidArrayCallable' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableMissingOptional' => [ + 'code' => ' 5 ? true : false; + } + + foo("bar");', + 'error_message' => 'PossiblyInvalidArgument', + ], + 'callableMissingOptionalThisArray' => [ + 'code' => ' 'PossiblyInvalidArgument', + ], + 'callableMissingOptionalVariableInstanceArray' => [ + 'code' => ' 'PossiblyInvalidArgument', + ], + 'callableMissingOptionalMultipleParams' => [ + 'code' => ' 'PossiblyInvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], + 'callableMissingRequiredMultipleParams' => [ + 'code' => ' 'PossiblyInvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], + 'callableAdditionalRequiredParam' => [ + 'code' => ' 'InvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], + 'callableMultipleParamsWithOptional' => [ + 'code' => ' 'PossiblyInvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], 'preventStringDocblockType' => [ 'code' => ' [], 'ignored_issues' => [ 'UndefinedClass', - 'MixedInferredReturnType', 'InvalidArgument', ], ], @@ -356,7 +355,6 @@ function foo() : D { 'assertions' => [], 'ignored_issues' => [ 'UndefinedClass', - 'MixedInferredReturnType', 'InvalidArgument', ], ], diff --git a/tests/CodebaseTest.php b/tests/CodebaseTest.php index 0a176430798..d10d9201411 100644 --- a/tests/CodebaseTest.php +++ b/tests/CodebaseTest.php @@ -161,7 +161,7 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) ? (string)$stmt->extends->getAttribute('resolvedName') : ''; $storage->custom_metadata['implements'] = array_map( - fn(Name $aspect): string => (string)$aspect->getAttribute('resolvedName'), + static fn(Name $aspect): string => (string)$aspect->getAttribute('resolvedName'), $stmt->implements, ); $storage->custom_metadata['a'] = 'b'; diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 9fa8042c55c..34a5b06f976 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -1108,7 +1108,7 @@ public function testAllPossibleIssues(): void * @param string $issue_name * @return string */ - fn($issue_name): string => '<' . $issue_name . ' errorLevel="suppress" />' . "\n", + static fn($issue_name): string => '<' . $issue_name . ' errorLevel="suppress" />' . "\n", IssueHandler::getAllIssueTypes(), ), ); diff --git a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index fb2853f3530..cb3284754f8 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -56,11 +56,10 @@ public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $e $custom_array_map_storage->return_type = self::createReturnType($all_expected_callables); $custom_array_map_storage->params = [ ...array_map( - function (TCallable $expected, int $offset) { + static function (TCallable $expected, int $offset) { $t = new Union([$expected]); $param = new FunctionLikeParameter('fn' . $offset, false, $t, $t); $param->is_optional = false; - return $param; }, $all_expected_callables, diff --git a/tests/DocblockInheritanceTest.php b/tests/DocblockInheritanceTest.php index 792c7972b86..84c50b6366b 100644 --- a/tests/DocblockInheritanceTest.php +++ b/tests/DocblockInheritanceTest.php @@ -149,6 +149,32 @@ function takesF(F $f) : B { return $f->map(); }', ], + 'inheritCorrectParamOnTypeChange' => [ + 'code' => '|int $className */ + public function a(array|int $className): int + { + return 0; + } + } + + class B extends A + { + public function a(array|int|bool $className): int + { + return 0; + } + } + + print_r((new A)->a(1)); + print_r((new B)->a(true)); + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index 9768eecfa83..cee36ca3a30 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -18,6 +18,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Issue\UnusedBaselineEntry; +use Psalm\Issue\UnusedIssueHandlerSuppression; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use UnexpectedValueException; @@ -270,6 +271,7 @@ public function providerInvalidCodeParse(): array case 'TraitMethodSignatureMismatch': case 'UncaughtThrowInGlobalScope': case UnusedBaselineEntry::getIssueType(): + case UnusedIssueHandlerSuppression::getIssueType(): continue 2; /** @todo reinstate this test when the issue is restored */ @@ -288,10 +290,6 @@ public function providerInvalidCodeParse(): array $ignored_issues = ['InvalidReturnStatement']; break; - case 'MixedInferredReturnType': - $ignored_issues = ['MixedReturnStatement']; - break; - case 'MixedStringOffsetAssignment': $ignored_issues = ['MixedAssignment']; break; @@ -348,7 +346,7 @@ public function testShortcodesAreUnique(): void $duplicate_shortcodes = array_filter( $all_shortcodes, - fn($issues): bool => count($issues) > 1 + static fn($issues): bool => count($issues) > 1 ); $this->assertEquals( diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 35df957b6b3..6991b89ae37 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -633,6 +633,9 @@ function noop(string $s): string noop($foo); noop(FooEnum::Foo->value); PHP, + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', ], 'backedEnumCaseValueFromClassConstant' => [ 'code' => <<<'PHP' @@ -654,6 +657,28 @@ enum BarEnum: int { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'stringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1104,6 +1129,50 @@ enum Bar: int 'ignored_issues' => [], 'php_version' => '8.1', ], + 'invalidStringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidStringBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } diff --git a/tests/ExpressionTest.php b/tests/ExpressionTest.php index 15fb517c116..5c63430829f 100644 --- a/tests/ExpressionTest.php +++ b/tests/ExpressionTest.php @@ -53,6 +53,14 @@ public function providerValidCodeParse(): iterable '$a===' => 'array{9223372036854775806: 0, 9223372036854775807: 1}', ], ]; + yield 'shellExecConcatInt' => [ + 'code' => <<<'PHP' + [$arr[2], $arr[3]], + static fn(array $arr): array => [$arr[2], $arr[3]], $diff[3], ); @@ -135,7 +135,7 @@ public function testPartialAstDiff( * @param array{0: int, 1: int, 2: int, 3: int} $arr * @return array{0: int, 1: int} */ - fn(array $arr): array => [$arr[2], $arr[3]], + static fn(array $arr): array => [$arr[2], $arr[3]], $diff[3], ); diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index d012e72fa9e..a9599e5e9e9 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -917,7 +917,7 @@ function portismaybeint(string $s) : ? int { '$porta' => 'false|int|null', '$porte' => 'false|int|null', ], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'parseUrlComponent' => [ 'code' => ' 'int<-4, 4>', '$i===' => 'int', '$j===' => 'int', - '$k===' => 'never', + '$k===' => 'mixed', '$l===' => 'int', '$m===' => 'int<0, max>', '$n===' => 'int', - '$o===' => 'never', + '$o===' => 'mixed', '$p===' => 'int', - '$q===' => 'never', + '$q===' => 'mixed', '$r===' => 'int<0, 2>', '$s===' => 'int<-2, 0>', - '$t===' => 'never', + '$t===' => 'mixed', '$u===' => 'int<-2, 0>', '$v===' => 'int<2, 0>', - '$w===' => 'never', + '$w===' => 'mixed', '$x===' => 'int<0, 2>', '$y===' => 'int<-2, 0>', - '$z===' => 'never', + '$z===' => 'mixed', '$aa===' => 'int<-2, 2>', '$ab===' => 'int<-2, 2>', ], diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php new file mode 100644 index 00000000000..704074560dc --- /dev/null +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -0,0 +1,584 @@ +file_provider = new FakeFileProvider(); + + $config = new TestConfig(); + + $providers = new Providers( + $this->file_provider, + new ParserInstanceCacheProvider(), + null, + null, + new FakeFileReferenceCacheProvider(), + new ProjectCacheProvider(), + ); + + $this->codebase = new Codebase($config, $providers); + + $this->project_analyzer = new ProjectAnalyzer( + $config, + $providers, + null, + [], + 1, + null, + $this->codebase, + ); + + $this->project_analyzer->setPhpVersion('7.3', 'tests'); + $this->project_analyzer->getCodebase()->store_node_types = true; + + $this->codebase->config->throw_exception = false; + } + + /** + * @return list + */ + protected function getCompletionLabels(string $content, string $class_name, string $gap): array + { + $this->addFile('somefile.php', $content); + + $this->analyzeFile('somefile.php', new Context()); + + $items = $this->codebase->getCompletionItemsForClassishThing($class_name, $gap, true); + + return array_map(fn($item) => $item->label, $items); + } + + /** + * @return iterable + */ + public function providerGaps(): iterable + { + return [ + 'object-gap' => ['->'], + 'static-gap' => ['::'], + ]; + } + + /** + * @dataProvider providerGaps + */ + public function testSimpleOnceClass(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + '::' => [ + 'magicStaticMethod', + + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testAbstractClass(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', + + 'abstractPublicMethod', + 'abstractProtectedMethod', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + '::' => [ + 'magicStaticMethod', + + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testUseTrait(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + 'magicStaticMethod', + + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', + + 'abstractPublicMethod', + 'abstractProtectedMethod', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + '::' => [ + 'magicStaticMethod', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testUseTraitWithAbstractClass(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + 'magicStaticMethod', + + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', + + 'abstractPublicMethod', + 'abstractProtectedMethod', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + '::' => [ + 'magicStaticMethod', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testClassWithExtends(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + 'magicStaticMethod', + + 'publicObjProp', + 'protectedObjProp', + + 'publicObjMethod', + 'protectedObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + ], + '::' => [ + 'magicStaticMethod', + 'publicStaticProp', + 'protectedStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testAstractClassWithInterface(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'publicObjMethod', + 'protectedObjMethod', + ], + '::' => [], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testClassWithAnnotationMixin(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + 'magicObjMethod', + + 'publicObjProp', + + 'publicObjMethod', + + 'publicStaticMethod', + ], + '::' => [], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + public function testResolveCollisionWithMixin(): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', '->'); + + $expected_labels = [ + 'myObjProp', + ]; + + $this->assertEqualsCanonicalizing($expected_labels, $actual_labels); + } +} diff --git a/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php b/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php index 096154de6e1..657ac4a2d93 100644 --- a/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php +++ b/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php @@ -12,7 +12,7 @@ class ClassLikeStorageInstanceCacheProvider extends ClassLikeStorageCacheProvider { - /** @var array */ + /** @var array */ private array $cache = []; public function __construct() @@ -25,6 +25,9 @@ public function writeToCache(ClassLikeStorage $storage, ?string $file_path, ?str $this->cache[$fq_classlike_name_lc] = $storage; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ public function getLatestFromCache(string $fq_classlike_name_lc, ?string $file_path, ?string $file_contents): ClassLikeStorage { $cached_value = $this->loadFromCache($fq_classlike_name_lc); @@ -36,6 +39,9 @@ public function getLatestFromCache(string $fq_classlike_name_lc, ?string $file_p return $cached_value; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function loadFromCache(string $fq_classlike_name_lc): ?ClassLikeStorage { return $this->cache[$fq_classlike_name_lc] ?? null; diff --git a/tests/JsonOutputTest.php b/tests/JsonOutputTest.php index 09cc0728981..7f5dc3ec67f 100644 --- a/tests/JsonOutputTest.php +++ b/tests/JsonOutputTest.php @@ -90,7 +90,7 @@ function fooFoo(int $a): string { function fooFoo(int $a): int { return $b + 1; }', - 'error_count' => 5, + 'error_count' => 4, 'message' => 'Cannot find referenced variable $b', 'line' => 3, 'error' => '$b', @@ -100,7 +100,7 @@ function fooFoo(int $a): int { function fooFoo(Badger\Bodger $a): Badger\Bodger { return $a; }', - 'error_count' => 3, + 'error_count' => 2, 'message' => 'Class, interface or enum named Badger\\Bodger does not exist', 'line' => 2, 'error' => 'Badger\\Bodger', diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index 7e6cb52bdae..4fa2518a624 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -17,6 +17,7 @@ use Psalm\Tests\TestConfig; use Psalm\Type; +use function array_map; use function count; class CompletionTest extends TestCase @@ -372,7 +373,7 @@ public function foo() : void { $codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $this->assertNull($codebase->getCompletionDataAtPosition('somefile.php', new Position(16, 41))); + $this->assertSame(['B\C', '->', 456], $codebase->getCompletionDataAtPosition('somefile.php', new Position(16, 41))); } public function testCompletionOnTemplatedThisProperty(): void @@ -727,6 +728,201 @@ public function baz() {} $this->assertSame('baz()', $completion_items[1]->insertText); } + public function testObjectPropertyOnAppendToEnd(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'aPr + } + }', + ); + + $codebase->file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A&static', '->', 223], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['aProp'], $completion_item_texts); + } + + public function testObjectPropertyOnReplaceEndPart(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'aProp2; + } + }', + ); + + $codebase->file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A&static', '->', 225], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['aProp1', 'aProp2'], $completion_item_texts); + } + + public function testSelfPropertyOnAppendToEnd(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A', '::', 237], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['$aProp'], $completion_item_texts); + } + + public function testStaticPropertyOnAppendToEnd(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 36); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A', '::', 239], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['$aProp'], $completion_item_texts); + } + + public function testStaticPropertyOnReplaceEndPart(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A', '::', 239], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['$aProp1', '$aProp2'], $completion_item_texts); + } + public function testCompletionOnNewExceptionWithoutNamespace(): void { $codebase = $this->codebase; @@ -1262,6 +1458,38 @@ static function add() : void { $this->assertCount(2, $completion_items); } + public function testCompletionStaticMethodOnDocBlock(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(7, 23); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + + $this->assertSame(['Bar\Alpha', '::', 177], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $this->assertCount(1, $completion_items); + $this->assertSame('foo()', $completion_items[0]->insertText); + } + public function testCompletionOnClassInstanceReferenceWithAssignmentAfter(): void { diff --git a/tests/Loop/ForTest.php b/tests/Loop/ForTest.php index f8fd5f2b22b..037893b1ef4 100644 --- a/tests/Loop/ForTest.php +++ b/tests/Loop/ForTest.php @@ -143,7 +143,7 @@ function test(Node $head) { * @param list $arr */ function cartesianProduct(array $arr) : void { - for ($i = 20; $arr[$i] === 5 && $i > 0; $i--) {} + for ($i = 20; $i > 0 && $arr[$i] === 5 ; $i--) {} }', ], 'noCrashOnLongThing' => [ diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index ad157dad39b..e6737ab0d78 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -824,7 +824,7 @@ function consumeInt(int $i): void {} 'callUsingParent' => [ 'code' => ' [ + 'code' => ' [], + 'ignored_issues' => ['ParamNameMismatch'], + ], ]; } @@ -1118,6 +1133,21 @@ class B extends A {} $b->foo();', 'error_message' => 'UndefinedMagicMethod', ], + 'inheritSealedMethodsWithoutPrefix' => [ + 'code' => 'foo();', + 'error_message' => 'UndefinedMagicMethod', + ], 'lonelyMethod' => [ 'code' => ' 'UndefinedVariable', ], + 'MagicMethodReturnTypesCheckedForClasses' => [ + 'code' => ' 'ImplementedReturnTypeMismatch', + ], + 'MagicMethodParamTypesCheckedForClasses' => [ + 'code' => ' 'ImplementedParamTypeMismatch', + ], + 'MagicMethodReturnTypesCheckedForInterfaces' => [ + 'code' => ' 'ImplementedReturnTypeMismatch', + ], + 'MagicMethodParamTypesCheckedForInterfaces' => [ + 'code' => ' 'ImplementedParamTypeMismatch', + ], + 'MagicMethodMadeConcreteChecksParams' => [ + 'code' => ' 'ImplementedParamTypeMismatch', + ], ]; } diff --git a/tests/MagicPropertyTest.php b/tests/MagicPropertyTest.php index d0e340719a5..abb03aed1a0 100644 --- a/tests/MagicPropertyTest.php +++ b/tests/MagicPropertyTest.php @@ -398,7 +398,7 @@ public function __get(string $name) : string { } }', 'assertions' => [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'overrideInheritedProperty' => [ 'code' => ' 'InvalidDocblock', ], + 'sealedWithNoProperties' => [ + 'code' => 'errors;', + 'error_message' => 'UndefinedMagicPropertyFetch', + ], + 'sealedWithNoPropertiesNoPrefix' => [ + 'code' => 'errors;', + 'error_message' => 'UndefinedMagicPropertyFetch', + ], ]; } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index f77eb36203e..f0ae5b7db77 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1150,7 +1150,7 @@ public static function createFromInterface(\DateTimeInterface $datetime): static } }', 'assertions' => [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], 'php_version' => '8.0', ], 'nullsafeShortCircuit' => [ @@ -1342,7 +1342,7 @@ public function returns_nullable_class() { } }', 'error_message' => 'LessSpecificReturnStatement', - 'ignored_issues' => ['MixedInferredReturnType', 'MixedReturnStatement', 'MixedMethodCall'], + 'ignored_issues' => ['MixedReturnStatement', 'MixedMethodCall'], ], 'undefinedVariableStaticCall' => [ 'code' => ' 'B', ], ], + 'returnIgnoresInlineComments' => [ + 'code' => ' [ 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'doesNotRequireInterfaceDestructorsToHaveReturnType' => [ 'code' => ' 'MethodSignatureMismatch', ], + 'methodAnnotationReturnMismatch' => [ + 'code' => ' 'MismatchingDocblockReturnType', + ], + 'methodAnnotationParamMismatch' => [ + 'code' => ' 'MismatchingDocblockParamType', + ], ]; } } diff --git a/tests/MixinAnnotationTest.php b/tests/MixinAnnotationTest.php index f4f0372cd13..807fc4a57a5 100644 --- a/tests/MixinAnnotationTest.php +++ b/tests/MixinAnnotationTest.php @@ -596,6 +596,28 @@ class FooModel extends Model {} '$g' => 'list', ], ], + 'mixinInheritMagicMethods' => [ + 'code' => 'active();', + 'assertions' => [ + '$c' => 'B', + ], + ], ]; } diff --git a/tests/ReferenceConstraintTest.php b/tests/ReferenceConstraintTest.php index 9fcd325ac7f..9c99575cdde 100644 --- a/tests/ReferenceConstraintTest.php +++ b/tests/ReferenceConstraintTest.php @@ -81,7 +81,6 @@ function testRef() : array { 'MixedAssignment', 'MixedArrayAccess', 'MixedReturnStatement', - 'MixedInferredReturnType', 'MixedOperand', ], ], @@ -195,6 +194,24 @@ function takesNullableObj(?A &$a): bool { return true; } if ($a) {}', ], + 'PHP80-paramOutChangeTypeWithNamedArgument' => [ + 'code' => ' [ + '$a' => 'int', + ], + ], ]; } diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index bee44b19d37..79f10443c30 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -712,6 +712,7 @@ public function testJsonReport(): void $issue_data = [ [ + 'link' => 'https://psalm.dev/024', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -727,13 +728,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => -1, 'shortcode' => 24, - 'link' => 'https://psalm.dev/024', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/138', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -749,35 +750,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => 1, 'shortcode' => 138, - 'link' => 'https://psalm.dev/138', - 'taint_trace' => null, - 'other_references' => null, - ], - [ - 'severity' => 'error', - 'line_from' => 2, - 'line_to' => 2, - 'type' => 'MixedInferredReturnType', - 'message' => 'Could not verify return type \'null|string\' for psalmCanVerify', - 'file_name' => 'somefile.php', - 'file_path' => 'somefile.php', - 'snippet' => 'function psalmCanVerify(int $your_code): ?string {', - 'selected_text' => '?string', - 'from' => 47, - 'to' => 54, - 'snippet_from' => 6, - 'snippet_to' => 56, - 'column_from' => 42, - 'column_to' => 49, 'error_level' => 1, - 'shortcode' => 47, - 'link' => 'https://psalm.dev/047', 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/047', 'severity' => 'error', 'line_from' => 8, 'line_to' => 8, @@ -793,13 +772,13 @@ public function testJsonReport(): void 'snippet_to' => 172, 'column_from' => 6, 'column_to' => 15, - 'error_level' => -1, 'shortcode' => 20, - 'link' => 'https://psalm.dev/020', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/126', 'severity' => 'info', 'line_from' => 17, 'line_to' => 17, @@ -815,9 +794,8 @@ public function testJsonReport(): void 'snippet_to' => 277, 'column_from' => 6, 'column_to' => 8, - 'error_level' => 3, 'shortcode' => 126, - 'link' => 'https://psalm.dev/126', + 'error_level' => 3, 'taint_trace' => null, 'other_references' => null, ], @@ -854,7 +832,7 @@ public function testFilteredJsonReportIsStillArray(): void ]; $report_options = ProjectAnalyzer::getFileReportOptions([__DIR__ . '/test-report.json'])[0]; - $fixable_issue_counts = ['MixedInferredReturnType' => 1]; + $fixable_issue_counts = []; $report = new JsonReport( $issues_data, @@ -902,22 +880,6 @@ public function testSonarqubeReport(): void 'type' => 'CODE_SMELL', 'severity' => 'CRITICAL', ], - [ - 'engineId' => 'Psalm', - 'ruleId' => 'MixedInferredReturnType', - 'primaryLocation' => [ - 'message' => 'Could not verify return type \'null|string\' for psalmCanVerify', - 'filePath' => 'somefile.php', - 'textRange' => [ - 'startLine' => 2, - 'endLine' => 2, - 'startColumn' => 41, - 'endColumn' => 48, - ], - ], - 'type' => 'CODE_SMELL', - 'severity' => 'CRITICAL', - ], [ 'engineId' => 'Psalm', 'ruleId' => 'UndefinedConstant', @@ -972,7 +934,6 @@ public function testEmacsReport(): void <<<'EOF' somefile.php:3:10:error - UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024) somefile.php:3:10:error - MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138) - somefile.php:2:42:error - MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) somefile.php:8:6:error - UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020) somefile.php:17:6:warning - PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) @@ -991,7 +952,6 @@ public function testPylintReport(): void <<<'EOF' somefile.php:3: [E0001] UndefinedVariable: Cannot find referenced variable $as_you_____type (column 10) somefile.php:3: [E0001] MixedReturnStatement: Could not infer a return type (column 10) - somefile.php:2: [E0001] MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (column 42) somefile.php:8: [E0001] UndefinedConstant: Const CHANGE_ME is not defined (column 6) somefile.php:17: [W0001] PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (column 6) @@ -1015,9 +975,6 @@ public function testConsoleReport(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) return $as_you_____type; - ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) - function psalmCanVerify(int $your_code): ?string { - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) echo CHANGE_ME; @@ -1046,9 +1003,6 @@ public function testConsoleReportNoInfo(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) return $as_you_____type; - ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) - function psalmCanVerify(int $your_code): ?string { - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) echo CHANGE_ME; @@ -1074,9 +1028,6 @@ public function testConsoleReportNoSnippet(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) - ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) - - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) @@ -1135,15 +1086,14 @@ public function testCompactReport(): void <<<'EOF' FILE: somefile.php - +----------+------+---------------------------------+---------------------------------------------------------------+ - | SEVERITY | LINE | ISSUE | DESCRIPTION | - +----------+------+---------------------------------+---------------------------------------------------------------+ - | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type | - | ERROR | 3 | MixedReturnStatement | Could not infer a return type | - | ERROR | 2 | MixedInferredReturnType | Could not verify return type 'null|string' for psalmCanVerify | - | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined | - | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 | - +----------+------+---------------------------------+---------------------------------------------------------------+ + +----------+------+---------------------------------+--------------------------------------------------------------+ + | SEVERITY | LINE | ISSUE | DESCRIPTION | + +----------+------+---------------------------------+--------------------------------------------------------------+ + | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type | + | ERROR | 3 | MixedReturnStatement | Could not infer a return type | + | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined | + | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 | + +----------+------+---------------------------------+--------------------------------------------------------------+ EOF, $this->toUnixLineEndings(IssueBuffer::getOutput(IssueBuffer::getIssuesData(), $compact_report_options)), @@ -1166,9 +1116,6 @@ public function testCheckstyleReport(): void - - - @@ -1199,8 +1146,8 @@ public function testJunitReport(): void $this->assertSame( <<<'EOF' - - + + message: Cannot find referenced variable $as_you_____type type: UndefinedVariable @@ -1219,16 +1166,6 @@ public function testJunitReport(): void line: 3 column_from: 10 column_to: 26 - - - - message: Could not verify return type 'null|string' for psalmCanVerify - type: MixedInferredReturnType - snippet: function psalmCanVerify(int $your_code): ?string { - selected_text: ?string - line: 2 - column_from: 42 - column_to: 49 @@ -1283,7 +1220,6 @@ public function testGithubActionsOutput(): void $expected_output = <<<'EOF' ::error file=somefile.php,line=3,col=10,title=UndefinedVariable::somefile.php:3:10: UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024) ::error file=somefile.php,line=3,col=10,title=MixedReturnStatement::somefile.php:3:10: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138) - ::error file=somefile.php,line=2,col=42,title=MixedInferredReturnType::somefile.php:2:42: MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) ::error file=somefile.php,line=8,col=6,title=UndefinedConstant::somefile.php:8:6: UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020) ::warning file=somefile.php,line=17,col=6,title=PossiblyUndefinedGlobalVariable::somefile.php:17:6: PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) @@ -1301,7 +1237,6 @@ public function testCountOutput(): void $report_options = new ReportOptions(); $report_options->format = Report::TYPE_COUNT; $expected_output = <<<'EOF' - MixedInferredReturnType: 1 MixedReturnStatement: 1 PossiblyUndefinedGlobalVariable: 1 UndefinedConstant: 1 diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 41d93d12453..0cfc5f52c89 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1380,14 +1380,6 @@ function fooFoo() { }', 'error_message' => 'MissingReturnType', ], - 'mixedInferredReturnType' => [ - 'code' => ' 'MixedInferredReturnType', - ], 'mixedInferredReturnStatement' => [ 'code' => ' 'MixedReturnStatement', ], - 'invalidReturnTypeClass' => [ - 'code' => ' 'UndefinedClass', - 'ignored_issues' => ['MixedInferredReturnType'], - ], 'invalidClassOnCall' => [ 'code' => 'bar();', 'error_message' => 'UndefinedClass', - 'ignored_issues' => ['MixedInferredReturnType', 'MixedReturnStatement'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'returnArrayOfNullableInvalid' => [ 'code' => 'analyzeFile($file_path, new Context()); } - public function testStubFileWithPartialClassDefinitionWithCoercion(): void - { - $this->expectExceptionMessage('TypeCoercion'); - $this->expectException(CodeException::class); - $this->project_analyzer = $this->getProjectAnalyzerWithConfig( - TestConfig::loadFromXML( - dirname(__DIR__), - ' - - - - - - - - - ', - ), - ); - - $file_path = (string) getcwd() . '/src/somefile.php'; - - $this->addFile( - $file_path, - 'foo("dasda");', - ); - - $this->analyzeFile($file_path, new Context()); - } - - public function testStubFileWithPartialClassDefinitionGeneralReturnType(): void - { - $this->expectExceptionMessage('InvalidReturnStatement'); - $this->expectException(CodeException::class); - $this->project_analyzer = $this->getProjectAnalyzerWithConfig( - TestConfig::loadFromXML( - dirname(__DIR__), - ' - - - - - - - - - ', - ), - ); - - $file_path = (string) getcwd() . '/src/somefile.php'; - - $this->addFile( - $file_path, - 'analyzeFile($file_path, new Context()); - } - public function testStubFileWithTemplatedClassDefinitionAndMagicMethodOverride(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( diff --git a/tests/TaintTest.php b/tests/TaintTest.php index f83d45bae19..6f6fdfe5757 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -2331,6 +2331,14 @@ function foo(array $arr) : void { ', 'error_message' => 'TaintedShell', ], + 'shellExecBacktickConcat' => [ + 'code' => ' 'TaintedShell', + ], /* // TODO: Stubs do not support this type of inference even with $this->message = $message. // Most uses of getMessage() would be with caught exceptions, so this is not representative of real code. @@ -2602,7 +2610,7 @@ public function multipleTaintIssuesAreDetected(string $code, array $expectedIssu $this->analyzeFile($filePath, new Context(), false); $actualIssueTypes = array_map( - fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', + static fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', IssueBuffer::getIssuesDataForFile($filePath), ); self::assertSame($expectedIssuesTypes, $actualIssueTypes); diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 0c6c4ab4afd..eb0d6fd9516 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -2350,7 +2350,7 @@ public function __construct(array $elements) { /** * @template U - * @param callable(T=):U $callback + * @param callable(T):U $callback * @return static */ public function map(callable $callback) { diff --git a/tests/Template/FunctionClassStringTemplateTest.php b/tests/Template/FunctionClassStringTemplateTest.php index 6f84801a6d3..2b8cc2515bf 100644 --- a/tests/Template/FunctionClassStringTemplateTest.php +++ b/tests/Template/FunctionClassStringTemplateTest.php @@ -406,7 +406,6 @@ public function checkExpectations() : void * @psalm-template RequestedType * @psalm-param class-string $className * @psalm-return RequestedType&MockObject - * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mockHelper(string $className) @@ -444,7 +443,6 @@ public function checkExpectations() : void * @psalm-template RequestedType * @psalm-param class-string $className * @psalm-return RequestedType&MockObject - * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mockHelper(string $className) @@ -482,7 +480,6 @@ public function checkExpectations() : void * @psalm-template RequestedType * @psalm-param class-string $className * @psalm-return MockObject&RequestedType - * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mockHelper(string $className) diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index 64210ed4bbc..efb5123e0c3 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -1336,7 +1336,6 @@ function foo(Closure $fn, $arg): void { * @param E $e * @param mixed $d * @return ?E - * @psalm-suppress MixedInferredReturnType */ function reduce_values($e, $d) { if (rand(0, 1)) { @@ -1359,7 +1358,6 @@ function reduce_values($e, $d) { * @param E $e * @param mixed $d * @return ?E - * @psalm-suppress MixedInferredReturnType */ function reduce_values($e, $d) { diff --git a/tests/Template/TraitTemplateTest.php b/tests/Template/TraitTemplateTest.php index 86cf5d8f022..7074c24ea10 100644 --- a/tests/Template/TraitTemplateTest.php +++ b/tests/Template/TraitTemplateTest.php @@ -168,6 +168,32 @@ class B { use T; }', ], + 'multilineTemplateUse' => [ + 'code' => ' + */ + use MyTrait; + } + + class Bar { + /** + * @template-use MyTrait + */ + use MyTrait; + }', + ], 'allowTraitExtendAndImplementWithExplicitParamType' => [ 'code' => ' $key === 0 || is_string($key); + $isZeroOrString = /** @param mixed $key */ static fn($key): bool => $key === 0 || is_string($key); $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index cd198d77504..4096837f22b 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -16,7 +16,6 @@ use const PHP_OS; use const PHP_VERSION; -use const PHP_VERSION_ID; trait ValidCodeAnalysisTestTrait { @@ -81,20 +80,6 @@ public function testValidCode( $codebase->enterServerMode(); $codebase->config->visitPreloadedStubFiles($codebase); - // avoid MethodSignatureMismatch for __unserialize/() when extending DateTime - if (PHP_VERSION_ID >= 8_02_00) { - $this->addStubFile( - 'stubOne.phpstub', - 'addFile($file_path, $code); diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 79ade7c41f6..07058f21998 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -835,6 +835,20 @@ class Foo { '$output===' => 'array{phone: string}', ], ], + 'multilineTypeWithExtraSpace' => [ + 'code' => ' [ 'code' => ' [$type], + static fn($type) => [$type], array_keys($basic_types), ); } diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 2ebae82ce4b..53536356689 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -935,6 +935,14 @@ public function testClassStringMap(): void ); } + public function testClassStringMapOf(): void + { + $this->assertSame( + 'class-string-map', + Type::parseString('class-string-map')->getId(false), + ); + } + public function testVeryLargeType(): void { $very_large_type = 'array{a: Closure():(array|null), b?: Closure():array, c?: Closure():array, d?: Closure():array, e?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), p?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), q: string, r?: Closure():(array|null), s: array}|null'; diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 4c96c6783f9..6ed17a19da3 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -79,7 +79,7 @@ public function bar(string $key): bool { } }', 'assertions' => [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'assertSelfClassConstantOffsetsInFunction' => [ 'code' => ' [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'assertNamedClassConstantOffsetsInFunction' => [ 'code' => ' [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'possiblyUndefinedArrayAccessWithArrayKeyExists' => [ 'code' => ' [ 'code' => ' [], - 'ignored_issues' => ['DocblockTypeContradiction'], + 'ignored_issues' => ['DocblockTypeContradiction', 'TypeDoesNotContainType'], ], 'notInstanceof' => [ 'code' => ' ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => ' [], - 'ignored_issues' => ['MixedInferredReturnType'], ], 'grandParentInstanceofConfusion' => [ 'code' => 'test(); @@ -1325,6 +1325,35 @@ public function b(): void {} new A; PHP, ], + 'callNeverReturnsSuppressed' => [ + 'code' => ' [ + 'code' => ' [ 'code' => ' ' $_) { diff --git a/tests/fixtures/SuicidalAutoloader/autoloader.php b/tests/fixtures/SuicidalAutoloader/autoloader.php index 365fa7b723e..d506219c141 100644 --- a/tests/fixtures/SuicidalAutoloader/autoloader.php +++ b/tests/fixtures/SuicidalAutoloader/autoloader.php @@ -3,7 +3,7 @@ use React\Promise\PromiseInterface as ReactPromise; use Composer\InstalledVersions; -spl_autoload_register(function (string $className) { +spl_autoload_register(static function (string $className) { $knownBadClasses = [ ReactPromise::class, // amphp/amp ResourceBundle::class, // symfony/polyfill-php73 @@ -25,11 +25,9 @@ 'Symfony\Component\String\s', 'Symfony\Component\Translation\t', ]; - if (in_array($className, $knownBadClasses)) { return; } - $ex = new RuntimeException('Attempted to load ' . $className); echo $ex->__toString() . "\n\n" . $ex->getTraceAsString() . "\n\n"; exit(70);