Skip to content

Commit 9763544

Browse files
zonuexeondrejmirtes
authored andcommitted
Add MemoizationPropertyRule
MemoizationPropertyRule supports static properties
1 parent f74f499 commit 9763544

File tree

5 files changed

+420
-0
lines changed

5 files changed

+420
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Build;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\Assign;
8+
use PhpParser\Node\Expr\AssignOp\Coalesce;
9+
use PhpParser\Node\Expr\BinaryOp\Identical;
10+
use PhpParser\Node\Expr\ConstFetch;
11+
use PhpParser\Node\Expr\PropertyFetch;
12+
use PhpParser\Node\Expr\StaticPropertyFetch;
13+
use PhpParser\Node\Expr\Variable;
14+
use PhpParser\Node\Identifier;
15+
use PhpParser\Node\Name;
16+
use PhpParser\Node\Stmt\Expression;
17+
use PhpParser\Node\Stmt\If_;
18+
use PhpParser\Node\VarLikeIdentifier;
19+
use PHPStan\Analyser\Scope;
20+
use PHPStan\File\FileHelper;
21+
use PHPStan\Node\InClassMethodNode;
22+
use PHPStan\Rules\Rule;
23+
use PHPStan\Rules\RuleErrorBuilder;
24+
use function count;
25+
use function dirname;
26+
use function is_string;
27+
use function str_starts_with;
28+
use function strcasecmp;
29+
30+
/**
31+
* @implements Rule<If_>
32+
*/
33+
final class MemoizationPropertyRule implements Rule
34+
{
35+
36+
public function __construct(private FileHelper $fileHelper, private bool $skipTests = true)
37+
{
38+
}
39+
40+
public function getNodeType(): string
41+
{
42+
return If_::class;
43+
}
44+
45+
public function processNode(Node $node, Scope $scope): array
46+
{
47+
$ifNode = $node;
48+
49+
if (count($ifNode->stmts) !== 1
50+
|| !$ifNode->stmts[0] instanceof Expression
51+
|| count($ifNode->elseifs) !== 0
52+
|| $ifNode->else !== null
53+
|| !$ifNode->cond instanceof Identical
54+
|| !$this->isSupportedFetchNode($ifNode->cond->left)
55+
|| !$ifNode->cond->right instanceof ConstFetch
56+
|| strcasecmp($ifNode->cond->right->name->name, 'null') !== 0
57+
) {
58+
return [];
59+
}
60+
61+
$ifThenNode = $ifNode->stmts[0]->expr;
62+
if (!$ifThenNode instanceof Assign || !$this->isSupportedFetchNode($ifThenNode->var)) {
63+
return [];
64+
}
65+
66+
if ($this->areNodesNotEqual($ifNode->cond->left, [$ifThenNode->var])) {
67+
return [];
68+
}
69+
70+
if ($this->skipTests && str_starts_with($this->fileHelper->normalizePath($scope->getFile()), $this->fileHelper->normalizePath(dirname(__DIR__, 3) . '/tests'))) {
71+
return [];
72+
}
73+
74+
$errorBuilder = RuleErrorBuilder::message('This initializing if statement can be replaced with null coalescing assignment operator (??=).')
75+
->fixNode($node, static fn (If_ $node) => new Expression(new Coalesce($ifThenNode->var, $ifThenNode->expr)))
76+
->identifier('phpstan.memoizationProperty');
77+
78+
return [
79+
$errorBuilder->build(),
80+
];
81+
}
82+
83+
/**
84+
* @phpstan-assert-if-true PropertyFetch|StaticPropertyFetch $node
85+
*/
86+
private function isSupportedFetchNode(?Expr $node): bool
87+
{
88+
return $node instanceof PropertyFetch || $node instanceof StaticPropertyFetch;
89+
}
90+
91+
/**
92+
* @param list<PropertyFetch|StaticPropertyFetch> $otherNodes
93+
*/
94+
private function areNodesNotEqual(PropertyFetch|StaticPropertyFetch $node, array $otherNodes): bool
95+
{
96+
if ($node instanceof PropertyFetch) {
97+
if (!$node->var instanceof Variable
98+
|| !is_string($node->var->name)
99+
|| !$node->name instanceof Identifier
100+
) {
101+
return true;
102+
}
103+
104+
foreach ($otherNodes as $otherNode) {
105+
if (!$otherNode instanceof PropertyFetch) {
106+
return true;
107+
}
108+
if (!$otherNode->var instanceof Variable
109+
|| !is_string($otherNode->var->name)
110+
|| !$otherNode->name instanceof Identifier
111+
) {
112+
return true;
113+
}
114+
115+
if ($node->var->name !== $otherNode->var->name
116+
|| $node->name->name !== $otherNode->name->name
117+
) {
118+
return true;
119+
}
120+
}
121+
122+
return false;
123+
}
124+
125+
if (!$node->class instanceof Name || !$node->name instanceof VarLikeIdentifier) {
126+
return true;
127+
}
128+
129+
foreach ($otherNodes as $otherNode) {
130+
if (!$otherNode instanceof StaticPropertyFetch) {
131+
return true;
132+
}
133+
134+
if (!$otherNode->class instanceof Name
135+
|| !$otherNode->name instanceof VarLikeIdentifier
136+
) {
137+
return true;
138+
}
139+
140+
if ($node->class->toLowerString() !== $otherNode->class->toLowerString()
141+
|| $node->name->toString() !== $otherNode->name->toString()
142+
) {
143+
return true;
144+
}
145+
}
146+
147+
return false;
148+
}
149+
150+
}

