1: <?php
2:
3: namespace PhpParser;
4:
5: use function array_merge;
6: use PhpParser\Node\Expr;
7: use 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: */
28: class 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: