Skip to content

Commit c33bb2b

Browse files
committed
Extract introduced Property Hooks-related functionalities into their own rules
1 parent c263971 commit c33bb2b

12 files changed

+249
-13
lines changed

conf/config.level0.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ rules:
9898
- PHPStan\Rules\Properties\PropertyAttributesRule
9999
- PHPStan\Rules\Properties\ReadOnlyPropertyRule
100100
- PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule
101+
- PHPStan\Rules\PropertyHooks\NonEmptyPropertyHookRule
102+
- PHPStan\Rules\PropertyHooks\NonPublicPropertyHookRule
101103
- PHPStan\Rules\Regexp\RegularExpressionPatternRule
102104
- PHPStan\Rules\Traits\ConflictingTraitConstantsRule
103105
- PHPStan\Rules\Traits\ConstantsInTraitsRule

src/Analyser/NodeScopeResolver.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,6 @@ private function processStmtNode(
899899
}
900900
$propStmt = clone $stmt;
901901
$propStmt->setAttributes($prop->getAttributes());
902-
rd($propStmt);
903902
$nodeCallback(
904903
new ClassPropertyNode(
905904
$propertyName,

src/Node/ClassPropertyNode.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,20 @@ public function getSubNodeNames(): array
142142
return [];
143143
}
144144

145+
public function isPropertyHook(): bool
146+
{
147+
return $this->originalNode->hooks !== [];
148+
}
149+
150+
public function areHooksBodiesEmpty(): bool
151+
{
152+
foreach ($this->originalNode->hooks as $hook) {
153+
if ($hook->body !== null) {
154+
return false;
155+
}
156+
}
157+
158+
return true;
159+
}
160+
145161
}

src/Php/PhpVersion.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,5 @@ public function substrReturnFalseInsteadOfEmptyString(): bool
379379
{
380380
return $this->versionId < 80000;
381381
}
382+
382383
}

src/Rules/Properties/PropertiesInInterfaceRule.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Node\ClassPropertyNode;
8-
use PHPStan\Php\PhpVersion;
98
use PHPStan\Rules\Rule;
109
use PHPStan\Rules\RuleErrorBuilder;
1110

