From fe64d1ac8a1332b9dcc1af05ec4fdfd9d8665f3a Mon Sep 17 00:00:00 2001 From: odain Date: Fri, 5 Sep 2025 09:29:44 +0200 Subject: [PATCH 1/4] Enhance/Add expression evaluations for iTop project: https://github.com/Combodo/iTop Added: - Variable - Isset - ClassConstFetch - Cast - StaticPropertyFetch - FuncCall - StaticCall - NullsafePropertyFetch - PropertyFetch - NullsafeMethodCall - MethodCall Enhanced: - ConstFetch - Coalesce --- lib/PhpParser/ConstExprEvaluator.php | 349 +++++++++++++++++++++- test/PhpParser/ConstExprEvaluatorTest.php | 114 ++++++- 2 files changed, 446 insertions(+), 17 deletions(-) diff --git a/lib/PhpParser/ConstExprEvaluator.php b/lib/PhpParser/ConstExprEvaluator.php index 9526787142..74fcc2fb89 100644 --- a/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/PhpParser/ConstExprEvaluator.php @@ -3,7 +3,10 @@ namespace PhpParser; use PhpParser\Node\Expr; +use PhpParser\Node\Identifier; +use PhpParser\Node\Name; use PhpParser\Node\Scalar; +use Exception; use function array_merge; @@ -30,6 +33,12 @@ class ConstExprEvaluator { /** @var callable|null */ private $fallbackEvaluator; + /** @var array $functionsWhiteList */ + private $functionsWhiteList; + + /** @var array $staticCallsWhitelist */ + private $staticCallsWhitelist; + /** * Create a constant expression evaluator. * @@ -44,6 +53,19 @@ public function __construct(?callable $fallbackEvaluator = null) { "Expression of type {$expr->getType()} cannot be evaluated" ); }; + + $this->functionsWhiteList = []; + $this->staticCallsWhitelist = []; + } + + public function setFunctionsWhitelist(array $functionsWhiteList): void + { + $this->functionsWhiteList = $functionsWhiteList; + } + + public function setStaticCallsWhitelist(array $staticCallsWhitelist): void + { + $this->staticCallsWhitelist = $staticCallsWhitelist; } /** @@ -115,6 +137,10 @@ private function evaluate(Expr $expr) { return $this->evaluateArray($expr); } + if ($expr instanceof Expr\Variable) { + return $this->evaluateVariable($expr); + } + // Unary operators if ($expr instanceof Expr\UnaryPlus) { return +$this->evaluate($expr->expr); @@ -145,6 +171,37 @@ private function evaluate(Expr $expr) { return $this->evaluateConstFetch($expr); } + if ($expr instanceof Expr\Isset_) { + return $this->evaluateIsset($expr); + } + + if ($expr instanceof Expr\ClassConstFetch) { + return $this->evaluateClassConstFetch($expr); + } + + if ($expr instanceof Expr\Cast) { + return $this->evaluateCast($expr); + } + + if ($expr instanceof Expr\StaticPropertyFetch) { + return $this->evaluateStaticPropertyFetch($expr); + } + + if ($expr instanceof Expr\FuncCall) { + return $this->evaluateFuncCall($expr); + } + + if ($expr instanceof Expr\StaticCall) { + return $this->evaluateStaticCall($expr); + } + + if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) { + return $this->evaluatePropertyFetch($expr); + } + + if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { + return $this->evaluateMethodCall($expr); + } return ($this->fallbackEvaluator)($expr); } @@ -175,12 +232,14 @@ private function evaluateTernary(Expr\Ternary $expr) { /** @return mixed */ 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); + if ($expr instanceof Expr\BinaryOp\Coalesce) { + try { + $var = $this->evaluate($expr->left); + return $var ?? $this->evaluate($expr->right); + } catch(\Throwable $t){ + //handle when isset($expr->left->var)===false + return $this->evaluate($expr->right); + } } // The evaluate() calls are repeated in each branch, because some of the operators are @@ -225,13 +284,279 @@ private function evaluateBinaryOp(Expr\BinaryOp $expr) { /** @return mixed */ private function evaluateConstFetch(Expr\ConstFetch $expr) { - $name = $expr->name->toLowerString(); - switch ($name) { - case 'null': return null; - case 'false': return false; - case 'true': return true; - } + try { + $name = $expr->name; + if(! is_string($name)){ + //PHP_VERSION_ID usecase + $name = $name->name; + } + + if (defined($name)){ + return constant($name); + } + } catch(\Throwable $t){} return ($this->fallbackEvaluator)($expr); } + + /** @return mixed */ + private function evaluateIsset(Expr\Isset_ $expr) { + try { + foreach ($expr->vars as $var){ + $var = $this->evaluate($var); + if (! isset($var)){ + return false; + } + } + + return true; + } catch(\Throwable $t){ + return false; + } + } + + /** @return mixed */ + private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { + try { + $classname = $expr->class->name; + $property = $expr->name->name; + + if ('class' === $property){ + return $classname; + } + + if (class_exists($classname)){ + $class = new \ReflectionClass($classname); + if (array_key_exists($property, $class->getConstants())) { + $oReflectionConstant = $class->getReflectionConstant($property); + if ($oReflectionConstant->isPublic()){ + return $class->getConstant($property); + } + } + } + } catch(\Throwable $t){} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateCast(Expr\Cast $expr) { + try { + $subexpr = $this->evaluate($expr->expr); + $type = get_class($expr); + switch ($type){ + case Expr\Cast\Array_::class: + return (array) $subexpr; + + case Expr\Cast\Bool_::class: + return (bool) $subexpr; + + case Expr\Cast\Double::class: + switch ($expr->getAttribute("kind")){ + case Expr\Cast\Double::KIND_DOUBLE: + return (double) $subexpr; + + case Expr\Cast\Double::KIND_FLOAT: + case Expr\Cast\Double::KIND_REAL: + return (float) $subexpr; + } + + break; + + case Expr\Cast\Int_::class: + return (int) $subexpr; + + case Expr\Cast\Object_::class: + return (object) $subexpr; + + case Expr\Cast\String_::class: + return (string) $subexpr; + } + } catch(\Throwable $t){ + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) + { + try { + $classname = $expr->class->name; + if ($expr->name instanceof Identifier){ + $property = $expr->name->name; + } else { + $property = $this->evaluate($expr->name); + } + + if (class_exists($classname)){ + $class = new \ReflectionClass($classname); + if (array_key_exists($property, $class->getStaticProperties())) { + $oReflectionProperty = $class->getProperty($property); + if ($oReflectionProperty->isPublic()){ + return $class->getStaticPropertyValue($property); + } + } + } + } + catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateFuncCall(Expr\FuncCall $expr) + { + try { + $name = $expr->name; + if ($name instanceof Name){ + $function = $name->name; + } else { + $function = $this->evaluate($name); + } + + if (! in_array($function, $this->functionsWhiteList)){ + throw new Exception("FuncCall $function not supported"); + } + + $args=[]; + foreach ($expr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $args[]=$arg->value->value; + } + + $reflection_function = new \ReflectionFunction($function); + return $reflection_function->invoke(...$args); + } + catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateVariable(Expr\Variable $expr) + { + try { + $name = $expr->name; + if (array_key_exists($name, get_defined_vars())) { + return $$name; + } + + if (array_key_exists($name, $GLOBALS)) { + global $$name; + return $$name; + } + } catch (\Throwable $t) { + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateStaticCall(Expr\StaticCall $expr) + { + try { + $class = $expr->class->name; + if ($expr->name instanceof Identifier){ + $method = $expr->name->name; + } else { + $method = $this->evaluate($expr->name); + } + + $static_call_description = "$class::$method"; + if (! in_array($static_call_description, $this->staticCallsWhitelist)){ + throw new Exception("StaticCall $static_call_description not supported"); + } + + $args=[]; + foreach ($expr->args as $arg){ + /** @var \PhpParser\Node\Arg $arg */ + $args[]=$arg->value->value; + } + + $class = new \ReflectionClass($class); + $method = $class->getMethod($method); + if ($method->isPublic()){ + return $method->invokeArgs(null, $args); + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** + * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr + * + * @return mixed + */ + private function evaluatePropertyFetch($expr) + { + try { + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } + + if (! is_null($var)) { + try { + if ($expr->name instanceof Identifier) { + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + + $reflectionClass = new \ReflectionClass(get_class($var)); + $property = $reflectionClass->getProperty($name); + if ($property->isPublic()) { + return $property->getValue($var); + } + } + catch (\Throwable $t) {} + } else if ($expr instanceof Expr\NullsafePropertyFetch){ + return null; + } + + return ($this->fallbackEvaluator)($expr); + } + + /** + * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr + * + * @return mixed + */ + private function evaluateMethodCall($expr) + { + try { + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } + + if (! is_null($var)) { + try { + $args = []; + foreach ($expr->args as $arg) { + /** @var \PhpParser\Node\Arg $arg */ + $args[] = $arg->value->value; + } + + if ($expr->name instanceof Identifier) { + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + + $reflectionClass = new \ReflectionClass(get_class($var)); + $method = $reflectionClass->getMethod($name); + if ($method->isPublic()) { + return $method->invokeArgs($var, $args); + } + } + catch (\Throwable $t) {} + } else if ($expr instanceof Expr\NullsafeMethodCall){ + return null; + } + + return ($this->fallbackEvaluator)($expr); + } } diff --git a/test/PhpParser/ConstExprEvaluatorTest.php b/test/PhpParser/ConstExprEvaluatorTest.php index 513918e56d..c05fd3ea69 100644 --- a/test/PhpParser/ConstExprEvaluatorTest.php +++ b/test/PhpParser/ConstExprEvaluatorTest.php @@ -8,10 +8,29 @@ class ConstExprEvaluatorTest extends \PHPUnit\Framework\TestCase { /** @dataProvider provideTestEvaluate */ public function testEvaluate($exprString, $expected): void { - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $expr = $parser->parse('expr; - $evaluator = new ConstExprEvaluator(); - $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); + global $globalNotDeclaredVar; + + global $globalNonNullVar; + $globalNonNullVar="a"; + + global $globalArray; + $globalArray=["gabu" => "zomeu"]; + + global $globalNullVar; + $globalNullVar=null; + + global $globalEvaluationFakeClass; + $globalEvaluationFakeClass = new EvaluationFakeClass(); + + $oNonNullVar2="a"; + $oNullVar2=null; + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $expr = $parser->parse('expr; + $evaluator = new ConstExprEvaluator(); + $evaluator->setStaticCallsWhitelist(["PhpParser\EvaluationFakeClass::GetStaticValue"]); + $evaluator->setFunctionsWhitelist(["class_exists"]); + $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); } public static function provideTestEvaluate() { @@ -26,7 +45,9 @@ public static function provideTestEvaluate() { ['["a", "b" => "b", ...["b" => "bb", "c"]]', ["a", "b" => "bb", "c"]], ['NULL', null], ['False', false], + ['True', true], ['true', true], + ['PHP_VERSION_ID', PHP_VERSION_ID], ['+1', 1], ['-1', -1], ['~0', -1], @@ -72,6 +93,44 @@ public static function provideTestEvaluate() { ['true or (1/0)', true], ['true xor false', true], ['"foo" |> "strlen"', 3], + + //Variable + ['$globalNonNullVar', "a"], + ['$globalNotDeclaredVar', null], + ['$globalArray', ["gabu" => "zomeu"]], + ['$globalNullVar', null], + ['$globalNonNullVar', "a"], + //Isset + ['isset($globalNotDeclaredVar)', false], + ['isset($globalNonNullVar)', true], + ['isset($globalArray)', true], + ['isset($globalNullVar)', false], + ['isset($globalNonNullVar)', true], + ['isset($oNonNullVar)', false], + ['isset($oNullVar)', false], + ['isset($eee)', false], + //Cast + ['(int)true', 1], + ['(string)1', "1"], + ['(bool)1', true], + ['(double)1', 1.0], + ['(float)1', 1.0], + ['(string) $globalEvaluationFakeClass', "toString"], + ['PhpParser\EvaluationFakeClass::CONST_4TEST', 456], + ['UnexistingClass::class', "UnexistingClass"], + ['PhpParser\EvaluationFakeClass::$STATICPROPERTY_4TEST', 123], + ['PhpParser\EvaluationFakeClass::GetStaticValue()', "shadok"], + ['class_exists("PhpParser\EvaluationFakeClass")', true], + ['$globalEvaluationFakeClass->iIsOk', 'IsOkValue'], + ['$globalNullVar?->iIsOk', null], + ['$globalEvaluationFakeClass->GetName()', 'gabuzomeu'], + ['$globalNullVar?->GetName()', null], + ['$globalEvaluationFakeClass->GetLongName("aa")', 'gabuzomeu_aa'], + ['$globalNullVar??1', 1], + ['$globalNotDeclaredVar??1', 1], + ['$globalNotDeclaredVar["a"]??1', 1], + ['$globalArray["gabu"]??1', "zomeu"], + ['$globalNonNullVar??1', "a"], ]; } @@ -79,9 +138,31 @@ public function testEvaluateFails(): void { $this->expectException(ConstExprEvaluationException::class); $this->expectExceptionMessage('Expression of type Expr_Variable cannot be evaluated'); $evaluator = new ConstExprEvaluator(); - $evaluator->evaluateDirectly(new Expr\Variable('a')); + $evaluator->evaluateDirectly(new Expr\Variable('a')); } + public function testEvaluateStaticCallOutsideWhitelistFails(): void { + $this->expectException(ConstExprEvaluationException::class); + $this->expectExceptionMessage('Expression of type Expr_StaticCall cannot be evaluated'); + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $exprString = "PhpParser\EvaluationFakeClass::GetStaticValue()"; + $expr = $parser->parse('expr; + $evaluator = new ConstExprEvaluator(); + $evaluator->evaluateDirectly($expr); + } + + public function testEvaluateFunCallOutsideWhitelistFails(): void { + $this->expectException(ConstExprEvaluationException::class); + $this->expectExceptionMessage('Expression of type Expr_FuncCall cannot be evaluated'); + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $exprString = 'class_exists("PhpParser\EvaluationFakeClass")'; + $expr = $parser->parse('expr; + $evaluator = new ConstExprEvaluator(); + $evaluator->evaluateDirectly($expr); + } + public function testEvaluateFallback(): void { $evaluator = new ConstExprEvaluator(function (Expr $expr) { if ($expr instanceof Scalar\MagicConst\Line) { @@ -133,3 +214,26 @@ public static function provideTestEvaluateSilently() { ]; } } + +class EvaluationFakeClass { + public static $STATICPROPERTY_4TEST = 123; + const CONST_4TEST = 456; + + public string $iIsOk = "IsOkValue"; + + public static function GetStaticValue(){ + return "shadok"; + } + + public function GetName() { + return "gabuzomeu"; + } + + public function GetLongName($suffix) { + return "gabuzomeu_".$suffix; + } + + public function __toString(): string { + return "toString"; + } +} From 8ffc1239ff48ed2476b2672dbcc939fcdc5b0f7a Mon Sep 17 00:00:00 2001 From: odain Date: Tue, 9 Sep 2025 11:14:16 +0200 Subject: [PATCH 2/4] code cleanup: fix formatting/php doc --- lib/PhpParser/ConstExprEvaluator.php | 85 +++++++++++------------ test/PhpParser/ConstExprEvaluatorTest.php | 2 +- 2 files changed, 40 insertions(+), 47 deletions(-) diff --git a/lib/PhpParser/ConstExprEvaluator.php b/lib/PhpParser/ConstExprEvaluator.php index 74fcc2fb89..457bddc860 100644 --- a/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/PhpParser/ConstExprEvaluator.php @@ -58,13 +58,11 @@ public function __construct(?callable $fallbackEvaluator = null) { $this->staticCallsWhitelist = []; } - public function setFunctionsWhitelist(array $functionsWhiteList): void - { + public function setFunctionsWhitelist(array $functionsWhiteList): void { $this->functionsWhiteList = $functionsWhiteList; } - public function setStaticCallsWhitelist(array $staticCallsWhitelist): void - { + public function setStaticCallsWhitelist(array $staticCallsWhitelist): void { $this->staticCallsWhitelist = $staticCallsWhitelist; } @@ -202,6 +200,7 @@ private function evaluate(Expr $expr) { if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { return $this->evaluateMethodCall($expr); } + return ($this->fallbackEvaluator)($expr); } @@ -235,11 +234,12 @@ private function evaluateBinaryOp(Expr\BinaryOp $expr) { if ($expr instanceof Expr\BinaryOp\Coalesce) { try { $var = $this->evaluate($expr->left); - return $var ?? $this->evaluate($expr->right); - } catch(\Throwable $t){ - //handle when isset($expr->left->var)===false + } catch(\Throwable $t) { + //left expression cannot be evaluated (! isset for exeample) return $this->evaluate($expr->right); } + + return $var ?? $this->evaluate($expr->right); } // The evaluate() calls are repeated in each branch, because some of the operators are @@ -286,31 +286,31 @@ private function evaluateBinaryOp(Expr\BinaryOp $expr) { private function evaluateConstFetch(Expr\ConstFetch $expr) { try { $name = $expr->name; - if(! is_string($name)){ + if(! is_string($name)) { //PHP_VERSION_ID usecase $name = $name->name; } - if (defined($name)){ + if (defined($name)) { return constant($name); } - } catch(\Throwable $t){} + } catch(\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } - /** @return mixed */ + /** @return bool */ private function evaluateIsset(Expr\Isset_ $expr) { try { - foreach ($expr->vars as $var){ + foreach ($expr->vars as $var) { $var = $this->evaluate($var); - if (! isset($var)){ + if (! isset($var)) { return false; } } return true; - } catch(\Throwable $t){ + } catch(\Throwable $t) { return false; } } @@ -321,20 +321,20 @@ private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { $classname = $expr->class->name; $property = $expr->name->name; - if ('class' === $property){ + if ('class' === $property) { return $classname; } - if (class_exists($classname)){ + if (class_exists($classname)) { $class = new \ReflectionClass($classname); if (array_key_exists($property, $class->getConstants())) { $oReflectionConstant = $class->getReflectionConstant($property); - if ($oReflectionConstant->isPublic()){ + if ($oReflectionConstant->isPublic()) { return $class->getConstant($property); } } } - } catch(\Throwable $t){} + } catch(\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } @@ -344,7 +344,7 @@ private function evaluateCast(Expr\Cast $expr) { try { $subexpr = $this->evaluate($expr->expr); $type = get_class($expr); - switch ($type){ + switch ($type) { case Expr\Cast\Array_::class: return (array) $subexpr; @@ -352,7 +352,7 @@ private function evaluateCast(Expr\Cast $expr) { return (bool) $subexpr; case Expr\Cast\Double::class: - switch ($expr->getAttribute("kind")){ + switch ($expr->getAttribute("kind")) { case Expr\Cast\Double::KIND_DOUBLE: return (double) $subexpr; @@ -372,28 +372,26 @@ private function evaluateCast(Expr\Cast $expr) { case Expr\Cast\String_::class: return (string) $subexpr; } - } catch(\Throwable $t){ - } + } catch(\Throwable $t) {} return ($this->fallbackEvaluator)($expr); } /** @return mixed */ - private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) - { + private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) { try { $classname = $expr->class->name; - if ($expr->name instanceof Identifier){ + if ($expr->name instanceof Identifier) { $property = $expr->name->name; } else { $property = $this->evaluate($expr->name); } - if (class_exists($classname)){ + if (class_exists($classname)) { $class = new \ReflectionClass($classname); if (array_key_exists($property, $class->getStaticProperties())) { $oReflectionProperty = $class->getProperty($property); - if ($oReflectionProperty->isPublic()){ + if ($oReflectionProperty->isPublic()) { return $class->getStaticPropertyValue($property); } } @@ -405,22 +403,21 @@ private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) } /** @return mixed */ - private function evaluateFuncCall(Expr\FuncCall $expr) - { + private function evaluateFuncCall(Expr\FuncCall $expr) { try { $name = $expr->name; - if ($name instanceof Name){ + if ($name instanceof Name) { $function = $name->name; } else { $function = $this->evaluate($name); } - if (! in_array($function, $this->functionsWhiteList)){ + if (! in_array($function, $this->functionsWhiteList)) { throw new Exception("FuncCall $function not supported"); } $args=[]; - foreach ($expr->args as $arg){ + foreach ($expr->args as $arg) { /** @var \PhpParser\Node\Arg $arg */ $args[]=$arg->value->value; } @@ -434,8 +431,7 @@ private function evaluateFuncCall(Expr\FuncCall $expr) } /** @return mixed */ - private function evaluateVariable(Expr\Variable $expr) - { + private function evaluateVariable(Expr\Variable $expr) { try { $name = $expr->name; if (array_key_exists($name, get_defined_vars())) { @@ -453,30 +449,29 @@ private function evaluateVariable(Expr\Variable $expr) } /** @return mixed */ - private function evaluateStaticCall(Expr\StaticCall $expr) - { + private function evaluateStaticCall(Expr\StaticCall $expr) { try { $class = $expr->class->name; - if ($expr->name instanceof Identifier){ + if ($expr->name instanceof Identifier) { $method = $expr->name->name; } else { $method = $this->evaluate($expr->name); } $static_call_description = "$class::$method"; - if (! in_array($static_call_description, $this->staticCallsWhitelist)){ + if (! in_array($static_call_description, $this->staticCallsWhitelist)) { throw new Exception("StaticCall $static_call_description not supported"); } $args=[]; - foreach ($expr->args as $arg){ + foreach ($expr->args as $arg) { /** @var \PhpParser\Node\Arg $arg */ $args[]=$arg->value->value; } $class = new \ReflectionClass($class); $method = $class->getMethod($method); - if ($method->isPublic()){ + if ($method->isPublic()) { return $method->invokeArgs(null, $args); } } catch (\Throwable $t) {} @@ -489,8 +484,7 @@ private function evaluateStaticCall(Expr\StaticCall $expr) * * @return mixed */ - private function evaluatePropertyFetch($expr) - { + private function evaluatePropertyFetch($expr) { try { $var = $this->evaluate($expr->var); } catch (\Throwable $t) { @@ -512,7 +506,7 @@ private function evaluatePropertyFetch($expr) } } catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafePropertyFetch){ + } else if ($expr instanceof Expr\NullsafePropertyFetch) { return null; } @@ -524,8 +518,7 @@ private function evaluatePropertyFetch($expr) * * @return mixed */ - private function evaluateMethodCall($expr) - { + private function evaluateMethodCall($expr) { try { $var = $this->evaluate($expr->var); } catch (\Throwable $t) { @@ -553,7 +546,7 @@ private function evaluateMethodCall($expr) } } catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafeMethodCall){ + } else if ($expr instanceof Expr\NullsafeMethodCall) { return null; } diff --git a/test/PhpParser/ConstExprEvaluatorTest.php b/test/PhpParser/ConstExprEvaluatorTest.php index c05fd3ea69..2fdaf3ad20 100644 --- a/test/PhpParser/ConstExprEvaluatorTest.php +++ b/test/PhpParser/ConstExprEvaluatorTest.php @@ -152,7 +152,7 @@ public function testEvaluateStaticCallOutsideWhitelistFails(): void { $evaluator->evaluateDirectly($expr); } - public function testEvaluateFunCallOutsideWhitelistFails(): void { + public function testEvaluateFuncCallOutsideWhitelistFails(): void { $this->expectException(ConstExprEvaluationException::class); $this->expectExceptionMessage('Expression of type Expr_FuncCall cannot be evaluated'); From f5f324a02b1654ff1596041b5973b7fc38a9824e Mon Sep 17 00:00:00 2001 From: odain Date: Thu, 18 Sep 2025 10:34:11 +0200 Subject: [PATCH 3/4] move non-constant evaluation in ExprEvaluator dedicated class --- lib/PhpParser/ConstExprEvaluator.php | 269 +--------------- lib/PhpParser/ExprEvaluationException.php | 6 + lib/PhpParser/ExprEvaluator.php | 366 ++++++++++++++++++++++ test/PhpParser/ConstExprEvaluatorTest.php | 104 +----- test/PhpParser/ExprEvaluatorTest.php | 202 ++++++++++++ 5 files changed, 592 insertions(+), 355 deletions(-) create mode 100644 lib/PhpParser/ExprEvaluationException.php create mode 100644 lib/PhpParser/ExprEvaluator.php create mode 100644 test/PhpParser/ExprEvaluatorTest.php diff --git a/lib/PhpParser/ConstExprEvaluator.php b/lib/PhpParser/ConstExprEvaluator.php index 457bddc860..381abaed9c 100644 --- a/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/PhpParser/ConstExprEvaluator.php @@ -3,10 +3,7 @@ namespace PhpParser; use PhpParser\Node\Expr; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; use PhpParser\Node\Scalar; -use Exception; use function array_merge; @@ -20,8 +17,6 @@ * 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. * @@ -31,13 +26,7 @@ */ class ConstExprEvaluator { /** @var callable|null */ - private $fallbackEvaluator; - - /** @var array $functionsWhiteList */ - private $functionsWhiteList; - - /** @var array $staticCallsWhitelist */ - private $staticCallsWhitelist; + protected $fallbackEvaluator; /** * Create a constant expression evaluator. @@ -53,17 +42,6 @@ public function __construct(?callable $fallbackEvaluator = null) { "Expression of type {$expr->getType()} cannot be evaluated" ); }; - - $this->functionsWhiteList = []; - $this->staticCallsWhitelist = []; - } - - public function setFunctionsWhitelist(array $functionsWhiteList): void { - $this->functionsWhiteList = $functionsWhiteList; - } - - public function setStaticCallsWhitelist(array $staticCallsWhitelist): void { - $this->staticCallsWhitelist = $staticCallsWhitelist; } /** @@ -123,7 +101,7 @@ public function evaluateDirectly(Expr $expr) { } /** @return mixed */ - private function evaluate(Expr $expr) { + protected function evaluate(Expr $expr) { if ($expr instanceof Scalar\Int_ || $expr instanceof Scalar\Float_ || $expr instanceof Scalar\String_ @@ -135,10 +113,6 @@ private function evaluate(Expr $expr) { return $this->evaluateArray($expr); } - if ($expr instanceof Expr\Variable) { - return $this->evaluateVariable($expr); - } - // Unary operators if ($expr instanceof Expr\UnaryPlus) { return +$this->evaluate($expr->expr); @@ -169,10 +143,6 @@ private function evaluate(Expr $expr) { return $this->evaluateConstFetch($expr); } - if ($expr instanceof Expr\Isset_) { - return $this->evaluateIsset($expr); - } - if ($expr instanceof Expr\ClassConstFetch) { return $this->evaluateClassConstFetch($expr); } @@ -181,26 +151,6 @@ private function evaluate(Expr $expr) { return $this->evaluateCast($expr); } - if ($expr instanceof Expr\StaticPropertyFetch) { - return $this->evaluateStaticPropertyFetch($expr); - } - - if ($expr instanceof Expr\FuncCall) { - return $this->evaluateFuncCall($expr); - } - - if ($expr instanceof Expr\StaticCall) { - return $this->evaluateStaticCall($expr); - } - - if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) { - return $this->evaluatePropertyFetch($expr); - } - - if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { - return $this->evaluateMethodCall($expr); - } - return ($this->fallbackEvaluator)($expr); } @@ -231,15 +181,12 @@ private function evaluateTernary(Expr\Ternary $expr) { /** @return mixed */ private function evaluateBinaryOp(Expr\BinaryOp $expr) { - if ($expr instanceof Expr\BinaryOp\Coalesce) { - try { - $var = $this->evaluate($expr->left); - } catch(\Throwable $t) { - //left expression cannot be evaluated (! isset for exeample) - return $this->evaluate($expr->right); - } - - return $var ?? $this->evaluate($expr->right); + 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 @@ -285,12 +232,8 @@ private function evaluateBinaryOp(Expr\BinaryOp $expr) { /** @return mixed */ private function evaluateConstFetch(Expr\ConstFetch $expr) { try { - $name = $expr->name; - if(! is_string($name)) { - //PHP_VERSION_ID usecase - $name = $name->name; - } - + $name = $expr->name->name; + if (defined($name)) { return constant($name); } @@ -299,22 +242,6 @@ private function evaluateConstFetch(Expr\ConstFetch $expr) { return ($this->fallbackEvaluator)($expr); } - /** @return bool */ - private function evaluateIsset(Expr\Isset_ $expr) { - try { - foreach ($expr->vars as $var) { - $var = $this->evaluate($var); - if (! isset($var)) { - return false; - } - } - - return true; - } catch(\Throwable $t) { - return false; - } - } - /** @return mixed */ private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { try { @@ -376,180 +303,4 @@ private function evaluateCast(Expr\Cast $expr) { return ($this->fallbackEvaluator)($expr); } - - /** @return mixed */ - private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) { - try { - $classname = $expr->class->name; - if ($expr->name instanceof Identifier) { - $property = $expr->name->name; - } else { - $property = $this->evaluate($expr->name); - } - - if (class_exists($classname)) { - $class = new \ReflectionClass($classname); - if (array_key_exists($property, $class->getStaticProperties())) { - $oReflectionProperty = $class->getProperty($property); - if ($oReflectionProperty->isPublic()) { - return $class->getStaticPropertyValue($property); - } - } - } - } - catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** @return mixed */ - private function evaluateFuncCall(Expr\FuncCall $expr) { - try { - $name = $expr->name; - if ($name instanceof Name) { - $function = $name->name; - } else { - $function = $this->evaluate($name); - } - - if (! in_array($function, $this->functionsWhiteList)) { - throw new Exception("FuncCall $function not supported"); - } - - $args=[]; - foreach ($expr->args as $arg) { - /** @var \PhpParser\Node\Arg $arg */ - $args[]=$arg->value->value; - } - - $reflection_function = new \ReflectionFunction($function); - return $reflection_function->invoke(...$args); - } - catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** @return mixed */ - private function evaluateVariable(Expr\Variable $expr) { - try { - $name = $expr->name; - if (array_key_exists($name, get_defined_vars())) { - return $$name; - } - - if (array_key_exists($name, $GLOBALS)) { - global $$name; - return $$name; - } - } catch (\Throwable $t) { - } - - return ($this->fallbackEvaluator)($expr); - } - - /** @return mixed */ - private function evaluateStaticCall(Expr\StaticCall $expr) { - try { - $class = $expr->class->name; - if ($expr->name instanceof Identifier) { - $method = $expr->name->name; - } else { - $method = $this->evaluate($expr->name); - } - - $static_call_description = "$class::$method"; - if (! in_array($static_call_description, $this->staticCallsWhitelist)) { - throw new Exception("StaticCall $static_call_description not supported"); - } - - $args=[]; - foreach ($expr->args as $arg) { - /** @var \PhpParser\Node\Arg $arg */ - $args[]=$arg->value->value; - } - - $class = new \ReflectionClass($class); - $method = $class->getMethod($method); - if ($method->isPublic()) { - return $method->invokeArgs(null, $args); - } - } catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** - * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr - * - * @return mixed - */ - private function evaluatePropertyFetch($expr) { - try { - $var = $this->evaluate($expr->var); - } catch (\Throwable $t) { - $var = null; - } - - if (! is_null($var)) { - try { - if ($expr->name instanceof Identifier) { - $name = $expr->name->name; - } else { - $name = $this->evaluate($expr->name); - } - - $reflectionClass = new \ReflectionClass(get_class($var)); - $property = $reflectionClass->getProperty($name); - if ($property->isPublic()) { - return $property->getValue($var); - } - } - catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafePropertyFetch) { - return null; - } - - return ($this->fallbackEvaluator)($expr); - } - - /** - * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr - * - * @return mixed - */ - private function evaluateMethodCall($expr) { - try { - $var = $this->evaluate($expr->var); - } catch (\Throwable $t) { - $var = null; - } - - if (! is_null($var)) { - try { - $args = []; - foreach ($expr->args as $arg) { - /** @var \PhpParser\Node\Arg $arg */ - $args[] = $arg->value->value; - } - - if ($expr->name instanceof Identifier) { - $name = $expr->name->name; - } else { - $name = $this->evaluate($expr->name); - } - - $reflectionClass = new \ReflectionClass(get_class($var)); - $method = $reflectionClass->getMethod($name); - if ($method->isPublic()) { - return $method->invokeArgs($var, $args); - } - } - catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafeMethodCall) { - return null; - } - - return ($this->fallbackEvaluator)($expr); - } } diff --git a/lib/PhpParser/ExprEvaluationException.php b/lib/PhpParser/ExprEvaluationException.php new file mode 100644 index 0000000000..25a6290bd1 --- /dev/null +++ b/lib/PhpParser/ExprEvaluationException.php @@ -0,0 +1,6 @@ + */ + private array $functionsWhiteList = []; + + /** @var array */ + private array $staticCallsWhitelist = []; + + /** + * 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) { + parent::__construct($fallbackEvaluator); + + $this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) { + throw new ExprEvaluationException( + "Expression of type {$expr->getType()} cannot be evaluated" + ); + }; + } + + /** + * @param array $functionsWhiteList + * + * @return void + */ + public function setFunctionsWhitelist(array $functionsWhiteList): void { + $this->functionsWhiteList = $functionsWhiteList; + } + + /** + * @param array $staticCallsWhitelist + * + * @return void + */ + public function setStaticCallsWhitelist(array $staticCallsWhitelist): void { + $this->staticCallsWhitelist = $staticCallsWhitelist; + } + + /** + * 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 + * ConstExprEvaluationException is thrown. + * + * See class doc comment for caveats and limitations. + * + * @param Expr $expr Constant expression to evaluate + * + * @return mixed Result of evaluation + * + * @throws ExprEvaluationException 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 ExprEvaluationException) { + $e = new ExprEvaluationException( + "An error occurred during 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 ExprEvaluationException if the expression cannot be evaluated + */ + public function evaluateDirectly(Expr $expr) { + return $this->evaluate($expr); + } + + /** @return mixed */ + protected function evaluate(Expr $expr) { + try { + return parent::evaluate($expr); + } catch (\Throwable $t) { + } + + if ($expr instanceof Expr\Variable) { + return $this->evaluateVariable($expr); + } + + if ($expr instanceof Expr\BinaryOp\Coalesce) { + try { + $var = $this->evaluate($expr->left); + } catch(\Throwable $t) { + //left expression cannot be evaluated (! isset for exeample) + return $this->evaluate($expr->right); + } + + return $var ?? $this->evaluate($expr->right); + } + + if ($expr instanceof Expr\Isset_) { + return $this->evaluateIsset($expr); + } + + if ($expr instanceof Expr\StaticPropertyFetch) { + return $this->evaluateStaticPropertyFetch($expr); + } + + if ($expr instanceof Expr\FuncCall) { + return $this->evaluateFuncCall($expr); + } + + if ($expr instanceof Expr\StaticCall) { + return $this->evaluateStaticCall($expr); + } + + if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) { + return $this->evaluatePropertyFetch($expr); + } + + if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { + return $this->evaluateMethodCall($expr); + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return bool */ + private function evaluateIsset(Expr\Isset_ $expr) { + try { + foreach ($expr->vars as $var) { + $var = $this->evaluate($var); + if (! isset($var)) { + return false; + } + } + + return true; + } catch(\Throwable $t) { + return false; + } + } + + /** @return mixed */ + private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) { + try { + $classname = $expr->class->name; + if ($expr->name instanceof Identifier) { + $property = $expr->name->name; + } else { + $property = $this->evaluate($expr->name); + } + + if (class_exists($classname)) { + $class = new \ReflectionClass($classname); + if (array_key_exists($property, $class->getStaticProperties())) { + $oReflectionProperty = $class->getProperty($property); + if ($oReflectionProperty->isPublic()) { + return $class->getStaticPropertyValue($property); + } + } + } + } + catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateFuncCall(Expr\FuncCall $expr) { + try { + $name = $expr->name; + if ($name instanceof Name) { + $function = $name->name; + } else { + $function = $this->evaluate($name); + } + + if (! in_array($function, $this->functionsWhiteList)) { + throw new Exception("FuncCall $function not supported"); + } + + $args=[]; + foreach ($expr->args as $arg) { + /** @var \PhpParser\Node\Arg $arg */ + $args[]= $this->evaluate($arg->value); + } + + $reflection_function = new \ReflectionFunction($function); + return $reflection_function->invoke(...$args); + } + catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateVariable(Expr\Variable $expr) { + try { + $name = $expr->name; + if (array_key_exists($name, get_defined_vars())) { + return $$name; + } + + if (array_key_exists($name, $GLOBALS)) { + global $$name; + return $$name; + } + } catch (\Throwable $t) { + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateStaticCall(Expr\StaticCall $expr) { + try { + $class = $expr->class->name; + if ($expr->name instanceof Identifier) { + $method = $expr->name->name; + } else { + $method = $this->evaluate($expr->name); + } + + $static_call_description = "$class::$method"; + if (! in_array($static_call_description, $this->staticCallsWhitelist)) { + throw new Exception("StaticCall $static_call_description not supported"); + } + + $args=[]; + foreach ($expr->args as $arg) { + /** @var \PhpParser\Node\Arg $arg */ + $args[]=$this->evaluate($arg->value); + } + + $class = new \ReflectionClass($class); + $method = $class->getMethod($method); + if ($method->isPublic()) { + return $method->invokeArgs(null, $args); + } + } catch (\Throwable $t) {} + + return ($this->fallbackEvaluator)($expr); + } + + /** + * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr + * + * @return mixed + */ + private function evaluatePropertyFetch($expr) { + try { + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } + + if (! is_null($var)) { + try { + if ($expr->name instanceof Identifier) { + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + + $reflectionClass = new \ReflectionClass(get_class($var)); + $property = $reflectionClass->getProperty($name); + if ($property->isPublic()) { + return $property->getValue($var); + } + } + catch (\Throwable $t) {} + } else if ($expr instanceof Expr\NullsafePropertyFetch) { + return null; + } + + return ($this->fallbackEvaluator)($expr); + } + + /** + * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr + * + * @return mixed + */ + private function evaluateMethodCall($expr) { + try { + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } + + if (! is_null($var)) { + try { + $args = []; + foreach ($expr->args as $arg) { + /** @var \PhpParser\Node\Arg $arg */ + $args[] = $this->evaluate($arg->value); + } + + if ($expr->name instanceof Identifier) { + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + + $reflectionClass = new \ReflectionClass(get_class($var)); + $method = $reflectionClass->getMethod($name); + if ($method->isPublic()) { + return $method->invokeArgs($var, $args); + } + } + catch (\Throwable $t) {} + } else if ($expr instanceof Expr\NullsafeMethodCall) { + return null; + } + + return ($this->fallbackEvaluator)($expr); + } +} diff --git a/test/PhpParser/ConstExprEvaluatorTest.php b/test/PhpParser/ConstExprEvaluatorTest.php index 2fdaf3ad20..90db318a83 100644 --- a/test/PhpParser/ConstExprEvaluatorTest.php +++ b/test/PhpParser/ConstExprEvaluatorTest.php @@ -8,29 +8,10 @@ class ConstExprEvaluatorTest extends \PHPUnit\Framework\TestCase { /** @dataProvider provideTestEvaluate */ public function testEvaluate($exprString, $expected): void { - global $globalNotDeclaredVar; - - global $globalNonNullVar; - $globalNonNullVar="a"; - - global $globalArray; - $globalArray=["gabu" => "zomeu"]; - - global $globalNullVar; - $globalNullVar=null; - - global $globalEvaluationFakeClass; - $globalEvaluationFakeClass = new EvaluationFakeClass(); - - $oNonNullVar2="a"; - $oNullVar2=null; - - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $expr = $parser->parse('expr; - $evaluator = new ConstExprEvaluator(); - $evaluator->setStaticCallsWhitelist(["PhpParser\EvaluationFakeClass::GetStaticValue"]); - $evaluator->setFunctionsWhitelist(["class_exists"]); - $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $expr = $parser->parse('expr; + $evaluator = new ConstExprEvaluator(); + $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); } public static function provideTestEvaluate() { @@ -94,43 +75,15 @@ public static function provideTestEvaluate() { ['true xor false', true], ['"foo" |> "strlen"', 3], - //Variable - ['$globalNonNullVar', "a"], - ['$globalNotDeclaredVar', null], - ['$globalArray', ["gabu" => "zomeu"]], - ['$globalNullVar', null], - ['$globalNonNullVar', "a"], - //Isset - ['isset($globalNotDeclaredVar)', false], - ['isset($globalNonNullVar)', true], - ['isset($globalArray)', true], - ['isset($globalNullVar)', false], - ['isset($globalNonNullVar)', true], - ['isset($oNonNullVar)', false], - ['isset($oNullVar)', false], - ['isset($eee)', false], //Cast ['(int)true', 1], ['(string)1', "1"], ['(bool)1', true], ['(double)1', 1.0], ['(float)1', 1.0], - ['(string) $globalEvaluationFakeClass', "toString"], - ['PhpParser\EvaluationFakeClass::CONST_4TEST', 456], + + ['PhpParser\ConstEvaluationFakeClass::CONST_4TEST', 456], ['UnexistingClass::class', "UnexistingClass"], - ['PhpParser\EvaluationFakeClass::$STATICPROPERTY_4TEST', 123], - ['PhpParser\EvaluationFakeClass::GetStaticValue()', "shadok"], - ['class_exists("PhpParser\EvaluationFakeClass")', true], - ['$globalEvaluationFakeClass->iIsOk', 'IsOkValue'], - ['$globalNullVar?->iIsOk', null], - ['$globalEvaluationFakeClass->GetName()', 'gabuzomeu'], - ['$globalNullVar?->GetName()', null], - ['$globalEvaluationFakeClass->GetLongName("aa")', 'gabuzomeu_aa'], - ['$globalNullVar??1', 1], - ['$globalNotDeclaredVar??1', 1], - ['$globalNotDeclaredVar["a"]??1', 1], - ['$globalArray["gabu"]??1', "zomeu"], - ['$globalNonNullVar??1', "a"], ]; } @@ -138,31 +91,9 @@ public function testEvaluateFails(): void { $this->expectException(ConstExprEvaluationException::class); $this->expectExceptionMessage('Expression of type Expr_Variable cannot be evaluated'); $evaluator = new ConstExprEvaluator(); - $evaluator->evaluateDirectly(new Expr\Variable('a')); + $evaluator->evaluateDirectly(new Expr\Variable('a')); } - public function testEvaluateStaticCallOutsideWhitelistFails(): void { - $this->expectException(ConstExprEvaluationException::class); - $this->expectExceptionMessage('Expression of type Expr_StaticCall cannot be evaluated'); - - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $exprString = "PhpParser\EvaluationFakeClass::GetStaticValue()"; - $expr = $parser->parse('expr; - $evaluator = new ConstExprEvaluator(); - $evaluator->evaluateDirectly($expr); - } - - public function testEvaluateFuncCallOutsideWhitelistFails(): void { - $this->expectException(ConstExprEvaluationException::class); - $this->expectExceptionMessage('Expression of type Expr_FuncCall cannot be evaluated'); - - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $exprString = 'class_exists("PhpParser\EvaluationFakeClass")'; - $expr = $parser->parse('expr; - $evaluator = new ConstExprEvaluator(); - $evaluator->evaluateDirectly($expr); - } - public function testEvaluateFallback(): void { $evaluator = new ConstExprEvaluator(function (Expr $expr) { if ($expr instanceof Scalar\MagicConst\Line) { @@ -215,25 +146,6 @@ public static function provideTestEvaluateSilently() { } } -class EvaluationFakeClass { - public static $STATICPROPERTY_4TEST = 123; +class ConstEvaluationFakeClass { const CONST_4TEST = 456; - - public string $iIsOk = "IsOkValue"; - - public static function GetStaticValue(){ - return "shadok"; - } - - public function GetName() { - return "gabuzomeu"; - } - - public function GetLongName($suffix) { - return "gabuzomeu_".$suffix; - } - - public function __toString(): string { - return "toString"; - } } diff --git a/test/PhpParser/ExprEvaluatorTest.php b/test/PhpParser/ExprEvaluatorTest.php new file mode 100644 index 0000000000..7120d5c6c7 --- /dev/null +++ b/test/PhpParser/ExprEvaluatorTest.php @@ -0,0 +1,202 @@ + "zomeu"]; + + global $globalNullVar; + $globalNullVar=null; + + global $globalEvaluationFakeClass; + $globalEvaluationFakeClass = new EvaluationFakeClass(); + + $oNonNullVar2="a"; + $oNullVar2=null; + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $expr = $parser->parse('expr; + $evaluator = new ExprEvaluator(); + $evaluator->setStaticCallsWhitelist(["PhpParser\EvaluationFakeClass::GetStaticValue"]); + $evaluator->setFunctionsWhitelist(["class_exists"]); + $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); + } + + public static function provideTestEvaluate() { + return [ + ['1', 1], + ['1.0', 1.0], + ['"foo"', "foo"], + ['[0, 1]', [0, 1]], + ['["foo" => "bar"]', ["foo" => "bar"]], + ['[...["bar"]]', ["bar"]], + ['[...["foo" => "bar"]]', ["foo" => "bar"]], + ['["a", "b" => "b", ...["b" => "bb", "c"]]', ["a", "b" => "bb", "c"]], + ['NULL', null], + ['False', false], + ['True', true], + ['true', true], + ['PHP_VERSION_ID', PHP_VERSION_ID], + ['+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], + ['"foo" |> "strlen"', 3], + + //Variable + ['$globalNonNullVar', "a"], + ['$globalNotDeclaredVar', null], + ['$globalArray', ["gabu" => "zomeu"]], + ['$globalNullVar', null], + ['$globalNonNullVar', "a"], + //Isset + ['isset($globalNotDeclaredVar)', false], + ['isset($globalNonNullVar)', true], + ['isset($globalArray)', true], + ['isset($globalNullVar)', false], + ['isset($globalNonNullVar)', true], + ['isset($oNonNullVar)', false], + ['isset($oNullVar)', false], + ['isset($eee)', false], + //Cast + ['(int)true', 1], + ['(string)1', "1"], + ['(bool)1', true], + ['(double)1', 1.0], + ['(float)1', 1.0], + ['(string) $globalEvaluationFakeClass', "toString"], + ['PhpParser\EvaluationFakeClass::CONST_4TEST', 456], + ['UnexistingClass::class', "UnexistingClass"], + ['PhpParser\EvaluationFakeClass::$STATICPROPERTY_4TEST', 123], + ['PhpParser\EvaluationFakeClass::GetStaticValue()', "shadok"], + ['class_exists("PhpParser\EvaluationFakeClass")', true], + ['$globalEvaluationFakeClass->iIsOk', 'IsOkValue'], + ['$globalNullVar?->iIsOk', null], + ['$globalEvaluationFakeClass->GetName()', 'gabuzomeu'], + ['$globalNullVar?->GetName()', null], + ['$globalEvaluationFakeClass->GetLongName("aa")', 'gabuzomeu_aa'], + ['$globalNullVar??1', 1], + ['$globalNotDeclaredVar??1', 1], + ['$globalNotDeclaredVar["a"]??1', 1], + ['$globalArray["gabu"]??1', "zomeu"], + ['$globalNonNullVar??1', "a"], + ]; + } + + public function testEvaluateFails(): void { + $this->expectException(ExprEvaluationException::class); + $this->expectExceptionMessage('Expression of type Expr_Variable cannot be evaluated'); + $evaluator = new ExprEvaluator(); + $evaluator->evaluateDirectly(new Expr\Variable('a')); + } + + public function testEvaluateStaticCallOutsideWhitelistFails(): void { + $this->expectException(ExprEvaluationException::class); + $this->expectExceptionMessage('Expression of type Expr_StaticCall cannot be evaluated'); + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $exprString = "PhpParser\EvaluationFakeClass::GetStaticValue()"; + $expr = $parser->parse('expr; + $evaluator = new ExprEvaluator(); + $evaluator->evaluateDirectly($expr); + } + + public function testEvaluateFuncCallOutsideWhitelistFails(): void { + $this->expectException(ExprEvaluationException::class); + $this->expectExceptionMessage('Expression of type Expr_FuncCall cannot be evaluated'); + + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $exprString = 'class_exists("PhpParser\EvaluationFakeClass")'; + $expr = $parser->parse('expr; + $evaluator = new ExprEvaluator(); + $evaluator->evaluateDirectly($expr); + } + + public function testEvaluateFallback(): void { + $evaluator = new ExprEvaluator(function (Expr $expr) { + if ($expr instanceof Scalar\MagicConst\Line) { + return 42; + } + throw new ExprEvaluationException(); + }); + $expr = new Expr\BinaryOp\Plus( + new Scalar\Int_(8), + new Scalar\MagicConst\Line() + ); + $this->assertSame(50, $evaluator->evaluateDirectly($expr)); + } +} + +class EvaluationFakeClass { + public static $STATICPROPERTY_4TEST = 123; + const CONST_4TEST = 456; + + public string $iIsOk = "IsOkValue"; + + public static function GetStaticValue(){ + return "shadok"; + } + + public function GetName() { + return "gabuzomeu"; + } + + public function GetLongName($suffix) { + return "gabuzomeu_".$suffix; + } + + public function __toString(): string { + return "toString"; + } +} From b2cd0735eb27788d5d41fa3c2cfaa01a593fd7fb Mon Sep 17 00:00:00 2001 From: odain Date: Thu, 18 Sep 2025 14:29:15 +0200 Subject: [PATCH 4/4] php-cs-fixer cleanup --- lib/PhpParser/ConstExprEvaluator.php | 151 +++---- lib/PhpParser/ExprEvaluator.php | 509 +++++++++++----------- test/PhpParser/ConstExprEvaluatorTest.php | 18 +- test/PhpParser/ExprEvaluatorTest.php | 176 ++++---- 4 files changed, 427 insertions(+), 427 deletions(-) diff --git a/lib/PhpParser/ConstExprEvaluator.php b/lib/PhpParser/ConstExprEvaluator.php index 381abaed9c..b10578088f 100644 --- a/lib/PhpParser/ConstExprEvaluator.php +++ b/lib/PhpParser/ConstExprEvaluator.php @@ -143,13 +143,13 @@ protected function evaluate(Expr $expr) { return $this->evaluateConstFetch($expr); } - if ($expr instanceof Expr\ClassConstFetch) { - return $this->evaluateClassConstFetch($expr); - } + if ($expr instanceof Expr\ClassConstFetch) { + return $this->evaluateClassConstFetch($expr); + } - if ($expr instanceof Expr\Cast) { - return $this->evaluateCast($expr); - } + if ($expr instanceof Expr\Cast) { + return $this->evaluateCast($expr); + } return ($this->fallbackEvaluator)($expr); } @@ -231,76 +231,79 @@ private function evaluateBinaryOp(Expr\BinaryOp $expr) { /** @return mixed */ private function evaluateConstFetch(Expr\ConstFetch $expr) { - try { - $name = $expr->name->name; - - if (defined($name)) { - return constant($name); - } - } catch(\Throwable $t) {} + try { + $name = $expr->name->name; + + if (defined($name)) { + return constant($name); + } + } catch (\Throwable $t) { + } return ($this->fallbackEvaluator)($expr); } - /** @return mixed */ - private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { - try { - $classname = $expr->class->name; - $property = $expr->name->name; - - if ('class' === $property) { - return $classname; - } - - if (class_exists($classname)) { - $class = new \ReflectionClass($classname); - if (array_key_exists($property, $class->getConstants())) { - $oReflectionConstant = $class->getReflectionConstant($property); - if ($oReflectionConstant->isPublic()) { - return $class->getConstant($property); - } - } - } - } catch(\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** @return mixed */ - private function evaluateCast(Expr\Cast $expr) { - try { - $subexpr = $this->evaluate($expr->expr); - $type = get_class($expr); - switch ($type) { - case Expr\Cast\Array_::class: - return (array) $subexpr; - - case Expr\Cast\Bool_::class: - return (bool) $subexpr; - - case Expr\Cast\Double::class: - switch ($expr->getAttribute("kind")) { - case Expr\Cast\Double::KIND_DOUBLE: - return (double) $subexpr; - - case Expr\Cast\Double::KIND_FLOAT: - case Expr\Cast\Double::KIND_REAL: - return (float) $subexpr; - } - - break; - - case Expr\Cast\Int_::class: - return (int) $subexpr; - - case Expr\Cast\Object_::class: - return (object) $subexpr; - - case Expr\Cast\String_::class: - return (string) $subexpr; - } - } catch(\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } + /** @return mixed */ + private function evaluateClassConstFetch(Expr\ClassConstFetch $expr) { + try { + $classname = $expr->class->name; + $property = $expr->name->name; + + if ('class' === $property) { + return $classname; + } + + if (class_exists($classname)) { + $class = new \ReflectionClass($classname); + if (array_key_exists($property, $class->getConstants())) { + $oReflectionConstant = $class->getReflectionConstant($property); + if ($oReflectionConstant->isPublic()) { + return $class->getConstant($property); + } + } + } + } catch (\Throwable $t) { + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateCast(Expr\Cast $expr) { + try { + $subexpr = $this->evaluate($expr->expr); + $type = get_class($expr); + switch ($type) { + case Expr\Cast\Array_::class: + return (array) $subexpr; + + case Expr\Cast\Bool_::class: + return (bool) $subexpr; + + case Expr\Cast\Double::class: + switch ($expr->getAttribute("kind")) { + case Expr\Cast\Double::KIND_DOUBLE: + return (float) $subexpr; + + case Expr\Cast\Double::KIND_FLOAT: + case Expr\Cast\Double::KIND_REAL: + return (float) $subexpr; + } + + break; + + case Expr\Cast\Int_::class: + return (int) $subexpr; + + case Expr\Cast\Object_::class: + return (object) $subexpr; + + case Expr\Cast\String_::class: + return (string) $subexpr; + } + } catch (\Throwable $t) { + } + + return ($this->fallbackEvaluator)($expr); + } } diff --git a/lib/PhpParser/ExprEvaluator.php b/lib/PhpParser/ExprEvaluator.php index 186e8db6ef..20ef7ae84d 100644 --- a/lib/PhpParser/ExprEvaluator.php +++ b/lib/PhpParser/ExprEvaluator.php @@ -21,13 +21,13 @@ */ class ExprEvaluator extends ConstExprEvaluator { /** @var callable|null */ - protected $fallbackEvaluator; + protected $fallbackEvaluator; - /** @var array */ + /** @var array */ private array $functionsWhiteList = []; - /** @var array */ - private array $staticCallsWhitelist = []; + /** @var array */ + private array $staticCallsWhitelist = []; /** * Create a constant expression evaluator. @@ -38,31 +38,27 @@ class ExprEvaluator extends ConstExprEvaluator { * @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated */ public function __construct(?callable $fallbackEvaluator = null) { - parent::__construct($fallbackEvaluator); + parent::__construct($fallbackEvaluator); - $this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) { + $this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) { throw new ExprEvaluationException( "Expression of type {$expr->getType()} cannot be evaluated" ); }; } - /** - * @param array $functionsWhiteList - * - * @return void - */ - public function setFunctionsWhitelist(array $functionsWhiteList): void { - $this->functionsWhiteList = $functionsWhiteList; - } - - /** - * @param array $staticCallsWhitelist - * - * @return void - */ - public function setStaticCallsWhitelist(array $staticCallsWhitelist): void { - $this->staticCallsWhitelist = $staticCallsWhitelist; + /** + * @param array $functionsWhiteList + */ + public function setFunctionsWhitelist(array $functionsWhiteList): void { + $this->functionsWhiteList = $functionsWhiteList; + } + + /** + * @param array $staticCallsWhitelist + */ + public function setStaticCallsWhitelist(array $staticCallsWhitelist): void { + $this->staticCallsWhitelist = $staticCallsWhitelist; } /** @@ -125,242 +121,243 @@ public function evaluateDirectly(Expr $expr) { /** @return mixed */ protected function evaluate(Expr $expr) { - try { - return parent::evaluate($expr); - } catch (\Throwable $t) { - } - - if ($expr instanceof Expr\Variable) { - return $this->evaluateVariable($expr); - } - - if ($expr instanceof Expr\BinaryOp\Coalesce) { - try { - $var = $this->evaluate($expr->left); - } catch(\Throwable $t) { - //left expression cannot be evaluated (! isset for exeample) - return $this->evaluate($expr->right); - } - - return $var ?? $this->evaluate($expr->right); - } - - if ($expr instanceof Expr\Isset_) { - return $this->evaluateIsset($expr); - } - - if ($expr instanceof Expr\StaticPropertyFetch) { - return $this->evaluateStaticPropertyFetch($expr); - } - - if ($expr instanceof Expr\FuncCall) { - return $this->evaluateFuncCall($expr); - } - - if ($expr instanceof Expr\StaticCall) { - return $this->evaluateStaticCall($expr); - } - - if ($expr instanceof Expr\NullsafePropertyFetch||$expr instanceof Expr\PropertyFetch) { - return $this->evaluatePropertyFetch($expr); - } - - if ($expr instanceof Expr\NullsafeMethodCall||$expr instanceof Expr\MethodCall) { - return $this->evaluateMethodCall($expr); - } + try { + return parent::evaluate($expr); + } catch (\Throwable $t) { + } + + if ($expr instanceof Expr\Variable) { + return $this->evaluateVariable($expr); + } + + if ($expr instanceof Expr\BinaryOp\Coalesce) { + try { + $var = $this->evaluate($expr->left); + } catch (\Throwable $t) { + //left expression cannot be evaluated (! isset for exeample) + return $this->evaluate($expr->right); + } + + return $var ?? $this->evaluate($expr->right); + } + + if ($expr instanceof Expr\Isset_) { + return $this->evaluateIsset($expr); + } + + if ($expr instanceof Expr\StaticPropertyFetch) { + return $this->evaluateStaticPropertyFetch($expr); + } + + if ($expr instanceof Expr\FuncCall) { + return $this->evaluateFuncCall($expr); + } + + if ($expr instanceof Expr\StaticCall) { + return $this->evaluateStaticCall($expr); + } + + if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\PropertyFetch) { + return $this->evaluatePropertyFetch($expr); + } + + if ($expr instanceof Expr\NullsafeMethodCall || $expr instanceof Expr\MethodCall) { + return $this->evaluateMethodCall($expr); + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return bool */ + private function evaluateIsset(Expr\Isset_ $expr) { + try { + foreach ($expr->vars as $var) { + $var = $this->evaluate($var); + if (! isset($var)) { + return false; + } + } + + return true; + } catch (\Throwable $t) { + return false; + } + } + + /** @return mixed */ + private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) { + try { + $classname = $expr->class->name; + if ($expr->name instanceof Identifier) { + $property = $expr->name->name; + } else { + $property = $this->evaluate($expr->name); + } + + if (class_exists($classname)) { + $class = new \ReflectionClass($classname); + if (array_key_exists($property, $class->getStaticProperties())) { + $oReflectionProperty = $class->getProperty($property); + if ($oReflectionProperty->isPublic()) { + return $class->getStaticPropertyValue($property); + } + } + } + } catch (\Throwable $t) { + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateFuncCall(Expr\FuncCall $expr) { + try { + $name = $expr->name; + if ($name instanceof Name) { + $function = $name->name; + } else { + $function = $this->evaluate($name); + } + + if (! in_array($function, $this->functionsWhiteList)) { + throw new Exception("FuncCall $function not supported"); + } + + $args = []; + foreach ($expr->args as $arg) { + /** @var \PhpParser\Node\Arg $arg */ + $args[] = $this->evaluate($arg->value); + } + + $reflection_function = new \ReflectionFunction($function); + return $reflection_function->invoke(...$args); + } catch (\Throwable $t) { + } + + return ($this->fallbackEvaluator)($expr); + } + + /** @return mixed */ + private function evaluateVariable(Expr\Variable $expr) { + try { + $name = $expr->name; + if (array_key_exists($name, get_defined_vars())) { + return $$name; + } + + if (array_key_exists($name, $GLOBALS)) { + global $$name; + return $$name; + } + } catch (\Throwable $t) { + } return ($this->fallbackEvaluator)($expr); } - /** @return bool */ - private function evaluateIsset(Expr\Isset_ $expr) { - try { - foreach ($expr->vars as $var) { - $var = $this->evaluate($var); - if (! isset($var)) { - return false; - } - } - - return true; - } catch(\Throwable $t) { - return false; - } - } - - /** @return mixed */ - private function evaluateStaticPropertyFetch(Expr\StaticPropertyFetch $expr) { - try { - $classname = $expr->class->name; - if ($expr->name instanceof Identifier) { - $property = $expr->name->name; - } else { - $property = $this->evaluate($expr->name); - } - - if (class_exists($classname)) { - $class = new \ReflectionClass($classname); - if (array_key_exists($property, $class->getStaticProperties())) { - $oReflectionProperty = $class->getProperty($property); - if ($oReflectionProperty->isPublic()) { - return $class->getStaticPropertyValue($property); - } - } - } - } - catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** @return mixed */ - private function evaluateFuncCall(Expr\FuncCall $expr) { - try { - $name = $expr->name; - if ($name instanceof Name) { - $function = $name->name; - } else { - $function = $this->evaluate($name); - } - - if (! in_array($function, $this->functionsWhiteList)) { - throw new Exception("FuncCall $function not supported"); - } - - $args=[]; - foreach ($expr->args as $arg) { - /** @var \PhpParser\Node\Arg $arg */ - $args[]= $this->evaluate($arg->value); - } - - $reflection_function = new \ReflectionFunction($function); - return $reflection_function->invoke(...$args); - } - catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** @return mixed */ - private function evaluateVariable(Expr\Variable $expr) { - try { - $name = $expr->name; - if (array_key_exists($name, get_defined_vars())) { - return $$name; - } - - if (array_key_exists($name, $GLOBALS)) { - global $$name; - return $$name; - } - } catch (\Throwable $t) { - } - - return ($this->fallbackEvaluator)($expr); - } - - /** @return mixed */ - private function evaluateStaticCall(Expr\StaticCall $expr) { - try { - $class = $expr->class->name; - if ($expr->name instanceof Identifier) { - $method = $expr->name->name; - } else { - $method = $this->evaluate($expr->name); - } - - $static_call_description = "$class::$method"; - if (! in_array($static_call_description, $this->staticCallsWhitelist)) { - throw new Exception("StaticCall $static_call_description not supported"); - } - - $args=[]; - foreach ($expr->args as $arg) { - /** @var \PhpParser\Node\Arg $arg */ - $args[]=$this->evaluate($arg->value); - } - - $class = new \ReflectionClass($class); - $method = $class->getMethod($method); - if ($method->isPublic()) { - return $method->invokeArgs(null, $args); - } - } catch (\Throwable $t) {} - - return ($this->fallbackEvaluator)($expr); - } - - /** - * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr - * - * @return mixed - */ - private function evaluatePropertyFetch($expr) { - try { - $var = $this->evaluate($expr->var); - } catch (\Throwable $t) { - $var = null; - } - - if (! is_null($var)) { - try { - if ($expr->name instanceof Identifier) { - $name = $expr->name->name; - } else { - $name = $this->evaluate($expr->name); - } - - $reflectionClass = new \ReflectionClass(get_class($var)); - $property = $reflectionClass->getProperty($name); - if ($property->isPublic()) { - return $property->getValue($var); - } - } - catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafePropertyFetch) { - return null; - } - - return ($this->fallbackEvaluator)($expr); - } - - /** - * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr - * - * @return mixed - */ - private function evaluateMethodCall($expr) { - try { - $var = $this->evaluate($expr->var); - } catch (\Throwable $t) { - $var = null; - } - - if (! is_null($var)) { - try { - $args = []; - foreach ($expr->args as $arg) { - /** @var \PhpParser\Node\Arg $arg */ - $args[] = $this->evaluate($arg->value); - } - - if ($expr->name instanceof Identifier) { - $name = $expr->name->name; - } else { - $name = $this->evaluate($expr->name); - } - - $reflectionClass = new \ReflectionClass(get_class($var)); - $method = $reflectionClass->getMethod($name); - if ($method->isPublic()) { - return $method->invokeArgs($var, $args); - } - } - catch (\Throwable $t) {} - } else if ($expr instanceof Expr\NullsafeMethodCall) { - return null; - } - - return ($this->fallbackEvaluator)($expr); - } + /** @return mixed */ + private function evaluateStaticCall(Expr\StaticCall $expr) { + try { + $class = $expr->class->name; + if ($expr->name instanceof Identifier) { + $method = $expr->name->name; + } else { + $method = $this->evaluate($expr->name); + } + + $static_call_description = "$class::$method"; + if (! in_array($static_call_description, $this->staticCallsWhitelist)) { + throw new Exception("StaticCall $static_call_description not supported"); + } + + $args = []; + foreach ($expr->args as $arg) { + /** @var \PhpParser\Node\Arg $arg */ + $args[] = $this->evaluate($arg->value); + } + + $class = new \ReflectionClass($class); + $method = $class->getMethod($method); + if ($method->isPublic()) { + return $method->invokeArgs(null, $args); + } + } catch (\Throwable $t) { + } + + return ($this->fallbackEvaluator)($expr); + } + + /** + * @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr + * + * @return mixed + */ + private function evaluatePropertyFetch($expr) { + try { + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } + + if (! is_null($var)) { + try { + if ($expr->name instanceof Identifier) { + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + + $reflectionClass = new \ReflectionClass(get_class($var)); + $property = $reflectionClass->getProperty($name); + if ($property->isPublic()) { + return $property->getValue($var); + } + } catch (\Throwable $t) { + } + } elseif ($expr instanceof Expr\NullsafePropertyFetch) { + return null; + } + + return ($this->fallbackEvaluator)($expr); + } + + /** + * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr + * + * @return mixed + */ + private function evaluateMethodCall($expr) { + try { + $var = $this->evaluate($expr->var); + } catch (\Throwable $t) { + $var = null; + } + + if (! is_null($var)) { + try { + $args = []; + foreach ($expr->args as $arg) { + /** @var \PhpParser\Node\Arg $arg */ + $args[] = $this->evaluate($arg->value); + } + + if ($expr->name instanceof Identifier) { + $name = $expr->name->name; + } else { + $name = $this->evaluate($expr->name); + } + + $reflectionClass = new \ReflectionClass(get_class($var)); + $method = $reflectionClass->getMethod($name); + if ($method->isPublic()) { + return $method->invokeArgs($var, $args); + } + } catch (\Throwable $t) { + } + } elseif ($expr instanceof Expr\NullsafeMethodCall) { + return null; + } + + return ($this->fallbackEvaluator)($expr); + } } diff --git a/test/PhpParser/ConstExprEvaluatorTest.php b/test/PhpParser/ConstExprEvaluatorTest.php index 90db318a83..72d5a5c0b8 100644 --- a/test/PhpParser/ConstExprEvaluatorTest.php +++ b/test/PhpParser/ConstExprEvaluatorTest.php @@ -75,15 +75,15 @@ public static function provideTestEvaluate() { ['true xor false', true], ['"foo" |> "strlen"', 3], - //Cast - ['(int)true', 1], - ['(string)1', "1"], - ['(bool)1', true], - ['(double)1', 1.0], - ['(float)1', 1.0], + //Cast + ['(int)true', 1], + ['(string)1', "1"], + ['(bool)1', true], + ['(double)1', 1.0], + ['(float)1', 1.0], - ['PhpParser\ConstEvaluationFakeClass::CONST_4TEST', 456], - ['UnexistingClass::class', "UnexistingClass"], + ['PhpParser\ConstEvaluationFakeClass::CONST_4TEST', 456], + ['UnexistingClass::class', "UnexistingClass"], ]; } @@ -147,5 +147,5 @@ public static function provideTestEvaluateSilently() { } class ConstEvaluationFakeClass { - const CONST_4TEST = 456; + public const CONST_4TEST = 456; } diff --git a/test/PhpParser/ExprEvaluatorTest.php b/test/PhpParser/ExprEvaluatorTest.php index 7120d5c6c7..0ca515db9f 100644 --- a/test/PhpParser/ExprEvaluatorTest.php +++ b/test/PhpParser/ExprEvaluatorTest.php @@ -8,29 +8,29 @@ class ExprEvaluatorTest extends \PHPUnit\Framework\TestCase { /** @dataProvider provideTestEvaluate */ public function testEvaluate($exprString, $expected): void { - global $globalNotDeclaredVar; + global $globalNotDeclaredVar; - global $globalNonNullVar; - $globalNonNullVar="a"; + global $globalNonNullVar; + $globalNonNullVar = "a"; - global $globalArray; - $globalArray=["gabu" => "zomeu"]; + global $globalArray; + $globalArray = ["gabu" => "zomeu"]; - global $globalNullVar; - $globalNullVar=null; + global $globalNullVar; + $globalNullVar = null; - global $globalEvaluationFakeClass; - $globalEvaluationFakeClass = new EvaluationFakeClass(); + global $globalEvaluationFakeClass; + $globalEvaluationFakeClass = new EvaluationFakeClass(); - $oNonNullVar2="a"; - $oNullVar2=null; + $oNonNullVar2 = "a"; + $oNullVar2 = null; - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $expr = $parser->parse('expr; - $evaluator = new ExprEvaluator(); - $evaluator->setStaticCallsWhitelist(["PhpParser\EvaluationFakeClass::GetStaticValue"]); - $evaluator->setFunctionsWhitelist(["class_exists"]); - $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $expr = $parser->parse('expr; + $evaluator = new ExprEvaluator(); + $evaluator->setStaticCallsWhitelist(["PhpParser\EvaluationFakeClass::GetStaticValue"]); + $evaluator->setFunctionsWhitelist(["class_exists"]); + $this->assertSame($expected, $evaluator->evaluateDirectly($expr)); } public static function provideTestEvaluate() { @@ -94,43 +94,43 @@ public static function provideTestEvaluate() { ['true xor false', true], ['"foo" |> "strlen"', 3], - //Variable - ['$globalNonNullVar', "a"], - ['$globalNotDeclaredVar', null], - ['$globalArray', ["gabu" => "zomeu"]], - ['$globalNullVar', null], - ['$globalNonNullVar', "a"], - //Isset - ['isset($globalNotDeclaredVar)', false], - ['isset($globalNonNullVar)', true], - ['isset($globalArray)', true], - ['isset($globalNullVar)', false], - ['isset($globalNonNullVar)', true], - ['isset($oNonNullVar)', false], - ['isset($oNullVar)', false], - ['isset($eee)', false], - //Cast - ['(int)true', 1], - ['(string)1', "1"], - ['(bool)1', true], - ['(double)1', 1.0], - ['(float)1', 1.0], - ['(string) $globalEvaluationFakeClass', "toString"], - ['PhpParser\EvaluationFakeClass::CONST_4TEST', 456], - ['UnexistingClass::class', "UnexistingClass"], - ['PhpParser\EvaluationFakeClass::$STATICPROPERTY_4TEST', 123], - ['PhpParser\EvaluationFakeClass::GetStaticValue()', "shadok"], - ['class_exists("PhpParser\EvaluationFakeClass")', true], - ['$globalEvaluationFakeClass->iIsOk', 'IsOkValue'], - ['$globalNullVar?->iIsOk', null], - ['$globalEvaluationFakeClass->GetName()', 'gabuzomeu'], - ['$globalNullVar?->GetName()', null], - ['$globalEvaluationFakeClass->GetLongName("aa")', 'gabuzomeu_aa'], - ['$globalNullVar??1', 1], - ['$globalNotDeclaredVar??1', 1], - ['$globalNotDeclaredVar["a"]??1', 1], - ['$globalArray["gabu"]??1', "zomeu"], - ['$globalNonNullVar??1', "a"], + //Variable + ['$globalNonNullVar', "a"], + ['$globalNotDeclaredVar', null], + ['$globalArray', ["gabu" => "zomeu"]], + ['$globalNullVar', null], + ['$globalNonNullVar', "a"], + //Isset + ['isset($globalNotDeclaredVar)', false], + ['isset($globalNonNullVar)', true], + ['isset($globalArray)', true], + ['isset($globalNullVar)', false], + ['isset($globalNonNullVar)', true], + ['isset($oNonNullVar)', false], + ['isset($oNullVar)', false], + ['isset($eee)', false], + //Cast + ['(int)true', 1], + ['(string)1', "1"], + ['(bool)1', true], + ['(double)1', 1.0], + ['(float)1', 1.0], + ['(string) $globalEvaluationFakeClass', "toString"], + ['PhpParser\EvaluationFakeClass::CONST_4TEST', 456], + ['UnexistingClass::class', "UnexistingClass"], + ['PhpParser\EvaluationFakeClass::$STATICPROPERTY_4TEST', 123], + ['PhpParser\EvaluationFakeClass::GetStaticValue()', "shadok"], + ['class_exists("PhpParser\EvaluationFakeClass")', true], + ['$globalEvaluationFakeClass->iIsOk', 'IsOkValue'], + ['$globalNullVar?->iIsOk', null], + ['$globalEvaluationFakeClass->GetName()', 'gabuzomeu'], + ['$globalNullVar?->GetName()', null], + ['$globalEvaluationFakeClass->GetLongName("aa")', 'gabuzomeu_aa'], + ['$globalNullVar??1', 1], + ['$globalNotDeclaredVar??1', 1], + ['$globalNotDeclaredVar["a"]??1', 1], + ['$globalArray["gabu"]??1', "zomeu"], + ['$globalNonNullVar??1', "a"], ]; } @@ -138,30 +138,30 @@ public function testEvaluateFails(): void { $this->expectException(ExprEvaluationException::class); $this->expectExceptionMessage('Expression of type Expr_Variable cannot be evaluated'); $evaluator = new ExprEvaluator(); - $evaluator->evaluateDirectly(new Expr\Variable('a')); + $evaluator->evaluateDirectly(new Expr\Variable('a')); } - public function testEvaluateStaticCallOutsideWhitelistFails(): void { - $this->expectException(ExprEvaluationException::class); - $this->expectExceptionMessage('Expression of type Expr_StaticCall cannot be evaluated'); + public function testEvaluateStaticCallOutsideWhitelistFails(): void { + $this->expectException(ExprEvaluationException::class); + $this->expectExceptionMessage('Expression of type Expr_StaticCall cannot be evaluated'); - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $exprString = "PhpParser\EvaluationFakeClass::GetStaticValue()"; - $expr = $parser->parse('expr; - $evaluator = new ExprEvaluator(); - $evaluator->evaluateDirectly($expr); - } + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $exprString = "PhpParser\EvaluationFakeClass::GetStaticValue()"; + $expr = $parser->parse('expr; + $evaluator = new ExprEvaluator(); + $evaluator->evaluateDirectly($expr); + } - public function testEvaluateFuncCallOutsideWhitelistFails(): void { - $this->expectException(ExprEvaluationException::class); - $this->expectExceptionMessage('Expression of type Expr_FuncCall cannot be evaluated'); + public function testEvaluateFuncCallOutsideWhitelistFails(): void { + $this->expectException(ExprEvaluationException::class); + $this->expectExceptionMessage('Expression of type Expr_FuncCall cannot be evaluated'); - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $exprString = 'class_exists("PhpParser\EvaluationFakeClass")'; - $expr = $parser->parse('expr; - $evaluator = new ExprEvaluator(); - $evaluator->evaluateDirectly($expr); - } + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $exprString = 'class_exists("PhpParser\EvaluationFakeClass")'; + $expr = $parser->parse('expr; + $evaluator = new ExprEvaluator(); + $evaluator->evaluateDirectly($expr); + } public function testEvaluateFallback(): void { $evaluator = new ExprEvaluator(function (Expr $expr) { @@ -179,24 +179,24 @@ public function testEvaluateFallback(): void { } class EvaluationFakeClass { - public static $STATICPROPERTY_4TEST = 123; - const CONST_4TEST = 456; + public static $STATICPROPERTY_4TEST = 123; + public const CONST_4TEST = 456; - public string $iIsOk = "IsOkValue"; + public string $iIsOk = "IsOkValue"; - public static function GetStaticValue(){ - return "shadok"; - } + public static function GetStaticValue() { + return "shadok"; + } - public function GetName() { - return "gabuzomeu"; - } + public function GetName() { + return "gabuzomeu"; + } - public function GetLongName($suffix) { - return "gabuzomeu_".$suffix; - } + public function GetLongName($suffix) { + return "gabuzomeu_".$suffix; + } - public function __toString(): string { - return "toString"; - } + public function __toString(): string { + return "toString"; + } }