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