From 1d2405dd6351fcbabb64cfc826f1070508495afa Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:34:16 +0000 Subject: [PATCH] Respect `@phpstan-all-methods-impure`, `@phpstan-all-methods-pure`, and method-level purity annotations on built-in classes - In PhpClassReflectionExtension::createMethod(), the built-in class path (line 614 condition) determined hasSideEffects solely from signature map metadata, ignoring PHPDoc purity annotations from stubs - Added checks for method-level @phpstan-pure/@phpstan-impure from $currentResolvedPhpDoc->isPure(), then class-level @phpstan-all-methods-pure/@phpstan-all-methods-impure from $declaringClass->getResolvedPhpDoc() - This mirrors the existing logic in createUserlandMethodReflection() (lines 908-932) which correctly handles these annotations - Initialized $currentResolvedPhpDoc before the method signature loop to avoid undefined variable when the loop doesn't execute --- .../Php/PhpClassReflectionExtension.php | 17 ++++++++ .../Comparison/Bug14534MethodImpureTest.php | 40 +++++++++++++++++++ .../PHPStan/Rules/Comparison/Bug14534Test.php | 40 +++++++++++++++++++ .../Comparison/bug-14534-method-impure.neon | 3 ++ tests/PHPStan/Rules/Comparison/bug-14534.neon | 3 ++ .../data/bug-14534-method-impure.php | 27 +++++++++++++ .../data/bug-14534-method-impure.stub | 8 ++++ .../Rules/Comparison/data/bug-14534.php | 27 +++++++++++++ .../Rules/Comparison/data/bug-14534.stub | 6 +++ .../Rules/Pure/Bug14534AllPureBuiltinTest.php | 33 +++++++++++++++ .../Pure/bug-14534-all-pure-builtin.neon | 3 ++ .../Pure/data/bug-14534-all-pure-builtin.php | 11 +++++ .../Pure/data/bug-14534-all-pure-builtin.stub | 6 +++ 13 files changed, 224 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/Bug14534MethodImpureTest.php create mode 100644 tests/PHPStan/Rules/Comparison/Bug14534Test.php create mode 100644 tests/PHPStan/Rules/Comparison/bug-14534-method-impure.neon create mode 100644 tests/PHPStan/Rules/Comparison/bug-14534.neon create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14534-method-impure.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14534-method-impure.stub create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14534.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14534.stub create mode 100644 tests/PHPStan/Rules/Pure/Bug14534AllPureBuiltinTest.php create mode 100644 tests/PHPStan/Rules/Pure/bug-14534-all-pure-builtin.neon create mode 100644 tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.php create mode 100644 tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.stub diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index fccce2fa772..930d00f28ae 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -618,6 +618,7 @@ private function createMethod( $acceptsNamedArguments = true; $selfOutType = null; $phpDocComment = null; + $currentResolvedPhpDoc = null; $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $methodReflection); foreach ($methodSignaturesResult as $signatureType => $methodSignatures) { if ($methodSignatures === null) { @@ -732,6 +733,22 @@ private function createMethod( } else { $hasSideEffects = TrinaryLogic::createMaybe(); } + + $isPure = null; + if ($currentResolvedPhpDoc !== null) { + $isPure = $currentResolvedPhpDoc->isPure(); + } + if ($isPure === null) { + $classResolvedPhpDoc = $declaringClass->getResolvedPhpDoc(); + if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) { + $isPure = true; + } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) { + $isPure = false; + } + } + if ($isPure !== null) { + $hasSideEffects = TrinaryLogic::createFromBoolean(!$isPure); + } return new NativeMethodReflection( $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, diff --git a/tests/PHPStan/Rules/Comparison/Bug14534MethodImpureTest.php b/tests/PHPStan/Rules/Comparison/Bug14534MethodImpureTest.php new file mode 100644 index 00000000000..4992b727d41 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/Bug14534MethodImpureTest.php @@ -0,0 +1,40 @@ + + */ +class Bug14534MethodImpureTest 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-method-impure.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/bug-14534-method-impure.neon'], + ); + } + +} 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-method-impure.neon b/tests/PHPStan/Rules/Comparison/bug-14534-method-impure.neon new file mode 100644 index 00000000000..44265fb1ebd --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/bug-14534-method-impure.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/bug-14534-method-impure.stub 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-method-impure.php b/tests/PHPStan/Rules/Comparison/data/bug-14534-method-impure.php new file mode 100644 index 00000000000..bf13d4b09f5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14534-method-impure.php @@ -0,0 +1,27 @@ +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)); + + $memcached->cas($cas, $key, $data); + + } while ($memcached->getResultCode() !== \Memcached::RES_SUCCESS); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14534-method-impure.stub b/tests/PHPStan/Rules/Comparison/data/bug-14534-method-impure.stub new file mode 100644 index 00000000000..e56e3d07a0a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14534-method-impure.stub @@ -0,0 +1,8 @@ +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)); + + $memcached->cas($cas, $key, $data); + + } while ($memcached->getResultCode() !== \Memcached::RES_SUCCESS); +} 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..5933a5d6c2c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14534.stub @@ -0,0 +1,6 @@ + + */ +class Bug14534AllPureBuiltinTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PureFunctionRule(new FunctionPurityCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-14534-all-pure-builtin.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/bug-14534-all-pure-builtin.neon'], + ); + } + +} diff --git a/tests/PHPStan/Rules/Pure/bug-14534-all-pure-builtin.neon b/tests/PHPStan/Rules/Pure/bug-14534-all-pure-builtin.neon new file mode 100644 index 00000000000..e45cdd6bf5e --- /dev/null +++ b/tests/PHPStan/Rules/Pure/bug-14534-all-pure-builtin.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/bug-14534-all-pure-builtin.stub diff --git a/tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.php b/tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.php new file mode 100644 index 00000000000..933fbff608b --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.php @@ -0,0 +1,11 @@ +getResultCode(); +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.stub b/tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.stub new file mode 100644 index 00000000000..6dd14a6173a --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-14534-all-pure-builtin.stub @@ -0,0 +1,6 @@ +