Evaluator fixes/enhancements + tests

This commit is contained in:
odain
2025-09-05 15:21:15 +02:00
parent 11f142b782
commit 7e7b5874a6
7 changed files with 164 additions and 93 deletions

View File

@@ -3,10 +3,6 @@
namespace PhpParser;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
@@ -37,13 +33,13 @@ class ConstExprEvaluator {
/** @var callable|null */
private $fallbackEvaluator;
/** @var array $functions_whitelist */
private $functions_whitelist;
/** @var array $functionsWhiteList */
private $functionsWhiteList;
/** @var array staticcalls_whitelist */
private $staticcalls_whitelist;
/** @var array $staticCallsWhitelist */
private $staticCallsWhitelist;
/**
/**
* Create a constant expression evaluator.
*
* The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See
@@ -58,19 +54,19 @@ class ConstExprEvaluator {
);
};
$this->functions_whitelist=[];
$this->staticcalls_whitelist=[];
$this->functionsWhiteList = [];
$this->staticCallsWhitelist = [];
}
public function setFunctionsWhitelist(array $functions_whitelist): void
public function setFunctionsWhitelist(array $functionsWhiteList): void
{
$this->functions_whitelist = $functions_whitelist;
$this->functionsWhiteList = $functionsWhiteList;
}
public function setStaticcallsWhitelist(array $staticcalls_whitelist): void
public function setStaticCallsWhitelist(array $staticCallsWhitelist): void
{
$this->staticcalls_whitelist = $staticcalls_whitelist;
}
$this->staticCallsWhitelist = $staticCallsWhitelist;
}
/**
* Silently evaluates a constant expression into a PHP value.
@@ -141,6 +137,10 @@ class ConstExprEvaluator {
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);
@@ -191,10 +191,6 @@ class ConstExprEvaluator {
return $this->evaluateFuncCall($expr);
}
if ($expr instanceof Expr\Variable) {
return $this->evaluateVariable($expr);
}
if ($expr instanceof Expr\StaticCall) {
return $this->evaluateStaticCall($expr);
}
@@ -236,12 +232,14 @@ class ConstExprEvaluator {
/** @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
@@ -291,7 +289,7 @@ class ConstExprEvaluator {
if(! is_string($name)){
//PHP_VERSION_ID usecase
$name = $name->name;
}
}
if (defined($name)){
return constant($name);
@@ -314,7 +312,7 @@ class ConstExprEvaluator {
return true;
} catch(\Throwable $t){
return false;
};
}
}
/** @return mixed */
@@ -410,13 +408,14 @@ class ConstExprEvaluator {
private function evaluateFuncCall(Expr\FuncCall $expr)
{
try {
if ($expr->name instanceof Name){
$function = $expr->name->name;
$name = $expr->name;
if ($name instanceof Name){
$function = $name->name;
} else {
$function = $this->evaluate($expr->name);
$function = $this->evaluate($name);
}
if (! in_array($function, $this->functions_whitelist)){
if (! in_array($function, $this->functionsWhiteList)){
throw new Exception("FuncCall $function not supported");
}
@@ -439,11 +438,16 @@ class ConstExprEvaluator {
{
try {
$name = $expr->name;
if (! is_null($name) && isset($name)) {
if (array_key_exists($name, get_defined_vars())) {
return $$name;
}
if (array_key_exists($name, $GLOBALS)) {
global $$name;
return $$name;
}
} catch (\Throwable $t) {}
} catch (\Throwable $t) {
}
return ($this->fallbackEvaluator)($expr);
}
@@ -452,15 +456,15 @@ class ConstExprEvaluator {
private function evaluateStaticCall(Expr\StaticCall $expr)
{
try {
$classname = $expr->class->name;
$class = $expr->class->name;
if ($expr->name instanceof Identifier){
$methodname = $expr->name->name;
$method = $expr->name->name;
} else {
$methodname = $this->evaluate($expr->name);
$method = $this->evaluate($expr->name);
}
$static_call_description = "$classname::$methodname";
if (! in_array($static_call_description, $this->staticcalls_whitelist)){
$static_call_description = "$class::$method";
if (! in_array($static_call_description, $this->staticCallsWhitelist)){
throw new Exception("StaticCall $static_call_description not supported");
}
@@ -470,8 +474,8 @@ class ConstExprEvaluator {
$args[]=$arg->value->value;
}
$class = new \ReflectionClass($classname);
$method = $class->getMethod($methodname);
$class = new \ReflectionClass($class);
$method = $class->getMethod($method);
if ($method->isPublic()){
return $method->invokeArgs(null, $args);
}
@@ -480,59 +484,79 @@ class ConstExprEvaluator {
return ($this->fallbackEvaluator)($expr);
}
/** @return mixed */
private function evaluatePropertyFetch(Expr\NullsafePropertyFetch|Expr\PropertyFetch $expr)
/**
* @param \PhpParser\Node\Expr\NullsafePropertyFetch|\PhpParser\Node\Expr\PropertyFetch $expr
*
* @return mixed
*/
private function evaluatePropertyFetch($expr)
{
try {
$var = $this->evaluateVariable($expr->var);
if (is_null($var)) {
return null;
}
$var = $this->evaluate($expr->var);
} catch (\Throwable $t) {
$var = null;
}
if ($expr->name instanceof Identifier){
$name = $expr->name->name;
} else {
$name = $this->evaluate($expr->name);
}
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);
$reflectionClass = new \ReflectionClass(get_class($var));
$property = $reflectionClass->getProperty($name);
if ($property->isPublic()) {
return $property->getValue($var);
}
}
} catch (\Throwable $t) {}
catch (\Throwable $t) {}
} else if ($expr instanceof Expr\NullsafePropertyFetch){
return null;
}
return ($this->fallbackEvaluator)($expr);
}
/** @return mixed */
private function evaluateMethodCall(Expr\MethodCall|Expr\NullsafeMethodCall $expr)
/**
* @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $expr
*
* @return mixed
*/
private function evaluateMethodCall($expr)
{
try {
$var = $this->evaluateVariable($expr->var);
if (is_null($var)) {
return null;
}
$var = $this->evaluate($expr->var);
} catch (\Throwable $t) {
$var = null;
}
$args=[];
foreach ($expr->args as $arg){
if (! is_null($var)) {
try {
$args = [];
foreach ($expr->args as $arg) {
/** @var \PhpParser\Node\Arg $arg */
$args[]=$arg->value->value;
$args[] = $arg->value->value;
}
if ($expr->name instanceof Identifier){
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);
if ($method->isPublic()) {
return $method->invokeArgs($var, $args);
}
}
} catch (\Throwable $t) {}
catch (\Throwable $t) {}
} else if ($expr instanceof Expr\NullsafeMethodCall){
return null;
}
return ($this->fallbackEvaluator)($expr);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Combodo\iTop\PhpParser\Evaluation;
use PhpParser\Node\Expr\BinaryOp\Identical;
class IdenticalEvaluator extends BinaryOpEvaluator {
public function GetHandledExpressionType(): ?string {
return Identical::class;
}
function EvaluateBinaryOperation(mixed $left, mixed $right) : mixed
{
return $left === $right;
}
}

View File

@@ -15,8 +15,12 @@ class IssetEvaluator extends AbstractExprEvaluator {
/** @var Isset_ $oExpr */
foreach ($oExpr->vars as $oVar){
$var = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oVar);
if (! isset($var)){
try{
$var = PhpExpressionEvaluator::GetInstance()->EvaluateExpression($oVar);
if (is_null($var)){
return false;
}
} catch (\Throwable $t) {
return false;
}
}

View File

@@ -12,10 +12,19 @@ class PhpExpressionEvaluator {
/** @var iExprEvaluator[] $aPhpParserEvaluators */
private static array $aPhpParserEvaluators;
private int $iMode=self::ITOP_ALGO;
protected function __construct() {
}
const LIB_AND_FALLBACK=1;
const LIB_ONLY=2;
const ITOP_ALGO=3;
public function SetMode($iMode)
{
$this->iMode =$iMode;
}
final public static function GetInstance(): PhpExpressionEvaluator {
if (!isset(static::$oInstance)) {
static::$oInstance = new static();
@@ -58,13 +67,13 @@ class PhpExpressionEvaluator {
static::$oInstance = $oInstance;
}
public function EvaluateExpression(Expr $oExpression, int $iMode=self::LIB_AND_FALLBACK) : mixed
public function EvaluateExpression(Expr $oExpression) : mixed
{
if ($iMode==self::ITOP_ALGO){
if ($this->iMode===self::ITOP_ALGO){
return $this->EvaluateExpressionLocally($oExpression);
}
if ($iMode==self::LIB_ONLY){
if ($this->iMode==self::LIB_ONLY){
$oConstExprEvaluator = new ConstExprEvaluator();
} else {
$oConstExprEvaluator = new ConstExprEvaluator([$this, "EvaluateExpressionLocally"]);
@@ -97,10 +106,7 @@ class PhpExpressionEvaluator {
return $this->ParseAndEvaluateExpression($sBooleanExpr);
}
const LIB_AND_FALLBACK=1;
const LIB_ONLY=2;
const ITOP_ALGO=3;
public function ParseAndEvaluateExpression(string $sExpr, int $iMode=self::LIB_AND_FALLBACK) : mixed
public function ParseAndEvaluateExpression(string $sExpr) : mixed
{
$sPhpContent = <<<PHP
<?php
@@ -109,7 +115,7 @@ PHP;
try{
$aNodes = ModuleFileParser::GetInstance()->ParsePhpCode($sPhpContent);
$oExpr = $aNodes[0];
return $this->EvaluateExpression($oExpr->expr, $iMode);
return $this->EvaluateExpression($oExpr->expr);
} catch (\Throwable $t) {
throw new ModuleFileReaderException("Eval of '$sExpr' caused an error:".$t->getMessage());
}

View File

@@ -9,19 +9,20 @@ class VariableEvaluator extends AbstractExprEvaluator {
public function GetHandledExpressionType(): ?string {
return Variable::class;
}
public function Evaluate(Expr $oExpr): mixed {
/** @var Variable $oExpr */
if (is_null($oExpr->name)){
return null;
$sName = $oExpr->name;
if (array_key_exists($sName, get_defined_vars())) {
return $$sName;
}
if (! isset($oExpr->name)) {
return null;
if (array_key_exists($sName, $GLOBALS)) {
global $$sName;
return $$sName;
}
$sVarname=$oExpr->name;
global $$sVarname;
return $$sVarname;
return null;
}
}

View File

@@ -27,7 +27,7 @@ class ModuleFileReaderTest extends ItopDataTestCase
$this->assertEquals('Bridge - Request management ITIL + Incident management ITIL', $aRes[2]['label'] ?? null);
}
/*public function testAllReadModuleFileConfiguration()
public function testAllReadModuleFileConfiguration()
{
$_SERVER=[
'SERVER_NAME' => 'titi'
@@ -54,7 +54,7 @@ class ModuleFileReaderTest extends ItopDataTestCase
}
$this->assertEquals([], $aErrors, var_export($aErrors, true));
}*/
}
public static function ReadModuleFileConfigurationFileNameProvider()
{

View File

@@ -10,6 +10,12 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase {
private static $PRIVATE_STATIC_PROPERTY = 123;
private const PRIVATE_CONSTANT = 123;
protected function tearDown(): void
{
parent::tearDown(); // TODO: Change the autogenerated stub
PhpExpressionEvaluator::GetInstance()->SetMode(PhpExpressionEvaluator::ITOP_ALGO);
}
public static function EvaluateExpressionProvider() {
return [
'Array: [1000 => "a"]' => ['sExpression' => '[1000 => "a"]'],
@@ -19,7 +25,7 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase {
'BinaryOperator: false|true' => [ 'sExpression' => 'false|true'],
'BinaryOperator: false||true' => [ 'sExpression' => 'false||true'],
'BinaryOperator: false&&true' => [ 'sExpression' => 'false&&true'],
'BinaryOperator: true&&true&&true&&false' => [ 'sExpression' => 'true&&true&&true&&false'],
'BinaryOperator: true&&true&&true&&false' => [ 'sExpression' => 'true && true && true && false'],
'BinaryOperator: false&true' => [ 'sExpression' => 'false&true'],
'BinaryOperator: ! true' => [ 'sExpression' => '! true'],
'BinaryOperator: 10 * 5' => [ 'sExpression' => '10 * 5'],
@@ -30,6 +36,7 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase {
'BinaryOperator: PHP_VERSION_ID != PHP_VERSION_ID' => [ 'sExpression' => 'PHP_VERSION_ID != PHP_VERSION_ID'],
'BitwiseNot: ~3' => ['sExpression' => '~3'],
'BitwiseXor: 3^2' => ['sExpression' => '3^2'],
'BooleanAnd: true && false' => ['sExpression' => 'true && false'],
'Cast: (array)3' => ['sExpression' => '(array)3'],
'Cast: (bool)1' => ['sExpression' => '(bool)1'],
'Cast: (bool)0' => ['sExpression' => '(bool)0'],
@@ -48,14 +55,21 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase {
],
'Coalesce: $oNullVar ?? 1' => ['sExpression' => '$oNullVar ?? 1', 1],
'Coalesce: $oNonNullVar ?? 1' => ['sExpression' => '$oNonNullVar ?? 1', 1],
'Coalesce: $_SERVER["toto"] ?? 1' => ['sExpression' => '$_SERVER["toto"] ?? 1', "titi"],
'Coalesce: $_SERVER["unknown_key"] ?? 1' => ['sExpression' => '$_SERVER["unknown_key"] ?? 1', 1],
'Coalesce: $oGlobalNonNullVar ?? 1' => ['sExpression' => '$oGlobalNonNullVar ?? 1', "a"],
'Coalesce: $oGlobalNullVar ?? 1' => ['sExpression' => '$oGlobalNullVar ?? 1', 1],
'Concat: "a"."b"' => ['sExpression' => '"a"."b"'],
'ConstFetch: false' => [ 'sExpression' => 'false'],
'ConstFetch: (false)' => [ 'sExpression' => 'false'],
'ConstFetch: true' => [ 'sExpression' => 'true'],
'ConstFetch: (true)' => [ 'sExpression' => 'true'],
'Equal: 1 == true' => [ 'sExpression' => '1 == true', true],
'Equal: 1 == false' => [ 'sExpression' => '1 == false', false],
'FuncCall: function_exists(\'ldap_connect\')' => [ 'sExpression' => 'function_exists(\'ldap_connect\')'],
'FuncCall: function_exists(\'gabuzomeushouldnotexist\')' => [ 'sExpression' => 'function_exists(\'gabuzomeushouldnotexist\')'],
'Identical: 1==="1"' => ['sExpression' => '1==="1"', false],
'Identical: "1"==="1"' => ['sExpression' => '"1"==="1"', true],
'Isset: isset($oNonNullVar)' => ['sExpression' => 'isset($oNonNullVar)', false],
'Isset: isset($oGlobalNonNullVar)' => ['sExpression' => 'isset($oGlobalNonNullVar)', true],
'Isset: isset($a, $_SERVER)' => ['sExpression' => 'isset($a, $_SERVER)', false],
@@ -85,6 +99,7 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase {
'Variable: $_SERVER' => ['sExpression' => '$_SERVER', ['toto' => 'titi']],
'Variable: $oNonNullVar' => ['sExpression' => '$oNonNullVar', null],
'Variable: $oGlobalNonNullVar' => ['sExpression' => '$oGlobalNonNullVar', "a"],
'Variable: $oEvaluationFakeClass' => ['sExpression' => '$oEvaluationFakeClass', new EvaluationFakeClass()],
];
}
@@ -116,6 +131,10 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase {
{
global $oGlobalNonNullVar;
$oGlobalNonNullVar="a";
global $oGlobalNullVar;
$oGlobalNullVar=null;
$oNonNullVar="a";
$oNullVar=null;
@@ -126,7 +145,8 @@ class PhpExpressionEvaluatorTest extends ItopDataTestCase {
global $oEvaluationFakeClass;
$oEvaluationFakeClass = new EvaluationFakeClass();
$res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression, $iMode);
PhpExpressionEvaluator::GetInstance()->SetMode($iMode);
$res = PhpExpressionEvaluator::GetInstance()->ParseAndEvaluateExpression($sExpression);
if ($forced_expected === "NOTPROVIDED"){
$this->assertEquals($this->UnprotectedComputeExpression($sExpression), $res, $sExpression);
} else {