Skip to content

Commit d8f5be7

Browse files
phpstan-botclaude
andauthored
Fix #12253: assign.readOnlyProperty (#5061)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f7758f commit d8f5be7

File tree

3 files changed

+147
-5
lines changed

3 files changed

+147
-5
lines changed

src/Node/ClassPropertiesNode.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,9 @@ public function getUninitializedProperties(
166166
$initializedInConstructor = array_diff_key($uninitializedProperties, $this->collectUninitializedProperties([$classReflection->getConstructor()->getName()], $uninitializedProperties));
167167
}
168168

169-
$methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors, $initializedInConstructor);
169+
$methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors, $initializedInConstructor, $constructors);
170170
$prematureAccess = [];
171171
$additionalAssigns = [];
172-
173172
foreach ($this->getPropertyUsages() as $usage) {
174173
$fetch = $usage->getFetch();
175174
if (!$fetch instanceof PropertyFetch) {
@@ -211,7 +210,10 @@ public function getUninitializedProperties(
211210

212211
if ($usage instanceof PropertyWrite) {
213212
if (array_key_exists($propertyName, $initializedPropertiesMap)) {
214-
$hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
213+
$hasInitialization = $initializedPropertiesMap[$propertyName];
214+
if (in_array($function->getName(), $constructors, true)) {
215+
$hasInitialization = $hasInitialization->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
216+
}
215217
if (
216218
!$hasInitialization->no()
217219
&& !$usage->isPromotedPropertyWrite()
@@ -318,6 +320,7 @@ private function collectUninitializedProperties(array $constructors, array $unin
318320
* @param array<string, TrinaryLogic> $initialInitializedProperties
319321
* @param array<string, array<string, TrinaryLogic>> $initializedProperties
320322
* @param array<string, ClassPropertyNode> $initializedInConstructorProperties
323+
* @param string[] $originalConstructors
321324
*
322325
* @return array<string, array<string, TrinaryLogic>>
323326
*/
@@ -327,6 +330,7 @@ private function getMethodsCalledFromConstructor(
327330
array $initializedProperties,
328331
array $methods,
329332
array $initializedInConstructorProperties,
333+
array $originalConstructors,
330334
): array
331335
{
332336
$originalMap = $initializedProperties;
@@ -363,7 +367,7 @@ private function getMethodsCalledFromConstructor(
363367
continue;
364368
}
365369

366-
if ($inMethod->getName() !== '__construct') {
370+
if ($inMethod->getName() !== '__construct' && in_array($inMethod->getName(), $originalConstructors, true)) {
367371
foreach (array_keys($initializedInConstructorProperties) as $propertyName) {
368372
$initializedProperties[$inMethod->getName()][$propertyName] = TrinaryLogic::createYes();
369373
}
@@ -391,7 +395,7 @@ private function getMethodsCalledFromConstructor(
391395
return $initializedProperties;
392396
}
393397

394-
return $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $methods, $initializedInConstructorProperties);
398+
return $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $methods, $initializedInConstructorProperties, $originalConstructors);
395399
}
396400

397401
/**

tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ protected function getRule(): Rule
2626
'Bug10523\\Controller::init',
2727
'Bug10523\\MultipleWrites::init',
2828
'Bug10523\\SingleWriteInConstructorCalledMethod::init',
29+
'Bug12253\\PayloadWithAdditionalConstructor::setUp',
2930
],
3031
),
3132
);
@@ -342,4 +343,10 @@ public function testBug11828(): void
342343
$this->analyse([__DIR__ . '/data/bug-11828.php'], []);
343344
}
344345

346+
#[RequiresPhp('>= 8.4')]
347+
public function testBug12253(): void
348+
{
349+
$this->analyse([__DIR__ . '/data/bug-12253.php'], []);
350+
}
351+
345352
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php // lint >= 8.4
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12253;
6+
7+
use stdClass;
8+
9+
class Payload
10+
{
11+
/** @var array<array<string, mixed>> */
12+
private(set) readonly array $validation;
13+
14+
/** @var array<string, string> */
15+
private array $ids = [];
16+
17+
public function __construct(private readonly stdClass $payload)
18+
{
19+
$this->parseValidation();
20+
}
21+
22+
private function parseValidation(): void
23+
{
24+
$validations = [];
25+
26+
foreach ($this->payload->validation as $key => $validation) {
27+
$validations[] = [
28+
'id' => $key,
29+
'field_id' => $this->ids[$validation->field_id],
30+
'rule' => $validation->rule,
31+
'value' => $this->validationValue($validation->value),
32+
'message' => $validation->message,
33+
];
34+
}
35+
36+
$this->validation = $validations;
37+
}
38+
39+
private function validationValue(mixed $value): mixed
40+
{
41+
if (is_null($value)) {
42+
return null;
43+
}
44+
45+
return $this->ids[$value] ?? $value;
46+
}
47+
}
48+
49+
class PayloadWithAdditionalConstructor
50+
{
51+
/** @var array<array<string, mixed>> */
52+
private(set) readonly array $validation;
53+
54+
/** @var array<string, string> */
55+
private array $ids = [];
56+
57+
public function __construct(private readonly stdClass $payload)
58+
{
59+
}
60+
61+
public function setUp(): void
62+
{
63+
$this->parseValidation();
64+
}
65+
66+
private function parseValidation(): void
67+
{
68+
$validations = [];
69+
70+
foreach ($this->payload->validation as $key => $validation) {
71+
$validations[] = [
72+
'id' => $key,
73+
'field_id' => $this->ids[$validation->field_id],
74+
'rule' => $validation->rule,
75+
'value' => $this->validationValue($validation->value),
76+
'message' => $validation->message,
77+
];
78+
}
79+
80+
$this->validation = $validations;
81+
}
82+
83+
private function validationValue(mixed $value): mixed
84+
{
85+
if (is_null($value)) {
86+
return null;
87+
}
88+
89+
return $this->ids[$value] ?? $value;
90+
}
91+
}
92+
93+
class PayloadWithoutAsymmetricVisibility
94+
{
95+
/** @var array<array<string, mixed>> */
96+
private readonly array $validation;
97+
98+
/** @var array<string, string> */
99+
private array $ids = [];
100+
101+
public function __construct(private readonly stdClass $payload)
102+
{
103+
$this->parseValidation();
104+
}
105+
106+
private function parseValidation(): void
107+
{
108+
$validations = [];
109+
110+
foreach ($this->payload->validation as $key => $validation) {
111+
$validations[] = [
112+
'id' => $key,
113+
'field_id' => $this->ids[$validation->field_id],
114+
'rule' => $validation->rule,
115+
'value' => $this->validationValue($validation->value),
116+
'message' => $validation->message,
117+
];
118+
}
119+
120+
$this->validation = $validations;
121+
}
122+
123+
private function validationValue(mixed $value): mixed
124+
{
125+
if (is_null($value)) {
126+
return null;
127+
}
128+
129+
return $this->ids[$value] ?? $value;
130+
}
131+
}

0 commit comments

Comments
 (0)