diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index e1026770cef..8c05f711ab7 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -287,8 +287,15 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi $result->paramTags = self::mergeParamTags($this->getParamTags(), $parent, $parameterMapping, $parentClass); $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parent, $parameterMapping, $parentClass); $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping); - $result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping); - $result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping); + // @phpstan-pure / @phpstan-impure on the method itself replaces the parent's + // conditional purity, so the @pure-unless-* tags are inherited only without them. + if ($this->isPure() === null) { + $result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping); + $result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping); + } else { + $result->paramsPureUnlessCallableIsImpure = $this->getParamsPureUnlessCallableIsImpure(); + $result->paramsPureUnlessParameterPassed = $this->getParamsPureUnlessParameterPassed(); + } $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass); $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent, $parameterMapping); diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index f67e9172b60..30801ca2cdf 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -438,4 +438,35 @@ public function testPureUnlessParameterPassed(): void ]); } + public function testPureUnlessImpureOverride(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/pure-unless-impure-override.php'], [ + [ + 'Method PureUnlessImpureOverride\ImpureReplacerWithoutSideEffects::replace() is marked as impure but does not have any side effects.', + 58, + ], + [ + 'Method PureUnlessImpureOverride\ImpureReplacerWithoutSideEffects::map() is marked as impure but does not have any side effects.', + 66, + ], + [ + 'Impure echo in pure method PureUnlessImpureOverride\InheritingReplacer::replace().', + 78, + ], + [ + 'Impure echo in pure method PureUnlessImpureOverride\InheritingReplacer::map().', + 86, + ], + [ + 'Method PureUnlessImpureOverride\PureReplacer::replace() is marked as pure but parameter $count is passed by reference.', + 102, + ], + [ + 'Possibly impure call to a callable in pure method PureUnlessImpureOverride\PureReplacer::map().', + 115, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-impure-override.php b/tests/PHPStan/Rules/Pure/data/pure-unless-impure-override.php new file mode 100644 index 00000000000..cf15156d48c --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-impure-override.php @@ -0,0 +1,118 @@ +