ConstExprEvaluator.php 9.2 KB

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