Improve constant evaluation and add docs

Split into evaluateDirectly() and evaluateSilently(), to be able
to treat errors more gracefully. Add documentation for constant
evaluation.
This commit is contained in:
Nikita Popov 2018-01-27 17:45:37 +01:00
parent d817818b5d
commit a513ccabb7
5 changed files with 205 additions and 10 deletions

View File

@ -203,6 +203,9 @@ Component documentation:
* [Error handling](doc/component/Error_handling.markdown)
* Column information for errors
* Error recovery (parsing of syntactically incorrect code)
* [Constant expression evaluation](component/Constant_expression_evaluation.markdown)
* Evaluating constant/property/etc initializers
* Handling errors and unsupported expressions
* [Performance](doc/component/Performance.markdown)
* Disabling XDebug
* Reusing objects

View File

@ -27,6 +27,9 @@ Component documentation
* [Error handling](component/Error_handling.markdown)
* Column information for errors
* Error recovery (parsing of syntactically incorrect code)
* [Constant expression evaluation](component/Constant_expression_evaluation.markdown)
* Evaluating constant/property/etc initializers
* Handling errors and unsupported expressions
* [Performance](component/Performance.markdown)
* Disabling XDebug
* Reusing objects

View File

@ -0,0 +1,115 @@
Constant expression evaluation
==============================
Initializers for constants, properties, parameters, etc. have limited support for expressions. For
example:
```php
<?php
class Test {
const SECONDS_IN_HOUR = 60 * 60;
const SECONDS_IN_DAY = 24 * self::SECONDS_IN_HOUR;
}
```
PHP-Parser supports evaluation of such constant expressions through the `ConstExprEvaluator` class:
```php
<?php
use PhpParser\{ConstExprEvaluator, ConstExprEvaluationException};
$evalutator = new ConstExprEvaluator();
try {
$value = $evalutator->evaluateSilently($someExpr);
} catch (ConstExprEvaluationException $e) {
// Either the expression contains unsupported expression types,
// or an error occurred during evaluation
}
```
Error handling
--------------
The constant evaluator provides two methods, `evaluateDirectly()` and `evaluateSilently()`, which
differ in error behavior. `evaluateDirectly()` will evaluate the expression as PHP would, including
any generated warnings or Errors. `evaluateSilently()` will instead convert warnings and Errors into
a `ConstExprEvaluationException`. For example:
```php
<?php
use PhpParser\{ConstExprEvaluator, ConstExprEvaluationException};
use PhpParser\Node\{Expr, Scalar};
$evaluator = new ConstExprEvaluator();
// 10 / 0
$expr = new Expr\BinaryOp\Div(new Scalar\LNumber(10), new Scalar\LNumber(0));
var_dump($evaluator->evaluateDirectly($expr)); // float(INF)
// Warning: Division by zero
try {
$evaluator->evaluateSilently($expr);
} catch (ConstExprEvaluationException $e) {
var_dump($e->getPrevious()->getMessage()); // Division by zero
}
```
For the purposes of static analysis, you will likely want to use `evaluateSilently()` and leave
erroring expressions unevaluated.
Unsupported expressions and evaluator fallback
----------------------------------------------
The constant expression evaluator supports all expression types that are permitted in constant
expressions, apart from the following:
* `Scalar\MagicConst\*`
* `Expr\ConstFetch` (only null/false/true are handled)
* `Expr\ClassConstFetch`
Handling these expression types requires non-local information, such as which global constants are
defined. By default, the evaluator will throw a `ConstExprEvaluationException` when it encounters
an unsupported expression type.
It is possible to override this behavior and support resolution for these expression types by
specifying an evaluation fallback function:
```php
<?php
use PhpParser\{ConstExprEvaluator, ConstExprEvaluationException};
use PhpParser\Node\Expr;
$evalutator = new ConstExprEvaluator(function(Expr $expr) {
if ($expr instanceof Expr\ConstFetch) {
return fetchConstantSomehow($expr);
}
if ($expr instanceof Expr\ClassConstFetch) {
return fetchClassConstantSomehow($expr);
}
// etc.
throw new ConstExprEvaluationException(
"Expression of type {$expr->getType()} cannot be evaluated");
});
try {
$evalutator->evaluateSilently($someExpr);
} catch (ConstExprEvaluationException $e) {
// Handle exception
}
```
Implementers are advised to ensure that evaluation of indirect constant references cannot lead to
infinite recursion. For example, the following code could lead to infinite recursion if constant
lookup is implemented naively.
```php
<?php
class Test {
const A = self::B;
const B = self::A;
}
```

