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/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/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/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/psalm-baseline.xml b/psalm-baseline.xml index addca310623..bc7e8c551fc 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -16,6 +16,9 @@ $deprecated_element_xml + + addAttribute + $this 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 dfb3360693e..9c188d9e0f2 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; @@ -480,6 +481,7 @@ final class Config "mysqli" => null, "pdo" => null, "random" => null, + "rdkafka" => null, "redis" => null, "simplexml" => null, "soap" => null, @@ -1142,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); } diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index cef877dc3c7..913c31d5338 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; @@ -41,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; @@ -120,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', @@ -134,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, @@ -203,10 +205,9 @@ public static function compare( ); } - if ($guide_classlike_storage->user_defined - && ($guide_classlike_storage->is_interface - || $guide_classlike_storage->preserve_constructor_signature - || $implementer_method_storage->cased_name !== '__construct') + if (($guide_classlike_storage->is_interface + || $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') { @@ -415,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; @@ -440,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()) @@ -492,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 @@ -505,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) ) @@ -587,9 +596,7 @@ private static function compareMethodParams( ); } - if ($guide_classlike_storage->user_defined && $implementer_param->by_ref !== $guide_param->by_ref) { - $config = Config::getInstance(); - + if ($implementer_param->by_ref !== $guide_param->by_ref) { IssueBuffer::maybeAdd( new MethodSignatureMismatch( 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' is' . @@ -640,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, @@ -963,12 +1014,18 @@ private static function compareMethodSignatureReturnTypes( : UnionTypeComparator::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type); 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( 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/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/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 4b3fcc1a2d2..cba0730a470 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -145,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, @@ -155,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; } 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/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/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index ccd3e2b65c5..4093c263db5 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -439,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; } @@ -543,6 +545,11 @@ private function populateDataFromParentClass( $parent_storage->dependent_classlikes[strtolower($storage->name)] = true; + 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; @@ -1000,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/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 9cf76e3f2c1..a573fc01e5b 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -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 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/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 89c7f3cd761..2c03d2c58d9 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -160,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 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/PluginManager/ComposerLock.php b/src/Psalm/Internal/PluginManager/ComposerLock.php index 25808fe557b..d806b09d81a 100644 --- a/src/Psalm/Internal/PluginManager/ComposerLock.php +++ b/src/Psalm/Internal/PluginManager/ComposerLock.php @@ -18,7 +18,7 @@ /** * @internal */ -class ComposerLock +final class ComposerLock { /** @param string[] $file_names */ public function __construct( 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 b23c16d1266..edf35f8b169 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/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/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 6bbdc46a543..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; @@ -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/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/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/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 de8474d4742..d14673868cf 100644 --- a/stubs/extensions/redis.phpstub +++ b/stubs/extensions/redis.phpstub @@ -276,12 +276,12 @@ class Redis { public function move(string $key, int $index): bool {} /** - * @param array + * @param array */ public function mset($key_values): Redis|bool {} /** - * @param array + * @param array */ public function msetnx($key_values): Redis|bool {} diff --git a/tests/ArgTest.php b/tests/ArgTest.php index adc03ac5e2b..93e7f022d68 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -350,11 +350,10 @@ function var_caller($callback) {} /** * @param string $a - * @param int $b - * @param int $c + * @param int ...$b * @return void */ - function foo($a, $b, $c) {} + function foo($a, ...$b) {} var_caller("foo");', ], diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 28ee56f74f7..710cc8b7ce3 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -909,20 +909,20 @@ public function offsetGet($name) } /** - * @param ?string $name + * @param ?string $offset * @param scalar|array $value * @psalm-suppress MixedArgumentTypeCoercion */ - public function offsetSet($name, $value) : void + public function offsetSet($offset, $value) : void { if (is_array($value)) { $value = new static($value); } - if (null === $name) { + if (null === $offset) { $this->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 4f5904085fc..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($offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet($offset, string $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset($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($offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet($offset, $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset($offset) : void { } + public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'conditionalRestrictedDocblockKeyAssignment' => [ '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/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' => ' [ + 'code' => ' [], + 'ignored_issues' => ['InvalidReturnType'], + ], 'abstractInvokeInTrait' => [ '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' => ' 'array{9223372036854775806: 0, 9223372036854775807: 1}', ], ]; + yield 'shellExecConcatInt' => [ + 'code' => <<<'PHP' + '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/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/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 18609a2ccb9..2446c5c65c0 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -510,22 +510,22 @@ class B extends A { class Observer implements \SplObserver { - public function update(SplSubject $subject) + public function update(SplSubject $subject): void { } } class Subject implements \SplSubject { - public function attach(SplObserver $observer) + public function attach(SplObserver $observer): void { } - public function detach(SplObserver $observer) + public function detach(SplObserver $observer): void { } - public function notify() + public function notify(): void { } }', @@ -927,7 +927,7 @@ final class B implements I public function a(mixed $a): void {} }', 'assertions' => [], - 'ignored_errors' => [], + 'ignored_issues' => [], 'php_version' => '8.0', ], 'doesNotRequireInterfaceDestructorsToHaveReturnType' => [ diff --git a/tests/ReferenceConstraintTest.php b/tests/ReferenceConstraintTest.php index 9b7811961e5..9c99575cdde 100644 --- a/tests/ReferenceConstraintTest.php +++ b/tests/ReferenceConstraintTest.php @@ -194,6 +194,24 @@ function takesNullableObj(?A &$a): bool { return true; } if ($a) {}', ], + 'PHP80-paramOutChangeTypeWithNamedArgument' => [ + 'code' => ' [ + '$a' => 'int', + ], + ], ]; } diff --git a/tests/StubTest.php b/tests/StubTest.php index 4ea6f219803..5b9aa0eb095 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -1199,94 +1199,6 @@ class Bar extends PartiallyStubbedClass {} $this->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 4e7c2bda417..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. 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/TestCase.php b/tests/TestCase.php index 5ddeebd3bfd..cf73c4b9e6b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -34,10 +34,23 @@ class TestCase extends BaseTestCase { protected static string $src_dir_path; + /** + * caused by phpunit using setUp() instead of __construct + * could perhaps use psalm-plugin-phpunit once https://github.com/psalm/psalm-plugin-phpunit/issues/129 + * to remove this suppression + * + * @psalm-suppress PropertyNotSetInConstructor + */ protected ProjectAnalyzer $project_analyzer; + /** + * @psalm-suppress PropertyNotSetInConstructor + */ protected FakeFileProvider $file_provider; + /** + * @psalm-suppress PropertyNotSetInConstructor + */ protected Config $testConfig; public static function setUpBeforeClass(): void diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index e8b7ffce80e..cd39b1ab895 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 { @@ -79,20 +78,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/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index a28a2c0360e..3c6b9975432 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -82,7 +82,7 @@ function foo($length) { } }', 'assertions' => [], - 'ignored_issues' => ['DocblockTypeContradiction'], + 'ignored_issues' => ['DocblockTypeContradiction', 'TypeDoesNotContainType'], ], 'notInstanceof' => [ 'code' => 'test(); @@ -1325,6 +1325,35 @@ public function b(): void {} new A; PHP, ], + 'callNeverReturnsSuppressed' => [ + 'code' => ' [ + 'code' => '