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
pull/17/head
韩天峰 2 months ago
parent 7618d25c4e
commit 7977893bbe
  1. 32
      src/CompilerBase.php
  2. 72
      src/UniversalMethodCall.php
  3. 65
      tests/aot/universal_method/object_extension.phpt

@ -3722,7 +3722,7 @@ class CompilerBase implements PropertyAccessContext
/**
* @param array<Node\Arg|Node\VariadicPlaceholder> $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)) {

@ -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.

@ -0,0 +1,65 @@
--TEST--
Namespaced object extension methods use Class_method naming
--FILE--
<?php
namespace App {
use native_types;
class User
{
public function __construct(public string $name)
{
}
public function existing(): string
{
return 'real method';
}
}
function User_test_method(User $user, string $suffix): string
{
return $user->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"
Loading…
Cancel
Save