diff --git a/lib/PhpParser/ConstExprEvaluator.php b/lib/PhpParser/ConstExprEvaluator.php index ecc4001b53..c95f5802b1 100644 --- a/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/PhpParser/ConstExprEvaluator.php @@ -103,7 +103,7 @@ public function evaluateDirectly(Expr $expr) { } /** @return mixed */ - private function evaluate(Expr $expr) { + private function evaluate(Expr $expr, bool $issetContext = false) { if ($expr instanceof Scalar\Int_ || $expr instanceof Scalar\Float_ || $expr instanceof Scalar\String_ @@ -138,6 +138,10 @@ private function evaluate(Expr $expr) { } if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) { + if ($issetContext) { + // This needs to be special cased to respect BP_VAR_IS fetch semantics + return $this->evaluate($expr->var, true)[$this->evaluate($expr->dim)] ?? null; + } return $this->evaluate($expr->var)[$this->evaluate($expr->dim)]; } @@ -175,14 +179,6 @@ private function evaluateTernary(Expr\Ternary $expr) { /** @return mixed */ private function evaluateBinaryOp(Expr\BinaryOp $expr) { - if ($expr instanceof Expr\BinaryOp\Coalesce - && $expr->left instanceof Expr\ArrayDimFetch - ) { - // This needs to be special cased to respect BP_VAR_IS fetch semantics - return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)] - ?? $this->evaluate($expr->right); - } - // The evaluate() calls are repeated in each branch, because some of the operators are // short-circuiting and evaluating the RHS in advance may be illegal in that case $l = $expr->left; @@ -193,7 +189,7 @@ private function evaluateBinaryOp(Expr\BinaryOp $expr) { case '^': return $this->evaluate($l) ^ $this->evaluate($r); case '&&': return $this->evaluate($l) && $this->evaluate($r); case '||': return $this->evaluate($l) || $this->evaluate($r); - case '??': return $this->evaluate($l) ?? $this->evaluate($r); + case '??': return $this->evaluate($l, true) ?? $this->evaluate($r); case '.': return $this->evaluate($l) . $this->evaluate($r); case '/': return $this->evaluate($l) / $this->evaluate($r); case '==': return $this->evaluate($l) == $this->evaluate($r); diff --git a/test/PhpParser/ConstExprEvaluatorTest.php b/test/PhpParser/ConstExprEvaluatorTest.php index 7b0e8a0e77..a3ac1f433e 100644 --- a/test/PhpParser/ConstExprEvaluatorTest.php +++ b/test/PhpParser/ConstExprEvaluatorTest.php @@ -12,6 +12,7 @@ public function testEvaluate($exprString, $expected): void { $expr = $parser->parse('expr; $evaluator = new ConstExprEvaluator(); $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); + $this->assertSame($expected, $evaluator->evaluateSilently($expr)); } public static function provideTestEvaluate() { @@ -41,6 +42,14 @@ public static function provideTestEvaluate() { ['null ?? 42', 42], ['[0][0] ?? 42', 0], ['[][0] ?? 42', 42], + ['["a" => 1]["b"]["c"] ?? 2', 2], + ['null["a"]["b"] ?? 3', 3], + ['["a" => null]["a"]["b"] ?? 3', 3], + ['["a" => 1]["a"]["b"] ?? 2', 2], + ['[][0][1][2] ?? 42', 42], + ['["a" => ["b" => null]]["a"]["b"] ?? 2', 2], + ['["a" => ["b" => 0]]["a"]["b"] ?? (1 / 0)', 0], + ['[["foo"]][0][0][1] ?? 42', 'o'], ['0b11 & 0b10', 0b10], ['0b11 | 0b10', 0b11], ['0b11 ^ 0b10', 0b01], @@ -81,6 +90,19 @@ public function testEvaluateFails(): void { $evaluator->evaluateDirectly(new Expr\Variable('a')); } + public function testCoalesceEvaluatesOffsets(): void { + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $expr = $parser->parse('expr; + $evaluator = new ConstExprEvaluator(); + try { + $evaluator->evaluateSilently($expr); + $this->fail('Expected an exception from the array offset expression'); + } catch (ConstExprEvaluationException $e) { + $this->assertInstanceOf(\Error::class, $e->getPrevious()); + $this->assertSame('Modulo by zero', $e->getPrevious()->getMessage()); + } + } + public function testEvaluateFallbackMagicConst(): void { $evaluator = new ConstExprEvaluator(function (Expr $expr) { if ($expr instanceof Scalar\MagicConst\Line) {