diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index dbd3f650f40..b85178f926b 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -33,6 +33,7 @@ public function __construct( private bool $isVariadic, private ?Type $throwType, private TemplateTypeMap $templateTypeMap, + private ?bool $isPure = null, ) { } @@ -132,6 +133,9 @@ public function hasSideEffects(): TrinaryLogic if ($this->returnType->isVoid()->yes()) { return TrinaryLogic::createYes(); } + if ($this->isPure !== null) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->returnType)->yes()) { return TrinaryLogic::createYes(); @@ -172,11 +176,7 @@ public function isAbstract(): TrinaryLogic public function isPure(): TrinaryLogic { - if ($this->hasSideEffects()->yes()) { - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createMaybe(); + return $this->hasSideEffects()->negate(); } public function getPureUnlessCallableIsImpureParameters(): array diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index efbf6423255..1e01e606d0d 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -71,6 +71,19 @@ private function findClassReflectionWithMethod( $isStatic = $methodTags[$methodName]->isStatic(); $nativeCallMethodName = $isStatic ? '__callStatic' : '__call'; + $nativeCallMethod = $classReflection->hasNativeMethod($nativeCallMethodName) + ? $classReflection->getNativeMethod($nativeCallMethodName) + : null; + $classResolvedPhpDoc = $classReflection->getResolvedPhpDoc(); + $isPure = null; + if ($nativeCallMethod !== null) { + $nativePurity = $nativeCallMethod->isPure(); + $isPure = $nativePurity->yes() ? true : ($nativePurity->no() ? false : null); + } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) { + $isPure = true; + } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) { + $isPure = false; + } return new AnnotationMethodReflection( $methodName, @@ -84,10 +97,9 @@ private function findClassReflectionWithMethod( $parameters, $isStatic, $this->detectMethodVariadic($parameters), - $classReflection->hasNativeMethod($nativeCallMethodName) - ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() - : null, + $nativeCallMethod !== null ? $nativeCallMethod->getThrowType() : null, $templateTypeMap, + $isPure, ); } diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php index 9aa45aba7a5..b76a3651a34 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php @@ -7,6 +7,8 @@ use AnnotationsMethods\BazBaz; use AnnotationsMethods\Foo; use AnnotationsMethods\FooInterface; +use AnnotationsMethods\ImpureMagicMethods; +use AnnotationsMethods\PureMagicMethods; use PHPStan\Analyser\Scope; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\PhpMethodReflection; @@ -1032,4 +1034,19 @@ public function testOverridingNativeMethodsWithAnnotationsDoesNotBreakGetNativeM $this->assertInstanceOf(PhpMethodReflection::class, $class->getNativeMethod('overridenMethodWithAnnotation')); } + public function testMagicMethodsInheritClassPurity(): void + { + $reflectionProvider = self::createReflectionProvider(); + foreach ([PureMagicMethods::class => true, ImpureMagicMethods::class => false] as $className => $isPure) { + $class = $reflectionProvider->getClass($className); + $scope = $this->createStub(Scope::class); + $scope->method('isInClass')->willReturn(true); + $scope->method('getClassReflection')->willReturn($class); + $scope->method('canCallMethod')->willReturn(true); + $method = $class->getMethod('magic', $scope); + $this->assertSame($isPure, $method->isPure()->yes()); + $this->assertSame(!$isPure, $method->hasSideEffects()->yes()); + } + } + } diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-methods.php b/tests/PHPStan/Reflection/Annotations/data/annotations-methods.php index effb233be9d..c45e9e99d2f 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-methods.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-methods.php @@ -147,3 +147,19 @@ trait FooTrait { } + +/** + * @phpstan-all-methods-pure + * @method int magic() + */ +class PureMagicMethods +{ +} + +/** + * @phpstan-all-methods-impure + * @method int magic() + */ +class ImpureMagicMethods +{ +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index 61e32c9d606..4085b397361 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -347,6 +347,15 @@ public function testAllMethodsArePure(): void ]); } + public function testBug15322(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-15322.php'], [ + ['Impure call to method Bug15322\\ImpureDispatcher::magic() in pure method Bug15322\\Test::runImpureDispatcher().', 63], + ['Impure call to method Bug15322\\ImpureStaticDispatcher::magic() in pure method Bug15322\\Test::runImpureStaticDispatcher().', 69], + ]); + } + #[RequiresPhp('>= 8.0.0')] public function testBug14138Pure(): void { diff --git a/tests/PHPStan/Rules/Pure/data/bug-15322.php b/tests/PHPStan/Rules/Pure/data/bug-15322.php new file mode 100644 index 00000000000..a5e72a0bc3d --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-15322.php @@ -0,0 +1,77 @@ +magic(); + } + + /** @phpstan-pure */ + public function runImpureDispatcher(ImpureDispatcher $object): int + { + return $object->magic(); + } + + /** @phpstan-pure */ + public function runImpureStaticDispatcher(): int + { + return ImpureStaticDispatcher::magic(); + } + + /** @phpstan-pure */ + public function runPureDispatcher(PureDispatcher $object): int + { + return $object->magic(); + } +}