123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- <?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 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 ConstExprEvaluationException) {
- $e = new 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);
- }
- }
|