Skip to content

Commit 6b7a389

Browse files
committed
Apply class purity tags to annotated methods
1 parent 86c58fc commit 6b7a389

6 files changed

Lines changed: 72 additions & 5 deletions

File tree

‎src/Reflection/Annotations/AnnotationMethodReflection.php‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function __construct(
3333
private bool $isVariadic,
3434
private ?Type $throwType,
3535
private TemplateTypeMap $templateTypeMap,
36+
private ?bool $isPure = null,
3637
)
3738
{
3839
}
@@ -132,6 +133,9 @@ public function hasSideEffects(): TrinaryLogic
132133
if ($this->returnType->isVoid()->yes()) {
133134
return TrinaryLogic::createYes();
134135
}
136+
if ($this->isPure !== null) {
137+
return TrinaryLogic::createFromBoolean(!$this->isPure);
138+
}
135139

136140
if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->returnType)->yes()) {
137141
return TrinaryLogic::createYes();
@@ -172,11 +176,7 @@ public function isAbstract(): TrinaryLogic
172176

173177
public function isPure(): TrinaryLogic
174178
{
175-
if ($this->hasSideEffects()->yes()) {
176-
return TrinaryLogic::createNo();
177-
}
178-
179-
return TrinaryLogic::createMaybe();
179+
return $this->hasSideEffects()->negate();
180180
}
181181

182182
public function getPureUnlessCallableIsImpureParameters(): array

‎src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ private function findClassReflectionWithMethod(
7171

7272
$isStatic = $methodTags[$methodName]->isStatic();
7373
$nativeCallMethodName = $isStatic ? '__callStatic' : '__call';
74+
$classResolvedPhpDoc = $classReflection->getResolvedPhpDoc();
75+
$isPure = null;
76+
if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) {
77+
$isPure = true;
78+
} elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) {
79+
$isPure = false;
80+
}
7481

7582
return new AnnotationMethodReflection(
7683
$methodName,
@@ -88,6 +95,7 @@ private function findClassReflectionWithMethod(
8895
? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType()
8996
: null,
9097
$templateTypeMap,
98+
$isPure,
9199
);
92100
}
93101

‎tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use AnnotationsMethods\BazBaz;
88
use AnnotationsMethods\Foo;
99
use AnnotationsMethods\FooInterface;
10+
use AnnotationsMethods\ImpureMagicMethods;
11+
use AnnotationsMethods\PureMagicMethods;
1012
use PHPStan\Analyser\Scope;
1113
use PHPStan\Reflection\PassedByReference;
1214
use PHPStan\Reflection\Php\PhpMethodReflection;
@@ -1032,4 +1034,19 @@ public function testOverridingNativeMethodsWithAnnotationsDoesNotBreakGetNativeM
10321034
$this->assertInstanceOf(PhpMethodReflection::class, $class->getNativeMethod('overridenMethodWithAnnotation'));
10331035
}
10341036

1037+
public function testMagicMethodsInheritClassPurity(): void
1038+
{
1039+
$reflectionProvider = self::createReflectionProvider();
1040+
foreach ([PureMagicMethods::class => true, ImpureMagicMethods::class => false] as $className => $isPure) {
1041+
$class = $reflectionProvider->getClass($className);
1042+
$scope = $this->createStub(Scope::class);
1043+
$scope->method('isInClass')->willReturn(true);
1044+
$scope->method('getClassReflection')->willReturn($class);
1045+
$scope->method('canCallMethod')->willReturn(true);
1046+
$method = $class->getMethod('magic', $scope);
1047+
$this->assertSame($isPure, $method->isPure()->yes());
1048+
$this->assertSame(!$isPure, $method->hasSideEffects()->yes());
1049+
}
1050+
}
1051+
10351052
}

‎tests/PHPStan/Reflection/Annotations/data/annotations-methods.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,19 @@ trait FooTrait
147147
{
148148

149149
}
150+
151+
/**
152+
* @phpstan-all-methods-pure
153+
* @method int magic()
154+
*/
155+
class PureMagicMethods
156+
{
157+
}
158+
159+
/**
160+
* @phpstan-all-methods-impure
161+
* @method int magic()
162+
*/
163+
class ImpureMagicMethods
164+
{
165+
}

‎tests/PHPStan/Rules/Pure/PureMethodRuleTest.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,12 @@ public function testAllMethodsArePure(): void
347347
]);
348348
}
349349

350+
public function testBug15322(): void
351+
{
352+
$this->treatPhpDocTypesAsCertain = true;
353+
$this->analyse([__DIR__ . '/data/bug-15322.php'], []);
354+
}
355+
350356
#[RequiresPhp('>= 8.0.0')]
351357
public function testBug14138Pure(): void
352358
{
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Bug15322;
4+
5+
/**
6+
* @phpstan-all-methods-pure
7+
* @method int magic()
8+
*/
9+
class MagicMethods
10+
{
11+
}
12+
13+
class Test
14+
{
15+
/** @phpstan-pure */
16+
public function run(MagicMethods $object): int
17+
{
18+
return $object->magic();
19+
}
20+
}

0 commit comments

Comments
 (0)