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