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