@@ -15,10 +14,6 @@
1514
final class PropertiesInInterfaceRule implements Rule
1615
{
1716

18-
public function __construct (private PhpVersion $phpVersion)
19-
{
20-
}
21-
2217
public function getNodeType(): string
2318
{
2419
return ClassPropertyNode::class;
@@ -30,7 +25,11 @@ public function processNode(Node $node, Scope $scope): array
3025
return [];
3126
}
3227

33-
if ($node->isPublic() && $this->phpVersion->supportsPublicPropertiesInInterfaces()) {
28+
if (!$node instanceof ClassPropertyNode) {
29+
return [];
30+
}
31+
32+
if ($node->isPropertyHook()) {
3433
return [];
3534
}
3635

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\Rules\PropertyHooks;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\ClassPropertyNode;
8+
use PHPStan\Php\PhpVersion;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
12+
/**
13+
* @implements Rule<ClassPropertyNode>
14+
*/
15+
final class NonEmptyPropertyHookRule implements Rule
16+
{
17+
18+
public function __construct(private PhpVersion $phpVersion)
19+
{
20+
}
21+
22+
public function getNodeType(): string
23+
{
24+
return ClassPropertyNode::class;
25+
}
26+
27+
public function processNode(Node $node, Scope $scope): array
28+
{
29+
if (!$node->getClassReflection()->isInterface()) {
30+
return [];
31+
}
32+
33+
if (!$node instanceof ClassPropertyNode) {
34+
return [];
35+
}
36+
37+
if (!$this->phpVersion->supportsPublicPropertiesInInterfaces() || !$node->isPropertyHook()) {
38+
return [];
39+
}
40+
41+
if ($node->areHooksBodiesEmpty()) {
42+
return [];
43+
}
44+
45+
return [
46+
RuleErrorBuilder::message('Property hook must not be empty.')
47+
->nonIgnorable()
48+
->identifier('propertyHook.nonEmpty')
49+
->build(),
50+
];
51+
}
52+
53+
}
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\Rules\PropertyHooks;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\ClassPropertyNode;
8+
use PHPStan\Php\PhpVersion;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
12+
/**
13+
* @implements Rule<ClassPropertyNode>
14+
*/
15+
final class NonPublicPropertyHookRule implements Rule
16+
{
17+
18+
public function __construct(private PhpVersion $phpVersion)
19+
{
20+
}
21+
22+
public function getNodeType(): string
23+
{
24+
return ClassPropertyNode::class;
25+
}
26+
27+
public function processNode(Node $node, Scope $scope): array
28+
{
29+
if (!$node->getClassReflection()->isInterface()) {
30+
return [];
31+
}
32+
33+
if (!$node instanceof ClassPropertyNode) {
34+
return [];
35+
}
36+
37+
if (!$this->phpVersion->supportsPublicPropertiesInInterfaces() || !$node->isPropertyHook()) {
38+
return [];
39+
}
40+
41+
if ($node->isPublic()) {
42+
return [];
43+
}
44+
45+
return [
46+
RuleErrorBuilder::message('Property hook must be public.')
47+
->nonIgnorable()
48+
->identifier('propertyHook.nonPublic')
49+
->build(),
50+
];
51+
}
52+
53+
}
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\Properties;
4+
5+
use PHPStan\Php\PhpVersion;
6+
use PHPStan\Rules\PropertyHooks\NonEmptyPropertyHookRule;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
use const PHP_VERSION_ID;
10+
11+
/**
12+
* @extends RuleTestCase<NonEmptyPropertyHookRule>
13+
*/
14+
class NonEmptyPropertyHookRuleTest extends RuleTestCase
15+
{
16+
17+
protected function getRule(): Rule
18+
{
19+
return new NonEmptyPropertyHookRule(new PhpVersion(PHP_VERSION_ID));
20+
}
21+
22+
public function testRule(): void
23+
{
24+
if (PHP_VERSION_ID < 80400) {
25+
$this->markTestSkipped('This test requires at least PHP 8.4.');
26+
}
27+
28+
$this->analyse([__DIR__ . '/data/non-empty-property-hook.php'], [
29+
[
30+
'Property hook must not be empty.',
31+
7,
32+
],
33+
[
34+
'Property hook must not be empty.',
35+
14,
36+
],
37+
]);
38+
}
39+
40+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Php\PhpVersion;
6+
use PHPStan\Rules\PropertyHooks\NonEmptyPropertyHookRule;
7+
use PHPStan\Rules\PropertyHooks\NonPublicPropertyHookRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use const PHP_VERSION_ID;
11+
12+
/**
13+
* @extends RuleTestCase<NonEmptyPropertyHookRule>
14+
*/
15+
class NonPublicPropertyHookRuleTest extends RuleTestCase
16+
{
17+
18+
protected function getRule(): Rule
19+
{
20+
return new NonPublicPropertyHookRule(new PhpVersion(PHP_VERSION_ID));
21+
}
22+
23+
public function testRule(): void
24+
{
25+
if (PHP_VERSION_ID < 80400) {
26+
$this->markTestSkipped('This test requires at least PHP 8.4.');
27+
}
28+
29+
$this->analyse([__DIR__ . '/data/non-public-property-hook.php'], [
30+
[
31+
'Property hook must be public.',
32+
7,
33+
],
34+
[
35+
'Property hook must be public.',
36+
9,
37+
],
38+
]);
39+
}
40+
41+
}

tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@ protected function getRule(): Rule
2727

2828
public function testRule(): void
2929
{
30-
if ($this->phpVersion->supportsPublicPropertiesInInterfaces()) {
31-
$this->analyse([__DIR__ . '/data/properties-in-interface.php'], []);
32-
33-
return;
34-
}
35-
3630
$this->analyse([__DIR__ . '/data/properties-in-interface.php'], [
3731
[
3832
'Interfaces may not include properties.',

0 commit comments

Comments
 (0)