Skip to content

Commit 86ff593

Browse files
committed
Implement a logic handling Property Hooks on PHP 8.4 or above in interfaces
1 parent c0bfae6 commit 86ff593

File tree

9 files changed

+192
-9
lines changed

9 files changed

+192
-9
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ lint:
7676
--exclude tests/PHPStan/Rules/Classes/data/extends-readonly-class.php \
7777
--exclude tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php \
7878
--exclude tests/PHPStan/Rules/Classes/data/bug-11592.php \
79+
--exclude tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php \
80+
--exclude tests/PHPStan/Rules/Properties/data/property-hooks-in-interface.php \
81+
--exclude tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php \
7982
src tests
8083

8184
cs:

build/collision-detector.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"../tests/PHPStan/Rules/Names/data/no-namespace.php",
1212
"../tests/notAutoloaded",
1313
"../tests/PHPStan/Rules/Functions/data/define-bug-3349.php",
14-
"../tests/PHPStan/Levels/data/stubs/function.php"
14+
"../tests/PHPStan/Levels/data/stubs/function.php",
15+
"../tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php",
16+
"../tests/PHPStan/Rules/Properties/data/property-hooks-in-interface.php",
17+
"../tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php"
1518
]
1619
}

src/Node/ClassPropertyNode.php

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

145+
public function hasHooks(): bool
146+
{
147+
return $this->getHooks() !== [];
148+
}
149+
150+
/**
151+
* @return Node\PropertyHook[]
152+
*/
153+
public function getHooks(): array
154+
{
155+
return $this->originalNode->hooks;
156+
}
157+
145158
}

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ public function supportsPregCaptureOnlyNamedGroups(): bool
347347
return $this->versionId >= 80200;
348348
}
349349

350+
public function supportsPropertyHooks(): bool
351+
{
352+
return $this->versionId >= 80400;
353+
}
354+
350355
public function hasDateTimeExceptions(): bool
351356
{
352357
return $this->versionId >= 80300;

src/Rules/Properties/PropertiesInInterfaceRule.php

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

@@ -14,6 +15,10 @@
1415
final class PropertiesInInterfaceRule implements Rule
1516
{
1617

18+
public function __construct(private PhpVersion $phpVersion)
19+
{
20+
}
21+
1722
public function getNodeType(): string
1823
{
1924
return ClassPropertyNode::class;
@@ -25,12 +30,54 @@ public function processNode(Node $node, Scope $scope): array
2530
return [];
2631
}
2732

28-
return [
29-
RuleErrorBuilder::message('Interfaces may not include properties.')
30-
->nonIgnorable()
31-
->identifier('property.inInterface')
32-
->build(),
33-
];
33+
if (!$this->phpVersion->supportsPropertyHooks()) {
34+
return [
35+
RuleErrorBuilder::message('Interfaces may not include properties.')
36+
->nonIgnorable()
37+
->identifier('property.inInterface')
38+
->build(),
39+
];
40+
}
41+
42+
if (!$node->hasHooks()) {
43+
return [
44+
RuleErrorBuilder::message('Interfaces may only include hooked properties.')
45+
->nonIgnorable()
46+
->identifier('property.nonHookedInInterface')
47+
->build(),
48+
];
49+
}
50+
51+
if (!$node->isPublic()) {
52+
return [
53+
RuleErrorBuilder::message('Interfaces may not include non-public properties.')
54+
->nonIgnorable()
55+
->identifier('property.nonPublicInInterface')
56+
->build(),
57+
];
58+
}
59+
60+
if ($this->hasAnyHookBody($node)) {
61+
return [
62+
RuleErrorBuilder::message('Interfaces may not include property hooks with bodies.')
63+
->nonIgnorable()
64+
->identifier('property.hookBodyInInterface')
65+
->build(),
66+
];
67+
}
68+
69+
return [];
70+
}
71+
72+
private function hasAnyHookBody(ClassPropertyNode $node): bool
73+
{
74+
foreach ($node->getHooks() as $hook) {
75+
if ($hook->body !== null) {
76+
return true;
77+
}
78+
}
79+
80+
return false;
3481
}
3582

3683
}

tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@
22

33
namespace PHPStan\Rules\Properties;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Testing\RuleTestCase;
8+
use const PHP_VERSION_ID;
79

810
/**
911
* @extends RuleTestCase<PropertiesInInterfaceRule>
1012
*/
1113
class PropertiesInInterfaceRuleTest extends RuleTestCase
1214
{
1315

16+
private int $phpVersion = PHP_VERSION_ID;
17+
1418
protected function getRule(): Rule
1519
{
16-
return new PropertiesInInterfaceRule();
20+
return new PropertiesInInterfaceRule(new PhpVersion($this->phpVersion));
1721
}
1822

19-
public function testRule(): void
23+
public function testPhp83AndPropertiesInInterface(): void
2024
{
25+
$this->phpVersion = 80300;
26+
2127
$this->analyse([__DIR__ . '/data/properties-in-interface.php'], [
2228
[
2329
'Interfaces may not include properties.',
@@ -30,4 +36,68 @@ public function testRule(): void
3036
]);
3137
}
3238

39+
public function testPhp83AndPropertyHooksInInterface(): void
40+
{
41+
$this->phpVersion = 80300;
42+
43+
$this->analyse([__DIR__ . '/data/property-hooks-in-interface.php'], [
44+
[
45+
'Interfaces may not include properties.',
46+
7,
47+
],
48+
[
49+
'Interfaces may not include properties.',
50+
9,
51+
],
52+
]);
53+
}
54+
55+
public function testPhp84AndPropertiesInInterface(): void
56+
{
57+
$this->phpVersion = 80400;
58+
59+
$this->analyse([__DIR__ . '/data/properties-in-interface.php'], [
60+
[
61+
'Interfaces may only include hooked properties.',
62+
7,
63+
],
64+
[
65+
'Interfaces may only include hooked properties.',
66+
9,
67+
],
68+
]);
69+
}
70+
71+
public function testPhp84AndNonPublicPropertyHooksInInterface(): void
72+
{
73+
$this->phpVersion = 80400;
74+
75+
$this->analyse([__DIR__ . '/data/property-hooks-visibility-in-interface.php'], [
76+
[
77+
'Interfaces may not include non-public properties.',
78+
7,
79+
],
80+
[
81+
'Interfaces may not include non-public properties.',
82+
9,
83+
],
84+
]);
85+
}
86+
87+
public function testPhp84AndPropertyHooksWithBodiesInInterface(): void
88+
{
89+
$this->phpVersion = 80400;
90+
91+
$this->analyse([__DIR__ . '/data/property-hooks-bodies-in-interface.php'], [
92+
[
93+
'Interfaces may not include property hooks with bodies.',
94+
7,
95+
],
96+
[
97+
'Interfaces may not include property hooks with bodies.',
98+
13,
99+
],
100+
]);
101+
}
102+
33103
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PropertyHooksBodiesInInterface;
4+
5+
interface HelloWorld
6+
{
7+
public string $firstName {
8+
get {
9+
return 'Foo';
10+
}
11+
}
12+
13+
public string $lastName {
14+
set {
15+
echo 'I will eventually be set to ' . $value;
16+
}
17+
}
18+
19+
public string $middleName { get; set; }
20+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PropertyHooksInInterface;
4+
5+
interface HelloWorld
6+
{
7+
public string $firstName { get; }
8+
9+
public string $lastName { get; set; }
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PropertyHooksVisibilityInInterface;
4+
5+
interface HelloWorld
6+
{
7+
private string $firstName { get; }
8+
9+
protected string $lastName { get; set; }
10+
11+
public string $fullName { get; set; }
12+
}

0 commit comments

Comments
 (0)