feat(php): 添加对参数解包后位置参数的验证支持

- 在函数调用参数验证中添加了对参数解包的支持
- 实现了 hasUnpackCallArg 方法用于检测参数解包
- 添加了 shouldUseDynamicCallForNativeArgs 方法优化动态调用判断
- 修复了参数解包后不能跟位置参数的错误检查逻辑
- 更新了命名参数和解包参数的验证顺序
- 添加了多个测试用例验证参数解包功能的正确性
- 增加了 AOT 测试确保多异常捕获和抛出异常对象的功能正常
pull/5/head
韩天峰 2 months ago
parent 2a9fcfb665
commit 3d6e0b5689
  1. 11
      phpunit/code/dynamic-call-positional-after-unpack.php
  2. 10
      phpunit/code/native-call-positional-after-unpack.php
  3. 13
      phpunit/code/new-positional-after-unpack.php
  4. 15
      phpunit/src/FunctionTest.php
  5. 65
      src/Php/CompilerBase.php
  6. 29
      tests/aot/exception/multi-catch.phpt
  7. 32
      tests/aot/exception/throw-invalid.phpt
  8. 45
      tests/aot/functions/unpack-fixed-native.phpt

@ -0,0 +1,11 @@
<?php
function target(int $a, int $b): void
{
}
function main(): void
{
$fn = 'target';
$fn(...[1], 2);
}

@ -0,0 +1,10 @@
<?php
function target(int $a, int $b): void
{
}
function main(): void
{
target(...[1], 2);
}

@ -0,0 +1,13 @@
<?php
class Target
{
public function __construct(int $a, int $b)
{
}
}
function main(): void
{
new Target(...[1], 2);
}

@ -42,4 +42,19 @@ class FunctionTest extends \BaseTest
$this->exec('Cannot use argument unpacking after named arguments', 'unpack-after-named-arg.php');
}
public function testNativeCallPositionalAfterUnpack()
{
$this->exec('Cannot use positional argument after argument unpacking', 'native-call-positional-after-unpack.php');
}
public function testDynamicCallPositionalAfterUnpack()
{
$this->exec('Cannot use positional argument after argument unpacking', 'dynamic-call-positional-after-unpack.php');
}
public function testNewPositionalAfterUnpack()
{
$this->exec('Cannot use positional argument after argument unpacking', 'new-positional-after-unpack.php');
}
}

