diff --git a/src/Rules/Variables/CoalesceRule.php b/src/Rules/Variables/CoalesceRule.php new file mode 100644 index 00000000000..13f75b883d4 --- /dev/null +++ b/src/Rules/Variables/CoalesceRule.php @@ -0,0 +1,74 @@ + + */ +class CoalesceRule implements \PHPStan\Rules\Rule +{ + + public function getNodeType(): string + { + return Node\Expr\BinaryOp\Coalesce::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $error = $this->canBeCoalesced($node->left, $scope); + + if ($error === null) { + return []; + } + + return [$error]; + } + + private function canBeCoalesced(Node $node, Scope $scope): ?RuleError + { + if ($node instanceof Node\Expr\Variable && is_string($node->name)) { + + $hasVariable = $scope->hasVariableType($node->name); + + if ($hasVariable->no()) { + return RuleErrorBuilder::message( + sprintf('Coalesce of undefined variable $%s.', $node->name) + )->line($node->getLine())->build(); + } + + $variableType = $scope->getVariableType($node->name); + + if ($variableType->isSuperTypeOf(new NullType())->no()) { + return RuleErrorBuilder::message( + sprintf('Coalesce of variable $%s, which cannot be null.', $node->name) + )->line($node->getLine())->build(); + } + + } elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { + $type = $scope->getType($node->var); + $dimType = $scope->getType($node->dim); + + if ($type->isOffsetAccessible()->no() || $type->hasOffsetValueType($dimType)->no()) { + return RuleErrorBuilder::message( + sprintf( + 'Coalesce of invalid offset %s on %s.', + $dimType->describe(VerbosityLevel::value()), + $type->describe(VerbosityLevel::value()) + ) + )->line($node->getLine())->build(); + } + + return $this->canBeCoalesced($node->var, $scope); + } + + return null; + } + +} diff --git a/tests/PHPStan/Rules/Variables/CoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/CoalesceRuleTest.php new file mode 100644 index 00000000000..042098eec57 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/CoalesceRuleTest.php @@ -0,0 +1,39 @@ + + */ +class CoalesceRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + return new CoalesceRule(); + } + + public function testUnsetRule(): void + { + require_once __DIR__ . '/data/coalesce.php'; + $this->analyse([__DIR__ . '/data/coalesce.php'], [ + [ + 'Coalesce of variable $scalar, which cannot be null.', + 7, + ], + [ + 'Coalesce of invalid offset \'string\' on array(1, 2, 3).', + 11, + ], + [ + 'Coalesce of invalid offset \'string\' on array(array(1), array(2), array(3)).', + 15, + ], + [ + 'Coalesce of undefined variable $doesNotExist.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/coalesce.php b/tests/PHPStan/Rules/Variables/data/coalesce.php new file mode 100644 index 00000000000..e20e22456ac --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/coalesce.php @@ -0,0 +1,19 @@ +