Skip to content

Commit 166dcbe

Browse files
Prevent declaring hooked properties as readonly
Co-authored-by: Ondrej Mirtes <[email protected]>
1 parent efdd51e commit 166dcbe

8 files changed

+104
-6
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ lint:
8888
--exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php \
8989
--exclude tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php \
9090
--exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \
91+
--exclude tests/PHPStan/Rules/Properties/data/readonly-property-hooks.php \
92+
--exclude tests/PHPStan/Rules/Properties/data/readonly-property-hooks-in-interface.php \
9193
--exclude tests/PHPStan/Rules/Classes/data/bug-12281.php \
9294
--exclude tests/PHPStan/Rules/Traits/data/bug-12281.php \
9395
--exclude tests/PHPStan/Rules/Classes/data/invalid-hooked-properties.php \

src/Rules/Properties/PropertiesInInterfaceRule.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ public function processNode(Node $node, Scope $scope): array
5757
];
5858
}
5959

60+
if ($node->isReadOnly()) {
61+
return [
62+
RuleErrorBuilder::message('Interfaces cannot include readonly hooked properties.')
63+
->nonIgnorable()
64+
->identifier('property.readOnlyInInterface')
65+
->build(),
66+
];
67+
}
68+
6069
if ($this->hasAnyHookBody($node)) {
6170
return [
6271
RuleErrorBuilder::message('Interfaces cannot include property hooks with bodies.')

src/Rules/Properties/PropertyInClassRule.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,7 @@ public function processNode(Node $node, Scope $scope): array
7272
->build(),
7373
];
7474
}
75-
76-
return [];
77-
}
78-
79-
if (!$this->doAllHooksHaveBody($node)) {
75+
} elseif (!$this->doAllHooksHaveBody($node)) {
8076
return [
8177
RuleErrorBuilder::message('Non-abstract properties cannot include hooks without bodies.')
8278
->nonIgnorable()
@@ -85,6 +81,17 @@ public function processNode(Node $node, Scope $scope): array
8581
];
8682
}
8783

84+
if ($node->isReadOnly()) {
85+
if ($node->hasHooks()) {
86+
return [
87+
RuleErrorBuilder::message('Hooked properties cannot be readonly.')
88+
->nonIgnorable()
89+
->identifier('property.hookReadOnly')
90+
->build(),
91+
];
92+
}
93+
}
94+
8895
return [];
8996
}
9097

tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,26 @@ public function testPhp84AndPropertyHooksWithBodiesInInterface(): void
118118
]);
119119
}
120120

121+
public function testPhp84AndReadonlyPropertyHooksInInterface(): void
122+
{
123+
if (PHP_VERSION_ID < 80400) {
124+
$this->markTestSkipped('Test requires PHP 8.4 or later.');
125+
}
126+
127+
$this->analyse([__DIR__ . '/data/readonly-property-hooks-in-interface.php'], [
128+
[
129+
'Interfaces cannot include readonly hooked properties.',
130+
7,
131+
],
132+
[
133+
'Interfaces cannot include readonly hooked properties.',
134+
9,
135+
],
136+
[
137+
'Interfaces cannot include readonly hooked properties.',
138+
11,
139+
],
140+
]);
141+
}
142+
121143
}

tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,30 @@ public function testPhp84AndAbstractHookedPropertiesWithBodies(): void
151151
]);
152152
}
153153

154+
public function testPhp84AndReadonlyHookedProperties(): void
155+
{
156+
if (PHP_VERSION_ID < 80400) {
157+
$this->markTestSkipped('Test requires PHP 8.4 or later.');
158+
}
159+
160+
$this->analyse([__DIR__ . '/data/readonly-property-hooks.php'], [
161+
[
162+
'Hooked properties cannot be readonly.',
163+
7,
164+
],
165+
[
166+
'Hooked properties cannot be readonly.',
167+
12,
168+
],
169+
[
170+
'Hooked properties cannot be readonly.',
171+
14,
172+
],
173+
[
174+
'Hooked properties cannot be readonly.',
175+
19,
176+
],
177+
]);
178+
}
179+
154180
}

tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class AbstractPerson
1212
class PromotedHookedPropertyWithoutVisibility
1313
{
1414

15-
public function __construct(mixed $test { get; })
15+
public function __construct(public mixed $test { get; })
1616
{
1717

1818
}
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 ReadonlyPropertyHooksInInterface;
4+
5+
interface HelloWorld
6+
{
7+
public readonly string $firstName { get; set; }
8+
9+
public readonly string $middleName { get; }
10+
11+
public readonly string $lastName { set; }
12+
}
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 ReadonlyPropertyHooks;
4+
5+
class HelloWorld
6+
{
7+
public readonly string $firstName {
8+
get => $this->firstName;
9+
set => $this->firstName;
10+
}
11+
12+
public readonly string $middleName { get => $this->middleName; }
13+
14+
public readonly string $lastName { set => $this->lastName; }
15+
}
16+
17+
abstract class HiWorld
18+
{
19+
public abstract readonly string $firstName { get { return 'jake'; } set; }
20+
}

0 commit comments

Comments
 (0)