rector/vendor/nikic/php-parser/lib/PhpParser/ConstExprEvaluator.php
Tomas Votruba 8d1aee03a3 Updated Rector to commit 560172463933f21b51b8ef306a068dc299f742e5
5601724639 [Dep] Temporary pin to use nikic/php-parser 4.18.0 (#5733)
2024-03-17 07:47:19 +00:00

236 lines
9.4 KiB
PHP

<?php
namespace PhpParser;
use function array_merge;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
/**
* Evaluates constant expressions.
*
* This evaluator is able to evaluate all constant expressions (as defined by PHP), which can be
* evaluated without further context. If a subexpression is not of this type, a user-provided
* fallback evaluator is invoked. To support all constant expressions that are also supported by
* PHP (and not already handled by this class), the fallback evaluator must be able to handle the
* following node types:
*
* * All Scalar\MagicConst\* nodes.
* * Expr\ConstFetch nodes. Only null/false/true are already handled by this class.
* * Expr\ClassConstFetch nodes.
*
* The fallback evaluator should throw ConstExprEvaluationException for nodes it cannot evaluate.
*
* The evaluation is dependent on runtime configuration in two respects: Firstly, floating
* point to string conversions are affected by the precision ini setting. Secondly, they are also
* affected by the LC_NUMERIC locale.
*/
class ConstExprEvaluator
{
private $fallbackEvaluator;
/**
* Create a constant expression evaluator.
*
* The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See
* class doc comment for more information.
*
* @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated
*/
public function __construct(callable $fallbackEvaluator = null)
{
$this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) {
throw new \PhpParser\ConstExprEvaluationException("Expression of type {$expr->getType()} cannot be evaluated");
};
}
/**
* Silently evaluates a constant expression into a PHP value.
*
* Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException.
* The original source of the exception is available through getPrevious().
*
* If some part of the expression cannot be evaluated, the fallback evaluator passed to the
* constructor will be invoked. By default, if no fallback is provided, an exception of type
* ConstExprEvaluationException is thrown.
*
* See class doc comment for caveats and limitations.
*
* @param Expr $expr Constant expression to evaluate
* @return mixed Result of evaluation
*
* @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred
*/
public function evaluateSilently(Expr $expr)
{
\set_error_handler(function ($num, $str, $file, $line) {
throw new \ErrorException($str, 0, $num, $file, $line);
});
try {
return $this->evaluate($expr);
} catch (\Throwable $e) {
if (!$e instanceof \PhpParser\ConstExprEvaluationException) {
$e = new \PhpParser\ConstExprEvaluationException("An error occurred during constant expression evaluation", 0, $e);
}
throw $e;
} finally {
\restore_error_handler();
}
}
/**
* Directly evaluates a constant expression into a PHP value.
*
* May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these
* into a ConstExprEvaluationException.
*
* If some part of the expression cannot be evaluated, the fallback evaluator passed to the
* constructor will be invoked. By default, if no fallback is provided, an exception of type
* ConstExprEvaluationException is thrown.
*
* See class doc comment for caveats and limitations.
*
* @param Expr $expr Constant expression to evaluate
* @return mixed Result of evaluation
*
* @throws ConstExprEvaluationException if the expression cannot be evaluated
*/
public function evaluateDirectly(Expr $expr)
{
return $this->evaluate($expr);
}
private function evaluate(Expr $expr)
{
if ($expr instanceof Scalar\LNumber || $expr instanceof Scalar\DNumber || $expr instanceof Scalar\String_) {
return $expr->value;
}
if ($expr instanceof Expr\Array_) {
return $this->evaluateArray($expr);
}
// Unary operators
if ($expr instanceof Expr\UnaryPlus) {
return +$this->evaluate($expr->expr);
}
if ($expr instanceof Expr\UnaryMinus) {
return -$this->evaluate($expr->expr);
}
if ($expr instanceof Expr\BooleanNot) {
return !$this->evaluate($expr->expr);
}
if ($expr instanceof Expr\BitwiseNot) {
return ~$this->evaluate($expr->expr);
}
if ($expr instanceof Expr\BinaryOp) {
return $this->evaluateBinaryOp($expr);
}
if ($expr instanceof Expr\Ternary) {
return $this->evaluateTernary($expr);
}
if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) {
return $this->evaluate($expr->var)[$this->evaluate($expr->dim)];
}
if ($expr instanceof Expr\ConstFetch) {
return $this->evaluateConstFetch($expr);
}
return ($this->fallbackEvaluator)($expr);
}
private function evaluateArray(Expr\Array_ $expr)
{
$array = [];
foreach ($expr->items as $item) {
if (null !== $item->key) {
$array[$this->evaluate($item->key)] = $this->evaluate($item->value);
} elseif ($item->unpack) {
$array = array_merge($array, $this->evaluate($item->value));
} else {
$array[] = $this->evaluate($item->value);
}
}
return $array;
}
private function evaluateTernary(Expr\Ternary $expr)
{
if (null === $expr->if) {
return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else);
}
return $this->evaluate($expr->cond) ? $this->evaluate($expr->if) : $this->evaluate($expr->else);
}
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;
$r = $expr->right;
switch ($expr->getOperatorSigil()) {
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) || $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) == $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 'and':
return $this->evaluate($l) and $this->evaluate($r);
case 'or':
return $this->evaluate($l) or $this->evaluate($r);
case 'xor':
return $this->evaluate($l) xor $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) != $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) << $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) <=> $this->evaluate($r);
}
throw new \Exception('Should not happen');
}
private function evaluateConstFetch(Expr\ConstFetch $expr)
{
$name = $expr->name->toLowerString();
switch ($name) {
case 'null':
return null;
case 'false':
return \false;
case 'true':
return \true;
}
return ($this->fallbackEvaluator)($expr);
}
}