diff --git a/src/Analyser/IssetabilityDescriptor.php b/src/Analyser/IssetabilityDescriptor.php index 3ac913dc4a..d31bf4534c 100644 --- a/src/Analyser/IssetabilityDescriptor.php +++ b/src/Analyser/IssetabilityDescriptor.php @@ -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, ); } @@ -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(), diff --git a/src/Analyser/IssetabilityLinkInfo.php b/src/Analyser/IssetabilityLinkInfo.php index a59416d6b9..a347a8039f 100644 --- a/src/Analyser/IssetabilityLinkInfo.php +++ b/src/Analyser/IssetabilityLinkInfo.php @@ -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, @@ -80,7 +79,6 @@ public static function property( Type $writableType, Type $nativeType, bool $hasExpressionTypeOfFetch, - bool $hasConditionalExpressionsOfFetch, bool $initializedThisProperty, bool $nativeReflectionExists, bool $nativeIsPromoted, @@ -99,7 +97,6 @@ public static function property( isVirtual: $isVirtual, nativeType: $nativeType, hasExpressionTypeOfFetch: $hasExpressionTypeOfFetch, - hasConditionalExpressionsOfFetch: $hasConditionalExpressionsOfFetch, initializedThisProperty: $initializedThisProperty, nativeReflectionExists: $nativeReflectionExists, nativeIsPromoted: $nativeIsPromoted, @@ -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; diff --git a/src/Analyser/IssetabilityResolution.php b/src/Analyser/IssetabilityResolution.php index 9695f977ec..cd96bf1913 100644 --- a/src/Analyser/IssetabilityResolution.php +++ b/src/Analyser/IssetabilityResolution.php @@ -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())) ) { diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 4c39265508..1635fe7170 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-15046.php b/tests/PHPStan/Rules/Variables/data/bug-15046.php new file mode 100644 index 0000000000..52abd5ad92 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-15046.php @@ -0,0 +1,161 @@ +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); + } + } + +}