diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 3627f1b0ff..d429d11b59 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -170,168 +170,6 @@ 'DateTimeImmutable::getTimestamp' => ['hasSideEffects' => false], 'DateTimeImmutable::getTimezone' => ['hasSideEffects' => false], - 'Redis::append' => ['hasSideEffects' => true], - 'Redis::bitcount' => ['hasSideEffects' => true], - 'Redis::bitop' => ['hasSideEffects' => true], - 'Redis::bitpos' => ['hasSideEffects' => true], - 'Redis::blPop' => ['hasSideEffects' => true], - 'Redis::blmove' => ['hasSideEffects' => true], - 'Redis::blmpop' => ['hasSideEffects' => true], - 'Redis::brPop' => ['hasSideEffects' => true], - 'Redis::brpoplpush' => ['hasSideEffects' => true], - 'Redis::bzmpop' => ['hasSideEffects' => true], - 'Redis::bzPopMax' => ['hasSideEffects' => true], - 'Redis::bzPopMin' => ['hasSideEffects' => true], - 'Redis::connect' => ['hasSideEffects' => true], - 'Redis::dbSize' => ['hasSideEffects' => true], - 'Redis::decr' => ['hasSideEffects' => true], - 'Redis::decrBy' => ['hasSideEffects' => true], - 'Redis::del' => ['hasSideEffects' => true], - 'Redis::delete' => ['hasSideEffects' => true], - 'Redis::exists' => ['hasSideEffects' => true], - 'Redis::expire' => ['hasSideEffects' => true], - 'Redis::expireAt' => ['hasSideEffects' => true], - 'Redis::expiretime' => ['hasSideEffects' => true], - 'Redis::flushAll' => ['hasSideEffects' => true], - 'Redis::flushDB' => ['hasSideEffects' => true], - 'Redis::function' => ['hasSideEffects' => true], - 'Redis::geoadd' => ['hasSideEffects' => true], - 'Redis::geodist' => ['hasSideEffects' => true], - 'Redis::geohash' => ['hasSideEffects' => true], - 'Redis::geopos' => ['hasSideEffects' => true], - 'Redis::georadius' => ['hasSideEffects' => true], - 'Redis::georadiusbymember' => ['hasSideEffects' => true], - 'Redis::georadiusbymember_ro' => ['hasSideEffects' => true], - 'Redis::geosearch' => ['hasSideEffects' => true], - 'Redis::geosearchstore' => ['hasSideEffects' => true], - 'Redis::get' => ['hasSideEffects' => true], - 'Redis::getBit' => ['hasSideEffects' => true], - 'Redis::getEx' => ['hasSideEffects' => true], - 'Redis::getDBNum' => ['hasSideEffects' => true], - 'Redis::getDel' => ['hasSideEffects' => true], - 'Redis::getLastError' => ['hasSideEffects' => true], - 'Redis::getMode' => ['hasSideEffects' => true], - 'Redis::getOption' => ['hasSideEffects' => true], - 'Redis::getPersistentID' => ['hasSideEffects' => true], - 'Redis::getRange' => ['hasSideEffects' => true], - 'Redis::lcs' => ['hasSideEffects' => true], - 'Redis::lmpop' => ['hasSideEffects' => true], - 'Redis::getReadTimeout' => ['hasSideEffects' => true], - 'Redis::getset' => ['hasSideEffects' => true], - 'Redis::getTimeout' => ['hasSideEffects' => true], - 'Redis::getTransferredBytes' => ['hasSideEffects' => true], - 'Redis::hDel' => ['hasSideEffects' => true], - 'Redis::hExists' => ['hasSideEffects' => true], - 'Redis::hGet' => ['hasSideEffects' => true], - 'Redis::hGetAll' => ['hasSideEffects' => true], - 'Redis::hIncrBy' => ['hasSideEffects' => true], - 'Redis::hIncrByFloat' => ['hasSideEffects' => true], - 'Redis::hKeys' => ['hasSideEffects' => true], - 'Redis::hLen' => ['hasSideEffects' => true], - 'Redis::hMget' => ['hasSideEffects' => true], - 'Redis::hMset' => ['hasSideEffects' => true], - 'Redis::hRandField' => ['hasSideEffects' => true], - 'Redis::hscan' => ['hasSideEffects' => true], - 'Redis::hSet' => ['hasSideEffects' => true], - 'Redis::hSetNx' => ['hasSideEffects' => true], - 'Redis::hStrLen' => ['hasSideEffects' => true], - 'Redis::hVals' => ['hasSideEffects' => true], - 'Redis::incr' => ['hasSideEffects' => true], - 'Redis::incrBy' => ['hasSideEffects' => true], - 'Redis::incrByFloat' => ['hasSideEffects' => true], - 'Redis::isConnected' => ['hasSideEffects' => true], - 'Redis::keys' => ['hasSideEffects' => true], - 'Redis::lastSave' => ['hasSideEffects' => true], - 'Redis::lInsert' => ['hasSideEffects' => true], - 'Redis::lLen' => ['hasSideEffects' => true], - 'Redis::lMove' => ['hasSideEffects' => true], - 'Redis::lPop' => ['hasSideEffects' => true], - 'Redis::lPos' => ['hasSideEffects' => true], - 'Redis::lPush' => ['hasSideEffects' => true], - 'Redis::lPushx' => ['hasSideEffects' => true], - 'Redis::lSet' => ['hasSideEffects' => true], - 'Redis::lindex' => ['hasSideEffects' => true], - 'Redis::lrange' => ['hasSideEffects' => true], - 'Redis::lrem' => ['hasSideEffects' => true], - 'Redis::ltrim' => ['hasSideEffects' => true], - 'Redis::mget' => ['hasSideEffects' => true], - 'Redis::move' => ['hasSideEffects' => true], - 'Redis::mset' => ['hasSideEffects' => true], - 'Redis::msetnx' => ['hasSideEffects' => true], - 'Redis::pconnect' => ['hasSideEffects' => true], - 'Redis::persist' => ['hasSideEffects' => true], - 'Redis::pexpire' => ['hasSideEffects' => true], - 'Redis::pexpireAt' => ['hasSideEffects' => true], - 'Redis::pexpiretime' => ['hasSideEffects' => true], - 'Redis::rpoplpush' => ['hasSideEffects' => true], - 'Redis::rPush' => ['hasSideEffects' => true], - 'Redis::rPushx' => ['hasSideEffects' => true], - 'Redis::sAdd' => ['hasSideEffects' => true], - 'Redis::sAddArray' => ['hasSideEffects' => true], - 'Redis::scan' => ['hasSideEffects' => true], - 'Redis::scard' => ['hasSideEffects' => true], - 'Redis::script' => ['hasSideEffects' => true], - 'Redis::sDiff' => ['hasSideEffects' => true], - 'Redis::sDiffStore' => ['hasSideEffects' => true], - 'Redis::set' => ['hasSideEffects' => true], - 'Redis::setBit' => ['hasSideEffects' => true], - 'Redis::setRange' => ['hasSideEffects' => true], - 'Redis::setOption' => ['hasSideEffects' => true], - 'Redis::setex' => ['hasSideEffects' => true], - 'Redis::setnx' => ['hasSideEffects' => true], - 'Redis::sInter' => ['hasSideEffects' => true], - 'Redis::sintercard' => ['hasSideEffects' => true], - 'Redis::sInterStore' => ['hasSideEffects' => true], - 'Redis::sismember' => ['hasSideEffects' => true], - 'Redis::sMembers' => ['hasSideEffects' => true], - 'Redis::sMisMember' => ['hasSideEffects' => true], - 'Redis::sMove' => ['hasSideEffects' => true], - 'Redis::sPop' => ['hasSideEffects' => true], - 'Redis::sort' => ['hasSideEffects' => true], - 'Redis::sort_ro' => ['hasSideEffects' => true], - 'Redis::sRandMember' => ['hasSideEffects' => true], - 'Redis::srem' => ['hasSideEffects' => true], - 'Redis::sscan' => ['hasSideEffects' => true], - 'Redis::sUnion' => ['hasSideEffects' => true], - 'Redis::sUnionStore' => ['hasSideEffects' => true], - 'Redis::time' => ['hasSideEffects' => true], - 'Redis::touch' => ['hasSideEffects' => true], - 'Redis::ttl' => ['hasSideEffects' => true], - 'Redis::type' => ['hasSideEffects' => true], - 'Redis::unlink' => ['hasSideEffects' => true], - 'Redis::zAdd' => ['hasSideEffects' => true], - 'Redis::zCard' => ['hasSideEffects' => true], - 'Redis::zCount' => ['hasSideEffects' => true], - 'Redis::zdiff' => ['hasSideEffects' => true], - 'Redis::zdiffstore' => ['hasSideEffects' => true], - 'Redis::zIncrBy' => ['hasSideEffects' => true], - 'Redis::zinter' => ['hasSideEffects' => true], - 'Redis::zintercard' => ['hasSideEffects' => true], - 'Redis::zinterstore' => ['hasSideEffects' => true], - 'Redis::zLexCount' => ['hasSideEffects' => true], - 'Redis::zmpop' => ['hasSideEffects' => true], - 'Redis::zMscore' => ['hasSideEffects' => true], - 'Redis::zPopMax' => ['hasSideEffects' => true], - 'Redis::zPopMin' => ['hasSideEffects' => true], - 'Redis::zRange' => ['hasSideEffects' => true], - 'Redis::zRangeByLex' => ['hasSideEffects' => true], - 'Redis::zRangeByScore' => ['hasSideEffects' => true], - 'Redis::zrangestore' => ['hasSideEffects' => true], - 'Redis::zRandMember' => ['hasSideEffects' => true], - 'Redis::zRank' => ['hasSideEffects' => true], - 'Redis::zRem' => ['hasSideEffects' => true], - 'Redis::zRemRangeByLex' => ['hasSideEffects' => true], - 'Redis::zRemRangeByRank' => ['hasSideEffects' => true], - 'Redis::zRemRangeByScore' => ['hasSideEffects' => true], - 'Redis::zRevRange' => ['hasSideEffects' => true], - 'Redis::zRevRangeByLex' => ['hasSideEffects' => true], - 'Redis::zRevRangeByScore' => ['hasSideEffects' => true], - 'Redis::zRevRank' => ['hasSideEffects' => true], - 'Redis::zscan' => ['hasSideEffects' => true], - 'Redis::zScore' => ['hasSideEffects' => true], - 'Redis::zunion' => ['hasSideEffects' => true], - 'Redis::zunionstore' => ['hasSideEffects' => true], - 'SplDoublyLinkedList::pop' => ['hasSideEffects' => true], 'SplDoublyLinkedList::shift' => ['hasSideEffects' => true], diff --git a/conf/config.neon b/conf/config.neon index 801fba87a9..2f98310ba1 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -113,6 +113,7 @@ parameters: universalObjectCratesClasses: - stdClass stubFiles: + - ../stubs/Redis.stub - ../stubs/ReflectionAttribute.stub - ../stubs/ReflectionClassConstant.stub - ../stubs/ReflectionFunctionAbstract.stub diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 67315e311c..073ac1050f 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -452,167 +452,6 @@ 'NumberFormatter::getPattern' => ['hasSideEffects' => false], 'NumberFormatter::getSymbol' => ['hasSideEffects' => false], 'NumberFormatter::getTextAttribute' => ['hasSideEffects' => false], - 'Redis::append' => ['hasSideEffects' => true], - 'Redis::bitcount' => ['hasSideEffects' => true], - 'Redis::bitop' => ['hasSideEffects' => true], - 'Redis::bitpos' => ['hasSideEffects' => true], - 'Redis::blPop' => ['hasSideEffects' => true], - 'Redis::blmove' => ['hasSideEffects' => true], - 'Redis::blmpop' => ['hasSideEffects' => true], - 'Redis::brPop' => ['hasSideEffects' => true], - 'Redis::brpoplpush' => ['hasSideEffects' => true], - 'Redis::bzPopMax' => ['hasSideEffects' => true], - 'Redis::bzPopMin' => ['hasSideEffects' => true], - 'Redis::bzmpop' => ['hasSideEffects' => true], - 'Redis::connect' => ['hasSideEffects' => true], - 'Redis::dbSize' => ['hasSideEffects' => true], - 'Redis::decr' => ['hasSideEffects' => true], - 'Redis::decrBy' => ['hasSideEffects' => true], - 'Redis::del' => ['hasSideEffects' => true], - 'Redis::delete' => ['hasSideEffects' => true], - 'Redis::exists' => ['hasSideEffects' => true], - 'Redis::expire' => ['hasSideEffects' => true], - 'Redis::expireAt' => ['hasSideEffects' => true], - 'Redis::expiretime' => ['hasSideEffects' => true], - 'Redis::flushAll' => ['hasSideEffects' => true], - 'Redis::flushDB' => ['hasSideEffects' => true], - 'Redis::function' => ['hasSideEffects' => true], - 'Redis::geoadd' => ['hasSideEffects' => true], - 'Redis::geodist' => ['hasSideEffects' => true], - 'Redis::geohash' => ['hasSideEffects' => true], - 'Redis::geopos' => ['hasSideEffects' => true], - 'Redis::georadius' => ['hasSideEffects' => true], - 'Redis::georadiusbymember' => ['hasSideEffects' => true], - 'Redis::georadiusbymember_ro' => ['hasSideEffects' => true], - 'Redis::geosearch' => ['hasSideEffects' => true], - 'Redis::geosearchstore' => ['hasSideEffects' => true], - 'Redis::get' => ['hasSideEffects' => true], - 'Redis::getBit' => ['hasSideEffects' => true], - 'Redis::getDBNum' => ['hasSideEffects' => true], - 'Redis::getDel' => ['hasSideEffects' => true], - 'Redis::getEx' => ['hasSideEffects' => true], - 'Redis::getLastError' => ['hasSideEffects' => true], - 'Redis::getMode' => ['hasSideEffects' => true], - 'Redis::getOption' => ['hasSideEffects' => true], - 'Redis::getPersistentID' => ['hasSideEffects' => true], - 'Redis::getRange' => ['hasSideEffects' => true], - 'Redis::getReadTimeout' => ['hasSideEffects' => true], - 'Redis::getTimeout' => ['hasSideEffects' => true], - 'Redis::getTransferredBytes' => ['hasSideEffects' => true], - 'Redis::getset' => ['hasSideEffects' => true], - 'Redis::hDel' => ['hasSideEffects' => true], - 'Redis::hExists' => ['hasSideEffects' => true], - 'Redis::hGet' => ['hasSideEffects' => true], - 'Redis::hGetAll' => ['hasSideEffects' => true], - 'Redis::hIncrBy' => ['hasSideEffects' => true], - 'Redis::hIncrByFloat' => ['hasSideEffects' => true], - 'Redis::hKeys' => ['hasSideEffects' => true], - 'Redis::hLen' => ['hasSideEffects' => true], - 'Redis::hMget' => ['hasSideEffects' => true], - 'Redis::hMset' => ['hasSideEffects' => true], - 'Redis::hRandField' => ['hasSideEffects' => true], - 'Redis::hSet' => ['hasSideEffects' => true], - 'Redis::hSetNx' => ['hasSideEffects' => true], - 'Redis::hStrLen' => ['hasSideEffects' => true], - 'Redis::hVals' => ['hasSideEffects' => true], - 'Redis::hscan' => ['hasSideEffects' => true], - 'Redis::incr' => ['hasSideEffects' => true], - 'Redis::incrBy' => ['hasSideEffects' => true], - 'Redis::incrByFloat' => ['hasSideEffects' => true], - 'Redis::isConnected' => ['hasSideEffects' => true], - 'Redis::keys' => ['hasSideEffects' => true], - 'Redis::lInsert' => ['hasSideEffects' => true], - 'Redis::lLen' => ['hasSideEffects' => true], - 'Redis::lMove' => ['hasSideEffects' => true], - 'Redis::lPop' => ['hasSideEffects' => true], - 'Redis::lPos' => ['hasSideEffects' => true], - 'Redis::lPush' => ['hasSideEffects' => true], - 'Redis::lPushx' => ['hasSideEffects' => true], - 'Redis::lSet' => ['hasSideEffects' => true], - 'Redis::lastSave' => ['hasSideEffects' => true], - 'Redis::lcs' => ['hasSideEffects' => true], - 'Redis::lindex' => ['hasSideEffects' => true], - 'Redis::lmpop' => ['hasSideEffects' => true], - 'Redis::lrange' => ['hasSideEffects' => true], - 'Redis::lrem' => ['hasSideEffects' => true], - 'Redis::ltrim' => ['hasSideEffects' => true], - 'Redis::mget' => ['hasSideEffects' => true], - 'Redis::move' => ['hasSideEffects' => true], - 'Redis::mset' => ['hasSideEffects' => true], - 'Redis::msetnx' => ['hasSideEffects' => true], - 'Redis::pconnect' => ['hasSideEffects' => true], - 'Redis::persist' => ['hasSideEffects' => true], - 'Redis::pexpire' => ['hasSideEffects' => true], - 'Redis::pexpireAt' => ['hasSideEffects' => true], - 'Redis::pexpiretime' => ['hasSideEffects' => true], - 'Redis::rPush' => ['hasSideEffects' => true], - 'Redis::rPushx' => ['hasSideEffects' => true], - 'Redis::rpoplpush' => ['hasSideEffects' => true], - 'Redis::sAdd' => ['hasSideEffects' => true], - 'Redis::sAddArray' => ['hasSideEffects' => true], - 'Redis::sDiff' => ['hasSideEffects' => true], - 'Redis::sDiffStore' => ['hasSideEffects' => true], - 'Redis::sInter' => ['hasSideEffects' => true], - 'Redis::sInterStore' => ['hasSideEffects' => true], - 'Redis::sMembers' => ['hasSideEffects' => true], - 'Redis::sMisMember' => ['hasSideEffects' => true], - 'Redis::sMove' => ['hasSideEffects' => true], - 'Redis::sPop' => ['hasSideEffects' => true], - 'Redis::sRandMember' => ['hasSideEffects' => true], - 'Redis::sUnion' => ['hasSideEffects' => true], - 'Redis::sUnionStore' => ['hasSideEffects' => true], - 'Redis::scan' => ['hasSideEffects' => true], - 'Redis::scard' => ['hasSideEffects' => true], - 'Redis::script' => ['hasSideEffects' => true], - 'Redis::set' => ['hasSideEffects' => true], - 'Redis::setBit' => ['hasSideEffects' => true], - 'Redis::setOption' => ['hasSideEffects' => true], - 'Redis::setRange' => ['hasSideEffects' => true], - 'Redis::setex' => ['hasSideEffects' => true], - 'Redis::setnx' => ['hasSideEffects' => true], - 'Redis::sintercard' => ['hasSideEffects' => true], - 'Redis::sismember' => ['hasSideEffects' => true], - 'Redis::sort' => ['hasSideEffects' => true], - 'Redis::sort_ro' => ['hasSideEffects' => true], - 'Redis::srem' => ['hasSideEffects' => true], - 'Redis::sscan' => ['hasSideEffects' => true], - 'Redis::time' => ['hasSideEffects' => true], - 'Redis::touch' => ['hasSideEffects' => true], - 'Redis::ttl' => ['hasSideEffects' => true], - 'Redis::type' => ['hasSideEffects' => true], - 'Redis::unlink' => ['hasSideEffects' => true], - 'Redis::zAdd' => ['hasSideEffects' => true], - 'Redis::zCard' => ['hasSideEffects' => true], - 'Redis::zCount' => ['hasSideEffects' => true], - 'Redis::zIncrBy' => ['hasSideEffects' => true], - 'Redis::zLexCount' => ['hasSideEffects' => true], - 'Redis::zMscore' => ['hasSideEffects' => true], - 'Redis::zPopMax' => ['hasSideEffects' => true], - 'Redis::zPopMin' => ['hasSideEffects' => true], - 'Redis::zRandMember' => ['hasSideEffects' => true], - 'Redis::zRange' => ['hasSideEffects' => true], - 'Redis::zRangeByLex' => ['hasSideEffects' => true], - 'Redis::zRangeByScore' => ['hasSideEffects' => true], - 'Redis::zRank' => ['hasSideEffects' => true], - 'Redis::zRem' => ['hasSideEffects' => true], - 'Redis::zRemRangeByLex' => ['hasSideEffects' => true], - 'Redis::zRemRangeByRank' => ['hasSideEffects' => true], - 'Redis::zRemRangeByScore' => ['hasSideEffects' => true], - 'Redis::zRevRange' => ['hasSideEffects' => true], - 'Redis::zRevRangeByLex' => ['hasSideEffects' => true], - 'Redis::zRevRangeByScore' => ['hasSideEffects' => true], - 'Redis::zRevRank' => ['hasSideEffects' => true], - 'Redis::zScore' => ['hasSideEffects' => true], - 'Redis::zdiff' => ['hasSideEffects' => true], - 'Redis::zdiffstore' => ['hasSideEffects' => true], - 'Redis::zinter' => ['hasSideEffects' => true], - 'Redis::zintercard' => ['hasSideEffects' => true], - 'Redis::zinterstore' => ['hasSideEffects' => true], - 'Redis::zmpop' => ['hasSideEffects' => true], - 'Redis::zrangestore' => ['hasSideEffects' => true], - 'Redis::zscan' => ['hasSideEffects' => true], - 'Redis::zunion' => ['hasSideEffects' => true], - 'Redis::zunionstore' => ['hasSideEffects' => true], 'ReflectionAttribute::getArguments' => ['hasSideEffects' => false], 'ReflectionAttribute::getName' => ['hasSideEffects' => false], 'ReflectionAttribute::getTarget' => ['hasSideEffects' => false], diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 603035e2d1..ecd613ffed 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6993,6 +6993,15 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $varTags = $resolvedPhpDoc->getVarTags(); } + if ($isPure === null && $scope->isInClass()) { + $classResolvedPhpDoc = $scope->getClassReflection()->getResolvedPhpDoc(); + if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) { + $isPure = true; + } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) { + $isPure = false; + } + } + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index ff91a44225..8159733e21 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -705,6 +705,16 @@ public function resolveIsImpure(PhpDocNode $phpDocNode): bool return false; } + public function resolveAllMethodsPure(PhpDocNode $phpDocNode): bool + { + return count($phpDocNode->getTagsByName('@phpstan-all-methods-pure')) > 0; + } + + public function resolveAllMethodsImpure(PhpDocNode $phpDocNode): bool + { + return count($phpDocNode->getTagsByName('@phpstan-all-methods-impure')) > 0; + } + public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool { foreach (['@readonly', '@phan-read-only', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) { diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index dc43e9af5e..59b4bc6422 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -139,6 +139,10 @@ final class ResolvedPhpDocBlock /** @var bool|'notLoaded'|null */ private bool|string|null $isPure = 'notLoaded'; + private ?bool $areAllMethodsPure = null; + + private ?bool $areAllMethodsImpure = null; + private ?bool $isReadOnly = null; private ?bool $isImmutable = null; @@ -233,6 +237,8 @@ public static function createEmpty(): self $self->isInternal = false; $self->isFinal = false; $self->isPure = null; + $self->areAllMethodsPure = false; + $self->areAllMethodsImpure = false; $self->isReadOnly = false; $self->isImmutable = false; $self->isAllowedPrivateMutation = false; @@ -298,6 +304,8 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); $result->isPure = self::mergePureTags($this->isPure(), $parents); + $result->areAllMethodsPure = $this->areAllMethodsPure(); + $result->areAllMethodsImpure = $this->areAllMethodsImpure(); $result->isReadOnly = $this->isReadOnly(); $result->isImmutable = $this->isImmutable(); $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation(); @@ -827,6 +835,20 @@ public function isPure(): ?bool return $this->isPure; } + public function areAllMethodsPure(): bool + { + return $this->areAllMethodsPure ??= $this->phpDocNodeResolver->resolveAllMethodsPure( + $this->phpDocNode, + ); + } + + public function areAllMethodsImpure(): bool + { + return $this->areAllMethodsImpure ??= $this->phpDocNodeResolver->resolveAllMethodsImpure( + $this->phpDocNode, + ); + } + public function isReadOnly(): bool { return $this->isReadOnly ??= $this->phpDocNodeResolver->resolveIsReadOnly( diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index a8a6e5a948..d4c16ba9d5 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -893,6 +893,15 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } $isPure ??= $resolvedPhpDoc->isPure(); + if ($isPure === null) { + $classResolvedPhpDoc = $phpDocBlockClassReflection->getResolvedPhpDoc(); + if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) { + $isPure = true; + } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) { + $isPure = false; + } + } + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index d909dfab72..2a75a6c8c6 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -62,6 +62,8 @@ final class InvalidPHPStanDocTagRule implements Rule '@phpstan-param-immediately-invoked-callable', '@phpstan-param-later-invoked-callable', '@phpstan-param-closure-this', + '@phpstan-all-methods-pure', + '@phpstan-all-methods-impure', ]; public function __construct( diff --git a/stubs/Redis.stub b/stubs/Redis.stub new file mode 100644 index 0000000000..072475482f --- /dev/null +++ b/stubs/Redis.stub @@ -0,0 +1,11 @@ +analyse([__DIR__ . '/data/bug-11019.php'], []); } + public function testBug10215(): void + { + $this->analyse([__DIR__ . '/data/bug-10215.php'], []); + } + public function testBug12946(): void { $this->analyse([__DIR__ . '/data/bug-12946.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10215.php b/tests/PHPStan/Rules/Comparison/data/bug-10215.php new file mode 100644 index 0000000000..50accb912c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10215.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug10215; + +class CacheManager +{ + public function __construct(private readonly \Redis $redis) + { + } + + public function getCachedValue(string $key, callable $callback): int + { + if (false !== ($value = $this->redis->get($key))) { + return (int) $value; + } + $callback(); + if (false !== ($value = $this->redis->get($key))) { + return (int) $value; + } + + throw new \LogicException('Cache was not filled by callback'); + } +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index b5a44ee822..16ce829d02 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -216,6 +216,37 @@ public function testBug12224(): void ]); } + public function testAllMethodsArePure(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/all-methods-are-pure.php'], [ + [ + 'Method AllMethodsArePure\Foo::test() is marked as pure but returns void.', + 10, + ], + [ + 'Method AllMethodsArePure\Foo::pure() is marked as pure but returns void.', + 17, + ], + [ + 'Method AllMethodsArePure\Foo::impure() is marked as impure but does not have any side effects.', + 24, + ], + [ + 'Method AllMethodsArePure\Bar::test() is marked as impure but does not have any side effects.', + 34, + ], + [ + 'Method AllMethodsArePure\Bar::pure() is marked as pure but returns void.', + 41, + ], + [ + 'Method AllMethodsArePure\Bar::impure() is marked as impure but does not have any side effects.', + 48, + ], + ]); + } + public function testBug12382(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Pure/data/all-methods-are-pure.php b/tests/PHPStan/Rules/Pure/data/all-methods-are-pure.php new file mode 100644 index 0000000000..ee400cfe6f --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/all-methods-are-pure.php @@ -0,0 +1,51 @@ +