diff --git a/conf/config.neon b/conf/config.neon index cb8c20ad481..6c71dae9225 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -125,6 +125,7 @@ parameters: universalObjectCratesClasses: - stdClass stubFiles: + - ../stubs/Memcached.stub - ../stubs/Redis.stub - ../stubs/ReflectionAttribute.stub - ../stubs/ReflectionClassConstant.stub diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index fccce2fa772..a31e70433e3 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -618,6 +618,12 @@ private function createMethod( $acceptsNamedArguments = true; $selfOutType = null; $phpDocComment = null; + + $isPure = null; + if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { + $isPure = !$this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects']; + } + $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $methodReflection); foreach ($methodSignaturesResult as $signatureType => $methodSignatures) { if ($methodSignatures === null) { @@ -693,6 +699,7 @@ private function createMethod( $asserts = Assertions::createFromResolvedPhpDocBlock($currentResolvedPhpDoc); $acceptsNamedArguments = $currentResolvedPhpDoc->acceptsNamedArguments(); + $isPure ??= $currentResolvedPhpDoc->isPure(); $selfOutTypeTag = $currentResolvedPhpDoc->getSelfOutTag(); if ($selfOutTypeTag !== null) { @@ -727,11 +734,15 @@ private function createMethod( } } - if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { - $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects']); - } else { - $hasSideEffects = TrinaryLogic::createMaybe(); + if ($isPure === null) { + $classResolvedPhpDoc = $declaringClass->getResolvedPhpDoc(); + if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) { + $isPure = true; + } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) { + $isPure = false; + } } + return new NativeMethodReflection( $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, @@ -739,7 +750,7 @@ private function createMethod( $currentResolvedPhpDoc ?? null, $variantsByType['positional'], $variantsByType['named'] ?? null, - $hasSideEffects, + $isPure !== null ? TrinaryLogic::createFromBoolean(!$isPure) : TrinaryLogic::createMaybe(), $throwType, $asserts, $acceptsNamedArguments, diff --git a/stubs/Memcached.stub b/stubs/Memcached.stub new file mode 100644 index 00000000000..5933a5d6c2c --- /dev/null +++ b/stubs/Memcached.stub @@ -0,0 +1,6 @@ + + */ +class Bug14534Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + new PossiblyImpureTipHelper(true), + true, + true, + true, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-14534.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/bug-14534.neon'], + ); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 9a057cd5392..9c8e1f25fc9 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1197,6 +1197,11 @@ public function testBug14446(): void $this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []); } + public function testBug13444(): void + { + $this->analyse([__DIR__ . '/data/bug-13444.php'], []); + } + public function testBug14473(): void { $this->analyse([__DIR__ . '/data/bug-14519.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/bug-14534.neon b/tests/PHPStan/Rules/Comparison/bug-14534.neon new file mode 100644 index 00000000000..06adf3f5db4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/bug-14534.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/bug-14534.stub diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13444.php b/tests/PHPStan/Rules/Comparison/data/bug-13444.php new file mode 100644 index 00000000000..487ce9ae19e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13444.php @@ -0,0 +1,28 @@ +get($key, null, \Memcached::GET_EXTENDED); + + if ($memcached->getResultCode() !== \Memcached::RES_SUCCESS) { + return; + } + + if (!is_array($extendedReturn) || !isset($extendedReturn['value']) || !isset($extendedReturn['cas'])) { + return; + } + + $data = $extendedReturn['value']; + $cas = $extendedReturn['cas']; + \assert(is_float($cas)); + + // Do some work on the data.. + $memcached->cas($cas, $key, $data); + + } while ($memcached->getResultCode() !== \Memcached::RES_SUCCESS); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14534.php b/tests/PHPStan/Rules/Comparison/data/bug-14534.php new file mode 100644 index 00000000000..31a893db5ac --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14534.php @@ -0,0 +1,12 @@ +key() === 1) { + return $spl->key() === 1; + } + + return false; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14534.stub b/tests/PHPStan/Rules/Comparison/data/bug-14534.stub new file mode 100644 index 00000000000..d1f3eeb3cc9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14534.stub @@ -0,0 +1,6 @@ +