1<?php
2
3namespace PhpParser;
4
5use function array_merge;
6use PhpParser\Node\Expr;
7use PhpParser\Node\Scalar;
8
9/**
10 * Evaluates constant expressions.
11 *
12 * This evaluator is able to evaluate all constant expressions (as defined by PHP), which can be
13 * evaluated without further context. If a subexpression is not of this type, a user-provided
14 * fallback evaluator is invoked. To support all constant expressions that are also supported by
15 * PHP (and not already handled by this class), the fallback evaluator must be able to handle the
16 * following node types:
17 *
18 *  * All Scalar\MagicConst\* nodes.
19 *  * Expr\ConstFetch nodes. Only null/false/true are already handled by this class.
20 *  * Expr\ClassConstFetch nodes.
21 *
22 * The fallback evaluator should throw ConstExprEvaluationException for nodes it cannot evaluate.
23 *
24 * The evaluation is dependent on runtime configuration in two respects: Firstly, floating
25 * point to string conversions are affected by the precision ini setting. Secondly, they are also
26 * affected by the LC_NUMERIC locale.
27 */
28class ConstExprEvaluator
29{
30    private $fallbackEvaluator;
31
32    /**
33     * Create a constant expression evaluator.
34     *
35     * The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See
36     * class doc comment for more information.
37     *
38     * @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated
39     */
40    public function __construct(callable $fallbackEvaluator = null) {
41        $this->fallbackEvaluator = $fallbackEvaluator ?? function(Expr $expr) {
42            throw new ConstExprEvaluationException(
43                "Expression of type {$expr->getType()} cannot be evaluated"
44            );
45        };
46    }
47
48    /**
49     * Silently evaluates a constant expression into a PHP value.
50     *
51     * Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException.
52     * The original source of the exception is available through getPrevious().
53     *
54     * If some part of the expression cannot be evaluated, the fallback evaluator passed to the
55     * constructor will be invoked. By default, if no fallback is provided, an exception of type
56     * ConstExprEvaluationException is thrown.
57     *
58     * See class doc comment for caveats and limitations.
59     *
60     * @param Expr $expr Constant expression to evaluate
61     * @return mixed Result of evaluation
62     *
63     * @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred
64     */
65    public function evaluateSilently(Expr $expr) {
66        set_error_handler(function($num, $str, $file, $line) {
67            throw new \ErrorException($str, 0, $num, $file, $line);
68        });
69
70        try {
71            return $this->evaluate($expr);
72        } catch (\Throwable $e) {
73            if (!$e instanceof ConstExprEvaluationException) {
74                $e = new ConstExprEvaluationException(
75                    "An error occurred during constant expression evaluation", 0, $e);
76            }
77            throw $e;
78        } finally {
79            restore_error_handler();
80        }
81    }
82
83    /**
84     * Directly evaluates a constant expression into a PHP value.
85     *
86     * May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these
87     * into a ConstExprEvaluationException.
88     *
89     * If some part of the expression cannot be evaluated, the fallback evaluator passed to the
90     * constructor will be invoked. By default, if no fallback is provided, an exception of type
91     * ConstExprEvaluationException is thrown.
92     *
93     * See class doc comment for caveats and limitations.
94     *
95     * @param Expr $expr Constant expression to evaluate
96     * @return mixed Result of evaluation
97     *
98     * @throws ConstExprEvaluationException if the expression cannot be evaluated
99     */
100    public function evaluateDirectly(Expr $expr) {
101        return $this->evaluate($expr);
102    }
103
104    private function evaluate(Expr $expr) {
105        if ($expr instanceof Scalar\LNumber
106            || $expr instanceof Scalar\DNumber
107            || $expr instanceof Scalar\String_
108        ) {
109            return $expr->value;
110        }
111
112        if ($expr instanceof Expr\Array_) {
113            return $this->evaluateArray($expr);
114        }
115
116        // Unary operators
117        if ($expr instanceof Expr\UnaryPlus) {
118            return +$this->evaluate($expr->expr);
119        }
120        if ($expr instanceof Expr\UnaryMinus) {
121            return -$this->evaluate($expr->expr);
122        }
123        if ($expr instanceof Expr\BooleanNot) {
124            return !$this->evaluate($expr->expr);
125        }
126        if ($expr instanceof Expr\BitwiseNot) {
127            return ~$this->evaluate($expr->expr);
128        }
129
130        if ($expr instanceof Expr\BinaryOp) {
131            return $this->evaluateBinaryOp($expr);
132        }
133
134        if ($expr instanceof Expr\Ternary) {
135            return $this->evaluateTernary($expr);
136        }
137
138        if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) {
139            return $this->evaluate($expr->var)[$this->evaluate($expr->dim)];
140        }
141
142        if ($expr instanceof Expr\ConstFetch) {
143            return $this->evaluateConstFetch($expr);
144        }
145
146        return ($this->fallbackEvaluator)($expr);
147    }
148
149    private function evaluateArray(Expr\Array_ $expr) {
150        $array = [];
151        foreach ($expr->items as $item) {
152            if (null !== $item->key) {
153                $array[$this->evaluate($item->key)] = $this->evaluate($item->value);
154            } elseif ($item->unpack) {
155                $array = array_merge($array, $this->evaluate($item->value));
156            } else {
157                $array[] = $this->evaluate($item->value);
158            }
159        }
160        return $array;
161    }
162
163    private function evaluateTernary(Expr\Ternary $expr) {
164        if (null === $expr->if) {
165            return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else);
166        }
167
168        return $this->evaluate($expr->cond)
169            ? $this->evaluate($expr->if)
170            : $this->evaluate($expr->else);
171    }
172
173    private function evaluateBinaryOp(Expr\BinaryOp $expr) {
174        if ($expr instanceof Expr\BinaryOp\Coalesce
175            && $expr->left instanceof Expr\ArrayDimFetch
176        ) {
177            // This needs to be special cased to respect BP_VAR_IS fetch semantics
178            return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)]
179                ?? $this->evaluate($expr->right);
180        }
181
182        // The evaluate() calls are repeated in each branch, because some of the operators are
183        // short-circuiting and evaluating the RHS in advance may be illegal in that case
184        $l = $expr->left;
185        $r = $expr->right;
186        switch ($expr->getOperatorSigil()) {
187            case '&':   return $this->evaluate($l) &   $this->evaluate($r);
188            case '|':   return $this->evaluate($l) |   $this->evaluate($r);
189            case '^':   return $this->evaluate($l) ^   $this->evaluate($r);
190            case '&&':  return $this->evaluate($l) &&  $this->evaluate($r);
191            case '||':  return $this->evaluate($l) ||  $this->evaluate($r);
192            case '??':  return $this->evaluate($l) ??  $this->evaluate($r);
193            case '.':   return $this->evaluate($l) .   $this->evaluate($r);
194            case '/':   return $this->evaluate($l) /   $this->evaluate($r);
195            case '==':  return $this->evaluate($l) ==  $this->evaluate($r);
196            case '>':   return $this->evaluate($l) >   $this->evaluate($r);
197            case '>=':  return $this->evaluate($l) >=  $this->evaluate($r);
198            case '===': return $this->evaluate($l) === $this->evaluate($r);
199            case 'and': return $this->evaluate($l) and $this->evaluate($r);
200            case 'or':  return $this->evaluate($l) or  $this->evaluate($r);
201            case 'xor': return $this->evaluate($l) xor $this->evaluate($r);
202            case '-':   return $this->evaluate($l) -   $this->evaluate($r);
203            case '%':   return $this->evaluate($l) %   $this->evaluate($r);
204            case '*':   return $this->evaluate($l) *   $this->evaluate($r);
205            case '!=':  return $this->evaluate($l) !=  $this->evaluate($r);
206            case '!==': return $this->evaluate($l) !== $this->evaluate($r);
207            case '+':   return $this->evaluate($l) +   $this->evaluate($r);
208            case '**':  return $this->evaluate($l) **  $this->evaluate($r);
209            case '<<':  return $this->evaluate($l) <<  $this->evaluate($r);
210            case '>>':  return $this->evaluate($l) >>  $this->evaluate($r);
211            case '<':   return $this->evaluate($l) <   $this->evaluate($r);
212            case '<=':  return $this->evaluate($l) <=  $this->evaluate($r);
213            case '<=>': return $this->evaluate($l) <=> $this->evaluate($r);
214        }
215
216        throw new \Exception('Should not happen');
217    }
218
219    private function evaluateConstFetch(Expr\ConstFetch $expr) {
220        $name = $expr->name->toLowerString();
221        switch ($name) {
222            case 'null': return null;
223            case 'false': return false;
224            case 'true': return true;
225        }
226
227        return ($this->fallbackEvaluator)($expr);
228    }
229}
230