build/phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ rules:
123123
- PHPStan\Build\NamedArgumentsRule
124124
- PHPStan\Build\OverrideAttributeThirdPartyMethodRule
125125
- PHPStan\Build\SkipTestsWithRequiresPhpAttributeRule
126+
- PHPStan\Build\MemoizationPropertyRule
126127

127128
services:
128129
-
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Build;
4+
5+
use PHPStan\File\FileHelper;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use PHPUnit\Framework\Attributes\RequiresPhp;
9+
10+
/**
11+
* @extends RuleTestCase<MemoizationPropertyRule>
12+
*/
13+
final class MemoizationPropertyRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new MemoizationPropertyRule(self::getContainer()->getByType(FileHelper::class), false);
19+
}
20+
21+
public function testRule(): void
22+
{
23+
$this->analyse([__DIR__ . '/data/memoization-property.php'], [
24+
[
25+
'This initializing if statement can be replaced with null coalescing assignment operator (??=).',
26+
13,
27+
],
28+
[
29+
'This initializing if statement can be replaced with null coalescing assignment operator (??=).',
30+
22,
31+
],
32+
[
33+
'This initializing if statement can be replaced with null coalescing assignment operator (??=).',
34+
55,
35+
],
36+
[
37+
'This initializing if statement can be replaced with null coalescing assignment operator (??=).',
38+
85,
39+
],
40+
[
41+
'This initializing if statement can be replaced with null coalescing assignment operator (??=).',
42+
96,
43+
],
44+
]);
45+
}
46+
47+
#[RequiresPhp('>= 8.0')]
48+
public function testFix(): void
49+
{
50+
$this->fix(__DIR__ . '/data/memoization-property.php', __DIR__ . '/data/memoization-property.php.fixed');
51+
}
52+
53+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php // lint >= 8.0
2+
3+
namespace MemoizationProperty;
4+
5+
final class A
6+
{
7+
private ?string $foo = null;
8+
private ?string $bar = null;
9+
private string|false $buz = false;
10+
11+
public function getFoo()
12+
{
13+
if ($this->foo === null) {
14+
$this->foo = random_bytes(1);
15+
}
16+
17+
return $this->foo;
18+
}
19+
20+
public function getBar()
21+
{
22+
if ($this->bar === null) {
23+
$this->bar = random_bytes(1);
24+
}
25+
26+
return $this->bar;
27+
}
28+
29+
/** Not applicable because it has an else clause in the if. */
30+
public function getBarElse()
31+
{
32+
if ($this->bar === null) {
33+
$this->bar = random_bytes(1);
34+
} else {
35+
// no-op
36+
}
37+
38+
return $this->bar;
39+
}
40+
41+
/** Not applicable because it has an elseif clause in the if. */
42+
public function getBarElseIf()
43+
{
44+
if ($this->bar === null) {
45+
$this->bar = random_bytes(1);
46+
} elseif (false) {
47+
// no-op
48+
}
49+
50+
return $this->bar;
51+
}
52+
53+
public function getBarReceiveParam(int $length)
54+
{
55+
if ($this->bar === null) {
56+
$this->bar = random_bytes($length);
57+
}
58+
59+
return $this->bar;
60+
}
61+
62+
/** Not applicable because the body of if is not just an assignment. */
63+
public function getBarComplex()
64+
{
65+
if ($this->bar === null) {
66+
$rand = random_bytes(1);
67+
$this->bar = $rand;
68+
}
69+
70+
return $this->bar;
71+
}
72+
73+
/** Not applicable because it is comparing a property with a non-null value. */
74+
public function getBuz()
75+
{
76+
if ($this->buz === false) {
77+
$this->buz = random_bytes(1);
78+
}
79+
80+
return $this->buz;
81+
}
82+
83+
public function printFoo(): void
84+
{
85+
if ($this->foo === null) {
86+
$this->foo = random_bytes(1);
87+
}
88+
89+
echo $this->foo;
90+
}
91+
92+
private static ?self $singleton = null;
93+
94+
public static function singleton(): self
95+
{
96+
if (self::$singleton === null) {
97+
self::$singleton = new self();
98+
}
99+
100+
return self::$singleton;
101+
}
102+
103+
/** Not applicable because property names are not matched. */
104+
public static function singletonBadProperty(): self
105+
{
106+
if (self::$singleton === null) {
107+
self::$singletom = new self();
108+
}
109+
110+
return self::$singleton;
111+
}
112+
113+
}

0 commit comments

Comments
 (0)