Skip to content

Commit 9bdcdf3

Browse files
committed
GetNonVirtualPropertyHookReadRule - level 3
1 parent fc33730 commit 9bdcdf3

File tree

4 files changed

+205
-0
lines changed

4 files changed

+205
-0
lines changed

conf/config.level3.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ rules:
1616
- PHPStan\Rules\Generators\YieldTypeRule
1717
- PHPStan\Rules\Methods\ReturnTypeRule
1818
- PHPStan\Rules\Properties\DefaultValueTypesAssignedToPropertiesRule
19+
- PHPStan\Rules\Properties\GetNonVirtualPropertyHookReadRule
1920
- PHPStan\Rules\Properties\ReadOnlyPropertyAssignRule
2021
- PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule
2122
- PHPStan\Rules\Properties\ReadOnlyPropertyAssignRefRule
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\ClassPropertiesNode;
8+
use PHPStan\Node\ClassPropertyNode;
9+
use PHPStan\Node\Property\PropertyRead;
10+
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use function array_key_exists;
14+
use function sprintf;
15+
16+
/**
17+
* @implements Rule<ClassPropertiesNode>
18+
*/
19+
final class GetNonVirtualPropertyHookReadRule implements Rule
20+
{
21+
22+
public function getNodeType(): string
23+
{
24+
return ClassPropertiesNode::class;
25+
}
26+
27+
public function processNode(Node $node, Scope $scope): array
28+
{
29+
$reads = [];
30+
$classReflection = $node->getClassReflection();
31+
foreach ($node->getPropertyUsages() as $propertyUsage) {
32+
if (!$propertyUsage instanceof PropertyRead) {
33+
continue;
34+
}
35+
36+
$fetch = $propertyUsage->getFetch();
37+
if (!$fetch instanceof Node\Expr\PropertyFetch) {
38+
continue;
39+
}
40+
41+
if (!$fetch->name instanceof Node\Identifier) {
42+
continue;
43+
}
44+
45+
$propertyName = $fetch->name->toString();
46+
if (!$fetch->var instanceof Node\Expr\Variable || $fetch->var->name !== 'this') {
47+
continue;
48+
}
49+
50+
$usageScope = $propertyUsage->getScope();
51+
$inFunction = $usageScope->getFunction();
52+
if (!$inFunction instanceof PhpMethodFromParserNodeReflection) {
53+
continue;
54+
}
55+
56+
if (!$inFunction->isPropertyHook()) {
57+
continue;
58+
}
59+
60+
if ($inFunction->getPropertyHookName() !== 'get') {
61+
continue;
62+
}
63+
64+
if ($propertyName !== $inFunction->getHookedPropertyName()) {
65+
continue;
66+
}
67+
68+
$reads[$propertyName] = true;
69+
}
70+
71+
$errors = [];
72+
foreach ($node->getProperties() as $propertyNode) {
73+
if (!$propertyNode->hasHooks()) {
74+
continue;
75+
}
76+
77+
if (array_key_exists($propertyNode->getName(), $reads)) {
78+
continue;
79+
}
80+
81+
$propertyReflection = $classReflection->getNativeProperty($propertyNode->getName());
82+
if ($propertyReflection->isVirtual()->yes()) {
83+
continue;
84+
}
85+
86+
$errors[] = RuleErrorBuilder::message(sprintf(
87+
'Get hook for non-virtual property %s::$%s does not read its value.',
88+
$classReflection->getDisplayName(),
89+
$propertyNode->getName(),
90+
))
91+
->line($this->getGetHookLine($propertyNode))
92+
->identifier('propertyGetHook.noRead')
93+
->build();
94+
}
95+
96+
return $errors;
97+
}
98+
99+
private function getGetHookLine(ClassPropertyNode $propertyNode): int
100+
{
101+
$getHook = null;
102+
foreach ($propertyNode->getHooks() as $hook) {
103+
if ($hook->name->toLowerString() !== 'get') {
104+
continue;
105+
}
106+
107+
$getHook = $hook;
108+
break;
109+
}
110+
111+
if ($getHook === null) {
112+
return $propertyNode->getStartLine();
113+
}
114+
115+
return $getHook->getStartLine();
116+
}
117+
118+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<GetNonVirtualPropertyHookReadRule>
11+
*/
12+
class GetNonVirtualPropertyHookReadRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new GetNonVirtualPropertyHookReadRule();
18+
}
19+
20+
public function testRule(): void
21+
{
22+
if (PHP_VERSION_ID < 80400) {
23+
$this->markTestSkipped('Test requires PHP 8.4.');
24+
}
25+
26+
$this->analyse([__DIR__ . '/data/get-non-virtual-property-hook-read.php'], [
27+
[
28+
'Get hook for non-virtual property GetNonVirtualPropertyHookRead\Foo::$k does not read its value.',
29+
24,
30+
],
31+
[
32+
'Get hook for non-virtual property GetNonVirtualPropertyHookRead\Foo::$l does not read its value.',
33+
30,
34+
],
35+
]);
36+
}
37+
38+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php // lint >= 8.4
2+
3+
namespace GetNonVirtualPropertyHookRead;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
// backed, read and written
10+
get => $this->i + 1;
11+
set => $this->i + $value;
12+
}
13+
14+
public int $j {
15+
// virtual
16+
get => 1;
17+
set {
18+
$this->a = $value;
19+
}
20+
}
21+
22+
public int $k {
23+
// backed, not read
24+
get => 1;
25+
set => $value + 1;
26+
}
27+
28+
public int $l {
29+
// backed, not read, long get
30+
get {
31+
return 1;
32+
}
33+
set => $value + 1;
34+
}
35+
36+
public int $m {
37+
// it is okay to only read it sometimes
38+
get {
39+
if (rand(0, 1)) {
40+
return 1;
41+
}
42+
43+
return $this->m;
44+
}
45+
set => $value + 1;
46+
}
47+
48+
}

0 commit comments

Comments
 (0)