From fc2cc2259405ffe2bd0c6998bfdb34d8b5a2e040 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 10 Jun 2026 12:54:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(php):=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E5=87=BD=E6=95=B0=E8=B0=83=E7=94=A8=E4=B8=AD?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E5=8F=82=E6=95=B0=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 checkNativeCallArgs 中验证命名参数 - 实现 getFunctionArgNameIndex 和 getVariadicArgIndex 辅助方法 - 添加 validateNativeNamedCallArgs 方法验证命名参数规则 - 重构函数调用参数处理逻辑支持命名参数重排序 - 实现变长参数和命名参数的混合处理 - 添加构建原生变长参数的方法 - 更新参数类型转换逻辑 - 添加相关测试用例验证功能正确性 --- .../code/native-call-duplicate-named-arg.php | 10 ++ ...ative-call-named-overwrites-positional.php | 10 ++ .../native-call-positional-after-named.php | 10 ++ .../code/native-call-unknown-named-arg.php | 10 ++ phpunit/src/FunctionTest.php | 20 +++ src/Php/CompilerBase.php | 156 ++++++++++++++---- src/Php/Translator.php | 4 +- tests/aot/functions/named-args-native.phpt | 41 +++++ .../aot/functions/named-variadic-native.phpt | 52 ++++++ 9 files changed, 280 insertions(+), 33 deletions(-) create mode 100644 phpunit/code/native-call-duplicate-named-arg.php create mode 100644 phpunit/code/native-call-named-overwrites-positional.php create mode 100644 phpunit/code/native-call-positional-after-named.php create mode 100644 phpunit/code/native-call-unknown-named-arg.php create mode 100644 tests/aot/functions/named-args-native.phpt create mode 100644 tests/aot/functions/named-variadic-native.phpt diff --git a/phpunit/code/native-call-duplicate-named-arg.php b/phpunit/code/native-call-duplicate-named-arg.php new file mode 100644 index 00000000..336378e3 --- /dev/null +++ b/phpunit/code/native-call-duplicate-named-arg.php @@ -0,0 +1,10 @@ +exec('The return type of the function `test` cannot be a reference type', 'function-return-ref.php'); } + public function testNativeCallUnknownNamedArgument() + { + $this->exec('Unknown named argument `missing`', 'native-call-unknown-named-arg.php'); + } + + public function testNativeCallDuplicateNamedArgument() + { + $this->exec('Duplicate named argument `value`', 'native-call-duplicate-named-arg.php'); + } + + public function testNativeCallPositionalAfterNamedArgument() + { + $this->exec('Cannot use positional argument after named argument', 'native-call-positional-after-named.php'); + } + + public function testNativeCallNamedArgumentOverwritesPositionalArgument() + { + $this->exec('Named argument `value` overwrites previous argument', 'native-call-named-overwrites-positional.php'); + } + } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index aa52ab7d..8b32f224 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -1682,6 +1682,8 @@ class CompilerBase extends \PhpAot\Core\Translator protected function checkNativeCallArgs(CallLike $expr, FunctionDef $funcDef, array $args, string $name): void { + $this->validateNativeNamedCallArgs($funcDef, $args); + $argc = count($args); $type = str_contains($name, '::') ? 'Method' : 'Function'; if ($argc < $funcDef->argCountRequired) { @@ -1691,6 +1693,76 @@ class CompilerBase extends \PhpAot\Core\Translator } } + protected function getFunctionArgNameIndex(FunctionDef $functionDef): array + { + $argNameIndex = []; + foreach ($functionDef->argInfoList as $k => $argInfo) { + $argNameIndex[$argInfo->name] = $k; + } + return $argNameIndex; + } + + protected function getVariadicArgIndex(FunctionDef $functionDef): ?int + { + $lastIndex = count($functionDef->argInfoList) - 1; + if ($lastIndex >= 0 and $functionDef->argInfoList[$lastIndex]->variadic) { + return $lastIndex; + } + return null; + } + + protected function validateNativeNamedCallArgs(FunctionDef $functionDef, array $callArgs): void + { + $hasNamedArg = false; + $seenNamedArgs = []; + $providedArgIndexes = []; + $argNameIndex = $this->getFunctionArgNameIndex($functionDef); + $variadicArgIndex = $this->getVariadicArgIndex($functionDef); + + foreach ($callArgs as $i => $arg) { + if ($this->isPlaceholderExpr($arg)) { + continue; + } + if ($arg->name === null) { + if ($hasNamedArg) { + $this->fatalError($arg, 'Cannot use positional argument after named argument'); + } + $providedArgIndexes[$i] = true; + continue; + } + if (!$this->isIdExpr($arg->name)) { + $this->fatalError($arg, 'Named argument must be a string'); + } + + $argName = $arg->name->name; + if (isset($seenNamedArgs[$argName])) { + $this->fatalError($arg, "Duplicate named argument `{$argName}`"); + } + if (!array_key_exists($argName, $argNameIndex)) { + if ($variadicArgIndex === null) { + $this->fatalError($arg, "Unknown named argument `{$argName}`"); + } + $seenNamedArgs[$argName] = true; + $hasNamedArg = true; + continue; + } + + $argIndex = $argNameIndex[$argName]; + if ($variadicArgIndex !== null and $argIndex === $variadicArgIndex) { + $seenNamedArgs[$argName] = true; + $hasNamedArg = true; + continue; + } + if (isset($providedArgIndexes[$argIndex])) { + $this->fatalError($arg, "Named argument `{$argName}` overwrites previous argument"); + } + + $seenNamedArgs[$argName] = true; + $providedArgIndexes[$argIndex] = true; + $hasNamedArg = true; + } + } + protected function getNativeMethod(CallLike $expr, string $class, string $method, bool $checkArgs = true): string|false { if (!$this->hasClass($class)) { @@ -2540,20 +2612,26 @@ class CompilerBase extends \PhpAot\Core\Translator $argList = []; $functionDef = $this->getFunction($nativeFunc); $args = []; + $variadicArgs = []; $hasNamedArg = false; + $argNameIndex = $this->getFunctionArgNameIndex($functionDef); + $variadicArgIndex = $this->getVariadicArgIndex($functionDef); // 对命名参数进行重排 foreach ($callArgs as $i => $arg) { if ($this->isPlaceholderExpr($arg)) { throw new PlaceHolder(); } if ($arg->name) { - foreach ($functionDef->argInfoList as $k => $argInfo) { - if ($argInfo->name === $arg->name->name) { - $args[$k] = $arg; - $hasNamedArg = true; - break; - } + $argName = $arg->name->name; + $k = $argNameIndex[$argName] ?? null; + if ($k !== null and ($variadicArgIndex === null or $k < $variadicArgIndex)) { + $args[$k] = $arg; + } else { + $variadicArgs[] = [$argName, $arg]; } + $hasNamedArg = true; + } elseif ($variadicArgIndex !== null and $i >= $variadicArgIndex) { + $variadicArgs[] = [null, $arg]; } else { $args[$i] = $arg; } @@ -2562,6 +2640,9 @@ class CompilerBase extends \PhpAot\Core\Translator if ($hasNamedArg) { // 命名参数中间存在空洞,需要使用默认参数填充 foreach ($functionDef->argInfoList as $k => $argInfo) { + if ($variadicArgIndex !== null and $k === $variadicArgIndex) { + continue; + } if (!isset($args[$k])) { if ($argInfo->defaultValue === null) { $this->fatalError($callArgs[$i], 'Named argument `' . $argInfo->name . '` is missing default value'); @@ -2572,42 +2653,52 @@ class CompilerBase extends \PhpAot\Core\Translator ksort($args); } + if ($variadicArgIndex !== null and $variadicArgs) { + $args[$variadicArgIndex] = $this->buildNativeVariadicArg($variadicArgs, $functionDef->argInfoList[$variadicArgIndex]); + ksort($args); + } + // 函数只接受一个变长参数,且调用参数为空,直接传入空数组 if (count($args) === 0 and count($functionDef->argInfoList) === 1 and $functionDef->argInfoList[0]->variadic) { return '{}'; } foreach ($args as $i => $arg) { + if (is_string($arg)) { + $argList[] = $arg; + continue; + } $argInfo = $this->getArgInfo($arg, $nativeFunc, $i); - if ($argInfo->variadic) { - $argsSlice = array_slice($args, $i); - if (count($argsSlice) === 1 and $argsSlice[0]->unpack) { - if ($this->isVarExpr($arg->value)) { - $var =$this->parseIdentifier($arg->value); - if ($this->getVarType($var) === self::TYPE_ARRAY) { - $argList[] = $var; - break; - } - } - $argList[] = $this->convertArrayExpr($this->parseExpr($arg->value)); - } else { - $tmpVar = $this->addTmpVar(self::TYPE_ARRAY); - foreach ($argsSlice as $item) { - if ($item->unpack) { - $this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArg($item) . ');'; - } else { - $this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->parseArg($item) . ');'; - } - } - $argList[] = $tmpVar; - break; + $argList[] = $this->getTypeConvertedArg($arg, $argInfo); + } + + return implode(', ', $argList); + } + + protected function buildNativeVariadicArg(array $variadicArgs, ArgInfo $argInfo): string + { + if (count($variadicArgs) === 1 and $variadicArgs[0][0] === null and $variadicArgs[0][1]->unpack) { + $arg = $variadicArgs[0][1]; + if ($this->isVarExpr($arg->value)) { + $var = $this->parseIdentifier($arg->value); + if ($this->getVarType($var) === self::TYPE_ARRAY) { + return $var; } - } else { - $argList[] = $this->getTypeConvertedArg($arg, $argInfo); } + return $this->convertArrayExpr($this->parseExpr($arg->value)); } - return implode(', ', $argList); + $tmpVar = $this->addTmpVar(self::TYPE_ARRAY); + foreach ($variadicArgs as [$name, $arg]) { + if ($arg->unpack) { + $this->context->beforeStmtLines[] = $tmpVar . '.merge(' . $this->parseArrayArg($arg) . ');'; + } elseif ($name !== null) { + $this->context->beforeStmtLines[] = $tmpVar . '.set(' . $this->getLiteralString($name) . ', ' . $this->getTypeConvertedArg($arg, $argInfo) . ');'; + } else { + $this->context->beforeStmtLines[] = $tmpVar . '.append(' . $this->getTypeConvertedArg($arg, $argInfo) . ');'; + } + } + return $tmpVar; } protected function parseNamedCallArgs(array $args, int $firstIndex, array $listArgs): string @@ -2623,6 +2714,9 @@ class CompilerBase extends \PhpAot\Core\Translator if (!$this->isIdExpr($arg->name)) { $this->fatalError($arg, 'Named argument must be a string'); } + if (array_key_exists($arg->name->name, $namedArgs)) { + $this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`"); + } $namedArgs[$arg->name->name] = $this->parseArg($arg); } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index da3b3fb6..6cd9c0dd 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -2386,8 +2386,8 @@ CODE; $this->addArgument('this_', self::TYPE_OBJECT); } foreach ($this->functionDef->argInfoList as $argInfo) { - $this->addArgument($argInfo->name, $argInfo->type); - if ($argInfo->class) { + $this->addArgument($argInfo->name, $argInfo->variadic ? self::TYPE_ARRAY : $argInfo->type); + if (!$argInfo->variadic and $argInfo->class) { $this->addObject($argInfo->name, $argInfo->class); } } diff --git a/tests/aot/functions/named-args-native.phpt b/tests/aot/functions/named-args-native.phpt new file mode 100644 index 00000000..4e22465a --- /dev/null +++ b/tests/aot/functions/named-args-native.phpt @@ -0,0 +1,41 @@ +--TEST-- +Native function call named arguments are reordered and validated +--FILE-- + $name, + "age" => $age, + "city" => $city, + "vip" => $vip, + ]; +} + +function main(): void +{ + var_dump(makeUser(age: 20, name: "Tom", vip: true)); + var_dump(makeUser("Jane", city: "Shanghai", age: 18)); +} +?> +--EXPECT-- +array(4) { + ["name"]=> + string(3) "Tom" + ["age"]=> + int(20) + ["city"]=> + string(7) "Beijing" + ["vip"]=> + bool(true) +} +array(4) { + ["name"]=> + string(4) "Jane" + ["age"]=> + int(18) + ["city"]=> + string(8) "Shanghai" + ["vip"]=> + bool(false) +} diff --git a/tests/aot/functions/named-variadic-native.phpt b/tests/aot/functions/named-variadic-native.phpt new file mode 100644 index 00000000..dd203026 --- /dev/null +++ b/tests/aot/functions/named-variadic-native.phpt @@ -0,0 +1,52 @@ +--TEST-- +Native variadic function calls accept named arguments +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + string(1) "A" + [1]=> + array(4) { + [0]=> + int(1) + [1]=> + int(2) + ["tail"]=> + int(3) + ["items"]=> + int(4) + } +} +array(2) { + [0]=> + string(1) "B" + [1]=> + array(1) { + ["extra"]=> + int(5) + } +} +array(2) { + ["one"]=> + int(1) + ["two"]=> + int(2) +}