From 7977893bbedcea68e6f5e0bc7e403047198537d6 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 11 Jul 2026 13:08:28 +0800 Subject: [PATCH] feat(compiler): add object extension method support with parameter offset handling - Add parameterOffset parameter to parseNativeCallArgs with validation logic - Implement named argument offset validation to prevent targeting extension receiver - Update variadic argument handling with offset calculations - Skip parameter validation for indices below offset threshold - Adjust function definition checks to account for parameter offset - Add object extension method lookup in findObjectExtensionMethod - Implement extension function resolution within specific namespaces only - Create genObjectExtensionFn to generate proper extension function calls - Add comprehensive test coverage for namespaced object extension methods - Support both snake_case and camelCase method naming conventions - Ensure extension methods receive correct parameter positioning with offset 1 --- src/CompilerBase.php | 32 +++++++-- src/UniversalMethodCall.php | 72 +++++++++++++++++-- .../universal_method/object_extension.phpt | 65 +++++++++++++++++ 3 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 tests/aot/universal_method/object_extension.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index cead8bd1..b97f0fda 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3722,7 +3722,7 @@ class CompilerBase implements PropertyAccessContext /** * @param array $callArgs */ - protected function parseNativeCallArgs(array $callArgs, string $nativeFunc): string + protected function parseNativeCallArgs(array $callArgs, string $nativeFunc, int $parameterOffset = 0): string { $argList = []; $functionDef = $this->getFunction($nativeFunc); @@ -3740,21 +3740,27 @@ class CompilerBase implements PropertyAccessContext $argName = $arg->name->name; $k = $argNameIndex[$argName] ?? null; if ($k !== null and ($variadicArgIndex === null or $k < $variadicArgIndex)) { + if ($k < $parameterOffset) { + $this->fatalError($arg, 'Named argument cannot target the extension receiver'); + } $args[$k] = $arg; } else { $variadicArgs[] = [$argName, $arg]; } $hasNamedArg = true; - } elseif ($variadicArgIndex !== null and $i >= $variadicArgIndex) { + } elseif ($variadicArgIndex !== null and $i + $parameterOffset >= $variadicArgIndex) { $variadicArgs[] = [null, $arg]; } else { - $args[$i] = $arg; + $args[$i + $parameterOffset] = $arg; } } // 对 key 进行排序,确保参数顺序正确 if ($hasNamedArg) { // 命名参数中间存在空洞,需要使用默认参数填充 foreach ($functionDef->argInfoList as $k => $argInfo) { + if ($k < $parameterOffset) { + continue; + } if ($variadicArgIndex !== null and $k === $variadicArgIndex) { continue; } @@ -3782,7 +3788,9 @@ class CompilerBase implements PropertyAccessContext } // 函数只接受一个变长参数,且调用参数为空,直接传入空数组 - if (count($args) === 0 and count($functionDef->argInfoList) === 1 and $functionDef->argInfoList[0]->variadic) { + if (count($args) === 0 + and count($functionDef->argInfoList) === $parameterOffset + 1 + and $functionDef->argInfoList[$parameterOffset]->variadic) { return '{}'; } @@ -6922,8 +6930,18 @@ class CompilerBase implements PropertyAccessContext } } } catch (DynamicCall) { + $extension = $this->findObjectExtensionMethod($class, $methodName); + if ($extension !== null) { + return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension); + } $magicMethod = true; } + if (!$nativeFunc) { + $extension = $this->findObjectExtensionMethod($class, $methodName); + if ($extension !== null) { + return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension); + } + } } // 表达式返回值也可使用内置方法:fn()->method(), $obj->fn()->method(), Foo::fn()->method(), $obj->prop->method() @@ -6948,6 +6966,12 @@ class CompilerBase implements PropertyAccessContext return $this->parseUniversalMethodCall($expr, $receiver, $methodName, $fn, false); } } + + $extensionClass = $this->detectClassOfExpr($expr->var); + $extension = $this->findObjectExtensionMethod($extensionClass, $methodName); + if ($extension !== null) { + return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension, false); + } } if ($this->isNamedMethod($expr->name)) { diff --git a/src/UniversalMethodCall.php b/src/UniversalMethodCall.php index 0d52dc70..c30c19ba 100644 --- a/src/UniversalMethodCall.php +++ b/src/UniversalMethodCall.php @@ -359,6 +359,21 @@ trait UniversalMethodCall ])); } + /** Resolve extension candidates in exactly one namespace. */ + protected function extensionFunctionDefinitions(string $prefix, string $method, string $namespace = ''): iterable + { + foreach ($this->extensionFunctionCandidates($prefix, $method) as $localName) { + $function = $this->getNativeName($localName, $namespace); + if (!$this->hasFunction($function)) { + continue; + } + $definition = $this->getFunction($function); + if ($definition->namespace === $namespace) { + yield $function => $definition; + } + } + } + protected const array TO_CONVERT_FN = [ CompilerBase::TYPE_BIGINT => ['toInt' => 'php::BigInt::toInt', 'toFloat' => 'php::BigInt::toFloat', 'toString' => 'php::BigInt::toString'], CompilerBase::TYPE_BIGFLOAT => ['toInt' => 'php::BigFloat::toInt', 'toFloat' => 'php::BigFloat::toFloat', 'toString' => 'php::BigFloat::toString'], @@ -397,6 +412,44 @@ trait UniversalMethodCall return $this->findExtensionMethod($type, $method); } + /** + * Look up an object extension in the object's own namespace. Functions use + * {Class}_{snake_case_method} or {Class}_{lowerCamelCaseMethod}, and their + * first parameter must be exactly the extended class. + */ + protected function findObjectExtensionMethod(string $class, string $method): ?array + { + $class = ltrim($class, '\\'); + if ($class === '' || !$this->hasClass($class)) { + return null; + } + $separator = strrpos($class, '\\'); + $namespace = $separator === false ? '' : substr($class, 0, $separator); + $shortClass = $separator === false ? $class : substr($class, $separator + 1); + + foreach ($this->extensionFunctionDefinitions($shortClass, $method, $namespace) as $function => $funcDef) { + if (empty($funcDef->argInfoList)) { + continue; + } + $receiver = $funcDef->argInfoList[0]; + if ($receiver->byRef + || $receiver->type !== CompilerBase::TYPE_OBJECT + || !$this->isSameClassName($receiver->declaredClass, $class)) { + continue; + } + + $totalParams = count($funcDef->argInfoList); + return [ + 'handler' => 'object_extension_fn', + 'fn' => $function, + 'return_type' => $funcDef->returnType, + 'min_args' => max(0, $funcDef->argCountRequired - 1), + 'max_args' => $funcDef->hasVariadicArg() ? -1 : $totalParams - 1, + ]; + } + return null; + } + /** * Look up an extension function for the given type+method. * Extension functions may use either {typePrefix}_{snake_case_method} @@ -417,6 +470,9 @@ trait UniversalMethodCall $resolvedName = $this->resolveExtensionFunctionName($funcName); if ($resolvedName !== null) { $funcDef = $this->getFunction($resolvedName); + if ($funcDef->namespace !== '') { + continue; + } if (!$this->validateExtensionFirstParam($type, $funcDef)) { continue; } @@ -447,13 +503,8 @@ trait UniversalMethodCall */ protected function findKeywordExtensionMethod(string $method): ?array { - foreach ($this->extensionFunctionCandidates('_', $method) as $funcName) { - if (!$this->hasFunction($funcName)) { - continue; - } - - $funcDef = $this->getFunction($funcName); - if ($funcDef->namespace !== '' || empty($funcDef->argInfoList)) { + foreach ($this->extensionFunctionDefinitions('_', $method) as $funcName => $funcDef) { + if (empty($funcDef->argInfoList)) { continue; } $firstParam = $funcDef->argInfoList[0]; @@ -602,10 +653,17 @@ trait UniversalMethodCall 'php_fn' => $this->genUniversalPhpFn($receiver, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0, $def['const_args'] ?? []), 'php_fn_ref' => $this->genUniversalPhpFnRef($receiver, $def['fn'], $expr->args, $def['return_type']), 'cpp_fn' => $this->genUniversalCppFn($receiver, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0), + 'object_extension_fn' => $this->genObjectExtensionFn($receiver, $def['fn'], $expr->args), default => null, }; } + protected function genObjectExtensionFn(string $receiver, string $nativeFunc, array $args): string + { + $tail = $this->parseNativeCallArgs($args, $nativeFunc, 1); + return self::PREFIX . $nativeFunc . '(' . $receiver . ($tail === '' ? '' : ', ' . $tail) . ')'; + } + /** * Generate a stream method call with null guard. * Stream resources are nullable — throws Error if the receiver is null. diff --git a/tests/aot/universal_method/object_extension.phpt b/tests/aot/universal_method/object_extension.phpt new file mode 100644 index 00000000..bd5fdd7a --- /dev/null +++ b/tests/aot/universal_method/object_extension.phpt @@ -0,0 +1,65 @@ +--TEST-- +Namespaced object extension methods use Class_method naming +--FILE-- +name . $suffix . ':snake'; + } + + function User_displayName(User $user): string + { + return strtoupper($user->name) . ':camel'; + } + + function User_format_name(int $invalid): string + { + return 'invalid'; + } + + function User_formatName(User $user): string + { + return '[' . $user->name . ']'; + } + + function User_existing(User $user): string + { + return 'extension'; + } + +} + +namespace { + function main(): void + { + $user = new \App\User('alice'); + var_dump($user->testMethod('!')); + var_dump($user->displayName()); + var_dump($user->formatName()); + var_dump($user->existing()); + var_dump((new \App\User('bob'))->displayName()); + } +} +?> +--EXPECT-- +string(12) "alice!:snake" +string(11) "ALICE:camel" +string(7) "[alice]" +string(11) "real method" +string(9) "BOB:camel"