From 6bd17958d4d7294a1513180893f5d2e714ef659f Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sat, 2 May 2026 18:12:00 +0000 Subject: [PATCH 01/10] Report impure method overriding pure parent method in `MethodSignatureRule` - Add purity compatibility check in MethodSignatureRule::processNode() - When a parent method has @phpstan-pure and the child method has @phpstan-impure, report an error with identifier method.impureOverridePure - The check runs for all parent sources: parent classes, interfaces, and abstract trait methods - Covers @phpstan-all-methods-pure on parent class (purity is inherited per-method) - Also covers grandchild inheritance and multiple interface implementation --- src/Rules/Methods/MethodSignatureRule.php | 10 + .../Rules/Methods/MethodSignatureRuleTest.php | 40 ++++ .../PHPStan/Rules/Methods/data/bug-14563.php | 212 ++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-14563.php diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 267b6ae5189..80d334bd231 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -66,6 +66,16 @@ public function processNode(Node $node, Scope $scope): array $errors = []; $declaringClass = $method->getDeclaringClass(); foreach ($this->parentMethodHelper->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { + if ($method->isPure()->no() && $parentMethod->isPure()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Impure method %s::%s() overrides pure method %s::%s().', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $parentMethodDeclaringClass->getDisplayName(), + $parentMethod->getName(), + ))->identifier('method.impureOverridePure')->build(); + } + $parentVariants = $parentMethod->getVariants(); if (count($parentVariants) !== 1) { continue; diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 39efdf4aeb7..4abefb3a01d 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -565,4 +565,44 @@ public function testBug14320(): void $this->analyse([__DIR__ . '/data/bug-14320.php'], []); } + public function testBug14563(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-14563.php'], [ + [ + 'Impure method Bug14563\ChildImpureOverridesPure::pure() overrides pure method Bug14563\Foo::pure().', + 31, + ], + [ + 'Impure method Bug14563\ImpureImplementation::pureMethod() overrides pure method Bug14563\PureInterface::pureMethod().', + 93, + ], + [ + 'Impure method Bug14563\ImpureChildOfAllMethodsPure::method() overrides pure method Bug14563\AllMethodsPureParent::method().', + 126, + ], + [ + 'Impure method Bug14563\ImpureTraitUser::pureTraitMethod() overrides pure method Bug14563\PureTrait::pureTraitMethod().', + 147, + ], + [ + 'Impure method Bug14563\ChildImpureOverridesPureExtended::pure() overrides pure method Bug14563\Foo::pure().', + 158, + ], + [ + 'Impure method Bug14563\GrandchildImpureOverridesPure::pure() overrides pure method Bug14563\ChildPureOverridesPure::pure().', + 169, + ], + [ + 'Impure method Bug14563\ImpureMultipleInterfaces::sharedMethod() overrides pure method Bug14563\PureInterfaceA::sharedMethod().', + 207, + ], + [ + 'Impure method Bug14563\ImpureMultipleInterfaces::sharedMethod() overrides pure method Bug14563\PureInterfaceB::sharedMethod().', + 207, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14563.php b/tests/PHPStan/Rules/Methods/data/bug-14563.php new file mode 100644 index 00000000000..ae653058654 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14563.php @@ -0,0 +1,212 @@ + Date: Sat, 2 May 2026 19:02:30 +0000 Subject: [PATCH 02/10] Add static method test cases for impure-overrides-pure check Co-Authored-By: Claude Opus 4.6 --- .../Rules/Methods/MethodSignatureRuleTest.php | 8 ++ .../PHPStan/Rules/Methods/data/bug-14563.php | 84 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 4abefb3a01d..995b7f53993 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -602,6 +602,14 @@ public function testBug14563(): void 'Impure method Bug14563\ImpureMultipleInterfaces::sharedMethod() overrides pure method Bug14563\PureInterfaceB::sharedMethod().', 207, ], + [ + 'Impure method Bug14563\StaticChildImpureOverridesPure::pure() overrides pure method Bug14563\StaticFoo::pure().', + 240, + ], + [ + 'Impure method Bug14563\StaticImpureImplementation::pureMethod() overrides pure method Bug14563\StaticPureInterface::pureMethod().', + 291, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14563.php b/tests/PHPStan/Rules/Methods/data/bug-14563.php index ae653058654..4fe2ffbdae2 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14563.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14563.php @@ -210,3 +210,87 @@ public function sharedMethod(): int } } + +class StaticFoo +{ + + /** @phpstan-pure */ + public static function pure(): int + { + return 1; + } + + /** @phpstan-impure */ + public static function impure(): int + { + return random_int(0, 1); + } + + public static function noAnnotation(): int + { + return 1; + } + +} + +class StaticChildImpureOverridesPure extends StaticFoo +{ + + /** @phpstan-impure */ + public static function pure(): int + { + return random_int(0, 1); + } + +} + +class StaticChildNoAnnotationOverridesPure extends StaticFoo +{ + + public static function pure(): int + { + return 2; + } + +} + +class StaticChildPureOverridesPure extends StaticFoo +{ + + /** @phpstan-pure */ + public static function pure(): int + { + return 2; + } + +} + +class StaticChildImpureOverridesNoAnnotation extends StaticFoo +{ + + /** @phpstan-impure */ + public static function noAnnotation(): int + { + return random_int(0, 1); + } + +} + +interface StaticPureInterface +{ + + /** @phpstan-pure */ + public static function pureMethod(): int; + +} + +class StaticImpureImplementation implements StaticPureInterface +{ + + /** @phpstan-impure */ + public static function pureMethod(): int + { + return random_int(0, 1); + } + +} From 8e785458ec3cc15aa5ba095809be820454c85896 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 2 May 2026 19:36:28 +0000 Subject: [PATCH 03/10] Skip trait abstract method purity test on PHP < 8.0 Trait abstract method detection in collectParentMethods does not work on PHP 7.4, consistent with other trait-related tests in this file that use #[RequiresPhp('>= 8.0.0')]. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Methods/MethodSignatureRuleTest.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 995b7f53993..70ae012121b 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -569,7 +569,7 @@ public function testBug14563(): void { $this->reportMaybes = true; $this->reportStatic = true; - $this->analyse([__DIR__ . '/data/bug-14563.php'], [ + $errors = [ [ 'Impure method Bug14563\ChildImpureOverridesPure::pure() overrides pure method Bug14563\Foo::pure().', 31, @@ -582,10 +582,16 @@ public function testBug14563(): void 'Impure method Bug14563\ImpureChildOfAllMethodsPure::method() overrides pure method Bug14563\AllMethodsPureParent::method().', 126, ], - [ + ]; + + if (PHP_VERSION_ID >= 80000) { + $errors[] = [ 'Impure method Bug14563\ImpureTraitUser::pureTraitMethod() overrides pure method Bug14563\PureTrait::pureTraitMethod().', 147, - ], + ]; + } + + $errors = array_merge($errors, [ [ 'Impure method Bug14563\ChildImpureOverridesPureExtended::pure() overrides pure method Bug14563\Foo::pure().', 158, @@ -611,6 +617,7 @@ public function testBug14563(): void 291, ], ]); + $this->analyse([__DIR__ . '/data/bug-14563.php'], $errors); } } From 9951d2418a355c77e05f4286aaa3c794d9722b5a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 2 May 2026 21:34:41 +0000 Subject: [PATCH 04/10] Change error identifier to method.purity Co-Authored-By: Claude Opus 4.6 --- src/Rules/Methods/MethodSignatureRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 80d334bd231..41ab14252e6 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -73,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array $method->getName(), $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), - ))->identifier('method.impureOverridePure')->build(); + ))->identifier('method.purity')->build(); } $parentVariants = $parentMethod->getVariants(); From 83d04625e5a44f14cb742801ff06a4912a739117 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 2 May 2026 21:34:44 +0000 Subject: [PATCH 05/10] Split trait purity test into dedicated file with RequiresPhp 8.0 Co-Authored-By: Claude Opus 4.6 --- .../Rules/Methods/MethodSignatureRuleTest.php | 38 ++++++++++--------- .../Rules/Methods/data/bug-14563-trait.php | 24 ++++++++++++ .../PHPStan/Rules/Methods/data/bug-14563.php | 21 ---------- 3 files changed, 44 insertions(+), 39 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-14563-trait.php diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 70ae012121b..477f6c30e46 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -569,7 +569,7 @@ public function testBug14563(): void { $this->reportMaybes = true; $this->reportStatic = true; - $errors = [ + $this->analyse([__DIR__ . '/data/bug-14563.php'], [ [ 'Impure method Bug14563\ChildImpureOverridesPure::pure() overrides pure method Bug14563\Foo::pure().', 31, @@ -582,42 +582,44 @@ public function testBug14563(): void 'Impure method Bug14563\ImpureChildOfAllMethodsPure::method() overrides pure method Bug14563\AllMethodsPureParent::method().', 126, ], - ]; - - if (PHP_VERSION_ID >= 80000) { - $errors[] = [ - 'Impure method Bug14563\ImpureTraitUser::pureTraitMethod() overrides pure method Bug14563\PureTrait::pureTraitMethod().', - 147, - ]; - } - - $errors = array_merge($errors, [ [ 'Impure method Bug14563\ChildImpureOverridesPureExtended::pure() overrides pure method Bug14563\Foo::pure().', - 158, + 137, ], [ 'Impure method Bug14563\GrandchildImpureOverridesPure::pure() overrides pure method Bug14563\ChildPureOverridesPure::pure().', - 169, + 148, ], [ 'Impure method Bug14563\ImpureMultipleInterfaces::sharedMethod() overrides pure method Bug14563\PureInterfaceA::sharedMethod().', - 207, + 186, ], [ 'Impure method Bug14563\ImpureMultipleInterfaces::sharedMethod() overrides pure method Bug14563\PureInterfaceB::sharedMethod().', - 207, + 186, ], [ 'Impure method Bug14563\StaticChildImpureOverridesPure::pure() overrides pure method Bug14563\StaticFoo::pure().', - 240, + 219, ], [ 'Impure method Bug14563\StaticImpureImplementation::pureMethod() overrides pure method Bug14563\StaticPureInterface::pureMethod().', - 291, + 270, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug14563Trait(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-14563-trait.php'], [ + [ + 'Impure method Bug14563Trait\ImpureTraitUser::pureTraitMethod() overrides pure method Bug14563Trait\PureTrait::pureTraitMethod().', + 19, ], ]); - $this->analyse([__DIR__ . '/data/bug-14563.php'], $errors); } } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14563-trait.php b/tests/PHPStan/Rules/Methods/data/bug-14563-trait.php new file mode 100644 index 00000000000..a9000b7e6e5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14563-trait.php @@ -0,0 +1,24 @@ + Date: Tue, 5 May 2026 13:54:26 +0000 Subject: [PATCH 06/10] Change error identifier to method.impure Co-Authored-By: Claude Opus 4.6 --- src/Rules/Methods/MethodSignatureRule.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 41ab14252e6..822d589463c 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -41,6 +41,7 @@ public function __construct( private ParentMethodHelper $parentMethodHelper, private bool $reportMaybes, private bool $reportStatic, + private bool $checkPurity, ) { } @@ -66,14 +67,14 @@ public function processNode(Node $node, Scope $scope): array $errors = []; $declaringClass = $method->getDeclaringClass(); foreach ($this->parentMethodHelper->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { - if ($method->isPure()->no() && $parentMethod->isPure()->yes()) { + if ($this->checkPurity && $method->isPure()->no() && $parentMethod->isPure()->yes()) { $errors[] = RuleErrorBuilder::message(sprintf( 'Impure method %s::%s() overrides pure method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), - ))->identifier('method.purity')->build(); + ))->identifier('method.impure')->build(); } $parentVariants = $parentMethod->getVariants(); From 9a80fb686d3d0615514b7109cf3885fefb80b005 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 5 May 2026 13:54:31 +0000 Subject: [PATCH 07/10] Put purity override check behind checkMethodPurityOverride toggle enabled in bleedingEdge Co-Authored-By: Claude Opus 4.6 --- conf/bleedingEdge.neon | 1 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + conf/services.neon | 1 + tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php | 6 +++++- tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php | 2 +- 6 files changed, 10 insertions(+), 2 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index e8e0152c6a1..1c97faf8655 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -17,3 +17,4 @@ parameters: assignToByRefForeachExpr: true curlSetOptArrayTypes: true checkDateIntervalConstructor: true + checkMethodPurityOverride: true diff --git a/conf/config.neon b/conf/config.neon index fcbb79b1d81..7a0a11ea4b1 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -44,6 +44,7 @@ parameters: assignToByRefForeachExpr: false curlSetOptArrayTypes: false checkDateIntervalConstructor: false + checkMethodPurityOverride: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 7cff420cc3f..711fd7340f0 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -46,6 +46,7 @@ parametersSchema: assignToByRefForeachExpr: bool() curlSetOptArrayTypes: bool() checkDateIntervalConstructor: bool() + checkMethodPurityOverride: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/conf/services.neon b/conf/services.neon index c5c1d4019ca..33a3ab1c9bc 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -98,6 +98,7 @@ services: arguments: reportMaybes: %reportMaybesInMethodSignatures% reportStatic: %reportStaticMethodSignatures% + checkPurity: %featureToggles.checkMethodPurityOverride% phpstanDiagnoseExtension: class: PHPStan\Diagnose\PHPStanDiagnoseExtension diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 477f6c30e46..f8eaf016b99 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -19,6 +19,8 @@ class MethodSignatureRuleTest extends RuleTestCase private bool $reportStatic; + private bool $checkPurity = false; + protected function getRule(): Rule { $phpVersion = new PhpVersion(PHP_VERSION_ID); @@ -27,7 +29,7 @@ protected function getRule(): Rule return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), $this->reportMaybes, $this->reportStatic), + new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), $this->reportMaybes, $this->reportStatic, $this->checkPurity), true, new MethodParameterComparisonHelper($phpVersion), new MethodVisibilityComparisonHelper(), @@ -569,6 +571,7 @@ public function testBug14563(): void { $this->reportMaybes = true; $this->reportStatic = true; + $this->checkPurity = true; $this->analyse([__DIR__ . '/data/bug-14563.php'], [ [ 'Impure method Bug14563\ChildImpureOverridesPure::pure() overrides pure method Bug14563\Foo::pure().', @@ -614,6 +617,7 @@ public function testBug14563Trait(): void { $this->reportMaybes = true; $this->reportStatic = true; + $this->checkPurity = true; $this->analyse([__DIR__ . '/data/bug-14563-trait.php'], [ [ 'Impure method Bug14563Trait\ImpureTraitUser::pureTraitMethod() overrides pure method Bug14563Trait\PureTrait::pureTraitMethod().', diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 8f2b1d488ff..b8c2954417b 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -30,7 +30,7 @@ protected function getRule(): Rule return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), true, true), + new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), true, true, false), false, new MethodParameterComparisonHelper($phpVersion), new MethodVisibilityComparisonHelper(), From 6d81c6b9993d1a6bfb8ee360b7b1a4dcdb4aa2bf Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 5 May 2026 18:22:05 +0000 Subject: [PATCH 08/10] Rename checkMethodPurityOverride toggle to reportMethodPurityOverride Co-Authored-By: Claude Opus 4.6 --- conf/bleedingEdge.neon | 2 +- conf/config.neon | 2 +- conf/parametersSchema.neon | 2 +- conf/services.neon | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 1c97faf8655..76afb8bdd54 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -17,4 +17,4 @@ parameters: assignToByRefForeachExpr: true curlSetOptArrayTypes: true checkDateIntervalConstructor: true - checkMethodPurityOverride: true + reportMethodPurityOverride: true diff --git a/conf/config.neon b/conf/config.neon index 7a0a11ea4b1..1628b4b83a0 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -44,7 +44,7 @@ parameters: assignToByRefForeachExpr: false curlSetOptArrayTypes: false checkDateIntervalConstructor: false - checkMethodPurityOverride: false + reportMethodPurityOverride: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 711fd7340f0..15e6e02c215 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -46,7 +46,7 @@ parametersSchema: assignToByRefForeachExpr: bool() curlSetOptArrayTypes: bool() checkDateIntervalConstructor: bool() - checkMethodPurityOverride: bool() + reportMethodPurityOverride: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/conf/services.neon b/conf/services.neon index 33a3ab1c9bc..fe78de00f8e 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -98,7 +98,7 @@ services: arguments: reportMaybes: %reportMaybesInMethodSignatures% reportStatic: %reportStaticMethodSignatures% - checkPurity: %featureToggles.checkMethodPurityOverride% + checkPurity: %featureToggles.reportMethodPurityOverride% phpstanDiagnoseExtension: class: PHPStan\Diagnose\PHPStanDiagnoseExtension From e0144c96a9270f1e9f982700b64eaf549754d4dd Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 5 May 2026 18:25:47 +0000 Subject: [PATCH 09/10] Rename checkPurity parameter to reportMethodPurityOverride for consistency Co-Authored-By: Claude Opus 4.6 --- conf/services.neon | 2 +- src/Rules/Methods/MethodSignatureRule.php | 4 ++-- tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/conf/services.neon b/conf/services.neon index fe78de00f8e..948baee6a76 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -98,7 +98,7 @@ services: arguments: reportMaybes: %reportMaybesInMethodSignatures% reportStatic: %reportStaticMethodSignatures% - checkPurity: %featureToggles.reportMethodPurityOverride% + reportMethodPurityOverride: %featureToggles.reportMethodPurityOverride% phpstanDiagnoseExtension: class: PHPStan\Diagnose\PHPStanDiagnoseExtension diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 822d589463c..3c476d14b87 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -41,7 +41,7 @@ public function __construct( private ParentMethodHelper $parentMethodHelper, private bool $reportMaybes, private bool $reportStatic, - private bool $checkPurity, + private bool $reportMethodPurityOverride, ) { } @@ -67,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; $declaringClass = $method->getDeclaringClass(); foreach ($this->parentMethodHelper->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { - if ($this->checkPurity && $method->isPure()->no() && $parentMethod->isPure()->yes()) { + if ($this->reportMethodPurityOverride && $method->isPure()->no() && $parentMethod->isPure()->yes()) { $errors[] = RuleErrorBuilder::message(sprintf( 'Impure method %s::%s() overrides pure method %s::%s().', $method->getDeclaringClass()->getDisplayName(), diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index f8eaf016b99..0fa8df0260e 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -19,7 +19,7 @@ class MethodSignatureRuleTest extends RuleTestCase private bool $reportStatic; - private bool $checkPurity = false; + private bool $reportMethodPurityOverride = false; protected function getRule(): Rule { @@ -29,7 +29,7 @@ protected function getRule(): Rule return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), $this->reportMaybes, $this->reportStatic, $this->checkPurity), + new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), $this->reportMaybes, $this->reportStatic, $this->reportMethodPurityOverride), true, new MethodParameterComparisonHelper($phpVersion), new MethodVisibilityComparisonHelper(), @@ -571,7 +571,7 @@ public function testBug14563(): void { $this->reportMaybes = true; $this->reportStatic = true; - $this->checkPurity = true; + $this->reportMethodPurityOverride = true; $this->analyse([__DIR__ . '/data/bug-14563.php'], [ [ 'Impure method Bug14563\ChildImpureOverridesPure::pure() overrides pure method Bug14563\Foo::pure().', @@ -617,7 +617,7 @@ public function testBug14563Trait(): void { $this->reportMaybes = true; $this->reportStatic = true; - $this->checkPurity = true; + $this->reportMethodPurityOverride = true; $this->analyse([__DIR__ . '/data/bug-14563-trait.php'], [ [ 'Impure method Bug14563Trait\ImpureTraitUser::pureTraitMethod() overrides pure method Bug14563Trait\PureTrait::pureTraitMethod().', From 3f611f716a5ae23753825f882000f0eab9f095a8 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 5 May 2026 19:12:05 +0000 Subject: [PATCH 10/10] Add void return type test cases for purity override check Test that methods returning :void (implicitly impure) are handled correctly when overriding pure/impure parent methods and when being overridden by pure/impure child methods. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Methods/MethodSignatureRuleTest.php | 8 ++- .../PHPStan/Rules/Methods/data/bug-14563.php | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 0fa8df0260e..b016afd0a48 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -601,13 +601,17 @@ public function testBug14563(): void 'Impure method Bug14563\ImpureMultipleInterfaces::sharedMethod() overrides pure method Bug14563\PureInterfaceB::sharedMethod().', 186, ], + [ + 'Impure method Bug14563\ChildImpureOverridesPureVoid::pureVoid() overrides pure method Bug14563\VoidFoo::pureVoid().', + 211, + ], [ 'Impure method Bug14563\StaticChildImpureOverridesPure::pure() overrides pure method Bug14563\StaticFoo::pure().', - 219, + 284, ], [ 'Impure method Bug14563\StaticImpureImplementation::pureMethod() overrides pure method Bug14563\StaticPureInterface::pureMethod().', - 270, + 335, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14563.php b/tests/PHPStan/Rules/Methods/data/bug-14563.php index 6210cbf3296..4e5ae00a044 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14563.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14563.php @@ -190,6 +190,71 @@ public function sharedMethod(): int } +class VoidFoo +{ + + /** @phpstan-pure */ + public function pureVoid(): void + { + } + + public function unannotatedVoid(): void + { + } + +} + +class ChildImpureOverridesPureVoid extends VoidFoo +{ + + /** @phpstan-impure */ + public function pureVoid(): void + { + random_int(0, 1); + } + +} + +class ChildPureOverridesPureVoid extends VoidFoo +{ + + /** @phpstan-pure */ + public function pureVoid(): void + { + } + +} + +class ChildNoAnnotationOverridesPureVoid extends VoidFoo +{ + + public function pureVoid(): void + { + } + +} + +class ChildImpureOverridesUnannotatedVoid extends VoidFoo +{ + + /** @phpstan-impure */ + public function unannotatedVoid(): void + { + random_int(0, 1); + } + +} + +class ChildPureOverridesUnannotatedVoid extends VoidFoo +{ + + /** @phpstan-pure */ + public function unannotatedVoid(): void + { + } + +} + class StaticFoo {