@ -1894,6 +1894,10 @@ class CompilerBase extends \PhpAot\Core\Translator
{
$this->validateNativeNamedCallArgs($funcDef, $args);
if ($this->hasUnpackCallArg($args)) {
return;
}
$argc = count($args);
$type = str_contains($name, '::') ? 'Method' : 'Function';
if ($argc < $funcDef->argCountRequired) {
@ -1924,6 +1928,7 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function validateNativeNamedCallArgs(FunctionDef $functionDef, array $callArgs): void
{
$hasNamedArg = false;
$hasUnpack = false;
$seenNamedArgs = [];
$providedArgIndexes = [];
$argNameIndex = $this->getFunctionArgNameIndex($functionDef);
@ -1937,10 +1942,14 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$hasUnpack = true;
$providedArgIndexes[$i] = true;
continue;
}
if ($arg->name === null) {
if ($hasUnpack) {
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
}
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument');
}
@ -2924,6 +2933,9 @@ class CompilerBase extends \PhpAot\Core\Translator
return;
}
$this->validateInternalNamedCallArgs($ref, $expr->args);
if ($this->hasUnpackCallArg($expr->args)) {
return;
}
$minArgs = $ref->getNumberOfRequiredParameters();
$maxArgs = $ref->getNumberOfParameters();
$actualArgCount = count($expr->args);
@ -2961,6 +2973,37 @@ class CompilerBase extends \PhpAot\Core\Translator
return false;
}
protected function hasUnpackCallArg(array $args): bool
{
foreach ($args as $arg) {
if ($arg instanceof Node\Arg && $arg->unpack) {
return true;
}
}
return false;
}
protected function shouldUseDynamicCallForNativeArgs(string $nativeFunc, array $args): bool
{
if (!$this->hasUnpackCallArg($args)) {
return false;
}
if ($this->hasUnpackBeforeNamedArg($args)) {
return true;
}
$variadicArgIndex = $this->getVariadicArgIndex($this->getFunction($nativeFunc));
foreach ($args as $i => $arg) {
if (!$arg instanceof Node\Arg || !$arg->unpack) {
continue;
}
if ($variadicArgIndex === null || $i < $variadicArgIndex) {
return true;
}
}
return false;
}
protected function genCallUserFuncArray(string $callback, array $args, string $funcName = '', string $className = ''): string
{
return 'php::call(' . $this->getFuncPtr('call_user_func_array') . ', '
@ -2976,6 +3019,7 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function validateInternalNamedCallArgs(\ReflectionFunctionAbstract $ref, array $callArgs): void
{
$hasNamedArg = false;
$hasUnpack = false;
$seenNamedArgs = [];
$providedArgIndexes = [];
$argNameIndex = [];
@ -3000,10 +3044,14 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$hasUnpack = true;
$providedArgIndexes[$i] = true;
continue;
}
if ($arg->name === null) {
if ($hasUnpack) {
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
}
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument');
}
@ -3042,7 +3090,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$hasNamedArg = true;
}
if ($hasNamedArg) {
if ($hasNamedArg && !$hasUnpack) {
foreach ($requiredArgIndexes as $index => $name) {
if (!isset($providedArgIndexes[$index])) {
$this->fatalError($callArgs[array_key_last($callArgs)] ?? null, "Named argument `{$name}` is missing default value");
@ -3073,7 +3121,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->genPlaceHolder($this->identifierToStr($expr->name));
}
$this->checkNativeCallArgs($expr, $this->getFunction($nativeFn), $expr->args, $name);
if ($this->hasUnpackBeforeNamedArg($expr->args)) {
if ($this->shouldUseDynamicCallForNativeArgs($nativeFn, $expr->args)) {
return $this->genCallUserFuncArray($this->getFunctionCallbackExpr($nativeFn), $expr->args, $name);
}
try {
@ -3282,6 +3330,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$namedArgsVar = null;
$namedArgs = [];
$hasNamedArg = false;
$hasUnpack = false;
$ensureArrayArgs = function () use (&$arrayArgsVar, &$list_args): string {
if ($arrayArgsVar === null) {
@ -3321,6 +3370,7 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$hasUnpack = true;
$arrayArgs = $ensureArrayArgs();
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
continue;
@ -3346,6 +3396,9 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use positional argument after named argument');
}
if ($hasUnpack) {
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
}
$byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i);
if ($this->isVarExpr($arg->value)) {
$name = $this->parseIdentifier($arg->value);
@ -3362,7 +3415,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->fatalError($arg, 'Undefined variable `$' . $obj . '`');
}
if ($byRef) {
$list_args[] = $obj . '.attrRef(' . $this->identifierToStr($arg->value->name) . ')';
$addPositionalArg($obj . '.attrRef(' . $this->identifierToStr($arg->value->name) . ')');
continue;
}
} elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) {
@ -5277,8 +5330,8 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($nativeFunc) {
$expr->setAttribute('nativeCall', $nativeFunc);
try {
if ($this->hasUnpackBeforeNamedArg($expr->args)) {
return $this->genCallUserFuncArray($this->genArray([$object, $method]), $expr->args, $funcName, $class);
if ($this->shouldUseDynamicCallForNativeArgs($nativeFunc, $expr->args)) {
return $this->genCallUserFuncArray($this->genArray([$object, $method]), $expr->args, $methodName, $class);
}
return $this->parseNativeMethodCall($object, $nativeFunc, $expr->args);
} catch (PlaceHolder) {
@ -5426,7 +5479,7 @@ class CompilerBase extends \PhpAot\Core\Translator
if ($nativeFunc) {
try {
if ($this->hasUnpackBeforeNamedArg($expr->args)) {
if ($this->shouldUseDynamicCallForNativeArgs($nativeFunc, $expr->args)) {
return $this->genCallUserFuncArray($this->genArray($callScope), $expr->args, $method, $class);
}
$args = $this->parseNativeCallArgs($expr->args, $nativeFunc);

@ -0,0 +1,29 @@
--TEST--
AOT multi-catch handles alternative exception types
--FILE--
<?php
class MultiCatchFirst extends Exception {}
class MultiCatchSecond extends Exception {}
function raise_multi(int $kind): void
{
if ($kind === 1) {
throw new MultiCatchFirst('first');
}
throw new MultiCatchSecond('second');
}
function main(): void
{
foreach ([1, 2] as $kind) {
try {
raise_multi($kind);
} catch (MultiCatchFirst|MultiCatchSecond $e) {
var_dump($e->getMessage());
}
}
}
?>
--EXPECT--
string(5) "first"
string(6) "second"

@ -0,0 +1,32 @@
--TEST--
AOT invalid throw object matches PHP error
--FILE--
<?php
function throw_non_throwable_object(): void
{
throw new stdClass();
}
function throw_mixed(mixed $value): void
{
throw $value;
}
function main(): void
{
try {
throw_mixed(1);
} catch (Error $e) {
var_dump($e->getMessage());
}
try {
throw_non_throwable_object();
} catch (Error $e) {
var_dump($e->getMessage());
}
}
?>
--EXPECT--
string(22) "Can only throw objects"
string(52) "Cannot throw objects that do not implement Throwable"

@ -0,0 +1,45 @@
--TEST--
Argument unpacking can fill fixed native parameters
--FILE--
<?php
function fixed_args(int $a, int $b): void
{
echo $a, ',', $b, PHP_EOL;
}
function fixed_and_variadic(int $a, int ...$rest): void
{
echo $a, ':', implode(',', $rest), PHP_EOL;
}
class NativeUnpackTarget
{
public function method(int $a, int $b): void
{
echo 'm=', $a, ',', $b, PHP_EOL;
}
public static function staticMethod(int $a, int $b): void
{
echo 's=', $a, ',', $b, PHP_EOL;
}
}
function main(): void
{
fixed_args(...[1, 2]);
fixed_and_variadic(...[3, 4, 5]);
fixed_and_variadic(6, ...[7, 8]);
$target = new NativeUnpackTarget();
$target->method(...[9, 10]);
NativeUnpackTarget::staticMethod(...[11, 12]);
}
?>
--EXPECT--
1,2
3:4,5
6:7,8
m=9,10
s=11,12
Loading…
Cancel
Save