1: <?php declare(strict_types=1);
2:
3: namespace PhpParser;
4:
5: use PhpParser\Node\Expr;
6: use PhpParser\Node\Scalar;
7:
8: use 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: */
29: class 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: