diff --git a/README.md b/README.md index 85c81c11..4c00619c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/README.md b/doc/README.md index 510b131a..c055f8b9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -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 diff --git a/doc/component/Constant_expression_evaluation.markdown b/doc/component/Constant_expression_evaluation.markdown new file mode 100644 index 00000000..9ab4f5c3 --- /dev/null +++ b/doc/component/Constant_expression_evaluation.markdown @@ -0,0 +1,115 @@ +Constant expression evaluation +============================== + +Initializers for constants, properties, parameters, etc. have limited support for expressions. For +example: + +```php +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 +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 +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 +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_ diff --git a/test/PhpParser/ConstExprEvaluatorTest.php b/test/PhpParser/ConstExprEvaluatorTest.php index 4f6cebd5..d0a83fff 100644 --- a/test/PhpParser/ConstExprEvaluatorTest.php +++ b/test/PhpParser/ConstExprEvaluatorTest.php @@ -13,7 +13,7 @@ class ConstExprEvaluatorTest extends TestCase $parser = new Parser\Php7(new Lexer()); $expr = $parser->parse('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' + ], + ]; } }