Skip to content

Commit 1b0c6a0

Browse files
committed
Properties set in the native constructor are initialized in additional constructors
1 parent 7031d82 commit 1b0c6a0

3 files changed

Lines changed: 74 additions & 7 deletions

File tree

src/Node/ClassPropertiesNode.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use PHPStan\TrinaryLogic;
2323
use PHPStan\Type\NeverType;
2424
use PHPStan\Type\TypeUtils;
25+
use function array_diff_key;
2526
use function array_key_exists;
2627
use function array_keys;
2728
use function in_array;
@@ -157,6 +158,11 @@ public function getUninitializedProperties(
157158
$prematureAccess = [];
158159
$additionalAssigns = [];
159160

161+
$initializedInConstructor = [];
162+
if ($classReflection->hasConstructor()) {
163+
$initializedInConstructor = array_diff_key($uninitializedProperties, $this->collectUninitializedProperties([$classReflection->getConstructor()->getName()], $uninitializedProperties));
164+
}
165+
160166
foreach ($this->getPropertyUsages() as $usage) {
161167
$fetch = $usage->getFetch();
162168
if (!$fetch instanceof PropertyFetch) {
@@ -213,6 +219,13 @@ public function getUninitializedProperties(
213219
}
214220
} elseif (array_key_exists($propertyName, $initializedPropertiesMap)) {
215221
$hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
222+
if (
223+
strtolower($function->getName()) !== '__construct'
224+
&& array_key_exists($propertyName, $initializedInConstructor)
225+
&& in_array($function->getName(), $constructors, true)
226+
) {
227+
continue;
228+
}
216229
if (!$hasInitialization->yes()) {
217230
$prematureAccess[] = [
218231
$propertyName,
@@ -224,7 +237,21 @@ public function getUninitializedProperties(
224237
}
225238
}
226239

227-
foreach (array_keys($methodsCalledFromConstructor) as $constructor) {
240+
return [
241+
$this->collectUninitializedProperties(array_keys($methodsCalledFromConstructor), $uninitializedProperties),
242+
$prematureAccess,
243+
$additionalAssigns,
244+
];
245+
}
246+
247+
/**
248+
* @param list<string> $constructors
249+
* @param array<string, ClassPropertyNode> $uninitializedProperties
250+
* @return array<string, ClassPropertyNode>
251+
*/
252+
private function collectUninitializedProperties(array $constructors, array $uninitializedProperties): array
253+
{
254+
foreach ($constructors as $constructor) {
228255
$lowerConstructorName = strtolower($constructor);
229256
if (!array_key_exists($lowerConstructorName, $this->returnStatementNodes)) {
230257
continue;
@@ -275,11 +302,7 @@ public function getUninitializedProperties(
275302
}
276303
}
277304

278-
return [
279-
$uninitializedProperties,
280-
$prematureAccess,
281-
$additionalAssigns,
282-
];
305+
return $uninitializedProperties;
283306
}
284307

285308
/**

tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ protected function getRule(): Rule
2222
[
2323
'UninitializedProperty\\TestCase::setUp',
2424
'Bug9619\\AdminPresenter::startup',
25+
'Bug9619\\AdminPresenter2::startup',
26+
'Bug9619\\AdminPresenter3::startup',
27+
'Bug9619\\AdminPresenter3::startup2',
2528
],
2629
),
2730
);
@@ -181,7 +184,12 @@ public function testEfabricaLatteBug(): void
181184

182185
public function testBug9619(): void
183186
{
184-
$this->analyse([__DIR__ . '/data/bug-9619.php'], []);
187+
$this->analyse([__DIR__ . '/data/bug-9619.php'], [
188+
[
189+
'Access to an uninitialized property Bug9619\AdminPresenter3::$user.',
190+
55,
191+
],
192+
]);
185193
}
186194

187195
}

tests/PHPStan/Rules/Properties/data/bug-9619.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,39 @@ public function startup()
2121
}
2222
}
2323
}
24+
25+
class AdminPresenter2
26+
{
27+
private User $user;
28+
29+
public function __construct(User $user)
30+
{
31+
$this->user = $user;
32+
}
33+
34+
public function startup()
35+
{
36+
// do not report uninitialized property - it's initialized for sure
37+
if (!$this->user->isLoggedIn()) {
38+
// do something
39+
}
40+
}
41+
}
42+
43+
class AdminPresenter3
44+
{
45+
private \stdClass $user;
46+
47+
public function startup()
48+
{
49+
$this->user = new \stdClass();
50+
}
51+
52+
public function startup2()
53+
{
54+
// we cannot be sure which additional constructor gets called first
55+
if (!$this->user->loggedIn) {
56+
// do something
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)