From 5027650611a199542cb94a6995043ddc7124c16e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 30 Jun 2026 18:52:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(php):=20=E6=B7=BB=E5=8A=A0=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E7=B1=BB=E5=9E=8B=E6=A3=80=E6=9F=A5=E7=A1=AE?= =?UTF-8?q?=E4=BF=9Dvoid=E8=A1=A8=E8=BE=BE=E5=BC=8F=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E4=BD=9C=E4=B8=BA=E5=80=BC=E6=88=96=E6=9D=A1=E4=BB=B6=E4=BD=BF?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在二元操作符解析中添加对左右操作数的类型检查 - 在有序操作数解析中添加对操作数的类型检查 - 修正类型检测逻辑,移除对void类型的特殊处理 - 在幂运算解析中添加对左右操作数的类型检查 - 在比较表达式解析中添加对操作数的类型检查 - 在短路逻辑操作符解析中添加对左右操作数的类型检查 - 添加assertExprCanBeUsedAsCondition方法用于条件表达式验证 - 在数组解析中添加对键值的类型检查 - 在for循环条件中添加对表达式的类型检查 - 在三元操作符解析中添加对条件和分支的类型检查 - 在match表达式解析中添加对条件和分支的类型检查 - 在位运算、布尔运算、打印、克隆等操作中添加类型检查 - 在类型转换操作中添加对操作数的类型检查 - 添加测试用例验证void表达式在各种场景下的使用限制 --- phpunit/code/void-expression-array-value.php | 21 +++++++++ .../code/void-expression-binary-operand.php | 21 +++++++++ phpunit/code/void-expression-condition.php | 23 ++++++++++ phpunit/code/void-expression-match-arm.php | 23 ++++++++++ .../code/void-expression-ternary-branch.php | 21 +++++++++ phpunit/src/ClassTest.php | 25 ++++++++++ src/Php/CompilerBase.php | 46 +++++++++++++++++++ src/Php/Parser/BinaryOpTrait.php | 12 ++++- 8 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 phpunit/code/void-expression-array-value.php create mode 100644 phpunit/code/void-expression-binary-operand.php create mode 100644 phpunit/code/void-expression-condition.php create mode 100644 phpunit/code/void-expression-match-arm.php create mode 100644 phpunit/code/void-expression-ternary-branch.php diff --git a/phpunit/code/void-expression-array-value.php b/phpunit/code/void-expression-array-value.php new file mode 100644 index 00000000..a91279fd --- /dev/null +++ b/phpunit/code/void-expression-array-value.php @@ -0,0 +1,21 @@ + parent::__construct(), + }; + } +} + +function main(): void +{ + new VoidExpressionMatchChild(); +} diff --git a/phpunit/code/void-expression-ternary-branch.php b/phpunit/code/void-expression-ternary-branch.php new file mode 100644 index 00000000..95fa2f09 --- /dev/null +++ b/phpunit/code/void-expression-ternary-branch.php @@ -0,0 +1,21 @@ +exec('Cannot use void expression as function argument', 'parent-constructor-used-as-argument.php'); } + public function testVoidExpressionCannotBeUsedAsBinaryOperand() + { + $this->exec('Cannot use void expression as binary operand', 'void-expression-binary-operand.php'); + } + + public function testVoidExpressionCannotBeUsedAsCondition() + { + $this->exec('Cannot use void expression as condition', 'void-expression-condition.php'); + } + + public function testVoidExpressionCannotBeUsedAsTernaryBranch() + { + $this->exec('Cannot use void expression as ternary branch', 'void-expression-ternary-branch.php'); + } + + public function testVoidExpressionCannotBeUsedAsArrayValue() + { + $this->exec('Cannot use void expression as array value', 'void-expression-array-value.php'); + } + + public function testVoidExpressionCannotBeUsedAsMatchArm() + { + $this->exec('Cannot use void expression as match arm', 'void-expression-match-arm.php'); + } + public function testDestructorCannotDeclareReturnType() { $this->exec('Method `DestructorReturnType::__destruct()` cannot declare a return type', 'destructor-return-type.php'); diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 912ab209..9bd09c47 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -904,6 +904,13 @@ class CompilerBase extends \PhpAot\Core\Translator } } + protected function assertExprCanBeUsedAsCondition(NodeAbstract $expr, string $context = 'condition'): void + { + if ($this->detectTypeOfExpr($expr) === self::TYPE_VOID) { + $this->fatalError($expr, 'Cannot use void expression as ' . $context); + } + } + public function getNamespacedClassName(string $class, string $currentNamespace = ''): string { if ($class === '') { @@ -1388,6 +1395,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function genConditionWithCapturedStmts(NodeAbstract $cond, string $openPrefix): string { + $this->assertExprCanBeUsedAsCondition($cond); [$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($cond); $code = ''; $this->appendCapturedStmtLines($code, $beforeStmts); @@ -2463,8 +2471,10 @@ class CompilerBase extends \PhpAot\Core\Translator $list = []; $this->indentLevel++; foreach ($items as $item) { + $this->assertExprCanBeUsedAsValue($item->value, 'array value'); $value = $this->parseIdentifier($item->value); if ($item->key) { + $this->assertExprCanBeUsedAsValue($item->key, 'array key'); $key = $this->parseArrayKey($item->key); $list[] = $this->getIndent() . '{ ' . $key . ', ' . self::TYPE_VAR . '(' . $value . ') }'; } else { @@ -2671,6 +2681,7 @@ class CompilerBase extends \PhpAot\Core\Translator $list_cond = []; $hasCondStmts = false; foreach ($cond as $expr) { + $this->assertExprCanBeUsedAsCondition($expr, 'for condition'); [$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); $hasCondStmts = $hasCondStmts || $beforeStmts || $afterStmts; $list_cond[] = [$condExpr, $beforeStmts, $afterStmts]; @@ -3867,6 +3878,9 @@ class CompilerBase extends \PhpAot\Core\Translator if ($expr->if === null) { return $this->parseValueSelection($expr, $expr->cond, $expr->else, self::OP_NOT_EMPTY); } + $this->assertExprCanBeUsedAsCondition($expr->cond, 'ternary condition'); + $this->assertExprCanBeUsedAsValue($expr->if, 'ternary branch'); + $this->assertExprCanBeUsedAsValue($expr->else, 'ternary branch'); [$cond, $condBeforeStmts, $condAfterStmts] = $this->parseExprWithCapturedStmts($expr->cond); $ifBeforeStmtCount = count($this->context->beforeStmtLines); $ifAfterStmtCount = count($this->context->afterStmtLines); @@ -3928,6 +3942,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseMatch(Expr\Match_ $expr): string { + $this->assertExprCanBeUsedAsValue($expr->cond, 'match condition'); $var = $this->parseIdentifier($expr->cond); if ($this->isVarExpr($expr->cond)) { if (!$this->hasVar($var)) { @@ -3957,6 +3972,7 @@ class CompilerBase extends \PhpAot\Core\Translator }; $appendMatchReturn = function (string &$code, NodeAbstract $body) use ($parseExprWithStmts, $appendStmtLines): void { + $this->assertExprCanBeUsedAsValue($body, 'match arm'); [$value, $beforeStmts, $afterStmts] = $parseExprWithStmts($body); $appendStmtLines($code, $beforeStmts); if ($afterStmts) { @@ -3982,6 +3998,7 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->isMatchExpr($cond)) { $this->fatalError($arm, 'Match expression cannot be used as a condition'); } + $this->assertExprCanBeUsedAsValue($cond, 'match arm condition'); [$condValue, $beforeStmts, $afterStmts] = $parseExprWithStmts($cond); $code .= $this->getIndent() . 'if (!' . $matched . ') {'; $appendStmtLines($code, $beforeStmts); @@ -4034,6 +4051,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseBitwiseNot(Expr\BitwiseNot $expr): string { $type = $this->detectTypeOfExpr($expr->expr); + $this->assertExprCanBeUsedAsValue($expr->expr, 'bitwise operand'); if ($type === self::TYPE_BIGINT) { return 'php::BigInt::bitNot(' . $this->parseExpr($expr->expr) . ')'; } @@ -4078,12 +4096,14 @@ class CompilerBase extends \PhpAot\Core\Translator */ protected function parseBooleanNot(Expr\BooleanNot $expr): string { + $this->assertExprCanBeUsedAsCondition($expr->expr, 'boolean operand'); return '!(' . $this->parseExpr($expr->expr) . ')'; } protected function parseWhile(Node\Stmt\While_ $v): string { $stmts = $v->stmts; + $this->assertExprCanBeUsedAsCondition($v->cond, 'while condition'); [$cond, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($v->cond); $code = $this->parseBeforeStmtLines() . PHP_EOL; @@ -4109,12 +4129,14 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parsePrint(Expr\Print_ $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'print operand'); return 'php::print(' . $this->parseExpr($expr->expr) . ')'; } protected function parseDo(Node\Stmt\Do_ $v): string { $stmts = $v->stmts; + $this->assertExprCanBeUsedAsCondition($v->cond, 'do-while condition'); [$cond, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($v->cond); if ($beforeStmts || $afterStmts) { $condCode = '[&]() -> bool {'; @@ -4143,6 +4165,8 @@ class CompilerBase extends \PhpAot\Core\Translator */ protected function parseValueSelection(NodeAbstract $expr, Expr $left, Expr $right, string $op): string { + $this->assertExprCanBeUsedAsValue($left, 'selection value'); + $this->assertExprCanBeUsedAsValue($right, 'selection value'); $leftExpr = $this->parseIdentifier($left); if ($this->isVarExpr($left)) { $this->checkVarMustExist($left, $leftExpr); @@ -4281,11 +4305,13 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseClone(Expr\Clone_ $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand'); return 'php::clone(' . $this->parseExpr($expr->expr) . ')'; } protected function parseInstanceof(Expr\Instanceof_ $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'instanceof operand'); if ($this->isNameExpr($expr->class)) { $className = $this->getNamespacedClassName($this->parseIdentifier($expr->class)); $className = $this->getClassEntryPtr($className); @@ -4297,11 +4323,13 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseCastInt(Expr\Cast\Int_ $node): string { + $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); return $this->convertIntExpr($this->parseExpr($node->expr)); } protected function parseCastString(Expr\Cast\String_ $node): string { + $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); return $this->convertExprToStringByType( $this->parseExpr($node->expr), $this->detectTypeOfExpr($node->expr) @@ -4310,11 +4338,13 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseCastBool(Expr\Cast\Bool_ $node): string { + $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); return $this->convertBoolExpr($this->parseExpr($node->expr)); } protected function parseCastObject(Expr\Cast\Object_ $node): string { + $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); return $this->convertObjectExpr($this->parseExpr($node->expr)); } @@ -4383,6 +4413,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseUnaryMinus(Expr\UnaryMinus $expr): string { $type = $this->detectTypeOfExpr($expr->expr); + $this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand'); if ($type === self::TYPE_BIGFLOAT) { return 'php::BigFloat::neg(' . $this->parseExpr($expr->expr) . ')'; } @@ -4399,6 +4430,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseUnaryPlus(Expr\UnaryPlus $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand'); return $this->parseExpr($expr->expr); } @@ -4407,6 +4439,9 @@ class CompilerBase extends \PhpAot\Core\Translator $parts = $expr->parts; $list = []; foreach ($parts as $part) { + if (!$part instanceof Node\InterpolatedStringPart) { + $this->assertExprCanBeUsedAsValue($part, 'string interpolation value'); + } $list[] = $this->parseExpr($part); } @@ -5058,6 +5093,7 @@ class CompilerBase extends \PhpAot\Core\Translator $cond = $v->cond; $tmp_var = $this->genTmpVarName(); $type = $this->detectTypeOfExpr($cond); + $this->assertExprCanBeUsedAsValue($cond, 'switch condition'); if ($this->isVarExpr($cond)) { $this->requireVar($v, $this->parseIdentifier($cond)); } @@ -5137,6 +5173,7 @@ class CompilerBase extends \PhpAot\Core\Translator $groupMatched = $this->genTmpVarName(); $code .= $this->getIndent() . 'bool ' . $groupMatched . ' = false;' . PHP_EOL; foreach ($caseConds as $caseCond) { + $this->assertExprCanBeUsedAsValue($caseCond, 'switch case condition'); $caseBeforeStmtCount = count($this->context->beforeStmtLines); $caseAfterStmtCount = count($this->context->afterStmtLines); $caseCondExpr = $this->parseIdentifier($caseCond); @@ -5178,6 +5215,9 @@ class CompilerBase extends \PhpAot\Core\Translator foreach ($v->vars as $var) { $varName = $this->escapeVarName($var->var->name); $type = $var->default ? $this->detectTypeOfExpr($var->default) : self::TYPE_VAR; + if ($var->default) { + $this->assertExprCanBeUsedAsValue($var->default, 'static variable default value'); + } $globalVar = $this->addStaticVar($var->var, $varName, $type); $list[] = self::TYPE_VAR . ' &' . $varName . ' = ' . $this->escapeGlobalVar($globalVar) . ';'; @@ -5206,6 +5246,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseEval(Expr\Eval_ $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'eval operand'); // 对 eval() 指令的 PHP 代码段禁止字面量优化 $expr->expr->setAttribute('noLiteralString', true); return 'php::eval(' . $this->identifierToStr($expr->expr) . ')'; @@ -5213,6 +5254,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseInclude(Expr\Include_ $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'include operand'); switch ($expr->type) { case Expr\Include_::TYPE_INCLUDE: $type = 'php::INCLUDE'; @@ -5406,6 +5448,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseCastArray(Expr\Cast\Array_ $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand'); return $this->convertArrayExpr($this->parseExpr($expr->expr)); } @@ -5426,6 +5469,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseCastDouble(mixed $expr): string { + $this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand'); return $this->convertFloatExpr($this->parseIdentifier($expr->expr)); } @@ -6859,10 +6903,12 @@ class CompilerBase extends \PhpAot\Core\Translator $items = $node->items; foreach ($items as $item) { + $this->assertExprCanBeUsedAsValue($item->value, $item->unpack ? 'array unpack value' : 'array value'); $value = $this->parseIdentifier($item->value); if ($item->unpack) { $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');'; } elseif ($item->key) { + $this->assertExprCanBeUsedAsValue($item->key, 'array key'); $key = $this->parseArrayKey($item->key); $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.set(' . $key . ', ' . $value . ');'; } else { diff --git a/src/Php/Parser/BinaryOpTrait.php b/src/Php/Parser/BinaryOpTrait.php index 1a1e4a22..66112bd0 100644 --- a/src/Php/Parser/BinaryOpTrait.php +++ b/src/Php/Parser/BinaryOpTrait.php @@ -18,6 +18,9 @@ trait BinaryOpTrait { protected function parseBinaryOp(NodeAbstract $left, NodeAbstract $right, string $op): string { + $this->assertExprCanBeUsedAsValue($left, 'binary operand'); + $this->assertExprCanBeUsedAsValue($right, 'binary operand'); + // 运算逻辑,优先转为数字 $leftExpr = $this->parseOrderedBinaryOperand($left); $rightExpr = $this->parseOrderedBinaryOperand($right); @@ -169,6 +172,7 @@ trait BinaryOpTrait protected function parseOrderedOperand(NodeAbstract $expr, bool $numeric): float|int|string { + $this->assertExprCanBeUsedAsValue($expr, 'operand'); if (!$this->shouldMaterializeOrderedOperand($expr)) { return $numeric ? $this->parseNumericIdentifier($expr) : $this->parseIdentifier($expr); } @@ -224,7 +228,7 @@ trait BinaryOpTrait } $type = $this->detectTypeOfExpr($expr); - return $type === self::TYPE_VOID ? self::TYPE_VAR : $type; + return $type; } protected function appendCapturedStmtLinesToContext(array $stmts): void @@ -298,6 +302,8 @@ trait BinaryOpTrait protected function parseBinaryOpPow(Expr\BinaryOp\Pow $expr): string { + $this->assertExprCanBeUsedAsValue($expr->left, 'binary operand'); + $this->assertExprCanBeUsedAsValue($expr->right, 'binary operand'); $leftType = $this->detectTypeOfExpr($expr->left); if ($leftType === self::TYPE_BIGINT) { $leftExpr = $this->parseOrderedOperand($expr->left, false); @@ -330,6 +336,7 @@ trait BinaryOpTrait protected function parseCompareExpr(NodeAbstract $expr): string { + $this->assertExprCanBeUsedAsValue($expr, 'comparison operand'); // PHPX 与 bool 值比较会出现重载错误,所以需要转换成 bool 值 if ($this->isScalarBool($expr)) { return $this->getBoolValue($expr); @@ -397,6 +404,9 @@ trait BinaryOpTrait protected function parseShortCircuitLogicalOp(NodeAbstract $left, NodeAbstract $right, string $op): string { + $this->assertExprCanBeUsedAsCondition($left, 'logical operand'); + $this->assertExprCanBeUsedAsCondition($right, 'logical operand'); + $leftExpr = $this->parseNumericIdentifier($left); $this->checkVarMustExist($left, $leftExpr);