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