| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 | <?phpnamespace 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);    }}
 |