mirror of
https://github.com/danog/PHP-Parser.git
synced 2024-11-30 04:19:30 +01:00
Add constant expression evaluator (#402)
This commit is contained in:
parent
a02990a39a
commit
4b1d9667af
@ -24,6 +24,8 @@ Version 4.0.0-dev
|
||||
* Added `getComments()`, `getStartLine()`, `getEndLine()`, `getStartTokenPos()`, `getEndTokenPos()`,
|
||||
`getStartFilePos()` and `getEndFilePos()` methods to `Node`. These provide a more obvious access
|
||||
point for the already existing attributes of the same name.
|
||||
* Added `ConstExprEvaluator` to evaluate constant expressions to PHP values.
|
||||
* Added `Expr\BinaryOp::getOperatorSigil()`, returning `+` for `Expr\BinaryOp\Plus`, etc.
|
||||
|
||||
### Changed
|
||||
|
||||
|
5
lib/PhpParser/ConstExprEvaluationException.php
Normal file
5
lib/PhpParser/ConstExprEvaluationException.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace PhpParser;
|
||||
|
||||
class ConstExprEvaluationException extends \Exception {}
|
186
lib/PhpParser/ConstExprEvaluator.php
Normal file
186
lib/PhpParser/ConstExprEvaluator.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace PhpParser;
|
||||
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Scalar;
|
||||
|
||||
/**
|
||||
* Evaluates constant expressions.
|
||||
*
|
||||
* This evaluator is able to evaluate all constant expressions (as defined by PHP), which can be
|
||||
* evaluated without further context. If a subexpression is not of this type, a user-provided
|
||||
* fallback evaluator is invoked. To support all constant expressions that are also supported by
|
||||
* PHP (and not already handled by this class), the fallback evaluator must be able to handle the
|
||||
* following node types:
|
||||
*
|
||||
* * All Scalar\MagicConst\* nodes.
|
||||
* * Expr\ConstFetch nodes. Only null/false/true are already handled by this class.
|
||||
* * Expr\ClassConstFetch nodes.
|
||||
*
|
||||
* The fallback evaluator should throw ConstExprEvaluationException for nodes it cannot evaluate.
|
||||
*
|
||||
* The evaluation is performed as PHP would perform it, and as such may generate notices, warnings
|
||||
* or Errors. For example, if the expression `1%0` is evaluated, an ArithmeticError is thrown. It is
|
||||
* left to the consumer to handle these as appropriate.
|
||||
*
|
||||
* The evaluation is also dependent on runtime configuration in two respects: Firstly, floating
|
||||
* point to string conversions are affected by the precision ini setting. Secondly, they are also
|
||||
* affected by the LC_NUMERIC locale.
|
||||
*/
|
||||
class ConstExprEvaluator {
|
||||
private $fallbackEvaluator;
|
||||
|
||||
/**
|
||||
* Create a constant expression evaluator.
|
||||
*
|
||||
* The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See
|
||||
* class doc comment for more information.
|
||||
*
|
||||
* @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated
|
||||
*/
|
||||
public function __construct(callable $fallbackEvaluator = null) {
|
||||
$this->fallbackEvaluator = $fallbackEvaluator ?? function(Expr $expr) {
|
||||
throw new ConstExprEvaluationException(
|
||||
"Expression of type {$expr->getType()} cannot be evaluated"
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a constant expression into a PHP value.
|
||||
*
|
||||
* If some part of the expression cannot be evaluated, the fallback evaluator passed to the
|
||||
* constructor will be invoked. By default, if no fallback is provided, an exception of type
|
||||
* ConstExprEvaluationException is thrown.
|
||||
*
|
||||
* See class doc comment for caveats and limitations.
|
||||
*
|
||||
* @param Expr $expr Constant expression to evaluate
|
||||
* @return mixed Result of evaluation
|
||||
* @throws ConstExprEvaluationException if the expression cannot be evaluated
|
||||
*/
|
||||
public function evaluate(Expr $expr) {
|
||||
if ($expr instanceof Scalar\LNumber
|
||||
|| $expr instanceof Scalar\DNumber
|
||||
|| $expr instanceof Scalar\String_
|
||||
) {
|
||||
return $expr->value;
|
||||
}
|
||||
|
||||
if ($expr instanceof Expr\Array_) {
|
||||
return $this->evaluateArray($expr);
|
||||
}
|
||||
|
||||
// Unary operators
|
||||
if ($expr instanceof Expr\UnaryPlus) {
|
||||
return +$this->evaluate($expr->expr);
|
||||
}
|
||||
if ($expr instanceof Expr\UnaryMinus) {
|
||||
return -$this->evaluate($expr->expr);
|
||||
}
|
||||
if ($expr instanceof Expr\BooleanNot) {
|
||||
return !$this->evaluate($expr->expr);
|
||||
}
|
||||
if ($expr instanceof Expr\BitwiseNot) {
|
||||
return ~$this->evaluate($expr->expr);
|
||||
}
|
||||
|
||||
if ($expr instanceof Expr\BinaryOp) {
|
||||
return $this->evaluateBinaryOp($expr);
|
||||
}
|
||||
|
||||
if ($expr instanceof Expr\Ternary) {
|
||||
return $this->evaluateTernary($expr);
|
||||
}
|
||||
|
||||
if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) {
|
||||
return $this->evaluate($expr->var)[$this->evaluate($expr->dim)];
|
||||
}
|
||||
|
||||
if ($expr instanceof Expr\ConstFetch) {
|
||||
return $this->evaluateConstFetch($expr);
|
||||
}
|
||||
|
||||
return ($this->fallbackEvaluator)($expr);
|
||||
}
|
||||
|
||||
private function evaluateArray(Expr\Array_ $expr) {
|
||||
$array = [];
|
||||
foreach ($expr->items as $item) {
|
||||
if (null !== $item->key) {
|
||||
$array[$this->evaluate($item->key)] = $this->evaluate($item->value);
|
||||
} else {
|
||||
$array[] = $this->evaluate($item->value);
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
private function evaluateTernary(Expr\Ternary $expr) {
|
||||
if (null === $expr->if) {
|
||||
return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else);
|
||||
}
|
||||
|
||||
return $this->evaluate($expr->cond)
|
||||
? $this->evaluate($expr->if)
|
||||
: $this->evaluate($expr->else);
|
||||
}
|
||||
|
||||
private function evaluateBinaryOp(Expr\BinaryOp $expr) {
|
||||
if ($expr instanceof Expr\BinaryOp\Coalesce
|
||||
&& $expr->left instanceof Expr\ArrayDimFetch
|
||||
) {
|
||||
// This needs to be special cased to respect BP_VAR_IS fetch semantics
|
||||
return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)]
|
||||
?? $this->evaluate($expr->right);
|
||||
}
|
||||
|
||||
// The evaluate() calls are repeated in each branch, because some of the operators are
|
||||
// short-circuiting and evaluating the RHS in advance may be illegal in that case
|
||||
$l = $expr->left;
|
||||
$r = $expr->right;
|
||||
switch ($expr->getOperatorSigil()) {
|
||||
case '&': return $this->evaluate($l) & $this->evaluate($r);
|
||||
case '|': return $this->evaluate($l) | $this->evaluate($r);
|
||||
case '^': return $this->evaluate($l) ^ $this->evaluate($r);
|
||||
case '&&': return $this->evaluate($l) && $this->evaluate($r);
|
||||
case '||': return $this->evaluate($l) || $this->evaluate($r);
|
||||
case '??': return $this->evaluate($l) ?? $this->evaluate($r);
|
||||
case '.': return $this->evaluate($l) . $this->evaluate($r);
|
||||
case '/': return $this->evaluate($l) / $this->evaluate($r);
|
||||
case '==': return $this->evaluate($l) == $this->evaluate($r);
|
||||
case '>': return $this->evaluate($l) > $this->evaluate($r);
|
||||
case '>=': return $this->evaluate($l) >= $this->evaluate($r);
|
||||
case '===': return $this->evaluate($l) === $this->evaluate($r);
|
||||
case 'and': return $this->evaluate($l) and $this->evaluate($r);
|
||||
case 'or': return $this->evaluate($l) or $this->evaluate($r);
|
||||
case 'xor': return $this->evaluate($l) xor $this->evaluate($r);
|
||||
case '-': return $this->evaluate($l) - $this->evaluate($r);
|
||||
case '%': return $this->evaluate($l) % $this->evaluate($r);
|
||||
case '*': return $this->evaluate($l) * $this->evaluate($r);
|
||||
case '!=': return $this->evaluate($l) != $this->evaluate($r);
|
||||
case '!==': return $this->evaluate($l) !== $this->evaluate($r);
|
||||
case '+': return $this->evaluate($l) + $this->evaluate($r);
|
||||
case '**': return $this->evaluate($l) ** $this->evaluate($r);
|
||||
case '<<': return $this->evaluate($l) << $this->evaluate($r);
|
||||
case '>>': return $this->evaluate($l) >> $this->evaluate($r);
|
||||
case '<': return $this->evaluate($l) < $this->evaluate($r);
|
||||
case '<=': return $this->evaluate($l) <= $this->evaluate($r);
|
||||
case '<=>': return $this->evaluate($l) <=> $this->evaluate($r);
|
||||
}
|
||||
|
||||
throw new \Exception('Should not happen');
|
||||
}
|
||||
|
||||
private function evaluateConstFetch(Expr\ConstFetch $expr) {
|
||||
$name = $expr->name->toLowerString();
|
||||
switch ($name) {
|
||||
case 'null': return null;
|
||||
case 'false': return false;
|
||||
case 'true': return true;
|
||||
}
|
||||
|
||||
return ($this->fallbackEvaluator)($expr);
|
||||
}
|
||||
}
|
@ -27,4 +27,14 @@ abstract class BinaryOp extends Expr
|
||||
public function getSubNodeNames() : array {
|
||||
return ['left', 'right'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the operator sigil for this binary operation.
|
||||
*
|
||||
* In the case there are multiple possible sigils for an operator, this method does not
|
||||
* necessarily return the one used in the parsed code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getOperatorSigil() : string;
|
||||
}
|
||||
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class BitwiseAnd extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '&';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class BitwiseOr extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '|';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class BitwiseXor extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '^';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class BooleanAnd extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '&&';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class BooleanOr extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '||';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Coalesce extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '??';
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Concat extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '.';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Div extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '/';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Equal extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '==';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Greater extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '>';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class GreaterOrEqual extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '>=';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Identical extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '===';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class LogicalAnd extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return 'and';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class LogicalOr extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return 'or';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class LogicalXor extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return 'xor';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Minus extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '-';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Mod extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '%';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Mul extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '*';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class NotEqual extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '!=';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class NotIdentical extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '!==';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Plus extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '+';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Pow extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '**';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class ShiftLeft extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '<<';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class ShiftRight extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '>>';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Smaller extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '<';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class SmallerOrEqual extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '<=';
|
||||
}
|
||||
}
|
@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
|
||||
|
||||
class Spaceship extends BinaryOp
|
||||
{
|
||||
public function getOperatorSigil() : string {
|
||||
return '<=>';
|
||||
}
|
||||
}
|
||||
|
97
test/PhpParser/ConstExprEvaluatorTest.php
Normal file
97
test/PhpParser/ConstExprEvaluatorTest.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace PhpParser;
|
||||
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Scalar;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ConstExprEvaluatorTest extends TestCase {
|
||||
/** @dataProvider provideTestEvaluate */
|
||||
public function testEvaluate($exprString, $expected) {
|
||||
$parser = new Parser\Php7(new Lexer());
|
||||
$expr = $parser->parse('<?php ' . $exprString . ';')[0]->expr;
|
||||
$evaluator = new ConstExprEvaluator();
|
||||
$this->assertSame($expected, $evaluator->evaluate($expr));
|
||||
}
|
||||
|
||||
public function provideTestEvaluate() {
|
||||
return [
|
||||
['1', 1],
|
||||
['1.0', 1.0],
|
||||
['"foo"', "foo"],
|
||||
['[0, 1]', [0, 1]],
|
||||
['["foo" => "bar"]', ["foo" => "bar"]],
|
||||
['NULL', null],
|
||||
['False', false],
|
||||
['true', true],
|
||||
['+1', 1],
|
||||
['-1', -1],
|
||||
['~0', -1],
|
||||
['!true', false],
|
||||
['[0][0]', 0],
|
||||
['"a"[0]', "a"],
|
||||
['true ? 1 : (1/0)', 1],
|
||||
['false ? (1/0) : 1', 1],
|
||||
['42 ?: (1/0)', 42],
|
||||
['false ?: 42', 42],
|
||||
['false ?? 42', false],
|
||||
['null ?? 42', 42],
|
||||
['[0][0] ?? 42', 0],
|
||||
['[][0] ?? 42', 42],
|
||||
['0b11 & 0b10', 0b10],
|
||||
['0b11 | 0b10', 0b11],
|
||||
['0b11 ^ 0b10', 0b01],
|
||||
['1 << 2', 4],
|
||||
['4 >> 2', 1],
|
||||
['"a" . "b"', "ab"],
|
||||
['4 + 2', 6],
|
||||
['4 - 2', 2],
|
||||
['4 * 2', 8],
|
||||
['4 / 2', 2],
|
||||
['4 % 2', 0],
|
||||
['4 ** 2', 16],
|
||||
['1 == 1.0', true],
|
||||
['1 != 1.0', false],
|
||||
['1 < 2.0', true],
|
||||
['1 <= 2.0', true],
|
||||
['1 > 2.0', false],
|
||||
['1 >= 2.0', false],
|
||||
['1 <=> 2.0', -1],
|
||||
['1 === 1.0', false],
|
||||
['1 !== 1.0', true],
|
||||
['true && true', true],
|
||||
['true and true', true],
|
||||
['false && (1/0)', false],
|
||||
['false and (1/0)', false],
|
||||
['false || false', false],
|
||||
['false or false', false],
|
||||
['true || (1/0)', true],
|
||||
['true or (1/0)', true],
|
||||
['true xor false', true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \PhpParser\ConstExprEvaluationException
|
||||
* @expectedExceptionMessage Expression of type Expr_Variable cannot be evaluated
|
||||
*/
|
||||
public function testEvaluateFails() {
|
||||
$evaluator = new ConstExprEvaluator();
|
||||
$evaluator->evaluate(new Expr\Variable('a'));
|
||||
}
|
||||
|
||||
public function testEvaluateFallback() {
|
||||
$evaluator = new ConstExprEvaluator(function(Expr $expr) {
|
||||
if ($expr instanceof Scalar\MagicConst\Line) {
|
||||
return 42;
|
||||
}
|
||||
throw new ConstExprEvaluationException();
|
||||
});
|
||||
$expr = new Expr\BinaryOp\Plus(
|
||||
new Scalar\LNumber(8),
|
||||
new Scalar\MagicConst\Line()
|
||||
);
|
||||
$this->assertSame(50, $evaluator->evaluate($expr));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user