Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/Analyser/IssetabilityDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public function resolve(MutatingScope $scope, bool $useNativeTypes, Expr $expr):
$propertyReflection = $reflectionResolver($scope);
if ($propertyReflection === null) {
return new IssetabilityResolution(
IssetabilityLinkInfo::property(null, $propertyFetch, false, false, TrinaryLogic::createNo(), new NeverType(), new NeverType(), false, false, false, false, false, false, false, false),
IssetabilityLinkInfo::property(null, $propertyFetch, false, false, TrinaryLogic::createNo(), new NeverType(), new NeverType(), false, false, false, false, false, false, false),
$inner,
);
}
Expand All @@ -148,7 +148,6 @@ public function resolve(MutatingScope $scope, bool $useNativeTypes, Expr $expr):
$propertyReflection->getWritableType(),
$hasNativeType ? $propertyReflection->getNativeType() : new NeverType(),
$scope->hasExpressionType($propertyFetch)->yes(),
isset($scope->getConditionalExpressions()[$scope->getNodeKey($propertyFetch)]),
$initializedThisProperty,
$nativeReflection !== null,
$nativeReflection !== null && $nativeReflection->isPromoted(),
Expand Down
15 changes: 0 additions & 15 deletions src/Analyser/IssetabilityLinkInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ private function __construct(
private ?TrinaryLogic $isVirtual = null,
private ?Type $nativeType = null,
private bool $hasExpressionTypeOfFetch = false,
private bool $hasConditionalExpressionsOfFetch = false,
private bool $initializedThisProperty = false,
private bool $nativeReflectionExists = false,
private bool $nativeIsPromoted = false,
Expand Down Expand Up @@ -80,7 +79,6 @@ public static function property(
Type $writableType,
Type $nativeType,
bool $hasExpressionTypeOfFetch,
bool $hasConditionalExpressionsOfFetch,
bool $initializedThisProperty,
bool $nativeReflectionExists,
bool $nativeIsPromoted,
Expand All @@ -99,7 +97,6 @@ public static function property(
isVirtual: $isVirtual,
nativeType: $nativeType,
hasExpressionTypeOfFetch: $hasExpressionTypeOfFetch,
hasConditionalExpressionsOfFetch: $hasConditionalExpressionsOfFetch,
initializedThisProperty: $initializedThisProperty,
nativeReflectionExists: $nativeReflectionExists,
nativeIsPromoted: $nativeIsPromoted,
Expand Down Expand Up @@ -248,18 +245,6 @@ public function hasExpressionTypeOfFetch(): bool
return $this->hasExpressionTypeOfFetch;
}

/**
* Whether the scope holds conditional-expression entries about the fetch.
* Such entries exist only when the fetch was narrowed in an evaluated
* condition - and evaluating a condition READS the fetch, which would have
* thrown on an uninitialized typed property. A typed-property read
* witnesses initialization.
*/
public function hasConditionalExpressionsOfFetch(): bool
{
return $this->hasConditionalExpressionsOfFetch;
}

public function isInitializedThisProperty(): bool
{
return $this->initializedThisProperty;
Expand Down
1 change: 0 additions & 1 deletion src/Analyser/IssetabilityResolution.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ public function isSet(callable $typeCallback, ?bool $result = null): ?bool
$link->hasNativeType()
&& !$link->isVirtual()->yes()
&& !$link->hasExpressionTypeOfFetch()
&& !$link->hasConditionalExpressionsOfFetch()
&& !$link->nativeHasDefaultValue()
&& (!$link->nativeReflectionExists() || !$link->nativeIsPromoted() || (!$link->nativeIsReadOnly() && !$link->nativeIsHooked()))
) {
Expand Down
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -597,4 +597,18 @@ public function testNullCoalesceAssignRightSideScope(): void
]);
}

public function testBug15046(): void
{
$this->analyse([__DIR__ . '/data/bug-15046.php'], [
[
'Property Bug15046\\BothBranches::$answer on left side of ?? is not nullable nor uninitialized.',
140,
],
[
'Property Bug15046\\ConditionMetAgain::$answer (int) on left side of ?? is not nullable.',
157,
],
]);
}

}
161 changes: 161 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-15046.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php declare(strict_types = 1);

namespace Bug15046;

class Example {

protected int $answer;

public function echo_answer(?int $my_answer = null): void {
// If the following conditional property initialisation is removed, the issue disappears.
if ($my_answer !== null)
$this->answer = $my_answer;
// At this point, $this->answer may still be uninitialised, so that `??` is not unnecessary
echo "The answer is ", self::format_answer($this->answer ?? null), ".\n";
}

private static function format_answer(?int $the_answer): string {
return (string) ($the_answer ?? 'unknown');
}

}

class StaticProperty
{

protected static int $answer;

public static function echoAnswer(?int $myAnswer = null): void
{
if ($myAnswer !== null) {
self::$answer = $myAnswer;
}

echo (self::$answer ?? null);
}

}

class BooleanCondition
{

protected int $answer;

public function echoAnswer(bool $cond): void
{
if ($cond) {
$this->answer = 1;
}

echo ($this->answer ?? null);
}

}

class CoalesceAssign
{

protected int $answer;

public function echoAnswer(?int $myAnswer = null): void
{
if ($myAnswer !== null) {
$this->answer = $myAnswer;
}

$this->answer ??= null;
}

}

class Ternary
{

protected int $answer;

public function echoAnswer(?int $myAnswer = null): void
{
$myAnswer !== null ? ($this->answer = $myAnswer) : null;

echo ($this->answer ?? null);
}

}

class SwitchStatement
{

protected int $answer;

public function echoAnswer(int $myAnswer): void
{
switch ($myAnswer) {
case 1:
$this->answer = 1;
break;
}

echo ($this->answer ?? null);
}

}

class Inner
{

public int $deep;

}

class NestedChain
{

protected Inner $inner;

public function echoAnswer(?int $myAnswer = null): void
{
$this->inner = new Inner();
if ($myAnswer !== null) {
$this->inner->deep = $myAnswer;
}

echo ($this->inner->deep ?? null);
}

}

class BothBranches
{

protected int $answer;

public function echoAnswer(bool $cond): void
{
if ($cond) {
$this->answer = 1;
} else {
$this->answer = 2;
}

echo ($this->answer ?? null);
}

}

class ConditionMetAgain
{

protected int $answer;

public function echoAnswer(?int $myAnswer = null): void
{
if ($myAnswer !== null) {
$this->answer = $myAnswer;
}

if ($myAnswer !== null) {
echo ($this->answer ?? null);
}
}

}
Loading