View File

@ -20,11 +20,7 @@ use PhpParser\Node\Scalar;
*
* 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
* The evaluation is 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.
*/
@ -49,7 +45,10 @@ class ConstExprEvaluator
}
/**
* Evaluates a constant expression into a PHP value.
* Silently evaluates a constant expression into a PHP value.
*
* Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException.
* The original source of the exception is available through getPrevious().
*
* 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
@ -59,9 +58,49 @@ class ConstExprEvaluator
*
* @param Expr $expr Constant expression to evaluate
* @return mixed Result of evaluation
*
* @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred
*/
public function evaluateSilently(Expr $expr) {
set_error_handler(function($num, $str, $file, $line) {
throw new \ErrorException($str, 0, $num, $file, $line);
});
try {
return $this->evaluate($expr);
} catch (\Throwable $e) {
if (!$e instanceof ConstExprEvaluationException) {
$e = new ConstExprEvaluationException(
"An error occurred during constant expression evaluation", 0, $e);
}
throw $e;
} finally {
restore_error_handler();
}
}
/**
* Directly evaluates a constant expression into a PHP value.
*
* May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these
* into a ConstExprEvaluationException.
*
* 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) {
public function evaluateDirectly(Expr $expr) {
return $this->evaluate($expr);
}
private function evaluate(Expr $expr) {
if ($expr instanceof Scalar\LNumber
|| $expr instanceof Scalar\DNumber
|| $expr instanceof Scalar\String_

View File

@ -13,7 +13,7 @@ class ConstExprEvaluatorTest extends TestCase
$parser = new Parser\Php7(new Lexer());
$expr = $parser->parse('<?php ' . $exprString . ';')[0]->expr;
$evaluator = new ConstExprEvaluator();
$this->assertSame($expected, $evaluator->evaluate($expr));
$this->assertSame($expected, $evaluator->evaluateDirectly($expr));
}
public function provideTestEvaluate() {
@ -79,7 +79,7 @@ class ConstExprEvaluatorTest extends TestCase
*/
public function testEvaluateFails() {
$evaluator = new ConstExprEvaluator();
$evaluator->evaluate(new Expr\Variable('a'));
$evaluator->evaluateDirectly(new Expr\Variable('a'));
}
public function testEvaluateFallback() {
@ -93,6 +93,41 @@ class ConstExprEvaluatorTest extends TestCase
new Scalar\LNumber(8),
new Scalar\MagicConst\Line()
);
$this->assertSame(50, $evaluator->evaluate($expr));
$this->assertSame(50, $evaluator->evaluateDirectly($expr));
}
/**
* @dataProvider provideTestEvaluateSilently
*/
public function testEvaluateSilently($expr, $exception, $msg) {
$evaluator = new ConstExprEvaluator();
try {
$evaluator->evaluateSilently($expr);
} catch (ConstExprEvaluationException $e) {
$this->assertSame(
'An error occurred during constant expression evaluation',
$e->getMessage()
);
$prev = $e->getPrevious();
$this->assertInstanceOf($exception, $prev);
$this->assertSame($msg, $prev->getMessage());
}
}
public function provideTestEvaluateSilently() {
return [
[
new Expr\BinaryOp\Mod(new Scalar\LNumber(42), new Scalar\LNumber(0)),
\Error::class,
'Modulo by zero'
],
[
new Expr\BinaryOp\Div(new Scalar\LNumber(42), new Scalar\LNumber(0)),
\ErrorException::class,
'Division by zero'
],
];
}
}