Skip to content

Commit 4e1889b

Browse files
committed
error on initialized non-nullable properties
1 parent 534b11b commit 4e1889b

File tree

5 files changed

+105
-1
lines changed

5 files changed

+105
-1
lines changed

src/Analyser/MutatingScope.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ private function rememberConstructorExpressions(array $currentExpressionTypes):
329329
if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) {
330330
continue;
331331
}
332-
} elseif (!$expr instanceof ConstFetch) {
332+
} elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) {
333333
continue;
334334
}
335335

src/Rules/IssetCheck.php

+19
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
use PhpParser\Node;
66
use PhpParser\Node\Expr;
77
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\Expr\PropertyInitializationExpr;
89
use PHPStan\Rules\Properties\PropertyDescriptor;
910
use PHPStan\Rules\Properties\PropertyReflectionFinder;
1011
use PHPStan\Type\NeverType;
1112
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
1214
use PHPStan\Type\VerbosityLevel;
1315
use function is_string;
1416
use function sprintf;
@@ -143,6 +145,23 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str
143145
}
144146

145147
if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) {
148+
if (
149+
$expr instanceof Node\Expr\PropertyFetch
150+
&& $expr->name instanceof Node\Identifier
151+
&& $expr->var instanceof Expr\Variable
152+
&& $expr->var->name === 'this'
153+
&& !TypeCombinator::containsNull($propertyReflection->getNativeType())
154+
&& $scope->hasExpressionType(new PropertyInitializationExpr($propertyReflection->getName()))->yes()
155+
) {
156+
return RuleErrorBuilder::message(
157+
sprintf(
158+
'%s cannot be null or uninitialized %s.',
159+
$this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr),
160+
$operatorDescription,
161+
),
162+
)->identifier(sprintf('%s.neverNullOrUninitialized', $identifier))->build();
163+
}
164+
146165
if (!$scope->hasExpressionType($expr)->yes()) {
147166
if ($expr instanceof Node\Expr\PropertyFetch) {
148167
return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier);

tests/PHPStan/Rules/Variables/IssetRuleTest.php

+17
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool
3232
return $this->treatPhpDocTypesAsCertain;
3333
}
3434

35+
public function shouldNarrowMethodScopeFromConstructor(): bool
36+
{
37+
return true;
38+
}
39+
3540
public function testRule(): void
3641
{
3742
$this->treatPhpDocTypesAsCertain = true;
@@ -480,4 +485,16 @@ public function testBug12771(): void
480485
$this->analyse([__DIR__ . '/data/bug-12771.php'], []);
481486
}
482487

488+
public function testIssetAfterRememberedConstructor(): void
489+
{
490+
$this->treatPhpDocTypesAsCertain = true;
491+
492+
$this->analyse([__DIR__ . '/data/isset-after-remembered-constructor.php'], [
493+
[
494+
'Property IssetOrCoalesceOnNonNullableInitializedProperty\User::$string cannot be null or uninitialized in isset().',
495+
36,
496+
],
497+
]);
498+
}
499+
483500
}

tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php

+17
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool
3232
return $this->treatPhpDocTypesAsCertain;
3333
}
3434

35+
public function shouldNarrowMethodScopeFromConstructor(): bool
36+
{
37+
return true;
38+
}
39+
3540
public function testCoalesceRule(): void
3641
{
3742
$this->treatPhpDocTypesAsCertain = true;
@@ -356,4 +361,16 @@ public function testBug12553(): void
356361
$this->analyse([__DIR__ . '/data/bug-12553.php'], []);
357362
}
358363

364+
public function testIssetAfterRememberedConstructor(): void
365+
{
366+
$this->treatPhpDocTypesAsCertain = true;
367+
368+
$this->analyse([__DIR__ . '/data/isset-after-remembered-constructor.php'], [
369+
[
370+
'Property IssetOrCoalesceOnNonNullableInitializedProperty\User::$string cannot be null or uninitialized on left side of ??.',
371+
48,
372+
],
373+
]);
374+
}
375+
359376
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php // lint >= 7.4
2+
3+
namespace IssetOrCoalesceOnNonNullableInitializedProperty;
4+
5+
use function PHPStan\debugScope;
6+
7+
class User
8+
{
9+
private ?string $nullableString;
10+
private string $maybeUninitializedString;
11+
private string $string;
12+
13+
private $untyped;
14+
15+
public function __construct(
16+
) {
17+
if (rand(0,1)) {
18+
$this->nullableString = 'hello';
19+
$this->string = 'world';
20+
$this->maybeUninitializedString = 'something';
21+
} else {
22+
$this->nullableString = null;
23+
$this->string = 'world 2';
24+
$this->untyped = 123;
25+
}
26+
}
27+
28+
public function doFoo(): void
29+
{
30+
if (isset($this->maybeUninitializedString)) {
31+
echo $this->maybeUninitializedString;
32+
}
33+
if (isset($this->nullableString)) {
34+
echo $this->nullableString;
35+
}
36+
if (isset($this->string)) {
37+
echo $this->string;
38+
}
39+
if (isset($this->untyped)) {
40+
echo $this->untyped;
41+
}
42+
}
43+
44+
public function doBar(): void
45+
{
46+
echo $this->maybeUninitializedString ?? 'default';
47+
echo $this->nullableString ?? 'default';
48+
echo $this->string ?? 'default';
49+
echo $this->untyped ?? 'default';
50+
}
51+
}

0 commit comments

Comments
 (0)