Skip to content

Commit 14044bc

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix phpstan/phpstan#14063: Readonly property modification through clone() not reported outside allowed scope
- Fixed PhpPropertyReflection::isProtectedSet() to handle readonly class promoted properties - BetterReflection's computeModifiers misses implicit protected(set) when readonly comes from the class rather than the property node - Added regression test in AccessPropertiesInAssignRuleTest and ReadOnlyPropertyAssignRuleTest
1 parent 06ea1e1 commit 14044bc

File tree

4 files changed

+113
-1
lines changed

4 files changed

+113
-1
lines changed

src/Reflection/Php/PhpPropertyReflection.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,24 @@ public function getHook(string $hookType): ExtendedMethodReflection
312312

313313
public function isProtectedSet(): bool
314314
{
315-
return $this->reflection->isProtectedSet();
315+
if ($this->reflection->isProtectedSet()) {
316+
return true;
317+
}
318+
319+
// Workaround: BetterReflection's computeModifiers only checks the property
320+
// node's readonly flag when adding implicit protected(set) for public readonly
321+
// properties. For promoted properties in readonly classes, the readonly flag
322+
// is on the class, not the property node, so the implicit protected(set) is missed.
323+
if (
324+
$this->declaringClass->isReadOnly()
325+
&& $this->reflection->isPublic()
326+
&& !$this->reflection->isPrivateSet()
327+
&& ($this->reflection->getModifiers() & ReflectionProperty::IS_READONLY_COMPATIBILITY) === 0
328+
) {
329+
return true;
330+
}
331+
332+
return false;
316333
}
317334

318335
public function isPrivateSet(): bool

tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,31 @@ public function testCloneWith(): void
234234
]);
235235
}
236236

237+
#[RequiresPhp('>= 8.5')]
238+
public function testBug14063(): void
239+
{
240+
$this->analyse([__DIR__ . '/data/bug-14063.php'], [
241+
[
242+
'Assign to protected(set) property Bug14063\Obj::$value.',
243+
51,
244+
],
245+
[
246+
'Assign to protected(set) property Bug14063\Bar::$value.',
247+
54,
248+
],
249+
[
250+
'Assign to protected(set) property Bug14063\Baz::$pub.',
251+
57,
252+
],
253+
[
254+
'Access to protected property Bug14063\Baz::$prot.',
255+
57,
256+
],
257+
[
258+
'Access to private property Bug14063\Baz::$priv.',
259+
57,
260+
],
261+
]);
262+
}
263+
237264
}

tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,10 @@ public function testCloneWith(): void
180180
$this->analyse([__DIR__ . '/data/readonly-property-assign-clone-with.php'], []);
181181
}
182182

183+
#[RequiresPhp('>= 8.5')]
184+
public function testBug14063(): void
185+
{
186+
$this->analyse([__DIR__ . '/data/bug-14063.php'], []);
187+
}
188+
183189
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php // lint >= 8.5
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14063;
6+
7+
final readonly class Obj
8+
{
9+
public function __construct(public string $value) {}
10+
11+
public function doFoo(): void
12+
{
13+
clone($this, ['value' => 'newVal']);
14+
}
15+
}
16+
17+
class Bar
18+
{
19+
public readonly string $value;
20+
21+
public function __construct(string $value)
22+
{
23+
$this->value = $value;
24+
}
25+
26+
public function doFoo(): void
27+
{
28+
clone($this, ['value' => 'newVal']);
29+
}
30+
}
31+
32+
readonly class Baz
33+
{
34+
public function __construct(
35+
public string $pub,
36+
protected string $prot,
37+
private string $priv,
38+
) {}
39+
40+
public function doFoo(): void
41+
{
42+
clone($this, [
43+
'pub' => 'newVal',
44+
'prot' => 'newVal',
45+
'priv' => 'newVal',
46+
]);
47+
}
48+
}
49+
50+
$obj = new Obj('val');
51+
$newObj = clone($obj, ['value' => 'newVal']);
52+
53+
$bar = new Bar('val');
54+
$newBar = clone($bar, ['value' => 'newVal']);
55+
56+
function (Baz $baz): void {
57+
clone($baz, [
58+
'pub' => 'newVal',
59+
'prot' => 'newVal',
60+
'priv' => 'newVal',
61+
]);
62+
};

0 commit comments

Comments
 (0)