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/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh index 9fd6483b0e6..fdcddae7d67 100755 --- a/bin/test-with-real-projects.sh +++ b/bin/test-with-real-projects.sh @@ -38,6 +38,7 @@ psl) cd endtoend-test-psl git checkout 2.3.x composer install + sed 's/ErrorOutputBehavior::Packed, ErrorOutputBehavior::Discard/ErrorOutputBehavior::Discard/g' -i src/Psl/Shell/execute.php "$PSALM" --monochrome -c config/psalm.xml "$PSALM" --monochrome -c config/psalm.xml tests/static-analysis ;; 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/dictionaries/CallMap.php b/dictionaries/CallMap.php index ccefe6a0b6f..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'], @@ -10636,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 73ce32e3d7e..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'], @@ -9288,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'], diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 055990282a9..4a87186bea7 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,7 +72,6 @@ use UnexpectedValueException; use function array_combine; -use function array_merge; use function array_pop; use function array_reverse; use function array_values; @@ -1556,13 +1556,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); @@ -1572,12 +1585,25 @@ public function getCompletionItemsForClassishThing( try { $class_storage = $this->classlike_storage_provider->get($atomic_type->value); - $methods = array_merge( - $class_storage->methods, - $class_storage->pseudo_methods, - $class_storage->pseudo_static_methods, - ); - foreach ($methods as $method_storage) { + $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; + } + + foreach ($method_storages as $method_storage) { + if (!in_array($method_storage->visibility, $allow_visibilities)) { + continue; + } if ($method_storage->is_static || $gap === '->') { $completion_item = new CompletionItem( $method_storage->cased_name, @@ -1606,43 +1632,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, @@ -1664,6 +1698,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; diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index b4586bc4a05..fddb73cca48 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; @@ -1144,6 +1145,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 a750940a27a..99ccb3c0ec5 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; @@ -39,6 +40,7 @@ use Psalm\Type\Union; use function array_filter; +use function count; use function in_array; use function strpos; use function strtolower; @@ -118,13 +120,13 @@ public static function compare( ); } + // CallMapHandler needed due to https://github.com/vimeo/psalm/issues/10378 if (!$guide_classlike_storage->user_defined && $implementer_classlike_storage->user_defined && $codebase->analysis_php_version_id >= 8_01_00 - && ($guide_method_storage->return_type + && (($guide_method_storage->return_type && InternalCallMapHandler::inCallMap($cased_guide_method_id)) || $guide_method_storage->signature_return_type - ) - && !$implementer_method_storage->signature_return_type + ) && !$implementer_method_storage->signature_return_type && !array_filter( $implementer_method_storage->attributes, static fn(AttributeStorage $s): bool => $s->fq_class_name === 'ReturnTypeWillChange', @@ -132,7 +134,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, @@ -201,10 +203,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') { @@ -363,10 +364,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; @@ -388,8 +399,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()) @@ -440,11 +449,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 @@ -453,7 +459,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) ) @@ -493,9 +502,7 @@ private static function compareMethodParams( } } - if ($guide_classlike_storage->user_defined - && $implementer_param->signature_type - ) { + if ($implementer_param->signature_type) { self::compareMethodSignatureParams( $codebase, $i, @@ -534,9 +541,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' . @@ -587,6 +592,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, @@ -898,12 +947,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 4f3668d129d..43327ddb7bf 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -528,21 +528,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 false; - } - - $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 6fc3a58d62e..24864e9dd9b 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; @@ -803,13 +802,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; } @@ -834,21 +836,55 @@ 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 TList) { + $atomic_type = $atomic_type->getKeyedArray(); + } + + 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); } @@ -916,6 +952,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]), @@ -927,7 +964,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/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 77a6d7d6118..ba425e189d9 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 baead15e9da..a22924ed21d 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/CliUtils.php b/src/Psalm/Internal/CliUtils.php index 78d964f0b94..b1b8cc8b4c7 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -286,7 +286,14 @@ public static function getPathsToCheck(string|array|false|null $f_paths): ?array } if (strpos($input_path, '--') === 0 && 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 d5ded4434a0..76635f987d5 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -452,6 +452,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; } @@ -562,6 +564,8 @@ private function populateDataFromParentClass( $parent_storage->dependent_classlikes[strtolower($storage->name)] = true; + $storage->pseudo_static_methods += $parent_storage->pseudo_static_methods; + $storage->pseudo_methods += $parent_storage->pseudo_methods; $storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids; } 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/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index ecc0b53fb39..21ab7445e88 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -176,6 +176,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 3ff897b4e5d..29a3e07d25f 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -154,13 +154,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 = array_merge($this->type_aliases, $classlike_node_scanner->type_aliases); } elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) { foreach ($node->catches as $catch) { diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index 09db0b64f96..a6b5033990e 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -79,6 +79,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, @@ -110,6 +113,9 @@ private function getCacheHash(?string $_unused_file_path, ?string $file_contents return PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $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)); @@ -120,6 +126,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 e062ff7d117..c660e0c8324 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -105,7 +105,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 976b8033f90..f7ce8e60e2a 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..44a4908d41f --- /dev/null +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php @@ -0,0 +1,263 @@ + + */ + 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..b510f950da4 --- /dev/null +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php @@ -0,0 +1,309 @@ + + */ + 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 2d1734a0d7d..baca7ce6579 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 82aa62d9108..ed65d78276d 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 3dae08c292b..e562f76c7b1 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -304,9 +304,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 3bf5ac4c80d..5686a31b5ee 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; @@ -974,9 +974,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(); } /** @@ -1038,6 +1036,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; @@ -1065,9 +1067,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(); } /** @@ -1159,9 +1159,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(); } /** @@ -1238,9 +1236,7 @@ private static function reconcileBool( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1323,9 +1319,7 @@ private static function reconcileFalse( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1408,9 +1402,7 @@ private static function reconcileTrue( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1483,9 +1475,7 @@ private static function reconcileScalar( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1576,9 +1566,7 @@ private static function reconcileNumeric( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1696,9 +1684,7 @@ private static function reconcileObject( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1754,9 +1740,7 @@ private static function reconcileResource( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1826,9 +1810,7 @@ private static function reconcileCountable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1889,9 +1871,7 @@ private static function reconcileIterable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1933,9 +1913,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; @@ -2251,9 +2229,7 @@ private static function reconcileTraversable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2363,9 +2339,7 @@ private static function reconcileArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2470,9 +2444,7 @@ private static function reconcileList( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2678,6 +2650,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; } @@ -2704,9 +2683,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 bc966c70d47..b04eeedbe04 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; @@ -143,9 +144,7 @@ public static function reconcile( } } - return $existing_var_type->from_docblock - ? Type::getNull() - : Type::getNever(); + return Type::getNever(); } return Type::getNull(); @@ -418,6 +417,8 @@ public static function reconcile( if ($assertion_type instanceof TCallable) { return self::reconcileCallable( $existing_var_type, + $codebase, + $assertion_type, ); } @@ -426,6 +427,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) { @@ -433,10 +436,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); } } @@ -509,9 +524,7 @@ private static function reconcileBool( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -705,9 +718,7 @@ private static function reconcileNull( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -787,9 +798,7 @@ private static function reconcileFalse( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -869,9 +878,7 @@ private static function reconcileTrue( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -926,9 +933,7 @@ private static function reconcileFalsyOrEmpty( $failed_reconciliation = 2; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } if ($redundant) { @@ -1141,9 +1146,7 @@ private static function reconcileScalar( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1242,9 +1245,7 @@ private static function reconcileObject( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1338,9 +1339,7 @@ private static function reconcileNumeric( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1440,9 +1439,7 @@ private static function reconcileInt( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1537,9 +1534,7 @@ private static function reconcileFloat( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1643,9 +1638,7 @@ private static function reconcileString( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1745,9 +1738,7 @@ private static function reconcileArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1817,9 +1808,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 a26b95a3c1a..e1659f2e0b0 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php @@ -14,7 +14,7 @@ final class FunctionParamsProviderEvent private StatementsSource $statements_source; private string $function_id; /** - * @var PhpParser\Node\Arg[] + * @var list */ private array $call_args; private ?Context $context; @@ -49,7 +49,7 @@ public function getFunctionId(): string } /** - * @return PhpParser\Node\Arg[] + * @return list */ public function getCallArgs(): array { 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/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 d7651949d58..56db2d79ab5 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1045,17 +1045,20 @@ function foo(array $arr) : void { * @template-implements ArrayAccess */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(?int $offset, string $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'checkEmptinessAfterConditionalArrayAdjustment' => [ 'code' => ' */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(int $offset, string $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'conditionalRestrictedDocblockKeyAssignment' => [ 'code' => ' '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' => ' '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..9519553237b --- /dev/null +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -0,0 +1,581 @@ +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', + + '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', + + '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', + + '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/MethodSignatureTest.php b/tests/MethodSignatureTest.php index e60ddb4c627..4be5494ba86 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 { } }', diff --git a/tests/StubTest.php b/tests/StubTest.php index 4ea6f219803..3de6261a059 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -222,7 +222,7 @@ public function testStubFileConstant(): void public function testStubFileParentClass(): void { $this->expectException(CodeException::class); - $this->expectExceptionMessage('ImplementedParamTypeMismatch'); + $this->expectExceptionMessage('MethodSignatureMismatch'); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__), 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 da1d639e246..cdb03637ab6 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' => '