Skip to content

Commit 1d2405d

Browse files
VincentLangletphpstan-bot
authored andcommitted
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
1 parent 7604335 commit 1d2405d

13 files changed

Lines changed: 224 additions & 0 deletions

src/Reflection/Php/PhpClassReflectionExtension.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ private function createMethod(
618618
$acceptsNamedArguments = true;
619619
$selfOutType = null;
620620
$phpDocComment = null;
621+
$currentResolvedPhpDoc = null;
621622
$methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $methodReflection);
622623
foreach ($methodSignaturesResult as $signatureType => $methodSignatures) {
623624
if ($methodSignatures === null) {
@@ -732,6 +733,22 @@ private function createMethod(
732733
} else {
733734
$hasSideEffects = TrinaryLogic::createMaybe();
734735
}
736+
737+
$isPure = null;
738+
if ($currentResolvedPhpDoc !== null) {
739+
$isPure = $currentResolvedPhpDoc->isPure();
740+
}
741+
if ($isPure === null) {
742+
$classResolvedPhpDoc = $declaringClass->getResolvedPhpDoc();
743+
if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) {
744+
$isPure = true;
745+
} elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) {
746+
$isPure = false;
747+
}
748+
}
749+
if ($isPure !== null) {
750+
$hasSideEffects = TrinaryLogic::createFromBoolean(!$isPure);
751+
}
735752
return new NativeMethodReflection(
736753
$this->reflectionProviderProvider->getReflectionProvider(),
737754
$declaringClass,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Analyser\RicherScopeGetTypeHelper;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use function array_merge;
9+
10+
/**
11+
* @extends RuleTestCase<StrictComparisonOfDifferentTypesRule>
12+
*/
13+
class Bug14534MethodImpureTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new StrictComparisonOfDifferentTypesRule(
19+
self::getContainer()->getByType(RicherScopeGetTypeHelper::class),
20+
new PossiblyImpureTipHelper(true),
21+
true,
22+
true,
23+
true,
24+
);
25+
}
26+
27+
public function testRule(): void
28+
{
29+
$this->analyse([__DIR__ . '/data/bug-14534-method-impure.php'], []);
30+
}
31+
32+
public static function getAdditionalConfigFiles(): array
33+
{
34+
return array_merge(
35+
parent::getAdditionalConfigFiles(),
36+
[__DIR__ . '/bug-14534-method-impure.neon'],
37+
);
38+
}
39+
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Analyser\RicherScopeGetTypeHelper;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use function array_merge;
9+
10+
/**
11+
* @extends RuleTestCase<StrictComparisonOfDifferentTypesRule>
12+
*/
13+
class Bug14534Test extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new StrictComparisonOfDifferentTypesRule(
19+
self::getContainer()->getByType(RicherScopeGetTypeHelper::class),
20+
new PossiblyImpureTipHelper(true),
21+
true,
22+
true,
23+
true,
24+
);
25+
}
26+
27+
public function testRule(): void
28+
{
29+
$this->analyse([__DIR__ . '/data/bug-14534.php'], []);
30+
}
31+
32+
public static function getAdditionalConfigFiles(): array
33+
{
34+
return array_merge(
35+
parent::getAdditionalConfigFiles(),
36+
[__DIR__ . '/bug-14534.neon'],
37+
);
38+
}
39+
40+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
parameters:
2+
stubFiles:
3+
- data/bug-14534-method-impure.stub
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
parameters:
2+
stubFiles:
3+
- data/bug-14534.stub
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14534MethodImpure;
4+
5+
function sayStoreCas(string $key): void
6+
{
7+
$memcached = new \Memcached();
8+
9+
do {
10+
$extendedReturn = $memcached->get($key, null, \Memcached::GET_EXTENDED);
11+
12+
if ($memcached->getResultCode() !== \Memcached::RES_SUCCESS) {
13+
return;
14+
}
15+
16+
if (!is_array($extendedReturn) || !isset($extendedReturn['value']) || !isset($extendedReturn['cas'])) {
17+
return;
18+
}
19+
20+
$data = $extendedReturn['value'];
21+
$cas = $extendedReturn['cas'];
22+
\assert(is_float($cas));
23+
24+
$memcached->cas($cas, $key, $data);
25+
26+
} while ($memcached->getResultCode() !== \Memcached::RES_SUCCESS);
27+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
class Memcached {
4+
/**
5+
* @phpstan-impure
6+
*/
7+
public function getResultCode(): int {}
8+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14534;
4+
5+
function sayStoreCas(string $key): void
6+
{
7+
$memcached = new \Memcached();
8+
9+
do {
10+
$extendedReturn = $memcached->get($key, null, \Memcached::GET_EXTENDED);
11+
12+
if ($memcached->getResultCode() !== \Memcached::RES_SUCCESS) {
13+
return;
14+
}
15+
16+
if (!is_array($extendedReturn) || !isset($extendedReturn['value']) || !isset($extendedReturn['cas'])) {
17+
return;
18+
}
19+
20+
$data = $extendedReturn['value'];
21+
$cas = $extendedReturn['cas'];
22+
\assert(is_float($cas));
23+
24+
$memcached->cas($cas, $key, $data);
25+
26+
} while ($memcached->getResultCode() !== \Memcached::RES_SUCCESS);
27+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
/**
4+
* @phpstan-all-methods-impure
5+
*/
6+
class Memcached {}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Pure;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use function array_merge;
8+
9+
/**
10+
* @extends RuleTestCase<PureFunctionRule>
11+
*/
12+
class Bug14534AllPureBuiltinTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new PureFunctionRule(new FunctionPurityCheck());
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/bug-14534-all-pure-builtin.php'], []);
23+
}
24+
25+
public static function getAdditionalConfigFiles(): array
26+
{
27+
return array_merge(
28+
parent::getAdditionalConfigFiles(),
29+
[__DIR__ . '/bug-14534-all-pure-builtin.neon'],
30+
);
31+
}
32+
33+
}

0 commit comments

Comments
 (0)