diff --git a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php index 00c35c3192b..bc6d601d31c 100644 --- a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php +++ b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php @@ -42,7 +42,7 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $originalNativeType = $originalScope->getNativeType($exprToSpecify); return new EnsuredNonNullabilityResult($scope, [ - new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty), + new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $hasExpressionType), ]); } return new EnsuredNonNullabilityResult($scope, []); @@ -55,16 +55,11 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina // To properly revert this, we must also save and restore the parent expression's type. if ($exprToSpecify instanceof Expr\ArrayDimFetch && $exprToSpecify->dim !== null) { $parentExpr = $exprToSpecify->var; - $parentCertainty = TrinaryLogic::createYes(); - $hasParentExpressionType = $originalScope->hasExpressionType($parentExpr); - if (!$hasParentExpressionType->no()) { - $parentCertainty = $hasParentExpressionType; - } $specifiedExpressions[] = new EnsuredNonNullabilityResultExpression( $parentExpr, $scope->getType($parentExpr), $scope->getNativeType($parentExpr), - $parentCertainty, + $originalScope->hasExpressionType($parentExpr), ); } @@ -104,6 +99,10 @@ public function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredN public function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope { foreach ($specifiedExpressions as $specifiedExpressionResult) { + if ($specifiedExpressionResult->getCertainty()->no()) { + $scope = $scope->invalidateExpression($specifiedExpressionResult->getExpression()); + continue; + } $scope = $scope->specifyExpressionType( $specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType(), diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index a7cd4cbc91e..8d4d321d108 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -253,6 +253,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/data/dio-functions.php'; } + yield __DIR__ . '/../Rules/Variables/data/bug-13921.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-14234.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php'; yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index 55cdc0c4e13..cc628575f79 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -371,4 +371,17 @@ public function testBug4004(): void $this->analyse([__DIR__ . '/data/bug-4004.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug10305(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-10305.php'], [ + [ + 'Result of || is always true.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10305.php b/tests/PHPStan/Rules/Comparison/data/bug-10305.php new file mode 100644 index 00000000000..2af4054cb68 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10305.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug10305; + +class HelloWorld +{ + public null|string $prop; + + public function isPropertySet(): bool { + return isset($this->prop) || null === $this->prop; + } +} diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 56b902212ae..7a92e200461 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -362,4 +362,14 @@ public function testBug14213(): void ]); } + public function testBug13921(): void + { + $this->analyse([__DIR__ . '/data/bug-13921.php'], [ + [ + 'Offset 0 on non-empty-list> on left side of ?? always exists and is not nullable.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-13921.php b/tests/PHPStan/Rules/Variables/data/bug-13921.php new file mode 100644 index 00000000000..91f86ee0db4 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13921.php @@ -0,0 +1,56 @@ +> $x */ +function foo(array $x): void { + var_dump($x[0]['bar'] ?? null); + assertType("list>", $x); + var_dump($x[0] ?? null); +} + +/** @param non-empty-list> $x */ +function nonEmptyFoo(array $x): void { + var_dump($x[0]['bar'] ?? null); + assertType("non-empty-list>", $x); + var_dump($x[0] ?? null); +} + +/** @param list> $x */ +function bar(array $x): void { + var_dump($x[0] ?? null); + assertType("list>", $x); + var_dump($x[0]['bar'] ?? null); +} + +/** @param list> $x */ +function baz(array $x): void { + var_dump($x[1] ?? null); + assertType("list>", $x); + var_dump($x[0]['bar'] ?? null); +} + +/** @param list> $x */ +function boo(array $x): void { + var_dump($x[0]['bar'] ?? null); + assertType("list>", $x); + var_dump($x[1] ?? null); +} + +function doBar(array $array) +{ + if (isset($array['foo'])) { + assertType("mixed~null", $array['foo']); + assertType("non-empty-array&hasOffsetValue('foo', mixed~null)", $array); + } +} + +/** @param list $x */ +function sooSimpleElement(array $x): void { + var_dump($x[0]['bar'] ?? null); + assertType("list", $x); + var_dump($x[0] ?? null); +}