From 673b3c79cf98f3368b2db7e84b8146c2f2964db2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 26 Apr 2026 09:58:26 +0200 Subject: [PATCH 1/4] Declare Memcached as impure --- conf/config.neon | 1 + stubs/Memcached.stub | 6 ++++ ...rictComparisonOfDifferentTypesRuleTest.php | 5 ++++ .../Rules/Comparison/data/bug-13444.php | 28 +++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 stubs/Memcached.stub create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13444.php 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/stubs/Memcached.stub b/stubs/Memcached.stub new file mode 100644 index 00000000000..5933a5d6c2c --- /dev/null +++ b/stubs/Memcached.stub @@ -0,0 +1,6 @@ +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/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); +} From 315cbb8eb236f55e8541a58736b4321a27603306 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 26 Apr 2026 13:49:57 +0200 Subject: [PATCH 2/4] Fix --- .../Php/PhpClassReflectionExtension.php | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index fccce2fa772..bc1cee298be 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, From 96be53f7d46abd0c6ee18df2b15b069ccb945849 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 26 Apr 2026 13:56:25 +0200 Subject: [PATCH 3/4] Add test --- .../PHPStan/Rules/Comparison/Bug14534Test.php | 40 +++++++++++++++++++ tests/PHPStan/Rules/Comparison/bug-14534.neon | 3 ++ .../Rules/Comparison/data/bug-14534.php | 12 ++++++ .../Rules/Comparison/data/bug-14534.stub | 6 +++ 4 files changed, 61 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/Bug14534Test.php create mode 100644 tests/PHPStan/Rules/Comparison/bug-14534.neon create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14534.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14534.stub diff --git a/tests/PHPStan/Rules/Comparison/Bug14534Test.php b/tests/PHPStan/Rules/Comparison/Bug14534Test.php new file mode 100644 index 00000000000..a759a867887 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/Bug14534Test.php @@ -0,0 +1,40 @@ + + */ +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/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-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 @@ + Date: Sun, 26 Apr 2026 14:01:47 +0200 Subject: [PATCH 4/4] Fix --- src/Reflection/Php/PhpClassReflectionExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index bc1cee298be..a31e70433e3 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -621,7 +621,7 @@ private function createMethod( $isPure = null; if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { - $isPure = $this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects']; + $isPure = !$this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects']; } $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $methodReflection);