diff --git a/conf/config.level1.neon b/conf/config.level1.neon index 769c4922cf3..54d25c8a43a 100644 --- a/conf/config.level1.neon +++ b/conf/config.level1.neon @@ -12,3 +12,4 @@ rules: - PHPStan\Rules\Constants\ConstantRule - PHPStan\Rules\Functions\UnusedClosureUsesRule - PHPStan\Rules\Variables\VariableCertaintyInIssetRule + - PHPStan\Rules\Variables\CoalesceRule diff --git a/src/Rules/Variables/CoalesceRule.php b/src/Rules/Variables/CoalesceRule.php index 13f75b883d4..7fc3368c9dc 100644 --- a/src/Rules/Variables/CoalesceRule.php +++ b/src/Rules/Variables/CoalesceRule.php @@ -45,7 +45,8 @@ private function canBeCoalesced(Node $node, Scope $scope): ?RuleError $variableType = $scope->getVariableType($node->name); - if ($variableType->isSuperTypeOf(new NullType())->no()) { + // For hasVariable->maybe a coalesce is valid + if ($hasVariable->yes() && $variableType->isSuperTypeOf(new NullType())->no()) { return RuleErrorBuilder::message( sprintf('Coalesce of variable $%s, which cannot be null.', $node->name) )->line($node->getLine())->build(); @@ -54,8 +55,9 @@ private function canBeCoalesced(Node $node, Scope $scope): ?RuleError } elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { $type = $scope->getType($node->var); $dimType = $scope->getType($node->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); - if ($type->isOffsetAccessible()->no() || $type->hasOffsetValueType($dimType)->no()) { + if ($type->isOffsetAccessible()->no() || $hasOffsetValue->no()) { return RuleErrorBuilder::message( sprintf( 'Coalesce of invalid offset %s on %s.', @@ -65,6 +67,20 @@ private function canBeCoalesced(Node $node, Scope $scope): ?RuleError )->line($node->getLine())->build(); } + if ($hasOffsetValue->maybe()) { + return null; + } + + if ($hasOffsetValue->yes() && $type->isSuperTypeOf(new NullType())->no()) { + return RuleErrorBuilder::message( + sprintf( + 'Coalesce of offset %s on %s, which cannot be null.', + $dimType->describe(VerbosityLevel::value()), + $type->describe(VerbosityLevel::value()) + ) + )->line($node->getLine())->build(); + } + return $this->canBeCoalesced($node->var, $scope); } diff --git a/tests/PHPStan/Rules/Variables/CoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/CoalesceRuleTest.php index 042098eec57..055ae483d7b 100644 --- a/tests/PHPStan/Rules/Variables/CoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/CoalesceRuleTest.php @@ -33,6 +33,10 @@ public function testUnsetRule(): void 'Coalesce of undefined variable $doesNotExist.', 17, ], + [ + 'Coalesce of offset \'dim\' on array(\'dim\' => 1), which cannot be null.', + 27, + ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/coalesce.php b/tests/PHPStan/Rules/Variables/data/coalesce.php index e20e22456ac..59b9cf6e2e6 100644 --- a/tests/PHPStan/Rules/Variables/data/coalesce.php +++ b/tests/PHPStan/Rules/Variables/data/coalesce.php @@ -16,4 +16,21 @@ function coalesce () { echo $doesNotExist ?? 0; + if(rand() > 0.5) { + $maybeVariable = 3; + } + + echo $maybeVariable ?? 0; + + $fixedDimArray = ['dim' => 1]; + + echo $fixedDimArray['dim'] ?? 0; +} + +/** + * @param array $array + */ +function coalesceStringOffset(array $array) +{ + echo $array['string'] ?? 0; }