1
0
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:
Nikita Popov 2017-09-30 18:56:44 +02:00
parent a02990a39a
commit 4b1d9667af
32 changed files with 381 additions and 0 deletions

View File

@ -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

View File

@ -0,0 +1,5 @@
<?php
namespace PhpParser;
class ConstExprEvaluationException extends \Exception {}

View 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);
}
}

View File

@ -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;
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class BitwiseAnd extends BinaryOp
{
public function getOperatorSigil() : string {
return '&';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class BitwiseOr extends BinaryOp
{
public function getOperatorSigil() : string {
return '|';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class BitwiseXor extends BinaryOp
{
public function getOperatorSigil() : string {
return '^';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class BooleanAnd extends BinaryOp
{
public function getOperatorSigil() : string {
return '&&';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class BooleanOr extends BinaryOp
{
public function getOperatorSigil() : string {
return '||';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Coalesce extends BinaryOp
{
public function getOperatorSigil() : string {
return '??';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Concat extends BinaryOp
{
public function getOperatorSigil() : string {
return '.';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Div extends BinaryOp
{
public function getOperatorSigil() : string {
return '/';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Equal extends BinaryOp
{
public function getOperatorSigil() : string {
return '==';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Greater extends BinaryOp
{
public function getOperatorSigil() : string {
return '>';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class GreaterOrEqual extends BinaryOp
{
public function getOperatorSigil() : string {
return '>=';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Identical extends BinaryOp
{
public function getOperatorSigil() : string {
return '===';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class LogicalAnd extends BinaryOp
{
public function getOperatorSigil() : string {
return 'and';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class LogicalOr extends BinaryOp
{
public function getOperatorSigil() : string {
return 'or';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class LogicalXor extends BinaryOp
{
public function getOperatorSigil() : string {
return 'xor';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Minus extends BinaryOp
{
public function getOperatorSigil() : string {
return '-';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Mod extends BinaryOp
{
public function getOperatorSigil() : string {
return '%';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Mul extends BinaryOp
{
public function getOperatorSigil() : string {
return '*';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class NotEqual extends BinaryOp
{
public function getOperatorSigil() : string {
return '!=';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class NotIdentical extends BinaryOp
{
public function getOperatorSigil() : string {
return '!==';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Plus extends BinaryOp
{
public function getOperatorSigil() : string {
return '+';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Pow extends BinaryOp
{
public function getOperatorSigil() : string {
return '**';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class ShiftLeft extends BinaryOp
{
public function getOperatorSigil() : string {
return '<<';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class ShiftRight extends BinaryOp
{
public function getOperatorSigil() : string {
return '>>';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Smaller extends BinaryOp
{
public function getOperatorSigil() : string {
return '<';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class SmallerOrEqual extends BinaryOp
{
public function getOperatorSigil() : string {
return '<=';
}
}

View File

@ -6,4 +6,7 @@ use PhpParser\Node\Expr\BinaryOp;
class Spaceship extends BinaryOp
{
public function getOperatorSigil() : string {
return '<=>';
}
}

View 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));
}
}